@tanstack/redact 0.0.17 → 0.0.19

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.
@@ -120,14 +120,7 @@ export class HydrationCursor {
120
120
  has(): boolean {
121
121
  let n = this.n
122
122
  while (n && n !== this.e) {
123
- if (n.nodeType === 1) {
124
- if ((n as Element).tagName === 'SCRIPT') {
125
- n = n.nextSibling
126
- continue
127
- }
128
- return true
129
- }
130
- if (n.nodeType === 3 && (n as Text).data.trim() !== '') return true
123
+ if (n.nodeType === 1 || n.nodeType === 3) return true
131
124
  n = n.nextSibling
132
125
  }
133
126
  return false
@@ -171,17 +164,12 @@ export function hydrateRootImpl(
171
164
  const target = container as any as Element | Document
172
165
  const isDocument = (container as Node).nodeType === 9
173
166
  const body = isDocument ? (target as Document).body : null
174
- const root = createFiberRoot(target, {
175
- ...options,
176
- identifierPrefix: options.identifierPrefix ?? ':R',
177
- })
167
+ const root = createFiberRoot(target, options)
178
168
 
179
169
  installHydrationScrollGuard()
180
170
 
181
171
  const normalizedInitialChildren =
182
172
  isDocument ? normalizeDocumentChildren(initialChildren) : initialChildren
183
- let documentBodyFallback = false
184
-
185
173
  let hydrationError: unknown = null
186
174
  beginHydration(root)
187
175
  try {
@@ -193,36 +181,7 @@ export function hydrateRootImpl(
193
181
  }
194
182
  endHydration(root)
195
183
 
196
- if (hydrationError) {
197
- if (!isHydrationBailout(hydrationError)) {
198
- throw hydrationError
199
- }
200
- let recoveryContainer: Element | Document = target
201
- let recoveryChildren = normalizedInitialChildren
202
- const hostRecovery = getRecoverableHostChildren(hydrationError)
203
- if (hostRecovery) {
204
- recoveryContainer = hostRecovery[0]
205
- recoveryChildren = hostRecovery[1]
206
- } else if (body) {
207
- const bodyChildren = getRecoverableDocumentBodyChildren(hydrationError)
208
- if (bodyChildren != null) {
209
- documentBodyFallback = true
210
- recoveryContainer = body
211
- recoveryChildren = bodyChildren
212
- }
213
- }
214
- resetAfterHydrationFailure(root, recoveryContainer)
215
- try {
216
- root.i = options.identifierPrefix ?? ':r'
217
- root.ic = 0
218
- flushSyncWork(() => {
219
- renderRoot(root, recoveryChildren)
220
- })
221
- } catch (clientError) {
222
- resetAfterHydrationFailure(root, recoveryContainer)
223
- throw clientError
224
- }
225
- }
184
+ if (hydrationError && !recoverHydration(root, hydrationError)) throw hydrationError
226
185
  drainReplayQueue()
227
186
 
228
187
  return {
@@ -231,7 +190,7 @@ export function hydrateRootImpl(
231
190
  const normalized = isDocument ? normalizeDocumentChildren(children) : children
232
191
  renderRoot(
233
192
  root,
234
- documentBodyFallback ? getStaticDocumentBodyChildren(normalized) ?? normalized : normalized,
193
+ root.c === body ? getStaticDocumentBodyChildren(normalized) ?? normalized : normalized,
235
194
  )
236
195
  })
237
196
  },
@@ -246,8 +205,9 @@ export function hydrateRootImpl(
246
205
  // Head elements that we match against server DOM by attribute signature.
247
206
  const HEAD_KEY_ATTRS: Record<string, ReadonlyArray<string>> = {
248
207
  link: ['rel', 'href', 'sizes', 'type'],
249
- meta: ['name', 'property', 'charset', 'http-equiv'],
208
+ meta: ['name', 'property', 'charSet', 'httpEquiv'],
250
209
  script: ['src', 'type'],
210
+ style: ['id', 'media', 'nonce', 'title'],
251
211
  }
252
212
 
253
213
  const DOCUMENT_HEAD_TAGS = new Set(['base', 'link', 'meta', 'script', 'style', 'title'])
@@ -262,21 +222,17 @@ function headAttrsMatch(
262
222
  ): boolean {
263
223
  if (CLAIMED.has(el)) return false
264
224
  if (!keys) return true
265
- let matched = false
266
225
  for (const k of keys) {
267
- const propVal = props[k] ?? (k === 'http-equiv' ? props.httpEquiv : undefined)
268
- const elVal = el.getAttribute(k)
269
- // If neither defines it, skip this key; if one defines it, they must match.
226
+ const propVal = props[k]
227
+ const elVal = el.getAttribute(attributeName(k))
270
228
  if (propVal == null && elVal == null) continue
271
- matched = true
272
- if (propVal == null || elVal == null) continue // tolerate missing on either side
273
- if (String(propVal) !== elVal) return false
229
+ if (propVal == null || elVal == null || String(propVal) !== elVal) return false
274
230
  }
275
- // At least one matching signal must be present.
276
- return matched
231
+ return true
277
232
  }
278
233
 
279
234
  export function beginHydration(root: FiberRoot): void {
235
+ root.ic = 0
280
236
  root.h = true
281
237
  hydrationCursors.set(root.r, new HydrationCursor(root.c))
282
238
  }
@@ -400,13 +356,24 @@ export function adoptHostDom(fiber: Fiber, parent: Fiber): boolean {
400
356
  tag === 'svg' ||
401
357
  ((candidate as Element).namespaceURI === 'http://www.w3.org/2000/svg' &&
402
358
  tag !== 'foreignobject')
359
+ const syncHydrationProps =
360
+ !props.suppressHydrationWarning &&
361
+ canRecoverHydrationPropMismatch(candidate, tag)
403
362
  validateHydrationProps(fiber, candidate as Element, props, tag, isSvg)
404
363
  for (const k in props) {
405
- if (k === 'children') continue
406
- if (k[0] === 'o' && k[1] === 'n' && typeof props[k] == 'function') {
364
+ if (
365
+ k === 'children' ||
366
+ k === 'key' ||
367
+ k === 'ref' ||
368
+ k === 'suppressHydrationWarning' ||
369
+ k === 'suppressContentEditableWarning'
370
+ ) continue
371
+ if (
372
+ syncHydrationProps ||
373
+ (k[0] === 'o' && k[1] === 'n' && typeof props[k] == 'function')
374
+ ) {
407
375
  setProp(candidate as Element, k, props[k], undefined, isSvg)
408
376
  }
409
- // Non-event props: trust the server HTML, skip
410
377
  }
411
378
  // Set up child cursor for this host's children
412
379
  hydrationCursors.set(fiber, new HydrationCursor(candidate))
@@ -417,16 +384,24 @@ export function adoptTextDom(fiber: Fiber, parent: Fiber, text: string): boolean
417
384
  const cursor = hydrationCursors.get(findHostParent(parent))
418
385
  if (!cursor) return false
419
386
  const candidate = cursor.take()
420
- if (!candidate) onMismatch(fiber, null)
387
+ if (!candidate) {
388
+ onMismatch(fiber, null)
389
+ return false
390
+ }
421
391
  if (candidate.nodeType === 3) {
422
392
  if ((candidate as Text).data !== text) {
423
- if (process.env.NODE_ENV !== 'production') {
424
- failHydration(
425
- fiber,
426
- new Error(`Hydration text mismatch: expected "${text}" but found "${(candidate as Text).data}".`),
427
- )
393
+ const recovered = failHydration(
394
+ fiber,
395
+ process.env.NODE_ENV !== 'production'
396
+ ? new Error(
397
+ `Hydration text mismatch: expected "${text}" but found "${(candidate as Text).data}".`,
398
+ )
399
+ : undefined,
400
+ canRecoverHydrationPropMismatch(candidate),
401
+ )
402
+ if (recovered) {
403
+ ;(candidate as Text).data = text
428
404
  }
429
- failHydration(fiber)
430
405
  }
431
406
  fiber.dom = candidate
432
407
  return true
@@ -452,28 +427,41 @@ export function findHostParent(fiber: Fiber): Fiber {
452
427
  throw new Error()
453
428
  }
454
429
 
455
- function onMismatch(fiber: Fiber, actualNode: ChildNode | null): never {
456
- if (process.env.NODE_ENV !== 'production') {
457
- failHydration(
458
- fiber,
459
- new Error(
460
- `Hydration mismatch: expected <${(fiber.type as string) ?? 'text'}> but found ${
461
- actualNode ? (actualNode.nodeType === 1 ? (actualNode as Element).tagName : 'text') : 'nothing'
462
- }.`,
463
- ),
464
- )
465
- }
466
- failHydration(fiber)
430
+ function onMismatch(fiber: Fiber, actualNode: ChildNode | null): void {
431
+ failHydration(
432
+ fiber,
433
+ process.env.NODE_ENV !== 'production'
434
+ ? new Error(
435
+ `Hydration mismatch: expected <${(fiber.type as string) ?? 'text'}> but found ${
436
+ actualNode ? (actualNode.nodeType === 1 ? (actualNode as Element).tagName : 'text') : 'nothing'
437
+ }.`,
438
+ )
439
+ : undefined,
440
+ )
467
441
  }
468
442
 
469
- function failHydration(fiber: Fiber, error: Error = new Error(PROD_HYDRATION_ERROR)): never {
443
+ function failHydration(
444
+ fiber: Fiber,
445
+ error: Error = new Error(PROD_HYDRATION_ERROR),
446
+ recoverInPlace = false,
447
+ ): boolean {
470
448
  const root = findRoot(fiber)
471
449
  if (root?.re) {
472
450
  root.re(error)
473
451
  }
452
+ if (recoverInPlace) return true
474
453
  abortHydration(error, findHostRecoveryParent(fiber) ?? fiber)
475
454
  }
476
455
 
456
+ function canRecoverHydrationPropMismatch(node: Node, tag?: string): boolean {
457
+ return (
458
+ tag === 'html' ||
459
+ tag === 'head' ||
460
+ tag === 'body' ||
461
+ !!node.ownerDocument?.head?.contains(node)
462
+ )
463
+ }
464
+
477
465
  function findHostRecoveryParent(fiber: Fiber): Fiber | null {
478
466
  if (fiber.tag === FiberTag.Text) {
479
467
  let directHost = fiber.parent
@@ -523,6 +511,33 @@ function isSafeHostRecoveryElement(fiber: Fiber): boolean {
523
511
  return tag !== 'html' && tag !== 'head' && tag !== 'body'
524
512
  }
525
513
 
514
+ export function recoverHydration(root: FiberRoot, error: unknown): boolean {
515
+ if (!isHydrationBailout(error)) return false
516
+
517
+ let container = root.c as Element | Document
518
+ let children = root.r.pp?.children ?? null
519
+ const hostRecovery = getRecoverableHostChildren(error)
520
+ if (hostRecovery) {
521
+ container = hostRecovery[0]
522
+ children = hostRecovery[1]
523
+ } else if (container.nodeType === 9) {
524
+ const bodyChildren = getRecoverableDocumentBodyChildren(error)
525
+ if (bodyChildren != null) {
526
+ container = (container as Document).body
527
+ children = bodyChildren
528
+ }
529
+ }
530
+
531
+ resetAfterHydrationFailure(root, container)
532
+ try {
533
+ flushSyncWork(() => renderRoot(root, children))
534
+ } catch (clientError) {
535
+ resetAfterHydrationFailure(root, container)
536
+ throw clientError
537
+ }
538
+ return true
539
+ }
540
+
526
541
  function resetAfterHydrationFailure(
527
542
  root: FiberRoot,
528
543
  container: Element | Document,
@@ -678,6 +693,7 @@ function validateHydrationProps(
678
693
  ): void {
679
694
  if (props.suppressHydrationWarning) return
680
695
 
696
+ const recoverInPlace = canRecoverHydrationPropMismatch(el, tag)
681
697
  let expected: Element | undefined
682
698
 
683
699
  for (const k in props) {
@@ -692,16 +708,20 @@ function validateHydrationProps(
692
708
  ) continue
693
709
 
694
710
  if (k === 'dangerouslySetInnerHTML') {
695
- const probe = document.createElement('div')
696
- probe.innerHTML = value?.__html ?? ''
697
- if ((el as HTMLElement).innerHTML !== probe.innerHTML) {
698
- if (process.env.NODE_ENV !== 'production') {
699
- failHydration(
700
- fiber,
701
- new Error(`Hydration HTML mismatch inside <${tag}>.`),
702
- )
703
- }
704
- failHydration(fiber)
711
+ let html = '' + (value?.__html ?? '')
712
+ if (tag !== 'script' && tag !== 'style') {
713
+ const probe = document.createElement('div')
714
+ probe.innerHTML = html
715
+ html = probe.innerHTML
716
+ }
717
+ if ((el as HTMLElement).innerHTML !== html) {
718
+ failHydration(
719
+ fiber,
720
+ process.env.NODE_ENV !== 'production'
721
+ ? new Error(`Hydration HTML mismatch inside <${tag}>.`)
722
+ : undefined,
723
+ recoverInPlace,
724
+ )
705
725
  }
706
726
  continue
707
727
  }
@@ -711,10 +731,13 @@ function validateHydrationProps(
711
731
  if (tag === 'input' || tag === 'textarea') {
712
732
  if (k === 'value' || props.value == null) {
713
733
  if (value != null && (el as HTMLInputElement).value !== '' + value) {
714
- if (process.env.NODE_ENV !== 'production') {
715
- failHydration(fiber, new Error(`Hydration ${tag} value mismatch on <${tag}>.`))
716
- }
717
- failHydration(fiber)
734
+ failHydration(
735
+ fiber,
736
+ process.env.NODE_ENV !== 'production'
737
+ ? new Error(`Hydration ${tag} value mismatch on <${tag}>.`)
738
+ : undefined,
739
+ recoverInPlace,
740
+ )
718
741
  }
719
742
  }
720
743
  continue
@@ -724,10 +747,13 @@ function validateHydrationProps(
724
747
  if (tag === 'input' && (k === 'checked' || k === 'defaultChecked')) {
725
748
  if (k === 'checked' || props.checked == null) {
726
749
  if (value != null && (el as HTMLInputElement).checked !== !!value) {
727
- if (process.env.NODE_ENV !== 'production') {
728
- failHydration(fiber, new Error(`Hydration checked mismatch on <input>.`))
729
- }
730
- failHydration(fiber)
750
+ failHydration(
751
+ fiber,
752
+ process.env.NODE_ENV !== 'production'
753
+ ? new Error(`Hydration checked mismatch on <input>.`)
754
+ : undefined,
755
+ recoverInPlace,
756
+ )
731
757
  }
732
758
  }
733
759
  continue
@@ -735,10 +761,13 @@ function validateHydrationProps(
735
761
 
736
762
  if (tag === 'option' && k === 'selected') {
737
763
  if (value != null && (el as HTMLOptionElement).selected !== !!value) {
738
- if (process.env.NODE_ENV !== 'production') {
739
- failHydration(fiber, new Error(`Hydration selected mismatch on <option>.`))
740
- }
741
- failHydration(fiber)
764
+ failHydration(
765
+ fiber,
766
+ process.env.NODE_ENV !== 'production'
767
+ ? new Error(`Hydration selected mismatch on <option>.`)
768
+ : undefined,
769
+ recoverInPlace,
770
+ )
742
771
  }
743
772
  continue
744
773
  }
@@ -746,14 +775,19 @@ function validateHydrationProps(
746
775
  if (k === 'style') {
747
776
  expected ??= createHostNode(tag, isSvg)
748
777
  setProp(expected, k, value, undefined, isSvg)
749
- if ((el as HTMLElement).style.cssText !== (expected as HTMLElement).style.cssText) {
750
- if (process.env.NODE_ENV !== 'production') {
751
- failHydration(
752
- fiber,
753
- new Error(`Hydration style mismatch on <${tag}>.`),
754
- )
755
- }
756
- failHydration(fiber)
778
+ if (
779
+ !hydrationStylesMatch(
780
+ (el as HTMLElement).style,
781
+ (expected as HTMLElement).style,
782
+ )
783
+ ) {
784
+ failHydration(
785
+ fiber,
786
+ process.env.NODE_ENV !== 'production'
787
+ ? new Error(`Hydration style mismatch on <${tag}>.`)
788
+ : undefined,
789
+ recoverInPlace,
790
+ )
757
791
  }
758
792
  continue
759
793
  }
@@ -771,21 +805,34 @@ function validateHydrationProps(
771
805
  }
772
806
  const actualValue = el.getAttribute(attr)
773
807
  if (expectedValue !== actualValue) {
774
- if (attr === 'id' && expectedValue != null && actualValue != null) {
775
- continue
776
- }
777
- if (process.env.NODE_ENV !== 'production') {
778
- failHydration(
779
- fiber,
780
- new Error(
781
- `Hydration attribute mismatch on <${tag}> for "${attr}": ` +
782
- `expected ${formatHydrationValue(expectedValue)} but found ${formatHydrationValue(actualValue)}.`,
783
- ),
784
- )
785
- }
786
- failHydration(fiber)
808
+ failHydration(
809
+ fiber,
810
+ process.env.NODE_ENV !== 'production'
811
+ ? new Error(
812
+ `Hydration attribute mismatch on <${tag}> for "${attr}": ` +
813
+ `expected ${formatHydrationValue(expectedValue)} but found ${formatHydrationValue(actualValue)}.`,
814
+ )
815
+ : undefined,
816
+ recoverInPlace,
817
+ )
818
+ }
819
+ }
820
+ }
821
+
822
+ function hydrationStylesMatch(
823
+ actual: CSSStyleDeclaration,
824
+ expected: CSSStyleDeclaration,
825
+ ): boolean {
826
+ for (let index = 0; index < expected.length; index++) {
827
+ const property = expected.item(index)
828
+ if (
829
+ actual.getPropertyValue(property) !== expected.getPropertyValue(property) ||
830
+ actual.getPropertyPriority(property) !== expected.getPropertyPriority(property)
831
+ ) {
832
+ return false
787
833
  }
788
834
  }
835
+ return true
789
836
  }
790
837
 
791
838
  function formatHydrationValue(value: string | null): string {
@@ -74,6 +74,10 @@ export function isHydrationBailout(_error: unknown): _error is HydrationBailoutE
74
74
  return false
75
75
  }
76
76
 
77
+ export function recoverHydration(_root: FiberRoot, _error: unknown): boolean {
78
+ return false
79
+ }
80
+
77
81
  export function abortHydration(cause: unknown, fiber: Fiber | null = null): never {
78
82
  const error = (cause instanceof Error ? cause : new Error('Hydration mismatch.')) as HydrationBailoutError
79
83
  ;(error as any).f = fiber
@@ -27,6 +27,7 @@ import {
27
27
  clearHydrationCursor,
28
28
  findHostParent as findHydrationHost,
29
29
  abortHydration,
30
+ recoverHydration,
30
31
  } from './features/hydration'
31
32
 
32
33
  // ---------------------------------------------------------------------------
@@ -117,7 +118,12 @@ function flushPending(): void {
117
118
  root.p.clear()
118
119
  pending.sort((a, b) => fiberDepth(a) - fiberDepth(b))
119
120
  for (const fiber of pending) {
120
- rerenderFiber(fiber, root)
121
+ try {
122
+ rerenderFiber(fiber, root)
123
+ } catch (error) {
124
+ if (!recoverHydration(root, error)) throw error
125
+ break
126
+ }
121
127
  }
122
128
  runEffects(root)
123
129
  }
@@ -815,7 +821,12 @@ function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
815
821
  const hasOpaqueHydrationChildren =
816
822
  props.dangerouslySetInnerHTML != null ||
817
823
  (parentTag === 'textarea' && (props.value != null || props.defaultValue != null))
818
- if (parentTag !== 'head' && parentTag !== 'html' && !hasOpaqueHydrationChildren) {
824
+ if (
825
+ parentTag !== 'head' &&
826
+ parentTag !== 'html' &&
827
+ parentTag !== 'body' &&
828
+ !hasOpaqueHydrationChildren
829
+ ) {
819
830
  const cursor = getHydrationCursor(fiber)
820
831
  if (cursor) {
821
832
  if (cursor.has()) {
@@ -20,7 +20,7 @@ export function createFiberRoot(
20
20
  re: options.onRecoverableError,
21
21
  ce: options.onCaughtError,
22
22
  ue: options.onUncaughtError,
23
- i: options.identifierPrefix ?? ':r',
23
+ i: options.identifierPrefix,
24
24
  ic: 0,
25
25
  h: false,
26
26
  }