diff --git a/e2e/react/src/components/SponsorInfo.tsx b/e2e/react/src/components/SponsorInfo.tsx index 0dc05b7263..f10ed92a79 100644 --- a/e2e/react/src/components/SponsorInfo.tsx +++ b/e2e/react/src/components/SponsorInfo.tsx @@ -1,4 +1,4 @@ -import { graphql, useFragment, type SponsorInfo } from '$houdini' +import { graphql, type SponsorInfo, useFragment } from '$houdini' type Props = { sponsor: SponsorInfo diff --git a/packages/houdini-react/src/plugin/codegen/render.ts b/packages/houdini-react/src/plugin/codegen/render.ts index 28593dd124..e175f8b344 100644 --- a/packages/houdini-react/src/plugin/codegen/render.ts +++ b/packages/houdini-react/src/plugin/codegen/render.ts @@ -24,11 +24,104 @@ export const renderToStream = render ` const app_index = ` +import { Router } from '$houdini/plugins/houdini-react/runtime' import React from 'react' + import Shell from '../../../../../src/+index' -import { Router } from '$houdini' -export default (props) => +export default (props) => ( + + + +) +` + + let renderer = ` + import { Cache } from '$houdini/runtime/cache/cache' +import { serverAdapterFactory, _serverHandler } from '$houdini/runtime/router/server' +import { HoudiniClient } from '$houdini/runtime/client' +import { renderToStream } from 'react-streaming/server' +import React from 'react' + +import { router_cache } from '../../runtime/routing' +// @ts-expect-error +import client from '../../../../../src/+client' +// @ts-expect-error +import App from './App' + +export const on_render = + ({ assetPrefix, pipe, production, documentPremable }) => + async ({ + url, + match, + session, + manifest, + }) => { + // instanitate a cache we can use for this request + const cache = new Cache({ disabled: false }) + + if (!match) { + return new Response('not found', { status: 404 }) + } + + const { + readable, + injectToStream, + pipe: pipeTo, + } = await renderToStream( + React.createElement(App, { + initialURL: url, + cache: cache, + session: session, + assetPrefix: assetPrefix, + manifest: manifest, + ...router_cache() + }), + { + userAgent: 'Vite', + } + ) + + // add the initial scripts to the page + injectToStream(\` + + + \${documentPremable} + + + + \`) + + if (pipeTo && pipe) { + pipeTo(pipe) + return true + } else { + return new Response(readable) + } + } + +export function reactServerHandler(options) { + return _serverHandler({ + ...options, + client, + on_render: on_render({ assetPrefix: options.assetPrefix, pipe: options.pipe, documentPremable: options.documentPremable }), + }) +} + +export default function createServerAdapter(options) { + return serverAdapterFactory({ + client, + production: true, + ...options, + on_render: on_render({ assetPrefix: options.assetPrefix }), + }) +} ` // and a file that adapters can import to get the local configuration @@ -55,74 +148,9 @@ export default (props) => } ` - // we need a file in the local runtime that we can use to drive the server-side responses - const server_adapter = ` -import React from 'react' -import { renderToStream } from 'react-streaming/server' -import { Cache } from '$houdini/runtime/cache/cache' -import { serverAdapterFactory } from '$houdini/runtime/router/server' - -import { Router, router_cache } from '../../runtime' -import manifest from '../../runtime/manifest' -import App from './App' - -import Shell from '../../../../../src/+index' - -export default (options) => { - return serverAdapterFactory({ - manifest, - ...options, - on_render: async ({url, match, session, pipe , manifest }) => { - // instanitate a cache we can use for this request - const cache = new Cache({ disabled: false }) - - if (!match) { - return new Response('not found', { status: 404 }) - } - - const { readable, injectToStream, pipe: pipeTo } = await renderToStream( - React.createElement(App, { - initialURL: url, - cache: cache, - session: session, - assetPrefix: options.assetPrefix, - manifest: manifest, - ...router_cache() - }), - { - userAgent: 'Vite', - } - ) - - // add the initial scripts to the page - injectToStream(\` - - - - - \`) - - if (pipe && pipeTo) { - // pipe the response to the client - pipeTo(pipe) - } else { - // and deliver our Response while that's running. - return new Response(readable) - } - }, - }) -} - ` - await Promise.all([ - fs.writeFile(routerConventions.server_adapter_path(config), server_adapter), fs.writeFile(routerConventions.adapter_config_path(config), adapter_config), + fs.writeFile(routerConventions.server_adapter_path(config), renderer), fs.writeFile(routerConventions.app_component_path(config), app_index), fs.writeFile(routerConventions.vite_render_path(config), vite_render), ]) diff --git a/packages/houdini-react/src/plugin/vite.tsx b/packages/houdini-react/src/plugin/vite.tsx index 839f9d1d05..b44a304229 100644 --- a/packages/houdini-react/src/plugin/vite.tsx +++ b/packages/houdini-react/src/plugin/vite.tsx @@ -1,31 +1,23 @@ -import * as graphql from 'graphql' import { GraphQLSchema } from 'graphql' -import { createYoga } from 'graphql-yoga' import { PluginHooks, path, fs, load_manifest, - routerConventions, isSecondaryBuild, type ProjectManifest, - type ServerAdapterFactory, type YogaServer, type RouterManifest, - find_match, localApiEndpoint, loadLocalSchema, - handle_request, - localApiSessionKeys, - get_session, - Cache, - HoudiniClient, + routerConventions, + find_match, + internalRoutes, } from 'houdini' import React from 'react' -import type { BuildOptions } from 'vite' +import type { BuildOptions, Connect } from 'vite' import { setManifest } from '.' -import { router_cache } from '../runtime' import { writeTsconfig } from './codegen/typeRoot' // in order to coordinate the client and server, the client's pending request cache @@ -172,8 +164,6 @@ export default { }) } - - // hydrate the cache with the information from the initial payload window.__houdini__cache__?.hydrate( window.__houdini__initial__cache__, @@ -209,6 +199,11 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca await writeTsconfig(server.houdiniConfig) server.middlewares.use(async (req, res, next) => { + if (!req.url) { + next() + return + } + // import the router manifest from the runtime // pull in the project's manifest const { default: router_manifest } = (await server.ssrLoadModule( @@ -218,19 +213,16 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca ) )) as { default: RouterManifest } - const graphqlEndpoint = localApiEndpoint(server.houdiniConfig.configFile) + const [match] = find_match(router_manifest, req.url) - // any requests for things that aren't routes shouldn't be handled - try { - const [match] = find_match(router_manifest, req.url ?? '/') - if (!match) { - throw new Error() - } - } catch { - if (req.url !== graphqlEndpoint) { - console.log('skipping', req.url) - return next() - } + if ( + !match && + !internalRoutes(server.houdiniConfig.configFile).find((route) => + req.url?.startsWith(route) + ) + ) { + next() + return } // its worth loading the project manifest @@ -241,6 +233,7 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca if (project_manifest.local_schema) { schema = await loadLocalSchema(server.houdiniConfig) } + // import the yoga server let yoga: YogaServer | null = null if (project_manifest.local_yoga) { @@ -249,112 +242,77 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca '+yoga?t=' + new Date().getTime() ) yoga = (await server.ssrLoadModule(yogaPath)) as YogaServer - } else if (project_manifest.local_schema) { - yoga = createYoga({ - schema: schema!, - landingPage: true, - graphqlEndpoint, - }) - } - - if (!req.url) { - next() - return } - // pull out the desired url - const url = req.url + // load the render factory + const { reactServerHandler: serverHandler } = (await server.ssrLoadModule( + routerConventions.server_adapter_path(server.houdiniConfig) + + '?t=' + + new Date().getTime() + )) as { reactServerHandler: any } - console.log(yoga, url) - // if its a req we can process with yoga, do it. - if (yoga && url === localApiEndpoint(server.houdiniConfig.configFile)) { - console.log('yoga response') - return yoga(req, res) + const requestHeaders = new Headers() + for (const header of Object.entries(req.headers ?? {})) { + requestHeaders.set(header[0], header[1] as string) } - const headers = new Headers(req.headers as Record) - const session_keys = localApiSessionKeys(server.houdiniConfig.configFile) - // load the session information - const session = get_session( - headers, - localApiSessionKeys(server.houdiniConfig.configFile) + // wrap the vite request in a proper on + const request = new Request( + 'https://localhost:5173' + req.url, + req.method === 'POST' + ? { + method: req.method, + headers: requestHeaders, + body: await getBody(req), + } + : undefined ) - // maybe its a session-related req - const authResponse = await handle_request({ - url, - config: server.houdiniConfig.configFile, - session_keys, - headers: new Headers(Object.entries(headers)), - }) - if (authResponse) { - return authResponse + for (const [key, value] of Object.entries(req.headers)) { + request.headers.set(key, value as string) } - // the req is for a server-side rendered page - - // find the matching url - const [match] = find_match(router_manifest, url) - // instanitate a cache we can use for this request - const cache = new Cache({ disabled: false }) - - if (!match) { - return new Response('not found', { status: 404 }) - } - - const { default: client } = (await server.ssrLoadModule( - path.join(server.houdiniConfig.projectRoot, 'src', '+client') - )) as { default: HoudiniClient } - const { renderToStream } = await server.ssrLoadModule( - routerConventions.vite_render_path(server.houdiniConfig) - ) - const { default: App } = (await server.ssrLoadModule( - routerConventions.app_component_path(server.houdiniConfig) + - '?t=' + - new Date().getTime() - )) as { default: () => React.ReactElement } - - if (schema) { - const graphqlEndpoint = localApiEndpoint(server.houdiniConfig.configFile) - client.registerProxy(graphqlEndpoint, async ({ query, variables, session }) => { - // get the parsed query - const parsed = graphql.parse(query) - - return await graphql.execute(schema!, parsed, null, session, variables) - }) + // instantiate the handler and invoke it with a mocked response + const result: Response = await serverHandler({ + schema, + yoga, + production: false, + manifest: router_manifest, + graphqlEndpoint: localApiEndpoint(server.houdiniConfig.configFile), + assetPrefix: '/virtual:houdini', + pipe: res, + documentPremable: ``, + })(request) + if (result && result.status === 404) { + next() } - - const { injectToStream, pipe } = await renderToStream( - React.createElement(App, { - initialURL: url, - cache: cache, - session: session, - assetPrefix: '/virtual:houdini', - manifest: manifest, - ...router_cache(), - }), - { - userAgent: 'Vite', + // if we got here but we didn't pipe a response then we have to send the result to the end + if (result && typeof result !== 'boolean') { + if (res.closed) { + return } - ) - - // add the initial scripts to the page - injectToStream(` - - - - -