From d7fe2be4087d7da37d454d0da3071a521f8e84e6 Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Wed, 11 Oct 2023 23:38:25 -0700 Subject: [PATCH] Stabilize react deployments (#1216) --- .changeset/eight-lemons-smile.md | 5 ++ .changeset/yellow-tables-pump.md | 5 ++ e2e/react/houdini.config.js | 12 +-- .../react-typescript/.meta.gitignore | 9 ++- .../templates/react-typescript/src/+index.tsx | 78 +++++++++---------- .../templates/react/.meta.gitignore | 4 + .../houdini-adapter-cloudflare/src/index.ts | 5 +- .../houdini-adapter-cloudflare/src/worker.ts | 1 - packages/houdini-react/package.json | 2 +- packages/houdini-react/src/plugin/vite.tsx | 27 ++++--- .../src/runtime/server/renderToStream.ts | 30 +++---- packages/houdini/src/lib/config.ts | 2 +- packages/houdini/src/lib/router/server.ts | 9 ++- packages/houdini/src/lib/types.ts | 2 +- .../houdini/src/runtime/router/session.ts | 19 +++-- packages/houdini/src/vite/houdini.ts | 25 ++++-- 16 files changed, 129 insertions(+), 106 deletions(-) create mode 100644 .changeset/eight-lemons-smile.md create mode 100644 .changeset/yellow-tables-pump.md diff --git a/.changeset/eight-lemons-smile.md b/.changeset/eight-lemons-smile.md new file mode 100644 index 000000000..06d361482 --- /dev/null +++ b/.changeset/eight-lemons-smile.md @@ -0,0 +1,5 @@ +--- +'houdini-react': patch +--- + +Stabilize react deployments diff --git a/.changeset/yellow-tables-pump.md b/.changeset/yellow-tables-pump.md new file mode 100644 index 000000000..d04ead999 --- /dev/null +++ b/.changeset/yellow-tables-pump.md @@ -0,0 +1,5 @@ +--- +'houdini-react': patch +--- + +Fix session redirects diff --git a/e2e/react/houdini.config.js b/e2e/react/houdini.config.js index 2441be158..0708c6951 100644 --- a/e2e/react/houdini.config.js +++ b/e2e/react/houdini.config.js @@ -27,11 +27,13 @@ const config = { }, plugins: { - 'houdini-react': { - auth: { - redirect: '/auth/token', - sessionKeys: ['supersecret'], - }, + 'houdini-react': {}, + }, + + router: { + auth: { + redirect: '/auth/token', + sessionKeys: ['supersecret'], }, }, } diff --git a/packages/create-houdini/templates/react-typescript/.meta.gitignore b/packages/create-houdini/templates/react-typescript/.meta.gitignore index dfc458a61..80ee705de 100644 --- a/packages/create-houdini/templates/react-typescript/.meta.gitignore +++ b/packages/create-houdini/templates/react-typescript/.meta.gitignore @@ -7,13 +7,16 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +.env.local +.env.production.local +.env.*.local + + node_modules dist dist-ssr *.local - -./$houdini -./dist +$houdini # Editor directories and files .vscode/* diff --git a/packages/create-houdini/templates/react-typescript/src/+index.tsx b/packages/create-houdini/templates/react-typescript/src/+index.tsx index 7ebaf4fae..d3b2ac928 100644 --- a/packages/create-houdini/templates/react-typescript/src/+index.tsx +++ b/packages/create-houdini/templates/react-typescript/src/+index.tsx @@ -1,47 +1,47 @@ -import React from "react"; +import React from 'react' export default function App({ children }) { - return ( - - - - - - - Houdini • React - - - {children} - - - ); + return ( + + + + + + + Houdini • React + + + {children} + + + ) } -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } +class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> { + constructor(props: { children: React.ReactNode }) { + super(props) + this.state = { hasError: false } + } - static getDerivedStateFromError(error) { - return { hasError: true }; - } + static getDerivedStateFromError(error: Error) { + return { hasError: true } + } - componentDidCatch(error, info) { - console.error("ErrorBoundary caught an error:", error, info); - } + componentDidCatch(error: Error, info: {}) { + console.error('ErrorBoundary caught an error:', error, info) + } - render() { - if (this.state.hasError) { - return

Something went wrong.

; - } - return this.props.children; - } + render() { + if (this.state.hasError) { + return

Something went wrong.

+ } + return this.props.children + } } diff --git a/packages/create-houdini/templates/react/.meta.gitignore b/packages/create-houdini/templates/react/.meta.gitignore index dfc458a61..9436fbf45 100644 --- a/packages/create-houdini/templates/react/.meta.gitignore +++ b/packages/create-houdini/templates/react/.meta.gitignore @@ -7,6 +7,10 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +.env.local +.env.production.local +.env.*.local + node_modules dist dist-ssr diff --git a/packages/houdini-adapter-cloudflare/src/index.ts b/packages/houdini-adapter-cloudflare/src/index.ts index 9d6aec536..880d430fd 100644 --- a/packages/houdini-adapter-cloudflare/src/index.ts +++ b/packages/houdini-adapter-cloudflare/src/index.ts @@ -3,13 +3,12 @@ import { fileURLToPath } from 'node:url' const adapter: Adapter = async ({ adapterPath, outDir, sourceDir }) => { // the first thing we have to do is copy the source directory over - await fs.recursiveCopy(sourceDir, outDir) + await fs.recursiveCopy(sourceDir, path.join(outDir, 'assets')) // read the contents of the worker file let workerContents = (await fs.readFile(sourcePath('./worker.js')))! - // if the project has a local schema, replace the schema import string with the - // import + // make sure that the adapter module imports from the correct path workerContents = workerContents.replaceAll('houdini/adapter', adapterPath) await fs.writeFile(path.join(outDir, '_worker.js'), workerContents!) diff --git a/packages/houdini-adapter-cloudflare/src/worker.ts b/packages/houdini-adapter-cloudflare/src/worker.ts index 7e46b1c10..133c2be9a 100644 --- a/packages/houdini-adapter-cloudflare/src/worker.ts +++ b/packages/houdini-adapter-cloudflare/src/worker.ts @@ -9,7 +9,6 @@ const server_adapter = createServerAdapter({ 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 diff --git a/packages/houdini-react/package.json b/packages/houdini-react/package.json index a6a96d483..c155d3062 100644 --- a/packages/houdini-react/package.json +++ b/packages/houdini-react/package.json @@ -64,4 +64,4 @@ }, "main": "./build/plugin-cjs/index.js", "types": "./build/plugin/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/houdini-react/src/plugin/vite.tsx b/packages/houdini-react/src/plugin/vite.tsx index 293a9b6e4..d76d3448c 100644 --- a/packages/houdini-react/src/plugin/vite.tsx +++ b/packages/houdini-react/src/plugin/vite.tsx @@ -1,4 +1,3 @@ -import { GraphQLSchema } from 'graphql' import { PluginHooks, path, @@ -6,13 +5,8 @@ import { load_manifest, isSecondaryBuild, type ProjectManifest, - type YogaServer, type RouterManifest, - localApiEndpoint, - loadLocalSchema, routerConventions, - find_match, - internalRoutes, } from 'houdini' import React from 'react' import { build, type BuildOptions, type Connect } from 'vite' @@ -117,11 +111,6 @@ export default { }, async closeBundle(this, config) { - // only build in production one - if (isSecondaryBuild() || devServer) { - return - } - // tell the user what we're doing console.log('🎩 Generating Server Assets...') @@ -305,7 +294,21 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca for (const header of Object.entries(result.headers ?? {})) { res.setHeader(header[0], header[1]) } - res.write(await result.text()) + // handle redirects + if (result.status >= 300 && result.status < 400) { + res.writeHead(result.status, { + Location: result.headers.get('Location') ?? '', + ...[...result.headers.entries()].reduce( + (headers, [key, value]) => ({ + ...headers, + [key]: value, + }), + {} + ), + }) + } else { + res.write(await result.text()) + } res.end() } }) diff --git a/packages/houdini-react/src/runtime/server/renderToStream.ts b/packages/houdini-react/src/runtime/server/renderToStream.ts index 565e24f2a..e31c3dbaa 100644 --- a/packages/houdini-react/src/runtime/server/renderToStream.ts +++ b/packages/houdini-react/src/runtime/server/renderToStream.ts @@ -1,12 +1,11 @@ import React from 'react' -import type { - renderToPipeableStream as RenderToPipeableStream, - renderToReadableStream as RenderToReadableStream, +import { + renderToPipeableStream, + renderToReadableStream, } from 'react-dom/server' import { createPipeWrapper, Pipe } from './renderToStream/createPipeWrapper' import { createReadableWrapper } from './renderToStream/createReadableWrapper' -import { nodeStreamModuleIsAvailable } from './renderToStream/loadNodeStreamModule' import { resolveSeoStrategy, SeoStrategy } from './renderToStream/resolveSeoStrategy' import { createDebugger } from './utils' @@ -21,8 +20,8 @@ type Options = { seoStrategy?: SeoStrategy userAgent?: string onBoundaryError?: (err: unknown) => void - renderToReadableStream?: typeof RenderToReadableStream - renderToPipeableStream?: typeof RenderToPipeableStream + renderToReadableStream?: typeof renderToReadableStream + renderToPipeableStream?: typeof renderToPipeableStream } type Result = ( | { @@ -55,7 +54,7 @@ async function renderToStream(element: React.ReactNode, options: Options = {}): const disable = globalConfig.disable || (options.disable ?? resolveSeoStrategy(options).disableStream) - const webStream = options.webStream ?? !(await nodeStreamModuleIsAvailable()) + const webStream = true debug(`disable === ${disable} && webStream === ${webStream}`) let result: Result @@ -80,7 +79,7 @@ async function renderToNodeStream( options: { debug?: boolean onBoundaryError?: (err: unknown) => void - renderToPipeableStream?: typeof RenderToPipeableStream + renderToPipeableStream?: typeof renderToPipeableStream } ) { debug('creating Node.js Stream Pipe') @@ -109,12 +108,8 @@ async function renderToNodeStream( } }) } - const renderToPipeableStream = - options.renderToPipeableStream ?? - // @ts-ignore - // We don't directly use import() because it shouldn't be bundled for Cloudflare Workers: the module react-dom/server.node contains a require('stream') which fails on Cloudflare Workers - ((await import('react-dom/server.node')) - .renderToPipeableStream as typeof RenderToPipeableStream) + + console.log("THIS ->", renderToPipeableStream) const { pipe: pipeOriginal } = renderToPipeableStream(element, { onShellReady() { @@ -160,7 +155,7 @@ async function renderToWebStream( options: { debug?: boolean onBoundaryError?: (err: unknown) => void - renderToReadableStream?: typeof RenderToReadableStream + renderToReadableStream?: typeof renderToReadableStream } ) { debug('creating Web Stream Pipe') @@ -178,11 +173,6 @@ async function renderToWebStream( } }) } - const renderToReadableStream = - options.renderToReadableStream ?? - // We directly use import() because it needs to be bundled for Cloudflare Workers - ((await import('react-dom/server.browser' as string)) - .renderToReadableStream as typeof RenderToReadableStream) const readableOriginal = await renderToReadableStream(element, { onError }) const { allReady } = readableOriginal diff --git a/packages/houdini/src/lib/config.ts b/packages/houdini/src/lib/config.ts index 1cd8ee853..3de5fcb25 100644 --- a/packages/houdini/src/lib/config.ts +++ b/packages/houdini/src/lib/config.ts @@ -348,7 +348,7 @@ export class Config { } get routerBuildDirectory() { - return path.join(this.projectRoot, 'dist', 'assets') + return path.join(this.projectRoot, 'dist') } get definitionsDocumentsPath() { diff --git a/packages/houdini/src/lib/router/server.ts b/packages/houdini/src/lib/router/server.ts index 103bad9f2..e032b628c 100644 --- a/packages/houdini/src/lib/router/server.ts +++ b/packages/houdini/src/lib/router/server.ts @@ -18,6 +18,9 @@ export function internalRoutes(config: ConfigFile): string[] { } export async function buildLocalSchema(config: Config): Promise { + // before we build the local schcema, we need to generate the typescript config file + // so that we can resolve all of the necessary imports + // load the current version of vite const { build } = await import('vite') @@ -32,15 +35,13 @@ export async function buildLocalSchema(config: Config): Promise { input: { schema: path.join(config.localApiDir, '+schema'), }, - external: ['graphql'], output: { entryFileNames: 'assets/[name].js', }, }, + ssr: true, lib: { - entry: { - schema: path.join(config.localApiDir, '+schema'), - }, + entry: path.join(config.localApiDir, '+schema'), formats: ['es'], }, }, diff --git a/packages/houdini/src/lib/types.ts b/packages/houdini/src/lib/types.ts index ee15fd822..1e7ecbbaa 100644 --- a/packages/houdini/src/lib/types.ts +++ b/packages/houdini/src/lib/types.ts @@ -285,7 +285,7 @@ export type PluginHooks = { buildStart?: ( this: PluginContext, options: NormalizedInputOptions & { houdiniConfig: Config } - ) => void + ) => void | Promise buildEnd?: ( this: PluginContext, diff --git a/packages/houdini/src/runtime/router/session.ts b/packages/houdini/src/runtime/router/session.ts index 753288557..213aafda8 100644 --- a/packages/houdini/src/runtime/router/session.ts +++ b/packages/houdini/src/runtime/router/session.ts @@ -13,7 +13,6 @@ type ServerHandlerArgs = { // so we want a single function that can be called to get the server export async function handle_request(args: ServerHandlerArgs): Promise { const plugin_config = args.config.router ?? {} - // if the project is configured to authorize users by redirect then // we might need to set the session value if ( @@ -25,18 +24,22 @@ export async function handle_request(args: ServerHandlerArgs): Promise { +async function redirect_auth(args: ServerHandlerArgs): Promise { // the session and configuration are passed as query parameters in // the url - const { searchParams } = new URL(args.url!, `http://${args.headers.get('host')}`) + const { searchParams, host } = new URL(args.url!, `http://${args.headers.get('host')}`) const { redirectTo, ...session } = Object.fromEntries(searchParams.entries()) // encode the session information as a cookie in the response and redirect the user - if (redirectTo) { - const response = Response.redirect(redirectTo, 302) - await set_session(args, response, session) - return response - } + const response = new Response('ok', { + status: 302, + headers: { + Location: redirectTo ?? '/', + }, + }) + await set_session(args, response, session) + + return response } export type Server = { diff --git a/packages/houdini/src/vite/houdini.ts b/packages/houdini/src/vite/houdini.ts index 5558a8a9b..49b3a2e75 100644 --- a/packages/houdini/src/vite/houdini.ts +++ b/packages/houdini/src/vite/houdini.ts @@ -81,6 +81,10 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { // we use this to generate the final assets needed for a production build of the server. // this is only called when bundling (ie, not in dev mode) async closeBundle() { + if (isSecondaryBuild() || viteEnv.mode !== 'production' || devServer) { + return + } + for (const plugin of config.plugins) { if (typeof plugin.vite?.closeBundle !== 'function') { continue @@ -89,15 +93,15 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { await plugin.vite!.closeBundle.call(this, config) } - if (isSecondaryBuild() || viteEnv.mode !== 'production' || devServer) { - return - } - // if we dont' have an adapter, we don't need to do anything if (!opts.adapter) { return } + // dry + const outDir = config.routerBuildDirectory + const sourceDir = viteConfig.build.outDir + // tell the user what we're doing console.log('🎩 Generating Deployment Assets...') @@ -113,15 +117,20 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { // load the project manifest const manifest = await load_manifest({ config, includeArtifacts: true }) + // before we load the adapter we want to do some manual prep on the directories + // pull the ssr directory out of assets + await fs.recursiveCopy(path.join(sourceDir, 'ssr'), path.join(outDir, 'ssr')) + await fs.rmdir(path.join(sourceDir, 'ssr')) + // invoke the adapter await opts.adapter({ config, conventions: routerConventions, - sourceDir: viteConfig.build.outDir, + sourceDir, publicBase: viteConfig.base, - outDir: config.routerBuildDirectory, + outDir, manifest, - adapterPath: './assets/ssr/entries/adapter', + adapterPath: './ssr/entries/adapter', }) // if there is a public directory at the root of the project, @@ -142,7 +151,7 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { } // @ts-expect-error - plugin.vite!.buildStart.call(this, { + await plugin.vite!.buildStart.call(this, { ...args, houdiniConfig: config, })