Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
aa75685
basid plugin setup working
siddarthvader Jun 11, 2026
9c936da
removed netwrok panel
siddarthvader Jun 11, 2026
10ab278
leanup
siddarthvader Jun 11, 2026
19dd13e
cleanup
siddarthvader Jun 11, 2026
5908c77
fixed typo
siddarthvader Jun 16, 2026
f9bd5b3
Squash merge main into 611-devtool-react
siddarthvader Jun 16, 2026
f0526e5
Merge branch 'main' into 611-devtool-react
siddarthvader Jun 16, 2026
2a2319e
moved to houdini-runtime
siddarthvader Jun 16, 2026
a36c2f0
improved config options and now copyibg css files into runtime .houdini
siddarthvader Jun 16, 2026
38509f2
formatting and type for css importq
siddarthvader Jun 16, 2026
e3bd900
added changeset
siddarthvader Jun 16, 2026
aaa479d
formatting
siddarthvader Jun 16, 2026
0db21e2
added refrences types
siddarthvader Jun 16, 2026
41115b5
formatting fixed
siddarthvader Jun 16, 2026
e9631b9
changeset
siddarthvader Jun 16, 2026
4505b9f
Squash merge main into 611-devtool-react
siddarthvader Jun 30, 2026
466c59d
Merge branch 'main' into 611-devtool-react
siddarthvader Jun 30, 2026
a659089
feat: enhance Houdini devtools request UI
siddarthvader Jun 30, 2026
ed44fb3
style: auto-format
github-actions[bot] Jun 30, 2026
35fd236
fix: show devtools request dot color by status
siddarthvader Jun 30, 2026
d4afba5
fix: handle missing devtools plugin context
siddarthvader Jun 30, 2026
da80d8f
Merge branch 'main' into 611-devtool-react
AlecAivazis Jul 12, 2026
0cd0551
devtools: tree-shakeable codegen entry, 'always' mode, leaner store, …
AlecAivazis Jul 14, 2026
324a00d
docs: clarify devtools defaults to dev with no config needed
AlecAivazis Jul 14, 2026
5e5c27c
Merge remote-tracking branch 'origin/main' into 611-devtool-react
AlecAivazis Jul 14, 2026
76ccc13
devtools: guard cleanup against null context from stores that never f…
AlecAivazis Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plugin-config-typing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"houdini": patch
---

Allow locally-defined plugins referenced by path in the `plugins` config while keeping type checking for known plugin options.
5 changes: 5 additions & 0 deletions .changeset/slick-llamas-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"houdini-react": minor
---

Add a devtools overlay for inspecting client-side requests, controlled by the `devtools` plugin config value (`'dev'`, `'always'`, or `'never'`).
43 changes: 43 additions & 0 deletions docs/react/05-guides/08-devtools.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: Devtools
description: Inspecting Houdini's client-side requests with the devtools overlay
---

# Devtools

Houdini ships with a devtools overlay that records every client-side request our
application sends: queries, mutations, and subscriptions. It shows each document's
lifecycle, its variables, the data it resolved with, whether it was served from the
cache or the network, and any errors along the way. During development a small hat
appears in the corner of the page; clicking it opens the panel.

The overlay renders inside a shadow root, so its styles never leak into our
application (and ours never leak into it).

## Configuration

The overlay is enabled during development by default and dropped from production
builds entirely, so there is nothing to configure for the usual workflow. If you want
different behavior, the `devtools` value in the plugin config controls when the
overlay is available:

```javascript title="houdini.config.js"
export default {
plugins: {
'houdini-react': {
devtools: 'always'
}
}
}
```

There are three modes:

- `dev` (the default): the overlay is only available during development. Production
builds drop it entirely, so it adds nothing to the bundle we ship.
- `always`: the overlay is available in development and in production. This is handy
for staging environments or debugging a deployed app.
- `never`: the overlay is disabled and never bundled.

Since the mode is resolved during code generation, changing it requires a fresh
`generate` (the vite plugin takes care of this on the next dev server start or build).
7 changes: 4 additions & 3 deletions e2e/react/houdini.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @ts-ignore
/// <references types="houdini-react">
/// <reference types="houdini-react" />
import type { ConfigFile } from 'houdini'

