@tanstack/redact 0.0.15 → 0.0.17

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.
@@ -6,6 +6,7 @@ import {
6
6
  type ReactElement,
7
7
  type ReactNode,
8
8
  } from '../../../core'
9
+ import { attributeName } from '../../../core/attributes'
9
10
  import { createHostNode, setProp } from '../../dom'
10
11
  import { drainReplayQueue } from '../../event-replay'
11
12
  import { discardPendingWork, findRoot, flushSyncWork, renderRoot } from '../../reconcile'
@@ -119,7 +120,14 @@ export class HydrationCursor {
119
120
  has(): boolean {
120
121
  let n = this.n
121
122
  while (n && n !== this.e) {
122
- if (n.nodeType === 1 || n.nodeType === 3) return true
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
131
  n = n.nextSibling
124
132
  }
125
133
  return false
@@ -163,7 +171,10 @@ export function hydrateRootImpl(
163
171
  const target = container as any as Element | Document
164
172
  const isDocument = (container as Node).nodeType === 9
165
173
  const body = isDocument ? (target as Document).body : null
166
- const root = createFiberRoot(target, options)
174
+ const root = createFiberRoot(target, {
175
+ ...options,
176
+ identifierPrefix: options.identifierPrefix ?? ':R',
177
+ })
167
178
 
168
179
  installHydrationScrollGuard()
169
180
 
@@ -202,6 +213,8 @@ export function hydrateRootImpl(
202
213
  }
203
214
  resetAfterHydrationFailure(root, recoveryContainer)
204
215
  try {
216
+ root.i = options.identifierPrefix ?? ':r'
217
+ root.ic = 0
205
218
  flushSyncWork(() => {
206
219
  renderRoot(root, recoveryChildren)
207
220
  })
@@ -490,14 +503,19 @@ function findHostRecoveryParent(fiber: Fiber): Fiber | null {
490
503
  }
491
504
 
492
505
  function findNearestSafeHostAboveComposite(fiber: Fiber | null): Fiber | null {
506
+ let host: Fiber | null = null
493
507
  let f = fiber
494
508
  while (f) {
495
509
  if (f.tag === FiberTag.Host && f.dom) {
496
- return isSafeHostRecoveryElement(f) ? f : null
510
+ if (!isSafeHostRecoveryElement(f)) return null
511
+ const parentTag = f.parent?.tag as number
512
+ if (!host || (parentTag > FiberTag.Text && parentTag < FiberTag.Suspense)) {
513
+ host = f
514
+ }
497
515
  }
498
516
  f = f.parent
499
517
  }
500
- return null
518
+ return host
501
519
  }
502
520
 
503
521
  function isSafeHostRecoveryElement(fiber: Fiber): boolean {
@@ -535,10 +553,8 @@ function getRecoverableHostChildren(
535
553
  ): [Element, ReactNode] | null {
536
554
  const host = error.f
537
555
  if (
538
- !host ||
539
- host.tag !== FiberTag.Host ||
556
+ host?.tag !== FiberTag.Host ||
540
557
  !host.dom ||
541
- !isSafeHostRecoveryElement(host) ||
542
558
  !findNearestSafeHostAboveComposite(host.parent)
543
559
  ) {
544
560
  return null
@@ -743,7 +759,7 @@ function validateHydrationProps(
743
759
  }
744
760
 
745
761
  const stringifiedBoolean = k.startsWith('aria-') || k.startsWith('data-')
746
- const attr = k === 'className' ? 'class' : k === 'htmlFor' ? 'for' : stringifiedBoolean ? k : k.toLowerCase()
762
+ const attr = attributeName(k, isSvg)
747
763
 
748
764
  let expectedValue: string | null
749
765
  if (value == null || (value === false && !stringifiedBoolean)) {
@@ -755,6 +771,9 @@ function validateHydrationProps(
755
771
  }
756
772
  const actualValue = el.getAttribute(attr)
757
773
  if (expectedValue !== actualValue) {
774
+ if (attr === 'id' && expectedValue != null && actualValue != null) {
775
+ continue
776
+ }
758
777
  if (process.env.NODE_ENV !== 'production') {
759
778
  failHydration(
760
779
  fiber,
@@ -104,7 +104,7 @@ function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): voi
104
104
  // Snapshot whether we have an existing committed primary tree before
105
105
  // attempting the new render. If the new attempt suspends and we did have a
106
106
  // committed primary, we keep it (hidden) rather than destroying it.
107
- const hadCommittedPrimary = fiber.mp !== undefined && fiber.child !== null
107
+ const hadCommittedPrimary = fiber.mp && fiber.child
108
108
 
109
109
  const prevHandler = suspendHandler
110
110
  let pendingThenable: any
@@ -125,12 +125,12 @@ function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): voi
125
125
  }
126
126
  pendingThenable.then(onSettle, onSettle)
127
127
 
128
- if (hadCommittedPrimary && fiber.child) {
128
+ if (hadCommittedPrimary) {
129
129
  // Hide the primary subtree's root host doms so the fallback is the only
130
130
  // thing visible, but the underlying nodes (and their scroll/state/focus)
131
131
  // survive. Save original `display` for the resume path.
132
132
  const hidden: Array<[HTMLElement, string]> = []
133
- let c: Fiber | null = fiber.child
133
+ let c: Fiber | null = hadCommittedPrimary
134
134
  while (c) {
135
135
  hideRootHostDoms(c, hidden)
136
136
  c = c.sibling
@@ -172,11 +172,9 @@ function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): voi
172
172
  // the root hides the whole element). Used by the hide-on-suspend path.
173
173
  function hideRootHostDoms(fiber: Fiber, out: Array<[HTMLElement, string]>): void {
174
174
  if (fiber.tag === FiberTag.Host) {
175
- const el = fiber.dom as HTMLElement | null
176
- if (el) {
177
- out.push([el, el.style.display])
178
- el.style.display = 'none'
179
- }
175
+ const el = fiber.dom as HTMLElement
176
+ out.push([el, el.style.display])
177
+ el.style.display = 'none'
180
178
  return
181
179
  }
182
180
  if (fiber.tag === FiberTag.Portal) return
@@ -294,11 +292,11 @@ function recoverFallbackHydration(fiber: Fiber, fallback: any, parent: HTMLEleme
294
292
 
295
293
  function rehydrateBoundary(fiber: Fiber): void {
296
294
  const state = fiber.ms
297
- if (!state || !state.b || !state.e) return
295
+ if (!state?.b || !state.e) return
298
296
 
299
297
  const root = findRoot(fiber)
300
298
  const parent = state.b.parentNode as Node
301
- if (!root || !parent) return
299
+ if (!(root && parent)) return
302
300
 
303
301
  // Unmount existing fallback subtree. Its DOM has already been removed by $RC
304
302
  // (or at least its container); unmounting here cleans up fibers + fx.
@@ -21,6 +21,7 @@ export function createFiberRoot(
21
21
  ce: options.onCaughtError,
22
22
  ue: options.onUncaughtError,
23
23
  i: options.identifierPrefix ?? ':r',
24
+ ic: 0,
24
25
  h: false,
25
26
  }
26
27
  rootFiber.root = root
@@ -34,6 +35,7 @@ export function attachRootFiber(
34
35
  ): void {
35
36
  const rootFiber = createFiber(FiberTag.Root, null, null)
36
37
  root.c = container as any
38
+ root.ic = 0
37
39
  rootFiber.root = root
38
40
  rootFiber.sn = container
39
41
  root.r = rootFiber
@@ -1,3 +1,5 @@
1
+ import { attributeName } from '../core/attributes'
2
+
1
3
  const ATTR_MAP: Record<string, string> = {
2
4
  '&': '&amp;',
3
5
  '"': '&quot;',
@@ -48,17 +50,6 @@ export const VOID_ELEMENTS = new Set([
48
50
 
49
51
  export const RAW_TEXT_ELEMENTS = new Set(['script', 'style'])
50
52
 
51
- // Map JSX prop names to HTML attribute names where they differ
52
- export const ATTR_ALIASES: Record<string, string> = {
53
- className: 'class',
54
- htmlFor: 'for',
55
- httpEquiv: 'http-equiv',
56
- acceptCharset: 'accept-charset',
57
- crossOrigin: 'crossorigin',
58
- viewBox: 'viewBox', // SVG keeps camelCase
59
- noModule: 'nomodule',
60
- }
61
-
62
53
  const BOOLEAN_ATTRS = new Set([
63
54
  'allowfullscreen',
64
55
  'async',
@@ -87,7 +78,7 @@ const BOOLEAN_ATTRS = new Set([
87
78
  'selected',
88
79
  ])
89
80
 
90
- export function attrToHtml(name: string, value: unknown): string {
81
+ export function attrToHtml(name: string, value: unknown, isSvg = false): string {
91
82
  if (
92
83
  name === 'children' ||
93
84
  name === 'key' ||
@@ -103,7 +94,7 @@ export function attrToHtml(name: string, value: unknown): string {
103
94
  if (name[0] === 'o' && name[1] === 'n' && typeof value === 'function') return ''
104
95
  if (value == null) return ''
105
96
 
106
- const htmlName = ATTR_ALIASES[name] ?? name.toLowerCase()
97
+ const htmlName = attributeName(name, isSvg)
107
98
 
108
99
  // aria-* and data-* stringify booleans to `"true"`/`"false"` rather than
109
100
  // using boolean-attribute presence semantics — matches React and the ARIA
@@ -44,6 +44,7 @@ export interface WalkOptions {
44
44
  nextBoundaryId: () => number
45
45
  bootstrapped?: boolean | undefined
46
46
  isBoundaryResolution?: boolean | undefined
47
+ isSvg?: boolean | undefined
47
48
  /**
48
49
  * Tracks whether the most recent emission within the *current text flow*
49
50
  * ended with a text node. When the next emission is also text, we emit a
@@ -232,6 +233,7 @@ function walkHost(
232
233
  props: Record<string, any>,
233
234
  opts: WalkOptions,
234
235
  ): void {
236
+ const isSvg = opts.isSvg || tag === 'svg'
235
237
  // <textarea value="..."> serializes its value as a TEXT CHILD, not an
236
238
  // attribute. `defaultValue` is the fallback when `value` is absent. This
237
239
  // matches React and the HTML spec — `<textarea value="x">` is not valid
@@ -282,13 +284,13 @@ function walkHost(
282
284
  if (isInput && (k === 'defaultValue' || k === 'defaultChecked')) continue
283
285
  if (isSelect && (k === 'value' || k === 'defaultValue')) continue
284
286
  if (isOption && k === 'selected') continue
285
- opts.emit(attrToHtml(k, props[k]))
287
+ opts.emit(attrToHtml(k, props[k], isSvg))
286
288
  }
287
289
  if (inputValueAttr !== undefined) {
288
- opts.emit(attrToHtml('value', inputValueAttr))
290
+ opts.emit(attrToHtml('value', inputValueAttr, isSvg))
289
291
  }
290
292
  if (inputCheckedAttr !== undefined) {
291
- opts.emit(attrToHtml('checked', inputCheckedAttr))
293
+ opts.emit(attrToHtml('checked', inputCheckedAttr, isSvg))
292
294
  }
293
295
  if (isOption) {
294
296
  const selectVal = currentSelectValue()
@@ -313,7 +315,11 @@ function walkHost(
313
315
  // Opening a host element starts a fresh text flow context for its children.
314
316
  // Children's text separator tracking is independent of the outer context.
315
317
  const parentTextState = opts.textState
316
- const childOpts: WalkOptions = { ...opts, textState: { lastWasText: false } }
318
+ const childOpts: WalkOptions = {
319
+ ...opts,
320
+ isSvg: isSvg && tag !== 'foreignObject',
321
+ textState: { lastWasText: false },
322
+ }
317
323
 
318
324
  if (tag === 'html' && !hasHeadChild(props.children)) {
319
325
  opts.emit('<head></head>')