diff --git a/.changeset/soft-impalas-yell.md b/.changeset/soft-impalas-yell.md new file mode 100644 index 000000000..afa45746f --- /dev/null +++ b/.changeset/soft-impalas-yell.md @@ -0,0 +1,5 @@ +--- +'houdini-react': patch +--- + +Fix hot module reloading diff --git a/e2e/react/src/components/SponsorInfo.tsx b/e2e/react/src/components/SponsorInfo.tsx index 0dc05b726..f10ed92a7 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-adapter-cloudflare/package.json b/packages/houdini-adapter-cloudflare/package.json index a5777e326..c9fadcdf3 100644 --- a/packages/houdini-adapter-cloudflare/package.json +++ b/packages/houdini-adapter-cloudflare/package.json @@ -24,7 +24,7 @@ "vitest": "^0.28.3" }, "scripts": { - "build": "tsup src/index.ts src/worker.ts --format esm,cjs --external ../\\$houdini --external ../src --external graphql --minify --dts --clean --out-dir build", + "build": "tsup src/index.ts src/worker.ts --format esm,cjs --external vite --external ../\\$houdini --external ../src --external graphql --minify --dts --clean --out-dir build", "build:": "cd ../../ && ((run build && cd -) || (cd - && exit 1))", "build:build": "pnpm build: && pnpm build" }, diff --git a/packages/houdini-react/src/plugin/codegen/render.ts b/packages/houdini-react/src/plugin/codegen/render.ts index 12aabb4d4..1badaaf66 100644 --- a/packages/houdini-react/src/plugin/codegen/render.ts +++ b/packages/houdini-react/src/plugin/codegen/render.ts @@ -14,20 +14,109 @@ export async function generate_renders({ config: Config manifest: ProjectManifest }) { + const adapter_path = routerConventions.server_adapter_path(config) + // make sure the necessary directories exist await fs.mkdirp(path.dirname(routerConventions.server_adapter_path(config))) 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" +import router_manifest from '$houdini/plugins/houdini-react/runtime/manifest' + +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 createServerAdapter(options) { + return serverAdapterFactory({ + client, + production: true, + manifest: router_manifest, + on_render: on_render(options), + ...options, + }) +} ` // and a file that adapters can import to get the local configuration let adapter_config = ` - import createAdapter from './server' + import { createServerAdapter as createAdapter } from './server' export const endpoint = ${JSON.stringify(localApiEndpoint(config.configFile))} @@ -49,74 +138,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(adapter_path, renderer), fs.writeFile(routerConventions.app_component_path(config), app_index), ]) } diff --git a/packages/houdini-react/src/plugin/vite.tsx b/packages/houdini-react/src/plugin/vite.tsx index f58907029..4b445e2b5 100644 --- a/packages/houdini-react/src/plugin/vite.tsx +++ b/packages/houdini-react/src/plugin/vite.tsx @@ -4,17 +4,18 @@ import { path, fs, load_manifest, - routerConventions, isSecondaryBuild, type ProjectManifest, - type ServerAdapterFactory, type YogaServer, - RouterManifest, - find_match, + type RouterManifest, localApiEndpoint, loadLocalSchema, + routerConventions, + find_match, + internalRoutes, } from 'houdini' -import type { BuildOptions } from 'vite' +import React from 'react' +import { build, type BuildOptions, type Connect } from 'vite' import { setManifest } from '.' import { writeTsconfig } from './codegen/typeRoot' @@ -33,6 +34,7 @@ import { writeTsconfig } from './codegen/typeRoot' // virtual:houdini/artifacts/[name] - An entry for loading an artifact and notifying the artifact cache let manifest: ProjectManifest +let devServer: boolean = false export default { // we want to set up some vite aliases by default @@ -41,34 +43,52 @@ export default { setManifest(manifest) // secondary builds have their own rollup config - let rollupConfig: BuildOptions | undefined + let conf: { build?: BuildOptions; base?: string } = { + build: { + rollupOptions: {}, + }, + } // build up the list of entries that we need vite to bundle - if (!isSecondaryBuild()) { - rollupConfig = { + if (!isSecondaryBuild() || process.env.HOUDINI_SECONDARY_BUILD === 'ssr') { + if (!devServer) { + conf.base = '/assets' + } + + conf.build = { rollupOptions: { output: { - entryFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].js', + entryFileNames: '[name].js', }, + external: ['react-streaming/server'], }, } await fs.mkdirp(config.compiledAssetsDir) - rollupConfig.outDir = config.compiledAssetsDir - rollupConfig.rollupOptions!.input = {} + conf.build!.rollupOptions!.input = { + 'entries/app': routerConventions.app_component_path(config), + 'entries/adapter': routerConventions.adapter_config_path(config), + } // every page in the manifest is a new entry point for vite for (const [id, page] of Object.entries(manifest.pages)) { - rollupConfig.rollupOptions!.input[ + console.log(page.id, page.queries) + conf.build!.rollupOptions!.input[ `pages/${id}` ] = `virtual:houdini/pages/${page.id}@${page.queries}.jsx` } // every artifact asset needs to be bundled individually for (const artifact of manifest.artifacts) { - rollupConfig.rollupOptions!.input[ + conf.build!.rollupOptions!.input[ `artifacts/${artifact}` ] = `virtual:houdini/artifacts/${artifact}.js` } + + // the SSR build has a different output + if (process.env.HOUDINI_SECONDARY_BUILD !== 'ssr') { + conf.build!.outDir = config.compiledAssetsDir + } } return { @@ -80,7 +100,7 @@ export default { '~/*': path.join(config.projectRoot, 'src', '*'), }, }, - build: rollupConfig, + ...conf, } }, @@ -98,6 +118,27 @@ export default { await writeTsconfig(houdiniConfig) }, + 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...') + + process.env.HOUDINI_SECONDARY_BUILD = 'ssr' + // in order to build the server-side of the application, we need to + // treat every file as an independent entry point and disable + await build({ + build: { + ssr: true, + outDir: path.join(config.rootDir, 'build', 'ssr'), + }, + }) + process.env.HOUDINI_SECONDARY_BUILD = 'false' + }, + async load(id, { config }) { // we only care about the virtual modules that generate if (!id.startsWith('virtual:houdini')) { @@ -115,7 +156,6 @@ export default { if (which === 'pages') { const [id, query_names] = arg.split('@') const queries = query_names ? query_names.split(',') : [] - return ` import { hydrateRoot } from 'react-dom/client'; import App from '$houdini/plugins/houdini-react/units/render/App' @@ -128,7 +168,6 @@ export default { // if there is pending data (or artifacts) then we should prime the caches let initialData = {} let initialArtifacts = {} - if (!window.__houdini__cache__) { window.__houdini__cache__ = new Cache() window.__houdini__hydration__layer__ = window.__houdini__cache__._internal_unstable.storage.createLayer(true) @@ -163,16 +202,24 @@ export default { }) } - - // hydrate the cache with the information from the initial payload window.__houdini__cache__?.hydrate( window.__houdini__initial__cache__, window.__houdini__hydration__layer__ ) + // get the initial url from the window + const url = window.location.pathname + + const app = + // hydrate the application for interactivity - hydrateRoot(document, ) + hydrateRoot(document, app) ` } @@ -197,9 +244,15 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca // render that we will use in production. This means that we need to // capture the request before vite's dev server processes it. async configureServer(server) { + devServer = true 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( @@ -209,16 +262,16 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca ) )) as { default: RouterManifest } - // 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 !== localApiEndpoint(server.houdiniConfig.configFile)) { - return next() - } + const [match] = find_match(router_manifest, req.url) + + if ( + !match && + !internalRoutes(server.houdiniConfig.configFile).find((route) => + req.url?.startsWith(route) + ) + ) { + next() + return } // its worth loading the project manifest @@ -229,34 +282,81 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca if (project_manifest.local_schema) { schema = await loadLocalSchema(server.houdiniConfig) } - // import the schema - const serverAdapter: ( - args: Omit[0], 'on_render'> - ) => ReturnType = ( - await server.ssrLoadModule( - routerConventions.server_adapter_path(server.houdiniConfig) + - '?t=' + - new Date().getTime() - ) - ).default + + // import the yoga server let yoga: YogaServer | null = null if (project_manifest.local_yoga) { - const yogaPath = path.join( - server.houdiniConfig.localApiDir, - '+yoga?t=' + new Date().getTime() - ) + const yogaPath = path.join(server.houdiniConfig.localApiDir, '+yoga') yoga = (await server.ssrLoadModule(yogaPath)) as YogaServer } - // call the adapter with the latest information - await serverAdapter({ + // load the render factory + const { createServerAdapter } = (await server.ssrLoadModule( + routerConventions.server_adapter_path(server.houdiniConfig) + )) as { createServerAdapter: any } + + const requestHeaders = new Headers() + for (const header of Object.entries(req.headers ?? {})) { + requestHeaders.set(header[0], header[1] as string) + } + + // 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 + ) + + for (const [key, value] of Object.entries(req.headers)) { + request.headers.set(key, value as string) + } + + // instantiate the handler and invoke it with a mocked response + const result: Response = await createServerAdapter({ schema, yoga, - assetPrefix: '/virtual:houdini', production: false, manifest: router_manifest, + graphqlEndpoint: localApiEndpoint(server.houdiniConfig.configFile), + assetPrefix: '/virtual:houdini', pipe: res, - })(req, res) + documentPremable: ``, + })(request) + if (result && result.status === 404) { + next() + } + // 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 + } + for (const header of Object.entries(result.headers ?? {})) { + res.setHeader(header[0], header[1]) + } + res.write(await result.text()) + res.end() + } }) }, } as PluginHooks['vite'] + +// function: +function getBody(request: Connect.IncomingMessage): Promise { + return new Promise((resolve) => { + const bodyParts: Uint8Array[] = [] + let body + request + .on('data', (chunk: Uint8Array) => { + bodyParts.push(chunk) + }) + .on('end', () => { + body = Buffer.concat(bodyParts).toString() + resolve(body) + }) + }) +} diff --git a/packages/houdini-react/src/runtime/index.tsx b/packages/houdini-react/src/runtime/index.tsx index 3490083d3..98dc43f80 100644 --- a/packages/houdini-react/src/runtime/index.tsx +++ b/packages/houdini-react/src/runtime/index.tsx @@ -1,20 +1,11 @@ import type { Cache } from '$houdini/runtime/cache/cache' -import { DocumentStore } from '$houdini/runtime/client' -import { LRUCache } from '$houdini/runtime/lib/lru' -import { GraphQLObject, GraphQLVariables, QueryArtifact } from '$houdini/runtime/lib/types' -import React from 'react' import client from './client' import manifest from './manifest' -import { - SuspenseCache, - suspense_cache, - Router as RouterImpl, - RouterContextProvider, - type PendingCache, -} from './routing' +import { Router as RouterImpl, RouterCache, RouterContextProvider } from './routing' export * from './hooks' +export { router_cache } from './routing' export function Router({ cache, @@ -47,65 +38,3 @@ export function Router({ ) } - -type RouterCache = { - artifact_cache: SuspenseCache - component_cache: SuspenseCache<(props: any) => React.ReactElement> - data_cache: SuspenseCache> - last_variables: LRUCache - pending_cache: PendingCache -} - -export function router_cache({ - pending_queries = [], - artifacts = {}, - components = {}, - initialData = {}, - initialArtifacts = {}, -}: { - pending_queries?: string[] - artifacts?: Record - components?: Record React.ReactElement> - initialData?: Record> - initialArtifacts?: Record -} = {}): RouterCache { - const result: RouterCache = { - artifact_cache: suspense_cache(initialArtifacts), - component_cache: suspense_cache(), - data_cache: suspense_cache(initialData), - pending_cache: suspense_cache(), - last_variables: suspense_cache(), - } - - // we need to fill each query with an externally resolvable promise - for (const query of pending_queries) { - result.pending_cache.set(query, signal_promise()) - } - - for (const [name, artifact] of Object.entries(artifacts)) { - result.artifact_cache.set(name, artifact) - } - - for (const [name, component] of Object.entries(components)) { - result.component_cache.set(name, component) - } - - return result -} - -// a signal promise is a promise is used to send signals by having listeners attach -// actions to the then() -function signal_promise(): Promise & { resolve: () => void; reject: () => void } { - let resolve: () => void = () => {} - let reject: () => void = () => {} - const promise = new Promise((res, rej) => { - resolve = res - reject = rej - }) - - return { - ...promise, - resolve, - reject, - } -} diff --git a/packages/houdini-react/src/runtime/routing/Router.tsx b/packages/houdini-react/src/runtime/routing/Router.tsx index 6679dfaac..ff86e849b 100644 --- a/packages/houdini-react/src/runtime/routing/Router.tsx +++ b/packages/houdini-react/src/runtime/routing/Router.tsx @@ -10,7 +10,7 @@ import React from 'react' import { useStream } from 'react-streaming' import { useDocumentStore } from '../hooks/useDocumentStore' -import { SuspenseCache } from './cache' +import { SuspenseCache, suspense_cache } from './cache' const PreloadWhich = { component: 'component', @@ -604,3 +604,65 @@ function usePreload({ preload }: { preload: (url: string, which: PreloadWhichVal } }, []) } + +export type RouterCache = { + artifact_cache: SuspenseCache + component_cache: SuspenseCache<(props: any) => React.ReactElement> + data_cache: SuspenseCache> + last_variables: LRUCache + pending_cache: PendingCache +} + +export function router_cache({ + pending_queries = [], + artifacts = {}, + components = {}, + initialData = {}, + initialArtifacts = {}, +}: { + pending_queries?: string[] + artifacts?: Record + components?: Record React.ReactElement> + initialData?: Record> + initialArtifacts?: Record +} = {}): RouterCache { + const result: RouterCache = { + artifact_cache: suspense_cache(initialArtifacts), + component_cache: suspense_cache(), + data_cache: suspense_cache(initialData), + pending_cache: suspense_cache(), + last_variables: suspense_cache(), + } + + // we need to fill each query with an externally resolvable promise + for (const query of pending_queries) { + result.pending_cache.set(query, signal_promise()) + } + + for (const [name, artifact] of Object.entries(artifacts)) { + result.artifact_cache.set(name, artifact) + } + + for (const [name, component] of Object.entries(components)) { + result.component_cache.set(name, component) + } + + return result +} + +// a signal promise is a promise is used to send signals by having listeners attach +// actions to the then() +function signal_promise(): Promise & { resolve: () => void; reject: () => void } { + let resolve: () => void = () => {} + let reject: () => void = () => {} + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + return { + ...promise, + resolve, + reject, + } +} diff --git a/packages/houdini/src/adapter/index.tsx b/packages/houdini/src/adapter/index.tsx index 565d352ed..3915ce07d 100644 --- a/packages/houdini/src/adapter/index.tsx +++ b/packages/houdini/src/adapter/index.tsx @@ -5,6 +5,6 @@ export const endpoint: string = '' export let createServerAdapter: ( args: Omit< Parameters[0], - 'on_render' | 'manifest' | 'yoga' | 'schema' | 'graphqlEndpoint' + 'on_render' | 'manifest' | 'yoga' | 'schema' | 'graphqlEndpoint' | 'client' > ) => ReturnType diff --git a/packages/houdini/src/lib/config.ts b/packages/houdini/src/lib/config.ts index 3de5fcb25..1cd8ee853 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') + return path.join(this.projectRoot, 'dist', 'assets') } get definitionsDocumentsPath() { diff --git a/packages/houdini/src/lib/router/conventions.ts b/packages/houdini/src/lib/router/conventions.ts index 1124775d4..68f5832f9 100644 --- a/packages/houdini/src/lib/router/conventions.ts +++ b/packages/houdini/src/lib/router/conventions.ts @@ -18,6 +18,10 @@ export function adapter_config_path(config: Config, base?: string) { return path.join(units_dir(config, base), 'render', 'config.js') } +export function vite_render_path(config: Config, base?: string) { + return path.join(units_dir(config, base), 'render', 'vite.js') +} + export function app_component_path(config: Config, base?: string) { return path.join(units_dir(config, base), 'render', 'App.jsx') } diff --git a/packages/houdini/src/lib/router/server.ts b/packages/houdini/src/lib/router/server.ts index f485f902f..103bad9f2 100644 --- a/packages/houdini/src/lib/router/server.ts +++ b/packages/houdini/src/lib/router/server.ts @@ -2,16 +2,26 @@ import type * as graphql from 'graphql' import path from 'node:path' import type { Config } from '../config' +import { type ConfigFile, localApiEndpoint } from '../types' export function isSecondaryBuild() { - return process.env.HOUDINI_SCHEMA_BUILD === 'true' + return process.env.HOUDINI_SECONDARY_BUILD && process.env.HOUDINI_SECONDARY_BUILD !== 'false' +} + +export function internalRoutes(config: ConfigFile): string[] { + const routes = [localApiEndpoint(config)] + if (config.router?.auth && 'redirect' in config.router.auth) { + routes.push(config.router.auth.redirect) + } + + return routes } export async function buildLocalSchema(config: Config): Promise { // load the current version of vite const { build } = await import('vite') - process.env.HOUDINI_SCHEMA_BUILD = 'true' + process.env.HOUDINI_SECONDARY_BUILD = 'true' // build the schema somewhere we can import from await build({ @@ -36,7 +46,7 @@ export async function buildLocalSchema(config: Config): Promise { }, }) - process.env.HOUDINI_SCHEMA_BUILD = 'false' + process.env.HOUDINI_SECONDARY_BUILD = 'false' } export async function loadLocalSchema(config: Config): Promise { diff --git a/packages/houdini/src/lib/types.ts b/packages/houdini/src/lib/types.ts index 1c5d254c5..ee15fd822 100644 --- a/packages/houdini/src/lib/types.ts +++ b/packages/houdini/src/lib/types.ts @@ -293,7 +293,7 @@ export type PluginHooks = { houdiniConfig?: Config ) => void | Promise - closeBundle?: (this: PluginContext) => void | Promise + closeBundle?: (this: PluginContext, config: Config) => void | Promise configResolved?: ObjectHook<(this: void, config: ResolvedConfig) => void | Promise> diff --git a/packages/houdini/src/runtime/router/match.test.ts b/packages/houdini/src/runtime/router/match.test.ts index df4f5693c..f0022225e 100644 --- a/packages/houdini/src/runtime/router/match.test.ts +++ b/packages/houdini/src/runtime/router/match.test.ts @@ -1,6 +1,7 @@ import { test, expect, describe } from 'vitest' -import { exec, parse_page_pattern } from './match' +import { exec, find_match, parse_page_pattern } from './match' +import type { RouterManifest } from './types' describe('route_params', () => { const testCases = [ @@ -104,3 +105,41 @@ describe('route_params', () => { }) }) }) + +describe('find_match parse and match', async function () { + // every test case needs to be an collection of urls and the expected match + const table = [ + { + name: 'root match is last', + urls: ['/foo', '/'], + expected: '/foo', + }, + ] + + for (const { name, urls, expected } of table) { + test(name, async function () { + // build up the list of patterns + const patterns = urls.map((url) => parse_page_pattern(url)) + + // wrap the patterns in a mocked manifest + const manifest: RouterManifest = { + pages: patterns.reduce( + (pages, pattern) => ({ + ...pages, + [pattern.page_id]: { + id: pattern.page_id, + pattern: pattern.pattern, + // the params used to execute the pattern and extract the variables + params: pattern.params, + }, + }), + {} + ), + } + + // find the match + const [match] = find_match(manifest, expected) + expect(match?.id).toEqual(expected) + }) + } +}) diff --git a/packages/houdini/src/runtime/router/match.ts b/packages/houdini/src/runtime/router/match.ts index 36090b433..cb48272f2 100644 --- a/packages/houdini/src/runtime/router/match.ts +++ b/packages/houdini/src/runtime/router/match.ts @@ -182,7 +182,7 @@ export function exec(match: RegExpMatchArray, params: RouteParam[]) { let buffered = '' - for (let i = 0; i < params.length; i += 1) { + for (let i = 0; i < (params || []).length; i += 1) { const param = params[i] let value = values[i] diff --git a/packages/houdini/src/runtime/router/server.ts b/packages/houdini/src/runtime/router/server.ts index 281cbc6fa..9da8def44 100644 --- a/packages/houdini/src/runtime/router/server.ts +++ b/packages/houdini/src/runtime/router/server.ts @@ -1,45 +1,40 @@ import { createServerAdapter as createAdapter } from '@whatwg-node/server' import { type GraphQLSchema, parse, execute } from 'graphql' import { createYoga } from 'graphql-yoga' -import type { IncomingMessage, ServerResponse } from 'node:http' -// @ts-ignore -import client from '../../../src/+client' -// @ts-ignore +import type { HoudiniClient } from '../client' import { localApiSessionKeys, localApiEndpoint, getCurrentConfig } from '../lib/config' import { find_match } from './match' -// @ts-ignore import { get_session, handle_request } from './session' import type { RouterManifest, RouterPageManifest, YogaServerOptions } from './types' // load the plugin config const config_file = getCurrentConfig() const session_keys = localApiSessionKeys(config_file) -const graphqlEndpoint = localApiEndpoint(config_file) -export const serverAdapterFactory = ({ +export function _serverHandler({ schema, yoga, + client, production, manifest, + graphqlEndpoint, on_render, - pipe, - assetPrefix, }: { schema?: GraphQLSchema | null yoga?: ReturnType | null + client: HoudiniClient + production: boolean + manifest: RouterManifest | null assetPrefix: string - production?: boolean - pipe?: ServerResponse + graphqlEndpoint: string on_render: (args: { url: string match: RouterPageManifest | null manifest: RouterManifest session: App.Session - pipe?: ServerResponse - }) => Response | Promise - manifest: RouterManifest | null -} & Omit): ReturnType => { + }) => Response | Promise | undefined +} & Omit) { if (schema && !yoga) { yoga = createYoga({ schema, @@ -48,19 +43,16 @@ export const serverAdapterFactory = ({ }) } - // @ts-ignore: schema is defined dynamically if (schema) { - // @ts-ignore: graphqlEndpoint is defined dynamically client.registerProxy(graphqlEndpoint, async ({ query, variables, session }) => { // get the parsed query const parsed = parse(query) - // @ts-ignore: schema is defined dynamically return await execute(schema, parsed, null, session, variables) }) } - return createAdapter(async (request) => { + return async (request: Request) => { if (!manifest) { return new Response( "Adapter did not provide the project's manifest. Please open an issue on github.", @@ -98,7 +90,6 @@ export const serverAdapterFactory = ({ match, session: await get_session(request.headers, session_keys), manifest, - pipe, }) if (rendered) { return rendered @@ -106,7 +97,13 @@ export const serverAdapterFactory = ({ // if we got this far its not a page we recognize return new Response('404', { status: 404 }) - }) + } +} + +export const serverAdapterFactory = ( + args: Parameters[0] +): ReturnType => { + return createAdapter(_serverHandler(args)) } export type ServerAdapterFactory = typeof serverAdapterFactory diff --git a/packages/houdini/src/vite/houdini.ts b/packages/houdini/src/vite/houdini.ts index 3e6e660cf..5558a8a9b 100644 --- a/packages/houdini/src/vite/houdini.ts +++ b/packages/houdini/src/vite/houdini.ts @@ -18,6 +18,7 @@ import { let config: Config let viteConfig: ResolvedConfig let viteEnv: ConfigEnv +let devServer = false export default function Plugin(opts: PluginConfig = {}): VitePlugin { return { @@ -80,16 +81,16 @@ 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') { - return - } - for (const plugin of config.plugins) { if (typeof plugin.vite?.closeBundle !== 'function') { continue } - await plugin.vite!.closeBundle.call(this) + 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 @@ -120,7 +121,7 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { publicBase: viteConfig.base, outDir: config.routerBuildDirectory, manifest, - adapterPath: '../$houdini/plugins/houdini-react/units/render/config.js', + adapterPath: './assets/ssr/entries/adapter', }) // if there is a public directory at the root of the project, @@ -148,9 +149,9 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { } // we need to generate the runtime if we are building in production - if (viteEnv.mode === 'production' && !isSecondaryBuild()) { + if (!devServer && !isSecondaryBuild()) { // make sure we have an up-to-date schema - if (config.localSchema) { + if (config.localSchema && !config.schema) { config.schema = await loadLocalSchema(config) } @@ -183,6 +184,8 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { }, async configureServer(server) { + devServer = true + for (const plugin of config.plugins) { if (typeof plugin.vite?.configureServer !== 'function') { continue @@ -195,7 +198,7 @@ export default function Plugin(opts: PluginConfig = {}): VitePlugin { } // if there is a local schema we need to use that when generating - if (config.localSchema) { + if (config.localSchema && !config.schema) { config.schema = await loadLocalSchema(config) }