@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.
- package/dist/core/attributes.d.ts +1 -0
- package/dist/core/attributes.js +19 -0
- package/dist/core/attributes.js.map +7 -0
- package/dist/core/internal.d.ts +1 -0
- package/dist/core/internal.js.map +2 -2
- package/dist/dom/dispatcher.js +1 -1
- package/dist/dom/dispatcher.js.map +2 -2
- package/dist/dom/dom.js +9 -7
- package/dist/dom/dom.js.map +2 -2
- package/dist/dom/features/hydration/full.js +27 -6
- package/dist/dom/features/hydration/full.js.map +2 -2
- package/dist/dom/features/suspense/full.js +7 -9
- package/dist/dom/features/suspense/full.js.map +2 -2
- package/dist/dom/root-internal.js +2 -0
- package/dist/dom/root-internal.js.map +2 -2
- package/dist/server/escape.d.ts +1 -2
- package/dist/server/escape.js +3 -13
- package/dist/server/escape.js.map +2 -2
- package/dist/server/walk.d.ts +1 -0
- package/dist/server/walk.js +9 -4
- package/dist/server/walk.js.map +2 -2
- package/package.json +1 -1
- package/src/core/attributes.ts +17 -0
- package/src/core/internal.ts +1 -0
- package/src/dom/dispatcher.ts +3 -1
- package/src/dom/dom.ts +10 -7
- package/src/dom/features/hydration/full.ts +27 -8
- package/src/dom/features/suspense/full.ts +8 -10
- package/src/dom/root-internal.ts +2 -0
- package/src/server/escape.ts +4 -13
- package/src/server/walk.ts +10 -4
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
|
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.
|
package/src/dom/root-internal.ts
CHANGED
|
@@ -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
|
package/src/server/escape.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { attributeName } from '../core/attributes'
|
|
2
|
+
|
|
1
3
|
const ATTR_MAP: Record<string, string> = {
|
|
2
4
|
'&': '&',
|
|
3
5
|
'"': '"',
|
|
@@ -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 =
|
|
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
|
package/src/server/walk.ts
CHANGED
|
@@ -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 = {
|
|
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>')
|