@tanstack/redact 0.0.21 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/dist/core/attributes.js +2 -1
  2. package/dist/core/attributes.js.map +2 -2
  3. package/dist/core/browser.d.ts +9 -0
  4. package/dist/core/browser.js +35 -0
  5. package/dist/core/browser.js.map +7 -0
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.js +3 -3
  8. package/dist/core/internal.d.ts +31 -8
  9. package/dist/core/internal.js +3 -1
  10. package/dist/core/internal.js.map +2 -2
  11. package/dist/core/resource-hints.d.ts +7 -0
  12. package/dist/core/resource-hints.js +19 -0
  13. package/dist/core/resource-hints.js.map +7 -0
  14. package/dist/core/types.js +1 -1
  15. package/dist/dom/_all.d.ts +11 -16
  16. package/dist/dom/_all.js +9 -20
  17. package/dist/dom/_all.js.map +2 -2
  18. package/dist/dom/client.d.ts +4 -4
  19. package/dist/dom/client.js +4 -4
  20. package/dist/dom/commit.d.ts +23 -0
  21. package/dist/dom/commit.js +191 -0
  22. package/dist/dom/commit.js.map +7 -0
  23. package/dist/dom/dispatcher.d.ts +4 -22
  24. package/dist/dom/dispatcher.js +184 -126
  25. package/dist/dom/dispatcher.js.map +3 -3
  26. package/dist/dom/dom.d.ts +2 -0
  27. package/dist/dom/dom.js +72 -52
  28. package/dist/dom/dom.js.map +2 -2
  29. package/dist/dom/error-info.d.ts +2 -0
  30. package/dist/dom/error-info.js +16 -0
  31. package/dist/dom/error-info.js.map +7 -0
  32. package/dist/dom/event-replay.js +1 -1
  33. package/dist/dom/features/activity/full.d.ts +1 -0
  34. package/dist/dom/features/activity/full.js +162 -0
  35. package/dist/dom/features/activity/full.js.map +7 -0
  36. package/dist/dom/features/activity/index.d.ts +1 -0
  37. package/dist/dom/features/activity/index.js +3 -0
  38. package/dist/dom/features/activity/index.js.map +7 -0
  39. package/dist/dom/features/activity/stub.d.ts +1 -0
  40. package/dist/dom/features/activity/stub.js +12 -0
  41. package/dist/dom/features/activity/stub.js.map +7 -0
  42. package/dist/dom/features/class/full.js +87 -27
  43. package/dist/dom/features/class/full.js.map +2 -2
  44. package/dist/dom/features/class/index.d.ts +1 -1
  45. package/dist/dom/features/class/index.js +2 -2
  46. package/dist/dom/features/class/stub.js +3 -3
  47. package/dist/dom/features/context/full.d.ts +4 -1
  48. package/dist/dom/features/context/full.js +72 -15
  49. package/dist/dom/features/context/full.js.map +2 -2
  50. package/dist/dom/features/context/index.d.ts +1 -1
  51. package/dist/dom/features/context/index.js +2 -2
  52. package/dist/dom/features/context/stub.d.ts +4 -1
  53. package/dist/dom/features/context/stub.js +17 -8
  54. package/dist/dom/features/context/stub.js.map +2 -2
  55. package/dist/dom/features/forward-ref/full.js +12 -21
  56. package/dist/dom/features/forward-ref/full.js.map +2 -2
  57. package/dist/dom/features/forward-ref/index.d.ts +1 -1
  58. package/dist/dom/features/forward-ref/index.js +2 -2
  59. package/dist/dom/features/forward-ref/stub.js +4 -4
  60. package/dist/dom/features/fragment-refs/index.d.ts +1 -0
  61. package/dist/dom/features/fragment-refs/index.js +8 -0
  62. package/dist/dom/features/fragment-refs/index.js.map +7 -0
  63. package/dist/dom/features/fragment-refs/stub.d.ts +3 -0
  64. package/dist/dom/features/fragment-refs/stub.js +14 -0
  65. package/dist/dom/features/fragment-refs/stub.js.map +7 -0
  66. package/dist/dom/features/hydration/full.d.ts +11 -6
  67. package/dist/dom/features/hydration/full.js +76 -77
  68. package/dist/dom/features/hydration/full.js.map +2 -2
  69. package/dist/dom/features/hydration/index.d.ts +1 -1
  70. package/dist/dom/features/hydration/index.js +2 -2
  71. package/dist/dom/features/hydration/stub.d.ts +7 -2
  72. package/dist/dom/features/hydration/stub.js +1 -1
  73. package/dist/dom/features/hydration/stub.js.map +1 -1
  74. package/dist/dom/features/index.d.ts +8 -7
  75. package/dist/dom/features/index.js +9 -8
  76. package/dist/dom/features/index.js.map +2 -2
  77. package/dist/dom/features/lazy/full.js +4 -4
  78. package/dist/dom/features/lazy/index.d.ts +1 -1
  79. package/dist/dom/features/lazy/index.js +2 -2
  80. package/dist/dom/features/lazy/stub.js +4 -4
  81. package/dist/dom/features/memo/full.js +8 -6
  82. package/dist/dom/features/memo/full.js.map +2 -2
  83. package/dist/dom/features/memo/index.d.ts +1 -1
  84. package/dist/dom/features/memo/index.js +2 -2
  85. package/dist/dom/features/memo/stub.js +4 -4
  86. package/dist/dom/features/portal/full.js +4 -4
  87. package/dist/dom/features/portal/index.d.ts +1 -1
  88. package/dist/dom/features/portal/index.js +2 -2
  89. package/dist/dom/features/portal/stub.js +4 -4
  90. package/dist/dom/features/suspense/full.js +197 -52
  91. package/dist/dom/features/suspense/full.js.map +2 -2
  92. package/dist/dom/features/suspense/index.d.ts +1 -1
  93. package/dist/dom/features/suspense/index.js +2 -2
  94. package/dist/dom/features/suspense/stub.js +4 -4
  95. package/dist/dom/features/view-transition/full.d.ts +6 -0
  96. package/dist/dom/features/view-transition/full.js +280 -0
  97. package/dist/dom/features/view-transition/full.js.map +7 -0
  98. package/dist/dom/features/view-transition/index.d.ts +1 -0
  99. package/dist/dom/features/view-transition/index.js +3 -0
  100. package/dist/dom/features/view-transition/index.js.map +7 -0
  101. package/dist/dom/features/view-transition/resources.d.ts +11 -0
  102. package/dist/dom/features/view-transition/resources.js +108 -0
  103. package/dist/dom/features/view-transition/resources.js.map +7 -0
  104. package/dist/dom/features/view-transition/stub.d.ts +6 -0
  105. package/dist/dom/features/view-transition/stub.js +22 -0
  106. package/dist/dom/features/view-transition/stub.js.map +7 -0
  107. package/dist/dom/features/view-transition/visual.d.ts +14 -0
  108. package/dist/dom/features/view-transition/visual.js +356 -0
  109. package/dist/dom/features/view-transition/visual.js.map +7 -0
  110. package/dist/dom/fragment-instance.d.ts +40 -0
  111. package/dist/dom/fragment-instance.js +324 -0
  112. package/dist/dom/fragment-instance.js.map +7 -0
  113. package/dist/dom/index.d.ts +11 -11
  114. package/dist/dom/index.js +11 -18
  115. package/dist/dom/index.js.map +3 -3
  116. package/dist/dom/portal.d.ts +2 -2
  117. package/dist/dom/portal.js +2 -2
  118. package/dist/dom/portal.js.map +1 -1
  119. package/dist/dom/reconcile.d.ts +32 -3
  120. package/dist/dom/reconcile.js +689 -221
  121. package/dist/dom/reconcile.js.map +2 -2
  122. package/dist/dom/resource-hints.d.ts +6 -0
  123. package/dist/dom/resource-hints.js +115 -0
  124. package/dist/dom/resource-hints.js.map +7 -0
  125. package/dist/dom/resource-store.d.ts +13 -0
  126. package/dist/dom/resource-store.js +43 -0
  127. package/dist/dom/resource-store.js.map +7 -0
  128. package/dist/dom/resources.d.ts +1 -0
  129. package/dist/dom/resources.js +44 -0
  130. package/dist/dom/resources.js.map +7 -0
  131. package/dist/dom/retained-effects.d.ts +6 -0
  132. package/dist/dom/retained-effects.js +49 -0
  133. package/dist/dom/retained-effects.js.map +7 -0
  134. package/dist/dom/root-internal.d.ts +4 -4
  135. package/dist/dom/root-internal.js +2 -2
  136. package/dist/dom/root-internal.js.map +2 -2
  137. package/dist/dom/root.d.ts +5 -5
  138. package/dist/dom/root.js +11 -11
  139. package/dist/dom/root.js.map +2 -2
  140. package/dist/dom/test-utils.js +1 -1
  141. package/dist/react/activity.d.ts +7 -0
  142. package/dist/react/activity.js +8 -0
  143. package/dist/react/activity.js.map +7 -0
  144. package/dist/react/children.d.ts +1 -1
  145. package/dist/react/children.js +3 -3
  146. package/dist/react/children.js.map +1 -1
  147. package/dist/react/class.d.ts +1 -1
  148. package/dist/react/class.js +3 -3
  149. package/dist/react/class.js.map +1 -1
  150. package/dist/react/compiler-runtime.js +2 -2
  151. package/dist/react/context.d.ts +7 -2
  152. package/dist/react/context.js +3 -11
  153. package/dist/react/context.js.map +3 -3
  154. package/dist/react/element.d.ts +6 -3
  155. package/dist/react/element.js +7 -11
  156. package/dist/react/element.js.map +2 -2
  157. package/dist/react/hooks.d.ts +26 -7
  158. package/dist/react/hooks.js +13 -3
  159. package/dist/react/hooks.js.map +2 -2
  160. package/dist/react/index.d.ts +39 -24
  161. package/dist/react/index.js +39 -19
  162. package/dist/react/index.js.map +3 -3
  163. package/dist/react/jsx-runtime.d.ts +2 -2
  164. package/dist/react/jsx-runtime.js +3 -3
  165. package/dist/react/memo.d.ts +1 -1
  166. package/dist/react/memo.js +1 -1
  167. package/dist/react/portal.js +1 -1
  168. package/dist/react/shared-internals.d.ts +15 -5
  169. package/dist/react/shared-internals.js +2 -3
  170. package/dist/react/shared-internals.js.map +2 -2
  171. package/dist/react/suspense.js +1 -1
  172. package/dist/react/view-transition.d.ts +32 -0
  173. package/dist/react/view-transition.js +13 -0
  174. package/dist/react/view-transition.js.map +7 -0
  175. package/dist/scheduler/index.js +1 -1
  176. package/dist/server/bootstrap-script.d.ts +1 -23
  177. package/dist/server/bootstrap-script.js +7 -6
  178. package/dist/server/bootstrap-script.js.map +2 -2
  179. package/dist/server/dispatcher.d.ts +28 -6
  180. package/dist/server/dispatcher.js +33 -15
  181. package/dist/server/dispatcher.js.map +2 -2
  182. package/dist/server/escape.js +16 -15
  183. package/dist/server/escape.js.map +2 -2
  184. package/dist/server/index.d.ts +6 -6
  185. package/dist/server/index.js +7 -7
  186. package/dist/server/renderToString.d.ts +1 -1
  187. package/dist/server/renderToString.js +12 -8
  188. package/dist/server/renderToString.js.map +2 -2
  189. package/dist/server/resource-hints.d.ts +22 -0
  190. package/dist/server/resource-hints.js +148 -0
  191. package/dist/server/resource-hints.js.map +7 -0
  192. package/dist/server/stream.d.ts +15 -2
  193. package/dist/server/stream.js +155 -63
  194. package/dist/server/stream.js.map +2 -2
  195. package/dist/server/walk.d.ts +15 -2
  196. package/dist/server/walk.js +150 -110
  197. package/dist/server/walk.js.map +2 -2
  198. package/dist/vite/index.d.ts +6 -0
  199. package/dist/vite/index.js +22 -6
  200. package/dist/vite/index.js.map +2 -2
  201. package/package.json +1 -1
  202. package/src/core/attributes.ts +1 -0
  203. package/src/core/browser.ts +33 -0
  204. package/src/core/internal.ts +24 -6
  205. package/src/core/resource-hints.ts +36 -0
  206. package/src/dom/_all.ts +1 -7
  207. package/src/dom/commit.ts +186 -0
  208. package/src/dom/dispatcher.ts +190 -134
  209. package/src/dom/dom.ts +70 -54
  210. package/src/dom/error-info.ts +15 -0
  211. package/src/dom/features/activity/full.ts +166 -0
  212. package/src/dom/features/activity/index.ts +1 -0
  213. package/src/dom/features/activity/stub.ts +14 -0
  214. package/src/dom/features/class/full.ts +72 -28
  215. package/src/dom/features/context/full.ts +70 -13
  216. package/src/dom/features/context/stub.ts +10 -5
  217. package/src/dom/features/forward-ref/full.ts +9 -19
  218. package/src/dom/features/fragment-refs/index.ts +1 -0
  219. package/src/dom/features/fragment-refs/stub.ts +3 -0
  220. package/src/dom/features/hydration/full.ts +84 -85
  221. package/src/dom/features/hydration/stub.ts +1 -1
  222. package/src/dom/features/index.ts +1 -0
  223. package/src/dom/features/memo/full.ts +4 -3
  224. package/src/dom/features/suspense/full.ts +192 -55
  225. package/src/dom/features/view-transition/full.ts +288 -0
  226. package/src/dom/features/view-transition/index.ts +1 -0
  227. package/src/dom/features/view-transition/resources.ts +107 -0
  228. package/src/dom/features/view-transition/stub.ts +7 -0
  229. package/src/dom/features/view-transition/visual.ts +406 -0
  230. package/src/dom/fragment-instance.ts +337 -0
  231. package/src/dom/index.ts +6 -7
  232. package/src/dom/portal.ts +1 -1
  233. package/src/dom/reconcile.ts +695 -242
  234. package/src/dom/resource-hints.ts +115 -0
  235. package/src/dom/resource-store.ts +52 -0
  236. package/src/dom/resources.ts +39 -0
  237. package/src/dom/retained-effects.ts +38 -0
  238. package/src/dom/root-internal.ts +4 -4
  239. package/src/dom/root.ts +12 -14
  240. package/src/react/activity.ts +10 -0
  241. package/src/react/context.ts +4 -11
  242. package/src/react/element.ts +11 -12
  243. package/src/react/hooks.ts +12 -3
  244. package/src/react/index.ts +18 -0
  245. package/src/react/shared-internals.ts +3 -3
  246. package/src/react/view-transition.ts +41 -0
  247. package/src/server/bootstrap-script.ts +36 -5
  248. package/src/server/dispatcher.ts +35 -13
  249. package/src/server/escape.ts +15 -12
  250. package/src/server/renderToString.ts +7 -5
  251. package/src/server/resource-hints.ts +165 -0
  252. package/src/server/stream.ts +154 -62
  253. package/src/server/walk.ts +79 -26
  254. package/src/vite/index.ts +30 -13
