Skip to content

Commit

Permalink
Cloudflare adapter (#1155)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlecAivazis authored Sep 15, 2023
1 parent 6b9fbb7 commit adf90d3
Show file tree
Hide file tree
Showing 52 changed files with 2,204 additions and 754 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-eels-destroy-more.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini': patch
---

Add adapter infrastructure when building for production
7 changes: 7 additions & 0 deletions .changeset/empty-eels-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'houdini-adapter-cloudflare': patch
'houdini-react': patch
'houdini': patch
---

Add cloudflare adapter
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx pretty-quick@latest --staged
npx lint-staged@latest
4 changes: 3 additions & 1 deletion e2e/react/houdini.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
/// <references types="houdini-router">
/** @type {import('houdini').ConfigFile} */
const config = {
schemaPath: '../_api/*.graphql',
watchSchema: {
url: 'https://houdinigraphql.com/graphql',
},
defaultPartial: true,
scalars: {
DateTime: {
Expand Down
17 changes: 11 additions & 6 deletions e2e/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
"build:web": "pnpm build: && pnpm web",
"build:test": "pnpm build: && pnpm test",
"build:build": "pnpm build: && pnpm build",
"web": "vite ",
"api": "cross-env TZ=utc e2e-api",
"dev": "concurrently \"pnpm run web\" \"pnpm run api\" -n \"web,api\" -c \"green,magenta\"",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@cloudflare/kv-asset-handler": "^0.3.0",
"@cloudflare/workers-types": "^4.20230904.0",
"cookie": "^0.5.0",
"houdini": "workspace:^",
"houdini-adapter-cloudflare": "workspace:^",
"houdini-react": "workspace:^",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"itty-router": "^4.0.23",
"react": "^18.3.0-canary-d6dcad6a8-20230914",
"react-dom": "^18.3.0-canary-d6dcad6a8-20230914",
"react-streaming": "^0.3.10"
},
"devDependencies": {
Expand All @@ -29,7 +32,9 @@
"concurrently": "7.1.0",
"cross-env": "^7.0.3",
"e2e-api": "workspace:^",
"hono": "^3.6.0",
"typescript": "^4.9.3",
"vite": "^4.1.0"
"vite": "^4.1.0",
"wrangler": "^3.7.0"
}
}
23 changes: 23 additions & 0 deletions e2e/react/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
type Link {
name: String
url: String
}

type Mutation {
hello(name: String!): String!
}

type Query {
giveMeAnError: String
links(delai: Int): [Link!]!
sponsors: [Sponsor!]!
welcome: String!
}

type Sponsor {
avatarUrl: String!
login: String!
name: String!
tiersTitle: String!
websiteUrl: String
}
2 changes: 1 addition & 1 deletion e2e/react/src/+client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { HoudiniClient } from '$houdini'

// Export the Houdini client
export default new HoudiniClient({
url: 'http://localhost:4000/graphql',
url: 'https://houdinigraphql.com/graphql',
})
3 changes: 0 additions & 3 deletions e2e/react/src/routes/+layout.gql

This file was deleted.

22 changes: 6 additions & 16 deletions e2e/react/src/routes/+layout.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import type { LayoutProps } from './$types'

export default function ({ HelloRouter, children }: LayoutProps) {
export default function ({ children }: LayoutProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
message: {HelloRouter.message}
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/users/1">Bruce Willis</a>
</li>
<li>
<a href="/users/2">Samuel Jackson</a>
</li>
<li>
<a href="/users/3">Morgan Freeman</a>
</li>
</ul>
Layout!
<div>
<a href="/">Sponsors</a>
<a href="/links">Links</a>
</div>
<div>{children}</div>
</div>
)
Expand Down
5 changes: 5 additions & 0 deletions e2e/react/src/routes/+page.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query SponsorList {
sponsors {
name
}
}
4 changes: 2 additions & 2 deletions e2e/react/src/routes/+page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PageProps } from './$types'

export default function ({ HelloRouter }: PageProps) {
return <div>{HelloRouter.message}!</div>
export default function ({ SponsorList }: PageProps) {
return <div>{JSON.stringify(SponsorList)}</div>
}
5 changes: 5 additions & 0 deletions e2e/react/src/routes/links/+page.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query LinkList {
links {
url
}
}
5 changes: 5 additions & 0 deletions e2e/react/src/routes/links/+page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { PageProps } from './$types'

export default function ({ LinkList }: PageProps) {
return <div>{JSON.stringify(LinkList)}</div>
}
6 changes: 0 additions & 6 deletions e2e/react/src/routes/users/[id]/+page.gql

This file was deleted.

23 changes: 0 additions & 23 deletions e2e/react/src/routes/users/[id]/+page.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion e2e/react/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import react from '@vitejs/plugin-react'
import adapter from 'houdini-adapter-cloudflare'
import houdini from 'houdini/vite'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [houdini(), react({ fastRefresh: false })],
plugins: [houdini({ adapter }), react({ fastRefresh: false })],
})
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@vitest/ui": "^0.28.3",
"eslint-plugin-unused-imports": "^2.0.0",
"graphql": "^15.8.0",
"lint-staged": "^12.3.4",
"prettier": "^2.8.3",
"turbo": "^1.8.8",
"typescript": "^4.9",
Expand All @@ -49,5 +50,11 @@
"memfs": "^3.4.7",
"recast": "^0.23.1"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"lint-staged": {
"*.ts": "prettier -w",
"*.tsx": "prettier -w",
"*.js": "prettier -w",
"*.json": "prettier -w"
}
}
64 changes: 64 additions & 0 deletions packages/houdini-adapter-cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "houdini-adapter-cloudflare",
"version": "1.2.9",
"description": "The adapter for deploying your Houdini application to Cloudflare Pages",
"keywords": [
"houdini",
"adpter",
"cloudflare",
"workers"
],
"homepage": "https://github.com/HoudiniGraphql/houdini",
"funding": "https://github.com/sponsors/HoudiniGraphql",
"repository": {
"type": "git",
"url": "https://github.com/HoudiniGraphql/houdini.git"
},
"license": "MIT",
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "^4.20230904.0",
"@types/cookie": "^0.5.2",
"scripts": "workspace:^",
"tsup": "^7.2.0",
"vitest": "^0.28.3"
},
"scripts": {
"build": "tsup src/index.ts src/worker.tsx --format esm,cjs --external ../\\$houdini --external __STATIC_CONTENT_MANIFEST --minify --dts --clean --out-dir build",
"build:": "cd ../../ && ((run build && cd -) || (cd - && exit 1))",
"build:build": "pnpm build: && pnpm build"
},
"dependencies": {
"@cloudflare/kv-asset-handler": "^0.3.0",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.0.10",
"cookie": "^0.5.0",
"houdini": "workspace:^",
"itty-router": "^4.0.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-streaming": "^0.3.14"
},
"files": [
"build"
],
"exports": {
"./package.json": "./package.json",
".": {
"import": "./build/index.js",
"require": "./build/index.cjs"
},
"./app": {
"import": "./build/app.js",
"require": "./build/app.cjs"
}
},
"types": "./build/index.d.ts",
"typesVersions": {
"*": {
"app": [
"build/app.d.ts"
]
}
}
}
18 changes: 18 additions & 0 deletions packages/houdini-adapter-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type Adapter, fs, path } from 'houdini'
import { fileURLToPath } from 'node:url'

