From e94044c6b6464795e9aa013fa7c326f590cdbef5 Mon Sep 17 00:00:00 2001 From: Richard Cooke Date: Wed, 15 Jun 2022 08:34:31 +0100 Subject: [PATCH] feat: add SSR adaptor for cloudflare pages functions --- .changeset/nine-dots-applaud.md | 6 ++ .../astro/src/runtime/server/hydration.ts | 11 ++- packages/astro/src/runtime/server/index.ts | 6 +- 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 | 20 +++++ 10 files changed, 215 insertions(+), 6 deletions(-) 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 0000000000000..d271b792c343c --- /dev/null +++ b/.changeset/nine-dots-applaud.md @@ -0,0 +1,6 @@ +--- +'@astrojs/cloudflare': minor +'astro': patch +--- + +add SSR adaptor for Cloudflare Pages functions diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index ec077adbec93f..c676ded554fdd 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -1,4 +1,3 @@ -import serializeJavaScript from 'serialize-javascript'; import type { AstroComponentMetadata, SSRElement, @@ -9,7 +8,13 @@ import { hydrationSpecifier, serializeListValue } from './util.js'; // Serializes props passed into a component so that they can be reused during hydration. // The value is any -export function serializeProps(value: any) { +export async function serializeProps(value: any) { + // We use a dynamic import here because 'serialize-javascript' generates a + // random number in module scope which is forbidden when using cloudflare + // workers/pages functions as a deploy target. By importing the dependency + // dynamically module scope vars are generated at call time, even when + // inlined. + const { default: serializeJavaScript } = await import('serialize-javascript'); return serializeJavaScript(value); } @@ -120,7 +125,7 @@ export async function generateHydrateScript( }: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve( componentUrl )}"), import("${await result.resolve(renderer.clientEntrypoint)}")]); - return (el, children) => hydrate(el)(Component, ${serializeProps( + return (el, children) => hydrate(el)(Component, ${await serializeProps( props )}, children, ${JSON.stringify({ client: hydrate })}); ` diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index 0f1e8de713925..392bf50579363 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -311,9 +311,9 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr // Include componentExport name, componentUrl, and props in hash to dedupe identical islands const astroId = shorthash( - `\n${html}\n${serializeProps( - props - )}` + `\n${html}\n${await serializeProps(props)}` ); // Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head. diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md new file mode 100644 index 0000000000000..73064e821aeca --- /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 0000000000000..ff8f2c64102a8 --- /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 0000000000000..dc65f23ce5b0c --- /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)[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 0000000000000..6a76c06ff48c1 --- /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 }; +}; + +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 0000000000000..1a4a6ee9be4c5 --- /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 0000000000000..44baf375c8825 --- /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 b65f9394b4f2d..e3b64280d193a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1651,6 +1651,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:* @@ -8134,6 +8145,11 @@ packages: /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.3 dev: false @@ -11027,6 +11043,8 @@ packages: debug: 3.2.7 iconv-lite: 0.4.24 sax: 1.2.4 + transitivePeerDependencies: + - supports-color dev: false /netmask/2.0.2: @@ -11110,6 +11128,8 @@ packages: rimraf: 2.7.1 semver: 5.7.1 tar: 4.4.19 + transitivePeerDependencies: + - supports-color dev: false /node-releases/2.0.5: