@fictjs/runtime 0.0.12 → 0.0.14

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.
Files changed (65) hide show
  1. package/dist/advanced.cjs +79 -0
  2. package/dist/advanced.cjs.map +1 -0
  3. package/dist/advanced.d.cts +50 -0
  4. package/dist/advanced.d.ts +50 -0
  5. package/dist/advanced.js +79 -0
  6. package/dist/advanced.js.map +1 -0
  7. package/dist/chunk-624QY53A.cjs +45 -0
  8. package/dist/chunk-624QY53A.cjs.map +1 -0
  9. package/dist/chunk-F3AIYQB7.js +45 -0
  10. package/dist/chunk-F3AIYQB7.js.map +1 -0
  11. package/dist/chunk-GJTYOFMO.cjs +109 -0
  12. package/dist/chunk-GJTYOFMO.cjs.map +1 -0
  13. package/dist/chunk-IUZXKAAY.js +109 -0
  14. package/dist/chunk-IUZXKAAY.js.map +1 -0
  15. package/dist/{slim.cjs → chunk-PMF6MWEV.cjs} +2557 -3110
  16. package/dist/chunk-PMF6MWEV.cjs.map +1 -0
  17. package/dist/{slim.js → chunk-RY4WDS6R.js} +2596 -3097
  18. package/dist/chunk-RY4WDS6R.js.map +1 -0
  19. package/dist/context-B7UYnfzM.d.ts +153 -0
  20. package/dist/context-UXySaqI_.d.cts +153 -0
  21. package/dist/effect-Auji1rz9.d.cts +350 -0
  22. package/dist/effect-Auji1rz9.d.ts +350 -0
  23. package/dist/index.cjs +108 -4441
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +5 -1492
  26. package/dist/index.d.ts +5 -1492
  27. package/dist/index.dev.js +1020 -2788
  28. package/dist/index.dev.js.map +1 -1
  29. package/dist/index.js +63 -4301
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal.cjs +901 -0
  32. package/dist/internal.cjs.map +1 -0
  33. package/dist/internal.d.cts +158 -0
  34. package/dist/internal.d.ts +158 -0
  35. package/dist/internal.js +901 -0
  36. package/dist/internal.js.map +1 -0
  37. package/dist/{jsx-dev-runtime.d.ts → props-CrOMYbLv.d.cts} +107 -18
  38. package/dist/{jsx-dev-runtime.d.cts → props-ES0Ag_Wd.d.ts} +107 -18
  39. package/dist/scope-DKYzWfTn.d.cts +55 -0
  40. package/dist/scope-S6eAzBJZ.d.ts +55 -0
  41. package/package.json +10 -5
  42. package/src/advanced.ts +101 -0
  43. package/src/binding.ts +25 -422
  44. package/src/constants.ts +345 -344
  45. package/src/context.ts +300 -0
  46. package/src/cycle-guard.ts +124 -97
  47. package/src/delegated-events.ts +24 -0
  48. package/src/dom.ts +19 -25
  49. package/src/effect.ts +4 -0
  50. package/src/hooks.ts +9 -1
  51. package/src/index.ts +41 -130
  52. package/src/internal.ts +130 -0
  53. package/src/lifecycle.ts +13 -2
  54. package/src/list-helpers.ts +6 -65
  55. package/src/props.ts +48 -46
  56. package/src/signal.ts +59 -39
  57. package/src/store.ts +47 -7
  58. package/src/versioned-signal.ts +3 -3
  59. package/dist/jsx-runtime.d.cts +0 -671
  60. package/dist/jsx-runtime.d.ts +0 -671
  61. package/dist/slim.cjs.map +0 -1
  62. package/dist/slim.d.cts +0 -504
  63. package/dist/slim.d.ts +0 -504
  64. package/dist/slim.js.map +0 -1
  65. package/src/slim.ts +0 -69
