houdini-core 2.0.0-next.32 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/postInstall.js +1 -1
- package/runtime/client.ts +27 -13
- package/runtime/config.ts +5 -2
- package/runtime/plugins/fetch.ts +33 -0
- package/runtime/plugins/index.ts +1 -0
- package/runtime/plugins/sessionRelay.ts +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "houdini-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "The core GraphQL client for Houdini",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"minimatch": "^10.2.5"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"houdini-core-darwin-x64": "2.0.
|
|
25
|
-
"houdini-core-darwin-arm64": "2.0.
|
|
26
|
-
"houdini-core-linux-x64": "2.0.
|
|
27
|
-
"houdini-core-linux-arm64": "2.0.
|
|
28
|
-
"houdini-core-win32-x64": "2.0.
|
|
29
|
-
"houdini-core-win32-arm64": "2.0.
|
|
30
|
-
"houdini-core-wasm": "2.0.
|
|
24
|
+
"houdini-core-darwin-x64": "2.0.1",
|
|
25
|
+
"houdini-core-darwin-arm64": "2.0.1",
|
|
26
|
+
"houdini-core-linux-x64": "2.0.1",
|
|
27
|
+
"houdini-core-linux-arm64": "2.0.1",
|
|
28
|
+
"houdini-core-win32-x64": "2.0.1",
|
|
29
|
+
"houdini-core-win32-arm64": "2.0.1",
|
|
30
|
+
"houdini-core-wasm": "2.0.1"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"bin",
|
package/postInstall.js
CHANGED
|
@@ -5,7 +5,7 @@ const https = require('https')
|
|
|
5
5
|
const child_process = require('child_process')
|
|
6
6
|
|
|
7
7
|
// Adjust the version you want to install. You can also make this dynamic.
|
|
8
|
-
const BINARY_DISTRIBUTION_VERSION = '2.0.
|
|
8
|
+
const BINARY_DISTRIBUTION_VERSION = '2.0.1'
|
|
9
9
|
|
|
10
10
|
// Windows binaries end with .exe so we need to special case them.
|
|
11
11
|
const binaryName = process.platform === 'win32' ? 'houdini-core.exe' : 'houdini-core'
|
package/runtime/client.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
throwOnError as throwOnErrorPlugin,
|
|
23
23
|
optimisticKeys,
|
|
24
24
|
cachePolicy,
|
|
25
|
+
sessionRelay,
|
|
25
26
|
} from './plugins/index.js'
|
|
26
27
|
import pluginsFromPlugins from './plugins/injectedPlugins.js'
|
|
27
28
|
|
|
@@ -30,7 +31,6 @@ export { fetch, mutation, query, subscription } from './plugins/index.js'
|
|
|
30
31
|
export { DocumentStore, type ClientPlugin, type SendParams } from 'houdini/runtime/documentStore'
|
|
31
32
|
|
|
32
33
|
export type HoudiniClientConstructorArgs = {
|
|
33
|
-
url?: string
|
|
34
34
|
fetchParams?: FetchParamFn
|
|
35
35
|
plugins?: NestedList<ClientPlugin>
|
|
36
36
|
pipeline?: NestedList<ClientPlugin>
|
|
@@ -49,14 +49,22 @@ export class HoudiniClient extends BaseClient {
|
|
|
49
49
|
// store throwOnError operations for access by stores
|
|
50
50
|
throwOnError_operations: string[] = []
|
|
51
51
|
|
|
52
|
-
constructor({
|
|
53
|
-
url
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
constructor(args: HoudiniClientConstructorArgs = {}) {
|
|
53
|
+
// Houdini 2.0 migration guard: the API url is no longer passed to the client. Pre-2.0 apps
|
|
54
|
+
// did `new HoudiniClient({ url })` (or `new HoudiniClient(url)`); that value would now be
|
|
55
|
+
// silently ignored, which is one of the widest blast radiuses in the migration — so fail
|
|
56
|
+
// loudly with the new home for it.
|
|
57
|
+
const legacyUrl =
|
|
58
|
+
typeof (args as unknown) === 'string' ? args : (args as { url?: unknown }).url
|
|
59
|
+
if (legacyUrl != null) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'HoudiniClient no longer accepts a `url`. Set `url` in houdini.config.js for a remote ' +
|
|
62
|
+
'API, or `endpoint` in src/server/+config to change the local API path.'
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { fetchParams, plugins, pipeline, throwOnError, cache = cacheRef } = args
|
|
67
|
+
|
|
60
68
|
// if we were given plugins and pipeline there's an error
|
|
61
69
|
if (plugins && pipeline) {
|
|
62
70
|
throw new Error(
|
|
@@ -67,12 +75,16 @@ export class HoudiniClient extends BaseClient {
|
|
|
67
75
|
const serverPort =
|
|
68
76
|
globalThis.process?.env?.HOUDINI_PORT ?? globalThis.process?.env?.PORT ?? '5173'
|
|
69
77
|
|
|
78
|
+
// resolve the endpoint from config (no `url` arg anymore). An absolute endpoint (a remote
|
|
79
|
+
// api) is used as-is; a relative one (the local mount) is prefixed with the origin on SSR.
|
|
80
|
+
const endpoint = localApiEndpoint()
|
|
81
|
+
const resolvedUrl = /^https?:\/\//.test(endpoint)
|
|
82
|
+
? endpoint
|
|
83
|
+
: (globalThis.window ? '' : `http://localhost:${serverPort}`) + endpoint
|
|
84
|
+
|
|
70
85
|
super({
|
|
71
86
|
config: getCurrentConfig,
|
|
72
|
-
url:
|
|
73
|
-
url ??
|
|
74
|
-
(globalThis.window ? '' : `http://localhost:${serverPort}`) +
|
|
75
|
-
localApiEndpoint(getCurrentConfig()),
|
|
87
|
+
url: resolvedUrl,
|
|
76
88
|
plugins: flatten(
|
|
77
89
|
([] as NestedList<ClientPlugin>).concat(
|
|
78
90
|
// if they specified a throw behavior
|
|
@@ -85,6 +97,8 @@ export class HoudiniClient extends BaseClient {
|
|
|
85
97
|
(
|
|
86
98
|
[
|
|
87
99
|
optimisticKeys(cache ?? cacheRef),
|
|
100
|
+
// relay an @session mutation's mint token to the auth endpoint (sets the cookie)
|
|
101
|
+
sessionRelay(),
|
|
88
102
|
// make sure that documents always work
|
|
89
103
|
queryPlugin(cache ?? cacheRef),
|
|
90
104
|
mutationPlugin(cache ?? cacheRef),
|
package/runtime/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigFile } from 'houdini'
|
|
2
|
+
import { resolveApiEndpoint } from 'houdini/runtime'
|
|
2
3
|
|
|
3
4
|
import config from './imports/config.js'
|
|
4
5
|
import pluginConfigs from './imports/pluginConfig.js'
|
|
@@ -49,8 +50,10 @@ export function computeID(configFile: ConfigFile, type: string, data: any): stri
|
|
|
49
50
|
// only compute the config file once
|
|
50
51
|
let _configFile: ConfigFile | null = null
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
// the GraphQL endpoint comes straight from the public config bundle (no injection): the remote
|
|
54
|
+
// `url`, the local mount `apiURL`, or the default — see resolveApiEndpoint.
|
|
55
|
+
export function localApiEndpoint() {
|
|
56
|
+
return resolveApiEndpoint(getCurrentConfig())
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export function getCurrentConfig(): ConfigFile {
|
package/runtime/plugins/fetch.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { getAuthUrl } from 'houdini/runtime'
|
|
1
2
|
import type { ClientPlugin, ClientPluginContext } from 'houdini/runtime/documentStore'
|
|
2
3
|
import { ArtifactKind, DataSource } from 'houdini/runtime/types'
|
|
3
4
|
import type { RequestPayload, FetchContext } from 'houdini/runtime/types'
|
|
4
5
|
|
|
6
|
+
import { getCurrentConfig } from '../config.js'
|
|
7
|
+
|
|
5
8
|
export const fetch = (target?: RequestHandler | string): ClientPlugin => {
|
|
6
9
|
return () => {
|
|
7
10
|
return {
|
|
@@ -38,6 +41,28 @@ export const fetch = (target?: RequestHandler | string): ClientPlugin => {
|
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// a @session mutation against a REMOTE api has to go through Houdini's same-origin
|
|
45
|
+
// proxy so the server can sit in the request path and write the session cookie
|
|
46
|
+
// server-authoritatively. The app is remote exactly when `url` is set in the public
|
|
47
|
+
// config (with a local schema the mutation hits the local Yoga and the mint plugin
|
|
48
|
+
// signs inline). The proxy lives under the session endpoint. We override the URL but
|
|
49
|
+
// never a user-supplied custom fetch function (it owns transport).
|
|
50
|
+
const artifact = ctx.artifact as typeof ctx.artifact & { sessionPath?: string }
|
|
51
|
+
if (
|
|
52
|
+
getCurrentConfig().url &&
|
|
53
|
+
artifact.kind === ArtifactKind.Mutation &&
|
|
54
|
+
artifact.sessionPath &&
|
|
55
|
+
typeof target !== 'function'
|
|
56
|
+
) {
|
|
57
|
+
// tell the proxy which operation this is via a header so it never has to parse the
|
|
58
|
+
// (possibly multipart) body to find the session path. Must stay in sync with
|
|
59
|
+
// HOUDINI_OPERATION_HEADER in router/server.ts.
|
|
60
|
+
fetchFn = defaultFetch(getAuthUrl() + '/proxy', {
|
|
61
|
+
...ctx.fetchParams,
|
|
62
|
+
headers: { ...ctx.fetchParams?.headers, 'x-houdini-operation': ctx.name },
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
const result = await fetchFn({
|
|
42
67
|
// wrap the user's fetch function so we can identify SSR by checking
|
|
43
68
|
// the response.url
|
|
@@ -61,6 +86,9 @@ export const fetch = (target?: RequestHandler | string): ClientPlugin => {
|
|
|
61
86
|
variables: ctx.variables ?? {},
|
|
62
87
|
data: result.data,
|
|
63
88
|
errors: !result.errors || result.errors.length === 0 ? null : result.errors,
|
|
89
|
+
// surface response-level extensions (e.g. the @session mint
|
|
90
|
+
// token) so the runtime/hooks can read them off the result
|
|
91
|
+
extensions: result.extensions,
|
|
64
92
|
partial: false,
|
|
65
93
|
stale: false,
|
|
66
94
|
source: DataSource.Network,
|
|
@@ -90,6 +118,11 @@ const defaultFetch = (
|
|
|
90
118
|
headers: {
|
|
91
119
|
Accept: 'application/graphql+json, application/json',
|
|
92
120
|
'Content-Type': 'application/json',
|
|
121
|
+
// a header a cross-origin <form>/simple request cannot set. The server
|
|
122
|
+
// requires it for CORS-simple POSTs to the graphql endpoint (uploads use
|
|
123
|
+
// multipart, which bypasses preflight) so it can't be a CSRF channel. Must
|
|
124
|
+
// stay in sync with HOUDINI_REQUEST_HEADER in router/server.ts.
|
|
125
|
+
'x-houdini-request': 'true',
|
|
93
126
|
...params?.headers,
|
|
94
127
|
},
|
|
95
128
|
})
|
package/runtime/plugins/index.ts
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getAuthUrl, HOUDINI_SESSION_EVENT, valueAtPath } from 'houdini/runtime'
|
|
2
|
+
import type { ClientPlugin } from 'houdini/runtime/documentStore'
|
|
3
|
+
import { ArtifactKind } from 'houdini/runtime/types'
|
|
4
|
+
|
|
5
|
+
// sessionRelay is the client half of @session. When a session mutation executes, the server
|
|
6
|
+
// signs the resolver's session subtree into a token in the response extensions
|
|
7
|
+
// (extensions.houdiniSession). This plugin relays that token to the auth endpoint, which
|
|
8
|
+
// verifies it and sets the httpOnly cookie — JS can't set httpOnly cookies itself — and then
|
|
9
|
+
// mirrors the same write into local state (a window event the router listens for) so
|
|
10
|
+
// useSession() updates without a refresh.
|
|
11
|
+
//
|
|
12
|
+
// It runs in the document pipeline, so it fires for ANY @session mutation execution
|
|
13
|
+
// (useMutation, useMutationForm, a raw send), not just form submits: session-by-mutation
|
|
14
|
+
// isn't tied to forms. The no-JS form path never reaches here (the server writes the cookie
|
|
15
|
+
// directly and marks its internal request so no token is minted).
|
|
16
|
+
export const sessionRelay = (): ClientPlugin => () => ({
|
|
17
|
+
async end(ctx, { value, resolve }) {
|
|
18
|
+
const artifact = ctx.artifact as typeof ctx.artifact & {
|
|
19
|
+
sessionPath?: string
|
|
20
|
+
sessionMerge?: boolean
|
|
21
|
+
}
|
|
22
|
+
const result = value as { data?: any; extensions?: Record<string, any> } | null
|
|
23
|
+
// the local-Yoga path mints a token here for the client to relay; the remote-api proxy
|
|
24
|
+
// instead writes the cookie itself and signals it with houdiniSessionApplied. Either way the
|
|
25
|
+
// session changed and useSession() must mirror it.
|
|
26
|
+
const token = result?.extensions?.houdiniSession
|
|
27
|
+
const proxyApplied = result?.extensions?.houdiniSessionApplied === true
|
|
28
|
+
const isSessionMutation = artifact.kind === ArtifactKind.Mutation && !!artifact.sessionPath
|
|
29
|
+
if (isSessionMutation && typeof window !== 'undefined' && (token || proxyApplied)) {
|
|
30
|
+
// relay the minted token to the auth endpoint to set the cookie. The proxy path already
|
|
31
|
+
// set the cookie server-side (proxyApplied, no token), so there's nothing to relay there.
|
|
32
|
+
if (token) {
|
|
33
|
+
try {
|
|
34
|
+
await fetch(getAuthUrl(), {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({ token }),
|
|
37
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
38
|
+
})
|
|
39
|
+
} catch {
|
|
40
|
+
// best-effort — if the relay fails the cookie just isn't updated this time
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// mirror the write into local state so useSession() updates without a refresh. A null
|
|
45
|
+
// subtree clears (replace with {}); otherwise merge upserts and the default replaces. The
|
|
46
|
+
// cookie (set by the relay or the proxy) stays the source of truth; this just reflects it.
|
|
47
|
+
const next = valueAtPath(result?.data, artifact.sessionPath!.split('.'))
|
|
48
|
+
window.dispatchEvent(
|
|
49
|
+
new CustomEvent(HOUDINI_SESSION_EVENT, {
|
|
50
|
+
bubbles: true,
|
|
51
|
+
detail: {
|
|
52
|
+
session: next ?? {},
|
|
53
|
+
merge: next != null && !!artifact.sessionMerge,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
resolve(ctx)
|
|
59
|
+
},
|
|
60
|
+
})
|