@fictjs/runtime 0.5.2 → 0.7.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/advanced.cjs +13 -9
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +4 -4
- package/dist/advanced.d.ts +4 -4
- package/dist/advanced.js +8 -4
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-D2IWOO4X.js → chunk-4LCHQ7U4.js} +250 -99
- package/dist/chunk-4LCHQ7U4.js.map +1 -0
- package/dist/{chunk-LRFMCJY3.js → chunk-7YQK3XKY.js} +120 -27
- package/dist/chunk-7YQK3XKY.js.map +1 -0
- package/dist/{chunk-QB2UD62G.cjs → chunk-CEV6TO5U.cjs} +8 -8
- package/dist/{chunk-QB2UD62G.cjs.map → chunk-CEV6TO5U.cjs.map} +1 -1
- package/dist/{chunk-ZR435MDC.cjs → chunk-FSCBL7RI.cjs} +120 -27
- package/dist/chunk-FSCBL7RI.cjs.map +1 -0
- package/dist/{chunk-KNGHYGK4.cjs → chunk-HHDHQGJY.cjs} +17 -17
- package/dist/{chunk-KNGHYGK4.cjs.map → chunk-HHDHQGJY.cjs.map} +1 -1
- package/dist/{chunk-Z6M3HKLG.cjs → chunk-PRF4QG73.cjs} +400 -249
- package/dist/chunk-PRF4QG73.cjs.map +1 -0
- package/dist/{chunk-4NUHM77Z.js → chunk-TLDT76RV.js} +3 -3
- package/dist/{chunk-SLFAEVKJ.js → chunk-WRU3IZOA.js} +3 -3
- package/dist/{context-CTBE00S_.d.cts → context-BFbHf9nC.d.cts} +1 -1
- package/dist/{context-lkLhbkFJ.d.ts → context-C4vBQbb4.d.ts} +1 -1
- package/dist/{effect-BpSNEJJz.d.cts → effect-DAzpH7Mm.d.cts} +33 -1
- package/dist/{effect-BpSNEJJz.d.ts → effect-DAzpH7Mm.d.ts} +33 -1
- package/dist/index.cjs +42 -42
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.dev.js +206 -46
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/internal.cjs +55 -41
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +17 -3
- package/dist/internal.js.map +1 -1
- package/dist/loader.cjs +9 -9
- package/dist/loader.js +1 -1
- package/dist/{props-XTHYD19o.d.cts → props-84UJeWO8.d.cts} +1 -1
- package/dist/{props-x-HbI-jX.d.ts → props-BRhFK50f.d.ts} +1 -1
- package/dist/{scope-CdbGmsFf.d.ts → scope-D3DpsfoG.d.ts} +1 -1
- package/dist/{scope-DfcP9I-A.d.cts → scope-DlCBL1Ft.d.cts} +1 -1
- package/package.json +1 -1
- package/src/advanced.ts +1 -1
- package/src/binding.ts +229 -101
- package/src/constants.ts +1 -1
- package/src/cycle-guard.ts +4 -3
- package/src/dom.ts +15 -4
- package/src/hooks.ts +1 -1
- package/src/internal.ts +7 -0
- package/src/lifecycle.ts +1 -1
- package/src/props.ts +60 -1
- package/src/signal.ts +60 -10
- package/src/store.ts +131 -18
- package/src/transition.ts +46 -9
- package/dist/chunk-D2IWOO4X.js.map +0 -1
- package/dist/chunk-LRFMCJY3.js.map +0 -1
- package/dist/chunk-Z6M3HKLG.cjs.map +0 -1
- package/dist/chunk-ZR435MDC.cjs.map +0 -1
- package/dist/jsx-dev-runtime.d.cts +0 -671
- package/dist/jsx-dev-runtime.d.ts +0 -671
- /package/dist/{chunk-4NUHM77Z.js.map → chunk-TLDT76RV.js.map} +0 -0
- /package/dist/{chunk-SLFAEVKJ.js.map → chunk-WRU3IZOA.js.map} +0 -0
package/src/binding.ts
CHANGED
|
@@ -44,7 +44,53 @@ import type { Cleanup, FictNode } from './types'
|
|
|
44
44
|
const isDev =
|
|
45
45
|
typeof __DEV__ !== 'undefined'
|
|
46
46
|
? __DEV__
|
|
47
|
-
: typeof process
|
|
47
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
48
|
+
|
|
49
|
+
const TEXT_CACHE = Symbol('fict:text')
|
|
50
|
+
const ATTR_CACHE = Symbol('fict:attr')
|
|
51
|
+
const PROP_CACHE = Symbol('fict:prop')
|
|
52
|
+
const STYLE_CACHE = Symbol('fict:style')
|
|
53
|
+
const CLASS_STATE_CACHE = Symbol('fict:class-state')
|
|
54
|
+
const CLASS_VALUE_CACHE = Symbol('fict:class-value')
|
|
55
|
+
const NON_REACTIVE_FN_MARKER = Symbol.for('fict:non-reactive-fn')
|
|
56
|
+
const REACTIVE_FN_MARKER = Symbol.for('fict:reactive-fn')
|
|
57
|
+
const NON_REACTIVE_FN_REGISTRY_KEY = Symbol.for('fict:non-reactive-fn-registry')
|
|
58
|
+
|
|
59
|
+
type NonReactiveRegistryHost = typeof globalThis & {
|
|
60
|
+
[NON_REACTIVE_FN_REGISTRY_KEY]?: WeakSet<(...args: unknown[]) => unknown>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PROPERTY_BINDING_KEYS = new Set([
|
|
64
|
+
'value',
|
|
65
|
+
'checked',
|
|
66
|
+
'selected',
|
|
67
|
+
'disabled',
|
|
68
|
+
'readOnly',
|
|
69
|
+
'multiple',
|
|
70
|
+
'muted',
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const STYLE_PROP_CACHE = new Map<string, string>()
|
|
74
|
+
const hasOwn = Object.prototype.hasOwnProperty
|
|
75
|
+
|
|
76
|
+
function getNonReactiveFnRegistry(): WeakSet<(...args: unknown[]) => unknown> {
|
|
77
|
+
const host = globalThis as NonReactiveRegistryHost
|
|
78
|
+
let registry = host[NON_REACTIVE_FN_REGISTRY_KEY]
|
|
79
|
+
if (!registry) {
|
|
80
|
+
registry = new WeakSet<(...args: unknown[]) => unknown>()
|
|
81
|
+
host[NON_REACTIVE_FN_REGISTRY_KEY] = registry
|
|
82
|
+
}
|
|
83
|
+
return registry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isExplicitReactiveFn(value: unknown): boolean {
|
|
87
|
+
if (typeof value !== 'function') return false
|
|
88
|
+
return (
|
|
89
|
+
(value as ((...args: unknown[]) => unknown) & { [REACTIVE_FN_MARKER]?: boolean })[
|
|
90
|
+
REACTIVE_FN_MARKER
|
|
91
|
+
] === true
|
|
92
|
+
)
|
|
93
|
+
}
|
|
48
94
|
|
|
49
95
|
// ============================================================================
|
|
50
96
|
// Type Definitions
|
|
@@ -99,9 +145,15 @@ export interface BindingHandle {
|
|
|
99
145
|
export function isReactive(value: unknown): value is () => unknown {
|
|
100
146
|
if (typeof value !== 'function') return false
|
|
101
147
|
|
|
148
|
+
// Explicit non-reactive marker always wins.
|
|
149
|
+
if (isNonReactiveFn(value)) return false
|
|
150
|
+
|
|
102
151
|
// Check for runtime-created signals/computed (most reliable)
|
|
103
152
|
if (isSignal(value) || isComputed(value)) return true
|
|
104
153
|
|
|
154
|
+
// Explicit marker for user-authored reactive getters.
|
|
155
|
+
if (isExplicitReactiveFn(value)) return true
|
|
156
|
+
|
|
105
157
|
// Exclude effect disposers and effect scopes - they are zero-arg
|
|
106
158
|
// functions but not reactive getters
|
|
107
159
|
if (isEffect(value) || isEffectScope(value)) return false
|
|
@@ -112,20 +164,22 @@ export function isReactive(value: unknown): value is () => unknown {
|
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
/**
|
|
115
|
-
*
|
|
116
|
-
* Used
|
|
117
|
-
*
|
|
167
|
+
* Stricter reactive check that only considers explicitly marked values.
|
|
168
|
+
* Used in DOM/event paths where regular callbacks must not be treated as
|
|
169
|
+
* reactive getters.
|
|
118
170
|
*
|
|
119
171
|
* Only returns true for:
|
|
120
172
|
* - Signal accessors (created by createSignal)
|
|
121
173
|
* - Computed accessors (created by createMemo)
|
|
122
174
|
* - Prop getters (marked by __fictProp)
|
|
175
|
+
* - Explicitly marked getters (reactive(...))
|
|
123
176
|
*/
|
|
124
|
-
function isStrictlyReactive(value: unknown): value is () => unknown {
|
|
177
|
+
export function isStrictlyReactive(value: unknown): value is () => unknown {
|
|
125
178
|
if (typeof value !== 'function') return false
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
179
|
+
// Do NOT use length === 0 as fallback - many callbacks have 0 params.
|
|
180
|
+
return (
|
|
181
|
+
isSignal(value) || isComputed(value) || isPropGetterFn(value) || isExplicitReactiveFn(value)
|
|
182
|
+
)
|
|
129
183
|
}
|
|
130
184
|
|
|
131
185
|
// Import-like check for prop getter marker without circular dependency
|
|
@@ -135,6 +189,48 @@ function isPropGetterFn(value: unknown): boolean {
|
|
|
135
189
|
return (value as any)[PROP_GETTER_MARKER] === true
|
|
136
190
|
}
|
|
137
191
|
|
|
192
|
+
function isNonReactiveFn(value: unknown): boolean {
|
|
193
|
+
if (typeof value !== 'function') return false
|
|
194
|
+
if (getNonReactiveFnRegistry().has(value as (...args: unknown[]) => unknown)) return true
|
|
195
|
+
return (
|
|
196
|
+
(value as ((...args: unknown[]) => unknown) & { [NON_REACTIVE_FN_MARKER]?: boolean })[
|
|
197
|
+
NON_REACTIVE_FN_MARKER
|
|
198
|
+
] === true
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Mark a function as non-reactive so runtime bindings won't treat it as a getter.
|
|
204
|
+
* Useful for callback props / function-as-child patterns that must remain callbacks.
|
|
205
|
+
*/
|
|
206
|
+
export function nonReactive<T extends (...args: unknown[]) => unknown>(fn: T): T {
|
|
207
|
+
getNonReactiveFnRegistry().add(fn as (...args: unknown[]) => unknown)
|
|
208
|
+
if (Object.isExtensible(fn)) {
|
|
209
|
+
try {
|
|
210
|
+
;(fn as T & { [NON_REACTIVE_FN_MARKER]?: boolean })[NON_REACTIVE_FN_MARKER] = true
|
|
211
|
+
} catch {
|
|
212
|
+
// Ignore marker failures on non-standard function objects.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return fn
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Mark a zero-arg function as an explicit reactive getter.
|
|
220
|
+
* Useful when authoring runtime code manually and you need function values
|
|
221
|
+
* to participate in reactive binding without relying on heuristics.
|
|
222
|
+
*/
|
|
223
|
+
export function reactive<T>(fn: () => T): () => T {
|
|
224
|
+
if (Object.isExtensible(fn)) {
|
|
225
|
+
try {
|
|
226
|
+
;(fn as (() => T) & { [REACTIVE_FN_MARKER]?: boolean })[REACTIVE_FN_MARKER] = true
|
|
227
|
+
} catch {
|
|
228
|
+
// Ignore marker failures on non-standard function objects.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return fn
|
|
232
|
+
}
|
|
233
|
+
|
|
138
234
|
/**
|
|
139
235
|
* Unwrap a potentially reactive value to get the actual value
|
|
140
236
|
*/
|
|
@@ -202,15 +298,11 @@ export function createTextBinding(value: MaybeReactive<unknown>): Text {
|
|
|
202
298
|
if (isReactive(value)) {
|
|
203
299
|
// Reactive: create effect to update text when value changes
|
|
204
300
|
createRenderEffect(() => {
|
|
205
|
-
|
|
206
|
-
const fmt = formatTextValue(v)
|
|
207
|
-
if (text.data !== fmt) {
|
|
208
|
-
text.data = fmt
|
|
209
|
-
}
|
|
301
|
+
setText(text, (value as () => unknown)())
|
|
210
302
|
})
|
|
211
303
|
} else {
|
|
212
304
|
// Static: set once
|
|
213
|
-
text
|
|
305
|
+
setText(text, value)
|
|
214
306
|
}
|
|
215
307
|
|
|
216
308
|
return text
|
|
@@ -221,12 +313,22 @@ export function createTextBinding(value: MaybeReactive<unknown>): Text {
|
|
|
221
313
|
* This is a convenience function for binding to existing DOM nodes.
|
|
222
314
|
*/
|
|
223
315
|
export function bindText(textNode: Text, getValue: () => unknown): Cleanup {
|
|
224
|
-
return createRenderEffect(() =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
316
|
+
return createRenderEffect(() => setText(textNode, getValue()))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Patch text node content with per-node value caching.
|
|
321
|
+
* This is the low-level primitive used by compiled render effects.
|
|
322
|
+
*/
|
|
323
|
+
export function setText(textNode: Text, value: unknown): void {
|
|
324
|
+
const next = formatTextValue(value)
|
|
325
|
+
const cache = textNode as unknown as Record<PropertyKey, unknown>
|
|
326
|
+
const prev = cache[TEXT_CACHE]
|
|
327
|
+
if (prev === next && textNode.data === next) return
|
|
328
|
+
cache[TEXT_CACHE] = next
|
|
329
|
+
if (textNode.data !== next) {
|
|
330
|
+
textNode.data = next
|
|
331
|
+
}
|
|
230
332
|
}
|
|
231
333
|
|
|
232
334
|
/**
|
|
@@ -279,52 +381,53 @@ export function createAttributeBinding(
|
|
|
279
381
|
* Bind a reactive value to an element's attribute.
|
|
280
382
|
*/
|
|
281
383
|
export function bindAttribute(el: Element, key: string, getValue: () => unknown): Cleanup {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const value = getValue()
|
|
285
|
-
if (value === prevValue) return
|
|
286
|
-
prevValue = value
|
|
384
|
+
return createRenderEffect(() => setAttr(el, key, getValue()))
|
|
385
|
+
}
|
|
287
386
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Patch attribute value with per-node, per-attribute cache.
|
|
389
|
+
*/
|
|
390
|
+
export function setAttr(el: Element, key: string, value: unknown): void {
|
|
391
|
+
const cacheTarget = el as unknown as Record<PropertyKey, unknown>
|
|
392
|
+
const attrCache =
|
|
393
|
+
(cacheTarget[ATTR_CACHE] as Record<string, unknown> | undefined) ??
|
|
394
|
+
(cacheTarget[ATTR_CACHE] = Object.create(null))
|
|
395
|
+
if (attrCache[key] === value) return
|
|
396
|
+
attrCache[key] = value
|
|
397
|
+
|
|
398
|
+
if (value === undefined || value === null || value === false) {
|
|
399
|
+
el.removeAttribute(key)
|
|
400
|
+
} else if (value === true) {
|
|
401
|
+
el.setAttribute(key, '')
|
|
402
|
+
} else {
|
|
403
|
+
el.setAttribute(key, String(value))
|
|
404
|
+
}
|
|
296
405
|
}
|
|
297
406
|
|
|
298
407
|
/**
|
|
299
408
|
* Bind a reactive value to an element's property.
|
|
300
409
|
*/
|
|
301
410
|
export function bindProperty(el: Element, key: string, getValue: () => unknown): Cleanup {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
])
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const fallback = key === 'checked' || key === 'selected' ? false : ''
|
|
323
|
-
;(el as unknown as Record<string, unknown>)[key] = fallback
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
;(el as unknown as Record<string, unknown>)[key] = next
|
|
327
|
-
})
|
|
411
|
+
return createRenderEffect(() => setProp(el, key, getValue()))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Patch DOM property with per-node, per-property cache.
|
|
416
|
+
*/
|
|
417
|
+
export function setProp(el: Element, key: string, value: unknown): void {
|
|
418
|
+
const cacheTarget = el as unknown as Record<PropertyKey, unknown>
|
|
419
|
+
const propCache =
|
|
420
|
+
(cacheTarget[PROP_CACHE] as Record<string, unknown> | undefined) ??
|
|
421
|
+
(cacheTarget[PROP_CACHE] = Object.create(null))
|
|
422
|
+
if (propCache[key] === value) return
|
|
423
|
+
propCache[key] = value
|
|
424
|
+
|
|
425
|
+
if (PROPERTY_BINDING_KEYS.has(key) && (value === undefined || value === null)) {
|
|
426
|
+
const fallback = key === 'checked' || key === 'selected' ? false : ''
|
|
427
|
+
;(el as unknown as Record<string, unknown>)[key] = fallback
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
;(el as unknown as Record<string, unknown>)[key] = value
|
|
328
431
|
}
|
|
329
432
|
|
|
330
433
|
// ============================================================================
|
|
@@ -338,16 +441,12 @@ export function createStyleBinding(
|
|
|
338
441
|
el: Element,
|
|
339
442
|
value: MaybeReactive<string | Record<string, string | number> | null | undefined>,
|
|
340
443
|
): void {
|
|
341
|
-
const target = el as Element & { style: CSSStyleDeclaration }
|
|
342
444
|
if (isReactive(value)) {
|
|
343
|
-
let prev: unknown
|
|
344
445
|
createRenderEffect(() => {
|
|
345
|
-
|
|
346
|
-
applyStyle(target, next, prev)
|
|
347
|
-
prev = next
|
|
446
|
+
setStyle(el, (value as () => unknown)() as string | Record<string, string | number> | null)
|
|
348
447
|
})
|
|
349
448
|
} else {
|
|
350
|
-
|
|
449
|
+
setStyle(el, value)
|
|
351
450
|
}
|
|
352
451
|
}
|
|
353
452
|
|
|
@@ -358,13 +457,23 @@ export function bindStyle(
|
|
|
358
457
|
el: Element,
|
|
359
458
|
getValue: () => string | Record<string, string | number> | null | undefined,
|
|
360
459
|
): Cleanup {
|
|
460
|
+
return createRenderEffect(() => setStyle(el, getValue()))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Patch style value with cached previous style payload.
|
|
465
|
+
*/
|
|
466
|
+
export function setStyle(
|
|
467
|
+
el: Element,
|
|
468
|
+
value: string | Record<string, string | number> | null | undefined,
|
|
469
|
+
): void {
|
|
361
470
|
const target = el as Element & { style: CSSStyleDeclaration }
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
471
|
+
const cache = target as unknown as Record<PropertyKey, unknown>
|
|
472
|
+
const prev = cache[STYLE_CACHE]
|
|
473
|
+
if (typeof value === 'string' && prev === value) return
|
|
474
|
+
if ((value === null || value === undefined) && (prev === null || prev === undefined)) return
|
|
475
|
+
applyStyle(target, value, prev)
|
|
476
|
+
cache[STYLE_CACHE] = value
|
|
368
477
|
}
|
|
369
478
|
|
|
370
479
|
/**
|
|
@@ -388,23 +497,25 @@ function applyStyle(
|
|
|
388
497
|
// Remove styles that were present in prev but not in current
|
|
389
498
|
if (prev && typeof prev === 'object') {
|
|
390
499
|
const prevStyles = prev as Record<string, string | number>
|
|
391
|
-
for (const key
|
|
392
|
-
if (!(key
|
|
393
|
-
|
|
500
|
+
for (const key in prevStyles) {
|
|
501
|
+
if (!hasOwn.call(prevStyles, key)) continue
|
|
502
|
+
if (!hasOwn.call(styles, key)) {
|
|
503
|
+
const cssProperty = normalizeStyleProperty(key)
|
|
394
504
|
el.style.removeProperty(cssProperty)
|
|
395
505
|
}
|
|
396
506
|
}
|
|
397
507
|
}
|
|
398
508
|
|
|
399
|
-
for (const
|
|
509
|
+
for (const prop in styles) {
|
|
510
|
+
if (!hasOwn.call(styles, prop)) continue
|
|
511
|
+
const v = styles[prop]
|
|
400
512
|
if (v != null) {
|
|
401
|
-
|
|
402
|
-
const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
513
|
+
const cssProperty = normalizeStyleProperty(prop)
|
|
403
514
|
const unitless = isUnitlessStyleProperty(prop) || isUnitlessStyleProperty(cssProperty)
|
|
404
515
|
const valueStr = typeof v === 'number' && !unitless ? `${v}px` : String(v)
|
|
405
516
|
el.style.setProperty(cssProperty, valueStr)
|
|
406
517
|
} else {
|
|
407
|
-
const cssProperty = prop
|
|
518
|
+
const cssProperty = normalizeStyleProperty(prop)
|
|
408
519
|
el.style.removeProperty(cssProperty) // Handle null/undefined values by removing
|
|
409
520
|
}
|
|
410
521
|
}
|
|
@@ -414,8 +525,9 @@ function applyStyle(
|
|
|
414
525
|
// Ideally we remove keys from prev.
|
|
415
526
|
if (prev && typeof prev === 'object') {
|
|
416
527
|
const prevStyles = prev as Record<string, string | number>
|
|
417
|
-
for (const key
|
|
418
|
-
|
|
528
|
+
for (const key in prevStyles) {
|
|
529
|
+
if (!hasOwn.call(prevStyles, key)) continue
|
|
530
|
+
const cssProperty = normalizeStyleProperty(key)
|
|
419
531
|
el.style.removeProperty(cssProperty)
|
|
420
532
|
}
|
|
421
533
|
} else if (typeof prev === 'string') {
|
|
@@ -428,6 +540,14 @@ const isUnitlessStyleProperty = isDev
|
|
|
428
540
|
? (prop: string): boolean => UnitlessStyles.has(prop)
|
|
429
541
|
: (prop: string): boolean => prop === 'opacity' || prop === 'zIndex'
|
|
430
542
|
|
|
543
|
+
function normalizeStyleProperty(prop: string): string {
|
|
544
|
+
const cached = STYLE_PROP_CACHE.get(prop)
|
|
545
|
+
if (cached) return cached
|
|
546
|
+
const normalized = prop.includes('-') ? prop : prop.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
547
|
+
STYLE_PROP_CACHE.set(prop, normalized)
|
|
548
|
+
return normalized
|
|
549
|
+
}
|
|
550
|
+
|
|
431
551
|
// ============================================================================
|
|
432
552
|
// Class Binding
|
|
433
553
|
// ============================================================================
|
|
@@ -440,13 +560,11 @@ export function createClassBinding(
|
|
|
440
560
|
value: MaybeReactive<string | Record<string, boolean> | null | undefined>,
|
|
441
561
|
): void {
|
|
442
562
|
if (isReactive(value)) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
prev = applyClass(el, next, prev)
|
|
447
|
-
})
|
|
563
|
+
createRenderEffect(() =>
|
|
564
|
+
setClass(el, (value as () => string | Record<string, boolean> | null | undefined)()),
|
|
565
|
+
)
|
|
448
566
|
} else {
|
|
449
|
-
|
|
567
|
+
setClass(el, value)
|
|
450
568
|
}
|
|
451
569
|
}
|
|
452
570
|
|
|
@@ -457,21 +575,31 @@ export function bindClass(
|
|
|
457
575
|
el: Element,
|
|
458
576
|
getValue: () => string | Record<string, boolean> | null | undefined,
|
|
459
577
|
): Cleanup {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
578
|
+
return createRenderEffect(() => setClass(el, getValue()))
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Patch class value using per-node cached class state.
|
|
583
|
+
*/
|
|
584
|
+
export function setClass(
|
|
585
|
+
el: Element,
|
|
586
|
+
value: string | Record<string, boolean> | null | undefined,
|
|
587
|
+
): void {
|
|
588
|
+
const cache = el as unknown as Record<PropertyKey, unknown>
|
|
589
|
+
const prevValue = cache[CLASS_VALUE_CACHE]
|
|
590
|
+
const prevState = (cache[CLASS_STATE_CACHE] as Record<string, boolean> | undefined) ?? {}
|
|
591
|
+
|
|
592
|
+
// Preserve existing behavior: short-circuit only for stable string values.
|
|
593
|
+
if (typeof value === 'string') {
|
|
594
|
+
if (typeof prevValue === 'string' && prevValue === value) return
|
|
595
|
+
el.className = value
|
|
596
|
+
cache[CLASS_STATE_CACHE] = {}
|
|
597
|
+
cache[CLASS_VALUE_CACHE] = value
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
cache[CLASS_STATE_CACHE] = applyClass(el, value, prevState)
|
|
602
|
+
cache[CLASS_VALUE_CACHE] = value
|
|
475
603
|
}
|
|
476
604
|
|
|
477
605
|
/**
|
package/src/constants.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { DelegatedEventNames } from './delegated-events'
|
|
|
15
15
|
const isDev =
|
|
16
16
|
typeof __DEV__ !== 'undefined'
|
|
17
17
|
? __DEV__
|
|
18
|
-
: typeof process
|
|
18
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
19
19
|
|
|
20
20
|
// ============================================================================
|
|
21
21
|
// Boolean Attributes
|
package/src/cycle-guard.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { getDevtoolsHook } from './devtools'
|
|
|
3
3
|
const isDev =
|
|
4
4
|
typeof __DEV__ !== 'undefined'
|
|
5
5
|
? __DEV__
|
|
6
|
-
: typeof process
|
|
6
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
7
7
|
|
|
8
8
|
export interface CycleProtectionOptions {
|
|
9
|
-
/** Enable cycle protection guards (
|
|
9
|
+
/** Enable cycle protection guards (enabled by default in all modes) */
|
|
10
10
|
enabled?: boolean
|
|
11
11
|
maxFlushCyclesPerMicrotask?: number
|
|
12
12
|
maxEffectRunsPerFlush?: number
|
|
@@ -35,7 +35,8 @@ let enterRootGuard: (root: object) => boolean = () => true
|
|
|
35
35
|
let exitRootGuard: (root: object) => void = () => {}
|
|
36
36
|
|
|
37
37
|
const defaultOptions = {
|
|
38
|
-
|
|
38
|
+
// Keep cycle guards on in production to avoid infinite flush loops.
|
|
39
|
+
enabled: true,
|
|
39
40
|
maxFlushCyclesPerMicrotask: 10_000,
|
|
40
41
|
maxEffectRunsPerFlush: 20_000,
|
|
41
42
|
windowSize: 5,
|
package/src/dom.ts
CHANGED
|
@@ -60,7 +60,7 @@ const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
|
|
|
60
60
|
const isDev =
|
|
61
61
|
typeof __DEV__ !== 'undefined'
|
|
62
62
|
? __DEV__
|
|
63
|
-
: typeof process
|
|
63
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
64
64
|
|
|
65
65
|
let nextComponentId = 1
|
|
66
66
|
|
|
@@ -201,6 +201,12 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
201
201
|
return createElementWithContext(resolved, namespace)
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Non-reactive function values are not valid DOM nodes.
|
|
205
|
+
// Keep callback values inert instead of stringifying function source.
|
|
206
|
+
if (typeof node === 'function') {
|
|
207
|
+
return document.createTextNode('')
|
|
208
|
+
}
|
|
209
|
+
|
|
204
210
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
205
211
|
// Handle BindingHandle (list/conditional bindings, etc)
|
|
206
212
|
if ('marker' in node) {
|
|
@@ -507,10 +513,15 @@ function appendChildNode(
|
|
|
507
513
|
return
|
|
508
514
|
}
|
|
509
515
|
|
|
510
|
-
// Handle
|
|
511
|
-
|
|
516
|
+
// Handle function children:
|
|
517
|
+
// - reactive accessors (signals/computed/getters) become child bindings
|
|
518
|
+
// - non-reactive callbacks stay inert
|
|
519
|
+
if (typeof child === 'function') {
|
|
512
520
|
const childGetter = child as () => FictNode
|
|
513
|
-
|
|
521
|
+
if (isReactive(childGetter)) {
|
|
522
|
+
createChildBinding(parent, childGetter, node => createElementWithContext(node, namespace))
|
|
523
|
+
return
|
|
524
|
+
}
|
|
514
525
|
return
|
|
515
526
|
}
|
|
516
527
|
|
package/src/hooks.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
const isDev =
|
|
12
12
|
typeof __DEV__ !== 'undefined'
|
|
13
13
|
? __DEV__
|
|
14
|
-
: typeof process
|
|
14
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
15
15
|
|
|
16
16
|
export interface HookContext {
|
|
17
17
|
slots: unknown[]
|
package/src/internal.ts
CHANGED
|
@@ -82,10 +82,17 @@ export {
|
|
|
82
82
|
bindAttribute,
|
|
83
83
|
bindStyle,
|
|
84
84
|
bindClass,
|
|
85
|
+
setText,
|
|
86
|
+
setAttr,
|
|
87
|
+
setProp,
|
|
88
|
+
setStyle,
|
|
89
|
+
setClass,
|
|
85
90
|
bindEvent,
|
|
86
91
|
callEventHandler,
|
|
87
92
|
bindProperty,
|
|
88
93
|
bindRef,
|
|
94
|
+
nonReactive,
|
|
95
|
+
reactive,
|
|
89
96
|
insert,
|
|
90
97
|
insertBetween,
|
|
91
98
|
createConditional,
|
package/src/lifecycle.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Cleanup, ErrorInfo, SuspenseToken } from './types'
|
|
|
4
4
|
const isDev =
|
|
5
5
|
typeof __DEV__ !== 'undefined'
|
|
6
6
|
? __DEV__
|
|
7
|
-
: typeof process
|
|
7
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
8
8
|
|
|
9
9
|
type LifecycleFn = () => void | Cleanup
|
|
10
10
|
|
package/src/props.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { createMemo } from './memo'
|
|
2
|
+
import { isComputed, isEffect, isEffectScope, isSignal } from './signal'
|
|
2
3
|
|
|
3
4
|
const PROP_GETTER_MARKER = Symbol.for('fict:prop-getter')
|
|
5
|
+
const NON_REACTIVE_FN_MARKER = Symbol.for('fict:non-reactive-fn')
|
|
6
|
+
const REACTIVE_FN_MARKER = Symbol.for('fict:reactive-fn')
|
|
7
|
+
const NON_REACTIVE_FN_REGISTRY_KEY = Symbol.for('fict:non-reactive-fn-registry')
|
|
4
8
|
const propGetters = new WeakSet<(...args: unknown[]) => unknown>()
|
|
5
9
|
const rawToProxy = new WeakMap<object, object>()
|
|
6
10
|
const proxyToRaw = new WeakMap<object, object>()
|
|
7
11
|
|
|
12
|
+
type NonReactiveRegistryHost = typeof globalThis & {
|
|
13
|
+
[NON_REACTIVE_FN_REGISTRY_KEY]?: WeakSet<(...args: unknown[]) => unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getNonReactiveFnRegistry(): WeakSet<(...args: unknown[]) => unknown> {
|
|
17
|
+
const host = globalThis as NonReactiveRegistryHost
|
|
18
|
+
let registry = host[NON_REACTIVE_FN_REGISTRY_KEY]
|
|
19
|
+
if (!registry) {
|
|
20
|
+
registry = new WeakSet<(...args: unknown[]) => unknown>()
|
|
21
|
+
host[NON_REACTIVE_FN_REGISTRY_KEY] = registry
|
|
22
|
+
}
|
|
23
|
+
return registry
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
/**
|
|
9
27
|
* @internal
|
|
10
28
|
* Marks a zero-arg getter so props proxy can lazily evaluate it.
|
|
@@ -30,6 +48,47 @@ function isPropGetter(value: unknown): value is () => unknown {
|
|
|
30
48
|
return propGetters.has(fn as (...args: unknown[]) => unknown) || fn[PROP_GETTER_MARKER] === true
|
|
31
49
|
}
|
|
32
50
|
|
|
51
|
+
function isNonReactiveFn(value: unknown): boolean {
|
|
52
|
+
if (typeof value !== 'function') return false
|
|
53
|
+
if (getNonReactiveFnRegistry().has(value as (...args: unknown[]) => unknown)) return true
|
|
54
|
+
return (
|
|
55
|
+
(value as ((...args: unknown[]) => unknown) & { [NON_REACTIVE_FN_MARKER]?: boolean })[
|
|
56
|
+
NON_REACTIVE_FN_MARKER
|
|
57
|
+
] === true
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markNonReactiveFn<T extends (...args: unknown[]) => unknown>(fn: T): T {
|
|
62
|
+
getNonReactiveFnRegistry().add(fn as (...args: unknown[]) => unknown)
|
|
63
|
+
if (Object.isExtensible(fn)) {
|
|
64
|
+
try {
|
|
65
|
+
;(fn as T & { [NON_REACTIVE_FN_MARKER]?: boolean })[NON_REACTIVE_FN_MARKER] = true
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignore marker failures on non-standard function objects.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return fn
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isExplicitReactiveFn(value: unknown): boolean {
|
|
74
|
+
if (typeof value !== 'function') return false
|
|
75
|
+
return (
|
|
76
|
+
(value as ((...args: unknown[]) => unknown) & { [REACTIVE_FN_MARKER]?: boolean })[
|
|
77
|
+
REACTIVE_FN_MARKER
|
|
78
|
+
] === true
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePropsFunction(value: unknown): unknown {
|
|
83
|
+
if (typeof value !== 'function') return value
|
|
84
|
+
if (value.length !== 0) return value
|
|
85
|
+
if (isPropGetter(value) || isSignal(value) || isComputed(value) || isExplicitReactiveFn(value)) {
|
|
86
|
+
return value
|
|
87
|
+
}
|
|
88
|
+
if (isEffect(value) || isEffectScope(value) || isNonReactiveFn(value)) return value
|
|
89
|
+
return markNonReactiveFn(value as (...args: unknown[]) => unknown)
|
|
90
|
+
}
|
|
91
|
+
|
|
33
92
|
export function createPropsProxy<T extends Record<string, unknown>>(props: T): T {
|
|
34
93
|
if (!props || typeof props !== 'object') {
|
|
35
94
|
return props
|
|
@@ -50,7 +109,7 @@ export function createPropsProxy<T extends Record<string, unknown>>(props: T): T
|
|
|
50
109
|
if (isPropGetter(value)) {
|
|
51
110
|
return value()
|
|
52
111
|
}
|
|
53
|
-
return value
|
|
112
|
+
return normalizePropsFunction(value)
|
|
54
113
|
},
|
|
55
114
|
set(target, prop, value, receiver) {
|
|
56
115
|
return Reflect.set(target, prop, value, receiver)
|