houdini
houdini copied to clipboard
Support SvelteKit's new API
This PR updates houdini's APIs to be compatible with the updates to SvelteKit as described here: https://github.com/sveltejs/kit/discussions/5774. Unfortunately, I won't be able to get the integration tests to pass until the update is released but I'll try to do as much as I can until then.
Just to get some points out of the way for people worried about major breaking changes:
- SvelteKit projects will now need to use the vite plugin defined at
houdini/kit
which replaces the old preprocessor (and the uglyserver.fs.allow
config). But don't worry, there are lots of really cool things that we're getting with this move. - The external document store api for SvelteKit routes is changing. This means that if you rely heavily on the document stores, you will have to update your load functions but as a result you can delete a lot of the code in your components and the overall mental model is a lot simpler (no more passing around
context
) - inline queries are staying the same. At a high level, everything that used to be defined in
context="module"
is now defined in+page.js/ts
.
For additional breaking changes and notes, check out the section at the bottom of this. We know this is a lot of changes but we have been thinking about some of these for awhile and since SvelteKit has forced us to change the API, it seemed like a good time to shift some things around.
Vite Plugin
For kit projects, the preprocessor has moved to houdini/kit
and should be included as a plugin in your vite.config.js
. You should also remove the the fs.server.allow
config since its baked into the plugin. This plugin not only processes our components and javascript files, but it also integrates into vite's dev
script to bring some huge improvements:
- automatically generates your artifacts and stores whenever it detects a change in your source code. if you have configured your own watcher, it might not be necessary anymore.
- poll your api for changes in your schema and regenerate when they are detected. This behavior is customizable between 3 modes: a duration in milliseconds, only pull when the project first starts, and never pull.
Ideally, you shouldn't need to run generate
ever again! It'll even get run when you build your application with vite build
so feel free to delete that generate
script from your package.json
if you have it.
New Store API
The core of this change is that instead of using stores that you import, your component will now get the store from its props. This is the full example for the component-side of a store-driven route:
<script>
/** @type {import('./$types').Data */
export let data: Data;
$: ({ UserInfo } = data);
</script>
{$UserInfo.data.firstName}
Notice there's no more need to thread variables around or that ugly $: browser && store.fetch
.
In order to make this work, your load
functions need to now return instances of the store embedded in the correct key. This can be easily done with the new load_StoreName
functions. The load_
prefix is to make it easy to autocomplete when you are working in your editor:
import { load_UserInfo } from '$houdini'
export async function load(event) {
return {
...await load_UserInfo({ event })
}
}
This means that when adding a query to a route, tasks like adding a store to a load, pulling that store out of data in your component, etc can be auto completed from just starting with load_
in your route's load function. It does make the manual logic that uses a fetch result kind of messy since they have to pull out UserInfo but I think thatβs a rare situation so Iβm in favor of supporting the most people with the clean API and letting the advanced users deal with the complexity.
Also, on a slightly unrelated note: you don't need to pass context
everywhere anymore! that all happens behind the scenes now.
Page Queries
You can now define a +page.gql
file inside of a route directory to automatically opt-into a generated load without having to do anything else:
# src/routes/myProfile/+page.gql
query MyQuery {
viewer {
id
}
}
With that file in place, you can just import and use the MyQuery
store passed to your route and the view will be SSR'd automatically.
Inline Stores
The graphql
template tag can now be used to define your stores in your javascript or svelte files:
<!-- src/routes/myProfile/+page.svelte -->
<script>
// this is only a store. no fetching has happened behind the scenes here
const store = graphql`
query ViewerInfo {
viewer {
id
}
}
`
</script>
id: {$store.data?.viewer.id}
<button onClick={store.fetch} />
Generating Loads For Stores
You can now tell the houdini plugin to generate loads for your stores. To do this, you need to export a houdini_load
variable from your +page.js/ts
file:
// src/routes/myProfile/+page.ts
import { GQL_MyQuery, GQL_Query1, GQL_Query2 } from '$houdini'
export const houdini_load = GQL_MyQuery
// or
export const houdini_load = [ GQL_Query1, GQL_Query2 ]
This can be mixed with the new graphql
tag api to define your queries inside of your javascript files:
import { GQL_MyQuery, graphql. } from '$houdini'
const otherQuery = graphql`
query ViewerInfo {
viewer {
id
}
}
`
export const houdini_load = [ GQL_MyQuery, otherQuery ]
or
// src/routes/myProfile/+page.ts
export const houdini_load = graphql`
query ViewerInfo {
viewer {
id
}
}
`
Breaking Changes / Notes
- configuration for inline queries (variable functions, hooks, etc.) go in
+page.js
- inline fragments have a reversed order for arguments
-
config.sourceGlob
is no longer required and has been deprecated in favor ofconfig.include
which has a default value that covers most projects - added
config.exclude
to filter out files that match theinclude
pattern -
generate --pull-header
is nowgenerate --header
(abbreviated-h
) -
generate --persist-output
is nowgenerate --output
(abbreviated-o
) - added
schemaPullHeaders
config value to specify the headers sent when pulling the schema - svelte projects not using kit should now import the preprocessor from
houdini/svelte-preprocess
Progress
- [ ] missing integration tests
- [ ]
houdini_load
with inline stores next to global stores - [ ]
houdini_load
list vs single - [ ] loadAll
- [ ] page query
- [ ]
- [ ] verify hmr from
houdini_load
- [ ]
this.error
andthis.redirect
in hooks and variable functions (placeholders are there) - [x] provide context whenever possible
- [x]
loadAll(load_foo, { val1: load_bar})
- [x] make sure
event.parent
is accessible everywhere - [x] invert fragment order
- [x] make copy friendlier
- [x] new version message with link to migration guide
- [x] test/handle aliased imports in
+page.ts
files (strip away all non-houdini related things when creating manifest?) - [x] embedded store references
- [x] remove session protection
- [x] update plugin to generate correct loads
- [x] update plugin to update routes
- [x] add pull-schema command
- [x] config option for page query file name
- [x] deprecate
sourceGlob
in favor ofinclude
andexclude
- [x] rename
houdni/vite
to something more svelte specific (went withhoudini/kit
) - [x] support typescript
+page.ts
- [x] support a single or multiple entries in
houdini_load
- [x] check if we can rename
page.gql
to+page.gql
- [x] verify return value props in generated load
- [x]
$$props
in__layout.svelte
error - [x] ~smart typing for
graphql
tag~ template tag overloads aren't supported by typescript yet - [x] try to hide
+page.js
using svemix trick - [x] avoid graphql template tag runtime exception in plugin (actually make import work)
- [x] Preprocessor tasks in plugin
- [x] inline query
- [x]
export const houdini_load = [...]
- [x] define page query in
page.gql
- [x] ~fragment updates~ not needed since reactive expression is the blessed api
- [x] component query inputs
- [x]
graphql
tags produce store references
- [x] update inline functions to use stores directly
- [x] schema watcher
- [x] generate
+page.js
or+page.ts
- [x] support old preprocessor package using vite plugin
- [x] Update init for svelte projects
- [x] move
houdini/preprocess
tohoudini/svelte-preprocess
- [x] update preprocessor tests
Documentation Updates
- [ ] config values
- [ ] typescript
- [ ] schema poll interval
- [ ] schema poll headers
- [ ] no more mention of
context="module"
- [ ]
export const houdini_load
- [ ]
+page.gql
- [ ] pull-schema command
- [ ] setting up your project
- [ ] use
houdini/vite
for kit - [ ] houdini plugin must come before sveltekit
- [ ] vanilla svelte projects need to use
houdini/svelte-preprocess
- [ ] use
- [ ] remove mentions of sapper
- [ ] migration guide
- [ ] The variable API isn't weird any more!!!!
- [ ] hooks no longer return
props
- [ ] inverted fragment order
- [ ] new api for loading stores
- [ ]
loadAll
- [ ]
- [ ] update getting started guide?
- [ ] endpoint fetch is optional
- [ ] context is no longer required
fixes #398, fixes #428, fixes #325, fixes #242
π¦ Changeset detected
Latest commit: 5575eae5c249330c7fdb8d23bdb361a97703486c
The changes in this PR will be included in the next version bump.
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
Really nice that you start working on it parallel to their development. Keep up the great work π
The latest updates on your projects. Learn more about Vercel for Git βοΈ
Name | Status | Preview | Updated |
---|---|---|---|
docs | β Ready (Inspect) | Visit Preview | Aug 25, 2022 at 3:09AM (UTC) |
docs-next | β Ready (Inspect) | Visit Preview | Aug 25, 2022 at 3:09AM (UTC) |
Amazing changes, really hyped for the release!
Wow @fehnomenal! Really appreciate all this help β€οΈ If these are the kinds of comments we're getting we're in a great place already. Was scared the first messages would be something more like "wtf none of this works...." π
Been eagerly checking out this PR for the past five days, great work!
Wow @fehnomenal! Really appreciate all this help :heart: If these are the kinds of comments we're getting we're in a great place already. Was scared the first messages would be something more like "wtf none of this works...." :sweat_smile:
Well, to be honest at first I struggled a bit :sweat_smile: But after reading the docs I am really happy how smooth everything works :star_struck: Well at least when compared with the sveltekit changes :sweat_smile:
Been eagerly checking out this PR for the past five days, great work!
Same, that's why I decided to help getting this released already :joy:
I need a way to pass an authorization
header after logging in. Unfortunately the access token is neither written nor read to/from a cookie by the backend I cannot control (at least not to that extent). So, I have a store that contains the auth session (you guessed it previously it was just $session
).
I propose to add a generic to HoudiniClient<RequestData>
-- the name is meh, but there are already multiple contexts. Maybe simply RequestSession
or Session
? -- which determines the type of a var that is passed to fetchQuery
:
export default new HoudiniClient<{
accessToken: string | null | undefined;
}>(async ({ requestData, fetch, text, variables }) => {
const headers: HeadersInit = {
'content-type': 'application/json',
};
if (requestData.accessToken) {
headers.authorization = `Bearer ${requestData.accessToken}`;
}
const response = await fetch('...', {
method: 'POST',
headers,
body: JSON.stringify({ query: text, variables }),
});
return await response.json();
});
To fill this variable we can use the .init()
method which still exists but does nothing. It could take a () => MaybePromise<RequestData>
function and the client would await
it in sendRequest
(if present) and pass it to the fetchFn
.
Then my +layout.server.ts
and +layout.ts
could look like this:
// +layout.server.ts
import { getLoginUrl } from '$lib/utils/auth-urls';
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = ({ locals, url }) => {
// locals.auth is set by `handle`
if (!locals.auth) {
throw redirect(302, getLoginUrl(url, undefined));
}
return {
auth: {
accessToken: locals.auth.accessToken,
expiresAt: locals.auth.expiresAt.toJSON(),
isAdmin: locals.auth.isAdmin,
},
};
};
// +layout.ts
import houdini from '$lib/houdini';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = ({ data }) => {
houdini.init(() => ({
accessToken: data.auth.accessToken,
}));
};
What do you think? Could this work?
Before I reply to @fehnomenal, I just want to say how crazy it is to see all these reactions and comments. Feels really great to see people are as excited about this as we are.
Okay, @fehnomenal: you're killing it today. I really like this idea.
It seems like a clean answer to a question that's been looming over me for awhile. Now that all of the session data is supposed to come from load results, we needed some way to get data out of a load and into a fetch and this does exactly that. It also addresses a lot of the other issues I was struggling with like how do we keep the value up to date, etc.
I think the only real question is if init
is a good name. Now that "session" is no longer a kit thing, maybe we can take it for ourselves? A quick note about init
in case it sparks any ideas: atm we need some way of identifying when the initial hydration of your application is finished. There is an open issue that is slated to be part of sveltekit's 1.0 but until that's merged we have to use a browser event. This means that every possible route the user might load from needs to have this event listener (even if it doesn't use Houdini). We hide this nasty detail by trying to force users to somehow import the runtime into that first render so we can register the event (this happens in the sveltekit adapter if you're curious). Anyway, the docs say to run client.init
but all that really matters is that they import the runtime, hence why init
is now empty an empty function. My only concern with naming init
something else is that a user that does not have custom session logic won't be inclined to call setSession
or whatever and so they won't import the runtime. Having it be called something generic like init
enforces that it needs to always happen. Maybe we just have 2 different functions for now and then one day init is just no longer "necessary"?
there are already multiple contexts
I was thinking about this last night and I'm pretty sure that we can actually merge most of the contexts into one and simplify that part of the codebase considerably. This is something that constantly confuses us when @jycouet and I are talking so it's worth taking the opportunity to simplify it.
Both of these are things I want to get in before we actually release 0.16.0
but I dont think they should block the first @next
version. That being said, I think the HoudiniClient
import issue and the errors in schema logic are big problems so I'm going to work on fixing those and then cut a version for more people to test out π
edit: I pushed up https://github.com/HoudiniGraphql/houdini/pull/449/commits/b1261e4071025b2717a391175870e0a598868764 to break CI as a reminder to fix this but things seem to be building successfully (verified locally on my machine). I know this was a problem a few days ago but I thought went through and fixed the imports. Maybe you have an old version? Mind giving it a shot with the latest updates?
Btw I want to make this clear: I absolutely love this project and am so grateful for all your work and effort you pour into your project. I absolutely don't want to discourage you but want to get the best GraphQL client for svelte in existence :smiley:
Oh no problem at all! I love the feedback and questions!
I'm a bit busy right now so my reply might be a little quick but just to address the mutation question: t's mostly there for consistency.
I had the same question for myself about query
and subscription
which both basically just return the store. I figured we'd keep it there for this version while we feel out the new store API and maybe just remove them all together in the next API breaking bump. I'm a bit unsure about what to do with your comments atm. I was going to merge this PR and deploy using the changesets but I don't want to lose your feedback. I guess I'll just deploy from this branch manually once I fix the schema issues tonight π
Okay, I finally got some time to sit down and go through all this. I'm pretty certain all of the immediately actionable things have been addressed. Thanks again!
Mutation again
I'm convinced. But I want to take it a step further and also remove query
, paginatedQuery
, and subscription
. Basically, only having the old fragment
and paginatedFragment
functions. The first group of 3 did nothing other than return the store and because of that we were paying the ambiguity cost you were talking about all over the codebase and documentation for literally no reason. it's better if we just leave only one way of working with graphql documents: graphql
tags returning Svelte stores.
So that leaves one question: how does a store that's defined inline opt in/out of a generated load? What I'd like to do is add a new directive to the pile @houdini
that we will use moving forward as the place to define one-off configurations (like #239 and #349). For this situation, I suggest we have @houdini(load: Boolean!)
with a default value of true
. This means a user can turn off the automatic load if they want a lazy inline query.
Route queries
There is a technical limitation here. Stores can't be serialized and so they cannot be embedded in the response from a server endpoint. At the moment, I'd like to leave this as a known limitation and we can explore what the right option is. This is an area of kit that's still very much in flux so I'd like to wait a little bit for the dust to settle before we start to jump through a bunch of hoops to hide that limitation from the user.
The type of AfterLoad is not correctly resolved
You definitely have a version of this release that predates the β
. The AfterLoad
type definition has changed in order for the generated PageData
type to include whatever you add there. I was honestly shocked to see this work (along with a few other things going on in this release tbh π
). Anyway, it now looks like this:
import { type AfterLoadEvent } from '$houdini'
export function afterLoad({ data }: AfterLoadEvent) {
return {
...
}
}
For manually loaded queries: How can I access the returned data inside the load function? The return value is the normal store. Should I just use get(Store).data? It works but get() always feels like a hack...
Moving this conversation to the discussion π
I'm going to work on merging this PR tonight so we can release the version with the changesets bot. I started this discussion for future conversations so we can thread messages and what not: https://github.com/HoudiniGraphql/houdini/discussions/475
Really appreciate all of the care and excitement here. It feels like we're really on the break of the something truly special.
Re Afterload:
Hmm, I got this generated:
type AfterLoadReturn = ReturnType<typeof import('./+page').afterLoad>;
type AfterLoadData = {
GetAvailableWorkshops: GetAvailableWorkshops$result
}
type AfterLoadInput = {
GetAvailableWorkshops: GetAvailableWorkshops$input
}
export type AfterLoadEvent = {
event: LoadEvent
data: AfterLoadData
input: AfterLoadInput
}
export type AfterLoad = AfterLoadFunction<Params, AfterLoadData, AfterLoadInput>
and assumed I can just use AfterLoad
. This works fine:
export const afterLoad = ({ data }: AfterLoadEvent) => {
console.log('afterLoad', data.GetAvailableWorkshops);
};
Maybe remove the generated AfterLoad
? https://github.com/HoudiniGraphql/houdini/blob/dce9397cc93d76dd9adea92f3b8ec5b27dde0bb4/src/cmd/generators/kit/index.ts#L128
Ah well, the AfterLoadEvent['event']
is still untyped. In the end I will avoid using hooks for all these edge cases. With the new load_...
functions writing the load
function manually isn't really that much longer when you need variables and an after hook.
Maybe remove the generated AfterLoad?
Good call!
Ah well, the AfterLoadEvent['event'] is still untyped.
I'm about to push a fix for this up right now (along with the removal of query
and the rest). Just cleaning up the documentation
I love all your recent changes. I just noticed one more thing:
Params is any
I suppose this is because QueryStoreFetchParams
expects two generic arguments but gets only one:
Okay, i finally got everything building and think this is finally ready to merge. Really appreciate everyone's patience. I think you're really gonna like what we've put together π