From 699382c5362a60411982ac5ffc4de6bb978f019a Mon Sep 17 00:00:00 2001
From: Richard Cooke <hello@richardcooke.io>
Date: Wed, 15 Jun 2022 17:52:01 +0100
Subject: [PATCH] feat: add SSR adaptor for cloudflare pages functions

---
 .changeset/nine-dots-applaud.md               |  5 ++
 packages/integrations/cloudflare/README.md    | 24 ++++++
 packages/integrations/cloudflare/package.json | 33 +++++++++
 packages/integrations/cloudflare/src/index.ts | 73 +++++++++++++++++++
 .../integrations/cloudflare/src/server.ts     | 34 +++++++++
 packages/integrations/cloudflare/src/shim.ts  |  4 +
 .../integrations/cloudflare/tsconfig.json     | 10 +++
 pnpm-lock.yaml                                | 11 +++
 8 files changed, 194 insertions(+)
 create mode 100644 .changeset/nine-dots-applaud.md
 create mode 100644 packages/integrations/cloudflare/README.md
 create mode 100644 packages/integrations/cloudflare/package.json
 create mode 100644 packages/integrations/cloudflare/src/index.ts
 create mode 100644 packages/integrations/cloudflare/src/server.ts
 create mode 100644 packages/integrations/cloudflare/src/shim.ts
 create mode 100644 packages/integrations/cloudflare/tsconfig.json

