Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix circular imports, add test script for circular imports #1450

Merged
merged 12 commits into from
Mar 20, 2025
Merged
8 changes: 8 additions & 0 deletions .changeset/bright-baboons-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'houdini-plugin-svelte-global-stores': patch
'houdini-svelte': patch
'houdini-react': patch
'houdini': patch
---

fix internal circular dependencies
7 changes: 7 additions & 0 deletions .madgerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"tests": "vitest",
"tests:ui": "vitest --ui --coverage",
"test": "pnpm run tests",
"check-circular-deps": "pnpm npx madge --circular --extensions ts,tsx ./",
"build:all": "turbo build",
"build": "turbo run build --filter=\"./packages/*\"",
"dev": "turbo dev --filter=\"./packages/*\"",
Expand Down Expand Up @@ -39,6 +40,7 @@
"eslint-plugin-unused-imports": "^3.0.0",
"graphql": "^15.8.0",
"lint-staged": "^12.3.4",
"madge": "^8.0.0",
"prettier": "^2.8.3",
"turbo": "^1.8.8",
"typescript": "^4.9",
Expand Down
10 changes: 2 additions & 8 deletions packages/houdini-react/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,17 @@ import {
type Document,
type Config,
type Plugin,
type ProjectManifest,
} from 'houdini'
import path from 'node:path'
import { loadEnv } from 'vite'

import generate from './codegen'
import { format_router_manifest } from './codegen/router'
import { extractDocuments } from './extract'
import { manifest, setManifest } from './state'
import { transformFile } from './transform'
import vite_plugin from './vite'

export let manifest: ProjectManifest

