diff --git a/.changeset/open-monkeys-boil.md b/.changeset/open-monkeys-boil.md index 315cd9bd2e7e..7c52829ad18a 100644 --- a/.changeset/open-monkeys-boil.md +++ b/.changeset/open-monkeys-boil.md @@ -5,3 +5,25 @@ Development server now runs in workerd `astro dev` now runs your Cloudflare application using Cloudflare's workerd runtime instead of Node.js. This means your development environment is now a near-exact replica of your production environment—the same JavaScript engine, the same APIs, the same behavior. You'll catch issues during development that would have only appeared in production, and features like Durable Objects, Workers Analytics Engine, and R2 bindings work exactly as they do on Cloudflare's platform. + +**Breaking Changes:** + +- `Astro.locals.runtime` no longer contains the `env` object. Instead, import it directly: + ```js + import { env } from 'cloudflare:workers'; + ``` + +- `Astro.locals.runtime` no longer contains the `cf` object. Instead, access it directly from the request: + ```js + Astro.request.cf + ``` + +- `Astro.locals.runtime` no longer contains the `caches` object. Instead, use the global `caches` object directly: + ```js + caches.default.put(request, response) + ``` + +- `Astro.locals.runtime` object is replaced with `Astro.locals.cfContext` which contains the Cloudflare `ExecutionContext`: + ```js + const cfContext = Astro.locals.cfContext; + ``` diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index d0783b6eca7b..0eea946ff782 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -22,15 +22,16 @@ "./entrypoints/server": "./dist/entrypoints/server.js", "./entrypoints/preview": "./dist/entrypoints/preview.js", "./entrypoints/server.js": "./dist/entrypoints/server.js", - "./entrypoints/middleware.js": "./dist/entrypoints/middleware.js", "./image-service": "./dist/entrypoints/image-service.js", "./image-endpoint": "./dist/entrypoints/image-endpoint.js", "./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js", "./handler": "./dist/utils/handler.js", + "./types.d.ts": "./types.d.ts", "./package.json": "./package.json" }, "files": [ - "dist" + "dist", + "types.d.ts" ], "scripts": { "dev": "astro-scripts dev \"src/**/*.ts\"", diff --git a/packages/integrations/cloudflare/src/entrypoints/image-transform-endpoint.ts b/packages/integrations/cloudflare/src/entrypoints/image-transform-endpoint.ts index b8be3e847693..816c6e058bcb 100644 --- a/packages/integrations/cloudflare/src/entrypoints/image-transform-endpoint.ts +++ b/packages/integrations/cloudflare/src/entrypoints/image-transform-endpoint.ts @@ -5,6 +5,7 @@ export const prerender = false; // @ts-expect-error The Header types between libdom and @cloudflare/workers-types are causing issues export const GET: APIRoute = async (ctx) => { + const { env } = await import('cloudflare:workers'); // @ts-expect-error The runtime locals types are not populated here - return transform(ctx.request.url, ctx.locals.runtime.env.IMAGES, ctx.locals.runtime.env.ASSETS); + return transform(ctx.request.url, env.IMAGES, env.ASSETS); }; diff --git a/packages/integrations/cloudflare/src/entrypoints/middleware.ts b/packages/integrations/cloudflare/src/entrypoints/middleware.ts deleted file mode 100644 index 187aface592b..000000000000 --- a/packages/integrations/cloudflare/src/entrypoints/middleware.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { MiddlewareHandler } from 'astro'; - -export const onRequest: MiddlewareHandler = (context, next) => { - if (context.isPrerendered) { - // @ts-expect-error - context.locals.runtime ??= { - env: process.env, - }; - } - - return next(); -}; diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index ee0279223c49..0d1edbfccc6b 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -2,7 +2,7 @@ import { createReadStream, writeFileSync } from 'node:fs'; import { appendFile, stat } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { createInterface } from 'node:readline/promises'; -import { pathToFileURL } from 'node:url'; +import { pathToFileURL, fileURLToPath } from 'node:url'; import { appendForwardSlash, prependForwardSlash, @@ -16,12 +16,9 @@ import type { HookParameters, IntegrationResolvedRoute, } from 'astro'; -import { AstroError } from 'astro/errors'; import type { PluginOption } from 'vite'; import { defaultClientConditions } from 'vite'; -import { type GetPlatformProxyOptions, getPlatformProxy } from 'wrangler'; import { cloudflareModuleLoader } from './utils/cloudflare-module-loader.js'; -import { createGetEnv } from './utils/env.js'; import { createRoutesFile, getParts } from './utils/generate-routes-json.js'; import { type ImageService, setImageConfig } from './utils/image-config.js'; import { createConfigPlugin } from './vite-plugin-config.js'; @@ -56,13 +53,6 @@ export type Options = { }[]; }; }; - /** - * Proxy configuration for the platform. - */ - platformProxy?: GetPlatformProxyOptions & { - /** Toggle the proxy. Default `undefined`, which equals to `true`. */ - enabled?: boolean; - }; /** * Allow bundling cloudflare worker specific file types as importable modules. Defaults to true. @@ -143,19 +133,6 @@ function wrapWithSlashes(path: string): string { return prependForwardSlash(appendForwardSlash(path)); } -function setProcessEnv(config: AstroConfig, env: Record) { - const getEnv = createGetEnv(env); - - if (config.env?.schema) { - for (const key of Object.keys(config.env.schema)) { - const value = getEnv(key); - if (value !== undefined) { - process.env[key] = value; - } - } - } -} - export default function createIntegration(args?: Options): AstroIntegration { let _config: AstroConfig; let finalBuildOutput: HookParameters<'astro:config:done'>['buildOutput']; @@ -177,7 +154,6 @@ export default function createIntegration(args?: Options): AstroIntegration { updateConfig, logger, addWatchFile, - addMiddleware, createCodegenDir, }) => { let session = config.session; @@ -215,7 +191,7 @@ export default function createIntegration(args?: Options): AstroIntegration { const codegenDir = createCodegenDir(); const cachedFile = new URL('wrangler.json', codegenDir); writeFileSync(cachedFile, wranglerTemplate(), 'utf-8'); - cfPluginConfig.configPath = cachedFile.pathname; + cfPluginConfig.configPath = fileURLToPath(cachedFile); } updateConfig({ @@ -227,6 +203,13 @@ export default function createIntegration(args?: Options): AstroIntegration { }, session, vite: { + ssr: { + optimizeDeps: { + // Disabled to prevent "prebundle" errors on first dev + // This can be removed when the issue is resolved with Cloudflare + noDiscovery: true, + }, + }, plugins: [ cfVitePlugin(cfPluginConfig), // https://developers.cloudflare.com/pages/functions/module-support/ @@ -262,26 +245,23 @@ export default function createIntegration(args?: Options): AstroIntegration { image: setImageConfig(args?.imageService ?? 'compile', config.image, command, logger), }); - if (args?.platformProxy?.configPath) { - addWatchFile(new URL(args.platformProxy.configPath, config.root)); - } else { - addWatchFile(new URL('./wrangler.toml', config.root)); - addWatchFile(new URL('./wrangler.json', config.root)); - addWatchFile(new URL('./wrangler.jsonc', config.root)); - } + addWatchFile(new URL('./wrangler.toml', config.root)); + addWatchFile(new URL('./wrangler.json', config.root)); + addWatchFile(new URL('./wrangler.jsonc', config.root)); - addMiddleware({ - entrypoint: '@astrojs/cloudflare/entrypoints/middleware.js', - order: 'pre', - }); }, 'astro:routes:resolved': ({ routes }) => { _routes = routes; }, - 'astro:config:done': ({ setAdapter, config, buildOutput }) => { + 'astro:config:done': ({ setAdapter, config, buildOutput, injectTypes }) => { _config = config; finalBuildOutput = buildOutput; + injectTypes({ + filename: 'cloudflare.d.ts', + content: '/// ', + }); + let customWorkerEntryPoint: URL | undefined; if (args?.workerEntryPoint && typeof args.workerEntryPoint.path === 'string') { const require = createRequire(config.root); @@ -318,40 +298,6 @@ export default function createIntegration(args?: Options): AstroIntegration { }, }); }, - 'astro:server:setup': async ({ server }) => { - if ((args?.platformProxy?.enabled ?? true) === true) { - const platformProxy = await getPlatformProxy(args?.platformProxy); - - // Ensures the dev server doesn't hang - server.httpServer?.on('close', async () => { - await platformProxy.dispose(); - }); - - setProcessEnv(_config, platformProxy.env); - - const clientLocalsSymbol = Symbol.for('astro.locals'); - - server.middlewares.use(async function middleware(req, _res, next) { - Reflect.set(req, clientLocalsSymbol, { - runtime: { - env: platformProxy.env, - cf: platformProxy.cf, - caches: platformProxy.caches, - ctx: { - waitUntil: (promise: Promise) => platformProxy.ctx.waitUntil(promise), - // Currently not available: https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions - passThroughOnException: () => { - throw new AstroError( - '`passThroughOnException` is currently not available in Cloudflare Pages. See https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions.', - ); - }, - }, - }, - }); - next(); - }); - } - }, 'astro:build:setup': ({ vite, target }) => { if (target === 'server') { vite.resolve ||= {}; diff --git a/packages/integrations/cloudflare/src/utils/handler.ts b/packages/integrations/cloudflare/src/utils/handler.ts index ed86826fd962..b82e7310c11b 100644 --- a/packages/integrations/cloudflare/src/utils/handler.ts +++ b/packages/integrations/cloudflare/src/utils/handler.ts @@ -1,9 +1,7 @@ -// @ts-expect-error - It is safe to expect the error here. import { env as globalEnv } from 'cloudflare:workers'; import { sessionKVBindingName } from 'virtual:astro-cloudflare:config'; import type { Response as CfResponse, - CacheStorage as CloudflareCacheStorage, ExecutionContext, ExportedHandlerFetchHandler, } from '@cloudflare/workers-types'; @@ -18,13 +16,8 @@ export type Env = { setGetEnv(createGetEnv(globalEnv as Env)); -export interface Runtime { - runtime: { - env: Env & T; - cf: Parameters[0]['cf']; - caches: CloudflareCacheStorage; - ctx: ExecutionContext; - }; +export interface Runtime { + cfContext: ExecutionContext; } declare global { @@ -64,21 +57,7 @@ export async function handle( } const locals: Runtime = { - runtime: { - env: env, - cf: request.cf, - caches: caches as unknown as CloudflareCacheStorage, - ctx: { - waitUntil: (promise: Promise) => context.waitUntil(promise), - // Currently not available: https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions - passThroughOnException: () => { - throw new Error( - '`passThroughOnException` is currently not available in Cloudflare Pages. See https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions.', - ); - }, - props: {}, - }, - }, + cfContext: context, }; const response = await app.render( diff --git a/packages/integrations/cloudflare/test/astro-dev-platform.test.js b/packages/integrations/cloudflare/test/astro-dev-platform.test.js index 46951117179e..02aaf09ebe21 100644 --- a/packages/integrations/cloudflare/test/astro-dev-platform.test.js +++ b/packages/integrations/cloudflare/test/astro-dev-platform.test.js @@ -12,19 +12,14 @@ describe('AstroDevPlatform', () => { logLevel: 'debug', }); devServer = await fixture.startDevServer(); + // Do an initial request to prime preloading + await fixture.fetch('/'); }); after(async () => { await devServer.stop(); }); - it('exists', async () => { - const res = await fixture.fetch('/'); - const html = await res.text(); - const $ = cheerio.load(html); - assert.equal($('#hasRuntime').text().includes('true'), true); - }); - it('adds cf object', async () => { const res = await fixture.fetch('/'); const html = await res.text(); diff --git a/packages/integrations/cloudflare/test/compile-image-service.test.js b/packages/integrations/cloudflare/test/compile-image-service.test.js index 2eb6d91ed8ea..918cc3c98281 100644 --- a/packages/integrations/cloudflare/test/compile-image-service.test.js +++ b/packages/integrations/cloudflare/test/compile-image-service.test.js @@ -40,7 +40,7 @@ describe( const res = await fixture.fetch('/_image?href=//placehold.co/600x400'); const html = await res.text(); const status = res.status; - assert.equal(html, 'Forbidden'); + assert.equal(html, 'Blocked'); assert.equal(status, 403); }); diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/caches.astro b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/caches.astro index 743111721e53..ea6c5a6eed4e 100644 --- a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/caches.astro +++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/caches.astro @@ -1,5 +1,4 @@ --- -const runtime = Astro.locals.runtime; --- @@ -10,6 +9,6 @@ const runtime = Astro.locals.runtime; CACHES -
{!!runtime.caches}
+
{!!caches}
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/d1.astro b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/d1.astro index a28940e9fe46..8a0042a52b40 100644 --- a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/d1.astro +++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/d1.astro @@ -1,6 +1,6 @@ --- -const runtime = Astro.locals.runtime; -const db = runtime.env?.D1; +import { env } from 'cloudflare:workers'; +const db = env.D1; await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); await db.exec("INSERT INTO test (name) VALUES ('true')"); const result = await db.prepare("SELECT * FROM test").all(); @@ -14,8 +14,8 @@ const result = await db.prepare("SELECT * FROM test").all(); D1 -
{!!runtime.env?.D1}
-
{!!runtime.env?.D1_PROD}
+
{!!env.D1}
+
{!!env.D1_PROD}
{!!result.results[0].name}
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/index.astro index 7d2ce1ef157f..2f7236b31806 100644 --- a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/index.astro +++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/index.astro @@ -1,5 +1,5 @@ --- -const runtime = Astro.locals.runtime; +const request = Astro.request; --- @@ -7,7 +7,6 @@ const runtime = Astro.locals.runtime;

