@fictjs/runtime 0.0.2

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 (51) hide show
  1. package/README.md +17 -0
  2. package/dist/index.cjs +4224 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +1572 -0
  5. package/dist/index.d.ts +1572 -0
  6. package/dist/index.dev.js +4240 -0
  7. package/dist/index.dev.js.map +1 -0
  8. package/dist/index.js +4133 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/jsx-dev-runtime.cjs +44 -0
  11. package/dist/jsx-dev-runtime.cjs.map +1 -0
  12. package/dist/jsx-dev-runtime.js +14 -0
  13. package/dist/jsx-dev-runtime.js.map +1 -0
  14. package/dist/jsx-runtime.cjs +44 -0
  15. package/dist/jsx-runtime.cjs.map +1 -0
  16. package/dist/jsx-runtime.js +14 -0
  17. package/dist/jsx-runtime.js.map +1 -0
  18. package/dist/slim.cjs +3384 -0
  19. package/dist/slim.cjs.map +1 -0
  20. package/dist/slim.d.cts +475 -0
  21. package/dist/slim.d.ts +475 -0
  22. package/dist/slim.js +3335 -0
  23. package/dist/slim.js.map +1 -0
  24. package/package.json +68 -0
  25. package/src/binding.ts +2127 -0
  26. package/src/constants.ts +456 -0
  27. package/src/cycle-guard.ts +134 -0
  28. package/src/devtools.ts +17 -0
  29. package/src/dom.ts +683 -0
  30. package/src/effect.ts +83 -0
  31. package/src/error-boundary.ts +118 -0
  32. package/src/hooks.ts +72 -0
  33. package/src/index.ts +184 -0
  34. package/src/jsx-dev-runtime.ts +2 -0
  35. package/src/jsx-runtime.ts +2 -0
  36. package/src/jsx.ts +786 -0
  37. package/src/lifecycle.ts +273 -0
  38. package/src/list-helpers.ts +619 -0
  39. package/src/memo.ts +14 -0
  40. package/src/node-ops.ts +185 -0
  41. package/src/props.ts +212 -0
  42. package/src/reconcile.ts +151 -0
  43. package/src/ref.ts +25 -0
  44. package/src/scheduler.ts +12 -0
  45. package/src/signal.ts +1278 -0
  46. package/src/slim.ts +68 -0
  47. package/src/store.ts +210 -0
  48. package/src/suspense.ts +187 -0
  49. package/src/transition.ts +128 -0
  50. package/src/types.ts +172 -0
  51. package/src/versioned-signal.ts +58 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Low-level DOM node helpers shared across runtime modules.