export const hooks: Plugin = async () => ({
order: 'core',

Expand All @@ -32,7 +30,7 @@ export const hooks: Plugin = async () => ({
// we generate anything
async beforeGenerate({ config }) {
try {
manifest = await load_manifest({ config })
setManifest(await load_manifest({ config }))
} catch (e) {
console.log('something went wrong: ' + (e as Error).message)
return
Expand Down Expand Up @@ -327,7 +325,3 @@ ${
export default plugin('houdini-react', hooks)

export type HoudiniReactPluginConfig = {}

export function setManifest(newManifest: ProjectManifest): void {
manifest = newManifest
}
7 changes: 7 additions & 0 deletions packages/houdini-react/src/plugin/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ProjectManifest } from 'houdini'

export let manifest: ProjectManifest

export function setManifest(newManifest: ProjectManifest): void {
manifest = newManifest
}
2 changes: 1 addition & 1 deletion packages/houdini-react/src/plugin/vite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import React from 'react'
import { build, type BuildOptions, ConfigEnv, type Connect } from 'vite'

import { manifest, setManifest } from '.'
import { manifest, setManifest } from './state'

let viteEnv: ConfigEnv
let devServer = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
} from '$houdini/runtime/lib/types'
import React from 'react'

import { useClient, useLocation, useSession } from '../routing/Router'
import { useClient, useSession, useLocation } from '../routing/hooks'

export function useDocumentHandle<
_Artifact extends QueryArtifact,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { DocumentStore, ObserveParams } from '$houdini/runtime/client'
import type { GraphQLObject } from 'houdini'
import * as React from 'react'

import { useClient } from '../routing'
import { useClient } from '../routing/hooks'
import { useIsMountedRef } from './useIsMounted'

export type UseDocumentStoreParams<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DocumentArtifact, GraphQLVariables, QueryResult } from '$houdini/l
import type { DocumentStore, SendParams } from '$houdini/runtime/client'
import type { GraphQLObject } from 'houdini'

import { useSession } from '../routing/Router'
import { useSession } from '../routing/hooks'
import useDeepCompareEffect from './useDeepCompareEffect'
import { useDocumentStore, type UseDocumentStoreParams } from './useDocumentStore'

Expand Down
2 changes: 1 addition & 1 deletion packages/houdini-react/src/runtime/hooks/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
GraphQLVariables,
} from '$houdini/runtime/lib/types'

import { useSession } from '../routing/Router'
import { useSession } from '../routing/hooks'
import { useDocumentStore } from './useDocumentStore'

export type MutationHandler<_Result, _Input, _Optimistic extends GraphQLObject> = (args: {
Expand Down
115 changes: 8 additions & 107 deletions packages/houdini-react/src/runtime/routing/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import type { Cache } from '$houdini/runtime/cache/cache'
import { DocumentStore, HoudiniClient } from '$houdini/runtime/client'
import configFile from '$houdini/runtime/imports/config'
import { deepEquals } from '$houdini/runtime/lib/deepEquals'
import { LRUCache } from '$houdini/runtime/lib/lru'
import type { LRUCache } from '$houdini/runtime/lib/lru'
import { marshalSelection, marshalInputs } from '$houdini/runtime/lib/scalars'
import { GraphQLObject, GraphQLVariables } from '$houdini/runtime/lib/types'
import { QueryArtifact } from '$houdini/runtime/lib/types'
import type { GraphQLObject, GraphQLVariables } from '$houdini/runtime/lib/types'
import type { QueryArtifact } from '$houdini/runtime/lib/types'
import { find_match } from '$houdini/runtime/router/match'
import type { RouterManifest, RouterPageManifest } from '$houdini/runtime/router/types'
import React from 'react'
import { useContext } from 'react'

import { DocumentHandle, useDocumentHandle } from '../hooks/useDocumentHandle'
import { useDocumentStore } from '../hooks/useDocumentStore'
import { SuspenseCache, suspense_cache } from './cache'
import { suspense_cache, type SuspenseCache } from './cache'

type PageComponent = React.ComponentType<{ url: string }>
import { useDocumentHandle, type DocumentHandle } from '../hooks/useDocumentHandle'
import { useDocumentStore } from '../hooks/useDocumentStore'
import { Context, useRouterContext, useSession, useLocation, LocationContext, type PageComponent, type PendingCache } from './hooks'

const PreloadWhich = {
component: 'component',
Expand Down Expand Up @@ -153,9 +153,6 @@ export function Router({
)
}

// export the location information in context
export const useLocation = () => useContext(LocationContext)

/**
* usePageData is responsible for kicking off the network requests necessary to render the page.
* This includes loading the artifact, the component source, and any query results. This hook
Expand Down Expand Up @@ -514,58 +511,6 @@ export function RouterContextProvider({
)
}

type RouterContext = {
client: HoudiniClient
cache: Cache

// We also need a cache for artifacts so that we can avoid suspending to
// load them if possible.
artifact_cache: SuspenseCache<QueryArtifact>

// We also need a cache for component references so we can avoid suspending
// when we load the same page multiple times
component_cache: SuspenseCache<PageComponent>

// Pages need a way to wait for data
data_cache: SuspenseCache<DocumentStore<GraphQLObject, GraphQLVariables>>

// A way to dedupe requests for a query
ssr_signals: PendingCache

// A way to track the last known good variables
last_variables: LRUCache<GraphQLVariables>

// The current session
session: App.Session

// a function to call that sets the client-side session singletone
setSession: (newSession: Partial<App.Session>) => void
}

export type PendingCache = SuspenseCache<
Promise<void> & { resolve: () => void; reject: (message: string) => void }
>

const Context = React.createContext<RouterContext | null>(null)

export const useRouterContext = () => {
const ctx = React.useContext(Context)

if (!ctx) {
throw new Error('Could not find router context')
}

return ctx
}

export function useClient() {
return useRouterContext().client
}

export function useCache() {
return useRouterContext().cache
}

export function updateLocalSession(session: App.Session) {
window.dispatchEvent(
new CustomEvent<App.Session>('_houdini_session_', {
Expand All @@ -575,56 +520,12 @@ export function updateLocalSession(session: App.Session) {
)
}

export function useSession(): [App.Session, (newSession: Partial<App.Session>) => void] {
const ctx = useRouterContext()

// when we update the session we have to do 2 things. (1) we have to update the local state
// that we will use on the client (2) we have to send a request to the server so that it
// can update the cookie that we use for the session
const updateSession = (newSession: Partial<App.Session>) => {
// clear the data cache so that we refetch queries with the new session (will force a cache-lookup)
ctx.data_cache.clear()

// update the local state
ctx.setSession(newSession)

// figure out the url that we will use to send values to the server
const auth = configFile.router?.auth
if (!auth) {
return
}
const url = 'redirect' in auth ? auth.redirect : auth.url

fetch(url, {
method: 'POST',
body: JSON.stringify(newSession),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
})
}

return [ctx.session, updateSession]
}

export function useCurrentVariables(): GraphQLVariables {
return React.useContext(VariableContext)
return React.useContext(VariableContext)
}

const VariableContext = React.createContext<GraphQLVariables>(null)

const LocationContext = React.createContext<{
pathname: string
params: Record<string, any>
// a function to imperatively navigate to a url
goto: (url: string) => void
}>({
pathname: '',
params: {},
goto: () => {},
})

export function useQueryResult<_Data extends GraphQLObject, _Input extends GraphQLVariables>(
name: string
): [_Data | null, DocumentHandle<any, _Data, _Input>] {
Expand Down
108 changes: 108 additions & 0 deletions packages/houdini-react/src/runtime/routing/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { Cache } from '$houdini/runtime/cache/cache'
import type { DocumentStore, HoudiniClient } from '$houdini/runtime/client'
import configFile from '$houdini/runtime/imports/config'
import type { LRUCache } from '$houdini/runtime/lib/lru'
import type { GraphQLObject, GraphQLVariables, QueryArtifact } from '$houdini/runtime/lib/types'
import { useContext, default as React } from 'react'

import type { SuspenseCache } from './cache'

export type PageComponent = React.ComponentType<{ url: string }>

export type PendingCache = SuspenseCache<
Promise<void> & { resolve: () => void; reject: (message: string) => void }
>

type RouterContext = {
client: HoudiniClient
cache: Cache

// We also need a cache for artifacts so that we can avoid suspending to
// load them if possible.
artifact_cache: SuspenseCache<QueryArtifact>

// We also need a cache for component references so we can avoid suspending
// when we load the same page multiple times
component_cache: SuspenseCache<PageComponent>

// Pages need a way to wait for data
data_cache: SuspenseCache<DocumentStore<GraphQLObject, GraphQLVariables>>

// A way to dedupe requests for a query
ssr_signals: PendingCache

// A way to track the last known good variables
last_variables: LRUCache<GraphQLVariables>

// The current session
session: App.Session

// a function to call that sets the client-side session singletone
setSession: (newSession: Partial<App.Session>) => void
}

export const Context = React.createContext<RouterContext | null>(null)

export const LocationContext = React.createContext<{
pathname: string
params: Record<string, any>
// a function to imperatively navigate to a url
goto: (url: string) => void
}>({
pathname: '',
params: {},
goto: () => {},
})
// export the location information in context
export const useLocation = () => useContext(LocationContext)

export const useRouterContext = () => {
const ctx = React.useContext(Context)

if (!ctx) {
throw new Error('Could not find router context')
}

return ctx
}

export function useClient() {
return useRouterContext().client
}

export function useCache() {
return useRouterContext().cache
}

export function useSession(): [App.Session, (newSession: Partial<App.Session>) => void] {
const ctx = useRouterContext()

// when we update the session we have to do 2 things. (1) we have to update the local state
// that we will use on the client (2) we have to send a request to the server so that it
// can update the cookie that we use for the session
const updateSession = (newSession: Partial<App.Session>) => {
// clear the data cache so that we refetch queries with the new session (will force a cache-lookup)
ctx.data_cache.clear()

// update the local state
ctx.setSession(newSession)

// figure out the url that we will use to send values to the server
const auth = configFile.router?.auth
if (!auth) {
return
}
const url = 'redirect' in auth ? auth.redirect : auth.url

fetch(url, {
method: 'POST',
body: JSON.stringify(newSession),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
})
}

return [ctx.session, updateSession]
}
1 change: 1 addition & 0 deletions packages/houdini-react/src/runtime/routing/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Router'
export * from './hooks'
export { type SuspenseCache, suspense_cache } from './cache'
Loading
Loading