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)
}