houdini-react 2.1.0 → 2.1.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 +9 -8
- package/postInstall.js +1 -1
- package/runtime/routing/Router.tsx +49 -19
- package/vite/index.js +30 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "houdini-react",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "The React plugin for houdini",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"estraverse": "^5.3.0",
|
|
35
35
|
"express": "^5.1.0",
|
|
36
36
|
"graphql-yoga": "^5.21.1",
|
|
37
|
+
"mrmime": "^2.0.1",
|
|
37
38
|
"react": "^19.2.7",
|
|
38
39
|
"react-dom": "^19.2.7",
|
|
39
40
|
"react-streaming": "^0.4.17",
|
|
@@ -81,13 +82,13 @@
|
|
|
81
82
|
}
|
|
82
83
|
},
|
|
83
84
|
"optionalDependencies": {
|
|
84
|
-
"houdini-react-darwin-x64": "2.1.
|
|
85
|
-
"houdini-react-darwin-arm64": "2.1.
|
|
86
|
-
"houdini-react-linux-x64": "2.1.
|
|
87
|
-
"houdini-react-linux-arm64": "2.1.
|
|
88
|
-
"houdini-react-win32-x64": "2.1.
|
|
89
|
-
"houdini-react-win32-arm64": "2.1.
|
|
90
|
-
"houdini-react-wasm": "2.1.
|
|
85
|
+
"houdini-react-darwin-x64": "2.1.1",
|
|
86
|
+
"houdini-react-darwin-arm64": "2.1.1",
|
|
87
|
+
"houdini-react-linux-x64": "2.1.1",
|
|
88
|
+
"houdini-react-linux-arm64": "2.1.1",
|
|
89
|
+
"houdini-react-win32-x64": "2.1.1",
|
|
90
|
+
"houdini-react-win32-arm64": "2.1.1",
|
|
91
|
+
"houdini-react-wasm": "2.1.1"
|
|
91
92
|
},
|
|
92
93
|
"scripts": {
|
|
93
94
|
"compile": "scripts build-go",
|
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.1.
|
|
8
|
+
const BINARY_DISTRIBUTION_VERSION = '2.1.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-react.exe' : 'houdini-react'
|
|
@@ -4,8 +4,7 @@ import type { Cache } from 'houdini/runtime/cache'
|
|
|
4
4
|
import type { DocumentStore, HoudiniClient } from 'houdini/runtime/client'
|
|
5
5
|
import { getCurrentConfig } from '$houdini/runtime'
|
|
6
6
|
import configFile from '$houdini/runtime/imports/config'
|
|
7
|
-
import { deepEquals } from 'houdini/runtime'
|
|
8
|
-
import type { LRUCache } from 'houdini/runtime'
|
|
7
|
+
import { deepEquals, LRUCache } from 'houdini/runtime'
|
|
9
8
|
import { marshalSelection, marshalInputs, getAuthUrl, HOUDINI_SESSION_EVENT } from 'houdini/runtime'
|
|
10
9
|
import type { HoudiniSessionEventDetail } from 'houdini/runtime'
|
|
11
10
|
import { find_match, find_prefix_match } from 'houdini/router/match'
|
|
@@ -197,9 +196,9 @@ export function Router({
|
|
|
197
196
|
injectToStream,
|
|
198
197
|
})
|
|
199
198
|
// if we get this far, it's safe to load the component
|
|
200
|
-
const { component_cache, data_cache, artifact_cache, ssr_signals } =
|
|
199
|
+
const { component_cache, data_cache, artifact_cache, ssr_signals, latestSession } =
|
|
200
|
+
useRouterContext()
|
|
201
201
|
const PageComponent = component_cache.get(targetPage.id)!
|
|
202
|
-
const [session] = useSession()
|
|
203
202
|
|
|
204
203
|
// decide whether the entry should render its @loading frame instead of the page.
|
|
205
204
|
// values come from the router config (bundled client-side; they're UI timing, not
|
|
@@ -354,7 +353,7 @@ export function Router({
|
|
|
354
353
|
lastRevalidatedURL.current = currentURL
|
|
355
354
|
for (const name of Object.keys(targetPage.documents)) {
|
|
356
355
|
if (data_cache.has(name)) {
|
|
357
|
-
data_cache.get(name).send({ variables, session })
|
|
356
|
+
data_cache.get(name).send({ variables, session: latestSession.current })
|
|
358
357
|
}
|
|
359
358
|
}
|
|
360
359
|
}, [currentURL])
|
|
@@ -544,11 +543,9 @@ function usePageData({
|
|
|
544
543
|
artifact_cache,
|
|
545
544
|
ssr_signals,
|
|
546
545
|
last_variables,
|
|
546
|
+
latestSession,
|
|
547
547
|
} = useRouterContext()
|
|
548
548
|
|
|
549
|
-
// grab the current session value
|
|
550
|
-
const [session] = useSession()
|
|
551
|
-
|
|
552
549
|
// the function to load a query using the cache references
|
|
553
550
|
function load_query({
|
|
554
551
|
id,
|
|
@@ -559,11 +556,12 @@ function usePageData({
|
|
|
559
556
|
artifact: QueryArtifact
|
|
560
557
|
variables: GraphQLVariables
|
|
561
558
|
}): Promise<void> {
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
559
|
+
// record the variables this document is being sent with. only the loaded document:
|
|
560
|
+
// a preload loads the *destination's* queries while another page is still rendered,
|
|
561
|
+
// so writing the whole page's documents here (the old behavior) would tag the
|
|
562
|
+
// current page's documents with the destination's variables — and never tag the
|
|
563
|
+
// preloaded document at all
|
|
564
|
+
last_variables.set(id, variables)
|
|
567
565
|
|
|
568
566
|
// TODO: AbortController on send()
|
|
569
567
|
// TODO: we can read from cache here before making an asynchronous network call
|
|
@@ -655,7 +653,10 @@ function usePageData({
|
|
|
655
653
|
observer
|
|
656
654
|
.send({
|
|
657
655
|
variables: variables,
|
|
658
|
-
session
|
|
656
|
+
// read the ref, not the render-scoped session: this send can run during a
|
|
657
|
+
// render whose committed session predates a just-written one, and it must
|
|
658
|
+
// not repopulate the invalidated caches with stale-session results
|
|
659
|
+
session: latestSession.current,
|
|
659
660
|
})
|
|
660
661
|
.then(async () => {
|
|
661
662
|
// a stale-generation result: release the signal so nothing hangs, but
|
|
@@ -947,6 +948,15 @@ export function RouterContextProvider({
|
|
|
947
948
|
// on the server, we can just use
|
|
948
949
|
const [session, setSession] = React.useState<App.Session>(ssrSession)
|
|
949
950
|
|
|
951
|
+
// the React state above lags behind session writes: a setSession scheduled inside a
|
|
952
|
+
// transition hasn't committed when another lane renders (e.g. the urgent isPending
|
|
953
|
+
// flip of a navigation started in the same transition), so a render-phase load_query
|
|
954
|
+
// in that window would send with the previous session and repopulate the caches the
|
|
955
|
+
// session change just invalidated — the new-session render then finds everything
|
|
956
|
+
// "cached" and never refetches. Every session write updates this ref synchronously,
|
|
957
|
+
// and query sends read it instead of the render-scoped state.
|
|
958
|
+
const latestSession = React.useRef<App.Session>(ssrSession)
|
|
959
|
+
|
|
950
960
|
// if we detect an event that contains a new session value. The detail carries the subtree
|
|
951
961
|
// and whether to merge it into the current session (an @session(merge:) upsert) or replace
|
|
952
962
|
// it wholesale; a legacy plain-session detail is treated as a replace.
|
|
@@ -966,7 +976,8 @@ export function RouterContextProvider({
|
|
|
966
976
|
// old session
|
|
967
977
|
invalidate_session_caches({ data_cache, ssr_signals, last_variables })
|
|
968
978
|
|
|
969
|
-
|
|
979
|
+
latestSession.current = merge ? { ...latestSession.current, ...next } : next
|
|
980
|
+
setSession(latestSession.current)
|
|
970
981
|
},
|
|
971
982
|
[data_cache, ssr_signals, last_variables]
|
|
972
983
|
)
|
|
@@ -991,9 +1002,19 @@ export function RouterContextProvider({
|
|
|
991
1002
|
ssr_signals,
|
|
992
1003
|
last_variables,
|
|
993
1004
|
session,
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1005
|
+
latestSession,
|
|
1006
|
+
setSession: (newSession) => {
|
|
1007
|
+
latestSession.current = { ...latestSession.current, ...newSession }
|
|
1008
|
+
setSession(latestSession.current)
|
|
1009
|
+
},
|
|
1010
|
+
replaceSession: (next) => {
|
|
1011
|
+
latestSession.current = next
|
|
1012
|
+
setSession(next)
|
|
1013
|
+
},
|
|
1014
|
+
clearSession: () => {
|
|
1015
|
+
latestSession.current = {}
|
|
1016
|
+
setSession({})
|
|
1017
|
+
},
|
|
997
1018
|
formResult,
|
|
998
1019
|
formToken,
|
|
999
1020
|
}}
|
|
@@ -1027,6 +1048,12 @@ export type RouterContext = {
|
|
|
1027
1048
|
// The current session
|
|
1028
1049
|
session: App.Session
|
|
1029
1050
|
|
|
1051
|
+
// the most recent session value, written synchronously by every session setter. The
|
|
1052
|
+
// `session` state above only updates when React commits, which can lag the write by a
|
|
1053
|
+
// render (e.g. a setSession inside a transition) — anything that fires a request during
|
|
1054
|
+
// render (load_query) must read this instead so it can't send a stale session.
|
|
1055
|
+
latestSession: { current: App.Session }
|
|
1056
|
+
|
|
1030
1057
|
// a function to call that sets the client-side session singletone
|
|
1031
1058
|
setSession: (newSession: Partial<App.Session>) => void
|
|
1032
1059
|
|
|
@@ -1406,7 +1433,10 @@ export function router_cache({
|
|
|
1406
1433
|
store.cleanup().catch(() => {})
|
|
1407
1434
|
}),
|
|
1408
1435
|
ssr_signals: suspense_cache(),
|
|
1409
|
-
|
|
1436
|
+
// a plain LRU, NOT a suspense cache: readers look up documents that may have no
|
|
1437
|
+
// entry yet (useQueryResult reads it during render and in its commit effect, where
|
|
1438
|
+
// a suspense cache's thrown promise would land in the nearest error boundary)
|
|
1439
|
+
last_variables: new LRUCache(),
|
|
1410
1440
|
}
|
|
1411
1441
|
|
|
1412
1442
|
// we need to fill each query with an externally resolvable promise
|
package/vite/index.js
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
client_build_directory
|
|
7
7
|
} from "houdini/router/conventions";
|
|
8
8
|
import { load_manifest } from "houdini/router/manifest";
|
|
9
|
-
import {
|
|
9
|
+
import { lookup } from "mrmime";
|
|
10
|
+
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { createRequire } from "node:module";
|
|
11
12
|
import { build } from "vite";
|
|
12
13
|
import { transform_file } from "./transform.js";
|
|
@@ -287,7 +288,7 @@ mount_static_app(App, manifest)
|
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
289
290
|
const url = req.url.split("?")[0];
|
|
290
|
-
if (url.startsWith("/@") || url.startsWith("/virtual:") || url.startsWith("/node_modules/") || /\.[a-z]+$/i.test(url)) {
|
|
291
|
+
if (url.startsWith("/@") || url.startsWith("/virtual:") || url.startsWith("/node_modules/") || /\.[a-z]+$/i.test(url) || lookup(url) !== void 0 || is_public_file(url, server.config.publicDir)) {
|
|
291
292
|
next();
|
|
292
293
|
return;
|
|
293
294
|
}
|
|
@@ -304,18 +305,16 @@ mount_static_app(App, manifest)
|
|
|
304
305
|
adapter_config_path(ctx.config)
|
|
305
306
|
);
|
|
306
307
|
const requestHeaders = new Headers();
|
|
307
|
-
for (const
|
|
308
|
-
requestHeaders.set(
|
|
308
|
+
for (const [name, value] of Object.entries(req.headers ?? {})) {
|
|
309
|
+
requestHeaders.set(name, Array.isArray(value) ? value.join(", ") : value ?? "");
|
|
309
310
|
}
|
|
310
311
|
const port = server.config.server.port ?? 5173;
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
req.method
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} : void 0
|
|
318
|
-
);
|
|
312
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
313
|
+
const request = new Request(`http://localhost:${port}` + req.url, {
|
|
314
|
+
method: req.method,
|
|
315
|
+
headers: requestHeaders,
|
|
316
|
+
...hasBody ? { body: await getBody(req) } : {}
|
|
317
|
+
});
|
|
319
318
|
let documentPremable = `<script type="module" src="/@vite/client" async=""></script>`;
|
|
320
319
|
try {
|
|
321
320
|
const transformed = await server.transformIndexHtml(
|
|
@@ -353,20 +352,17 @@ mount_static_app(App, manifest)
|
|
|
353
352
|
if (res.closed) {
|
|
354
353
|
return;
|
|
355
354
|
}
|
|
356
|
-
for (const
|
|
357
|
-
|
|
355
|
+
for (const [key, value] of result.headers ?? []) {
|
|
356
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
357
|
+
res.setHeader(key, value);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const setCookies = result.headers?.getSetCookie() ?? [];
|
|
361
|
+
if (setCookies.length > 0) {
|
|
362
|
+
res.setHeader("Set-Cookie", setCookies);
|
|
358
363
|
}
|
|
359
364
|
if (result.status >= 300 && result.status < 400) {
|
|
360
|
-
res.writeHead(result.status
|
|
361
|
-
Location: result.headers.get("Location") ?? "",
|
|
362
|
-
...[...result.headers].reduce(
|
|
363
|
-
(headers, [key, value]) => ({
|
|
364
|
-
...headers,
|
|
365
|
-
[key]: value
|
|
366
|
-
}),
|
|
367
|
-
{}
|
|
368
|
-
)
|
|
369
|
-
});
|
|
365
|
+
res.writeHead(result.status);
|
|
370
366
|
} else {
|
|
371
367
|
res.write(await result.text());
|
|
372
368
|
}
|
|
@@ -380,6 +376,16 @@ mount_static_app(App, manifest)
|
|
|
380
376
|
}
|
|
381
377
|
};
|
|
382
378
|
}
|
|
379
|
+
function is_public_file(url, publicDir) {
|
|
380
|
+
if (!publicDir) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
return statSync(path.join(publicDir, decodeURIComponent(url))).isFile();
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
383
389
|
function getBody(request) {
|
|
384
390
|
return new Promise((resolve) => {
|
|
385
391
|
const bodyParts = [];
|