@barefootjs/client 0.1.3 → 0.3.0
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/runtime/apply-rest-attrs.d.ts.map +1 -1
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/index.js +219 -111
- package/dist/runtime/map-array.d.ts.map +1 -1
- package/dist/runtime/render.d.ts.map +1 -1
- package/dist/runtime/spread-attrs.d.ts +4 -7
- package/dist/runtime/spread-attrs.d.ts.map +1 -1
- package/dist/runtime/standalone.js +219 -111
- package/package.json +3 -4
- package/src/runtime/apply-rest-attrs.ts +28 -42
- package/src/runtime/component.ts +31 -20
- package/src/runtime/hydrate.ts +6 -0
- package/src/runtime/map-array.ts +11 -0
- package/src/runtime/render.ts +32 -10
- package/src/runtime/spread-attrs.ts +13 -63
package/src/runtime/hydrate.ts
CHANGED
|
@@ -294,6 +294,12 @@ function hydrateCommentScope(comment: Comment): void {
|
|
|
294
294
|
const proxyEl = nextElementSibling(comment) ?? comment.parentElement
|
|
295
295
|
if (!proxyEl) return
|
|
296
296
|
|
|
297
|
+
// A synchronous CSR render() may have already initialized this fragment
|
|
298
|
+
// scope and claimed its proxy before this async walk runs. Honour the same
|
|
299
|
+
// `hydratedScopes` signal the element-scope path uses (hydrateElementScope),
|
|
300
|
+
// so a comment-rooted scope is never re-initialized once claimed.
|
|
301
|
+
if (hydratedScopes.has(proxyEl)) return
|
|
302
|
+
|
|
297
303
|
commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId })
|
|
298
304
|
|
|
299
305
|
const parsed = parseProps(propsJson || null, `comment scope ${scopeId}`)
|
package/src/runtime/map-array.ts
CHANGED
|
@@ -277,6 +277,17 @@ export function mapArray<T>(
|
|
|
277
277
|
scopes.set(key, scope)
|
|
278
278
|
insertScope(scope, container, anchor)
|
|
279
279
|
}
|
|
280
|
+
|
|
281
|
+
// If client has fewer items than SSR rendered, remove orphaned nodes
|
|
282
|
+
for (let i = items.length; i < existingRanges.length; i++) {
|
|
283
|
+
const range = existingRanges[i]
|
|
284
|
+
if (range.startMarker?.parentNode) range.startMarker.remove()
|
|
285
|
+
if (range.primaryEl.parentNode) range.primaryEl.remove()
|
|
286
|
+
for (const ex of range.extras) {
|
|
287
|
+
if (ex.parentNode) ex.remove()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
280
291
|
return // Hydration complete — effects handle future updates
|
|
281
292
|
}
|
|
282
293
|
}
|
package/src/runtime/render.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* never import this module.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { BF_SCOPE } from '@barefootjs/shared'
|
|
9
|
+
import { BF_SCOPE, BF_SCOPE_COMMENT_PREFIX } from '@barefootjs/shared'
|
|
10
10
|
import { setParentScopeId } from './component'
|
|
11
11
|
import { hydratedScopes } from './hydration-state'
|
|
12
12
|
import { getComponentInit } from './registry'
|
|
13
|
+
import { commentScopeRegistry } from './scope'
|
|
13
14
|
import { getTemplate, type TemplateFn } from './template'
|
|
14
15
|
import type { ComponentDef, InitFn } from './types'
|
|
15
16
|
|
|
@@ -86,20 +87,41 @@ export function render(
|
|
|
86
87
|
|
|
87
88
|
const tpl = document.createElement('template')
|
|
88
89
|
tpl.innerHTML = html
|
|
89
|
-
const element = tpl.content.firstChild as HTMLElement
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
const rootElements = Array.from(tpl.content.childNodes).filter(
|
|
92
|
+
(n): n is HTMLElement => n.nodeType === Node.ELEMENT_NODE
|
|
93
|
+
)
|
|
94
94
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
95
|
+
if (rootElements.length === 0) {
|
|
96
|
+
throw new Error('[BarefootJS] render(): template returned empty HTML')
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
container.innerHTML = ''
|
|
100
|
-
container.appendChild(element)
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
if (rootElements.length === 1) {
|
|
102
|
+
const element = rootElements[0]
|
|
103
|
+
if (!element.getAttribute(BF_SCOPE)) {
|
|
104
|
+
element.setAttribute(BF_SCOPE, scopeId)
|
|
105
|
+
}
|
|
106
|
+
container.appendChild(element)
|
|
107
|
+
init(element, props)
|
|
108
|
+
hydratedScopes.add(element)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Multi-root (fragment) template: the child scopes are siblings, not
|
|
113
|
+
// descendants of one root, so $c() can't resolve them by subtree walk.
|
|
114
|
+
// Recreate the SSR fragment layout — a `bf-scope:` comment marker followed
|
|
115
|
+
// by the sibling roots — and register it so candidatesInScope() walks the
|
|
116
|
+
// comment range. Without this only the first root would hydrate.
|
|
117
|
+
const commentNode = document.createComment(`${BF_SCOPE_COMMENT_PREFIX}${scopeId}`)
|
|
118
|
+
container.appendChild(commentNode)
|
|
119
|
+
for (const node of Array.from(tpl.content.childNodes)) {
|
|
120
|
+
container.appendChild(node)
|
|
121
|
+
}
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+
const proxyEl = rootElements[0]
|
|
124
|
+
commentScopeRegistry.set(proxyEl, { commentNode, scopeId })
|
|
125
|
+
init(proxyEl, props)
|
|
126
|
+
hydratedScopes.add(proxyEl)
|
|
105
127
|
}
|
|
@@ -6,83 +6,33 @@
|
|
|
6
6
|
* a static string for server/template rendering of computed local spreads.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { classifyDOMProp } from '@barefootjs/shared'
|
|
9
10
|
import { styleToCss } from './style'
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* SVG attributes that are case-sensitive and MUST stay in camelCase.
|
|
13
|
-
*
|
|
14
|
-
* The default JSX-prop → HTML-attribute rewrite lower-cases camelCase
|
|
15
|
-
* (`fooBar` → `foo-bar`), which is correct for HTML attrs and for the
|
|
16
|
-
* CSS-style SVG presentation attrs (`strokeWidth` → `stroke-width`).
|
|
17
|
-
* The XML-namespaced SVG attrs below, though, are case-sensitive in
|
|
18
|
-
* the spec: `viewBox` lower-cased to `view-box` makes the browser
|
|
19
|
-
* treat it as an unknown user attribute and the SVG no longer renders
|
|
20
|
-
* (pointer events stop hitting the inner geometry — surfaced as the
|
|
21
|
-
* Form Builder e2e regression in #1244's merge-emit follow-up).
|
|
22
|
-
*
|
|
23
|
-
* Coordinates with the compile-time `SVG_CAMEL_TO_KEBAB` table in
|
|
24
|
-
* `packages/jsx/src/ir-to-client-js/utils.ts`: presentation attrs
|
|
25
|
-
* (`clipPath`, `strokeWidth`, …) live there and must NOT appear here,
|
|
26
|
-
* or the same JSX prop would lower to `clip-path` via the explicit-
|
|
27
|
-
* attr path and stay `clipPath` via the spread path — a silent
|
|
28
|
-
* divergence. The list below is XML attribute names that have no
|
|
29
|
-
* kebab-case mirror (`viewBox`, `clipPathUnits`, …).
|
|
30
|
-
*
|
|
31
|
-
* The list mirrors React DOM's `DOMProperty` case-preserving entries
|
|
32
|
-
* (only the attributes that appear on actual SVG elements; ARIA and
|
|
33
|
-
* XLink namespaces are unrelated and handled by their `aria-*` /
|
|
34
|
-
* `xlink:*` literal prefix).
|
|
35
|
-
*/
|
|
36
|
-
const SVG_CAMEL_CASE_ATTRS: ReadonlySet<string> = new Set([
|
|
37
|
-
'allowReorder', 'attributeName', 'attributeType', 'autoReverse',
|
|
38
|
-
'baseFrequency', 'baseProfile', 'calcMode', 'clipPathUnits',
|
|
39
|
-
'contentScriptType', 'contentStyleType', 'diffuseConstant', 'edgeMode',
|
|
40
|
-
'externalResourcesRequired', 'filterRes', 'filterUnits', 'glyphRef',
|
|
41
|
-
'gradientTransform', 'gradientUnits', 'kernelMatrix', 'kernelUnitLength',
|
|
42
|
-
'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'limitingConeAngle',
|
|
43
|
-
'markerHeight', 'markerUnits', 'markerWidth', 'maskContentUnits',
|
|
44
|
-
'maskUnits', 'numOctaves', 'pathLength', 'patternContentUnits',
|
|
45
|
-
'patternTransform', 'patternUnits', 'pointsAtX', 'pointsAtY', 'pointsAtZ',
|
|
46
|
-
'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits', 'refX', 'refY',
|
|
47
|
-
'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures',
|
|
48
|
-
'specularConstant', 'specularExponent', 'spreadMethod', 'startOffset',
|
|
49
|
-
'stdDeviation', 'stitchTiles', 'surfaceScale', 'systemLanguage',
|
|
50
|
-
'tableValues', 'targetX', 'targetY', 'textLength', 'viewBox', 'viewTarget',
|
|
51
|
-
'xChannelSelector', 'yChannelSelector', 'zoomAndPan',
|
|
52
|
-
])
|
|
53
|
-
|
|
54
12
|
/**
|
|
55
13
|
* Convert an object to an HTML attribute string.
|
|
56
|
-
*
|
|
57
|
-
* event handlers,
|
|
58
|
-
* prop is routed through `styleToCss` so object
|
|
59
|
-
* a real CSS string
|
|
60
|
-
*
|
|
61
|
-
* SVG attributes listed in `SVG_CAMEL_CASE_ATTRS` are preserved
|
|
62
|
-
* verbatim — the SVG XML spec is case-sensitive for those names.
|
|
14
|
+
* Uses the shared classifyDOMProp classifier to determine how each prop
|
|
15
|
+
* maps to the DOM. Skips null/undefined/false, event handlers, ref, and
|
|
16
|
+
* children. The `style` prop is routed through `styleToCss` so object
|
|
17
|
+
* literals serialize to a real CSS string.
|
|
63
18
|
*/
|
|
64
19
|
export function spreadAttrs(obj: Record<string, unknown>): string {
|
|
65
20
|
if (!obj || typeof obj !== 'object') return ''
|
|
66
21
|
const parts: string[] = []
|
|
67
22
|
for (const [key, value] of Object.entries(obj)) {
|
|
68
23
|
if (value == null || value === false) continue
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
if (key === 'children') continue
|
|
73
|
-
if (key === 'style') {
|
|
24
|
+
const c = classifyDOMProp(key)
|
|
25
|
+
if (c.kind === 'event' || c.kind === 'skip' || c.kind === 'ref') continue
|
|
26
|
+
if (c.kind === 'style') {
|
|
74
27
|
const css = styleToCss(value)
|
|
75
28
|
if (css != null) parts.push(`style="${css}"`)
|
|
76
29
|
continue
|
|
77
30
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: SVG_CAMEL_CASE_ATTRS.has(key) ? key
|
|
84
|
-
: key.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
85
|
-
parts.push(value === true ? attr : `${attr}="${value}"`)
|
|
31
|
+
if (c.kind === 'boolean' && value === true) {
|
|
32
|
+
parts.push(c.attrName)
|
|
33
|
+
} else {
|
|
34
|
+
parts.push(`${c.attrName}="${value}"`)
|
|
35
|
+
}
|
|
86
36
|
}
|
|
87
37
|
return parts.join(' ')
|
|
88
38
|
}
|