@plastic-js/plastic 1.0.2 → 1.0.4
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/package.json +2 -2
- package/src/jsx-runtime.js +373 -74
- package/src/merge-props.js +20 -0
- package/src/reactivity.js +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plastic-js/plastic",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"access": "public",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"@babel/core": "^7.29.0",
|
|
58
58
|
"@babel/preset-react": "^7.28.5",
|
|
59
59
|
"@testing-library/jest-dom": "^6.9.1",
|
|
60
|
-
"babel-preset-plastic": "^0.1.1",
|
|
61
60
|
"@vitejs/plugin-react": "^6.0.2",
|
|
62
61
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
62
|
+
"babel-preset-plastic": "^0.1.5",
|
|
63
63
|
"eslint": "^9.39.2",
|
|
64
64
|
"eslint-config-janus": "^9.0.21",
|
|
65
65
|
"eslint-plugin-mocha": "^11.3.0",
|
package/src/jsx-runtime.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
batch, createComputed, createSignal, createTree, effect, isComputed, isSignal, isTree, runUntracked, toRaw,
|
|
3
3
|
} from './reactivity.js'
|
|
4
|
+
import { getActiveSub, setActiveSub } from 'alien-signals'
|
|
4
5
|
import {
|
|
5
6
|
flattenChildren, isEventProp, normalizeTextNodeValue, toClassMap, toClassTokens,
|
|
6
7
|
} from './utils.js'
|
|
7
8
|
import { getCurrentComputation, setCurrentComputation } from './computation-context.js'
|
|
8
9
|
import { createControlFlow } from './control-flow.js'
|
|
9
|
-
import { isMergedProps, mergeProps } from './merge-props.js'
|
|
10
|
+
import { hasMergedPropsStaticKeys, isMergedProps, mergeProps } from './merge-props.js'
|
|
10
11
|
|
|
11
12
|
const Fragment = Symbol('Fragment')
|
|
12
13
|
const OWNER = Symbol('owner')
|
|
@@ -21,32 +22,32 @@ const CONTEXT_ID = Symbol('context-id')
|
|
|
21
22
|
|
|
22
23
|
const getCurrentOwner = ()=> currentOwner
|
|
23
24
|
|
|
25
|
+
// Owner fields are lazily allocated. Most components register nothing
|
|
26
|
+
// (no onMount, no context, no refs), so paying for a Set + Map + 4 arrays
|
|
27
|
+
// per component dominated mount cost. Allocate each collection only on first
|
|
28
|
+
// use. Readers must tolerate `undefined`.
|
|
24
29
|
const createOwner = (parent = null)=> {
|
|
25
30
|
const owner = {
|
|
26
31
|
parent,
|
|
27
|
-
children:
|
|
28
|
-
cleanups:
|
|
29
|
-
effects:
|
|
30
|
-
contexts:
|
|
31
|
-
refs:
|
|
32
|
-
mounts:
|
|
32
|
+
children: undefined,
|
|
33
|
+
cleanups: undefined,
|
|
34
|
+
effects: undefined,
|
|
35
|
+
contexts: undefined,
|
|
36
|
+
refs: undefined,
|
|
37
|
+
mounts: undefined,
|
|
33
38
|
mounted: false,
|
|
34
39
|
}
|
|
35
40
|
if (parent){
|
|
36
|
-
parent.children.add(owner)
|
|
41
|
+
(parent.children ??= new Set()).add(owner)
|
|
37
42
|
}
|
|
38
43
|
return owner
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
const runOwnerMounts = (owner)=> {
|
|
42
47
|
runUntracked(()=> {
|
|
43
|
-
owner.children
|
|
44
|
-
owner.refs
|
|
45
|
-
|
|
46
|
-
})
|
|
47
|
-
owner.mounts.forEach((fn)=> {
|
|
48
|
-
fn()
|
|
49
|
-
})
|
|
48
|
+
owner.children?.forEach(child=> runOwnerMounts(child))
|
|
49
|
+
owner.refs?.forEach((fn)=> { fn() })
|
|
50
|
+
owner.mounts?.forEach((fn)=> { fn() })
|
|
50
51
|
owner.mounted = true
|
|
51
52
|
})
|
|
52
53
|
}
|
|
@@ -87,29 +88,33 @@ const mountOwnedSubtree = (node)=> {
|
|
|
87
88
|
|
|
88
89
|
const disposeOwner = (owner)=> {
|
|
89
90
|
if (owner.parent){
|
|
90
|
-
owner.parent.children
|
|
91
|
+
owner.parent.children?.delete(owner)
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
// Dispose all child owners recursively
|
|
94
|
-
owner.children
|
|
95
|
-
|
|
95
|
+
if (owner.children){
|
|
96
|
+
owner.children.forEach(child=> disposeOwner(child))
|
|
97
|
+
owner.children.clear()
|
|
98
|
+
}
|
|
96
99
|
|
|
97
100
|
// Stop owner-bound effects for this scope during unmount.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
stop
|
|
101
|
+
if (owner.effects){
|
|
102
|
+
for (let i = owner.effects.length - 1; i >= 0; i -= 1){
|
|
103
|
+
const stop = owner.effects[i]
|
|
104
|
+
if (typeof stop === 'function') stop()
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
owner.effects.length = 0
|
|
107
|
+
}
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
cleanup
|
|
109
|
+
if (owner.cleanups){
|
|
110
|
+
for (let i = owner.cleanups.length - 1; i >= 0; i -= 1){
|
|
111
|
+
const cleanup = owner.cleanups[i]
|
|
112
|
+
if (typeof cleanup === 'function') cleanup()
|
|
108
113
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
owner.refs.length = 0
|
|
112
|
-
owner.mounts.length = 0
|
|
114
|
+
owner.cleanups.length = 0
|
|
115
|
+
}
|
|
116
|
+
if (owner.refs) owner.refs.length = 0
|
|
117
|
+
if (owner.mounts) owner.mounts.length = 0
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
const flushCleanups = (list)=> {
|
|
@@ -151,8 +156,8 @@ const createBindingEffect = (runner)=> {
|
|
|
151
156
|
return stop
|
|
152
157
|
}
|
|
153
158
|
// Register effect and its disposal in the owner
|
|
154
|
-
owner.effects.push(stop)
|
|
155
|
-
owner.cleanups.push(()=> {
|
|
159
|
+
(owner.effects ??= []).push(stop)
|
|
160
|
+
;(owner.cleanups ??= []).push(()=> {
|
|
156
161
|
// Run remaining local cleanups, untracked to avoid accidental dependency registration
|
|
157
162
|
flushCleanups(local)
|
|
158
163
|
})
|
|
@@ -168,7 +173,7 @@ const registerCleanup = (fn)=> {
|
|
|
168
173
|
if (currentComputation){
|
|
169
174
|
currentComputation.cleanups.push(fn)
|
|
170
175
|
} else {
|
|
171
|
-
currentOwner.cleanups.push(fn)
|
|
176
|
+
(currentOwner.cleanups ??= []).push(fn)
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
|
|
@@ -177,14 +182,14 @@ const onMount = (fn)=> {
|
|
|
177
182
|
if (!currentOwner){
|
|
178
183
|
throw new Error('onMount must be called within a component scope')
|
|
179
184
|
}
|
|
180
|
-
currentOwner.mounts.push(fn)
|
|
185
|
+
(currentOwner.mounts ??= []).push(fn)
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
const onUnmount = (fn)=> {
|
|
184
189
|
if (!currentOwner){
|
|
185
190
|
throw new Error('onUnmount must be called within a component scope')
|
|
186
191
|
}
|
|
187
|
-
currentOwner.cleanups.push(fn)
|
|
192
|
+
(currentOwner.cleanups ??= []).push(fn)
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
const createContext = (defaultValue)=> {
|
|
@@ -198,7 +203,7 @@ const createContext = (defaultValue)=> {
|
|
|
198
203
|
throw new Error('Context Provider must be rendered within a component scope')
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
currentOwner.contexts.set(context[CONTEXT_ID], value)
|
|
206
|
+
(currentOwner.contexts ??= new Map()).set(context[CONTEXT_ID], value)
|
|
202
207
|
|
|
203
208
|
if (typeof children === 'function'){
|
|
204
209
|
return children()
|
|
@@ -221,7 +226,7 @@ const useContext = (context)=> {
|
|
|
221
226
|
|
|
222
227
|
let owner = currentOwner
|
|
223
228
|
while (owner){
|
|
224
|
-
if (owner.contexts
|
|
229
|
+
if (owner.contexts?.has(context[CONTEXT_ID])){
|
|
225
230
|
return owner.contexts.get(context[CONTEXT_ID])
|
|
226
231
|
}
|
|
227
232
|
owner = owner.parent
|
|
@@ -331,8 +336,21 @@ const materializeComponentDescriptor = (descriptor)=> {
|
|
|
331
336
|
componentProps = mergeProps(componentProps, { children: kids })
|
|
332
337
|
}
|
|
333
338
|
|
|
334
|
-
|
|
335
|
-
|
|
339
|
+
// Fuse the owner swap and untracked context into one try/finally. Inlining
|
|
340
|
+
// avoids the runUntracked closure + extra try/finally per component, which
|
|
341
|
+
// shows up in profile (3000 components × these wrappers was a meaningful
|
|
342
|
+
// share of mount time).
|
|
343
|
+
const prevOwner = currentOwner
|
|
344
|
+
const prevSub = getActiveSub()
|
|
345
|
+
currentOwner = owner
|
|
346
|
+
setActiveSub(undefined)
|
|
347
|
+
let normalized
|
|
348
|
+
try {
|
|
349
|
+
normalized = node2Element(descriptor.tag(componentProps))
|
|
350
|
+
} finally {
|
|
351
|
+
currentOwner = prevOwner
|
|
352
|
+
setActiveSub(prevSub)
|
|
353
|
+
}
|
|
336
354
|
|
|
337
355
|
if (normalized instanceof Node){
|
|
338
356
|
normalized[OWNER] = owner
|
|
@@ -594,7 +612,7 @@ const applyRefProp = (element, ref)=> {
|
|
|
594
612
|
const owner = currentOwner
|
|
595
613
|
|
|
596
614
|
if (owner && !owner.mounted) {
|
|
597
|
-
owner.refs.push(() => assignRef(element))
|
|
615
|
+
(owner.refs ??= []).push(() => assignRef(element))
|
|
598
616
|
} else {
|
|
599
617
|
assignRef(element)
|
|
600
618
|
}
|
|
@@ -622,7 +640,45 @@ const disposeBindings = (bindings)=> {
|
|
|
622
640
|
// free. The value is resolved through `resolveReactiveValue` so signals and
|
|
623
641
|
// zero-arg accessor thunks (used widely by ark-plastic / zag adapters) are
|
|
624
642
|
// unwrapped before being applied to the DOM.
|
|
625
|
-
const bindReactiveProp = (element, props, key)=> {
|
|
643
|
+
const bindReactiveProp = (element, props, key, propsIsTracked)=> {
|
|
644
|
+
const rawValue = props[key]
|
|
645
|
+
|
|
646
|
+
// Static (non-reactive) fast path: write the prop directly with no binding
|
|
647
|
+
// effect. Most JSX props in real apps are static (className='row', id=...,
|
|
648
|
+
// type='button' etc.) — building an effect for each one was the biggest
|
|
649
|
+
// per-component cost in mount benchmarks.
|
|
650
|
+
// Skip the fast path when `props` is itself a tracking proxy (tree /
|
|
651
|
+
// mergeProps): the value may be a plain string but reading `props[key]`
|
|
652
|
+
// subscribes through the proxy, so we need an effect to re-run on change.
|
|
653
|
+
if (!propsIsTracked && !isReactive(rawValue)){
|
|
654
|
+
let prevStyleValue
|
|
655
|
+
if (key === 'className' || key === 'class'){
|
|
656
|
+
applyClassProp(element, rawValue)
|
|
657
|
+
} else if (key === 'style'){
|
|
658
|
+
prevStyleValue = applyStyleProp(element, rawValue, undefined)
|
|
659
|
+
} else {
|
|
660
|
+
const domKey = JSX_PROP_MAP[key] ?? key
|
|
661
|
+
setDomProp(element, domKey, rawValue)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return ()=> {
|
|
665
|
+
if (key === 'className' || key === 'class'){
|
|
666
|
+
element.removeAttribute('class')
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
if (key === 'style'){
|
|
670
|
+
if (prevStyleValue && typeof prevStyleValue === 'object'){
|
|
671
|
+
Object.keys(prevStyleValue).forEach(prop=> clearStyleKey(element, prop))
|
|
672
|
+
} else if (typeof prevStyleValue === 'string'){
|
|
673
|
+
element.style.cssText = ''
|
|
674
|
+
}
|
|
675
|
+
return
|
|
676
|
+
}
|
|
677
|
+
const domKey = JSX_PROP_MAP[key] ?? key
|
|
678
|
+
clearDomProp(element, domKey)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
626
682
|
let prevStyleValue
|
|
627
683
|
const stop = createBindingEffect(()=> {
|
|
628
684
|
const value = resolveReactiveValue(props[key])
|
|
@@ -694,11 +750,16 @@ const bindReactiveEvent = (element, props, key)=> {
|
|
|
694
750
|
// keys later we tear down the previous bindings and rebuild them from the
|
|
695
751
|
// current key set.
|
|
696
752
|
const applyProps = (element, props = {})=> {
|
|
697
|
-
|
|
753
|
+
const propsIsTracked = isMergedProps(props) || isTree(props)
|
|
754
|
+
const setup = ()=> {
|
|
698
755
|
const bindings = []
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
756
|
+
// Only register cleanup if there's an owner/computation to attach to.
|
|
757
|
+
// Allows top-level h() calls outside any component scope (e.g. tests).
|
|
758
|
+
if (currentOwner || getCurrentComputation()){
|
|
759
|
+
registerCleanup(()=> {
|
|
760
|
+
disposeBindings(bindings)
|
|
761
|
+
})
|
|
762
|
+
}
|
|
702
763
|
|
|
703
764
|
for (const key of Reflect.ownKeys(props)){
|
|
704
765
|
if (typeof key === 'symbol' || key === 'children' || key === 'key'){
|
|
@@ -721,42 +782,71 @@ const applyProps = (element, props = {})=> {
|
|
|
721
782
|
bindings.push(bindReactiveEvent(element, props, key))
|
|
722
783
|
continue
|
|
723
784
|
}
|
|
724
|
-
bindings.push(bindReactiveProp(element, props, key))
|
|
785
|
+
bindings.push(bindReactiveProp(element, props, key, propsIsTracked))
|
|
725
786
|
}
|
|
726
|
-
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Only wrap in an outer binding effect when the proxy has dynamic keys (a
|
|
790
|
+
// function-typed spread source like `{...api()}`, or any tree proxy whose
|
|
791
|
+
// key set may grow/shrink at runtime). For static-key proxies (the common
|
|
792
|
+
// babel reactive transform output with no spreads), and for plain object
|
|
793
|
+
// props, keys never change — skip the outer effect to avoid one
|
|
794
|
+
// owner-effect allocation per element.
|
|
795
|
+
if ((isMergedProps(props) && !hasMergedPropsStaticKeys(props)) || isTree(props)){
|
|
796
|
+
createBindingEffect(setup)
|
|
797
|
+
} else {
|
|
798
|
+
setup()
|
|
799
|
+
}
|
|
727
800
|
return element
|
|
728
801
|
}
|
|
729
802
|
|
|
730
803
|
// Normalize any JSX return value into a DOM node that can be appended safely.
|
|
804
|
+
// Dispatch is organized as a `typeof` switch so the engine can lower it to a
|
|
805
|
+
// jump table, and the most frequent shapes are handled first:
|
|
806
|
+
// - 'string'/'number': text children like `["item ", i]` (hottest in lists)
|
|
807
|
+
// - 'object' → Node: the Element returned from a component body
|
|
808
|
+
// - 'object' → descriptor: child components in an array
|
|
809
|
+
// Within the 'object' arm we use `node.nodeType != null` instead of
|
|
810
|
+
// `instanceof Node`, since a property read is cheaper than walking the
|
|
811
|
+
// prototype chain, and we read the descriptor symbol directly to skip the
|
|
812
|
+
// redundant `typeof === 'object'` check inside isComponentDescriptor.
|
|
731
813
|
const node2Element = (node)=> {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
814
|
+
switch (typeof node){
|
|
815
|
+
case 'string':
|
|
816
|
+
case 'number':
|
|
817
|
+
return document.createTextNode(String(node))
|
|
818
|
+
case 'function':
|
|
819
|
+
// Signals and computeds are functions too (alien-signals binds an
|
|
820
|
+
// operator function) — they render as reactive text nodes, while a
|
|
821
|
+
// plain function child is a thunk that returns dynamic content.
|
|
822
|
+
if (isReactivePrimitive(node)) return createReactiveTextNode(node)
|
|
823
|
+
return createReactiveChildNode(node)
|
|
824
|
+
case 'undefined':
|
|
825
|
+
return createPlaceholder()
|
|
826
|
+
case 'object': {
|
|
827
|
+
if (node === null) return createPlaceholder()
|
|
828
|
+
// Real DOM node returned from h()/jsx() — by far the most common
|
|
829
|
+
// 'object' case during initial mount.
|
|
830
|
+
if (node.nodeType != null){
|
|
831
|
+
flushPendingDescriptors(node)
|
|
832
|
+
return node
|
|
833
|
+
}
|
|
834
|
+
if (node[COMPONENT_DESCRIPTOR] === true){
|
|
835
|
+
return materializeComponentDescriptor(node)
|
|
836
|
+
}
|
|
837
|
+
if (Array.isArray(node)){
|
|
838
|
+
const fragment = document.createDocumentFragment()
|
|
839
|
+
appendChildren(fragment, node)
|
|
840
|
+
// Do NOT flush here — the caller (appendChild) will transfer any pending
|
|
841
|
+
// descriptors to the real parent element before draining the fragment, so
|
|
842
|
+
// flushPendingDescriptors runs later with the correct owner active.
|
|
843
|
+
return fragment
|
|
844
|
+
}
|
|
845
|
+
return createPlaceholder()
|
|
846
|
+
}
|
|
847
|
+
default:
|
|
848
|
+
return createPlaceholder()
|
|
758
849
|
}
|
|
759
|
-
return createPlaceholder()
|
|
760
850
|
}
|
|
761
851
|
|
|
762
852
|
const materializeNode = node=> node2Element(node)
|
|
@@ -956,10 +1046,212 @@ const jsx = (tag, props, key)=> {
|
|
|
956
1046
|
return h(tag, mergeProps(props, { key }))
|
|
957
1047
|
}
|
|
958
1048
|
const jsxs = jsx
|
|
1049
|
+
|
|
1050
|
+
// Static fast path counterpart to applyProps: writes a single literal value
|
|
1051
|
+
// straight to the DOM with no isReactive check, no binding-effect wrapper, and
|
|
1052
|
+
// no JSX_PROP_MAP miss for the hot keys. Used exclusively by jsxStatic, which
|
|
1053
|
+
// the babel reactive transform emits when every attribute value is provably a
|
|
1054
|
+
// scalar literal (string/number/boolean/null) — so there is nothing to track,
|
|
1055
|
+
// nothing to dispose, and the prop never changes after initial mount.
|
|
1056
|
+
const applyStaticProp = (element, key, value)=> {
|
|
1057
|
+
if (key === 'className' || key === 'class'){
|
|
1058
|
+
applyClassProp(element, value)
|
|
1059
|
+
return
|
|
1060
|
+
}
|
|
1061
|
+
if (key === 'style'){
|
|
1062
|
+
applyStyleProp(element, value, undefined)
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
setDomProp(element, JSX_PROP_MAP[key] ?? key, value)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// jsxStatic — emitted by the babel reactive transform when ALL of these hold:
|
|
1069
|
+
// - tag is a lowercase intrinsic string (no components, no Dynamic)
|
|
1070
|
+
// - at least one attribute, none of them spread
|
|
1071
|
+
// - every attribute value is a scalar literal (string/number/boolean/null)
|
|
1072
|
+
//
|
|
1073
|
+
// Under those conditions there are no reactive props, no event handlers, no
|
|
1074
|
+
// refs, no merged-props proxy — applyProps' entire dispatch machinery
|
|
1075
|
+
// (createBindingEffect / isMergedProps / Reflect.ownKeys / bindReactiveProp /
|
|
1076
|
+
// isReactive) is pure overhead. We bypass it and write each prop directly.
|
|
1077
|
+
// Children pass through unchanged: babel emits the same child expressions it
|
|
1078
|
+
// would for jsx(), so dynamic children still arrive as thunks/descriptors and
|
|
1079
|
+
// are routed through appendChild's existing reactive/defer paths.
|
|
1080
|
+
const jsxStatic = (tag, props, child)=> {
|
|
1081
|
+
const element = SVG_TAGS.has(tag)
|
|
1082
|
+
? document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
1083
|
+
: document.createElement(tag)
|
|
1084
|
+
for (const key in props){
|
|
1085
|
+
// Defensive: skip framework-reserved keys in case the babel transform
|
|
1086
|
+
// ever emits them here. `children` shouldn't appear (it's the third
|
|
1087
|
+
// arg), and `key` is consumed by the reconciler upstream.
|
|
1088
|
+
if (key === 'children' || key === 'key') continue
|
|
1089
|
+
applyStaticProp(element, key, props[key])
|
|
1090
|
+
}
|
|
1091
|
+
if (child !== undefined){
|
|
1092
|
+
// Babel emits the third arg as a single value when there is one child
|
|
1093
|
+
// and an array when there are several — mirror jsx() / jsxs() shape.
|
|
1094
|
+
if (Array.isArray(child)){
|
|
1095
|
+
appendChildren(element, child)
|
|
1096
|
+
} else {
|
|
1097
|
+
appendChild(element, child)
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return element
|
|
1101
|
+
}
|
|
959
1102
|
// jsxDEV is the development-mode variant used by automatic JSX transforms; the extra
|
|
960
1103
|
// debug arguments (isStaticChildren, source, self) are unused at runtime.
|
|
961
1104
|
const jsxDEV = (tag, props, key) => jsx(tag, props, key)
|
|
962
1105
|
|
|
1106
|
+
// ============ DOM template cloning (Solid-style) ============
|
|
1107
|
+
// Three helpers the babel reactive transform can emit when it identifies a
|
|
1108
|
+
// statically-shaped JSX subtree. They let the framework skip per-element
|
|
1109
|
+
// createElement/applyProps/appendChild for the static skeleton and only do
|
|
1110
|
+
// runtime work for the dynamic holes that babel can't fold into the template.
|
|
1111
|
+
//
|
|
1112
|
+
// Emission shape from babel:
|
|
1113
|
+
// const _tmpl$ = /*#__PURE__*/ template('<div class="row"><span>item </span></div>')
|
|
1114
|
+
// const Row = ({i}) => {
|
|
1115
|
+
// const _el = _tmpl$.cloneNode(true)
|
|
1116
|
+
// const _span = _el.firstChild
|
|
1117
|
+
// insert(_span, () => i, null) // dynamic text appended at end of span
|
|
1118
|
+
// return _el
|
|
1119
|
+
// }
|
|
1120
|
+
// Static props go into the template string itself; dynamic props use setProp;
|
|
1121
|
+
// dynamic children use insert. There is no separate clone helper — babel emits
|
|
1122
|
+
// `_tmpl$.cloneNode(true)` directly.
|
|
1123
|
+
|
|
1124
|
+
// Parse `html` into a detached template and return the first child of its
|
|
1125
|
+
// content. Babel emits ONE module-level `template()` call per unique static
|
|
1126
|
+
// shape, so the parse cost is amortized across every instance — only the
|
|
1127
|
+
// per-instance `cloneNode(true)` remains hot.
|
|
1128
|
+
//
|
|
1129
|
+
// SVG note: an `<svg>` element parsed via innerHTML of an HTMLTemplateElement
|
|
1130
|
+
// correctly inherits the SVG namespace, but standalone SVG children (`<circle>`
|
|
1131
|
+
// at the root) do not — pass isSVG=true to wrap-then-unwrap so the namespace
|
|
1132
|
+
// resolves on every node.
|
|
1133
|
+
const template = (html, isSVG = false)=> {
|
|
1134
|
+
const t = document.createElement('template')
|
|
1135
|
+
if (isSVG){
|
|
1136
|
+
t.innerHTML = `<svg>${html}</svg>`
|
|
1137
|
+
const svgRoot = t.content.firstChild
|
|
1138
|
+
// Unwrap so the caller's first cloneNode points at the real root.
|
|
1139
|
+
return svgRoot.firstChild ?? svgRoot
|
|
1140
|
+
}
|
|
1141
|
+
t.innerHTML = html
|
|
1142
|
+
return t.content.firstChild
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Recursively resolve `value` (which may be a thunk, signal, descriptor, node,
|
|
1146
|
+
// primitive, or array) into a flat list of Nodes ready to insert. Mirrors what
|
|
1147
|
+
// node2Element does but flattens arrays in place instead of producing a
|
|
1148
|
+
// DocumentFragment, since insert() needs direct control over each Node so the
|
|
1149
|
+
// next reactive run can remove exactly the nodes it inserted.
|
|
1150
|
+
const resolveChildToNodes = (value, out)=> {
|
|
1151
|
+
let v = value
|
|
1152
|
+
// Walk through plain thunks (NOT signals/computeds — those resolve to a
|
|
1153
|
+
// reactive text node so the framework can update .data in place).
|
|
1154
|
+
let steps = 0
|
|
1155
|
+
while (typeof v === 'function' && !isReactivePrimitive(v) && steps < MAX_REACTIVE_RESOLVE_STEPS){
|
|
1156
|
+
v = v()
|
|
1157
|
+
steps += 1
|
|
1158
|
+
}
|
|
1159
|
+
if (v == null || v === true || v === false) return
|
|
1160
|
+
if (v instanceof Node){
|
|
1161
|
+
out.push(v)
|
|
1162
|
+
return
|
|
1163
|
+
}
|
|
1164
|
+
if (Array.isArray(v)){
|
|
1165
|
+
for (const item of v) resolveChildToNodes(item, out)
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
if (isComponentDescriptor(v)){
|
|
1169
|
+
out.push(materializeComponentDescriptor(v))
|
|
1170
|
+
return
|
|
1171
|
+
}
|
|
1172
|
+
if (isReactivePrimitive(v)){
|
|
1173
|
+
out.push(createReactiveTextNode(v))
|
|
1174
|
+
return
|
|
1175
|
+
}
|
|
1176
|
+
out.push(document.createTextNode(String(v)))
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Insert dynamic content into `parent` immediately before `marker` (or at the
|
|
1180
|
+
// end when marker is null). Static accessors run once; functions/signals wrap
|
|
1181
|
+
// in a binding effect so the slot updates whenever the source changes.
|
|
1182
|
+
//
|
|
1183
|
+
// Single-text-node fast path: when both the previous and next render produce a
|
|
1184
|
+
// single text/string/number, mutate the existing text node's `.data` instead
|
|
1185
|
+
// of swapping nodes. This matches Solid's behavior and avoids DOM thrash for
|
|
1186
|
+
// the common `{count()}` shape.
|
|
1187
|
+
const insert = (parent, accessor, marker = null)=> {
|
|
1188
|
+
if (typeof accessor !== 'function'){
|
|
1189
|
+
const nodes = []
|
|
1190
|
+
resolveChildToNodes(accessor, nodes)
|
|
1191
|
+
for (const n of nodes) parent.insertBefore(n, marker)
|
|
1192
|
+
return
|
|
1193
|
+
}
|
|
1194
|
+
let current = []
|
|
1195
|
+
createBindingEffect(()=> {
|
|
1196
|
+
const next = accessor()
|
|
1197
|
+
|
|
1198
|
+
// Fast path: single existing text node + scalar next value → in-place data update.
|
|
1199
|
+
if (current.length === 1 && current[0].nodeType === 3
|
|
1200
|
+
&& (typeof next === 'string' || typeof next === 'number')){
|
|
1201
|
+
const str = String(next)
|
|
1202
|
+
if (current[0].data !== str) current[0].data = str
|
|
1203
|
+
return
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const nodes = []
|
|
1207
|
+
resolveChildToNodes(next, nodes)
|
|
1208
|
+
|
|
1209
|
+
// Naive diff: remove current, insert next. Adequate for single-slot
|
|
1210
|
+
// inserts; keyed list reconciliation is handled separately by <Loop>.
|
|
1211
|
+
for (const n of current){
|
|
1212
|
+
if (n.parentNode === parent) parent.removeChild(n)
|
|
1213
|
+
}
|
|
1214
|
+
for (const n of nodes) parent.insertBefore(n, marker)
|
|
1215
|
+
|
|
1216
|
+
// Mount any owners attached to inserted subtrees (component roots inside
|
|
1217
|
+
// the inserted nodes register their owner on the DOM node — runOwnerMounts
|
|
1218
|
+
// must fire once they're connected).
|
|
1219
|
+
if (parent.isConnected){
|
|
1220
|
+
for (const n of nodes) mountOwnedSubtree(n)
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
current = nodes
|
|
1224
|
+
})
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Apply a single prop (static or reactive) to an element. Static values write
|
|
1228
|
+
// once; function/signal values wrap in a binding effect that re-applies on
|
|
1229
|
+
// change. Class and style go through the existing helpers so the
|
|
1230
|
+
// merge/diff semantics stay identical to the JSX path.
|
|
1231
|
+
const setProp = (element, key, accessor)=> {
|
|
1232
|
+
if (typeof accessor !== 'function' || isReactivePrimitive(accessor)){
|
|
1233
|
+
// Reactive primitives (signal/computed) also go through the effect path
|
|
1234
|
+
// so the prop stays in sync — handled in the else branch below.
|
|
1235
|
+
if (!isReactive(accessor)){
|
|
1236
|
+
applyStaticProp(element, key, accessor)
|
|
1237
|
+
return
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
let prevStyle
|
|
1241
|
+
createBindingEffect(()=> {
|
|
1242
|
+
const v = resolveReactiveValue(accessor)
|
|
1243
|
+
if (key === 'className' || key === 'class'){
|
|
1244
|
+
applyClassProp(element, v)
|
|
1245
|
+
return
|
|
1246
|
+
}
|
|
1247
|
+
if (key === 'style'){
|
|
1248
|
+
prevStyle = applyStyleProp(element, v, prevStyle)
|
|
1249
|
+
return
|
|
1250
|
+
}
|
|
1251
|
+
setDomProp(element, JSX_PROP_MAP[key] ?? key, v)
|
|
1252
|
+
})
|
|
1253
|
+
}
|
|
1254
|
+
|
|
963
1255
|
// Render by appending the normalized root node into the target container.
|
|
964
1256
|
// Returns a disposer function that cleans up all effects and listeners.
|
|
965
1257
|
const renderApp = (container, node)=> {
|
|
@@ -995,6 +1287,13 @@ export {
|
|
|
995
1287
|
jsx,
|
|
996
1288
|
jsxDEV,
|
|
997
1289
|
jsxs,
|
|
1290
|
+
jsxStatic,
|
|
1291
|
+
// DOM template cloning helpers (babel reactive transform emits these for
|
|
1292
|
+
// statically-shaped JSX subtrees — they let cloned templates skip the
|
|
1293
|
+
// per-element jsx()/applyProps machinery)
|
|
1294
|
+
template,
|
|
1295
|
+
insert,
|
|
1296
|
+
setProp,
|
|
998
1297
|
onMount,
|
|
999
1298
|
onUnmount,
|
|
1000
1299
|
createContext,
|
package/src/merge-props.js
CHANGED
|
@@ -218,11 +218,30 @@ const readOnlyTrap = ()=> {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const IS_MERGED_PROPS = Symbol('mergeProps')
|
|
221
|
+
const HAS_STATIC_KEYS = Symbol('mergeProps.staticKeys')
|
|
221
222
|
|
|
222
223
|
export const mergeProps = (...sources)=> {
|
|
224
|
+
// Fast path: a single plain-object source is structurally equivalent to the
|
|
225
|
+
// object itself for prop access (no class/className aliasing across sources,
|
|
226
|
+
// no style merging, no spread thunks). Babel reactive transform wraps every
|
|
227
|
+
// JSX in `mergeProps({...})` even with no spreads, so this short-circuit
|
|
228
|
+
// eliminates one Proxy allocation per element on the typical render path.
|
|
229
|
+
if (sources.length === 1 && sources[0] != null && typeof sources[0] === 'object' && isPlainObject(sources[0])){
|
|
230
|
+
return sources[0]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// When no source is a function-typed spread (`{...api()}`), the key set is
|
|
234
|
+
// fixed at construction time — applyProps can skip its outer key-tracking
|
|
235
|
+
// effect entirely. This is the common case under the babel reactive transform
|
|
236
|
+
// which wraps every JSX in mergeProps even when there are no spreads.
|
|
237
|
+
let hasStaticKeys = true
|
|
238
|
+
for (let i = 0; i < sources.length; i += 1){
|
|
239
|
+
if (typeof sources[i] === 'function'){ hasStaticKeys = false; break }
|
|
240
|
+
}
|
|
223
241
|
return new Proxy({}, {
|
|
224
242
|
get: (_, key)=> {
|
|
225
243
|
if (key === IS_MERGED_PROPS) return true
|
|
244
|
+
if (key === HAS_STATIC_KEYS) return hasStaticKeys
|
|
226
245
|
return resolveKey(sources, key)
|
|
227
246
|
},
|
|
228
247
|
has: (_, key)=> hasKey(sources, key),
|
|
@@ -243,3 +262,4 @@ export const mergeProps = (...sources)=> {
|
|
|
243
262
|
}
|
|
244
263
|
|
|
245
264
|
export const isMergedProps = (value)=> value != null && typeof value === 'object' && value[IS_MERGED_PROPS] === true
|
|
265
|
+
export const hasMergedPropsStaticKeys = (value)=> value != null && typeof value === 'object' && value[HAS_STATIC_KEYS] === true
|
package/src/reactivity.js
CHANGED
|
@@ -353,9 +353,19 @@ const tree = (obj)=> {
|
|
|
353
353
|
// Internal binding effects/computations create their own active subscribers,
|
|
354
354
|
// so suppressing the outer one here only affects bare signal reads in the
|
|
355
355
|
// component body.
|
|
356
|
+
// Fast path: when no subscriber is currently active (the typical case during
|
|
357
|
+
// initial mount from `renderApp`, outside any enclosing effect), the
|
|
358
|
+
// swap-to-undefined and restore are both no-ops. Skip the try/finally and the
|
|
359
|
+
// swap entirely — this is invoked once per component, so eliminating the
|
|
360
|
+
// instrumentation here removes measurable per-component overhead during mount.
|
|
361
|
+
//
|
|
362
|
+
// Slow path: a single `setActiveSub(undefined)` swap returns the previous
|
|
363
|
+
// subscriber in one call, saving the separate `getActiveSub()` read.
|
|
356
364
|
const runUntracked = (fn)=> {
|
|
357
|
-
|
|
358
|
-
|
|
365
|
+
if (getActiveSub() === undefined){
|
|
366
|
+
return fn()
|
|
367
|
+
}
|
|
368
|
+
const prevSub = setActiveSub(undefined)
|
|
359
369
|
try {
|
|
360
370
|
return fn()
|
|
361
371
|
} finally {
|