@fictjs/runtime 0.0.11 → 0.0.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fictjs/runtime",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Fict reactive runtime",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -30,11 +30,6 @@
30
30
  "import": "./dist/index.js",
31
31
  "require": "./dist/index.cjs"
32
32
  },
33
- "./slim": {
34
- "types": "./dist/slim.d.ts",
35
- "import": "./dist/slim.js",
36
- "require": "./dist/slim.cjs"
37
- },
38
33
  "./jsx-runtime": {
39
34
  "types": "./dist/jsx-runtime.d.ts",
40
35
  "import": "./dist/jsx-runtime.js",
package/src/binding.ts CHANGED
@@ -19,12 +19,10 @@ import {
19
19
  ChildProperties,
20
20
  getPropAlias,
21
21
  SVGNamespace,
22
- Aliases,
23
22
  } from './constants'
24
23
  import { createRenderEffect } from './effect'
25
24
  import { Fragment } from './jsx'
26
25
  import {
27
- clearRoot,
28
26
  createRootContext,
29
27
  destroyRoot,
30
28
  flushOnMount,
@@ -36,12 +34,16 @@ import {
36
34
  registerRootCleanup,
37
35
  type RootContext,
38
36
  } from './lifecycle'
39
- import { createVersionedSignalAccessor } from './list-helpers'
40
37
  import { toNodeArray, removeNodes, insertNodesBefore } from './node-ops'
41
38
  import { batch } from './scheduler'
42
- import { computed, createSignal, untrack, type Signal } from './signal'
39
+ import { computed, untrack } from './signal'
43
40
  import type { Cleanup, FictNode } from './types'
44
41
 
42
+ const isDev =
43
+ typeof __DEV__ !== 'undefined'
44
+ ? __DEV__
45
+ : typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
46
+
45
47
  // ============================================================================
46
48
  // Type Definitions
47
49
  // ============================================================================
@@ -63,16 +65,6 @@ export interface BindingHandle {
63
65
  }
64
66
 
65
67
  /** Managed child node with its dispose function */
66
- interface ManagedBlock<T = unknown> {
67
- nodes: Node[]
68
- root: RootContext
69
- value: Signal<T>
70
- index: Signal<number>
71
- start: Comment
72
- end: Comment
73
- renderCurrent: () => FictNode
74
- }
75
-
76
68
  // ============================================================================
77
69
  // Utility Functions
78
70
  // ============================================================================
@@ -130,151 +122,6 @@ export function callEventHandler(
130
122
  invoke(handler)
131
123
  }
132
124
 
133
- export const PRIMITIVE_PROXY = Symbol('fict:primitive-proxy')
134
- const PRIMITIVE_PROXY_RAW_VALUE = Symbol('fict:primitive-proxy:raw-value')
135
-
136
- /**
137
- * Unwrap a primitive proxy value to get the raw primitive value.
138
- * This is primarily useful for advanced scenarios where you need the actual
139
- * primitive type (e.g., for typeof checks or strict equality comparisons).
140
- *
141
- * @param value - A potentially proxied primitive value
142
- * @returns The raw primitive value
143
- *
144
- * @example
145
- * ```ts
146
- * createList(
147
- * () => [1, 2, 3],
148
- * (item) => {
149
- * const raw = unwrapPrimitive(item)
150
- * typeof raw === 'number' // true
151
- * raw === 1 // true (for first item)
152
- * },
153
- * item => item
154
- * )
155
- * ```
156
- */
157
- export function unwrapPrimitive<T>(value: T): T {
158
- if (value && typeof value === 'object' && PRIMITIVE_PROXY in value) {
159
- // Use the internal raw value getter
160
- const getRawValue = (value as Record<PropertyKey, unknown>)[PRIMITIVE_PROXY_RAW_VALUE]
161
- if (typeof getRawValue === 'function') {
162
- return (getRawValue as () => T)()
163
- }
164
- }
165
- return value
166
- }
167
-
168
- function _createValueProxy<T>(read: () => T): T {
169
- const getPrimitivePrototype = (value: unknown): Record<PropertyKey, unknown> | undefined => {
170
- switch (typeof value) {
171
- case 'string':
172
- return String.prototype as unknown as Record<PropertyKey, unknown>
173
- case 'number':
174
- return Number.prototype as unknown as Record<PropertyKey, unknown>
175
- case 'boolean':
176
- return Boolean.prototype as unknown as Record<PropertyKey, unknown>
177
- case 'bigint':
178
- return BigInt.prototype as unknown as Record<PropertyKey, unknown>
179
- case 'symbol':
180
- return Symbol.prototype as unknown as Record<PropertyKey, unknown>
181
- default:
182
- return undefined
183
- }
184
- }
185
-
186
- const target: Record<PropertyKey, unknown> = {}
187
- const handler: ProxyHandler<Record<PropertyKey, unknown>> = {
188
- get(_target, prop, receiver) {
189
- if (prop === PRIMITIVE_PROXY) {
190
- return true
191
- }
192
- if (prop === PRIMITIVE_PROXY_RAW_VALUE) {
193
- return () => read()
194
- }
195
- if (prop === Symbol.toPrimitive) {
196
- return (hint: 'string' | 'number' | 'default') => {
197
- const value = read() as unknown
198
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
199
- const toPrimitive = (value as { [Symbol.toPrimitive]?: (hint: string) => unknown })[
200
- Symbol.toPrimitive
201
- ]
202
- if (typeof toPrimitive === 'function') {
203
- return toPrimitive.call(value, hint)
204
- }
205
- if (hint === 'string') return value.toString?.() ?? '[object Object]'
206
- if (hint === 'number') return value.valueOf?.() ?? value
207
- return value.valueOf?.() ?? value
208
- }
209
- return value
210
- }
211
- }
212
- if (prop === 'valueOf') {
213
- return () => {
214
- const value = read() as unknown
215
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
216
- return typeof (value as { valueOf?: () => unknown }).valueOf === 'function'
217
- ? (value as { valueOf: () => unknown }).valueOf()
218
- : value
219
- }
220
- return value
221
- }
222
- }
223
- if (prop === 'toString') {
224
- return () => String(read())
225
- }
226
-
227
- const value = read() as unknown
228
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
229
- return Reflect.get(value as object, prop, receiver === _target ? value : receiver)
230
- }
231
-
232
- const proto = getPrimitivePrototype(value)
233
- if (proto && prop in proto) {
234
- const descriptor = Reflect.get(proto, prop, value)
235
- return typeof descriptor === 'function' ? descriptor.bind(value) : descriptor
236
- }
237
- return undefined
238
- },
239
- set(_target, prop, newValue, receiver) {
240
- const value = read() as unknown
241
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
242
- return Reflect.set(value as object, prop, newValue, receiver === _target ? value : receiver)
243
- }
244
- return false
245
- },
246
- has(_target, prop) {
247
- if (prop === PRIMITIVE_PROXY || prop === PRIMITIVE_PROXY_RAW_VALUE) {
248
- return true
249
- }
250
- const value = read() as unknown
251
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
252
- return prop in (value as object)
253
- }
254
- const proto = getPrimitivePrototype(value)
255
- return proto ? prop in proto : false
256
- },
257
- ownKeys() {
258
- const value = read() as unknown
259
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
260
- return Reflect.ownKeys(value as object)
261
- }
262
- const proto = getPrimitivePrototype(value)
263
- return proto ? Reflect.ownKeys(proto) : []
264
- },
265
- getOwnPropertyDescriptor(_target, prop) {
266
- const value = read() as unknown
267
- if (value != null && (typeof value === 'object' || typeof value === 'function')) {
268
- return Object.getOwnPropertyDescriptor(value as object, prop)
269
- }
270
- const proto = getPrimitivePrototype(value)
271
- return proto ? Object.getOwnPropertyDescriptor(proto, prop) || undefined : undefined
272
- },
273
- }
274
-
275
- return new Proxy(target, handler) as T
276
- }
277
-
278
125
  // ============================================================================
