@fictjs/runtime 0.0.12 → 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.12",
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
@@ -1388,7 +1235,7 @@ function assignProp(
1388
1235
  // Standard event handling: onClick, onInput, etc.
1389
1236
  if (prop.slice(0, 2) === 'on') {
1390
1237
  const eventName = prop.slice(2).toLowerCase()
1391
- const shouldDelegate = DelegatedEvents.has(eventName)
1238
+ const shouldDelegate = isDev && DelegatedEvents.has(eventName)
1392
1239
  if (!shouldDelegate && prev) {
1393
1240
  const handler = Array.isArray(prev) ? prev[0] : prev
1394
1241
  node.removeEventListener(eventName, handler as EventListener)
@@ -1432,13 +1279,20 @@ function assignProp(
1432
1279
 
1433
1280
  // Property handling (for non-SVG elements)
1434
1281
  if (!isSVG) {
1435
- const propAlias = getPropAlias(prop, node.tagName)
1436
- const isProperty = Properties.has(prop)
1437
- 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'
1438
1292
 
1439
1293
  if (propAlias || isProperty || isChildProp || isCE) {
1440
1294
  const propName = propAlias || prop
1441
- if (isCE && !isProperty && !isChildProp) {
1295
+ if (isCE && !isProperty && !isChildProp && !propAlias) {
1442
1296
  ;(node as unknown as Record<string, unknown>)[toPropertyName(propName)] = value
1443
1297
  } else {
1444
1298
  ;(node as unknown as Record<string, unknown>)[propName] = value
@@ -1459,7 +1313,7 @@ function assignProp(
1459
1313
  }
1460
1314
 
1461
1315
  // Default: set as attribute
1462
- const attrName = Aliases[prop] || prop
1316
+ const attrName = prop === 'htmlFor' ? 'for' : prop
1463
1317
  if (value == null) node.removeAttribute(attrName)
1464
1318
  else node.setAttribute(attrName, String(value))
1465
1319
  return value
@@ -1607,116 +1461,6 @@ export function createConditional(
1607
1461
  }
1608
1462
  }
1609
1463
 
1610
- // ============================================================================
1611
- // List Rendering
1612
- // ============================================================================
1613
-
1614
- /** Key extractor function type */
1615
- export type KeyFn<T> = (item: T, index: number) => string | number
1616
-
1617
- /**
1618
- * Create a reactive list rendering binding with optional keying.
1619
- * The render callback receives signal accessors for the item and index.
1620
- */
1621
- export function createList<T>(
1622
- items: () => T[],
1623
- renderItem: (item: Signal<T>, index: Signal<number>) => FictNode,
1624
- createElementFn: CreateElementFn,
1625
- getKey?: KeyFn<T>,
1626
- ): BindingHandle {
1627
- const startMarker = document.createComment('fict:list:start')
1628
- const endMarker = document.createComment('fict:list:end')
1629
- const fragment = document.createDocumentFragment()
1630
- fragment.append(startMarker, endMarker)
1631
- const hostRoot = getCurrentRoot()
1632
-
1633
- const nodeMap = new Map<string | number, ManagedBlock<T>>()
1634
- let pendingItems: T[] | null = null
1635
-
1636
- const runListUpdate = () => {
1637
- const arr = items()
1638
- const parent = startMarker.parentNode as (ParentNode & Node) | null
1639
- if (!parent) {
1640
- pendingItems = arr
1641
- return
1642
- }
1643
- pendingItems = null
1644
-
1645
- const newNodeMap = new Map<string | number, ManagedBlock<T>>()
1646
- const blocks: ManagedBlock<T>[] = []
1647
-
1648
- for (let i = 0; i < arr.length; i++) {
1649
- const item = arr[i]! as T
1650
- const key = getKey ? getKey(item, i) : i
1651
- const existing = nodeMap.get(key)
1652
-
1653
- let block: ManagedBlock<T>
1654
- if (existing) {
1655
- const previousValue = existing.value()
1656
- if (!getKey && previousValue !== item) {
1657
- destroyRoot(existing.root)
1658
- removeBlockNodes(existing)
1659
- block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn, hostRoot)
1660
- } else {
1661
- const previousIndex = existing.index()
1662
- existing.value(item)
1663
- existing.index(i)
1664
-
1665
- const needsRerender = getKey ? true : previousValue !== item || previousIndex !== i
1666
- block = needsRerender ? rerenderBlock(existing, createElementFn) : existing
1667
- }
1668
- } else {
1669
- block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn, hostRoot)
1670
- }
1671
-
1672
- newNodeMap.set(key, block)
1673
- blocks.push(block)
1674
- }
1675
-
1676
- for (const [key, managed] of nodeMap) {
1677
- if (!newNodeMap.has(key)) {
1678
- destroyRoot(managed.root)
1679
- removeBlockNodes(managed)
1680
- }
1681
- }
1682
-
1683
- let anchor: Node = endMarker
1684
- for (let i = blocks.length - 1; i >= 0; i--) {
1685
- const block = blocks[i]!
1686
- insertNodesBefore(parent, block.nodes, anchor)
1687
- if (block.nodes.length > 0) {
1688
- anchor = block.nodes[0]!
1689
- }
1690
- }
1691
-
1692
- nodeMap.clear()
1693
- for (const [k, v] of newNodeMap) {
1694
- nodeMap.set(k, v)
1695
- }
1696
- }
1697
-
1698
- const dispose = createRenderEffect(runListUpdate)
1699
-
1700
- return {
1701
- marker: fragment,
1702
- flush: () => {
1703
- if (pendingItems !== null) {
1704
- runListUpdate()
1705
- }
1706
- },
1707
- dispose: () => {
1708
- dispose()
1709
- for (const [, managed] of nodeMap) {
1710
- destroyRoot(managed.root)
1711
- removeBlockNodes(managed)
1712
- }
1713
- nodeMap.clear()
1714
- startMarker.parentNode?.removeChild(startMarker)
1715
- endMarker.parentNode?.removeChild(endMarker)
1716
- },
1717
- }
1718
- }
1719
-
1720
1464
  // ============================================================================
1721
1465
  // Show/Hide Helper
1722
1466
  // ==========================================================================
@@ -1848,131 +1592,6 @@ export function createPortal(
1848
1592
  }
1849
1593
  }
1850
1594
 
1851
- // ============================================================================
1852
- // Internal helpers
1853
- // ============================================================================
1854
-
1855
- function mountBlock<T>(
1856
- initialValue: T,
1857
- initialIndex: number,
1858
- renderItem: (item: Signal<T>, index: Signal<number>) => FictNode,
1859
- parent: ParentNode & Node,
1860
- anchor: Node,
1861
- createElementFn: CreateElementFn,
1862
- hostRoot?: RootContext | undefined,
1863
- ): ManagedBlock<T> {
1864
- const start = document.createComment('fict:block:start')
1865
- const end = document.createComment('fict:block:end')
1866
- const valueSig = createVersionedSignalAccessor<T>(initialValue)
1867
- const indexSig = createSignal<number>(initialIndex)
1868
- const renderCurrent = () => renderItem(valueSig, indexSig)
1869
- const root = createRootContext(hostRoot)
1870
- const prev = pushRoot(root)
1871
- const nodes: Node[] = [start]
1872
- let handledError = false
1873
- try {
1874
- const output = renderCurrent()
1875
- if (output != null && output !== false) {
1876
- const el = createElementFn(output)
1877
- const rendered = toNodeArray(el)
1878
- nodes.push(...rendered)
1879
- }
1880
- nodes.push(end)
1881
- insertNodesBefore(parent, nodes, anchor)
1882
- } catch (err) {
1883
- if (handleSuspend(err as any, root)) {
1884
- handledError = true
1885
- nodes.push(end)
1886
- insertNodesBefore(parent, nodes, anchor)
1887
- } else if (handleError(err, { source: 'renderChild' }, root)) {
1888
- handledError = true
1889
- nodes.push(end)
1890
- insertNodesBefore(parent, nodes, anchor)
1891
- } else {
1892
- throw err
1893
- }
1894
- } finally {
1895
- popRoot(prev)
1896
- if (!handledError) {
1897
- flushOnMount(root)
1898
- } else {
1899
- destroyRoot(root)
1900
- }
1901
- }
1902
- return {
1903
- nodes,
1904
- root,
1905
- value: valueSig,
1906
- index: indexSig,
1907
- start,
1908
- end,
1909
- renderCurrent,
1910
- }
1911
- }
1912
-
1913
- function rerenderBlock<T>(
1914
- block: ManagedBlock<T>,
1915
- createElementFn: CreateElementFn,
1916
- ): ManagedBlock<T> {
1917
- const currentContent = block.nodes.slice(1, Math.max(1, block.nodes.length - 1))
1918
- const currentNode = currentContent.length === 1 ? currentContent[0] : null
1919
-
1920
- clearRoot(block.root)
1921
-
1922
- const prev = pushRoot(block.root)
1923
- let nextOutput: FictNode
1924
- let handledError = false
1925
- try {
1926
- nextOutput = block.renderCurrent()
1927
- } catch (err) {
1928
- if (handleSuspend(err as any, block.root)) {
1929
- handledError = true
1930
- popRoot(prev)
1931
- destroyRoot(block.root)
1932
- block.nodes = [block.start, block.end]
1933
- return block
1934
- }
1935
- if (handleError(err, { source: 'renderChild' }, block.root)) {
1936
- handledError = true
1937
- popRoot(prev)
1938
- destroyRoot(block.root)
1939
- block.nodes = [block.start, block.end]
1940
- return block
1941
- }
1942
- throw err
1943
- } finally {
1944
- if (!handledError) {
1945
- popRoot(prev)
1946
- }
1947
- }
1948
-
1949
- if (isFragmentVNode(nextOutput) && currentContent.length > 0) {
1950
- const patched = patchFragmentChildren(currentContent, nextOutput.props?.children)
1951
- if (patched) {
1952
- block.nodes = [block.start, ...currentContent, block.end]
1953
- return block
1954
- }
1955
- }
1956
-
1957
- if (currentNode && patchNode(currentNode, nextOutput)) {
1958
- block.nodes = [block.start, currentNode, block.end]
1959
- return block
1960
- }
1961
-
1962
- clearContent(block)
1963
-
1964
- if (nextOutput != null && nextOutput !== false) {
1965
- const newNodes = toNodeArray(
1966
- nextOutput instanceof Node ? nextOutput : (createElementFn(nextOutput) as Node),
1967
- )
1968
- insertNodesBefore(block.start.parentNode as ParentNode & Node, newNodes, block.end)
1969
- block.nodes = [block.start, ...newNodes, block.end]
1970
- } else {
1971
- block.nodes = [block.start, block.end]
1972
- }
1973
- return block
1974
- }
1975
-
1976
1595
  function patchElement(el: Element, output: FictNode): boolean {
1977
1596
  if (
1978
1597
  output === null ||
@@ -2104,7 +1723,7 @@ function patchNode(currentNode: Node | null, nextOutput: FictNode): boolean {
2104
1723
  return false
2105
1724
  }
2106
1725
 
2107
- function isFragmentVNode(
1726
+ function _isFragmentVNode(
2108
1727
  value: unknown,
2109
1728
  ): value is { type: typeof Fragment; props?: { children?: FictNode | FictNode[] } } {
2110
1729
  return (
@@ -2135,7 +1754,7 @@ function normalizeChildren(
2135
1754
  return result
2136
1755
  }
2137
1756
 
2138
- function patchFragmentChildren(
1757
+ function _patchFragmentChildren(
2139
1758
  nodes: Node[],
2140
1759
  children: FictNode | FictNode[] | undefined,
2141
1760
  ): boolean {
@@ -2151,20 +1770,4 @@ function patchFragmentChildren(
2151
1770
  return true
2152
1771
  }
2153
1772
 
2154
- function clearContent<T>(block: ManagedBlock<T>): void {
2155
- const nodes = block.nodes.slice(1, Math.max(1, block.nodes.length - 1))
2156
- removeNodes(nodes)
2157
- }
2158
-
2159
- function removeBlockNodes<T>(block: ManagedBlock<T>): void {
2160
- let cursor: Node | null = block.start
2161
- const end = block.end
2162
- while (cursor) {
2163
- const next: Node | null = cursor.nextSibling
2164
- cursor.parentNode?.removeChild(cursor)
2165
- if (cursor === end) break
2166
- cursor = next
2167
- }
2168
- }
2169
-
2170
1773
  // DOM utility functions are imported from './node-ops' to avoid duplication