const config: ConfigFile = {
Expand Down Expand Up @@ -47,7 +46,9 @@ const config: ConfigFile = {
},

plugins: {
'houdini-react': {},
'houdini-react': {
devtools: 'always',
},
'./plugins/node-plugin.mjs': {},
},

Expand Down
3 changes: 3 additions & 0 deletions e2e/react/src/routes/devtools/+page.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query DevtoolsQuery {
hello
}
32 changes: 32 additions & 0 deletions e2e/react/src/routes/devtools/+page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { graphql, useMutation } from '$houdini'

import type { PageProps } from './$types'

export default function ({ DevtoolsQuery }: PageProps) {
const [update] = useMutation(
graphql(`
mutation DevtoolsUpdateMutation($snapshot: String!, $id: ID!, $name: String!) {
updateUser(id: $id, snapshot: $snapshot, name: $name) {
id
name
}
}
`)
)

return (
<>
<div id="result">{DevtoolsQuery.hello}</div>
<button
id="trigger-mutation"
onClick={() =>
update({
variables: { snapshot: 'devtools', id: '1', name: 'Devtools User' },
})
}
>
mutate
</button>
</>
)
}
26 changes: 26 additions & 0 deletions e2e/react/src/routes/devtools/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, test } from '@playwright/test'

import { routes } from '~/utils/routes'
import { goto } from '~/utils/testsHelper.js'