package/src/context.ts ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * @fileoverview Context API for Fict
3
+ *
4
+ * Provides a way to pass data through the component tree without having to pass
5
+ * props down manually at every level. Context is designed for:
6
+ *
7
+ * - SSR isolation (different request = different context values)
8
+ * - Multi-instance support (multiple app roots with different values)
9
+ * - Subtree scoping (override values in specific parts of the tree)
10
+ *
11
+ * ## Design Principles
12
+ *
13
+ * 1. **Reuses existing RootContext hierarchy** - Uses parent chain for value lookup,
14
+ * consistent with handleError/handleSuspend mechanisms.
15
+ *
16
+ * 2. **Zero extra root creation overhead** - Provider doesn't create new root,
17
+ * only mounts value on current root.
18
+ *
19
+ * 3. **Auto-aligned with insert/suspense boundaries** - Because they create child
20
+ * roots that inherit parent, context values propagate correctly.
21
+ *
22
+ * ## Usage
23
+ *
24
+ * ```tsx
25
+ * // Create context with default value
26
+ * const ThemeContext = createContext<'light' | 'dark'>('light')
27
+ *
28
+ * // Provide value to subtree
29
+ * function App() {
30
+ * return (
31
+ * <ThemeContext.Provider value="dark">
32
+ * <ThemedComponent />
33
+ * </ThemeContext.Provider>
34
+ * )
35
+ * }
36
+ *
37
+ * // Consume value
38
+ * function ThemedComponent() {
39
+ * const theme = useContext(ThemeContext)
40
+ * return <div class={theme}>...</div>
41
+ * }
42
+ * ```
43
+ *
44
+ * @module
45
+ */
46
+
47
+ import { createElement } from './dom'
48
+ import {
49
+ createRootContext,
50
+ destroyRoot,
51
+ flushOnMount,
52
+ getCurrentRoot,
53
+ popRoot,
54
+ pushRoot,
55
+ type RootContext,
56
+ } from './lifecycle'
57
+ import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
58
+ import { createRenderEffect } from './effect'
59
+ import type { BaseProps, FictNode } from './types'
60
+
61
+ // ============================================================================
62
+ // Types
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Context object created by createContext.
67
+ * Contains the Provider component and serves as a key for context lookup.
68
+ */
69
+ export interface Context<T> {
70
+ /** Unique identifier for this context */
71
+ readonly id: symbol
72
+ /** Default value when no provider is found */
73
+ readonly defaultValue: T
74
+ /** Provider component for supplying context values */
75
+ Provider: ContextProvider<T>
76
+ /** Display name for debugging */
77
+ displayName?: string
78
+ }
79
+
80
+ /**
81
+ * Props for the Context Provider component
82
+ */
83
+ export interface ProviderProps<T> extends BaseProps {
84
+ /** The value to provide to the subtree */
85
+ value: T
86
+ }
87
+
88
+ /**
89
+ * Provider component type
90
+ */
91
+ export type ContextProvider<T> = (props: ProviderProps<T>) => FictNode
92
+
93
+ // ============================================================================
94
+ // Internal Context Storage
95
+ // ============================================================================
96
+
97
+ /**
98
+ * WeakMap to store context values per RootContext.
99
+ * Using WeakMap ensures proper garbage collection when roots are destroyed.
100
+ */
101
+ const contextStorage = new WeakMap<RootContext, Map<symbol, unknown>>()
102
+
103
+ /**
104
+ * Get the context map for a root, creating it if needed
105
+ */
106
+ function getContextMap(root: RootContext): Map<symbol, unknown> {
107
+ let map = contextStorage.get(root)
108
+ if (!map) {
109
+ map = new Map()
110
+ contextStorage.set(root, map)
111
+ }
112
+ return map
113
+ }
114
+
115
+ // ============================================================================
116
+ // Context API
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Creates a new context with the given default value.
121
+ *
122
+ * Context provides a way to pass values through the component tree without
123
+ * explicit props drilling. It's especially useful for:
124
+ *
125
+ * - Theme data
126
+ * - Locale/i18n settings
127
+ * - Authentication state
128
+ * - Feature flags
129
+ * - Any data that many components at different nesting levels need
130
+ *
131
+ * @param defaultValue - The value to use when no Provider is found above in the tree
132
+ * @returns A context object with a Provider component
133
+ *
134
+ * @example
135
+ * ```tsx
136
+ * // Create a theme context
137
+ * const ThemeContext = createContext<'light' | 'dark'>('light')
138
+ *
139
+ * // Use the provider
140
+ * function App() {
141
+ * return (
142
+ * <ThemeContext.Provider value="dark">
143
+ * <Content />
144
+ * </ThemeContext.Provider>
145
+ * )
146
+ * }
147
+ *
148
+ * // Consume the context
149
+ * function Content() {
150
+ * const theme = useContext(ThemeContext)
151
+ * return <div class={`theme-${theme}`}>Hello</div>
152
+ * }
153
+ * ```
154
+ */
155
+ export function createContext<T>(defaultValue: T): Context<T> {
156
+ const id = Symbol('fict.context')
157
+
158
+ const context: Context<T> = {
159
+ id,
160
+ defaultValue,
161
+ Provider: null as unknown as ContextProvider<T>,
162
+ }
163
+
164
+ // Create the Provider component
165
+ context.Provider = function Provider(props: ProviderProps<T>): FictNode {
166
+ const hostRoot = getCurrentRoot()
167
+
168
+ // Create a child root for the provider's subtree
169
+ // This establishes the provider boundary - children will look up from here
170
+ const providerRoot = createRootContext(hostRoot)
171
+
172
+ // Store the context value on this root
173
+ const contextMap = getContextMap(providerRoot)
174
+ contextMap.set(id, props.value)
175
+
176
+ // Create DOM structure
177
+ const fragment = document.createDocumentFragment()
178
+ const marker = document.createComment('fict:ctx')
179
+ fragment.appendChild(marker)
180
+
181
+ let cleanup: (() => void) | undefined
182
+ let activeNodes: Node[] = []
183
+
184
+ const renderChildren = (children: FictNode) => {
185
+ // Cleanup previous render
186
+ if (cleanup) {
187
+ cleanup()
188
+ cleanup = undefined
189
+ }
190
+ if (activeNodes.length) {
191
+ removeNodes(activeNodes)
192
+ activeNodes = []
193
+ }
194
+
195
+ if (children == null || children === false) {
196
+ return
197
+ }
198
+
199
+ const prev = pushRoot(providerRoot)
200
+ let nodes: Node[] = []
201
+ try {
202
+ const output = createElement(children)
203
+ nodes = toNodeArray(output)
204
+ const parentNode = marker.parentNode as (ParentNode & Node) | null
205
+ if (parentNode) {
206
+ insertNodesBefore(parentNode, nodes, marker)
207
+ }
208
+ } finally {
209
+ popRoot(prev)
210
+ flushOnMount(providerRoot)
211
+ }
212
+
213
+ cleanup = () => {
214
+ destroyRoot(providerRoot)
215
+ removeNodes(nodes)
216
+ }
217
+ activeNodes = nodes
218
+ }
219
+
220
+ // Initial render
221
+ createRenderEffect(() => {
222
+ // Update context value on re-render (if value prop changes reactively)
223
+ contextMap.set(id, props.value)
224
+ renderChildren(props.children)
225
+ })
226
+
227
+ return fragment
228
+ }
229
+
230
+ return context
231
+ }
232
+
233
+ /**
234
+ * Reads the current value of a context.
235
+ *
236
+ * useContext looks up through the RootContext parent chain to find the
237
+ * nearest Provider for this context. If no Provider is found, returns
238
+ * the context's default value.
239
+ *
240
+ * @param context - The context object created by createContext
241
+ * @returns The current context value
242
+ *
243
+ * @example
244
+ * ```tsx
245
+ * const ThemeContext = createContext('light')
246
+ *
247
+ * function ThemedButton() {
248
+ * const theme = useContext(ThemeContext)
249
+ * return <button class={theme === 'dark' ? 'btn-dark' : 'btn-light'}>Click</button>
250
+ * }
251
+ * ```
252
+ */
253
+ export function useContext<T>(context: Context<T>): T {
254
+ let root = getCurrentRoot()
255
+
256
+ // Walk up the parent chain looking for the context value
257
+ while (root) {
258
+ const contextMap = contextStorage.get(root)
259
+ if (contextMap && contextMap.has(context.id)) {
260
+ return contextMap.get(context.id) as T
261
+ }
262
+ root = root.parent
263
+ }
264
+
265
+ // No provider found, return default value
266
+ return context.defaultValue
267
+ }
268
+
269
+ /**
270
+ * Checks if a context value is currently provided in the tree.
271
+ *
272
+ * Useful for conditional behavior when a provider may or may not exist.
273
+ *
274
+ * @param context - The context object to check
275
+ * @returns true if a Provider exists above in the tree
276
+ *
277
+ * @example
278
+ * ```tsx
279
+ * function OptionalTheme() {
280
+ * if (hasContext(ThemeContext)) {
281
+ * const theme = useContext(ThemeContext)
282
+ * return <div class={theme}>Themed content</div>
283
+ * }
284
+ * return <div>Default content</div>
285
+ * }
286
+ * ```
287
+ */
288
+ export function hasContext<T>(context: Context<T>): boolean {
289
+ let root = getCurrentRoot()
290
+
291
+ while (root) {
292
+ const contextMap = contextStorage.get(root)
293
+ if (contextMap && contextMap.has(context.id)) {
294
+ return true
295
+ }
296
+ root = root.parent
297
+ }
298
+
299
+ return false
300
+ }
@@ -1,5 +1,10 @@
1
1
  import { getDevtoolsHook } from './devtools'
