@octanejs/tanstack-start 0.1.1 → 0.1.5

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.
Files changed (43) hide show
  1. package/package.json +13 -8
  2. package/src/GenericHydrate.tsrx +396 -0
  3. package/src/GenericHydrate.tsrx.d.ts +5 -0
  4. package/src/Hydrate.tsrx +106 -0
  5. package/src/Hydrate.tsrx.d.ts +63 -0
  6. package/src/client-only-server-strip.js +461 -0
  7. package/src/hydration/generic.d.ts +18 -0
  8. package/src/hydration/generic.js +26 -0
  9. package/src/hydration/idle.d.ts +9 -0
  10. package/src/hydration/idle.js +10 -0
  11. package/src/hydration/load.tsrx +38 -0
  12. package/src/hydration/load.tsrx.d.ts +8 -0
  13. package/src/hydration/never.tsrx +71 -0
  14. package/src/hydration/never.tsrx.d.ts +6 -0
  15. package/src/hydration/visible.tsrx +123 -0
  16. package/src/hydration/visible.tsrx.d.ts +12 -0
  17. package/src/hydration.d.ts +20 -0
  18. package/src/hydration.js +8 -0
  19. package/src/index.d.ts +11 -0
  20. package/src/index.js +2 -0
  21. package/src/internal/router-generator/filesystem/physical/getRouteNodes.js +4 -6
  22. package/src/internal/router-generator/generator.js +3 -0
  23. package/src/internal/router-plugin/core/code-splitter/compilers.js +3 -6
  24. package/src/internal/router-plugin/core/config.d.ts +1 -3
  25. package/src/internal/router-plugin/core/router-code-splitter-plugin.js +6 -3
  26. package/src/internal/router-plugin/esbuild.d.ts +4 -10
  27. package/src/internal/router-plugin/vite.d.ts +4 -10
  28. package/src/internal/start-plugin-core/import-protection/analysis.js +13 -12
  29. package/src/internal/start-plugin-core/import-protection/constants.d.ts +0 -1
  30. package/src/internal/start-plugin-core/import-protection/constants.js +0 -3
  31. package/src/internal/start-plugin-core/schema.d.ts +4 -14
  32. package/src/internal/start-plugin-core/start-compiler/compiler.d.ts +1 -6
  33. package/src/internal/start-plugin-core/start-compiler/compiler.js +4 -4
  34. package/src/internal/start-plugin-core/start-compiler/utils.d.ts +7 -0
  35. package/src/internal/start-plugin-core/start-compiler/utils.js +7 -0
  36. package/src/internal/start-plugin-core/types.d.ts +1 -2
  37. package/src/internal/start-plugin-core/vite/import-protection-plugin/plugin.js +21 -21
  38. package/src/internal/start-plugin-core/vite/module-id.d.ts +6 -0
  39. package/src/internal/start-plugin-core/vite/module-id.js +23 -0
  40. package/src/internal/start-plugin-core/vite/schema.d.ts +3 -12
  41. package/src/internal/start-plugin-core/vite/start-compiler-plugin/plugin.js +9 -3
  42. package/src/plugin-vite.d.ts +9 -1
  43. package/src/plugin-vite.js +20 -1