test('devtools overlay captures client requests', async ({ page }) => {
await goto(page, routes.devtools)

// make sure the page rendered before interacting
await expect(page.locator('#result')).toHaveText('Hello World! // From Houdini!')

// fire a client-side request for the overlay to capture
await page.click('#trigger-mutation')

// the overlay mounts shortly after hydration; playwright locators pierce the open shadow root
const overlay = page.locator('#houdini-devtools-overlay')
await overlay.locator('.hdt-trigger').click()

// the mutation shows up in the request list
const row = overlay.locator('.hdt-row', { hasText: 'DevtoolsUpdateMutation' })
await expect(row).toBeVisible()

// selecting it shows its variables in the default tab
await row.click()
await expect(overlay.locator('.hdt-pre')).toContainText('Devtools User')
})
1 change: 1 addition & 0 deletions e2e/react/src/utils/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const routes = {
api: '/_api',
hello: '/hello-world',
devtools: '/devtools',
use_query: '/use-query',
use_query_rerender: '/use-query-rerender',
use_query_reactivity: '/use-query-reactivity',
Expand Down
5 changes: 4 additions & 1 deletion packages/_scripts/buildNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,17 @@ export async function build({ outDir, packages, source, bundle = true, plugin, c

// copy runtime files as raw .ts/.tsx files without compilation
export async function copyRuntimeFiles({ outDir, source }) {
// find all .ts, .tsx, and .json files in the runtime directory, excluding test files
// find all .ts, .tsx, .css, and .json files in the runtime directory, excluding test files
// (tsconfig.json and similar config files need to be included for Go plugins that read from runtimeDir)
const files = (
await Promise.all([
glob(path.join(source, '**/*.ts*').replaceAll('\\', '/'), {
nodir: true,
ignore: ['**/*.test.*', '**/test.ts'],
}),
glob(path.join(source, '**/*.css').replaceAll('\\', '/'), {
nodir: true,
}),
glob(path.join(source, '**/*.json').replaceAll('\\', '/'), {
nodir: true,
ignore: ['**/package.json'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <references types="houdini-react">
/// <reference types="houdini-react" />
/** @type {import('houdini').ConfigFile} */
const config = {'CONFIG_FILE'
plugins: {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-houdini/templates/react/houdini.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <references types="houdini-react">
/// <reference types="houdini-react" />
/** @type {import('houdini').ConfigFile} */
const config = {'CONFIG_FILE'
plugins: {
Expand Down
19 changes: 19 additions & 0 deletions packages/houdini-react/package/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
declare module 'houdini' {
// @ts-ignore
interface HoudiniPluginConfig {
'houdini-react': HoudiniReactConfig
}
}

export type HoudiniReactConfig = {
/**
* Controls when the Houdini React devtools overlay is shown.
*
* - `dev`: only during development (production builds drop the overlay entirely)
* - `always`: in development and production
* - `never`: never (the overlay is never bundled)
*
* @default 'dev'
*/
devtools?: 'dev' | 'always' | 'never'
}
38 changes: 37 additions & 1 deletion packages/houdini-react/plugin/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package plugin

import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sort"
Expand All @@ -28,7 +29,21 @@ func (p *HoudiniReact) TransformRuntime(ctx context.Context, fp string, content
}
}

switch fp {
switch filepath.ToSlash(fp) {
case "devtools/index.ts":
// the devtools entry is generated from the plugin config so that disabled modes
// never import the overlay and bundlers can drop it entirely
switch p.devtoolsMode(ctx) {
case "never":
return "export default null\n", nil
case "always":
return "import plugin from './plugin.js'\n\nexport default plugin\n", nil
default:
// 'dev': keep the import.meta.env.DEV guard from the static file so
// production builds tree-shake the overlay
return content, nil
}

case "client.ts":
projectConfig, err := p.DB.ProjectConfig(ctx)
if err != nil {
Expand Down Expand Up @@ -80,6 +95,27 @@ func (p *HoudiniReact) TransformRuntime(ctx context.Context, fp string, content
return content, nil
}

// devtoolsMode reads the `devtools` value from the plugin's config in houdini.config,
// defaulting to 'dev'. It queries the plugins table directly instead of going through
// PluginConfig so a missing row (nothing configured) falls back cleanly.
func (p *HoudiniReact) devtoolsMode(ctx context.Context) string {
var configJSON string
_ = p.DB.StepQuery(ctx, `SELECT config FROM plugins WHERE name = 'houdini-react'`, nil, func(q plugins.Row) {
configJSON = q.ColumnText(0)
})
if configJSON == "" {
return "dev"
}

var config struct {
Devtools string `json:"devtools"`
}
if err := json.Unmarshal([]byte(configJSON), &config); err != nil || config.Devtools == "" {
return "dev"
}
return config.Devtools
}

// UpdateIndexFiles injects typed graphql() overloads into the core runtime index.ts.
// For each visible document it adds an import of the artifact type and an overload that
// maps the document's literal template string to { artifact: ${name}$artifact }.
Expand Down
73 changes: 73 additions & 0 deletions packages/houdini-react/plugin/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,79 @@ func TestTransformRuntimeLoginURL(t *testing.T) {
})
}

// devtoolsIndexStub mirrors the static devtools/index.ts entry that ships with the
// runtime: the 'dev' default that gates the overlay behind import.meta.env.DEV.
const devtoolsIndexStub = "import plugin from './plugin.js'\n\n" +
"// @ts-ignore: vite provides import.meta.env\n" +
"export default import.meta.env.DEV ? plugin : null\n"

// TestTransformRuntimeDevtools verifies that the devtools entry is generated from the
// plugin's `devtools` config value: 'dev' (and no config at all) keeps the static
// import.meta.env.DEV guard, 'always' exports the plugin unconditionally, and 'never'
// exports null without importing the overlay.
func TestTransformRuntimeDevtools(t *testing.T) {
tests.RunTable(t, tests.Table[coreConfig.PluginConfig, *plugin.HoudiniReact]{
Schema: `type Query { id: ID }`,

SetupTest: func(t *testing.T, p *plugin.HoudiniReact, test tests.Test[coreConfig.PluginConfig]) {
config, _ := test.Extra["config"].(string)
if config == "" {
return
}
ctx := context.Background()
conn, err := p.DB.Take(ctx)
require.NoError(t, err)
defer p.DB.Put(conn)
stmt, err := conn.Prepare(
`INSERT INTO plugins (name, port, hooks, config) VALUES ('houdini-react', 0, '[]', $config)`,
)
require.NoError(t, err)
require.NoError(t, p.DB.ExecStatement(stmt, map[string]any{"config": config}))
stmt.Finalize()
},

PerformTest: func(t *testing.T, p *plugin.HoudiniReact, test tests.Test[coreConfig.PluginConfig]) {
got, err := p.TransformRuntime(context.Background(), "devtools/index.ts", devtoolsIndexStub)
require.NoError(t, err)
require.Equal(t, test.Extra["expected"].(string), got)
},

Tests: []tests.Test[coreConfig.PluginConfig]{
{
Name: "defaults to the dev guard when no plugin config exists",
Pass: true,
Extra: map[string]any{
"expected": devtoolsIndexStub,
},
},
{
Name: "keeps the dev guard for devtools: dev",
Pass: true,
Extra: map[string]any{
"config": `{"devtools":"dev"}`,
"expected": devtoolsIndexStub,
},
},
{
Name: "exports the plugin unconditionally for devtools: always",
Pass: true,
Extra: map[string]any{
"config": `{"devtools":"always"}`,
"expected": "import plugin from './plugin.js'\n\nexport default plugin\n",
},
},
{
Name: "exports null for devtools: never",
Pass: true,
Extra: map[string]any{
"config": `{"devtools":"never"}`,
"expected": "export default null\n",
},
},
},
})
}

// indexStub mirrors the real core runtime index.ts: it has leading imports and
// exports before the generic graphql() declaration, so tests verify that
// overloads land immediately before the marker rather than at the file top.
Expand Down
25 changes: 15 additions & 10 deletions packages/houdini-react/runtime/clientPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import type { ClientPlugin } from 'houdini/runtime/client'

import devtools from './devtools/index.js'

const plugin: () => ClientPlugin = () => () => {
return {
start(ctx, { next }) {
next({
...ctx,
cacheParams: {
...ctx.cacheParams,
serverSideFallback: false,
},
})
return [
{
start(ctx, { next }) {
next({
...ctx,
cacheParams: {
...ctx.cacheParams,
serverSideFallback: false,
},
})
},
},
}
devtools,
]
}

export default plugin
18 changes: 18 additions & 0 deletions packages/houdini-react/runtime/devtools/HatLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

export function HatLogo() {
return (
<svg className="hdt-hat" viewBox="0 0 296 283" aria-hidden="true">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M181.432 94.5111C178.081 90.9972 174.15 88.0119 169.785 85.7024C168.358 78.8892 168.565 69.3173 170.727 59.1173C174.631 40.6969 183.436 27.2096 190.388 28.9904C197.344 30.7712 199.817 47.1445 195.909 65.5648C192.932 79.6047 187.113 90.7786 181.432 94.5111M131.212 83.4049C125.321 85.6667 120.03 89.0812 115.641 93.3663C112.012 85.0426 107.636 77.2515 102.524 69.9891C107.54 64.1696 111.531 57.2013 114.5 49.0843C116.11 44.5448 118.495 42.9905 120.686 43.5351C122.276 43.9287 124.665 45.3796 125.658 48.496C129.494 60.282 131.347 71.917 131.212 83.4049ZM147.97 80.3202C145.542 80.3202 143.161 80.507 140.835 80.8608C139.523 64.0821 136.029 49.4023 130.353 36.8213C126.608 28.5253 120.439 15.7972 110.557 13.9647C103.863 12.7245 97.6264 16.8427 92.6615 23.4532C88.3406 29.2051 84.9817 36.8531 83.1333 44.5011C81.0067 53.2899 80.5973 73.6501 86.051 80.682C88.7421 84.1522 92.6297 82.8961 97.7098 76.9176C103.406 85.5355 107.572 93.1557 110.203 99.7741C105.282 106.826 102.408 115.328 102.408 124.479V124.539C116.118 126.649 130.643 127.782 145.681 127.782C162.404 127.782 178.495 126.379 193.536 123.791V123.819C193.437 117.185 191.827 110.9 189.025 105.284C196.152 96.0495 202.691 80.8608 206.507 62.9453C213.233 31.3794 208.975 3.31956 196.995 0.270703C185.014 -2.78213 169.849 20.3367 163.123 51.8986C160.849 62.5637 159.836 72.8273 159.943 81.8625C156.131 80.8569 152.12 80.3202 147.978 80.3202H147.97Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M86.881 233.189C86.8055 235.454 86.7022 237.609 86.567 239.652C86.1059 246.684 90.4427 245.07 97.2003 247.093C131.406 257.321 165.182 257.214 198.516 246.759C204.853 244.776 208.991 246.998 208.991 240.36V233.189C167.757 244.434 127.053 244.434 86.881 233.189M101.477 104.592C82.1825 107.76 69.6651 112.86 69.6651 118.608C69.6651 118.902 69.7009 119.192 69.7645 119.482C71.9547 139.175 72.9445 155.898 72.7299 169.655C71.2512 182.976 73.7395 200.712 74.1052 214.49C36.4894 220.707 16.1212 232.099 18.1366 245.285C21.726 268.737 59.4453 276.62 94.847 279.915C139.153 284.041 159.052 284.113 203.767 279.617C238.243 276.147 273.899 268.062 277.385 245.285C279.405 232.103 259.032 220.711 221.417 214.498C221.858 198.001 222.124 185.599 222.402 176.031C222.959 156.852 223.543 139.179 225.757 119.51C225.825 119.212 225.857 118.914 225.857 118.616C225.857 113.011 213.971 108.03 195.511 104.846C196.771 107.601 197.781 110.491 198.512 113.48C206.892 115.543 211.88 118.131 211.88 120.941C211.88 127.687 183.173 133.157 147.759 133.157C112.345 133.157 83.6374 127.687 83.6374 120.941C83.6374 117.972 89.1985 115.253 98.4445 113.134C99.1918 110.177 100.209 107.319 101.473 104.596L101.477 104.592Z"
/>
</svg>
)
}
Loading
Loading