@astrojs/cloudflare 13.0.0-alpha.0 → 13.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.
@@ -20,7 +20,6 @@ const service = {
20
20
  return options.src;
21
21
  }
22
22
  const imageEndpoint = joinPaths(
23
- // @ts-expect-error Can't recognise import.meta.env
24
23
  import.meta.env.BASE_URL,
25
24
  "/cdn-cgi/image",
26
25
  resizingParams.join(","),
@@ -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,10 @@
1
+ import { transform } from "../utils/image-binding-transform.js";
2
+ const prerender = false;
3
+ const GET = async (ctx) => {
4
+ const { env } = await import("cloudflare:workers");
5
+ return transform(ctx.request.url, env.IMAGES, env.ASSETS);
6
+ };
7
+ export {
8
+ GET,
9
+ prerender
10
+ };
@@ -0,0 +1,11 @@
1
+ import type { CreatePreviewServer } from 'astro';
2
+ import { type ResolvedServerUrls } from 'vite';
3
+ declare const createPreviewServer: CreatePreviewServer;
4
+ /** Display server host and startup time */
5
+ export declare function serverStart({ startupTime, resolvedUrls, host, base, }: {
6
+ startupTime: number;
7
+ resolvedUrls: ResolvedServerUrls;
8
+ host: string | undefined;
9
+ base: string;
10
+ }): string;
11
+ export { createPreviewServer as default };
@@ -0,0 +1,137 @@
1
+ import {
2
+ preview
3
+ } from "vite";
4
+ import { fileURLToPath } from "node:url";
5
+ import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
6
+ import colors from "piccolore";
7
+ import { performance } from "node:perf_hooks";
8
+ import { hasWranglerConfig, defaultCloudflareConfig } from "../wrangler.js";
9
+ import { copyFileSync, existsSync } from "node:fs";
10
+ const createPreviewServer = async ({
11
+ logger,
12
+ base,
13
+ outDir,
14
+ headers,
15
+ port,
16
+ host,
17
+ createCodegenDir,
18
+ root
19
+ }) => {
20
+ const startServerTime = performance.now();
21
+ let previewServer;
22
+ let cfPluginConfig = { viteEnvironment: { name: "ssr" } };
23
+ if (!hasWranglerConfig(outDir)) {
24
+ cfPluginConfig.config = defaultCloudflareConfig();
25
+ const codegenDir = createCodegenDir();
26
+ const devVarsPath = new URL(".dev.vars", outDir);
27
+ const devVarsCodegenPath = new URL(".dev.vars", codegenDir);
28
+ if (existsSync(devVarsPath)) {
29
+ copyFileSync(devVarsPath, devVarsCodegenPath);
30
+ }
31
+ }
32
+ try {
33
+ previewServer = await preview({
34
+ configFile: false,
35
+ base,
36
+ appType: "mpa",
37
+ build: {
38
+ outDir: fileURLToPath(outDir)
39
+ },
40
+ root: fileURLToPath(root),
41
+ preview: {
42
+ host,
43
+ port,
44
+ headers,
45
+ open: false,
46
+ allowedHosts: []
47
+ },
48
+ plugins: [cfVitePlugin(cfPluginConfig)]
49
+ });
50
+ } catch (err) {
51
+ if (err instanceof Error) {
52
+ logger.error(err.stack || err.message);
53
+ }
54
+ throw err;
55
+ }
56
+ const customShortcuts = [
57
+ // Disable default Vite shortcuts that don't work well with Astro
58
+ { key: "r", description: "" },
59
+ { key: "u", description: "" },
60
+ { key: "c", description: "" },
61
+ { key: "s", description: "" }
62
+ ];
63
+ previewServer.bindCLIShortcuts({
64
+ customShortcuts
65
+ });
66
+ logger.info(
67
+ serverStart({
68
+ startupTime: performance.now() - startServerTime,
69
+ resolvedUrls: previewServer.resolvedUrls ?? { local: [], network: [] },
70
+ host,
71
+ base
72
+ })
73
+ );
74
+ function closed() {
75
+ return new Promise((resolve, reject) => {
76
+ previewServer.httpServer.addListener("close", resolve);
77
+ previewServer.httpServer.addListener("error", reject);
78
+ });
79
+ }
80
+ return {
81
+ host,
82
+ port,
83
+ closed,
84
+ server: previewServer.httpServer,
85
+ stop: previewServer.close.bind(previewServer)
86
+ };
87
+ };
88
+ function serverStart({
89
+ startupTime,
90
+ resolvedUrls,
91
+ host,
92
+ base
93
+ }) {
94
+ const version = "13.0.0-alpha.1";
95
+ const localPrefix = `${colors.dim("\u2503")} Local `;
96
+ const networkPrefix = `${colors.dim("\u2503")} Network `;
97
+ const emptyPrefix = " ".repeat(11);
98
+ const localUrlMessages = resolvedUrls.local.map((url, i) => {
99
+ return `${i === 0 ? localPrefix : emptyPrefix}${colors.cyan(new URL(url).origin + base)}`;
100
+ });
101
+ const networkUrlMessages = resolvedUrls.network.map((url, i) => {
102
+ return `${i === 0 ? networkPrefix : emptyPrefix}${colors.cyan(new URL(url).origin + base)}`;
103
+ });
104
+ if (networkUrlMessages.length === 0) {
105
+ const networkLogging = getNetworkLogging(host);
106
+ if (networkLogging === "host-to-expose") {
107
+ networkUrlMessages.push(`${networkPrefix}${colors.dim("use --host to expose")}`);
108
+ } else if (networkLogging === "visible") {
109
+ networkUrlMessages.push(`${networkPrefix}${colors.dim("unable to find network to expose")}`);
110
+ }
111
+ }
112
+ const messages = [
113
+ "",
114
+ `${colors.bgGreen(colors.bold(` astro `))} ${colors.green(`v${version}`)} ${colors.dim(`ready in`)} ${Math.round(
115
+ startupTime
116
+ )} ${colors.dim("ms")}`,
117
+ "",
118
+ ...localUrlMessages,
119
+ ...networkUrlMessages,
120
+ ""
121
+ ];
122
+ return messages.filter((msg) => typeof msg === "string").join("\n");
123
+ }
124
+ const LOCAL_IP_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1"]);
125
+ function getNetworkLogging(host) {
126
+ if (!host) {
127
+ return "host-to-expose";
128
+ } else if (typeof host === "string" && LOCAL_IP_HOSTS.has(host)) {
129
+ return "none";
130
+ } else {
131
+ return "visible";
132
+ }
133
+ }
134
+ export {
135
+ createPreviewServer as default,
136
+ serverStart
137
+ };
@@ -1,14 +1,5 @@
1
- import type { ExecutionContext, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
2
- import type { SSRManifest } from 'astro';
3
- type Env = {
4
- [key: string]: unknown;
5
- ASSETS: {
6
- fetch: (req: Request | string) => Promise<Response>;
7
- };
1
+ import { handle } from '../utils/handler.js';
2
+ declare const _default: {
3
+ fetch: typeof handle;
8
4
  };
9
- export declare function createExports(manifest: SSRManifest): {
10
- default: {
11
- fetch: (request: Parameters<ExportedHandlerFetchHandler>[0], env: Env, context: ExecutionContext) => Promise<Response>;
12
- };
13
- };
14
- export {};
5
+ export default _default;
@@ -1,12 +1,7 @@
1
- import { App } from "astro/app";
2
1
  import { handle } from "../utils/handler.js";
3
- function createExports(manifest) {
4
- const app = new App(manifest);
5
- const fetch = async (request, env, context) => {
6
- return await handle(manifest, app, request, env, context);
7
- };
8
- return { default: { fetch } };
9
- }
2
+ var server_default = {
3
+ fetch: handle
4
+ };
10
5
  export {
11
- createExports
6
+ server_default as default
12
7
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { AstroIntegration } from 'astro';
2
- import { type GetPlatformProxyOptions } from 'wrangler';
3
2
  import { type ImageService } from './utils/image-config.js';
4
3
  export type { Runtime } from './utils/handler.js';
5
4
  export type Options = {
@@ -29,13 +28,6 @@ export type Options = {
29
28
  }[];
30
29
  };
31
30
  };
32
- /**
33
- * Proxy configuration for the platform.
34
- */
35
- platformProxy?: GetPlatformProxyOptions & {
36
- /** Toggle the proxy. Default `undefined`, which equals to `true`. */
37
- enabled?: boolean;
38
- };
39
31
  /**
40
32
  * Allow bundling cloudflare worker specific file types as importable modules. Defaults to true.
41
33
  * When enabled, allows imports of '.wasm', '.bin', and '.txt' file types
@@ -70,6 +62,22 @@ export type Options = {
70
62
  *
71
63
  */
72
64
  sessionKVBindingName?: string;
65
+ /**
66
+ * When configured as `cloudflare-binding`, the Cloudflare Images binding will be used to transform images:
67
+ * - https://developers.cloudflare.com/images/transform-images/bindings/
68
+ *
69
+ * By default, this will use the "IMAGES" binding name, but this can be customised in your `wrangler.json`:
70
+ *
71
+ * ```json
72
+ * {
73
+ * "images": {
74
+ * "binding": "IMAGES" // <-- this should match `imagesBindingName`
75
+ * }
76
+ * }
77
+ * ```
78
+ *
79
+ */
80
+ imagesBindingName?: string;
73
81
  /**
74
82
  * This configuration option allows you to specify a custom entryPoint for your Cloudflare Worker.
75
83
  * The entry point is the file that will be executed when your Worker is invoked.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createReadStream } from "node:fs";
1
+ import { createReadStream, copyFileSync, existsSync, readFileSync } from "node:fs";
2
2
  import { appendFile, stat } from "node:fs/promises";
3
3
  import { createRequire } from "node:module";
4
4
  import { createInterface } from "node:readline/promises";
@@ -9,29 +9,16 @@ import {
9
9
  removeLeadingForwardSlash
10
10
  } from "@astrojs/internal-helpers/path";
11
11
  import { createRedirectsFromAstroRoutes, printAsRedirects } from "@astrojs/underscore-redirects";
12
- import { AstroError } from "astro/errors";
13
- import { defaultClientConditions } from "vite";
14
- import { getPlatformProxy } from "wrangler";
15
- import {
16
- cloudflareModuleLoader
17
- } from "./utils/cloudflare-module-loader.js";
18
- import { createGetEnv } from "./utils/env.js";
12
+ import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
13
+ import { cloudflareModuleLoader } from "./utils/cloudflare-module-loader.js";
19
14
  import { createRoutesFile, getParts } from "./utils/generate-routes-json.js";
20
15
  import { setImageConfig } from "./utils/image-config.js";
16
+ import { createConfigPlugin } from "./vite-plugin-config.js";
17
+ import { hasWranglerConfig, defaultCloudflareConfig } from "./wrangler.js";
18
+ import { parse } from "dotenv";
21
19
  function wrapWithSlashes(path) {
22
20
  return prependForwardSlash(appendForwardSlash(path));
23
21
  }
24
- function setProcessEnv(config, env) {
25
- const getEnv = createGetEnv(env);
26
- if (config.env?.schema) {
27
- for (const key of Object.keys(config.env.schema)) {
28
- const value = getEnv(key);
29
- if (value !== void 0) {
30
- process.env[key] = value;
31
- }
32
- }
33
- }
34
- }
35
22
  function createIntegration(args) {
36
23
  let _config;
37
24
  let finalBuildOutput;
@@ -49,9 +36,18 @@ function createIntegration(args) {
49
36
  updateConfig,
50
37
  logger,
51
38
  addWatchFile,
52
- addMiddleware
39
+ createCodegenDir
53
40
  }) => {
54
41
  let session = config.session;
42
+ if (args?.imageService === "cloudflare-binding") {
43
+ const bindingName = args?.imagesBindingName ?? "IMAGES";
44
+ logger.info(
45
+ `Enabling image processing with Cloudflare Images for production with the "${bindingName}" Images binding.`
46
+ );
47
+ logger.info(
48
+ `If you see the error "Invalid binding \`${bindingName}\`" in your build output, you need to add the binding to your wrangler config file.`
49
+ );
50
+ }
55
51
  if (!session?.driver) {
56
52
  logger.info(
57
53
  `Enabling sessions with Cloudflare KV with the "${SESSION_KV_BINDING_NAME}" KV binding.`
@@ -68,6 +64,16 @@ function createIntegration(args) {
68
64
  }
69
65
  };
70
66
  }
67
+ const cfPluginConfig = { viteEnvironment: { name: "ssr" } };
68
+ if (!hasWranglerConfig(config.root)) {
69
+ cfPluginConfig.config = defaultCloudflareConfig();
70
+ const codegenDir = createCodegenDir();
71
+ const devVarsPath = new URL(".dev.vars", config.root);
72
+ const devVarsCodegenPath = new URL(".dev.vars", codegenDir);
73
+ if (existsSync(devVarsPath)) {
74
+ copyFileSync(devVarsPath, devVarsCodegenPath);
75
+ }
76
+ }
71
77
  updateConfig({
72
78
  build: {
73
79
  client: new URL(`.${wrapWithSlashes(config.base)}`, config.outDir),
@@ -78,11 +84,12 @@ function createIntegration(args) {
78
84
  session,
79
85
  vite: {
80
86
  plugins: [
87
+ cfVitePlugin(cfPluginConfig),
81
88
  // https://developers.cloudflare.com/pages/functions/module-support/
82
89
  // Allows imports of '.wasm', '.bin', and '.txt' file types
83
90
  cloudflareModulePlugin,
84
91
  {
85
- name: "vite:cf-imports",
92
+ name: "@astrojs/cloudflare:cf-imports",
86
93
  enforce: "pre",
87
94
  resolveId(source) {
88
95
  if (source.startsWith("cloudflare:")) {
@@ -90,29 +97,72 @@ function createIntegration(args) {
90
97
  }
91
98
  return null;
92
99
  }
93
- }
100
+ },
101
+ {
102
+ name: "@astrojs/cloudflare:environment",
103
+ configEnvironment(environmentName, _options) {
104
+ const isServerEnvironment = ["ssr", "prerender"].includes(environmentName);
105
+ if (isServerEnvironment && _options.optimizeDeps?.noDiscovery === false) {
106
+ return {
107
+ optimizeDeps: {
108
+ include: [
109
+ "astro",
110
+ "astro/runtime/**",
111
+ "astro > html-escaper",
112
+ "astro > mrmime",
113
+ "astro > zod/v4",
114
+ "astro > zod/v4/core",
115
+ "astro > clsx",
116
+ "astro > cssesc",
117
+ "astro > cookie",
118
+ "astro > devalue",
119
+ "astro > @oslojs/encoding",
120
+ "astro > es-module-lexer",
121
+ "astro > unstorage",
122
+ "astro > neotraverse/modern"
123
+ ],
124
+ exclude: [
125
+ "unstorage/drivers/cloudflare-kv-binding",
126
+ "astro:toolbar:internal",
127
+ "virtual:astro:middleware"
128
+ ]
129
+ }
130
+ };
131
+ }
132
+ }
133
+ },
134
+ {
135
+ enforce: "post",
136
+ name: "@astrojs/cloudflare:cf-externals",
137
+ applyToEnvironment: (environment) => environment.name === "ssr",
138
+ config(conf) {
139
+ if (conf.ssr) {
140
+ conf.ssr.external = void 0;
141
+ conf.ssr.noExternal = true;
142
+ }
143
+ }
144
+ },
145
+ createConfigPlugin({
146
+ sessionKVBindingName: SESSION_KV_BINDING_NAME
147
+ })
94
148
  ]
95
149
  },
96
150
  image: setImageConfig(args?.imageService ?? "compile", config.image, command, logger)
97
151
  });
98
- if (args?.platformProxy?.configPath) {
99
- addWatchFile(new URL(args.platformProxy.configPath, config.root));
100
- } else {
101
- addWatchFile(new URL("./wrangler.toml", config.root));
102
- addWatchFile(new URL("./wrangler.json", config.root));
103
- addWatchFile(new URL("./wrangler.jsonc", config.root));
104
- }
105
- addMiddleware({
106
- entrypoint: "@astrojs/cloudflare/entrypoints/middleware.js",
107
- order: "pre"
108
- });
152
+ addWatchFile(new URL("./wrangler.toml", config.root));
153
+ addWatchFile(new URL("./wrangler.json", config.root));
154
+ addWatchFile(new URL("./wrangler.jsonc", config.root));
109
155
  },
110
156
  "astro:routes:resolved": ({ routes }) => {
111
157
  _routes = routes;
112
158
  },
113
- "astro:config:done": ({ setAdapter, config, buildOutput }) => {
159
+ "astro:config:done": ({ setAdapter, config, buildOutput, injectTypes, logger }) => {
114
160
  _config = config;
115
161
  finalBuildOutput = buildOutput;
162
+ injectTypes({
163
+ filename: "cloudflare.d.ts",
164
+ content: '/// <reference types="@astrojs/cloudflare/types.d.ts" />'
165
+ });
116
166
  let customWorkerEntryPoint;
117
167
  if (args?.workerEntryPoint && typeof args.workerEntryPoint.path === "string") {
118
168
  const require2 = createRequire(config.root);
@@ -130,6 +180,7 @@ function createIntegration(args) {
130
180
  edgeMiddleware: false,
131
181
  buildOutput: "server"
132
182
  },
183
+ previewEntrypoint: "@astrojs/cloudflare/entrypoints/preview",
133
184
  supportedAstroFeatures: {
134
185
  serverOutput: "stable",
135
186
  hybridOutput: "stable",
@@ -145,75 +196,40 @@ function createIntegration(args) {
145
196
  envGetSecret: "stable"
146
197
  }
147
198
  });
148
- },
149
- "astro:server:setup": async ({ server }) => {
150
- if ((args?.platformProxy?.enabled ?? true) === true) {
151
- const platformProxy = await getPlatformProxy(args?.platformProxy);
152
- server.httpServer?.on("close", async () => {
153
- await platformProxy.dispose();
154
- });
155
- setProcessEnv(_config, platformProxy.env);
156
- globalThis.__env__ ??= {};
157
- globalThis.__env__[SESSION_KV_BINDING_NAME] = platformProxy.env[SESSION_KV_BINDING_NAME];
158
- const clientLocalsSymbol = Symbol.for("astro.locals");
159
- server.middlewares.use(async function middleware(req, _res, next) {
160
- Reflect.set(req, clientLocalsSymbol, {
161
- runtime: {
162
- env: platformProxy.env,
163
- cf: platformProxy.cf,
164
- caches: platformProxy.caches,
165
- ctx: {
166
- waitUntil: (promise) => platformProxy.ctx.waitUntil(promise),
167
- // Currently not available: https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions
168
- passThroughOnException: () => {
169
- throw new AstroError(
170
- "`passThroughOnException` is currently not available in Cloudflare Pages. See https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions."
171
- );
172
- }
173
- }
174
- }
175
- });
176
- next();
177
- });
199
+ const devVarsPath = new URL(".dev.vars", config.root);
200
+ if (existsSync(devVarsPath)) {
201
+ try {
202
+ const data = readFileSync(devVarsPath, "utf-8");
203
+ const parsed = parse(data);
204
+ Object.assign(process.env, parsed);
205
+ } catch {
206
+ logger.error(
207
+ `Unable to parse .dev.vars, variables will not be available to your application.`
208
+ );
209
+ }
178
210
  }
179
211
  },
180
212
  "astro:build:setup": ({ vite, target }) => {
181
213
  if (target === "server") {
182
214
  vite.resolve ||= {};
183
215
  vite.resolve.alias ||= {};
184
- const aliases = [
185
- {
186
- find: "react-dom/server",
187
- replacement: "react-dom/server.browser"
188
- }
189
- ];
190
- if (Array.isArray(vite.resolve.alias)) {
191
- vite.resolve.alias = [...vite.resolve.alias, ...aliases];
192
- } else {
193
- for (const alias of aliases) {
194
- vite.resolve.alias[alias.find] = alias.replacement;
195
- }
196
- }
197
216
  vite.ssr ||= {};
198
- vite.ssr.resolve ||= {};
199
- vite.ssr.resolve.conditions ||= [...defaultClientConditions];
200
- vite.ssr.resolve.conditions.push("workerd", "worker");
201
- vite.ssr.target = "webworker";
202
217
  vite.ssr.noExternal = true;
203
218
  vite.build ||= {};
204
219
  vite.build.rollupOptions ||= {};
205
220
  vite.build.rollupOptions.output ||= {};
221
+ vite.build.rollupOptions.external = ["sharp"];
206
222
  vite.build.rollupOptions.output.banner ||= "globalThis.process ??= {}; globalThis.process.env ??= {};";
207
223
  vite.define = {
208
224
  "process.env": "process.env",
209
- // Allows the request handler to know what the binding name is
210
- "globalThis.__ASTRO_SESSION_BINDING_NAME": JSON.stringify(SESSION_KV_BINDING_NAME),
225
+ "globalThis.__ASTRO_IMAGES_BINDING_NAME": JSON.stringify(
226
+ args?.imagesBindingName ?? "IMAGES"
227
+ ),
211
228
  ...vite.define
212
229
  };
213
230
  }
214
231
  },
215
232
  "astro:build:done": async ({ pages, dir, logger, assets }) => {
216
- await cloudflareModulePlugin.afterBuildCompleted(_config);
217
233
  let redirectsExists = false;
218
234
  try {
219
235
  const redirectsStat = await stat(new URL("./_redirects", _config.outDir));
@@ -1,49 +1,5 @@
1
1
  import { isRemotePath } from "@astrojs/internal-helpers/path";
2
- function matchHostname(url, hostname, allowWildcard) {
3
- if (!hostname) {
4
- return true;
5
- }
6
- if (!allowWildcard || !hostname.startsWith("*")) {
7
- return hostname === url.hostname;
8
- }
9
- if (hostname.startsWith("**.")) {
10
- const slicedHostname = hostname.slice(2);
11
- return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
12
- }
13
- if (hostname.startsWith("*.")) {
14
- const slicedHostname = hostname.slice(1);
15
- const additionalSubdomains = url.hostname.replace(slicedHostname, "").split(".").filter(Boolean);
16
- return additionalSubdomains.length === 1;
17
- }
18
- return false;
19
- }
20
- function matchPort(url, port) {
21
- return !port || port === url.port;
22
- }
23
- function matchProtocol(url, protocol) {
24
- return !protocol || protocol === url.protocol.slice(0, -1);
25
- }
26
- function matchPathname(url, pathname, allowWildcard) {
27
- if (!pathname) {
28
- return true;
29
- }
30
- if (!allowWildcard || !pathname.endsWith("*")) {
31
- return pathname === url.pathname;
32
- }
33
- if (pathname.endsWith("/**")) {
34
- const slicedPathname = pathname.slice(0, -2);
35
- return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
36
- }
37
- if (pathname.endsWith("/*")) {
38
- const slicedPathname = pathname.slice(0, -1);
39
- const additionalPathChunks = url.pathname.replace(slicedPathname, "").split("/").filter(Boolean);
40
- return additionalPathChunks.length === 1;
41
- }
42
- return false;
43
- }
44
- function matchPattern(url, remotePattern) {
45
- return matchProtocol(url, remotePattern.protocol) && matchHostname(url, remotePattern.hostname, true) && matchPort(url, remotePattern.port) && matchPathname(url, remotePattern.pathname, true);
46
- }
2
+ import { matchHostname, matchPattern } from "@astrojs/internal-helpers/remote";
47
3
  function isRemoteAllowed(src, {
48
4
  domains = [],
49
5
  remotePatterns = []
@@ -1,8 +1,4 @@
1
- import type { AstroConfig } from 'astro';
2
1
  import type { PluginOption } from 'vite';
3
- export interface CloudflareModulePluginExtra {
4
- afterBuildCompleted(config: AstroConfig): Promise<void>;
5
- }
6
2
  /**
7
3
  * Enables support for various non-standard extensions in module imports that cloudflare workers supports.
8
4
  *
@@ -17,4 +13,4 @@ export interface CloudflareModulePluginExtra {
17
13
  * @param enabled - if true, will load all cloudflare pages supported types
18
14
  * @returns Vite plugin with additional extension method to hook into astro build
19
15
  */
20
- export declare function cloudflareModuleLoader(enabled: boolean): PluginOption & CloudflareModulePluginExtra;
16
+ export declare function cloudflareModuleLoader(enabled: boolean): PluginOption;
@@ -1,6 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import * as url from "node:url";
4
3
  function cloudflareModuleLoader(enabled) {
5
4
  const adaptersByExtension = enabled ? { ...defaultAdapters } : {};
6
5
  const extensions = Object.keys(adaptersByExtension);
@@ -10,6 +9,9 @@ function cloudflareModuleLoader(enabled) {
10
9
  return {
11
10
  name: "vite:cf-module-loader",
12
11
  enforce: "pre",
12
+ applyToEnvironment(environment) {
13
+ return environment.name === "ssr" || environment.name === "client";
14
+ },
13
15
  configResolved(config) {
14
16
  isDev = config.command === "serve";
15
17
  },
@@ -20,7 +22,7 @@ function cloudflareModuleLoader(enabled) {
20
22
  rollupOptions: {
21
23
  // mark the wasm files as external so that they are not bundled and instead are loaded from the files
22
24
  external: extensions.map(
23
- (x) => new RegExp(`^${MAGIC_STRING}.+${escapeRegExp(x)}.mjs$`, "i")
25
+ (x) => new RegExp(`^${MAGIC_STRING}.+${escapeRegExp(x)}$`, "i")
24
26
  )
25
27
  }
26
28
  }
@@ -50,18 +52,18 @@ function cloudflareModuleLoader(enabled) {
50
52
  const assetName = `${path.basename(filePath).split(".")[0]}.${hash}${extension}`;
51
53
  this.emitFile({
52
54
  type: "asset",
53
- // emit the data explicitly as an esset with `fileName` rather than `name` so that
55
+ // emit the data explicitly as an asset with `fileName` rather than `name` so that
54
56
  // vite doesn't give it a random hash-id in its name--We need to be able to easily rewrite from
55
- // the .mjs loader and the actual wasm asset later in the ESbuild for the worker
57
+ // the loader and the actual asset later in the build for the worker
56
58
  fileName: assetName,
57
59
  source: data
58
60
  });
59
61
  const chunkId = this.emitFile({
60
62
  type: "prebuilt-chunk",
61
- fileName: `${assetName}.mjs`,
63
+ fileName: assetName,
62
64
  code: inlineModule
63
65
  });
64
- return `import module from "${MAGIC_STRING}${chunkId}${extension}.mjs";export default module;`;
66
+ return `import module from "${MAGIC_STRING}${chunkId}${extension}";export default module;`;
65
67
  },
66
68
  // output original wasm file relative to the chunk now that chunking has been achieved
67
69
  renderChunk(code, chunk, _) {
@@ -71,13 +73,13 @@ function cloudflareModuleLoader(enabled) {
71
73
  for (const ext of extensions) {
72
74
  const extension = ext.replace(/\?\w+$/, "");
73
75
  replaced = replaced.replaceAll(
74
- new RegExp(`${MAGIC_STRING}([^\\s]+?)${escapeRegExp(extension)}\\.mjs`, "g"),
76
+ new RegExp(`${MAGIC_STRING}([^\\s]+?)${escapeRegExp(extension)}`, "g"),
75
77
  (_s, assetId) => {
76
78
  const fileName = this.getFileName(assetId);
77
79
  const relativePath = path.relative(path.dirname(chunk.fileName), fileName).replaceAll("\\", "/");
78
80
  replacements.push({
79
81
  chunkName: chunk.name,
80
- cloudflareImport: relativePath.replace(/\.mjs$/, ""),
82
+ cloudflareImport: relativePath,
81
83
  nodejsImport: relativePath
82
84
  });
83
85
  return `./${relativePath}`;
@@ -104,34 +106,6 @@ function cloudflareModuleLoader(enabled) {
104
106
  replacement.fileName.push(chunk.fileName);
105
107
  }
106
108
  }
107
- },
108
- /**
109
- * Once prerendering is complete, restore the imports in the _worker.js to cloudflare compatible ones, removing the .mjs suffix.
110
- */
111
- async afterBuildCompleted(config) {
112
- const baseDir = url.fileURLToPath(config.outDir);
113
- const replacementsByFileName = /* @__PURE__ */ new Map();
114
- for (const replacement of replacements) {
115
- if (!replacement.fileName) {
116
- continue;
117
- }
118
- for (const fileName of replacement.fileName) {
119
- const repls = replacementsByFileName.get(fileName) || [];
120
- if (!repls.length) {
121
- replacementsByFileName.set(fileName, repls);
122
- }
123
- repls.push(replacement);
124
- }
125
- }
126
- for (const [fileName, repls] of replacementsByFileName.entries()) {
127
- const filepath = path.join(baseDir, "_worker.js", fileName);
128
- const contents = await fs.readFile(filepath, "utf-8");
129
- let updated = contents;
130
- for (const replacement of repls) {
131
- updated = updated.replaceAll(replacement.nodejsImport, replacement.cloudflareImport);
132
- }
133
- await fs.writeFile(filepath, updated, "utf-8");
134
- }
135
109
  }
136
110
  };
137
111
  }
@@ -1,23 +1,14 @@
1
- import type { CacheStorage as CloudflareCacheStorage, ExecutionContext, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
2
- import type { SSRManifest } from 'astro';
3
- import type { App } from 'astro/app';
4
- type Env = {
1
+ import type { Response as CfResponse, ExecutionContext, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
2
+ export type Env = {
5
3
  [key: string]: unknown;
6
4
  ASSETS: {
7
- fetch: (req: Request | string) => Promise<Response>;
5
+ fetch: (req: Request | string) => Promise<CfResponse>;
8
6
  };
9
7
  };
10
- export interface Runtime<T extends object = object> {
11
- runtime: {
12
- env: Env & T;
13
- cf: Parameters<ExportedHandlerFetchHandler>[0]['cf'];
14
- caches: CloudflareCacheStorage;
15
- ctx: ExecutionContext;
16
- };
8
+ export interface Runtime {
9
+ cfContext: ExecutionContext;
17
10
  }
18
11
  declare global {
19
- var __ASTRO_SESSION_BINDING_NAME: string;
20
- var __env__: Partial<Env>;
12
+ var __ASTRO_IMAGES_BINDING_NAME: string;
21
13
  }
22
- export declare function handle(manifest: SSRManifest, app: App, request: Parameters<ExportedHandlerFetchHandler>[0], env: Env, context: ExecutionContext): Promise<Response>;
23
- export {};
14
+ export declare function handle(request: Parameters<ExportedHandlerFetchHandler>[0], env: Env, context: ExecutionContext): Promise<CfResponse>;
@@ -1,13 +1,19 @@
1
1
  import { env as globalEnv } from "cloudflare:workers";
2
+ import { sessionKVBindingName } from "virtual:astro-cloudflare:config";
3
+ import { createApp } from "astro/app/entrypoint";
2
4
  import { setGetEnv } from "astro/env/setup";
3
5
  import { createGetEnv } from "../utils/env.js";
4
6
  setGetEnv(createGetEnv(globalEnv));
5
- async function handle(manifest, app, request, env, context) {
7
+ async function handle(request, env, context) {
8
+ const app = createApp(import.meta.env.DEV);
6
9
  const { pathname } = new URL(request.url);
7
- const bindingName = globalThis.__ASTRO_SESSION_BINDING_NAME;
8
- globalThis.__env__ ??= {};
9
- globalThis.__env__[bindingName] = env[bindingName];
10
- if (manifest.assets.has(pathname)) {
10
+ if (env[sessionKVBindingName]) {
11
+ const sessionConfigOptions = app.manifest.sessionConfig?.options ?? {};
12
+ Object.assign(sessionConfigOptions, {
13
+ binding: env[sessionKVBindingName]
14
+ });
15
+ }
16
+ if (app.manifest.assets.has(pathname)) {
11
17
  return env.ASSETS.fetch(request.url.replace(/\.html$/, ""));
12
18
  }
13
19
  const routeData = app.match(request);
@@ -20,21 +26,7 @@ async function handle(manifest, app, request, env, context) {
20
26
  }
21
27
  }
22
28
  const locals = {
23
- runtime: {
24
- env,
25
- cf: request.cf,
26
- caches,
27
- ctx: {
28
- waitUntil: (promise) => context.waitUntil(promise),
29
- // Currently not available: https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions
30
- passThroughOnException: () => {
31
- throw new Error(
32
- "`passThroughOnException` is currently not available in Cloudflare Pages. See https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions."
33
- );
34
- },
35
- props: {}
36
- }
37
- }
29
+ cfContext: context
38
30
  };
39
31
  const response = await app.render(
40
32
  request,
@@ -0,0 +1,2 @@
1
+ import type { Fetcher, ImagesBinding } from '@cloudflare/workers-types';
2
+ export declare function transform(rawUrl: string, images: ImagesBinding, assets: Fetcher): Promise<import("@cloudflare/workers-types").Response | Response>;
@@ -0,0 +1,30 @@
1
+ import { imageConfig } from "astro:assets";
2
+ import { isRemotePath } from "@astrojs/internal-helpers/path";
3
+ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
4
+ async function transform(rawUrl, images, assets) {
5
+ const url = new URL(rawUrl);
6
+ const href = url.searchParams.get("href");
7
+ if (!href || isRemotePath(href) && !isRemoteAllowed(href, imageConfig)) {
8
+ return new Response("Forbidden", { status: 403 });
9
+ }
10
+ const imageSrc = new URL(href, url.origin);
11
+ const content = await (isRemotePath(href) ? fetch(imageSrc) : assets.fetch(imageSrc));
12
+ if (!content.body) {
13
+ return new Response(null, { status: 404 });
14
+ }
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 });
19
+ }
20
+ 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,
23
+ // `quality` is documented, but doesn't appear to work in manual testing...
24
+ // quality: url.searchParams.get('q'),
25
+ fit: url.searchParams.get("fit")
26
+ }).output({ format: `image/${format}` })).response();
27
+ }
28
+ export {
29
+ transform
30
+ };
@@ -1,20 +1,20 @@
1
1
  import type { AstroConfig, AstroIntegrationLogger, HookParameters } from 'astro';
2
- export type ImageService = 'passthrough' | 'cloudflare' | 'compile' | 'custom';
2
+ export type ImageService = 'passthrough' | 'cloudflare' | 'cloudflare-binding' | 'compile' | 'custom';
3
3
  export declare function setImageConfig(service: ImageService, config: AstroConfig['image'], command: HookParameters<'astro:config:setup'>['command'], logger: AstroIntegrationLogger): {
4
4
  service: import("astro").ImageServiceConfig<Record<string, any>>;
5
- responsiveStyles: boolean;
6
5
  endpoint: {
7
6
  route: string;
8
7
  entrypoint?: string | undefined;
9
8
  };
10
9
  domains: string[];
11
10
  remotePatterns: {
12
- port?: string | undefined;
13
11
  protocol?: string | undefined;
14
12
  hostname?: string | undefined;
13
+ port?: string | undefined;
15
14
  pathname?: string | undefined;
16
15
  }[];
17
- layout?: "fixed" | "constrained" | "full-width" | "none" | undefined;
16
+ responsiveStyles: boolean;
17
+ layout?: "none" | "fixed" | "constrained" | "full-width" | undefined;
18
18
  objectFit?: string | undefined;
19
19
  objectPosition?: string | undefined;
20
20
  breakpoints?: number[] | undefined;
@@ -23,15 +23,15 @@ export declare function setImageConfig(service: ImageService, config: AstroConfi
23
23
  endpoint: {
24
24
  entrypoint: string | undefined;
25
25
  };
26
- responsiveStyles: boolean;
27
26
  domains: string[];
28
27
  remotePatterns: {
29
- port?: string | undefined;
30
28
  protocol?: string | undefined;
31
29
  hostname?: string | undefined;
30
+ port?: string | undefined;
32
31
  pathname?: string | undefined;
33
32
  }[];
34
- layout?: "fixed" | "constrained" | "full-width" | "none" | undefined;
33
+ responsiveStyles: boolean;
34
+ layout?: "none" | "fixed" | "constrained" | "full-width" | undefined;
35
35
  objectFit?: string | undefined;
36
36
  objectPosition?: string | undefined;
37
37
  breakpoints?: number[] | undefined;
@@ -8,6 +8,13 @@ function setImageConfig(service, config, command, logger) {
8
8
  ...config,
9
9
  service: command === "dev" ? sharpImageService() : { entrypoint: "@astrojs/cloudflare/image-service" }
10
10
  };
11
+ case "cloudflare-binding":
12
+ return {
13
+ ...config,
14
+ endpoint: {
15
+ entrypoint: "@astrojs/cloudflare/image-transform-endpoint"
16
+ }
17
+ };
11
18
  case "compile":
12
19
  return {
13
20
  ...config,
@@ -0,0 +1,6 @@
1
+ import type { PluginOption } from 'vite';
2
+ interface CloudflareConfig {
3
+ sessionKVBindingName: string;
4
+ }
5
+ export declare function createConfigPlugin(config: CloudflareConfig): PluginOption;
6
+ export {};
@@ -0,0 +1,20 @@
1
+ const VIRTUAL_CONFIG_ID = "virtual:astro-cloudflare:config";
2
+ const RESOLVED_VIRTUAL_CONFIG_ID = "\0" + VIRTUAL_CONFIG_ID;
3
+ function createConfigPlugin(config) {
4
+ return {
5
+ name: "vite:astro-cloudflare-config",
6
+ resolveId(id) {
7
+ if (id === VIRTUAL_CONFIG_ID) {
8
+ return RESOLVED_VIRTUAL_CONFIG_ID;
9
+ }
10
+ },
11
+ load(id) {
12
+ if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
13
+ return `export const sessionKVBindingName = ${JSON.stringify(config.sessionKVBindingName)};`;
14
+ }
15
+ }
16
+ };
17
+ }
18
+ export {
19
+ createConfigPlugin
20
+ };
@@ -0,0 +1,10 @@
1
+ import type { PluginConfig } from '@cloudflare/vite-plugin';
2
+ /**
3
+ * Checks whether a wrangler file exists at the given path
4
+ * @param root
5
+ */
6
+ export declare function hasWranglerConfig(root: URL): boolean;
7
+ /**
8
+ * Returns the default wrangler configuration used by Astro Cloudflare configuration.
9
+ */
10
+ export declare function defaultCloudflareConfig(): PluginConfig['config'];
@@ -0,0 +1,29 @@
1
+ import { existsSync } from "node:fs";
2
+ function hasWranglerConfig(root) {
3
+ return existsSync(new URL("wrangler.jsonc", root)) || existsSync(new URL("wrangler.toml", root)) || existsSync(new URL("wrangler.json", root));
4
+ }
5
+ function defaultCloudflareConfig() {
6
+ return {
7
+ // TODO: better way to handle name, maybe package.json#name ?
8
+ name: "test-application",
9
+ compatibility_date: "2025-05-21",
10
+ main: "@astrojs/cloudflare/entrypoints/server",
11
+ assets: {
12
+ directory: "./dist",
13
+ binding: "ASSETS"
14
+ },
15
+ images: {
16
+ binding: "IMAGES"
17
+ },
18
+ kv_namespaces: [
19
+ {
20
+ binding: "SESSION",
21
+ id: "SESSION"
22
+ }
23
+ ]
24
+ };
25
+ }
26
+ export {
27
+ defaultCloudflareConfig,
28
+ hasWranglerConfig
29
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to Cloudflare Workers/Pages",
4
- "version": "13.0.0-alpha.0",
4
+ "version": "13.0.0-alpha.1",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -19,22 +19,29 @@
19
19
  "homepage": "https://docs.astro.build/en/guides/integrations-guide/cloudflare/",
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
+ "./entrypoints/server": "./dist/entrypoints/server.js",
23
+ "./entrypoints/preview": "./dist/entrypoints/preview.js",
22
24
  "./entrypoints/server.js": "./dist/entrypoints/server.js",
23
- "./entrypoints/middleware.js": "./dist/entrypoints/middleware.js",
24
25
  "./image-service": "./dist/entrypoints/image-service.js",
25
26
  "./image-endpoint": "./dist/entrypoints/image-endpoint.js",
27
+ "./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js",
26
28
  "./handler": "./dist/utils/handler.js",
29
+ "./types.d.ts": "./types.d.ts",
27
30
  "./package.json": "./package.json"
28
31
  },
29
32
  "files": [
30
- "dist"
33
+ "dist",
34
+ "types.d.ts"
31
35
  ],
32
36
  "dependencies": {
33
- "@cloudflare/workers-types": "^4.20251106.1",
37
+ "@cloudflare/vite-plugin": "^1.17.0",
38
+ "@cloudflare/workers-types": "^4.20251121.0",
39
+ "dotenv": "^17.2.3",
40
+ "piccolore": "^0.1.3",
34
41
  "tinyglobby": "^0.2.15",
35
- "vite": "^7.1.7",
36
- "wrangler": "4.46.0",
37
- "@astrojs/internal-helpers": "0.7.4",
42
+ "vite": "^7.1.12",
43
+ "wrangler": "4.53.0",
44
+ "@astrojs/internal-helpers": "0.7.5",
38
45
  "@astrojs/underscore-redirects": "1.0.0"
39
46
  },
40
47
  "peerDependencies": {
@@ -42,9 +49,9 @@
42
49
  },
43
50
  "devDependencies": {
44
51
  "cheerio": "1.1.2",
45
- "devalue": "^5.4.2",
46
- "rollup": "^4.52.5",
47
- "astro": "6.0.0-alpha.0",
52
+ "devalue": "^5.5.0",
53
+ "rollup": "^4.53.3",
54
+ "astro": "6.0.0-alpha.1",
48
55
  "astro-scripts": "0.0.14"
49
56
  },
50
57
  "publishConfig": {
package/types.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cloudflare Worker Request types
3
+ * Extends the global Request object with Cloudflare-specific properties
4
+ */
5
+
6
+ import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
7
+
8
+ declare global {
9
+ interface Request {
10
+ /**
11
+ * Cloudflare-specific properties available on incoming requests
12
+ * Contains metadata about the request such as:
13
+ * - Geographic information (country, colo, timezone)
14
+ * - TLS/Security details (cipher, protocol version)
15
+ * - Bot Management scores
16
+ * - Client information (ASN, TCP metrics)
17
+ */
18
+ readonly cf?: IncomingRequestCfProperties;
19
+ }
20
+ }
@@ -1,2 +0,0 @@
1
- import type { MiddlewareHandler } from 'astro';
2
- export declare const onRequest: MiddlewareHandler;
@@ -1,11 +0,0 @@
1
- const onRequest = (context, next) => {
2
- if (context.isPrerendered) {
3
- context.locals.runtime ??= {
4
- env: process.env
5
- };
6
- }
7
- return next();
8
- };
9
- export {
10
- onRequest
11
- };