2
2
 
3
+ const isDev =
4
+ typeof __DEV__ !== 'undefined'
5
+ ? __DEV__
6
+ : typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
7
+
3
8
  export interface CycleProtectionOptions {
4
9
  maxFlushCyclesPerMicrotask?: number
5
10
  maxEffectRunsPerFlush?: number
@@ -15,120 +20,142 @@ interface CycleWindowEntry {
15
20
  budget: number
16
21
  }
17
22
 
18
- const defaultOptions = {
19
- maxFlushCyclesPerMicrotask: 10_000,
20
- maxEffectRunsPerFlush: 20_000,
21
- windowSize: 5,
22
- highUsageRatio: 0.8,
23
- maxRootReentrantDepth: 10,
24
- enableWindowWarning: true,
25
- devMode: false,
26
- }
23
+ let setCycleProtectionOptions: (opts: CycleProtectionOptions) => void = () => {}
24
+ let resetCycleProtectionStateForTests: () => void = () => {}
25
+ let beginFlushGuard: () => void = () => {}
26
+ let beforeEffectRunGuard: () => boolean = () => true
27
+ let endFlushGuard: () => void = () => {}
28
+ let enterRootGuard: (root: object) => boolean = () => true
29
+ let exitRootGuard: (root: object) => void = () => {}
27
30
 
28
- let options: Required<CycleProtectionOptions> = {
29
- ...defaultOptions,
30
- } as Required<CycleProtectionOptions>
31
+ if (isDev) {
32
+ const defaultOptions = {
33
+ maxFlushCyclesPerMicrotask: 10_000,
34
+ maxEffectRunsPerFlush: 20_000,
35
+ windowSize: 5,
36
+ highUsageRatio: 0.8,
37
+ maxRootReentrantDepth: 10,
38
+ enableWindowWarning: true,
39
+ devMode: false,
40
+ }
31
41
 
32
- let effectRunsThisFlush = 0
33
- let windowUsage: CycleWindowEntry[] = []
34
- let rootDepth = new WeakMap<object, number>()
35
- let flushWarned = false
36
- let rootWarned = false
37
- let windowWarned = false
42
+ let options: Required<CycleProtectionOptions> = {
43
+ ...defaultOptions,
44
+ } as Required<CycleProtectionOptions>
38
45
 
39
- export function setCycleProtectionOptions(opts: CycleProtectionOptions): void {
40
- options = { ...options, ...opts }
41
- }
46
+ let effectRunsThisFlush = 0
47
+ let windowUsage: CycleWindowEntry[] = []
48
+ let rootDepth = new WeakMap<object, number>()
49
+ let flushWarned = false
50
+ let rootWarned = false
51
+ let windowWarned = false
42
52
 
43
- export function resetCycleProtectionStateForTests(): void {
44
- options = { ...defaultOptions } as Required<CycleProtectionOptions>
45
- effectRunsThisFlush = 0
46
- windowUsage = []
47
- rootDepth = new WeakMap<object, number>()
48
- flushWarned = false
49
- rootWarned = false
50
- windowWarned = false
51
- }
53
+ setCycleProtectionOptions = opts => {
54
+ options = { ...options, ...opts }
55
+ }
52
56
 
53
- export function beginFlushGuard(): void {
54
- effectRunsThisFlush = 0
55
- flushWarned = false
56
- windowWarned = false
57
- }
57
+ resetCycleProtectionStateForTests = () => {
58
+ options = { ...defaultOptions } as Required<CycleProtectionOptions>
59
+ effectRunsThisFlush = 0
60
+ windowUsage = []
61
+ rootDepth = new WeakMap<object, number>()
62
+ flushWarned = false
63
+ rootWarned = false
64
+ windowWarned = false
65
+ }
58
66
 
59
- export function beforeEffectRunGuard(): boolean {
60
- const next = ++effectRunsThisFlush
61
- if (next > options.maxFlushCyclesPerMicrotask || next > options.maxEffectRunsPerFlush) {
62
- const message = `[fict] cycle protection triggered: flush-budget-exceeded`
63
- if (options.devMode) {
64
- throw new Error(message)
65
- }
66
- if (!flushWarned) {
67
- flushWarned = true
68
- console.warn(message, { effectRuns: next })
67
+ beginFlushGuard = () => {
68
+ effectRunsThisFlush = 0
69
+ flushWarned = false
70
+ windowWarned = false
71
+ }
72
+
73
+ beforeEffectRunGuard = () => {
74
+ const next = ++effectRunsThisFlush
75
+ if (next > options.maxFlushCyclesPerMicrotask || next > options.maxEffectRunsPerFlush) {
76
+ const message = `[fict] cycle protection triggered: flush-budget-exceeded`
77
+ if (options.devMode) {
78
+ throw new Error(message)
79
+ }
80
+ if (!flushWarned) {
81
+ flushWarned = true
82
+ console.warn(message, { effectRuns: next })
83
+ }
84
+ return false
69
85
  }
70
- return false
86
+ return true
71
87
  }
72
- return true
73
- }
74
88
 
75
- export function endFlushGuard(): void {
76
- recordWindowUsage(effectRunsThisFlush, options.maxFlushCyclesPerMicrotask)
77
- effectRunsThisFlush = 0
78
- }
89
+ endFlushGuard = () => {
90
+ recordWindowUsage(effectRunsThisFlush, options.maxFlushCyclesPerMicrotask)
91
+ effectRunsThisFlush = 0
92
+ }
79
93
 
80
- export function enterRootGuard(root: object): boolean {
81
- const depth = (rootDepth.get(root) ?? 0) + 1
82
- if (depth > options.maxRootReentrantDepth) {
83
- const message = `[fict] cycle protection triggered: root-reentry`
84
- if (options.devMode) {
85
- throw new Error(message)
94
+ enterRootGuard = root => {
95
+ const depth = (rootDepth.get(root) ?? 0) + 1
96
+ if (depth > options.maxRootReentrantDepth) {
97
+ const message = `[fict] cycle protection triggered: root-reentry`
98
+ if (options.devMode) {
99
+ throw new Error(message)
100
+ }
101
+ if (!rootWarned) {
102
+ rootWarned = true
103
+ console.warn(message, { depth })
104
+ }
105
+ return false
86
106
  }
87
- if (!rootWarned) {
88
- rootWarned = true
89
- console.warn(message, { depth })
90
- }
91
- return false
107
+ rootDepth.set(root, depth)
108
+ return true
92
109
  }
93
- rootDepth.set(root, depth)
94
- return true
95
- }
96
110
 
97
- export function exitRootGuard(root: object): void {
98
- const depth = rootDepth.get(root)
99
- if (depth === undefined) return
100
- if (depth <= 1) {
101
- rootDepth.delete(root)
102
- } else {
103
- rootDepth.set(root, depth - 1)
111
+ exitRootGuard = root => {
112
+ const depth = rootDepth.get(root)
113
+ if (depth === undefined) return
114
+ if (depth <= 1) {
115
+ rootDepth.delete(root)
116
+ } else {
117
+ rootDepth.set(root, depth - 1)
118
+ }
104
119
  }
105
- }
106
120
 
107
- function recordWindowUsage(used: number, budget: number): void {
108
- if (!options.enableWindowWarning) return
109
- const entry = { used, budget }
110
- windowUsage.push(entry)
111
- if (windowUsage.length > options.windowSize) {
112
- windowUsage.shift()
121
+ const recordWindowUsage = (used: number, budget: number): void => {
122
+ if (!options.enableWindowWarning) return
123
+ const entry = { used, budget }
124
+ windowUsage.push(entry)
125
+ if (windowUsage.length > options.windowSize) {
126
+ windowUsage.shift()
127
+ }
128
+ if (windowWarned) return
129
+ if (
130
+ windowUsage.length >= options.windowSize &&
131
+ windowUsage.every(
132
+ item => item.budget > 0 && item.used / item.budget >= options.highUsageRatio,
133
+ )
134
+ ) {
135
+ windowWarned = true
136
+ reportCycle('high-usage-window', {
137
+ windowSize: options.windowSize,
138
+ ratio: options.highUsageRatio,
139
+ })
140
+ }
113
141
  }
114
- if (windowWarned) return
115
- if (
116
- windowUsage.length >= options.windowSize &&
117
- windowUsage.every(item => item.budget > 0 && item.used / item.budget >= options.highUsageRatio)
118
- ) {
119
- windowWarned = true
120
- reportCycle('high-usage-window', {
121
- windowSize: options.windowSize,
122
- ratio: options.highUsageRatio,
123
- })
142
+
143
+ const reportCycle = (
144
+ reason: string,
145
+ detail: Record<string, unknown> | undefined = undefined,
146
+ ): void => {
147
+ const hook = getDevtoolsHook()
148
+ hook?.cycleDetected?.(detail ? { reason, detail } : { reason })
149
+ console.warn(`[fict] cycle protection triggered: ${reason}`, detail ?? '')
124
150
  }
125
151
  }
126
152
 
127
- function reportCycle(
128
- reason: string,
129
- detail: Record<string, unknown> | undefined = undefined,
130
- ): void {
131
- const hook = getDevtoolsHook()
132
- hook?.cycleDetected?.(detail ? { reason, detail } : { reason })
133
- console.warn(`[fict] cycle protection triggered: ${reason}`, detail ?? '')
153
+ export {
154
+ setCycleProtectionOptions,
155
+ resetCycleProtectionStateForTests,
156
+ beginFlushGuard,
157
+ beforeEffectRunGuard,
158
+ endFlushGuard,
159
+ enterRootGuard,
160
+ exitRootGuard,
134
161
  }
@@ -0,0 +1,24 @@
1
+ export const DelegatedEventNames = [
2
+ 'beforeinput',
3
+ 'click',
4
+ 'dblclick',
5
+ 'contextmenu',
6
+ 'focusin',
7
+ 'focusout',
8
+ 'input',
9
+ 'keydown',
10
+ 'keyup',
11
+ 'mousedown',
12
+ 'mousemove',
13
+ 'mouseout',
14
+ 'mouseover',
15
+ 'mouseup',
16
+ 'pointerdown',
17
+ 'pointermove',
18
+ 'pointerout',
19
+ 'pointerover',
20
+ 'pointerup',
21
+ 'touchend',
22
+ 'touchmove',
23
+ 'touchstart',
24
+ ] as const
package/src/dom.ts CHANGED
@@ -20,19 +20,11 @@ import {
20
20
  createChildBinding,
21
21
  bindEvent,
22
22
  isReactive,
23
- PRIMITIVE_PROXY,
24
23
  type MaybeReactive,
25
24
  type AttributeSetter,
26
25
  type BindingHandle,
27
26
  } from './binding'
28
- import {
29
- Properties,
30
- ChildProperties,
31
- Aliases,
32
- getPropAlias,
33
- SVGElements,
34
- SVGNamespace,
35
- } from './constants'
27
+ import { Properties, ChildProperties, getPropAlias, SVGElements, SVGNamespace } from './constants'
36
28
  import { __fictPushContext, __fictPopContext } from './hooks'
37
29
  import { Fragment } from './jsx'
38
30
  import {
@@ -128,7 +120,7 @@ function resolveNamespace(tagName: string, namespace: NamespaceContext): Namespa
128
120
  if (tagName === 'math') return 'mathml'
129
121
  if (namespace === 'mathml') return 'mathml'
130
122
  if (namespace === 'svg') return 'svg'
131
- if (SVGElements.has(tagName)) return 'svg'
123
+ if (isDev && SVGElements.has(tagName)) return 'svg'
132
124
  return null
133
125
  }
134
126
 
@@ -143,9 +135,8 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
143
135
  return document.createTextNode('')
144
136
  }
145
137
 
146
- // Primitive proxy produced by keyed list binding
147
138
  if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
148
- // Handle BindingHandle (createList, createConditional, etc)
139
+ // Handle BindingHandle (list/conditional bindings, etc)
149
140
  if ('marker' in node) {
150
141
  const handle = node as { marker: unknown; dispose?: () => void; flush?: () => void }
151
142
  // Register dispose cleanup if available
@@ -164,14 +155,6 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
164
155
  }
165
156
  return createElement(handle.marker as FictNode)
166
157
  }
167
-
168
- const nodeRecord = node as unknown as Record<PropertyKey, unknown>
169
- if (nodeRecord[PRIMITIVE_PROXY]) {
170
- const primitiveGetter = nodeRecord[Symbol.toPrimitive]
171
- const value =
172
- typeof primitiveGetter === 'function' ? primitiveGetter.call(node, 'default') : node
173
- return document.createTextNode(value == null || value === false ? '' : String(value))
174
- }
175
158
  }
176
159
 
177
160
  // Array - create fragment
@@ -559,7 +542,13 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
559
542
  }
560
543
 
561
544
  // Child properties (innerHTML, textContent, etc.)
562
- if (ChildProperties.has(key)) {
545
+ if (
546
+ (isDev && ChildProperties.has(key)) ||
547
+ key === 'innerHTML' ||
548
+ key === 'textContent' ||
549
+ key === 'innerText' ||
550
+ key === 'children'
551
+ ) {
563
552
  createAttributeBinding(el, key, value as MaybeReactive<unknown>, setProperty)
564
553
  continue
565
554
  }
@@ -583,13 +572,18 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
583
572
  }
584
573
 
585
574
  // Check for property alias (element-specific mappings)
586
- const propAlias = !isSVG ? getPropAlias(key, tagName) : undefined
575
+ const propAlias = !isSVG && isDev ? getPropAlias(key, tagName) : undefined
576
+ const isProperty = !isSVG
577
+ ? isDev
578
+ ? Properties.has(key)
579
+ : key in (el as unknown as Record<string, unknown>)
580
+ : false
587
581
 
588
582
  // Handle properties and element-specific attributes
589
- if (propAlias || (!isSVG && Properties.has(key)) || (isCE && !isSVG)) {
583
+ if (propAlias || isProperty || (isCE && !isSVG)) {
590
584
  const propName = propAlias || key
591
585
  // Custom elements use toPropertyName conversion
592
- if (isCE && !Properties.has(key)) {
586
+ if (isCE && !isProperty && !propAlias) {
593
587
  createAttributeBinding(
594
588
  el,
595
589
  toPropertyName(propName),
@@ -616,7 +610,7 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
616
610
 
617
611
  // Regular attributes (potentially reactive)
618
612
  // Apply alias mapping (className -> class, htmlFor -> for)
619
- const attrName = Aliases[key] || key
613
+ const attrName = key === 'htmlFor' ? 'for' : key
620
614
  createAttributeBinding(el, attrName, value as MaybeReactive<unknown>, setAttribute)
621
615
  }
622
616
  }
package/src/effect.ts CHANGED
@@ -8,6 +8,10 @@ import {
8
8
  import { effectWithCleanup } from './signal'
9
9
  import type { Cleanup } from './types'
10
10
 
11
+ /**
12
+ * Effect callback run synchronously; async callbacks are not tracked after the first await.
13
+ * TypeScript will reject `async () => {}` here—split async work or read signals before awaiting.
14
+ */
11
15
  export type Effect = () => void | Cleanup
12
16
 
13
17
  export function createEffect(fn: Effect): () => void {