Testing

-
{!!runtime}
-
{!!runtime.cf?.colo}
+
{!!request.cf?.colo}
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/kv.astro b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/kv.astro index d21f163a092c..93998bfdc5ff 100644 --- a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/kv.astro +++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/kv.astro @@ -1,6 +1,6 @@ --- -const runtime = Astro.locals.runtime; -const kv = runtime.env?.KV; +import { env } from "cloudflare:workers"; +const kv = env.KV; await kv.put("test", "true"); const result = await kv.get("test") --- @@ -13,8 +13,8 @@ const result = await kv.get("test") KV -
{!!runtime.env?.KV}
-
{!!runtime.env?.KV_PROD}
+
{!!env.KV}
+
{!!env.KV_PROD}
{!!result}
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/r2.astro b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/r2.astro index fbb9fc61b03f..feb25e9084ce 100644 --- a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/r2.astro +++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/r2.astro @@ -1,6 +1,6 @@ --- -const runtime = Astro.locals.runtime; -const bucket = runtime.env?.R2; +import { env } from "cloudflare:workers"; +const bucket = env.R2; await bucket.put("test", "true"); const result = await (await bucket.get("test")).text() --- @@ -13,8 +13,8 @@ const result = await (await bucket.get("test")).text() R2 -
{!!runtime.env?.R2}
-
{!!runtime.env?.R2_PROD}
+
{!!env.R2}
+
{!!env.R2_PROD}
{!!result}
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-env/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/astro-env/src/pages/index.astro index 7e50474edd94..a041063579ee 100644 --- a/packages/integrations/cloudflare/test/fixtures/astro-env/src/pages/index.astro +++ b/packages/integrations/cloudflare/test/fixtures/astro-env/src/pages/index.astro @@ -1,8 +1,7 @@ --- import { API_URL } from "astro:env/client" import { PORT, API_SECRET } from "astro:env/server" - -const runtime = Astro.locals.runtime; +import { env } from "cloudflare:workers" --- @@ -10,7 +9,7 @@ const runtime = Astro.locals.runtime;

