@octanejs/tanstack-start 0.1.1 → 0.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/package.json +13 -8
- package/src/GenericHydrate.tsrx +396 -0
- package/src/GenericHydrate.tsrx.d.ts +5 -0
- package/src/Hydrate.tsrx +106 -0
- package/src/Hydrate.tsrx.d.ts +63 -0
- package/src/client-only-server-strip.js +76 -0
- package/src/hydration/generic.d.ts +18 -0
- package/src/hydration/generic.js +26 -0
- package/src/hydration/idle.d.ts +9 -0
- package/src/hydration/idle.js +10 -0
- package/src/hydration/load.tsrx +38 -0
- package/src/hydration/load.tsrx.d.ts +8 -0
- package/src/hydration/never.tsrx +71 -0
- package/src/hydration/never.tsrx.d.ts +6 -0
- package/src/hydration/visible.tsrx +123 -0
- package/src/hydration/visible.tsrx.d.ts +12 -0
- package/src/hydration.d.ts +20 -0
- package/src/hydration.js +8 -0
- package/src/index.d.ts +11 -0
- package/src/index.js +2 -0
- package/src/internal/router-generator/filesystem/physical/getRouteNodes.js +4 -6
- package/src/internal/router-generator/generator.js +3 -0
- package/src/internal/router-plugin/core/code-splitter/compilers.js +3 -6
- package/src/internal/router-plugin/core/config.d.ts +1 -3
- package/src/internal/router-plugin/esbuild.d.ts +4 -10
- package/src/internal/router-plugin/vite.d.ts +4 -10
- package/src/internal/start-plugin-core/import-protection/analysis.js +13 -12
- package/src/internal/start-plugin-core/schema.d.ts +4 -14
- package/src/internal/start-plugin-core/start-compiler/compiler.d.ts +1 -6
- package/src/internal/start-plugin-core/types.d.ts +1 -2
- package/src/internal/start-plugin-core/vite/import-protection-plugin/plugin.js +16 -16
- package/src/internal/start-plugin-core/vite/schema.d.ts +3 -12
- package/src/plugin-vite.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tanstack-start",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TanStack Start for Octane, including file-route generation, server functions, SSR streaming, hydration, and Vite integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -86,6 +86,11 @@
|
|
|
86
86
|
"import": "./src/client-only.js",
|
|
87
87
|
"default": "./src/client-only.js"
|
|
88
88
|
},
|
|
89
|
+
"./hydration": {
|
|
90
|
+
"types": "./src/hydration.d.ts",
|
|
91
|
+
"import": "./src/hydration.js",
|
|
92
|
+
"default": "./src/hydration.js"
|
|
93
|
+
},
|
|
89
94
|
"./plugin/vite": {
|
|
90
95
|
"types": "./src/plugin-vite.d.ts",
|
|
91
96
|
"import": "./src/plugin-vite.js",
|
|
@@ -116,7 +121,7 @@
|
|
|
116
121
|
"magic-string": "^0.30.21",
|
|
117
122
|
"pathe": "^2.0.3",
|
|
118
123
|
"picomatch": "^4.0.3",
|
|
119
|
-
"prettier": "^3.
|
|
124
|
+
"prettier": "^3.9.5",
|
|
120
125
|
"seroval": "^1.5.4",
|
|
121
126
|
"source-map": "^0.7.6",
|
|
122
127
|
"srvx": "^0.11.9",
|
|
@@ -126,11 +131,11 @@
|
|
|
126
131
|
"vitefu": "^1.1.1",
|
|
127
132
|
"xmlbuilder2": "^4.0.3",
|
|
128
133
|
"zod": "^4.4.3",
|
|
129
|
-
"@octanejs/tanstack-router": "0.1.
|
|
134
|
+
"@octanejs/tanstack-router": "0.1.12"
|
|
130
135
|
},
|
|
131
136
|
"peerDependencies": {
|
|
132
137
|
"vite": ">=7.0.0",
|
|
133
|
-
"octane": "0.1.
|
|
138
|
+
"octane": "0.1.13"
|
|
134
139
|
},
|
|
135
140
|
"peerDependenciesMeta": {
|
|
136
141
|
"vite": {
|
|
@@ -138,10 +143,10 @@
|
|
|
138
143
|
}
|
|
139
144
|
},
|
|
140
145
|
"devDependencies": {
|
|
141
|
-
"@types/node": "^24.3
|
|
142
|
-
"vite": "^8.
|
|
143
|
-
"vitest": "^4.1.
|
|
144
|
-
"octane": "0.1.
|
|
146
|
+
"@types/node": "^24.13.3",
|
|
147
|
+
"vite": "^8.1.5",
|
|
148
|
+
"vitest": "^4.1.10",
|
|
149
|
+
"octane": "0.1.13"
|
|
145
150
|
},
|
|
146
151
|
"scripts": {
|
|
147
152
|
"test": "cd ../.. && vitest run --project tanstack-start"
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// GenericHydrate — port of @tanstack/react-start-client's GenericHydrate.tsx.
|
|
2
|
+
// The shared renderer behind the `idle` / `media` / `condition` / `interaction`
|
|
3
|
+
// strategies (and dynamic `when` functions): it renders the hydration marker
|
|
4
|
+
// `<div>` and gates the children behind a Suspense boundary whose promise is
|
|
5
|
+
// the strategy's hydration gate (`use(gate.promise)`), so deferred subtrees
|
|
6
|
+
// stay dehydrated until the strategy resolves the gate. All gate bookkeeping,
|
|
7
|
+
// delegated-intent listening, and prefetch waiting delegate to the
|
|
8
|
+
// framework-agnostic `@tanstack/start-client-core/hydration` runtime, exactly
|
|
9
|
+
// as upstream does. `reactUse` feature-detection is dropped: octane's `use`
|
|
10
|
+
// always exists. Upstream's SSR-safe `useLayoutEffect` re-export from
|
|
11
|
+
// `@tanstack/react-router` maps to octane's own `useLayoutEffect` (octane's
|
|
12
|
+
// server runtime ignores effects, so no SSR warning shim is needed).
|
|
13
|
+
//
|
|
14
|
+
// OCTANE ADAPTATION: upstream invokes strategy renderers inline
|
|
15
|
+
// (`props.when._h(props)` runs the renderer's hooks on the caller's fiber).
|
|
16
|
+
// Octane's compiled components take the `(props, __s, __extra)` ABI and cannot
|
|
17
|
+
// be bare-called, so octane's `Hydrate` renders `_h` as a child component
|
|
18
|
+
// instead — `GenericHydrate` is that component. Same DOM and effects; the
|
|
19
|
+
// hooks live in their own component scope rather than Hydrate's.
|
|
20
|
+
import {
|
|
21
|
+
Suspense,
|
|
22
|
+
use,
|
|
23
|
+
useCallback,
|
|
24
|
+
useEffect,
|
|
25
|
+
useId,
|
|
26
|
+
useLayoutEffect,
|
|
27
|
+
useRef,
|
|
28
|
+
useState,
|
|
29
|
+
} from 'octane';
|
|
30
|
+
import type { OctaneNode } from 'octane';
|
|
31
|
+
import { useHydrated } from '@octanejs/tanstack-router';
|
|
32
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
33
|
+
import {
|
|
34
|
+
hydrateIdAttribute,
|
|
35
|
+
hydrateWhenAttribute,
|
|
36
|
+
} from '@tanstack/start-client-core/hydration/constants';
|
|
37
|
+
import {
|
|
38
|
+
createResolvedGate,
|
|
39
|
+
getFallbackHtml,
|
|
40
|
+
getOrCreateGate,
|
|
41
|
+
onGateResolve,
|
|
42
|
+
releaseGate,
|
|
43
|
+
runHydrationStrategyCleanup,
|
|
44
|
+
saveFallbackHtml,
|
|
45
|
+
waitForHydrationPrefetchStrategy,
|
|
46
|
+
} from '@tanstack/start-client-core/hydration/runtime';
|
|
47
|
+
import { listenForDelegatedHydrationIntent } from '@tanstack/start-client-core/hydration';
|
|
48
|
+
import type {
|
|
49
|
+
HydrationRuntimeContext,
|
|
50
|
+
HydrationStrategy,
|
|
51
|
+
HydrationWhen,
|
|
52
|
+
} from '@tanstack/start-client-core/hydration';
|
|
53
|
+
import type { HydrationGateRecord } from '@tanstack/start-client-core/hydration/runtime';
|
|
54
|
+
import type { HydrateProps, InternalHydrateProps } from './Hydrate.tsrx';
|
|
55
|
+
|
|
56
|
+
type Gate = HydrationGateRecord & { promise: Promise<void> };
|
|
57
|
+
type PrefetchController = {
|
|
58
|
+
abortController: AbortController;
|
|
59
|
+
hydrationRequested: boolean;
|
|
60
|
+
hydrationListeners: Set<() => void>;
|
|
61
|
+
hydrationResolvePending: boolean;
|
|
62
|
+
started: boolean;
|
|
63
|
+
promise?: Promise<void>;
|
|
64
|
+
cleanup?: () => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const dynamicType = 'dynamic';
|
|
68
|
+
const dynamicHydrateStrategy: HydrationStrategy<typeof dynamicType, false> = {
|
|
69
|
+
_t: dynamicType,
|
|
70
|
+
_d: () => true,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function shouldDeferHydration(strategy: HydrationStrategy) {
|
|
74
|
+
return strategy._d ? strategy._d() : strategy._t !== 'load';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function useLatest<T>(value: T) {
|
|
78
|
+
const ref = useRef(value);
|
|
79
|
+
ref.current = value;
|
|
80
|
+
return ref;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function useHydrationGate(props: InternalHydrateProps) {
|
|
84
|
+
const hydrated = useHydrated();
|
|
85
|
+
const octaneId = useId();
|
|
86
|
+
const id = props.h ? `${props.h}${octaneId}` : octaneId;
|
|
87
|
+
const when = props.when;
|
|
88
|
+
const isDynamicHydrate = typeof when === 'function';
|
|
89
|
+
const dynamicHydrateStrategyRef = useRef<HydrationStrategy | undefined>(undefined);
|
|
90
|
+
if (isDynamicHydrate) {
|
|
91
|
+
dynamicHydrateStrategyRef.current ??= isServer ?? typeof window === 'undefined'
|
|
92
|
+
? dynamicHydrateStrategy
|
|
93
|
+
: when();
|
|
94
|
+
}
|
|
95
|
+
const hydrateStrategy = isDynamicHydrate ? dynamicHydrateStrategyRef.current! : when;
|
|
96
|
+
const markerHydrateType: HydrationWhen = isDynamicHydrate ? dynamicType : hydrateStrategy._t!;
|
|
97
|
+
const [prefetchError, setPrefetchError] = useState<unknown>();
|
|
98
|
+
const latestRef = useLatest({
|
|
99
|
+
prefetch: props.prefetch,
|
|
100
|
+
preload: props.p,
|
|
101
|
+
});
|
|
102
|
+
const gateRef = useRef<HydrationGateRecord | undefined>(undefined);
|
|
103
|
+
const markerElementRef = useRef<HTMLDivElement | null>(null);
|
|
104
|
+
const shouldPreserveServerHTMLRef = useRef<boolean | undefined>(undefined);
|
|
105
|
+
const shouldDeferInitialHydrationRef = useRef<boolean | undefined>(undefined);
|
|
106
|
+
const didPrefetchRef = useRef(false);
|
|
107
|
+
const prefetchControllerRef = useRef<PrefetchController | undefined>(undefined);
|
|
108
|
+
|
|
109
|
+
prefetchControllerRef.current ??= {
|
|
110
|
+
abortController: new AbortController(),
|
|
111
|
+
hydrationRequested: false,
|
|
112
|
+
hydrationListeners: new Set<() => void>(),
|
|
113
|
+
hydrationResolvePending: false,
|
|
114
|
+
started: false,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
shouldPreserveServerHTMLRef.current ??= (isServer ?? typeof window === 'undefined') || !hydrated;
|
|
118
|
+
shouldDeferInitialHydrationRef.current ??= !hydrated && shouldDeferHydration(hydrateStrategy);
|
|
119
|
+
|
|
120
|
+
if (!gateRef.current) {
|
|
121
|
+
gateRef.current = isServer ?? typeof window === 'undefined'
|
|
122
|
+
? createResolvedGate(id, hydrateStrategy._t!)
|
|
123
|
+
: getOrCreateGate(id, hydrateStrategy._t!);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
gateRef.current.when = hydrateStrategy._t!;
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
!(isServer ?? typeof window === 'undefined') && hydrateStrategy._t !== 'never' &&
|
|
130
|
+
(!shouldDeferInitialHydrationRef.current || !shouldDeferHydration(hydrateStrategy))
|
|
131
|
+
) {
|
|
132
|
+
gateRef.current.resolve();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const markerRef = useCallback((element: HTMLDivElement | null) => {
|
|
136
|
+
markerElementRef.current = element;
|
|
137
|
+
if (element) {
|
|
138
|
+
if (hydrateStrategy._t === 'never' && !shouldPreserveServerHTMLRef.current) {
|
|
139
|
+
element.replaceChildren();
|
|
140
|
+
}
|
|
141
|
+
saveFallbackHtml(id, element);
|
|
142
|
+
}
|
|
143
|
+
}, [hydrateStrategy._t, id]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const gate = gateRef.current!;
|
|
147
|
+
return () => {
|
|
148
|
+
const controller = prefetchControllerRef.current;
|
|
149
|
+
controller?.abortController.abort();
|
|
150
|
+
controller?.cleanup?.();
|
|
151
|
+
controller?.hydrationListeners.clear();
|
|
152
|
+
releaseGate(gate);
|
|
153
|
+
};
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if ((isServer ?? typeof window === 'undefined') || !latestRef.current.prefetch) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const controller = prefetchControllerRef.current!;
|
|
162
|
+
if (controller.started) return;
|
|
163
|
+
controller.started = true;
|
|
164
|
+
|
|
165
|
+
const onHydrate = (listener: () => void) => {
|
|
166
|
+
if (controller.hydrationRequested) {
|
|
167
|
+
listener();
|
|
168
|
+
return () => {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
controller.hydrationListeners.add(listener);
|
|
172
|
+
return () => {
|
|
173
|
+
controller.hydrationListeners.delete(listener);
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const preload = () => latestRef.current.preload?.() ?? Promise.resolve();
|
|
178
|
+
const prefetchInput = latestRef.current.prefetch;
|
|
179
|
+
|
|
180
|
+
if (typeof prefetchInput === 'function') {
|
|
181
|
+
const promise = Promise.resolve().then(
|
|
182
|
+
() => prefetchInput({
|
|
183
|
+
element: markerElementRef.current,
|
|
184
|
+
signal: controller.abortController.signal,
|
|
185
|
+
preload,
|
|
186
|
+
waitFor: (strategy) => waitForHydrationPrefetchStrategy(strategy, {
|
|
187
|
+
element: markerElementRef.current,
|
|
188
|
+
signal: controller.abortController.signal,
|
|
189
|
+
onHydrate,
|
|
190
|
+
}),
|
|
191
|
+
}),
|
|
192
|
+
).then(() => undefined);
|
|
193
|
+
|
|
194
|
+
controller.promise = promise;
|
|
195
|
+
promise.catch((error) => {
|
|
196
|
+
if (!controller.abortController.signal.aborted) {
|
|
197
|
+
setPrefetchError(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!latestRef.current.preload) return;
|
|
204
|
+
|
|
205
|
+
const prefetch = () => {
|
|
206
|
+
if (didPrefetchRef.current) return;
|
|
207
|
+
didPrefetchRef.current = true;
|
|
208
|
+
void preload();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
controller.cleanup = runHydrationStrategyCleanup(
|
|
212
|
+
prefetchInput._s?.({
|
|
213
|
+
element: markerElementRef.current,
|
|
214
|
+
prefetch,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}, [hydrateStrategy, latestRef]);
|
|
218
|
+
|
|
219
|
+
useLayoutEffect(() => {
|
|
220
|
+
const gate = gateRef.current!;
|
|
221
|
+
if (!shouldDeferInitialHydrationRef.current || hydrateStrategy._t === 'never') {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (gate.resolved) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const cleanups: Array<() => void> = [];
|
|
230
|
+
let removeResolveListener = () => {};
|
|
231
|
+
let disposed = false;
|
|
232
|
+
const resolveGate = gate.resolve;
|
|
233
|
+
|
|
234
|
+
const cleanup = () => {
|
|
235
|
+
if (disposed) return;
|
|
236
|
+
disposed = true;
|
|
237
|
+
if (gate.resolve === requestHydration) {
|
|
238
|
+
gate.resolve = resolveGate;
|
|
239
|
+
}
|
|
240
|
+
removeResolveListener();
|
|
241
|
+
cleanups.forEach((fn) => fn());
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const addCleanup = (fn: void | (() => void)) => {
|
|
245
|
+
if (!fn) return;
|
|
246
|
+
if (disposed || gate.resolved) {
|
|
247
|
+
fn();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
cleanups.push(fn);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const requestHydration = () => {
|
|
254
|
+
const controller = prefetchControllerRef.current!;
|
|
255
|
+
if (!controller.hydrationRequested) {
|
|
256
|
+
controller.hydrationRequested = true;
|
|
257
|
+
controller.hydrationListeners.forEach((listener) => listener());
|
|
258
|
+
controller.hydrationListeners.clear();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!controller.promise) {
|
|
262
|
+
resolveGate();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (controller.hydrationResolvePending) return;
|
|
266
|
+
controller.hydrationResolvePending = true;
|
|
267
|
+
|
|
268
|
+
controller.promise.then(() => resolveGate(), (error) => {
|
|
269
|
+
if (!controller.abortController.signal.aborted) {
|
|
270
|
+
setPrefetchError(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
gate.resolve = requestHydration;
|
|
276
|
+
removeResolveListener = onGateResolve(gate, cleanup);
|
|
277
|
+
|
|
278
|
+
const context: HydrationRuntimeContext = {
|
|
279
|
+
element: markerElementRef.current,
|
|
280
|
+
gate,
|
|
281
|
+
};
|
|
282
|
+
addCleanup(runHydrationStrategyCleanup(hydrateStrategy._s?.(context)));
|
|
283
|
+
|
|
284
|
+
if (hydrateStrategy._t !== 'interaction') {
|
|
285
|
+
addCleanup(
|
|
286
|
+
runHydrationStrategyCleanup(
|
|
287
|
+
markerElementRef.current
|
|
288
|
+
? listenForDelegatedHydrationIntent(markerElementRef.current, context)
|
|
289
|
+
: undefined,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return cleanup;
|
|
295
|
+
}, [hydrateStrategy, latestRef]);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
gate: gateRef.current,
|
|
299
|
+
markerRef,
|
|
300
|
+
markerElementRef,
|
|
301
|
+
hydrateStrategy,
|
|
302
|
+
markerHydrateType,
|
|
303
|
+
prefetchError,
|
|
304
|
+
shouldPreserveServerHTML: shouldPreserveServerHTMLRef.current,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function HydrationGate(props: { gate: Gate; children?: OctaneNode }) {
|
|
309
|
+
if (isServer ?? typeof window === 'undefined') {
|
|
310
|
+
return props.children;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (props.gate.resolved) {
|
|
314
|
+
return props.children;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
use(props.gate.promise);
|
|
318
|
+
|
|
319
|
+
return props.children;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function HydratedBoundary(props: {
|
|
323
|
+
id: string;
|
|
324
|
+
onHydrated?: () => void;
|
|
325
|
+
onStrategyHydrated?: (id: string) => void;
|
|
326
|
+
children?: OctaneNode;
|
|
327
|
+
}) {
|
|
328
|
+
const { id, onHydrated, onStrategyHydrated } = props;
|
|
329
|
+
const didHydrateRef = useRef(false);
|
|
330
|
+
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (didHydrateRef.current) return;
|
|
333
|
+
didHydrateRef.current = true;
|
|
334
|
+
onHydrated?.();
|
|
335
|
+
onStrategyHydrated?.(id);
|
|
336
|
+
}, [id, onHydrated, onStrategyHydrated]);
|
|
337
|
+
|
|
338
|
+
return props.children;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function GenericHydrate(props: HydrateProps) @{
|
|
342
|
+
const internalProps = props as InternalHydrateProps;
|
|
343
|
+
const {
|
|
344
|
+
gate,
|
|
345
|
+
hydrateStrategy,
|
|
346
|
+
markerHydrateType,
|
|
347
|
+
markerElementRef,
|
|
348
|
+
markerRef,
|
|
349
|
+
prefetchError,
|
|
350
|
+
shouldPreserveServerHTML,
|
|
351
|
+
} = useHydrationGate(internalProps);
|
|
352
|
+
if (prefetchError) throw prefetchError;
|
|
353
|
+
|
|
354
|
+
let fallback: OctaneNode;
|
|
355
|
+
if (shouldPreserveServerHTML) {
|
|
356
|
+
const html = getFallbackHtml(gate.id);
|
|
357
|
+
fallback = html
|
|
358
|
+
? <div style={{ display: 'contents' }} dangerouslySetInnerHTML={{ __html: html }} />
|
|
359
|
+
: null;
|
|
360
|
+
} else {
|
|
361
|
+
fallback = props.fallback ?? null;
|
|
362
|
+
}
|
|
363
|
+
const markerAttributes =
|
|
364
|
+
markerHydrateType === dynamicType ? undefined : hydrateStrategy._a?.();
|
|
365
|
+
|
|
366
|
+
const hydrateType = hydrateStrategy._t!;
|
|
367
|
+
const preserveNever = hydrateType === 'never' && !shouldPreserveServerHTML;
|
|
368
|
+
|
|
369
|
+
<div
|
|
370
|
+
ref={markerRef}
|
|
371
|
+
{...{
|
|
372
|
+
[hydrateIdAttribute]: gate.id,
|
|
373
|
+
[hydrateWhenAttribute]: markerHydrateType,
|
|
374
|
+
...markerAttributes,
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
@if (preserveNever) {
|
|
378
|
+
<>
|
|
379
|
+
{props.fallback ?? null}
|
|
380
|
+
</>
|
|
381
|
+
} @else {
|
|
382
|
+
<Suspense fallback={fallback}>
|
|
383
|
+
<HydrationGate gate={gate as Gate}>
|
|
384
|
+
<HydratedBoundary
|
|
385
|
+
id={gate.id}
|
|
386
|
+
onHydrated={props.onHydrated}
|
|
387
|
+
onStrategyHydrated={(id) => {
|
|
388
|
+
markerElementRef.current?.removeAttribute(hydrateWhenAttribute);
|
|
389
|
+
hydrateStrategy._o?.(id);
|
|
390
|
+
}}
|
|
391
|
+
>{props.children}</HydratedBoundary>
|
|
392
|
+
</HydrationGate>
|
|
393
|
+
</Suspense>
|
|
394
|
+
}
|
|
395
|
+
</div>
|
|
396
|
+
}
|
package/src/Hydrate.tsrx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Hydrate — port of @tanstack/react-start-client's Hydrate.tsx (partial
|
|
2
|
+
// hydration entry point). `props.when` carries a hydration strategy created by
|
|
3
|
+
// the `@octanejs/tanstack-start/hydration` factories; each strategy brings its
|
|
4
|
+
// own renderer (`_h`), so `Hydrate` delegates to it. A function-valued `when`
|
|
5
|
+
// picks the strategy on the client only; on the server it renders the generic
|
|
6
|
+
// `dynamic` marker so the streamed HTML stays strategy-agnostic.
|
|
7
|
+
//
|
|
8
|
+
// OCTANE ADAPTATION: upstream invokes the renderer inline as a method call
|
|
9
|
+
// (`props.when._h(props)` — the renderer's hooks run on Hydrate's own fiber
|
|
10
|
+
// and it reads the strategy off `this`). Octane's compiled components use the
|
|
11
|
+
// `(props, __s, __extra)` ABI and cannot be bare-called, so `_h` is rendered
|
|
12
|
+
// as a child COMPONENT here instead, and renderers that need their strategy
|
|
13
|
+
// derive it from `props.when` (resolving function-valued `when`, exactly as
|
|
14
|
+
// upstream's Hydrate does per render). Same DOM, markers, and effects; the
|
|
15
|
+
// renderer's hooks live in their own component scope rather than Hydrate's.
|
|
16
|
+
import { Suspense, useId } from 'octane';
|
|
17
|
+
import type { OctaneNode } from 'octane';
|
|
18
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
19
|
+
import {
|
|
20
|
+
hydrateIdAttribute,
|
|
21
|
+
hydrateWhenAttribute,
|
|
22
|
+
} from '@tanstack/start-client-core/hydration/constants';
|
|
23
|
+
import type {
|
|
24
|
+
HydrationStrategy as CoreHydrationStrategy,
|
|
25
|
+
HydrationPrefetchFunction,
|
|
26
|
+
HydrationPrefetchStrategy,
|
|
27
|
+
HydrationWhen,
|
|
28
|
+
} from '@tanstack/start-client-core/hydration';
|
|
29
|
+
|
|
30
|
+
export type OctaneHydrationStrategy<
|
|
31
|
+
TWhen extends HydrationWhen = HydrationWhen,
|
|
32
|
+
TCanPrefetch extends boolean = boolean,
|
|
33
|
+
> =
|
|
34
|
+
CoreHydrationStrategy<TWhen, TCanPrefetch> & {
|
|
35
|
+
_h: (props: HydrateProps) => OctaneNode;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type HydrationStrategy<
|
|
39
|
+
TWhen extends HydrationWhen = HydrationWhen,
|
|
40
|
+
TCanPrefetch extends boolean = boolean,
|
|
41
|
+
> =
|
|
42
|
+
OctaneHydrationStrategy<TWhen, TCanPrefetch>;
|
|
43
|
+
|
|
44
|
+
export type HydrateWhen = OctaneHydrationStrategy | (() => OctaneHydrationStrategy);
|
|
45
|
+
|
|
46
|
+
type HydrateCommonOptions = {
|
|
47
|
+
when: HydrateWhen;
|
|
48
|
+
fallback?: OctaneNode;
|
|
49
|
+
onHydrated?: () => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type HydrateOptions =
|
|
53
|
+
| (HydrateCommonOptions & {
|
|
54
|
+
prefetch?: never;
|
|
55
|
+
split?: boolean;
|
|
56
|
+
})
|
|
57
|
+
| (HydrateCommonOptions & {
|
|
58
|
+
prefetch: HydrationPrefetchStrategy;
|
|
59
|
+
split?: true;
|
|
60
|
+
})
|
|
61
|
+
| (HydrateCommonOptions & {
|
|
62
|
+
prefetch: HydrationPrefetchFunction;
|
|
63
|
+
split?: boolean;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type HydrateProps =
|
|
67
|
+
HydrateOptions & {
|
|
68
|
+
children: OctaneNode;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type InternalHydrateProps =
|
|
72
|
+
HydrateProps & {
|
|
73
|
+
h?: string;
|
|
74
|
+
p?: () => Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const dynamicType = 'dynamic';
|
|
78
|
+
|
|
79
|
+
function ServerDynamicHydrate(props: HydrateProps) @{
|
|
80
|
+
const internalProps = props as InternalHydrateProps;
|
|
81
|
+
const octaneId = useId();
|
|
82
|
+
const id = internalProps.h ? `${internalProps.h}${octaneId}` : octaneId;
|
|
83
|
+
|
|
84
|
+
<div
|
|
85
|
+
{...{
|
|
86
|
+
[hydrateIdAttribute]: id,
|
|
87
|
+
[hydrateWhenAttribute]: dynamicType,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Suspense fallback={props.fallback ?? null}>{props.children}</Suspense>
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Hydrate(props: HydrateProps) {
|
|
95
|
+
if (typeof props.when === 'function') {
|
|
96
|
+
if (isServer ?? typeof window === 'undefined') {
|
|
97
|
+
return <ServerDynamicHydrate {...props} />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DynamicRenderer = props.when()._h;
|
|
101
|
+
return <DynamicRenderer {...props} />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const Renderer = props.when._h;
|
|
105
|
+
return <Renderer {...props} />;
|
|
106
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Type declarations for the .tsrx module (resolved by relative path).
|
|
2
|
+
import type { OctaneNode } from 'octane';
|
|
3
|
+
import type {
|
|
4
|
+
HydrationStrategy as CoreHydrationStrategy,
|
|
5
|
+
HydrationPrefetchFunction,
|
|
6
|
+
HydrationPrefetchStrategy,
|
|
7
|
+
HydrationWhen,
|
|
8
|
+
} from '@tanstack/start-client-core/hydration';
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
HydrationInteractionEvent,
|
|
12
|
+
HydrationInteractionEvents,
|
|
13
|
+
HydrationPrefetchContext,
|
|
14
|
+
HydrationPrefetchFunction,
|
|
15
|
+
HydrationPrefetchStrategy,
|
|
16
|
+
HydrationPrefetchWaitReason,
|
|
17
|
+
HydrationWhen,
|
|
18
|
+
} from '@tanstack/start-client-core/hydration';
|
|
19
|
+
|
|
20
|
+
export type OctaneHydrationStrategy<
|
|
21
|
+
TWhen extends HydrationWhen = HydrationWhen,
|
|
22
|
+
TCanPrefetch extends boolean = boolean,
|
|
23
|
+
> = CoreHydrationStrategy<TWhen, TCanPrefetch> & {
|
|
24
|
+
_h: (props: HydrateProps) => OctaneNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type HydrationStrategy<
|
|
28
|
+
TWhen extends HydrationWhen = HydrationWhen,
|
|
29
|
+
TCanPrefetch extends boolean = boolean,
|
|
30
|
+
> = OctaneHydrationStrategy<TWhen, TCanPrefetch>;
|
|
31
|
+
|
|
32
|
+
export type HydrateWhen = OctaneHydrationStrategy | (() => OctaneHydrationStrategy);
|
|
33
|
+
|
|
34
|
+
type HydrateCommonOptions = {
|
|
35
|
+
when: HydrateWhen;
|
|
36
|
+
fallback?: OctaneNode;
|
|
37
|
+
onHydrated?: () => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type HydrateOptions =
|
|
41
|
+
| (HydrateCommonOptions & {
|
|
42
|
+
prefetch?: never;
|
|
43
|
+
split?: boolean;
|
|
44
|
+
})
|
|
45
|
+
| (HydrateCommonOptions & {
|
|
46
|
+
prefetch: HydrationPrefetchStrategy;
|
|
47
|
+
split?: true;
|
|
48
|
+
})
|
|
49
|
+
| (HydrateCommonOptions & {
|
|
50
|
+
prefetch: HydrationPrefetchFunction;
|
|
51
|
+
split?: boolean;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type HydrateProps = HydrateOptions & {
|
|
55
|
+
children: OctaneNode;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type InternalHydrateProps = HydrateProps & {
|
|
59
|
+
h?: string;
|
|
60
|
+
p?: () => Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export declare function Hydrate(props: HydrateProps): OctaneNode;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { compileToVolarMappings } from 'octane/compiler/volar';
|
|
2
|
+
import { START_ENVIRONMENT_NAMES } from '#tanstack-start/plugin-core/vite';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Octane counterpart of the start-compiler's `handleClientOnlyJSX` babel pass:
|
|
6
|
+
* on the SERVER environment, `<ClientOnly>` children are removed at compile
|
|
7
|
+
* time (only the `fallback` prop may render during SSR). The upstream pass
|
|
8
|
+
* operates on React JSX via babel; octane's `.tsrx` compiles its JSX away
|
|
9
|
+
* before any babel-based pass could see it, so this plugin performs the strip
|
|
10
|
+
* on the `.tsrx` SOURCE — replacing the children span with a `{null}` hole —
|
|
11
|
+
* before `octane/compiler/vite` runs.
|
|
12
|
+
*
|
|
13
|
+
* Removing the children (rather than relying on the runtime `ClientOnly`,
|
|
14
|
+
* which already renders nothing during SSR) matters for the STATIC graph:
|
|
15
|
+
* client-only subtrees routinely reference `*.client.*` modules, and Start's
|
|
16
|
+
* import-protection verifies after tree-shaking that no denied module stays
|
|
17
|
+
* reachable from the server bundle. With the children intact, the octane
|
|
18
|
+
* server build retained those edges and failed the build where the react
|
|
19
|
+
* build passes.
|
|
20
|
+
*/
|
|
21
|
+
export function octaneClientOnlyServerStrip() {
|
|
22
|
+
return {
|
|
23
|
+
name: 'octanejs-tanstack-start:client-only-server-strip',
|
|
24
|
+
enforce: 'pre',
|
|
25
|
+
applyToEnvironment(environment) {
|
|
26
|
+
return environment.name === START_ENVIRONMENT_NAMES.server;
|
|
27
|
+
},
|
|
28
|
+
transform: {
|
|
29
|
+
filter: {
|
|
30
|
+
id: { include: [/\.tsrx($|\?)/] },
|
|
31
|
+
code: { include: ['ClientOnly'] },
|
|
32
|
+
},
|
|
33
|
+
handler(code, id) {
|
|
34
|
+
if (!code.includes('<ClientOnly')) return undefined;
|
|
35
|
+
|
|
36
|
+
const { sourceAst } = compileToVolarMappings(code, id.split('?')[0]);
|
|
37
|
+
const spans = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const elementName = (node) => node.openingElement?.name?.name ?? node.id?.name;
|
|
40
|
+
const visit = (value) => {
|
|
41
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return;
|
|
42
|
+
seen.add(value);
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
for (const item of value) visit(item);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const children = value.children;
|
|
48
|
+
if (
|
|
49
|
+
elementName(value) === 'ClientOnly' &&
|
|
50
|
+
Array.isArray(children) &&
|
|
51
|
+
children.length > 0
|
|
52
|
+
) {
|
|
53
|
+
const start = children[0].start;
|
|
54
|
+
const end = children[children.length - 1].end;
|
|
55
|
+
if (typeof start === 'number' && typeof end === 'number' && end > start) {
|
|
56
|
+
spans.push([start, end]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const key in value) {
|
|
60
|
+
if (key !== 'metadata' && key !== 'loc') visit(value[key]);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
visit(sourceAst);
|
|
64
|
+
if (spans.length === 0) return undefined;
|
|
65
|
+
|
|
66
|
+
// Replace back-to-front so earlier spans keep their offsets.
|
|
67
|
+
spans.sort((a, b) => b[0] - a[0]);
|
|
68
|
+
let out = code;
|
|
69
|
+
for (const [start, end] of spans) {
|
|
70
|
+
out = `${out.slice(0, start)}{null}${out.slice(end)}`;
|
|
71
|
+
}
|
|
72
|
+
return { code: out, map: null };
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|