diff --git a/.changeset/nine-dots-applaud.md b/.changeset/nine-dots-applaud.md
new file mode 100644
index 000000000000..b1823dd26bb0
--- /dev/null
+++ b/.changeset/nine-dots-applaud.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/cloudflare': minor
+---
+
+add SSR adaptor for Cloudflare Pages functions
diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md
new file mode 100644
index 000000000000..73064e821aec
--- /dev/null
+++ b/packages/integrations/cloudflare/README.md
@@ -0,0 +1,24 @@
+# @astrojs/cloudflare
+
+An SSR adapter for use with Cloudflare Pages Functions targets. Write your code in Astro/Node and deploy to Cloudflare Pages.
+
+In your astro.config.mjs use:
+
+```js
+import { defineConfig } from 'astro/config';
+import cloudflare from '@astrojs/cloudflare';
+
+export default defineConfig({
+  adapter: cloudflare()
+});
+```
+
+## Enabling Preview
+
+In order for preview to work you must install `wrangler`
+
+```sh
+$ pnpm install wrangler --save-dev
+```
+
+It's then possible to update the preview script in your `package.json` to `"preview": "wrangler pages dev ./dist"`
diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json
new file mode 100644
index 000000000000..ff8f2c64102a
--- /dev/null
+++ b/packages/integrations/cloudflare/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "@astrojs/cloudflare",
+  "description": "Deploy your site to cloudflare pages functions",
+  "version": "0.1.0",
+  "type": "module",
+  "types": "./dist/index.d.ts",
+  "author": "withastro",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/withastro/astro.git",
+    "directory": "packages/integrations/cloudflare"
+  },
+  "bugs": "https://github.com/withastro/astro/issues",
+  "homepage": "https://astro.build",
+  "exports": {
+    ".": "./dist/index.js",
+    "./server.js": "./dist/server.js",
+    "./package.json": "./package.json"
+  },
+  "scripts": {
+    "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+    "build:ci": "astro-scripts build \"src/**/*.ts\"",
+    "dev": "astro-scripts dev \"src/**/*.ts\""
+  },
+  "dependencies": {
+    "esbuild": "^0.14.42"
+  },
+  "devDependencies": {
+    "astro": "workspace:*",
+    "astro-scripts": "workspace:*"
+  }
+}
\ No newline at end of file
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
new file mode 100644
index 000000000000..dc65f23ce5b0
--- /dev/null
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -0,0 +1,73 @@
+import type { AstroAdapter, AstroConfig, AstroIntegration, BuildConfig } from 'astro';
+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'],
+	};
+}
+
+export default function createIntegration(): AstroIntegration {
+	let _config: AstroConfig;
+	let _buildConfig: BuildConfig;
+
+	return {
+		name: '@astrojs/cloudflare',
+		hooks: {
+			'astro:config:done': ({ setAdapter, config }) => {
+				setAdapter(getAdapter());
+				_config = config;
+			},
+			'astro:build:start': ({ buildConfig }) => {
+				_buildConfig = buildConfig;
+				buildConfig.serverEntry = '_worker.js';
+				buildConfig.client = new URL('./static/', _config.outDir);
+				buildConfig.server = new URL('./', _config.outDir);
+			},
+			'astro:build:setup': ({ vite, target }) => {
+				if (target === 'server') {
+					vite.resolve = vite.resolve || {};
+					vite.resolve.alias = vite.resolve.alias || {};
+
+					const aliases = [{ find: 'react-dom/server', replacement: 'react-dom/server.browser' }];
+
+					if (Array.isArray(vite.resolve.alias)) {
+						vite.resolve.alias = [...vite.resolve.alias, ...aliases];
+					} else {
+						for (const alias of aliases) {
+							(vite.resolve.alias as Record<string, string>)[alias.find] = alias.replacement;
+						}
+					}
+
+					vite.ssr = {
+						target: 'webworker',
+						noExternal: true,
+					};
+				}
+			},
+			'astro:build:done': async () => {
+				const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
+				const pkg = fileURLToPath(entryUrl);
+
+				await esbuild.build({
+					target: 'es2020',
+					platform: 'browser',
+					entryPoints: [pkg],
+					outfile: pkg,
+					allowOverwrite: true,
+					format: 'esm',
+					bundle: true,
+					minify: true,
+				});
+
+				// throw the server folder in the bin
+				const chunksUrl = new URL('./chunks', _buildConfig.server);
+				await fs.promises.rm(chunksUrl, { recursive: true, force: true });
+			},
+		},
+	};
+}
diff --git a/packages/integrations/cloudflare/src/server.ts b/packages/integrations/cloudflare/src/server.ts
new file mode 100644
index 000000000000..6a76c06ff48c
--- /dev/null
+++ b/packages/integrations/cloudflare/src/server.ts
@@ -0,0 +1,34 @@
+import './shim.js';
+
+import type { SSRManifest } from 'astro';
+import { App } from 'astro/app';
+
+type Env = {
+	ASSETS: { fetch: (req: Request) => Promise<Response> };
+};
+
+export function createExports(manifest: SSRManifest) {
+	const app = new App(manifest);
+
+	const fetch = async (request: Request, env: Env) => {
+		const { origin, pathname } = new URL(request.url);
+
+		// static assets
+		if (manifest.assets.has(pathname)) {
+			const assetRequest = new Request(`${origin}/static${pathname}`, request);
+			return env.ASSETS.fetch(assetRequest);
+		}
+
+		if (app.match(request)) {
+			return app.render(request);
+		}
+
+		// 404
+		return new Response(null, {
+			status: 404,
+			statusText: 'Not found',
+		});
+	};
+
+	return { default: { fetch } };
+}
diff --git a/packages/integrations/cloudflare/src/shim.ts b/packages/integrations/cloudflare/src/shim.ts
new file mode 100644
index 000000000000..1a4a6ee9be4c
--- /dev/null
+++ b/packages/integrations/cloudflare/src/shim.ts
@@ -0,0 +1,4 @@
+(globalThis as any).process = {
+	argv: [],
+	env: {},
+};
diff --git a/packages/integrations/cloudflare/tsconfig.json b/packages/integrations/cloudflare/tsconfig.json
new file mode 100644
index 000000000000..44baf375c882
--- /dev/null
+++ b/packages/integrations/cloudflare/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "include": ["src"],
+  "compilerOptions": {
+    "allowJs": true,
+    "module": "ES2020",
+    "outDir": "./dist",
+    "target": "ES2020"
+  }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 533f97ef70c1..50e86253c4e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1664,6 +1664,17 @@ importers:
       mocha: 9.2.2
       uvu: 0.5.3
 
+  packages/integrations/cloudflare:
+    specifiers:
+      astro: workspace:*
+      astro-scripts: workspace:*
+      esbuild: ^0.14.42
+    dependencies:
+      esbuild: 0.14.43
+    devDependencies:
+      astro: link:../../astro
+      astro-scripts: link:../../../scripts
+
   packages/integrations/deno:
     specifiers:
       astro: workspace:*