houdini-react 2.0.0-next.29 → 2.0.0-next.30

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 CHANGED
@@ -1,88 +1,173 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Simple shim that can be replaced with the actual binary for optimal performance
4
- // This follows esbuild's approach: the postInstall script will replace this entire file
5
- // with the native binary when possible, eliminating Node.js startup overhead
6
-
7
3
  const fs = require('fs');
8
4
  const path = require('path');
9
5
  const { execFileSync } = require('child_process');
10
6
 
11
- // Manual binary path override support
12
- const MANUAL_BINARY_PATH = process.env.HOUDINI_BINARY_PATH || process.env.HOUDINI_REACT_BINARY_PATH;
7
+ const binaryName = process.platform === 'win32' ? 'houdini-react.exe' : 'houdini-react';
8
+
9
+ const BINARY_DISTRIBUTION_PACKAGES = {
10
+ 'linux-x64': 'houdini-react-linux-x64',
11
+ 'linux-arm64': 'houdini-react-linux-arm64',
12
+ 'win32-x64': 'houdini-react-win32-x64',
13
+ 'win32-arm64': 'houdini-react-win32-arm64',
14
+ 'darwin-x64': 'houdini-react-darwin-x64',
15
+ 'darwin-arm64':'houdini-react-darwin-arm64',
16
+ };
17
+
18
+ const PLATFORM_OVERRIDE = process.env.HOUDINI_PLATFORM;
19
+ const MANUAL_BINARY_PATH = process.env.HOUDINI_REACT_BINARY_PATH || process.env.HOUDINI_BINARY_PATH;
20
+
21
+ // --- WASM path ---
22
+ if (PLATFORM_OVERRIDE === 'wasm') {
23
+ const wasmPackage = 'houdini-react-wasm';
24
+ const wasmBinaryName = 'houdini-react.wasm';
25
+ let wasmBin = null;
26
+
27
+ try {
28
+ const pkgPath = require.resolve(`${wasmPackage}/package.json`);
29
+ wasmBin = path.join(path.dirname(pkgPath), 'bin', wasmBinaryName);
30
+ } catch {
31
+ const sibling = path.join(__dirname, '..', wasmPackage, 'bin', wasmBinaryName);
32
+ if (fs.existsSync(sibling)) wasmBin = sibling;
33
+ }
34
+
35
+ if (!wasmBin || !fs.existsSync(wasmBin)) {
36
+ process.stderr.write(`[houdini-react] WASM package not installed. Try: npm install ${wasmPackage}\n`);
37
+ process.exit(1);
38
+ }
39
+
40
+ // In WebContainers, fs.readSync on a pipe fd is not supported in any thread
41
+ // (the child gets EBADF). Instead:
42
+ // Main thread — async process.stdin.on('data') works fine (event loop)
43
+ // Worker thread — Atomics.wait + receiveMessageOnPort provides real blocking
44
+ // A custom fd_read override feeds WASM stdin from the message channel so WASI
45
+ // never touches fd 0 directly.
46
+ const { Worker, isMainThread, workerData, MessageChannel, receiveMessageOnPort } = require('worker_threads');
47
+
48
+ if (isMainThread) {
49
+ const { port1: stdinMain, port2: stdinWorker } = new MessageChannel();
50
+ // Counter: main increments (Atomics.add) per message so rapid bursts
51
+ // (two frames in the same tick) produce two distinct wakeups.
52
+ const syncBuf = new Int32Array(new SharedArrayBuffer(4));
53
+
54
+ process.stdin.on('error', () => {});
55
+ process.stdin.on('data', data => {
56
+ stdinMain.postMessage(data);
57
+ Atomics.add(syncBuf, 0, 1);
58
+ Atomics.notify(syncBuf, 0);
59
+ });
60
+ process.stdin.on('end', () => {
61
+ stdinMain.postMessage(null); // EOF sentinel
62
+ Atomics.add(syncBuf, 0, 1);
63
+ Atomics.notify(syncBuf, 0);
64
+ });
65
+
66
+ const worker = new Worker(__filename, {
67
+ workerData: { wasmBin, args: process.argv.slice(2), stdinPort: stdinWorker, syncBuf },
68
+ transferList: [stdinWorker],
69
+ });
70
+ worker.on('exit', code => process.exit(code ?? 0));
71
+ return; // prevent fallthrough to native binary path
72
+ } else {
73
+ const { wasmBin: wb, args, stdinPort, syncBuf } = workerData;
74
+ const { WASI } = require('node:wasi');
75
+
76
+ const wasi = new WASI({
77
+ args: [wb, ...args],
78
+ env: process.env,
79
+ preopens: { '/': '/' },
80
+ version: 'preview1',
81
+ });
82
+
83
+ let wasmMem = null;
84
+ const importObj = wasi.getImportObject();
85
+ const realFdRead = importObj.wasi_snapshot_preview1.fd_read;
86
+
87
+ // Override fd_read for stdin (fd 0) only: block via Atomics until the main
88
+ // thread delivers a chunk, then copy it into WASM memory via iov buffers.
89
+ importObj.wasi_snapshot_preview1.fd_read = (fd, iovs, iovsLen, nread) => {
90
+ if (fd !== 0 || !wasmMem) return realFdRead(fd, iovs, iovsLen, nread);
13
91
 
92
+ while (Atomics.load(syncBuf, 0) === 0) {
93
+ Atomics.wait(syncBuf, 0, 0);
94
+ }
95
+ const msg = receiveMessageOnPort(stdinPort);
96
+ Atomics.sub(syncBuf, 0, 1);
97
+
98
+ const view = new DataView(wasmMem.buffer);
99
+ if (!msg || msg.message === null) {
100
+ view.setUint32(nread, 0, true); // EOF
101
+ return 0;
102
+ }
103
+
104
+ const chunk = msg.message;
105
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
106
+ let written = 0;
107
+ for (let i = 0; i < iovsLen; i++) {
108
+ const ptr = view.getUint32(iovs + i * 8, true);
109
+ const len = view.getUint32(iovs + i * 8 + 4, true);
110
+ const n = Math.min(len, buf.length - written);
111
+ if (n <= 0) break;
112
+ new Uint8Array(wasmMem.buffer, ptr, n).set(buf.subarray(written, written + n));
113
+ written += n;
114
+ }
115
+ view.setUint32(nread, written, true);
116
+ return 0;
117
+ };
118
+
119
+ const mod = new WebAssembly.Module(fs.readFileSync(wb));
120
+ const inst = new WebAssembly.Instance(mod, importObj);
121
+ wasmMem = inst.exports.memory;
122
+ wasi.start(inst);
123
+ process.exit(0);
124
+ }
125
+ }
126
+
127
+ // --- Native binary path ---
14
128
  function getBinaryPath() {
15
- // Check for manual override first
16
129
  if (MANUAL_BINARY_PATH && fs.existsSync(MANUAL_BINARY_PATH)) {
17
130
  return MANUAL_BINARY_PATH;
18
131
  }
19
132
 
20
- // Platform-specific package lookup
21
- const BINARY_DISTRIBUTION_PACKAGES = {
22
- 'linux-x64': 'houdini-react-linux-x64',
23
- 'linux-arm64': 'houdini-react-linux-arm64',
24
- 'win32-x64': 'houdini-react-win32-x64',
25
- 'win32-arm64': 'houdini-react-win32-arm64',
26
- 'darwin-x64': 'houdini-react-darwin-x64',
27
- 'darwin-arm64': 'houdini-react-darwin-arm64',
28
- }
29
-
30
- const binaryName = process.platform === 'win32' ? 'houdini-react.exe' : 'houdini-react'
31
- const platformSpecificPackageName = BINARY_DISTRIBUTION_PACKAGES[`${process.platform}-${process.arch}`]
133
+ const platformKey = PLATFORM_OVERRIDE || `${process.platform}-${process.arch}`;
134
+ const platformSpecificPackageName = BINARY_DISTRIBUTION_PACKAGES[platformKey];
32
135
 
33
136
  if (!platformSpecificPackageName) {
34
- // Fallback to downloaded binary if platform not supported
35
- return path.join(__dirname, binaryName)
137
+ if (PLATFORM_OVERRIDE) {
138
+ process.stderr.write(`[houdini-react] Unknown platform "${PLATFORM_OVERRIDE}". Valid values: ${Object.keys(BINARY_DISTRIBUTION_PACKAGES).join(', ')}, wasm\n`);
139
+ process.exit(1);
140
+ }
141
+ return path.join(__dirname, binaryName);
36
142
  }
37
143
 
38
144
  try {
39
- // Method 1: Use require.resolve to find the platform-specific package
40
- const platformPackagePath = require.resolve(`${platformSpecificPackageName}/package.json`)
41
- const platformPackageDir = path.dirname(platformPackagePath)
42
- return path.join(platformPackageDir, 'bin', binaryName)
43
- } catch (error) {
44
- // Method 2: Check sibling directory (npm structure)
45
- const siblingPath = path.join(__dirname, '..', platformSpecificPackageName)
46
- const siblingBinaryPath = path.join(siblingPath, 'bin', binaryName)
47
-
48
- if (fs.existsSync(siblingBinaryPath)) {
49
- return siblingBinaryPath
50
- }
145
+ const platformPackagePath = require.resolve(`${platformSpecificPackageName}/package.json`);
146
+ return path.join(path.dirname(platformPackagePath), 'bin', binaryName);
147
+ } catch {
148
+ const siblingPath = path.join(__dirname, '..', platformSpecificPackageName, 'bin', binaryName);
149
+ if (fs.existsSync(siblingPath)) return siblingPath;
51
150
 
52
- // Method 3: Check pnpm structure
53
- const pnpmMatch = __dirname.match(/(.+\/node_modules\/)\.pnpm\/([^\/]+)\/node_modules\//)
151
+ const pnpmMatch = __dirname.match(/(.+\/node_modules\/)\.pnpm\/[^/]+\/node_modules\//);
54
152
  if (pnpmMatch) {
55
- const [, nodeModulesRoot] = pnpmMatch
56
- const pnpmDir = path.join(nodeModulesRoot, '.pnpm')
57
-
153
+ const pnpmDir = path.join(pnpmMatch[1], '.pnpm');
58
154
  try {
59
- const pnpmEntries = fs.readdirSync(pnpmDir)
60
- // Get the expected version from the main package
61
- const packageJSON = require(path.join(__dirname, '..', 'package.json'))
62
- const expectedVersion = packageJSON.version
63
- const expectedPnpmEntry = `${platformSpecificPackageName}@${expectedVersion}`
64
- const platformEntry = pnpmEntries.find(entry => entry === expectedPnpmEntry)
65
-
66
- if (platformEntry) {
67
- const pnpmBinaryPath = path.join(pnpmDir, platformEntry, 'node_modules', platformSpecificPackageName, 'bin', binaryName)
68
- if (fs.existsSync(pnpmBinaryPath)) {
69
- return pnpmBinaryPath
70
- }
155
+ const packageJSON = require(path.join(__dirname, '..', 'package.json'));
156
+ const entry = `${platformSpecificPackageName}@${packageJSON.version}`;
157
+ const found = fs.readdirSync(pnpmDir).find(e => e === entry);
158
+ if (found) {
159
+ const p = path.join(pnpmDir, found, 'node_modules', platformSpecificPackageName, 'bin', binaryName);
160
+ if (fs.existsSync(p)) return p;
71
161
  }
72
- } catch (err) {
73
- // Ignore pnpm detection errors
74
- }
162
+ } catch {}
75
163
  }
76
164
 
77
- // Method 4: Fallback to downloaded binary in main package
78
- return path.join(__dirname, binaryName)
165
+ return path.join(__dirname, binaryName);
79
166
  }
80
167
  }
81
168
 
82
- // Execute the binary directly (this entire file may be replaced with the actual binary)
83
169
  try {
84
170
  execFileSync(getBinaryPath(), process.argv.slice(2), { stdio: 'inherit' });
85
171
  } catch (error) {
86
- // If execFileSync fails, exit with the same code
87
172
  process.exit(error.status || 1);
88
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "houdini-react",
3
- "version": "2.0.0-next.29",
3
+ "version": "2.0.0-next.30",
4
4
  "description": "The React plugin for houdini",
5
5
  "keywords": [
6
6
  "typescript",
@@ -16,29 +16,29 @@
16
16
  },
17
17
  "license": "MIT",
18
18
  "devDependencies": {
19
- "@types/cookie-parser": "^1.4.3",
20
- "@types/cookie-session": "^2.0.44",
21
- "@types/cookies": "^0.7.7",
22
- "@types/estraverse": "^5.1.2",
23
- "@types/express": "^4.17.17",
24
- "@types/react": "^19.0.7",
25
- "@types/react-dom": "^19.0.3"
19
+ "@types/cookie-parser": "^1.4.10",
20
+ "@types/cookie-session": "^2.0.49",
21
+ "@types/cookies": "^0.9.2",
22
+ "@types/estraverse": "^5.1.7",
23
+ "@types/express": "^5.0.3",
24
+ "@types/react": "^19.2.16",
25
+ "@types/react-dom": "^19.2.3"
26
26
  },
27
27
  "dependencies": {
28
- "@babel/parser": "^7.24.6",
29
- "@babel/types": "^7.24.6",
30
- "@whatwg-node/server": "^0.9.14",
31
- "cookie-parser": "^1.4.6",
32
- "cookie-session": "^2.0.0",
33
- "cookies": "^0.8.0",
28
+ "@babel/parser": "^7.29.7",
29
+ "@babel/types": "^7.29.7",
30
+ "@whatwg-node/server": "^0.11.0",
31
+ "cookie-parser": "^1.4.7",
32
+ "cookie-session": "^2.1.1",
33
+ "cookies": "^0.9.1",
34
34
  "estraverse": "^5.3.0",
35
- "express": "^4.18.2",
36
- "graphql-yoga": "^4.0.4",
37
- "react": "^19.0.0",
38
- "react-dom": "^19.0.0",
35
+ "express": "^5.1.0",
36
+ "graphql-yoga": "^5.21.1",
37
+ "react": "^19.2.7",
38
+ "react-dom": "^19.2.7",
39
39
  "react-streaming": "^0.4.17",
40
- "recast": "^0.23.1",
41
- "rollup": "^4.28.1",
40
+ "recast": "^0.23.11",
41
+ "rollup": "^4.61.1",
42
42
  "use-deep-compare-effect": "^1.8.1"
43
43
  },
44
44
  "peerDependencies": {
@@ -81,17 +81,20 @@
81
81
  }
82
82
  },
83
83
  "optionalDependencies": {
84
- "houdini-react-darwin-x64": "2.0.0-next.29",
85
- "houdini-react-darwin-arm64": "2.0.0-next.29",
86
- "houdini-react-linux-x64": "2.0.0-next.29",
87
- "houdini-react-linux-arm64": "2.0.0-next.29",
88
- "houdini-react-win32-x64": "2.0.0-next.29",
89
- "houdini-react-win32-arm64": "2.0.0-next.29"
84
+ "houdini-react-darwin-x64": "2.0.0-next.30",
85
+ "houdini-react-darwin-arm64": "2.0.0-next.30",
86
+ "houdini-react-linux-x64": "2.0.0-next.30",
87
+ "houdini-react-linux-arm64": "2.0.0-next.30",
88
+ "houdini-react-win32-x64": "2.0.0-next.30",
89
+ "houdini-react-win32-arm64": "2.0.0-next.30",
90
+ "houdini-react-wasm": "2.0.0-next.30"
90
91
  },
91
- "bin": "bin/houdini-react",
92
92
  "scripts": {
93
93
  "compile": "scripts build-go",
94
94
  "typedefs": "scripts typedefs --plugin --go-package",
95
95
  "postinstall": "node postInstall.js"
96
+ },
97
+ "bin": {
98
+ "houdini-react": "bin/houdini-react"
96
99
  }
97
100
  }
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.0-next.29'
8
+ const BINARY_DISTRIBUTION_VERSION = '2.0.0-next.30'
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'
@@ -181,6 +181,13 @@ if (!platformSpecificPackageName) {
181
181
  // Replace the JavaScript shim with the actual binary for optimal performance (skip Node.js overhead)
182
182
  // This is inspired by esbuild's approach: https://github.com/evanw/esbuild/blob/main/lib/npm/node-install.ts
183
183
  function maybeOptimizePackage() {
184
+ // Allow callers to opt out of the optimization (e.g. when building a snapshot
185
+ // for an environment that can't run native binaries, like WebContainers).
186
+ if (process.env.HOUDINI_SKIP_SHIM_INSTALL) {
187
+ console.log(`[${packageJSON.name}] HOUDINI_SKIP_SHIM_INSTALL set, keeping JavaScript shim`)
188
+ return
189
+ }
190
+
184
191
  // This optimization doesn't work on Windows because the binary must be called with .exe extension
185
192
  // It also doesn't work with Yarn due to various compatibility issues
186
193
  if (process.platform === 'win32' || isYarn()) {
@@ -19,9 +19,12 @@ import { useIsMountedRef } from './useIsMounted.js'
19
19
  // When the Component unmounts, we need to remove the entry from the cache (so we can load again)
20
20
 
21
21
  const promiseCache = createLRUCache<QuerySuspenseUnit>()
22
- type QuerySuspenseUnit = {
22
+ type QuerySuspenseUnit<
23
+ _Data extends GraphQLObject = GraphQLObject,
24
+ _Input extends GraphQLVariables = GraphQLVariables,
25
+ > = {
23
26
  resolve: () => void
24
- resolved?: DocumentHandle<QueryArtifact, GraphQLObject, {}>
27
+ resolved?: DocumentHandle<QueryArtifact, _Data, _Input>
25
28
  then: (val: any) => any
26
29
  }
27
30
 
@@ -106,7 +109,7 @@ export function useQueryHandle<
106
109
  let resolve: () => void = () => {}
107
110
  const loadPromise = new Promise<void>((r) => (resolve = r))
108
111
 
109
- const suspenseUnit: QuerySuspenseUnit = {
112
+ const suspenseUnit: QuerySuspenseUnit<_Data, _Input> = {
110
113
  // biome-ignore lint/suspicious/noThenProperty: suspense protocol requires a thenable
111
114
  then: loadPromise.then.bind(loadPromise),
112
115
  resolve,
@@ -114,7 +117,7 @@ export function useQueryHandle<
114
117
  variables,
115
118
  }
116
119
 
117
- promiseCache.set(identifier, suspenseUnit)
120
+ promiseCache.set(identifier, suspenseUnit as QuerySuspenseUnit)
118
121
 
119
122
  // the suspense unit gives react something to hold onto
120
123
  // and it acts as a place for us to register a callback on
@@ -134,7 +137,7 @@ export function useQueryHandle<
134
137
  data: value.data,
135
138
  partia: value.partial,
136
139
  artifact,
137
- }
140
+ } as unknown as DocumentHandle<QueryArtifact, _Data, _Input>
138
141
 
139
142
  suspenseUnit.resolve()
140
143
  })
@@ -481,8 +481,8 @@ export function RouterContextProvider({
481
481
  const [session, setSession] = React.useState<App.Session>(ssrSession)
482
482
 
483
483
  // if we detect an event that contains a new session value
484
- const handleNewSession = React.useCallback((event: CustomEvent<App.Session>) => {
485
- setSession(event.detail)
484
+ const handleNewSession = React.useCallback((event: Event) => {
485
+ setSession((event as CustomEvent<App.Session>).detail)
486
486
  }, [])
487
487
 
488
488
  React.useEffect(() => {
package/vite/index.js CHANGED
@@ -17,7 +17,7 @@ try {
17
17
  reactStreamingServerPath = path.join(pkgDir, "dist/server/index.node-and-web.js");
18
18
  } catch {
19
19
  }
20
- function vite_default(ctx) {
20
+ function index_default(ctx) {
21
21
  let manifest;
22
22
  let viteEnv;
23
23
  let devServer = false;
@@ -73,7 +73,7 @@ function vite_default(ctx) {
73
73
  };
74
74
  if (env.command === "build" && ctx.adapter && ctx.adapter.includePaths) {
75
75
  const extra = typeof ctx.adapter.includePaths === "function" ? ctx.adapter.includePaths({ config: ctx.config }) : ctx.adapter.includePaths;
76
- Object.assign(conf.build.rollupOptions.input, extra);
76
+ Object.assign(conf.build.rollupOptions.input ?? {}, extra);
77
77
  }
78
78
  for (const [id, page] of Object.entries(manifest.pages)) {
79
79
  ;
@@ -100,7 +100,9 @@ function vite_default(ctx) {
100
100
  }
101
101
  if (cfCache === null) {
102
102
  try {
103
- cfCache = ctx.db.prepare("SELECT type, field, fragment FROM component_fields").all();
103
+ cfCache = ctx.db.all(
104
+ "SELECT type, field, fragment FROM component_fields"
105
+ );
104
106
  } catch {
105
107
  cfCache = [];
106
108
  }
@@ -213,7 +215,7 @@ mount_static_app(App, manifest)
213
215
  body: await getBody(req)
214
216
  } : void 0
215
217
  );
216
- let documentPremable = `<script type="module" src="/@vite/client" async=""><\/script>`;
218
+ let documentPremable = `<script type="module" src="/@vite/client" async=""></script>`;
217
219
  try {
218
220
  const transformed = await server.transformIndexHtml(
219
221
  req.url,
@@ -280,5 +282,5 @@ function getBody(request) {
280
282
  });
281
283
  }
282
284
  export {
283
- vite_default as default
285
+ index_default as default
284
286
  };
package/vite/transform.js CHANGED
@@ -54,11 +54,9 @@ async function transform_file(page, cfRows) {
54
54
  Field(fieldNode) {
55
55
  const parentType = typeInfo.getParentType();
56
56
  const typeName = parentType?.name;
57
- if (!typeName)
58
- return;
57
+ if (!typeName) return;
59
58
  const fragmentName = cfMap[typeName]?.[fieldNode.name.value];
60
- if (!fragmentName)
61
- return;
59
+ if (!fragmentName) return;
62
60
  const entryPointPath = componentField_unit_path(
63
61
  page.config,
64
62
  fragmentName