const adapter: Adapter = async ({ config, conventions, publicBase, outDir, sourceDir }) => {
// the first thing we have to do is copy the source directory over
await fs.recursiveCopy(sourceDir, outDir)

// read the contents of the worker file
const workerContents = await fs.readFile(sourcePath('./worker.js'))

await fs.writeFile(path.join(outDir, '_worker.js'), workerContents!)
}

export default adapter

function sourcePath(path: string) {
return fileURLToPath(new URL(path, import.meta.url).href)
}
84 changes: 84 additions & 0 deletions packages/houdini-adapter-cloudflare/src/worker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ExportedHandler } from '@cloudflare/workers-types'
import { parse } from 'cookie'
import type { QueryArtifact } from 'houdini'
import { renderToStream } from 'react-streaming/server'

// The following imports local assets from the generated runtime
// This is not the desired API. just the easiest way to get this working
// and validate the rendering strategy
//
//
// @ts-expect-error
import { router_cache } from '../$houdini'
// @ts-expect-error
import manifest from '../$houdini/plugins/houdini-react/runtime/manifest'
// @ts-expect-error
import { find_match } from '../$houdini/plugins/houdini-react/runtime/routing/lib/match'
// @ts-expect-error
import App from '../$houdini/plugins/houdini-react/units/render/App'
// @ts-expect-error
import { Cache } from '../$houdini/runtime/cache/cache.js'

const handlers: ExportedHandler = {
async fetch(req, env: any, ctx) {
// if we aren't loading an asset, push the request through our router
const url = new URL(req.url).pathname

// we are handling an asset
if (url.startsWith('/assets/') || url === '/favicon.ico') {
return await env.ASSETS.fetch(req)
}

// otherwise we just need to render the application
return await render_app(req)
},
}

async function render_app(request: Parameters<Required<ExportedHandler>['fetch']>[0]) {
// pull out the desired url
const url = new URL(request.url).pathname

// load the session cookie
const cookie = parse(request.headers.get('Cookie') || '')['houdini-session']
const session = cookie ? JSON.parse(cookie) : null

// find the matching url
const [match] = find_match(manifest, url, true)
if (!match) {
throw new Error('no match')
}

// instanitate a cache we can use
const cache = new Cache({ disabled: false })

const { readable, injectToStream } = await renderToStream(
<App
loaded_queries={{}}
loaded_artifacts={{}}
initialURL={url}
cache={cache}
session={session}
assetPrefix={'/assets'}
{...router_cache()}
/>,
{
userAgent: 'Vite',
}
)

// add the initial scripts to the page
injectToStream(`
<script>
window.__houdini__initial__cache__ = ${cache.serialize()};
window.__houdini__initial__session__ = ${JSON.stringify(session)};
</script>
<!-- add a virtual module that hydrates the client and sets up the initial pending cache -->
<script type="module" src="/assets/pages/${match.id}.js" async=""></script>
`)

// and deliver our Response while that’s running.
return new Response(readable)
}

export default handlers
Loading

0 comments on commit adf90d3

Please sign in to comment.