@@ -0,0 +1,461 @@
1
+ import MagicString from 'magic-string';
2
+ import { compileToVolarMappings } from 'octane/compiler/volar';
3
+ import { START_ENVIRONMENT_NAMES } from '#tanstack-start/plugin-core/vite';
4
+
5
+ /**
6
+ * Remove the children of the Router ClientOnly binding before Octane compiles
7
+ * server TSRX. This keeps client-only imports out of the server module graph,
8
+ * while preserving fallback content and identically-named local components.
9
+ */
10
+ export function octaneClientOnlyServerStrip() {
11
+ return {
12
+ name: 'octanejs-tanstack-start:client-only-server-strip',
13
+ enforce: 'pre',
14
+ applyToEnvironment(environment) {
15
+ return environment.name === START_ENVIRONMENT_NAMES.server;
16
+ },
17
+ transform: {
18
+ filter: {
19
+ id: { include: [/\.tsrx($|\?)/] },
20
+ code: { include: ['ClientOnly'] },
21
+ },
22
+ handler(code, id) {
23
+ if (!code.includes('ClientOnly')) return undefined;
24
+
25
+ const filename = id.split('?', 1)[0];
26
+ const { sourceAst } = compileToVolarMappings(code, filename);
27
+ const childReplacements = stripClientOnlyChildren(sourceAst);
28
+ if (childReplacements.length === 0) return undefined;
29
+
30
+ const prunedImportSpecifiers = findImportsUsedOnlyInRanges(sourceAst, childReplacements);
31
+ const replacements = [
32
+ ...rewritePrunedImports(code, sourceAst, prunedImportSpecifiers),
33
+ ...childReplacements,
34
+ ];
35
+ const output = new MagicString(code);
36
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
37
+ output.overwrite(replacement.start, replacement.end, replacement.content);
38
+ }
39
+
40
+ return {
41
+ code: output.toString(),
42
+ map: output.generateMap({
43
+ source: filename,
44
+ includeContent: true,
45
+ hires: true,
46
+ }),
47
+ };
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ function rewritePrunedImports(code, program, prunedImportSpecifiers) {
54
+ const replacements = [];
55
+
56
+ for (const statement of asNodes(program.body)) {
57
+ if (
58
+ statement.type !== 'ImportDeclaration' ||
59
+ !hasRange(statement) ||
60
+ !hasRange(statement.source)
61
+ ) {
62
+ continue;
63
+ }
64
+
65
+ const prunedSpecifiers = prunedImportSpecifiers.get(statement);
66
+ if (!prunedSpecifiers?.size) continue;
67
+
68
+ const remainingSpecifiers = (statement.specifiers ?? []).filter(
69
+ (specifier) => !prunedSpecifiers.has(specifier),
70
+ );
71
+ replacements.push({
72
+ start: statement.start,
73
+ end: statement.end,
74
+ content: printRemainingImport(code, statement, remainingSpecifiers),
75
+ });
76
+ }
77
+
78
+ return replacements;
79
+ }
80
+
81
+ function findImportsUsedOnlyInRanges(program, removedRanges) {
82
+ const bindings = new Map();
83
+ for (const statement of asNodes(program.body)) {
84
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type') {
85
+ continue;
86
+ }
87
+ for (const specifier of statement.specifiers ?? []) {
88
+ const localName = specifier.local?.name;
89
+ if (specifier.importKind === 'type' || !localName) continue;
90
+ bindings.set(localName, {
91
+ declaration: statement,
92
+ specifier,
93
+ removed: false,
94
+ live: false,
95
+ });
96
+ }
97
+ }
98
+
99
+ if (bindings.size === 0 || removedRanges.length === 0) return new Map();
100
+
101
+ visitImportedBindingReferences(program, bindings, removedRanges);
102
+
103
+ const result = new Map();
104
+ for (const usage of bindings.values()) {
105
+ // Keep imports that were already unused: importing may intentionally run
106
+ // module initialization. Only remove bindings whose uses were stripped.
107
+ if (!usage.removed || usage.live) continue;
108
+ const specifiers = result.get(usage.declaration) ?? new Set();
109
+ specifiers.add(usage.specifier);
110
+ result.set(usage.declaration, specifiers);
111
+ }
112
+ return result;
113
+ }
114
+
115
+ function visitImportedBindingReferences(program, bindings, removedRanges) {
116
+ const visit = (value, shadowed, parent, parentKey, bindingPattern = false) => {
117
+ if (!value || typeof value !== 'object') return;
118
+ if (Array.isArray(value)) {
119
+ for (const item of value) {
120
+ visit(item, shadowed, parent, parentKey, bindingPattern);
121
+ }
122
+ return;
123
+ }
124
+
125
+ const node = value;
126
+ if (node.type === 'ImportDeclaration') return;
127
+
128
+ const scopedNames = scopeBindings(node);
129
+ const nextShadowed = scopedNames.size ? new Set([...shadowed, ...scopedNames]) : shadowed;
130
+
131
+ if (
132
+ !bindingPattern &&
133
+ isBindingReference(node, parent, parentKey) &&
134
+ node.name &&
135
+ !nextShadowed.has(node.name)
136
+ ) {
137
+ const usage = bindings.get(node.name);
138
+ if (usage && hasRange(node)) {
139
+ if (isInsideRange(node, removedRanges)) usage.removed = true;
140
+ else usage.live = true;
141
+ }
142
+ }
143
+
144
+ for (const [key, child] of Object.entries(node)) {
145
+ if (key === 'metadata' || key === 'loc' || key === 'parent') continue;
146
+ visit(child, nextShadowed, node, key, isBindingPatternChild(node, key, bindingPattern));
147
+ }
148
+ };
149
+
150
+ visit(program, new Set());
151
+ }
152
+
153
+ function isBindingReference(node, parent, parentKey) {
154
+ if (node.type === 'Identifier') {
155
+ if (!parent) return true;
156
+ if (
157
+ (parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') &&
158
+ parentKey === 'property' &&
159
+ !parent.computed
160
+ ) {
161
+ return false;
162
+ }
163
+ if (
164
+ (parent.type === 'Property' ||
165
+ parent.type === 'PropertyDefinition' ||
166
+ parent.type === 'MethodDefinition') &&
167
+ parentKey === 'key' &&
168
+ !parent.computed
169
+ ) {
170
+ return Boolean(parent.shorthand);
171
+ }
172
+ if (parent.type === 'ExportSpecifier') return parentKey === 'local';
173
+ if (
174
+ (parent.type === 'LabeledStatement' ||
175
+ parent.type === 'BreakStatement' ||
176
+ parent.type === 'ContinueStatement') &&
177
+ parentKey === 'label'
178
+ ) {
179
+ return false;
180
+ }
181
+ return true;
182
+ }
183
+
184
+ if (node.type !== 'JSXIdentifier' || !node.name || !parent) return false;
185
+ if (
186
+ (parent.type === 'JSXOpeningElement' || parent.type === 'JSXClosingElement') &&
187
+ parentKey === 'name'
188
+ ) {
189
+ return true;
190
+ }
191
+ return parent.type === 'JSXMemberExpression' && parentKey === 'object';
192
+ }
193
+
194
+ function isBindingPatternChild(parent, key, parentIsBindingPattern) {
195
+ if (parentIsBindingPattern) {
196
+ if (parent.type === 'AssignmentPattern') return key === 'left';
197
+ if (parent.type === 'Property') {
198
+ return key === 'value' || (key === 'key' && !parent.computed);
199
+ }
200
+ return true;
201
+ }
202
+
203
+ if (parent.type === 'VariableDeclarator') return key === 'id';
204
+ if (isFunction(parent)) return key === 'id' || key === 'params';
205
+ if (parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression') {
206
+ return key === 'id';
207
+ }
208
+ if (parent.type === 'CatchClause') return key === 'param';
209
+ if (parent.type === 'ImportSpecifier') return true;
210
+ return false;
211
+ }
212
+
213
+ function isInsideRange(node, ranges) {
214
+ return ranges.some((range) => range.start <= node.start && range.end >= node.end);
215
+ }
216
+
217
+ function printRemainingImport(code, statement, specifiers) {
218
+ const sourceNode = statement.source;
219
+ if (specifiers.length === 0 || !hasRange(sourceNode)) return '';
220
+
221
+ const defaultSpecifier = specifiers.find(
222
+ (specifier) => specifier.type === 'ImportDefaultSpecifier',
223
+ );
224
+ const namespaceSpecifier = specifiers.find(
225
+ (specifier) => specifier.type === 'ImportNamespaceSpecifier',
226
+ );
227
+ const namedSpecifiers = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
228
+ const clauses = [];
229
+
230
+ if (defaultSpecifier && hasRange(defaultSpecifier)) {
231
+ clauses.push(code.slice(defaultSpecifier.start, defaultSpecifier.end));
232
+ }
233
+ if (namespaceSpecifier && hasRange(namespaceSpecifier)) {
234
+ clauses.push(code.slice(namespaceSpecifier.start, namespaceSpecifier.end));
235
+ }
236
+ if (namedSpecifiers.length > 0) {
237
+ clauses.push(
238
+ `{ ${namedSpecifiers
239
+ .filter(hasRange)
240
+ .map((specifier) => code.slice(specifier.start, specifier.end))
241
+ .join(', ')} }`,
242
+ );
243
+ }
244
+
245
+ const source = code.slice(sourceNode.start, sourceNode.end);
246
+ const suffix = hasRange(statement) ? code.slice(sourceNode.end, statement.end) : '';
247
+ return `import ${clauses.join(', ')} from ${source}${suffix}`;
248
+ }
249
+
250
+ function stripClientOnlyChildren(program) {
251
+ const importedNames = new Set();
252
+ for (const statement of asNodes(program.body)) {
253
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type') {
254
+ continue;
255
+ }
256
+
257
+ for (const specifier of statement.specifiers ?? []) {
258
+ if (
259
+ specifier.type === 'ImportSpecifier' &&
260
+ specifier.importKind !== 'type' &&
261
+ specifier.imported?.name === 'ClientOnly' &&
262
+ specifier.local?.name
263
+ ) {
264
+ importedNames.add(specifier.local.name);
265
+ }
266
+ }
267
+ }
268
+
269
+ if (importedNames.size === 0) return [];
270
+
271
+ const replacements = [];
272
+ const visited = new WeakSet();
273
+ const visit = (value, shadowed) => {
274
+ if (!value || typeof value !== 'object' || visited.has(value)) return;
275
+ visited.add(value);
276
+
277
+ if (Array.isArray(value)) {
278
+ for (const item of value) visit(item, shadowed);
279
+ return;
280
+ }
281
+
282
+ const node = value;
283
+ const scopedNames = scopeBindings(node);
284
+ const nextShadowed = scopedNames.size ? new Set([...shadowed, ...scopedNames]) : shadowed;
285
+ const elementName = node.openingElement?.name?.name;
286
+
287
+ if (
288
+ node.type === 'JSXElement' &&
289
+ elementName &&
290
+ importedNames.has(elementName) &&
291
+ !nextShadowed.has(elementName) &&
292
+ node.children?.length
293
+ ) {
294
+ const first = node.children[0];
295
+ const last = node.children[node.children.length - 1];
296
+ if (first && last && hasRange(first) && hasRange(last) && last.end > first.start) {
297
+ replacements.push({
298
+ start: first.start,
299
+ end: last.end,
300
+ content: '{null}',
301
+ });
302
+ }
303
+ }
304
+
305
+ for (const [key, child] of Object.entries(node)) {
306
+ if (key !== 'metadata' && key !== 'loc' && key !== 'parent') {
307
+ visit(child, nextShadowed);
308
+ }
309
+ }
310
+ };
311
+
312
+ visit(program, new Set());
313
+
314
+ // Replacing an outer ClientOnly child range also removes nested boundaries.
315
+ return replacements.filter(
316
+ (candidate, index) =>
317
+ !replacements.some(
318
+ (other, otherIndex) =>
319
+ otherIndex !== index && other.start <= candidate.start && other.end >= candidate.end,
320
+ ),
321
+ );
322
+ }
323
+
324
+ function scopeBindings(node) {
325
+ const names = new Set();
326
+
327
+ if (isFunction(node)) {
328
+ for (const param of node.params ?? []) collectBindingNames(param, names);
329
+ collectBindingNames(node.id, names);
330
+ for (const statement of directStatements(node.body)) {
331
+ collectStatementBindings(statement, names);
332
+ }
333
+ collectFunctionVarBindings(node.body, names);
334
+ } else if (node.type === 'BlockStatement' || node.type === 'JSXCodeBlock') {
335
+ for (const statement of directStatements(node)) {
336
+ collectStatementBindings(statement, names);
337
+ }
338
+ } else if (node.type === 'CatchClause') {
339
+ collectBindingNames(node.param, names);
340
+ } else if (
341
+ node.type === 'ForStatement' ||
342
+ node.type === 'ForInStatement' ||
343
+ node.type === 'ForOfStatement'
344
+ ) {
345
+ const declaration = node.init ?? node.left;
346
+ if (declaration?.type === 'VariableDeclaration' && declaration.kind !== 'var') {
347
+ for (const item of declaration.declarations ?? []) {
348
+ collectBindingNames(item.id, names);
349
+ }
350
+ }
351
+ } else if (node.type === 'SwitchStatement') {
352
+ for (const switchCase of asNodes(node.cases)) {
353
+ for (const statement of asNodes(switchCase.consequent)) {
354
+ collectStatementBindings(statement, names);
355
+ }
356
+ }
357
+ } else if (node.type === 'StaticBlock') {
358
+ for (const statement of directStatements(node)) {
359
+ collectStatementBindings(statement, names);
360
+ }
361
+ } else if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
362
+ collectBindingNames(node.id, names);
363
+ }
364
+
365
+ return names;
366
+ }
367
+
368
+ function directStatements(node) {
369
+ if (Array.isArray(node)) return node;
370
+ if (!node || !Array.isArray(node.body)) return [];
371
+ return asNodes(node.body);
372
+ }
373
+
374
+ function collectStatementBindings(statement, output) {
375
+ const declaration =
376
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
377
+ ? statement.declaration
378
+ : statement;
379
+
380
+ if (declaration?.type === 'VariableDeclaration') {
381
+ for (const item of declaration.declarations ?? []) {
382
+ collectBindingNames(item.id, output);
383
+ }
384
+ } else if (
385
+ declaration?.type === 'FunctionDeclaration' ||
386
+ declaration?.type === 'ClassDeclaration'
387
+ ) {
388
+ collectBindingNames(declaration.id, output);
389
+ }
390
+ }
391
+
392
+ function collectFunctionVarBindings(value, output) {
393
+ const visited = new WeakSet();
394
+ const visit = (child, root = false) => {
395
+ if (!child || typeof child !== 'object' || visited.has(child)) return;
396
+ visited.add(child);
397
+
398
+ if (Array.isArray(child)) {
399
+ for (const item of child) visit(item);
400
+ return;
401
+ }
402
+
403
+ const node = child;
404
+ if (!root && isFunction(node)) return;
405
+ if (node.type === 'VariableDeclaration' && node.kind === 'var') {
406
+ for (const item of node.declarations ?? []) {
407
+ collectBindingNames(item.id, output);
408
+ }
409
+ }
410
+ for (const [key, nested] of Object.entries(node)) {
411
+ if (key !== 'metadata' && key !== 'loc' && key !== 'parent') {
412
+ visit(nested);
413
+ }
414
+ }
415
+ };
416
+
417
+ visit(value, true);
418
+ }
419
+
420
+ function collectBindingNames(pattern, output) {
421
+ if (!pattern) return;
422
+ if (pattern.type === 'Identifier' && pattern.name) {
423
+ output.add(pattern.name);
424
+ return;
425
+ }
426
+ if (pattern.type === 'RestElement') {
427
+ collectBindingNames(pattern.argument, output);
428
+ return;
429
+ }
430
+ if (pattern.type === 'AssignmentPattern') {
431
+ collectBindingNames(pattern.left, output);
432
+ return;
433
+ }
434
+ if (pattern.type === 'ArrayPattern') {
435
+ for (const element of asNodes(pattern.elements)) {
436
+ collectBindingNames(element, output);
437
+ }
438
+ return;
439
+ }
440
+ if (pattern.type === 'ObjectPattern') {
441
+ for (const property of asNodes(pattern.properties)) {
442
+ collectBindingNames(property.argument ?? property.value, output);
443
+ }
444
+ }
445
+ }
446
+
447
+ function isFunction(node) {
448
+ return (
449
+ node.type === 'FunctionDeclaration' ||
450
+ node.type === 'FunctionExpression' ||
451
+ node.type === 'ArrowFunctionExpression'
452
+ );
453
+ }
454
+
455
+ function hasRange(node) {
456
+ return typeof node?.start === 'number' && typeof node.end === 'number';
457
+ }
458
+
459
+ function asNodes(value) {
460
+ return Array.isArray(value) ? value : [];
461
+ }
@@ -0,0 +1,18 @@
1
+ import type {
2
+ HydrationCondition,
3
+ HydrationInteractionEvents,
4
+ HydrationPrefetchStrategy,
5
+ } from '@tanstack/start-client-core/hydration';
6
+ import type { OctaneHydrationStrategy } from '../Hydrate.tsrx';
7
+
8
+ export declare function media(
9
+ query: string,
10
+ ): OctaneHydrationStrategy<'media', true> & HydrationPrefetchStrategy<'media'>;
11
+
12
+ export declare function condition(
13
+ condition: HydrationCondition,
14
+ ): OctaneHydrationStrategy<'condition', false>;
15
+
16
+ export declare function interaction(options?: {
17
+ events?: HydrationInteractionEvents;
18
+ }): OctaneHydrationStrategy<'interaction', true> & HydrationPrefetchStrategy<'interaction'>;
@@ -0,0 +1,26 @@
1
+ // media / condition / interaction hydration strategies — port of
2
+ // @tanstack/react-start-client's hydration/generic.ts. The gating logic lives
3
+ // in @tanstack/start-client-core; these factories just attach octane's
4
+ // GenericHydrate renderer.
5
+ import {
6
+ condition as coreCondition,
7
+ interaction as coreInteraction,
8
+ media as coreMedia,
9
+ withHydrationRenderer,
10
+ } from '@tanstack/start-client-core/hydration';
11
+ import { GenericHydrate } from '../GenericHydrate.tsrx';
12
+
13
+ /* @__NO_SIDE_EFFECTS__ */
14
+ export function media(query) {
15
+ return /* @__PURE__ */ withHydrationRenderer(coreMedia(query), GenericHydrate);
16
+ }
17
+
18
+ /* @__NO_SIDE_EFFECTS__ */
19
+ export function condition(condition) {
20
+ return /* @__PURE__ */ withHydrationRenderer(coreCondition(condition), GenericHydrate);
21
+ }
22
+
23
+ /* @__NO_SIDE_EFFECTS__ */
24
+ export function interaction(options) {
25
+ return /* @__PURE__ */ withHydrationRenderer(coreInteraction(options), GenericHydrate);
26
+ }
@@ -0,0 +1,9 @@
1
+ import type {
2
+ HydrationPrefetchStrategy,
3
+ IdleHydrationOptions,
4
+ } from '@tanstack/start-client-core/hydration';
5
+ import type { OctaneHydrationStrategy } from '../Hydrate.tsrx';
6
+
7
+ export declare function idle(
8
+ options?: IdleHydrationOptions,
9
+ ): OctaneHydrationStrategy<'idle', true> & HydrationPrefetchStrategy<'idle'>;
@@ -0,0 +1,10 @@
1
+ // idle hydration strategy — port of @tanstack/react-start-client's
2
+ // hydration/idle.ts. Delegates the requestIdleCallback gating to
3
+ // @tanstack/start-client-core and attaches octane's GenericHydrate renderer.
4
+ import { idle as coreIdle, withHydrationRenderer } from '@tanstack/start-client-core/hydration';
5
+ import { GenericHydrate } from '../GenericHydrate.tsrx';
6
+
7
+ /* @__NO_SIDE_EFFECTS__ */
8
+ export function idle(options = {}) {
9
+ return /* @__PURE__ */ withHydrationRenderer(coreIdle(options), GenericHydrate);
10
+ }
@@ -0,0 +1,38 @@
1
+ // load hydration strategy — port of @tanstack/react-start-client's
2
+ // hydration/load.tsx. `load` hydrates immediately, so its renderer skips the
3
+ // marker/gate machinery entirely: a bare Suspense wrapper plus an onHydrated
4
+ // notification effect.
5
+ import { Suspense, useEffect, useRef } from 'octane';
6
+ import type { OctaneNode } from 'octane';
7
+ import { load as coreLoad, withHydrationRenderer } from '@tanstack/start-client-core/hydration';
8
+ import type { HydrateProps } from '../Hydrate.tsrx';
9
+
10
+ function HydratedBoundary(props: { onHydrated?: () => void; children?: OctaneNode }) {
11
+ const { onHydrated } = props;
12
+ const didHydrateRef = useRef(false);
13
+
14
+ useEffect(() => {
15
+ if (didHydrateRef.current) return;
16
+ didHydrateRef.current = true;
17
+ onHydrated?.();
18
+ }, [onHydrated]);
19
+
20
+ return props.children;
21
+ }
22
+
23
+ // OCTANE ADAPTATION: octane's `Hydrate` renders `_h` as a child component
24
+ // (upstream bare-calls it inline); LoadHydrate is that component.
25
+ export function LoadHydrate(props: HydrateProps) @{
26
+ <div>
27
+ <Suspense fallback={props.fallback ?? null}>
28
+ <HydratedBoundary onHydrated={props.onHydrated}>{props.children}</HydratedBoundary>
29
+ </Suspense>
30
+ </div>
31
+ }
32
+
33
+ const loadStrategy = /* @__PURE__ */ withHydrationRenderer(coreLoad(), LoadHydrate);
34
+
35
+ /* @__NO_SIDE_EFFECTS__ */
36
+ export function load() {
37
+ return loadStrategy;
38
+ }
@@ -0,0 +1,8 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { HydrationPrefetchStrategy } from '@tanstack/start-client-core/hydration';
3
+ import type { HydrateProps, OctaneHydrationStrategy } from '../Hydrate.tsrx';
4
+
5
+ export declare function LoadHydrate(props: HydrateProps): OctaneNode;
6
+
7
+ export declare function load(): OctaneHydrationStrategy<'load', true> &
8
+ HydrationPrefetchStrategy<'load'>;
@@ -0,0 +1,71 @@
1
+ // never hydration strategy — port of @tanstack/react-start-client's
2
+ // hydration/never.tsx. Server HTML is preserved verbatim and the subtree is
3
+ // never hydrated: the gate promise never resolves, so the Suspense boundary
4
+ // keeps showing the saved server HTML (re-injected via dangerouslySetInnerHTML)
5
+ // forever. `reactUse` feature-detection is dropped — octane's `use` always
6
+ // exists.
7
+ import { Suspense, use, useCallback, useId, useRef } from 'octane';
8
+ import type { OctaneNode } from 'octane';
9
+ import { useHydrated } from '@octanejs/tanstack-router';
10
+ import { isServer } from '@tanstack/router-core/isServer';
11
+ import { never as coreNever, withHydrationRenderer } from '@tanstack/start-client-core/hydration';
12
+ import {
13
+ hydrateIdAttribute,
14
+ hydrateWhenAttribute,
15
+ } from '@tanstack/start-client-core/hydration/constants';
16
+ import { getFallbackHtml, saveFallbackHtml } from '@tanstack/start-client-core/hydration/runtime';
17
+ import type { HydrateProps, InternalHydrateProps } from '../Hydrate.tsrx';
18
+
19
+ const neverType = 'never';
20
+ const neverPromise = new Promise<void>(() => {});
21
+
22
+ function NeverGate(props: { children?: OctaneNode }) {
23
+ if (isServer ?? typeof window === 'undefined') {
24
+ return props.children;
25
+ }
26
+
27
+ use(neverPromise);
28
+
29
+ return props.children;
30
+ }
31
+
32
+ // OCTANE ADAPTATION: octane's `Hydrate` renders `_h` as a child component
33
+ // (upstream bare-calls it inline); NeverHydrate is that component.
34
+ export function NeverHydrate(props: HydrateProps) @{
35
+ const internalProps = props as InternalHydrateProps;
36
+ const hydrated = useHydrated();
37
+ const octaneId = useId();
38
+ const id = internalProps.h ? `${internalProps.h}${octaneId}` : octaneId;
39
+ const shouldPreserveServerHTMLRef = useRef<boolean | undefined>(undefined);
40
+ shouldPreserveServerHTMLRef.current ??= (isServer ?? typeof window === 'undefined') || !hydrated;
41
+ const markerRef = useCallback((element: HTMLDivElement | null) => {
42
+ if (!element) return;
43
+ if (!shouldPreserveServerHTMLRef.current) {
44
+ element.replaceChildren();
45
+ } else {
46
+ saveFallbackHtml(id, element);
47
+ }
48
+ }, [id]);
49
+ const savedHtml = getFallbackHtml(id);
50
+
51
+ <div
52
+ ref={markerRef}
53
+ {...{
54
+ [hydrateIdAttribute]: id,
55
+ [hydrateWhenAttribute]: neverType,
56
+ }}
57
+ >
58
+ <Suspense
59
+ fallback={savedHtml
60
+ ? <div style={{ display: 'contents' }} dangerouslySetInnerHTML={{ __html: savedHtml }} />
61
+ : props.fallback ?? null}
62
+ >
63
+ <NeverGate>{props.children}</NeverGate>
64
+ </Suspense>
65
+ </div>
66
+ }
67
+
68
+ /* @__NO_SIDE_EFFECTS__ */
69
+ export function never() {
70
+ return /* @__PURE__ */ withHydrationRenderer(coreNever(), NeverHydrate);
71
+ }
@@ -0,0 +1,6 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { HydrateProps, OctaneHydrationStrategy } from '../Hydrate.tsrx';
3
+
4
+ export declare function NeverHydrate(props: HydrateProps): OctaneNode;
5
+
6
+ export declare function never(): OctaneHydrationStrategy<'never', false>;