279
126
  // Text Binding
280
127
  // ============================================================================
@@ -519,9 +366,9 @@ function applyStyle(
519
366
  }
520
367
  }
521
368
 
522
- function isUnitlessStyleProperty(prop: string): boolean {
523
- return UnitlessStyles.has(prop)
524
- }
369
+ const isUnitlessStyleProperty = isDev
370
+ ? (prop: string): boolean => UnitlessStyles.has(prop)
371
+ : (prop: string): boolean => prop === 'opacity' || prop === 'zIndex'
525
372
 
526
373
  // ============================================================================
527
374
  // Class Binding
@@ -1107,7 +954,7 @@ export function bindEvent(
1107
954
  // Optimization: Global Event Delegation
1108
955
  // If the event is delegatable and no special options (capture, passive) are used,
1109
956
  // we attach the handler to the element property and rely on the global listener.
1110
- if (DelegatedEvents.has(eventName) && !options) {
957
+ if (isDev && DelegatedEvents.has(eventName) && !options) {
1111
958
  const key = `$$${eventName}`
1112
959
 
1113
960
  // Ensure global delegation is active for this event
@@ -1124,7 +971,9 @@ export function bindEvent(
1124
971
  const fn = resolveHandler()
1125
972
  callEventHandler(fn as EventListenerOrEventListenerObject, args[0] as Event, el)
1126
973
  } catch (err) {
1127
- handleError(err, { source: 'event', eventName }, rootRef)
974
+ if (!handleError(err, { source: 'event', eventName }, rootRef)) {
975
+ throw err
976
+ }
1128
977
  }
1129
978
  }
1130
979
 
@@ -1386,7 +1235,7 @@ function assignProp(
1386
1235
  // Standard event handling: onClick, onInput, etc.
1387
1236
  if (prop.slice(0, 2) === 'on') {
1388
1237
  const eventName = prop.slice(2).toLowerCase()
1389
- const shouldDelegate = DelegatedEvents.has(eventName)
1238
+ const shouldDelegate = isDev && DelegatedEvents.has(eventName)
1390
1239
  if (!shouldDelegate && prev) {
1391
1240
  const handler = Array.isArray(prev) ? prev[0] : prev
1392
1241
  node.removeEventListener(eventName, handler as EventListener)
@@ -1430,13 +1279,20 @@ function assignProp(
1430
1279
 
1431
1280
  // Property handling (for non-SVG elements)
1432
1281
  if (!isSVG) {
1433
- const propAlias = getPropAlias(prop, node.tagName)
1434
- const isProperty = Properties.has(prop)
1435
- const isChildProp = ChildProperties.has(prop)
1282
+ const propAlias = isDev ? getPropAlias(prop, node.tagName) : undefined
1283
+ const isProperty = isDev
1284
+ ? Properties.has(prop)
1285
+ : prop in (node as unknown as Record<string, unknown>)
1286
+ const isChildProp = isDev
1287
+ ? ChildProperties.has(prop)
1288
+ : prop === 'innerHTML' ||
1289
+ prop === 'textContent' ||
1290
+ prop === 'innerText' ||
1291
+ prop === 'children'
1436
1292
 
1437
1293
  if (propAlias || isProperty || isChildProp || isCE) {
1438
1294
  const propName = propAlias || prop
1439
- if (isCE && !isProperty && !isChildProp) {
1295
+ if (isCE && !isProperty && !isChildProp && !propAlias) {
1440
1296
  ;(node as unknown as Record<string, unknown>)[toPropertyName(propName)] = value
1441
1297
  } else {
1442
1298
  ;(node as unknown as Record<string, unknown>)[propName] = value
@@ -1457,7 +1313,7 @@ function assignProp(
1457
1313
  }
1458
1314
 
1459
1315
  // Default: set as attribute
1460
- const attrName = Aliases[prop] || prop
1316
+ const attrName = prop === 'htmlFor' ? 'for' : prop
1461
1317
  if (value == null) node.removeAttribute(attrName)
1462
1318
  else node.setAttribute(attrName, String(value))
1463
1319
  return value
@@ -1605,116 +1461,6 @@ export function createConditional(
1605
1461
  }
1606
1462
  }
1607
1463
 
1608
- // ============================================================================
1609
- // List Rendering
1610
- // ============================================================================
1611
-
1612
- /** Key extractor function type */
1613
- export type KeyFn<T> = (item: T, index: number) => string | number
1614
-
1615
- /**
1616
- * Create a reactive list rendering binding with optional keying.
1617
- * The render callback receives signal accessors for the item and index.
1618
- */
1619
- export function createList<T>(
1620
- items: () => T[],
1621
- renderItem: (item: Signal<T>, index: Signal<number>) => FictNode,
1622
- createElementFn: CreateElementFn,
1623
- getKey?: KeyFn<T>,
1624
- ): BindingHandle {
1625
- const startMarker = document.createComment('fict:list:start')
1626
- const endMarker = document.createComment('fict:list:end')
1627
- const fragment = document.createDocumentFragment()
1628
- fragment.append(startMarker, endMarker)
1629
- const hostRoot = getCurrentRoot()
1630
-
1631
- const nodeMap = new Map<string | number, ManagedBlock<T>>()
1632
- let pendingItems: T[] | null = null
1633
-
1634
- const runListUpdate = () => {
1635
- const arr = items()
1636
- const parent = startMarker.parentNode as (ParentNode & Node) | null
1637
- if (!parent) {
1638
- pendingItems = arr
1639
- return
1640
- }
1641
- pendingItems = null
1642
-
1643
- const newNodeMap = new Map<string | number, ManagedBlock<T>>()
1644
- const blocks: ManagedBlock<T>[] = []
1645
-
1646
- for (let i = 0; i < arr.length; i++) {
1647
- const item = arr[i]! as T
1648
- const key = getKey ? getKey(item, i) : i
1649
- const existing = nodeMap.get(key)
1650
-
1651
- let block: ManagedBlock<T>
1652
- if (existing) {
1653
- const previousValue = existing.value()
1654
- if (!getKey && previousValue !== item) {
1655
- destroyRoot(existing.root)
1656
- removeBlockNodes(existing)
1657
- block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn, hostRoot)
1658
- } else {
1659
- const previousIndex = existing.index()
1660
- existing.value(item)
1661
- existing.index(i)
1662
-
1663
- const needsRerender = getKey ? true : previousValue !== item || previousIndex !== i
1664
- block = needsRerender ? rerenderBlock(existing, createElementFn) : existing
1665
- }
1666
- } else {
1667
- block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn, hostRoot)
1668
- }
1669
-
1670
- newNodeMap.set(key, block)
1671
- blocks.push(block)
1672
- }
1673
-
1674
- for (const [key, managed] of nodeMap) {
1675
- if (!newNodeMap.has(key)) {
1676
- destroyRoot(managed.root)
1677
- removeBlockNodes(managed)
1678
- }
1679
- }
1680
-
1681
- let anchor: Node = endMarker
1682
- for (let i = blocks.length - 1; i >= 0; i--) {
1683
- const block = blocks[i]!
1684
- insertNodesBefore(parent, block.nodes, anchor)
1685
- if (block.nodes.length > 0) {
1686
- anchor = block.nodes[0]!
1687
- }
1688
- }
1689
-
1690
- nodeMap.clear()
1691
- for (const [k, v] of newNodeMap) {
1692
- nodeMap.set(k, v)
1693
- }
1694
- }
1695
-
1696
- const dispose = createRenderEffect(runListUpdate)
1697
-
1698
- return {
1699
- marker: fragment,
1700
- flush: () => {
1701
- if (pendingItems !== null) {
1702
- runListUpdate()
1703
- }
1704
- },
1705
- dispose: () => {
1706
- dispose()
1707
- for (const [, managed] of nodeMap) {
1708
- destroyRoot(managed.root)
1709
- removeBlockNodes(managed)
1710
- }
1711
- nodeMap.clear()
1712
- startMarker.parentNode?.removeChild(startMarker)
1713
- endMarker.parentNode?.removeChild(endMarker)
1714
- },
1715
- }
1716
- }
1717
-
1718
1464
  // ============================================================================
1719
1465
  // Show/Hide Helper
1720
1466
  // ==========================================================================
@@ -1846,131 +1592,6 @@ export function createPortal(
1846
1592
  }
1847
1593
  }
1848
1594
 
1849
- // ============================================================================
1850
- // Internal helpers
1851
- // ============================================================================
1852
-
1853
- function mountBlock<T>(
1854
- initialValue: T,
1855
- initialIndex: number,
1856
- renderItem: (item: Signal<T>, index: Signal<number>) => FictNode,
1857
- parent: ParentNode & Node,
1858
- anchor: Node,
1859
- createElementFn: CreateElementFn,
1860
- hostRoot?: RootContext | undefined,
1861
- ): ManagedBlock<T> {
1862
- const start = document.createComment('fict:block:start')
1863
- const end = document.createComment('fict:block:end')
1864
- const valueSig = createVersionedSignalAccessor<T>(initialValue)
1865
- const indexSig = createSignal<number>(initialIndex)
1866
- const renderCurrent = () => renderItem(valueSig, indexSig)
1867
- const root = createRootContext(hostRoot)
1868
- const prev = pushRoot(root)
1869
- const nodes: Node[] = [start]
1870
- let handledError = false
1871
- try {
1872
- const output = renderCurrent()
1873
- if (output != null && output !== false) {
1874
- const el = createElementFn(output)
1875
- const rendered = toNodeArray(el)
1876
- nodes.push(...rendered)
1877
- }
1878
- nodes.push(end)
1879
- insertNodesBefore(parent, nodes, anchor)
1880
- } catch (err) {
1881
- if (handleSuspend(err as any, root)) {
1882
- handledError = true
1883
- nodes.push(end)
1884
- insertNodesBefore(parent, nodes, anchor)
1885
- } else if (handleError(err, { source: 'renderChild' }, root)) {
1886
- handledError = true
1887
- nodes.push(end)
1888
- insertNodesBefore(parent, nodes, anchor)
1889
- } else {
1890
- throw err
1891
- }
1892
- } finally {
1893
- popRoot(prev)
1894
- if (!handledError) {
1895
- flushOnMount(root)
1896
- } else {
1897
- destroyRoot(root)
1898
- }
1899
- }
1900
- return {
1901
- nodes,
1902
- root,
1903
- value: valueSig,
1904
- index: indexSig,
1905
- start,
1906
- end,
1907
- renderCurrent,
1908
- }
1909
- }
1910
-
1911
- function rerenderBlock<T>(
1912
- block: ManagedBlock<T>,
1913
- createElementFn: CreateElementFn,
1914
- ): ManagedBlock<T> {
1915
- const currentContent = block.nodes.slice(1, Math.max(1, block.nodes.length - 1))
1916
- const currentNode = currentContent.length === 1 ? currentContent[0] : null
1917
-
1918
- clearRoot(block.root)
1919
-
1920
- const prev = pushRoot(block.root)
1921
- let nextOutput: FictNode
1922
- let handledError = false
1923
- try {
1924
- nextOutput = block.renderCurrent()
1925
- } catch (err) {
1926
- if (handleSuspend(err as any, block.root)) {
1927
- handledError = true
1928
- popRoot(prev)
1929
- destroyRoot(block.root)
1930
- block.nodes = [block.start, block.end]
1931
- return block
1932
- }
1933
- if (handleError(err, { source: 'renderChild' }, block.root)) {
1934
- handledError = true
1935
- popRoot(prev)
1936
- destroyRoot(block.root)
1937
- block.nodes = [block.start, block.end]
1938
- return block
1939
- }
1940
- throw err
1941
- } finally {
1942
- if (!handledError) {
1943
- popRoot(prev)
1944
- }
1945
- }
1946
-
1947
- if (isFragmentVNode(nextOutput) && currentContent.length > 0) {
1948
- const patched = patchFragmentChildren(currentContent, nextOutput.props?.children)
1949
- if (patched) {
1950
- block.nodes = [block.start, ...currentContent, block.end]
1951
- return block
1952
- }
1953
- }
1954
-
1955
- if (currentNode && patchNode(currentNode, nextOutput)) {
1956
- block.nodes = [block.start, currentNode, block.end]
1957
- return block
1958
- }
1959
-
1960
- clearContent(block)
1961
-
1962
- if (nextOutput != null && nextOutput !== false) {
1963
- const newNodes = toNodeArray(
1964
- nextOutput instanceof Node ? nextOutput : (createElementFn(nextOutput) as Node),
1965
- )
1966
- insertNodesBefore(block.start.parentNode as ParentNode & Node, newNodes, block.end)
1967
- block.nodes = [block.start, ...newNodes, block.end]
1968
- } else {
1969
- block.nodes = [block.start, block.end]
1970
- }
1971
- return block
1972
- }
1973
-
1974
1595
  function patchElement(el: Element, output: FictNode): boolean {
1975
1596
  if (
1976
1597
  output === null ||
@@ -2102,7 +1723,7 @@ function patchNode(currentNode: Node | null, nextOutput: FictNode): boolean {
2102
1723
  return false
2103
1724
  }
2104
1725
 
2105
- function isFragmentVNode(
1726
+ function _isFragmentVNode(
2106
1727
  value: unknown,
2107
1728
  ): value is { type: typeof Fragment; props?: { children?: FictNode | FictNode[] } } {
2108
1729
  return (
@@ -2133,7 +1754,7 @@ function normalizeChildren(
2133
1754
  return result
2134
1755
  }
2135
1756
 
2136
- function patchFragmentChildren(
1757
+ function _patchFragmentChildren(
2137
1758
  nodes: Node[],
2138
1759
  children: FictNode | FictNode[] | undefined,
2139
1760
  ): boolean {
@@ -2149,20 +1770,4 @@ function patchFragmentChildren(
2149
1770
  return true
2150
1771
  }
2151
1772
 
2152
- function clearContent<T>(block: ManagedBlock<T>): void {
2153
- const nodes = block.nodes.slice(1, Math.max(1, block.nodes.length - 1))
2154
- removeNodes(nodes)
2155
- }
2156
-
2157
- function removeBlockNodes<T>(block: ManagedBlock<T>): void {
2158
- let cursor: Node | null = block.start
2159
- const end = block.end
2160
- while (cursor) {
2161
- const next: Node | null = cursor.nextSibling
2162
- cursor.parentNode?.removeChild(cursor)
2163
- if (cursor === end) break
2164
- cursor = next
2165
- }
2166
- }
2167
-
2168
1773
  // DOM utility functions are imported from './node-ops' to avoid duplication