3
+ * Keep this file dependency-free to avoid module cycles.
4
+ */
5
+
6
+ /**
7
+ * Convert a value to a flat array of DOM nodes.
8
+ * Defensively handles proxies and non-DOM values.
9
+ */
10
+ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
11
+ try {
12
+ if (Array.isArray(node)) {
13
+ // Preserve original array reference when it's already a flat Node array
14
+ let allNodes = true
15
+ for (const item of node) {
16
+ let isItemNode = false
17
+ try {
18
+ isItemNode = item instanceof Node
19
+ } catch {
20
+ isItemNode = false
21
+ }
22
+ if (!isItemNode) {
23
+ allNodes = false
24
+ break
25
+ }
26
+ }
27
+ if (allNodes) {
28
+ return node as Node[]
29
+ }
30
+ const result: Node[] = []
31
+ for (const item of node) {
32
+ result.push(...toNodeArray(item))
33
+ }
34
+ return result
35
+ }
36
+ if (node === null || node === undefined || node === false) {
37
+ return []
38
+ }
39
+ } catch {
40
+ return []
41
+ }
42
+
43
+ let isNode = false
44
+ try {
45
+ isNode = node instanceof Node
46
+ } catch {
47
+ // If safe check fails, treat as primitive string
48
+ isNode = false
49
+ }
50
+
51
+ if (isNode) {
52
+ try {
53
+ if (node instanceof DocumentFragment) {
54
+ return Array.from(node.childNodes)
55
+ }
56
+ } catch {
57
+ // Ignore fragment check error
58
+ }
59
+ return [node as Node]
60
+ }
61
+
62
+ try {
63
+ // Duck-type BindingHandle-like values
64
+ if (typeof node === 'object' && node !== null && 'marker' in node) {
65
+ return toNodeArray((node as { marker: unknown }).marker)
66
+ }
67
+ } catch {
68
+ // Ignore property check error
69
+ }
70
+
71
+ // Primitive fallback
72
+ try {
73
+ return [document.createTextNode(String(node))]
74
+ } catch {
75
+ return [document.createTextNode('')]
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Insert nodes before an anchor node, preserving order.
81
+ * Uses DocumentFragment for batch insertion when inserting multiple nodes.
82
+ */
83
+ export function insertNodesBefore(
84
+ parent: ParentNode & Node,
85
+ nodes: Node[],
86
+ anchor: Node | null,
87
+ ): void {
88
+ if (nodes.length === 0) return
89
+
90
+ // Single node optimization - direct insertion
91
+ if (nodes.length === 1) {
92
+ const node = nodes[0]!
93
+ if (node === undefined || node === null) return
94
+ if (node.ownerDocument !== parent.ownerDocument && parent.ownerDocument) {
95
+ parent.ownerDocument.adoptNode(node)
96
+ }
97
+ try {
98
+ parent.insertBefore(node, anchor)
99
+ } catch (e: unknown) {
100
+ if (parent.ownerDocument) {
101
+ try {
102
+ const clone = parent.ownerDocument.importNode(node, true)
103
+ parent.insertBefore(clone, anchor)
104
+ return
105
+ } catch {
106
+ // Clone fallback failed
107
+ }
108
+ }
109
+ throw e
110
+ }
111
+ return
112
+ }
113
+
114
+ // Batch insertion using DocumentFragment for multiple nodes
115
+ const doc = parent.ownerDocument
116
+ if (doc) {
117
+ const frag = doc.createDocumentFragment()
118
+ for (let i = 0; i < nodes.length; i++) {
119
+ const node = nodes[i]!
120
+ if (node === undefined || node === null) continue
121
+ // Handle DocumentFragment - append children
122
+ if (node.nodeType === 11) {
123
+ const childrenArr = Array.from(node.childNodes)
124
+ for (let j = 0; j < childrenArr.length; j++) {
125
+ frag.appendChild(childrenArr[j]!)
126
+ }
127
+ } else {
128
+ if (node.ownerDocument !== doc) {
129
+ doc.adoptNode(node)
130
+ }
131
+ frag.appendChild(node)
132
+ }
133
+ }
134
+ parent.insertBefore(frag, anchor)
135
+ return
136
+ }
137
+
138
+ // Fallback for non-document contexts (rare)
139
+ const insertSingle = (nodeToInsert: Node, anchorNode: Node | null): Node => {
140
+ if (nodeToInsert.ownerDocument !== parent.ownerDocument && parent.ownerDocument) {
141
+ parent.ownerDocument.adoptNode(nodeToInsert)
142
+ }
143
+ try {
144
+ parent.insertBefore(nodeToInsert, anchorNode)
145
+ return nodeToInsert
146
+ } catch (e: unknown) {
147
+ if (parent.ownerDocument) {
148
+ try {
149
+ const clone = parent.ownerDocument.importNode(nodeToInsert, true)
150
+ parent.insertBefore(clone, anchorNode)
151
+ return clone
152
+ } catch {
153
+ // Clone fallback failed
154
+ }
155
+ }
156
+ throw e
157
+ }
158
+ }
159
+
160
+ for (let i = nodes.length - 1; i >= 0; i--) {
161
+ const node = nodes[i]!
162
+ if (node === undefined || node === null) continue
163
+
164
+ // Handle DocumentFragment - insert children in reverse order
165
+ const isFrag = node.nodeType === 11
166
+ if (isFrag) {
167
+ const childrenArr = Array.from(node.childNodes)
168
+ for (let j = childrenArr.length - 1; j >= 0; j--) {
169
+ const child = childrenArr[j]!
170
+ anchor = insertSingle(child, anchor)
171
+ }
172
+ } else {
173
+ anchor = insertSingle(node, anchor)
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Remove an array of nodes from the DOM.
180
+ */
181
+ export function removeNodes(nodes: Node[]): void {
182
+ for (const node of nodes) {
183
+ node.parentNode?.removeChild(node)
184
+ }
185
+ }
package/src/props.ts ADDED
@@ -0,0 +1,212 @@
1
+ import { createMemo } from './memo'
2
+
3
+ const propGetters = new WeakSet<(...args: unknown[]) => unknown>()
4
+ const rawToProxy = new WeakMap<object, object>()
5
+ const proxyToRaw = new WeakMap<object, object>()
6
+
7
+ /**
8
+ * @internal
9
+ * Marks a zero-arg getter so props proxy can lazily evaluate it.
10
+ * Users normally never call this directly; the compiler injects it.
11
+ */
12
+ export function __fictProp<T>(getter: () => T): () => T {
13
+ if (typeof getter === 'function' && getter.length === 0) {
14
+ propGetters.add(getter)
15
+ }
16
+ return getter
17
+ }
18
+
19
+ function isPropGetter(value: unknown): value is () => unknown {
20
+ return typeof value === 'function' && propGetters.has(value as (...args: unknown[]) => unknown)
21
+ }
22
+
23
+ export function createPropsProxy<T extends Record<string, unknown>>(props: T): T {
24
+ if (!props || typeof props !== 'object') {
25
+ return props
26
+ }
27
+
28
+ if (proxyToRaw.has(props)) {
29
+ return props
30
+ }
31
+
32
+ const cached = rawToProxy.get(props)
33
+ if (cached) {
34
+ return cached as T
35
+ }
36
+
37
+ const proxy = new Proxy(props, {
38
+ get(target, prop, receiver) {
39
+ const value = Reflect.get(target, prop, receiver)
40
+ if (isPropGetter(value)) {
41
+ return value()
42
+ }
43
+ return value
44
+ },
45
+ set(target, prop, value, receiver) {
46
+ return Reflect.set(target, prop, value, receiver)
47
+ },
48
+ has(target, prop) {
49
+ return prop in target
50
+ },
51
+ ownKeys(target) {
52
+ return Reflect.ownKeys(target)
53
+ },
54
+ getOwnPropertyDescriptor(target, prop) {
55
+ return Object.getOwnPropertyDescriptor(target, prop)
56
+ },
57
+ })
58
+
59
+ rawToProxy.set(props, proxy)
60
+ proxyToRaw.set(proxy, props)
61
+
62
+ return proxy as T
63
+ }
64
+
65
+ export function unwrapProps<T extends Record<string, unknown>>(props: T): T {
66
+ if (!props || typeof props !== 'object') {
67
+ return props
68
+ }
69
+ return (proxyToRaw.get(props) as T | undefined) ?? props
70
+ }
71
+
72
+ /**
73
+ * Create a rest-like props object while preserving prop getters.
74
+ * Excludes the specified keys from the returned object.
75
+ */
76
+ export function __fictPropsRest<T extends Record<string, unknown>>(
77
+ props: T,
78
+ exclude: (string | number | symbol)[],
79
+ ): Record<string, unknown> {
80
+ const raw = unwrapProps(props)
81
+ const out: Record<string, unknown> = {}
82
+ const excludeSet = new Set(exclude)
83
+
84
+ for (const key of Reflect.ownKeys(raw)) {
85
+ if (excludeSet.has(key)) continue
86
+ out[key as string] = (raw as Record<string | symbol, unknown>)[key]
87
+ }
88
+
89
+ // Wrap in props proxy so getters remain lazy when accessed via rest
90
+ return createPropsProxy(out)
91
+ }
92
+
93
+ /**
94
+ * Merge multiple props-like objects while preserving lazy getters.
95
+ * Later sources override earlier ones.
96
+ *
97
+ * Uses lazy lookup strategy - properties are only accessed when read,
98
+ * avoiding upfront iteration of all keys.
99
+ */
100
+ type MergeSource<T extends Record<string, unknown>> = T | (() => T)
101
+
102
+ export function mergeProps<T extends Record<string, unknown>>(
103
+ ...sources: (MergeSource<T> | null | undefined)[]
104
+ ): Record<string, unknown> {
105
+ // Filter out null/undefined sources upfront and store as concrete type
106
+ const validSources: MergeSource<T>[] = sources.filter(
107
+ (s): s is MergeSource<T> => s != null && (typeof s === 'object' || typeof s === 'function'),
108
+ )
109
+
110
+ if (validSources.length === 0) {
111
+ return {}
112
+ }
113
+
114
+ if (validSources.length === 1 && typeof validSources[0] === 'object') {
115
+ // Return source directly to preserve getter behavior (consistent with multi-source)
116
+ return validSources[0]!
117
+ }
118
+
119
+ const resolveSource = (src: MergeSource<T>): T | undefined => {
120
+ const value = typeof src === 'function' ? src() : src
121
+ if (!value || typeof value !== 'object') return undefined
122
+ return unwrapProps(value as T)
123
+ }
124
+
125
+ return new Proxy({} as Record<string, unknown>, {
126
+ get(_, prop) {
127
+ // Symbol properties (like Symbol.iterator) should return undefined
128
+ if (typeof prop === 'symbol') {
129
+ return undefined
130
+ }
131
+ // Search sources in reverse order (last wins)
132
+ for (let i = validSources.length - 1; i >= 0; i--) {
133
+ const src = validSources[i]!
134
+ const raw = resolveSource(src)
135
+ if (!raw || !(prop in raw)) continue
136
+
137
+ const value = (raw as Record<string | symbol, unknown>)[prop]
138
+ // Preserve prop getters - let child component's createPropsProxy unwrap lazily
139
+ if (typeof src === 'function' && !isPropGetter(value)) {
140
+ return __fictProp(() => {
141
+ const latest = resolveSource(src)
142
+ if (!latest || !(prop in latest)) return undefined
143
+ return (latest as Record<string | symbol, unknown>)[prop]
144
+ })
145
+ }
146
+ return value
147
+ }
148
+ return undefined
149
+ },
150
+
151
+ has(_, prop) {
152
+ for (const src of validSources) {
153
+ const raw = resolveSource(src)
154
+ if (raw && prop in raw) {
155
+ return true
156
+ }
157
+ }
158
+ return false
159
+ },
160
+
161
+ ownKeys() {
162
+ const keys = new Set<string | symbol>()
163
+ for (const src of validSources) {
164
+ const raw = resolveSource(src)
165
+ if (raw) {
166
+ for (const key of Reflect.ownKeys(raw)) {
167
+ keys.add(key)
168
+ }
169
+ }
170
+ }
171
+ return Array.from(keys)
172
+ },
173
+
174
+ getOwnPropertyDescriptor(_, prop) {
175
+ for (let i = validSources.length - 1; i >= 0; i--) {
176
+ const raw = resolveSource(validSources[i]!)
177
+ if (raw && prop in raw) {
178
+ return {
179
+ enumerable: true,
180
+ configurable: true,
181
+ get: () => {
182
+ const value = (raw as Record<string | symbol, unknown>)[prop]
183
+ // Preserve prop getters - let child component's createPropsProxy unwrap lazily
184
+ return value
185
+ },
186
+ }
187
+ }
188
+ }
189
+ return undefined
190
+ },
191
+ })
192
+ }
193
+
194
+ export type PropGetter<T> = (() => T) & { __fictProp: true }
195
+ /**
196
+ * Memoize a prop getter to cache expensive computations.
197
+ * Use when prop expressions involve heavy calculations.
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * // Without useProp - recomputes on every access
202
+ * <Child data={expensiveComputation(list, filter)} />
203
+ *
204
+ * // With useProp - cached until dependencies change, auto-unwrapped by props proxy
205
+ * const memoizedData = useProp(() => expensiveComputation(list, filter))
206
+ * <Child data={memoizedData} />
207
+ * ```
208
+ */
209
+ export function useProp<T>(getter: () => T): PropGetter<T> {
210
+ // Wrap in prop so component props proxy auto-unwraps when passed down.
211
+ return __fictProp(createMemo(getter)) as PropGetter<T>
212
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Fict DOM Reconciliation
3
+ *
4
+ * Efficient array reconciliation algorithm based on udomdiff.
5
+ * https://github.com/WebReflection/udomdiff
6
+ *
7
+ * This algorithm uses a 5-step strategy:
8
+ * 1. Common prefix - skip matching nodes at start
9
+ * 2. Common suffix - skip matching nodes at end
10
+ * 3. Append - insert remaining new nodes
11
+ * 4. Remove - remove remaining old nodes
12
+ * 5. Swap/Map fallback - handle complex rearrangements
13
+ *
14
+ * Most real-world updates (95%+) use fast paths without building a Map.
15
+ */
16
+
17
+ /**
18
+ * Reconcile two arrays of DOM nodes, efficiently updating the DOM.
19
+ *
20
+ * @param parentNode - The parent element containing the nodes
21
+ * @param a - The old array of nodes (currently in DOM)
22
+ * @param b - The new array of nodes (target state)
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const oldNodes = [node1, node2, node3]
27
+ * const newNodes = [node1, node4, node3] // node2 replaced with node4
28
+ * reconcileArrays(parent, oldNodes, newNodes)
29
+ * ```
30
+ */
31
+ export default function reconcileArrays(parentNode: ParentNode, a: Node[], b: Node[]): void {
32
+ const bLength = b.length
33
+ let aEnd = a.length
34
+ let bEnd = bLength
35
+ let aStart = 0
36
+ let bStart = 0
37
+ const after = aEnd > 0 ? a[aEnd - 1]!.nextSibling : null
38
+ let map: Map<Node, number> | null = null
39
+
40
+ while (aStart < aEnd || bStart < bEnd) {
41
+ // 1. Common prefix - nodes match at start
42
+ if (a[aStart] === b[bStart]) {
43
+ aStart++
44
+ bStart++
45
+ continue
46
+ }
47
+
48
+ // 2. Common suffix - nodes match at end
49
+ while (a[aEnd - 1] === b[bEnd - 1]) {
50
+ aEnd--
51
+ bEnd--
52
+ }
53
+
54
+ // 3. Append - old array exhausted, insert remaining new nodes
55
+ if (aEnd === aStart) {
56
+ const node: Node | null =
57
+ bEnd < bLength ? (bStart ? b[bStart - 1]!.nextSibling : (b[bEnd - bStart] ?? null)) : after
58
+
59
+ const count = bEnd - bStart
60
+ const doc = (parentNode as Node).ownerDocument
61
+ if (count > 1 && doc) {
62
+ const frag = doc.createDocumentFragment()
63
+ for (let i = bStart; i < bEnd; i++) {
64
+ frag.appendChild(b[i]!)
65
+ }
66
+ parentNode.insertBefore(frag, node)
67
+ bStart = bEnd
68
+ } else {
69
+ while (bStart < bEnd) {
70
+ parentNode.insertBefore(b[bStart++]!, node)
71
+ }
72
+ }
73
+ }
74
+ // 4. Remove - new array exhausted, remove remaining old nodes
75
+ else if (bEnd === bStart) {
76
+ while (aStart < aEnd) {
77
+ const nodeToRemove = a[aStart]!
78
+ if (!map || !map.has(nodeToRemove)) {
79
+ nodeToRemove.parentNode?.removeChild(nodeToRemove)
80
+ }
81
+ aStart++
82
+ }
83
+ }
84
+ // 5a. Swap backward - detect backward swap pattern
85
+ else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
86
+ const node = a[--aEnd]!.nextSibling
87
+ parentNode.insertBefore(b[bStart++]!, a[aStart++]!.nextSibling)
88
+ parentNode.insertBefore(b[--bEnd]!, node)
89
+ // Update reference in old array for potential future matches
90
+ a[aEnd] = b[bEnd]!
91
+ }
92
+ // 5b. Map fallback - use Map for complex rearrangements
93
+ else {
94
+ // Build map on first use (lazy initialization)
95
+ if (!map) {
96
+ map = new Map()
97
+ let i = bStart
98
+ while (i < bEnd) {
99
+ map.set(b[i]!, i++)
100
+ }
101
+ }
102
+
103
+ const index = map.get(a[aStart]!)
104
+
105
+ if (index != null) {
106
+ if (bStart < index && index < bEnd) {
107
+ // Check for longest increasing subsequence
108
+ let i = aStart
109
+ let sequence = 1
110
+ let t: number | undefined
111
+
112
+ while (++i < aEnd && i < bEnd) {
113
+ t = map.get(a[i]!)
114
+ if (t == null || t !== index + sequence) break
115
+ sequence++
116
+ }
117
+
118
+ // Use optimal strategy based on sequence length
119
+ if (sequence > index - bStart) {
120
+ // Sequence is long enough - insert nodes before current
121
+ const node = a[aStart]!
122
+ while (bStart < index) {
123
+ parentNode.insertBefore(b[bStart++]!, node)
124
+ }
125
+ } else {
126
+ // Short sequence - replace
127
+ parentNode.replaceChild(b[bStart++]!, a[aStart++]!)
128
+ }
129
+ } else {
130
+ aStart++
131
+ }
132
+ } else {
133
+ // Node not in new array - remove it
134
+ const nodeToRemove = a[aStart++]!
135
+ nodeToRemove.parentNode?.removeChild(nodeToRemove)
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Simple reconciliation for keyed lists.
143
+ * Uses the same algorithm but works with keyed blocks.
144
+ *
145
+ * @param parentNode - The parent element
146
+ * @param oldNodes - Old nodes in DOM order
147
+ * @param newNodes - New nodes in target order
148
+ */
149
+ export function reconcileNodes(parentNode: ParentNode, oldNodes: Node[], newNodes: Node[]): void {
150
+ reconcileArrays(parentNode, oldNodes, newNodes)
151
+ }
package/src/ref.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { RefObject } from './types'
2
+
3
+ /**
4
+ * Create a ref object for DOM element references.
5
+ *
6
+ * @returns A ref object with a `current` property initialized to `null`
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { createRef } from 'fict'
11
+ *
12
+ * function Component() {
13
+ * const inputRef = createRef<HTMLInputElement>()
14
+ *
15
+ * $effect(() => {
16
+ * inputRef.current?.focus()
17
+ * })
18
+ *
19
+ * return <input ref={inputRef} />
20
+ * }
21
+ * ```
22
+ */
23
+ export function createRef<T extends HTMLElement = HTMLElement>(): RefObject<T> {
24
+ return { current: null }
25
+ }
@@ -0,0 +1,12 @@
1
+ import { batch as baseBatch, untrack as baseUntrack } from './signal'
2
+
3
+ export function batch<T>(fn: () => T): T {
4
+ return baseBatch(fn)
5
+ }
6
+
7
+ export function untrack<T>(fn: () => T): T {
8
+ return baseUntrack(fn)
9
+ }
10
+
11
+ // Transition APIs for priority scheduling
12
+ export { startTransition, useTransition, useDeferredValue } from './transition'