@@ -7,6 +7,7 @@ import {
7
7
  } from '../core'
8
8
  import {
9
9
  REACT_SUSPENSE_TYPE,
10
+ REACT_CONTEXT_TYPE,
10
11
  REACT_PROVIDER_TYPE,
11
12
  REACT_CONSUMER_TYPE,
12
13
  REACT_FORWARD_REF_TYPE,
@@ -15,6 +16,8 @@ import {
15
16
  REACT_STRICT_MODE_TYPE,
16
17
  REACT_PROFILER_TYPE,
17
18
  REACT_PORTAL_TYPE,
19
+ REACT_ACTIVITY_TYPE,
20
+ REACT_VIEW_TRANSITION_TYPE,
18
21
  } from '../react'
19
22
  import {
20
23
  attrToHtml,
@@ -27,8 +30,19 @@ import {
27
30
  pushContext,
28
31
  popContext,
29
32
  snapshotContexts,
33
+ recordHostResource,
34
+ currentSSRFrame,
30
35
  type ContextSnapshot,
31
36
  } from './dispatcher'
37
+ import { isBrowserError } from '../core/browser'
38
+ import { resourceKind } from '../core/resource-hints'
39
+ import type { ResourceScope } from './resource-hints'
40
+
41
+ export interface ServerErrorInfo {
42
+ componentStack: string
43
+ }
44
+
45
+ export type BrowserBailoutCallback = (error: Error, errorInfo: ServerErrorInfo) => void
32
46
 
33
47
  export interface SuspendedBoundary {
34
48
  id: number
@@ -36,15 +50,21 @@ export interface SuspendedBoundary {
36
50
  children: ReactNode
37
51
  thenable: Promise<any>
38
52
  contextSnapshot: ContextSnapshot
53
+ componentStack: string
39
54
  }
40
55
 
41
56
  export interface WalkOptions {
42
57
  emit: (chunk: string) => void
58
+ onHead?: () => void
43
59
  onSuspend?: ((boundary: SuspendedBoundary) => void) | undefined
60
+ onBrowserBailout?: BrowserBailoutCallback | undefined
61
+ componentStack?: string | undefined
62
+ thrownInfo?: { componentStack?: string | undefined }
44
63
  nextBoundaryId: () => number
45
64
  bootstrapped?: boolean | undefined
46
65
  isBoundaryResolution?: boolean | undefined
47
66
  isSvg?: boolean | undefined
67
+ resourceScope?: ResourceScope | undefined
48
68
  /**
49
69
  * Tracks whether the most recent emission within the *current text flow*
50
70
  * ended with a text node. When the next emission is also text, we emit a
@@ -150,6 +170,11 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
150
170
  const type = el.type
151
171
  const props = el.props ?? {}
152
172
 
173
+ if (typeof type === 'string') {
174
+ walkHost(type, props, opts)
175
+ return
176
+ }
177
+
153
178
  if (type === REACT_FRAGMENT_TYPE || type === REACT_STRICT_MODE_TYPE || type === REACT_PROFILER_TYPE) {
154
179
  walkNode(props.children, opts)
155
180
  return
@@ -160,8 +185,12 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
160
185
  return
161
186
  }
162
187
 
163
- if (typeof type === 'string') {
164
- walkHost(type, props, opts)
188
+ if (type === REACT_ACTIVITY_TYPE) {
189
+ if (props.mode !== 'hidden') walkNode(props.children, opts)
190
+ return
191
+ }
192
+ if (type === REACT_VIEW_TRANSITION_TYPE) {
193
+ walkNode(props.children, opts)
165
194
  return
166
195
  }
167
196
 
@@ -172,7 +201,7 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
172
201
  return
173
202
  }
174
203
 
175
- if (marker === REACT_PROVIDER_TYPE) {
204
+ if (marker === REACT_CONTEXT_TYPE || marker === REACT_PROVIDER_TYPE) {
176
205
  const ctx = (type as any)._context
177
206
  pushContext(ctx, props.value)
178
207
  try {
@@ -184,10 +213,9 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
184
213
  }
185
214
 
186
215
  if (marker === REACT_CONSUMER_TYPE) {
187
- const ctx = (type as any)._context
188
216
  const render = props.children
189
217
  if (typeof render === 'function') {
190
- walkNode(render(ctx._currentValue), opts)
218
+ walkNode(render((type as any)._context._currentValue), opts)
191
219
  }
192
220
  return
193
221
  }
@@ -196,8 +224,7 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
196
224
  const render = (type as any).render
197
225
  const ref = (props as any).ref ?? null
198
226
  const { ref: _omit, ...rest } = props as any
199
- const rendered = render(rest, ref)
200
- walkNode(rendered, opts)
227
+ walkComponent(render, rest, opts, ref)
201
228
  return
202
229
  }
203
230
 
@@ -209,16 +236,8 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
209
236
 
210
237
  if (marker === REACT_LAZY_TYPE) {
211
238
  const { _payload, _init } = type as any
212
- try {
213
- const resolved = _init(_payload)
214
- walkElement({ ...el, type: resolved } as ReactElement, opts)
215
- } catch (thenable: any) {
216
- if (isThenable(thenable)) {
217
- // Suspend this point
218
- throw thenable
219
- }
220
- throw thenable
221
- }
239
+ const resolved = _init(_payload)
240
+ walkElement({ ...el, type: resolved } as ReactElement, opts)
222
241
  return
223
242
  }
224
243
 
@@ -234,6 +253,7 @@ function walkHost(
234
253
  opts: WalkOptions,
235
254
  ): void {
236
255
  const isSvg = opts.isSvg || tag === 'svg'
256
+ if (!isSvg && resourceKind(tag, props) && recordHostResource(tag, props, opts.resourceScope)) return
237
257
  // <textarea value="..."> serializes its value as a TEXT CHILD, not an
238
258
  // attribute. `defaultValue` is the fallback when `value` is absent. This
239
259
  // matches React and the HTML spec — `<textarea value="x">` is not valid
@@ -270,6 +290,7 @@ function walkHost(
270
290
  pushSelectContext(val)
271
291
  }
272
292
  const isOption = tag === 'option'
293
+ try {
273
294
 
274
295
  // Prepend the HTML5 doctype to the stream when rendering an <html> root.
275
296
  // Without it the browser parses the document in quirks mode, which breaks
@@ -312,6 +333,7 @@ function walkHost(
312
333
  return
313
334
  }
314
335
  opts.emit('>')
336
+ if (tag === 'head') opts.onHead?.()
315
337
  // Opening a host element starts a fresh text flow context for its children.
316
338
  // Children's text separator tracking is independent of the outer context.
317
339
  const parentTextState = opts.textState
@@ -322,7 +344,9 @@ function walkHost(
322
344
  }
323
345
 
324
346
  if (tag === 'html' && !hasHeadChild(props.children)) {
325
- opts.emit('<head></head>')
347
+ opts.emit('<head>')
348
+ opts.onHead?.()
349
+ opts.emit('</head>')
326
350
  }
327
351
 
328
352
  const dangerouslyHtml = props.dangerouslySetInnerHTML?.__html
@@ -358,9 +382,11 @@ function walkHost(
358
382
  walkNode(props.children, childOpts)
359
383
  }
360
384
  opts.emit(`</${tag}>`)
361
- if (isSelect) popSelectContext()
362
385
  // Host element closing resets outer flow — next sibling text starts fresh.
363
386
  if (parentTextState) parentTextState.lastWasText = false
387
+ } finally {
388
+ if (isSelect) popSelectContext()
389
+ }
364
390
  }
365
391
 
366
392
  function hasHeadChild(children: unknown): boolean {
@@ -393,7 +419,12 @@ function walkComponent(
393
419
  fn: Function,
394
420
  props: Record<string, any>,
395
421
  opts: WalkOptions,
422
+ ref?: unknown,
396
423
  ): void {
424
+ if (opts.onBrowserBailout) {
425
+ opts = { ...opts, componentStack: `\n at ${(fn as any).displayName || fn.name || 'Anonymous'}${opts.componentStack || ''}` }
426
+ }
427
+ try {
397
428
  if ((fn as any).prototype?.isReactComponent) {
398
429
  const ctxType = (fn as any).contextType
399
430
  const ctxValue = ctxType ? ctxType._currentValue : undefined
@@ -407,8 +438,15 @@ function walkComponent(
407
438
  walkNode(instance.render(), opts)
408
439
  return
409
440
  }
410
- const rendered = (fn as any)(props)
441
+ const rendered = (fn as any)(props, ref)
411
442
  walkNode(rendered, opts)
443
+ } catch (error) {
444
+ if (opts.thrownInfo && !opts.thrownInfo.componentStack) opts.thrownInfo.componentStack = opts.componentStack
445
+ if (isBrowserError(error) && !(error as any).componentStack) {
446
+ Object.defineProperty(error, 'componentStack', { value: opts.componentStack })
447
+ }
448
+ throw error
449
+ }
412
450
  }
413
451
 
414
452
  function walkSuspense(
@@ -423,26 +461,37 @@ function walkSuspense(
423
461
  // Try to render the children synchronously. If a thenable is thrown,
424
462
  // record the boundary and emit the fallback.
425
463
  const childParts: string[] = []
464
+ const nested: SuspendedBoundary[] = []
465
+ const thrownInfo: { componentStack?: string | undefined } = {}
426
466
  const childEmit = (s: string) => childParts.push(s)
467
+ const resourceScope: ResourceScope | undefined = currentSSRFrame().shellFlushed ? {} : undefined
427
468
  try {
428
469
  walkNode(props.children, {
470
+ ...opts,
471
+ resourceScope,
472
+ thrownInfo,
429
473
  emit: childEmit,
430
- onSuspend: opts.onSuspend,
474
+ onSuspend: opts.onSuspend ? (boundary) => nested.push(boundary) : undefined,
431
475
  nextBoundaryId: opts.nextBoundaryId,
432
476
  })
433
477
  } catch (thenable: any) {
434
- if (isThenable(thenable)) {
478
+ nested.length = 0
479
+ if (isThenable(thenable) || isBrowserError(thenable)) {
435
480
  const fallbackParts: string[] = []
436
- try {
437
481
  walkNode(props.fallback, {
482
+ ...opts,
438
483
  emit: (s) => fallbackParts.push(s),
439
- onSuspend: opts.onSuspend,
484
+ onSuspend: opts.onSuspend ? (boundary) => nested.push(boundary) : undefined,
440
485
  nextBoundaryId: opts.nextBoundaryId,
441
486
  })
442
- } catch {
443
- // Fallback suspending is unsupported; emit nothing
487
+ if (isBrowserError(thenable)) {
488
+ opts.onBrowserBailout?.(thenable, { componentStack: (thenable as any).componentStack || opts.componentStack || '' })
489
+ opts.emit(`<!--$!${id}-->${fallbackParts.join('')}<!--/$-->`)
490
+ for (const boundary of nested) opts.onSuspend?.(boundary)
491
+ return
444
492
  }
445
493
  emitBoundary(opts, id, fallbackParts.join(''))
494
+ for (const boundary of nested) opts.onSuspend?.(boundary)
446
495
 
447
496
  if (opts.onSuspend) {
448
497
  opts.onSuspend({
@@ -451,6 +500,7 @@ function walkSuspense(
451
500
  children: props.children,
452
501
  thenable,
453
502
  contextSnapshot,
503
+ componentStack: thrownInfo.componentStack || opts.componentStack || '',
454
504
  })
455
505
  }
456
506
  return
@@ -458,6 +508,8 @@ function walkSuspense(
458
508
  throw thenable
459
509
  }
460
510
 
511
+ if (resourceScope) currentSSRFrame().resources?.mergeScope(resourceScope, opts.resourceScope)
512
+
461
513
  // Children rendered fully — emit them wrapped in resolved-boundary markers
462
514
  // (`<!--$N-->` / `<!--/$-->`). Without markers, the client hydrator has no
463
515
  // way to know this subtree is inside a Suspense, so if the client version
@@ -468,6 +520,7 @@ function walkSuspense(
468
520
  opts.emit(`<!--$${id}-->`)
469
521
  opts.emit(childParts.join(''))
470
522
  opts.emit(`<!--/$-->`)
523
+ for (const boundary of nested) opts.onSuspend?.(boundary)
471
524
  }
472
525
 
473
526
  function emitBoundary(opts: WalkOptions, id: number, fallbackHTML: string): void {
package/src/vite/index.ts CHANGED
@@ -12,6 +12,12 @@ export type RedactPreset = 'nano' | 'full'
12
12
  * configs as autocompleted options.
13
13
  */
14
14
  export interface RedactFeatures {
15
+ /** Activity visibility, retained state and disconnected effects. */
16
+ activity?: boolean
17
+ /** DOM instances, events, focus, layout and observers for Fragment refs. */
18
+ fragmentRefs?: boolean
19
+ /** Experimental native ViewTransition animations. Opt-in until snapshot parity is complete. */
20
+ viewTransitions?: boolean
15
21
  /**
16
22
  * `createPortal`. When `false`, portal elements render in place as a
17
23
  * Fragment (the `container` prop is ignored). `renderPortal` and its
@@ -72,6 +78,9 @@ export interface RedactFeatures {
72
78
  }
73
79
 
74
80
  interface ResolvedFeatures {
81
+ activity: boolean
82
+ fragmentRefs: boolean
83
+ viewTransitions: boolean
75
84
  portal: boolean
76
85
  context: boolean
77
86
  suspense: boolean
@@ -85,11 +94,13 @@ interface ResolvedFeatures {
85
94
  const PRESET_DEFAULTS: Record<RedactPreset, ResolvedFeatures> = {
86
95
  // Opt-in: everything off. Turn individual features on via `features`.
87
96
  nano: {
97
+ activity: false, fragmentRefs: false, viewTransitions: false,
88
98
  portal: false, context: false, suspense: false, memo: false,
89
99
  forwardRef: false, lazy: false, classComponents: false, hydration: false,
90
100
  },
91
101
  // Opt-out: everything on (drop-in React parity). Turn features off via `features`.
92
102
  full: {
103
+ activity: true, fragmentRefs: true, viewTransitions: false,
93
104
  portal: true, context: true, suspense: true, memo: true,
94
105
  forwardRef: true, lazy: true, classComponents: true, hydration: true,
95
106
  },
@@ -101,6 +112,9 @@ function resolveFeatures(
101
112
  ): ResolvedFeatures {
102
113
  const p = PRESET_DEFAULTS[preset]
103
114
  return {
115
+ activity: overrides.activity ?? p.activity,
116
+ fragmentRefs: overrides.fragmentRefs ?? p.fragmentRefs,
117
+ viewTransitions: overrides.viewTransitions ?? p.viewTransitions,
104
118
  portal: overrides.portal ?? p.portal,
105
119
  context: overrides.context ?? p.context,
106
120
  suspense: overrides.suspense ?? p.suspense,
@@ -166,7 +180,10 @@ const ALIASES: Record<string, string> = {
166
180
  'react/jsx-dev-runtime': '@tanstack/redact/jsx-dev-runtime',
167
181
  'react/compiler-runtime': '@tanstack/redact/compiler-runtime',
168
182
  'react-dom/client': '@tanstack/redact/dom-client',
183
+ 'react-dom/server.browser': '@tanstack/redact/server',
184
+ 'react-dom/server.bun': '@tanstack/redact/server',
169
185
  'react-dom/server.edge': '@tanstack/redact/server',
186
+ 'react-dom/server.node': '@tanstack/redact/server',
170
187
  'react-dom/static.edge': '@tanstack/redact/server',
171
188
  'react-dom/server': '@tanstack/redact/server',
172
189
  'react-dom/test-utils': '@tanstack/redact/dom-test-utils',
@@ -225,10 +242,8 @@ function resolveExport(packageDir: string, sub: string): string | null {
225
242
  }
226
243
  const key = sub ? './' + sub : '.'
227
244
  const exp = pkg.exports?.[key]
228
- // Prefer published `import` (dist/.js) over `source` dist is a single
229
- // transformed bundle so Vite's dep optimizer doesn't thrash on dozens of
230
- // individual source files. The package keeps cross-subpath imports
231
- // external, so there's still only one runtime instance.
245
+ // Prefer published JavaScript over TypeScript sources. Dist preserves one
246
+ // file per module, so subpath imports share state and feature boundaries.
232
247
  const pick = (v: any): string | null => {
233
248
  if (typeof v === 'string') return v
234
249
  if (v && typeof v === 'object') {
@@ -388,11 +403,12 @@ export function redact(options: RedactOptions = {}): any {
388
403
  // if the flag is off. The stub registers a graceful-degradation
389
404
  // matcher (e.g. Portal → Fragment) so user code keeps working.
390
405
  if (importer && /[\\/]features[\\/]index\.[jt]sx?$/.test(importer)) {
391
- const m = id.match(/^\.\/([a-z-]+)$/)
406
+ const m = id.match(/^\.\/([a-z-]+)(\/index\.js)?$/)
392
407
  if (m) {
393
- const name = m[1] as keyof ResolvedFeatures
408
+ const dir = m[1]!
409
+ const name = (dir === 'forward-ref' ? 'forwardRef' : dir === 'fragment-refs' ? 'fragmentRefs' : dir === 'view-transition' ? 'viewTransitions' : dir === 'class' ? 'classComponents' : dir) as keyof ResolvedFeatures
394
410
  if (name in features && !features[name]) {
395
- const r = await this.resolve(`./${name}/stub`, importer, {
411
+ const r = await this.resolve(`./${dir}/stub${m[2] ? '.js' : ''}`, importer, {
396
412
  ...opts,
397
413
  skipSelf: true,
398
414
  })
@@ -401,13 +417,14 @@ export function redact(options: RedactOptions = {}): any {
401
417
  }
402
418
  }
403
419
 
404
- // Hydration swap: hydration isn't self-registering, so it's imported
405
- // from reconcile.ts, root.ts, and the Suspense/Lazy feature modules.
406
- // Any specifier ending in `/hydration` that resolves to our feature
407
- // module gets redirected to the stub when the flag is off.
408
- if (!features.hydration && importer && /[\\/]hydration$/.test(id)) {
420
+ // Peer imports must use the same feature selection as the registration
421
+ // entrypoint, or an enabled feature can bring a disabled one back in.
422
+ const peer = id.match(/[\\/](context|hydration|fragment-refs|view-transition)(?:[\\/]index\.js)?$/)
423
+ const peerKey = peer?.[1] === 'fragment-refs' ? 'fragmentRefs' : peer?.[1] === 'view-transition' ? 'viewTransitions' : peer?.[1] as 'context' | 'hydration' | undefined
424
+ if (importer && peer && peerKey && !features[peerKey]) {
409
425
  const r = await this.resolve(id, importer, { ...opts, skipSelf: true })
410
- if (r && /features[\\/]hydration[\\/]index\.(ts|js)$/.test(r.id)) {
426
+ const resolvedFeature = r?.id.match(/features[\\/](context|hydration|fragment-refs|view-transition)[\\/]index\.(ts|js)$/)
427
+ if (resolvedFeature?.[1] === peer[1]) {
411
428
  return r.id.replace(/index\.(ts|js)$/, 'stub.$1')
412
429
  }
413
430
  }