diff --git a/.changeset/proud-moose-push.md b/.changeset/proud-moose-push.md new file mode 100644 index 000000000000..43dd7ecd9d7a --- /dev/null +++ b/.changeset/proud-moose-push.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': minor +--- + +add support for compiling functions to a functions directory rather than `_worker.js` diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index 3ab29b44486e..1bd65f5aed2e 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -14,6 +14,29 @@ export default defineConfig({ }); ``` +## Options + + +### Mode + +`mode: "advanced" | "directory"` + +default `"advanced"` + +Cloudflare Pages has 2 different modes for deploying functions, `advanced` mode which picks up the `_worker.js` in `dist`, or a directory mode where pages will compile the worker out of a functions folder in the project root. + +For most projects the adaptor default of `advanced` will be sufficiant, when in this mode the `dist` folder will contain your compiled project. However if you'd like to use [pages plugins](https://developers.cloudflare.com/pages/platform/functions/plugins/) such as [Sentry](https://developers.cloudflare.com/pages/platform/functions/plugins/sentry/) for example to enable logging, you'll need to use directory mode. + +In directory mode the adaptor will compile the client side part of you app the same way, but it will move the worker script into a `functions` folder in the project root. The adaptor will only ever place a `[[path]].js` in that folder, allowing you to add additional plugins and pages middlewhere which can be checked into version control . + +```ts +// directory mode +export default defineConfig({ + adapter: cloudflare({ mode: "directory" }), +}); + +``` + ## Enabling Preview In order for preview to work you must install `wrangler` diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index da192ec0c7c6..0056cec12c70 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -18,7 +18,8 @@ "homepage": "https://docs.astro.build/en/guides/integrations-guide/cloudflare/", "exports": { ".": "./dist/index.js", - "./server.js": "./dist/server.js", + "./server.advanced.js": "./dist/server.advanced.js", + "./server.directory.js": "./dist/server.directory.js", "./package.json": "./package.json" }, "scripts": { diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 76904ed95b40..e207bdb44b16 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -3,12 +3,22 @@ import esbuild from 'esbuild'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; -export function getAdapter(): AstroAdapter { - return { - name: '@astrojs/cloudflare', - serverEntrypoint: '@astrojs/cloudflare/server.js', - exports: ['default'], - }; +type Options = { + mode: 'directory' | 'advanced'; +}; + +export function getAdapter(isModeDirectory: boolean): AstroAdapter { + return isModeDirectory + ? { + name: '@astrojs/cloudflare', + serverEntrypoint: '@astrojs/cloudflare/server.directory.js', + exports: ['onRequest'], + } + : { + name: '@astrojs/cloudflare', + serverEntrypoint: '@astrojs/cloudflare/server.advanced.js', + exports: ['default'], + }; } const SHIM = `globalThis.process = { @@ -16,15 +26,16 @@ const SHIM = `globalThis.process = { env: {}, };`; -export default function createIntegration(): AstroIntegration { +export default function createIntegration(args?: Options): AstroIntegration { let _config: AstroConfig; let _buildConfig: BuildConfig; + const isModeDirectory = args?.mode === 'directory'; return { name: '@astrojs/cloudflare', hooks: { 'astro:config:done': ({ setAdapter, config }) => { - setAdapter(getAdapter()); + setAdapter(getAdapter(isModeDirectory)); _config = config; if (config.output === 'static') { @@ -36,8 +47,8 @@ export default function createIntegration(): AstroIntegration { }, 'astro:build:start': ({ buildConfig }) => { _buildConfig = buildConfig; - buildConfig.serverEntry = '_worker.js'; buildConfig.client = new URL('./static/', _config.outDir); + buildConfig.serverEntry = '_worker.js'; buildConfig.server = new URL('./', _config.outDir); }, 'astro:build:setup': ({ vite, target }) => { @@ -64,7 +75,6 @@ export default function createIntegration(): AstroIntegration { 'astro:build:done': async () => { const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server); const pkg = fileURLToPath(entryUrl); - await esbuild.build({ target: 'es2020', platform: 'browser', @@ -82,6 +92,13 @@ export default function createIntegration(): AstroIntegration { // throw the server folder in the bin const chunksUrl = new URL('./chunks', _buildConfig.server); await fs.promises.rm(chunksUrl, { recursive: true, force: true }); + + if (isModeDirectory) { + const functionsUrl = new URL(`file://${process.cwd()}/functions/`); + await fs.promises.mkdir(functionsUrl, { recursive: true }); + const directoryUrl = new URL('[[path]].js', functionsUrl); + await fs.promises.rename(entryUrl, directoryUrl); + } }, }, }; diff --git a/packages/integrations/cloudflare/src/server.ts b/packages/integrations/cloudflare/src/server.advanced.ts similarity index 100% rename from packages/integrations/cloudflare/src/server.ts rename to packages/integrations/cloudflare/src/server.advanced.ts diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts new file mode 100644 index 000000000000..abcc49b1079a --- /dev/null +++ b/packages/integrations/cloudflare/src/server.directory.ts @@ -0,0 +1,41 @@ +import './shim.js'; + +import type { SSRManifest } from 'astro'; +import { App } from 'astro/app'; + +export function createExports(manifest: SSRManifest) { + const app = new App(manifest, false) + + const onRequest = async ({ + request, + next, + }: { + request: Request; + next: (request: Request) => void; + }) => { + const { origin, pathname } = new URL(request.url); + + // static assets + if (manifest.assets.has(pathname)) { + const assetRequest = new Request(`${origin}/static${pathname}`, request); + return next(assetRequest); + } + + let routeData = app.match(request, { matchNotFound: true }); + if (routeData) { + Reflect.set( + request, + Symbol.for('astro.clientAddress'), + request.headers.get('cf-connecting-ip') + ); + return app.render(request, routeData); + } + + return new Response(null, { + status: 404, + statusText: 'Not found', + }); + }; + + return { onRequest }; +}