houdini-react 2.1.0 → 2.1.2
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/bin/houdini-react +67 -19
- package/package.json +9 -8
- package/postInstall.js +1 -1
- package/runtime/Link.tsx +1 -1
- package/runtime/routing/Router.tsx +49 -19
- package/vite/index.js +34 -24
package/bin/houdini-react
CHANGED
|
@@ -125,6 +125,8 @@ if (PLATFORM_OVERRIDE === 'wasm') {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// --- Native binary path ---
|
|
128
|
+
const attempted = [];
|
|
129
|
+
|
|
128
130
|
function getBinaryPath() {
|
|
129
131
|
if (MANUAL_BINARY_PATH && fs.existsSync(MANUAL_BINARY_PATH)) {
|
|
130
132
|
return MANUAL_BINARY_PATH;
|
|
@@ -138,36 +140,82 @@ function getBinaryPath() {
|
|
|
138
140
|
process.stderr.write(`[houdini-react] Unknown platform "${PLATFORM_OVERRIDE}". Valid values: ${Object.keys(BINARY_DISTRIBUTION_PACKAGES).join(', ')}, wasm\n`);
|
|
139
141
|
process.exit(1);
|
|
140
142
|
}
|
|
141
|
-
return path.join(__dirname, binaryName);
|
|
143
|
+
return path.join(__dirname, '..', binaryName);
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
// module resolution from the shim's own location: real installs, and any
|
|
147
|
+
// environment that provides NODE_PATH (pnpm scripts do)
|
|
144
148
|
try {
|
|
145
149
|
const platformPackagePath = require.resolve(`${platformSpecificPackageName}/package.json`);
|
|
146
150
|
return path.join(path.dirname(platformPackagePath), 'bin', binaryName);
|
|
147
151
|
} catch {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} catch {}
|
|
163
|
-
}
|
|
152
|
+
attempted.push(`require.resolve('${platformSpecificPackageName}') from ${__dirname}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// module resolution from the invoking project: a linked development setup puts
|
|
156
|
+
// the platform package in the project's node_modules, which the walk from the
|
|
157
|
+
// shim's realpath (inside the linked repo) never visits
|
|
158
|
+
try {
|
|
159
|
+
const platformPackagePath = require.resolve(`${platformSpecificPackageName}/package.json`, {
|
|
160
|
+
paths: [process.cwd()],
|
|
161
|
+
});
|
|
162
|
+
return path.join(path.dirname(platformPackagePath), 'bin', binaryName);
|
|
163
|
+
} catch {
|
|
164
|
+
attempted.push(`require.resolve('${platformSpecificPackageName}') from ${process.cwd()}`);
|
|
165
|
+
}
|
|
164
166
|
|
|
165
|
-
|
|
167
|
+
// flat node_modules: the platform package next to this one
|
|
168
|
+
const siblingPath = path.join(__dirname, '..', platformSpecificPackageName, 'bin', binaryName);
|
|
169
|
+
if (fs.existsSync(siblingPath)) return siblingPath;
|
|
170
|
+
attempted.push(siblingPath);
|
|
171
|
+
|
|
172
|
+
// monorepo build layout: the shim lives at <pkg>/build/houdini-react/bin and the
|
|
173
|
+
// platform package at <pkg>/build/<platform package>
|
|
174
|
+
const buildSiblingPath = path.join(__dirname, '..', '..', platformSpecificPackageName, 'bin', binaryName);
|
|
175
|
+
if (fs.existsSync(buildSiblingPath)) return buildSiblingPath;
|
|
176
|
+
attempted.push(buildSiblingPath);
|
|
177
|
+
|
|
178
|
+
const pnpmMatch = __dirname.match(/(.+\/node_modules\/)\.pnpm\/[^/]+\/node_modules\//);
|
|
179
|
+
if (pnpmMatch) {
|
|
180
|
+
const pnpmDir = path.join(pnpmMatch[1], '.pnpm');
|
|
181
|
+
try {
|
|
182
|
+
const packageJSON = require(path.join(__dirname, '..', 'package.json'));
|
|
183
|
+
const entry = `${platformSpecificPackageName}@${packageJSON.version}`;
|
|
184
|
+
const found = fs.readdirSync(pnpmDir).find(e => e === entry);
|
|
185
|
+
if (found) {
|
|
186
|
+
const p = path.join(pnpmDir, found, 'node_modules', platformSpecificPackageName, 'bin', binaryName);
|
|
187
|
+
if (fs.existsSync(p)) return p;
|
|
188
|
+
}
|
|
189
|
+
attempted.push(path.join(pnpmDir, entry, 'node_modules', platformSpecificPackageName, 'bin', binaryName));
|
|
190
|
+
} catch {}
|
|
166
191
|
}
|
|
192
|
+
|
|
193
|
+
// the binary postInstall downloads into the package root when no platform
|
|
194
|
+
// package could be installed (the shim lives in bin/, one level down)
|
|
195
|
+
return path.join(__dirname, '..', binaryName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Refuse to exec ourselves: when every resolution attempt fails, the final
|
|
199
|
+
// candidate (the postInstall download location) can be this very script — exec'ing
|
|
200
|
+
// it recurses forever, forking a new node per iteration until the machine chokes.
|
|
201
|
+
// A missing binary must be a loud error, never a spawn loop.
|
|
202
|
+
const binaryPath = getBinaryPath();
|
|
203
|
+
let realBinaryPath = null;
|
|
204
|
+
try {
|
|
205
|
+
realBinaryPath = fs.realpathSync(binaryPath);
|
|
206
|
+
} catch {}
|
|
207
|
+
if (!realBinaryPath || realBinaryPath === fs.realpathSync(__filename)) {
|
|
208
|
+
attempted.push(binaryPath);
|
|
209
|
+
process.stderr.write(
|
|
210
|
+
`[houdini-react] Could not locate the houdini-react binary for ${process.platform}-${process.arch}. Tried:\n` +
|
|
211
|
+
attempted.map((p) => ` - ${p}\n`).join('') +
|
|
212
|
+
`Install the platform package for your system or point HOUDINI_REACT_BINARY_PATH at the binary.\n`
|
|
213
|
+
);
|
|
214
|
+
process.exit(1);
|
|
167
215
|
}
|
|
168
216
|
|
|
169
217
|
try {
|
|
170
|
-
execFileSync(
|
|
218
|
+
execFileSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
|
|
171
219
|
} catch (error) {
|
|
172
220
|
process.exit(error.status || 1);
|
|
173
221
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "houdini-react",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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.2",
|
|
86
|
+
"houdini-react-darwin-arm64": "2.1.2",
|
|
87
|
+
"houdini-react-linux-x64": "2.1.2",
|
|
88
|
+
"houdini-react-linux-arm64": "2.1.2",
|
|
89
|
+
"houdini-react-win32-x64": "2.1.2",
|
|
90
|
+
"houdini-react-win32-arm64": "2.1.2",
|
|
91
|
+
"houdini-react-wasm": "2.1.2"
|
|
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.2'
|
|
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'
|
package/runtime/Link.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import type { RouteHrefs, ExternalHref, ParamsForRoute, SearchForRoute } from '.
|
|
|
11
11
|
import { buildHref, type RouteHrefInfo } from './resolve-href.js'
|
|
12
12
|
|
|
13
13
|
// re-exported for custom link wrappers that constrain their own `to` prop
|
|
14
|
-
export type { RouteHrefs }
|
|
14
|
+
export type { RouteHrefs, ExternalHref }
|
|
15
15
|
|
|
16
16
|
export type LinkProps<H extends RouteHrefs | ExternalHref = RouteHrefs | ExternalHref> = Omit<
|
|
17
17
|
DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>,
|
|
@@ -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,20 @@ 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
|
+
const resolved = path.resolve(publicDir, `.${decodeURIComponent(url)}`);
|
|
385
|
+
if (!resolved.startsWith(publicDir + path.sep)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
return statSync(resolved).isFile();
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
383
393
|
function getBody(request) {
|
|
384
394
|
return new Promise((resolve) => {
|
|
385
395
|
const bodyParts = [];
|