Astro Env

-
{JSON.stringify(runtime.env, null, 2)}
+
{JSON.stringify(env, null, 2)}
API_URL {API_URL} diff --git a/packages/integrations/cloudflare/test/fixtures/wrangler-preview-platform/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/wrangler-preview-platform/src/pages/index.astro index 8a3b20925173..875d0e58568e 100644 --- a/packages/integrations/cloudflare/test/fixtures/wrangler-preview-platform/src/pages/index.astro +++ b/packages/integrations/cloudflare/test/fixtures/wrangler-preview-platform/src/pages/index.astro @@ -1,5 +1,7 @@ --- -const runtime = Astro.locals.runtime; +import { env } from 'cloudflare:workers'; + +const cfContext = Astro.locals.cfContext; --- @@ -7,9 +9,9 @@ const runtime = Astro.locals.runtime;

Testing

-
{!!runtime}
-
{!!runtime.env?.COOL}
-
{!!runtime.cf?.colo}
-
{!!runtime.caches}
+
{!!cfContext}
+
{!!env?.COOL}
+
{!!Astro.request.cf?.colo}
+
{!!caches}
diff --git a/packages/integrations/cloudflare/tsconfig.json b/packages/integrations/cloudflare/tsconfig.json index ea1c17c2952b..f626a74ff932 100644 --- a/packages/integrations/cloudflare/tsconfig.json +++ b/packages/integrations/cloudflare/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src", "virtual.d.ts"], + "include": ["src", "virtual.d.ts", "types.d.ts"], "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": ["@cloudflare/workers-types"] } } diff --git a/packages/integrations/cloudflare/types.d.ts b/packages/integrations/cloudflare/types.d.ts new file mode 100644 index 000000000000..0f2883020a1b --- /dev/null +++ b/packages/integrations/cloudflare/types.d.ts @@ -0,0 +1,20 @@ +/** + * Cloudflare Worker Request types + * Extends the global Request object with Cloudflare-specific properties + */ + +import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; + +declare global { + interface Request { + /** + * Cloudflare-specific properties available on incoming requests + * Contains metadata about the request such as: + * - Geographic information (country, colo, timezone) + * - TLS/Security details (cipher, protocol version) + * - Bot Management scores + * - Client information (ASN, TCP metrics) + */ + readonly cf?: IncomingRequestCfProperties; + } +}