@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,456 @@
1
+ /**
2
+ * Fict DOM Constants
3
+ *
4
+ * Property constants and configurations for DOM attribute handling.
5
+ * Borrowed from dom-expressions for comprehensive DOM support.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Boolean Attributes
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Complete list of boolean attributes (lowercase)
14
+ * These attributes are set as empty strings when true, removed when false
15
+ */
16
+ const booleans = [
17
+ 'allowfullscreen',
18
+ 'async',
19
+ 'alpha', // HTMLInputElement
20
+ 'autofocus', // HTMLElement prop
21
+ 'autoplay',
22
+ 'checked',
23
+ 'controls',
24
+ 'default',
25
+ 'disabled',
26
+ 'formnovalidate',
27
+ 'hidden', // HTMLElement prop
28
+ 'indeterminate',
29
+ 'inert', // HTMLElement prop
30
+ 'ismap',
31
+ 'loop',
32
+ 'multiple',
33
+ 'muted',
34
+ 'nomodule',
35
+ 'novalidate',
36
+ 'open',
37
+ 'playsinline',
38
+ 'readonly',
39
+ 'required',
40
+ 'reversed',
41
+ 'seamless', // HTMLIframeElement - non-standard
42
+ 'selected',
43
+ // Experimental attributes
44
+ 'adauctionheaders',
45
+ 'browsingtopics',
46
+ 'credentialless',
47
+ 'defaultchecked',
48
+ 'defaultmuted',
49
+ 'defaultselected',
50
+ 'defer',
51
+ 'disablepictureinpicture',
52
+ 'disableremoteplayback',
53
+ 'preservespitch',
54
+ 'shadowrootclonable',
55
+ 'shadowrootcustomelementregistry',
56
+ 'shadowrootdelegatesfocus',
57
+ 'shadowrootserializable',
58
+ 'sharedstoragewritable',
59
+ ] as const
60
+
61
+ export const BooleanAttributes = new Set<string>(booleans)
62
+
63
+ // ============================================================================
64
+ // Properties Set
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Properties that should be set via DOM property (not attribute)
69
+ * Includes camelCase versions of boolean attributes
70
+ */
71
+ export const Properties = new Set<string>([
72
+ // Core properties
73
+ 'className',
74
+ 'value',
75
+
76
+ // CamelCase booleans
77
+ 'readOnly',
78
+ 'noValidate',
79
+ 'formNoValidate',
80
+ 'isMap',
81
+ 'noModule',
82
+ 'playsInline',
83
+
84
+ // Experimental (camelCase)
85
+ 'adAuctionHeaders',
86
+ 'allowFullscreen',
87
+ 'browsingTopics',
88
+ 'defaultChecked',
89
+ 'defaultMuted',
90
+ 'defaultSelected',
91
+ 'disablePictureInPicture',
92
+ 'disableRemotePlayback',
93
+ 'preservesPitch',
94
+ 'shadowRootClonable',
95
+ 'shadowRootCustomElementRegistry',
96
+ 'shadowRootDelegatesFocus',
97
+ 'shadowRootSerializable',
98
+ 'sharedStorageWritable',
99
+
100
+ // All lowercase booleans
101
+ ...booleans,
102
+ ])
103
+
104
+ // ============================================================================
105
+ // Child Properties
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Properties that represent children/content
110
+ */
111
+ export const ChildProperties = new Set<string>([
112
+ 'innerHTML',
113
+ 'textContent',
114
+ 'innerText',
115
+ 'children',
116
+ ])
117
+
118
+ // ============================================================================
119
+ // Property Aliases
120
+ // ============================================================================
121
+
122
+ /**
123
+ * React compatibility aliases (className -> class)
124
+ */
125
+ export const Aliases: Record<string, string> = {
126
+ className: 'class',
127
+ htmlFor: 'for',
128
+ }
129
+
130
+ /**
131
+ * Element-specific property aliases
132
+ * Maps lowercase attribute names to their camelCase property equivalents
133
+ * Only for specific elements that have these properties
134
+ */
135
+ export const PropAliases: Record<
136
+ string,
137
+ string | { $: string; [tagName: string]: string | number }
138
+ > = {
139
+ // Direct mapping
140
+ class: 'className',
141
+
142
+ // Element-specific mappings
143
+ novalidate: {
144
+ $: 'noValidate',
145
+ FORM: 1,
146
+ },
147
+ formnovalidate: {
148
+ $: 'formNoValidate',
149
+ BUTTON: 1,
150
+ INPUT: 1,
151
+ },
152
+ ismap: {
153
+ $: 'isMap',
154
+ IMG: 1,
155
+ },
156
+ nomodule: {
157
+ $: 'noModule',
158
+ SCRIPT: 1,
159
+ },
160
+ playsinline: {
161
+ $: 'playsInline',
162
+ VIDEO: 1,
163
+ },
164
+ readonly: {
165
+ $: 'readOnly',
166
+ INPUT: 1,
167
+ TEXTAREA: 1,
168
+ },
169
+
170
+ // Experimental element-specific
171
+ adauctionheaders: {
172
+ $: 'adAuctionHeaders',
173
+ IFRAME: 1,
174
+ },
175
+ allowfullscreen: {
176
+ $: 'allowFullscreen',
177
+ IFRAME: 1,
178
+ },
179
+ browsingtopics: {
180
+ $: 'browsingTopics',
181
+ IMG: 1,
182
+ },
183
+ defaultchecked: {
184
+ $: 'defaultChecked',
185
+ INPUT: 1,
186
+ },
187
+ defaultmuted: {
188
+ $: 'defaultMuted',
189
+ AUDIO: 1,
190
+ VIDEO: 1,
191
+ },
192
+ defaultselected: {
193
+ $: 'defaultSelected',
194
+ OPTION: 1,
195
+ },
196
+ disablepictureinpicture: {
197
+ $: 'disablePictureInPicture',
198
+ VIDEO: 1,
199
+ },
200
+ disableremoteplayback: {
201
+ $: 'disableRemotePlayback',
202
+ AUDIO: 1,
203
+ VIDEO: 1,
204
+ },
205
+ preservespitch: {
206
+ $: 'preservesPitch',
207
+ AUDIO: 1,
208
+ VIDEO: 1,
209
+ },
210
+ shadowrootclonable: {
211
+ $: 'shadowRootClonable',
212
+ TEMPLATE: 1,
213
+ },
214
+ shadowrootdelegatesfocus: {
215
+ $: 'shadowRootDelegatesFocus',
216
+ TEMPLATE: 1,
217
+ },
218
+ shadowrootserializable: {
219
+ $: 'shadowRootSerializable',
220
+ TEMPLATE: 1,
221
+ },
222
+ sharedstoragewritable: {
223
+ $: 'sharedStorageWritable',
224
+ IFRAME: 1,
225
+ IMG: 1,
226
+ },
227
+ }
228
+
229
+ /**
230
+ * Get the property alias for a given attribute and tag name
231
+ */
232
+ export function getPropAlias(prop: string, tagName: string): string | undefined {
233
+ const a = PropAliases[prop]
234
+ if (typeof a === 'object') {
235
+ return a[tagName] ? a['$'] : undefined
236
+ }
237
+ return a
238
+ }
239
+
240
+ // ============================================================================
241
+ // Event Delegation
242
+ // ============================================================================
243
+
244
+ /**
245
+ * Symbol for storing delegated events on the document
246
+ */
247
+ export const $$EVENTS = '_$FICT_DELEGATE'
248
+
249
+ /**
250
+ * Events that should use event delegation for performance
251
+ * These events bubble and are commonly used across many elements
252
+ */
253
+ export const DelegatedEvents = new Set<string>([
254
+ 'beforeinput',
255
+ 'click',
256
+ 'dblclick',
257
+ 'contextmenu',
258
+ 'focusin',
259
+ 'focusout',
260
+ 'input',
261
+ 'keydown',
262
+ 'keyup',
263
+ 'mousedown',
264
+ 'mousemove',
265
+ 'mouseout',
266
+ 'mouseover',
267
+ 'mouseup',
268
+ 'pointerdown',
269
+ 'pointermove',
270
+ 'pointerout',
271
+ 'pointerover',
272
+ 'pointerup',
273
+ 'touchend',
274
+ 'touchmove',
275
+ 'touchstart',
276
+ ])
277
+
278
+ // ============================================================================
279
+ // SVG Support
280
+ // ============================================================================
281
+
282
+ /**
283
+ * SVG element names (excluding common ones that overlap with HTML)
284
+ */
285
+ export const SVGElements = new Set<string>([
286
+ 'altGlyph',
287
+ 'altGlyphDef',
288
+ 'altGlyphItem',
289
+ 'animate',
290
+ 'animateColor',
291
+ 'animateMotion',
292
+ 'animateTransform',
293
+ 'circle',
294
+ 'clipPath',
295
+ 'color-profile',
296
+ 'cursor',
297
+ 'defs',
298
+ 'desc',
299
+ 'ellipse',
300
+ 'feBlend',
301
+ 'feColorMatrix',
302
+ 'feComponentTransfer',
303
+ 'feComposite',
304
+ 'feConvolveMatrix',
305
+ 'feDiffuseLighting',
306
+ 'feDisplacementMap',
307
+ 'feDistantLight',
308
+ 'feDropShadow',
309
+ 'feFlood',
310
+ 'feFuncA',
311
+ 'feFuncB',
312
+ 'feFuncG',
313
+ 'feFuncR',
314
+ 'feGaussianBlur',
315
+ 'feImage',
316
+ 'feMerge',
317
+ 'feMergeNode',
318
+ 'feMorphology',
319
+ 'feOffset',
320
+ 'fePointLight',
321
+ 'feSpecularLighting',
322
+ 'feSpotLight',
323
+ 'feTile',
324
+ 'feTurbulence',
325
+ 'filter',
326
+ 'font',
327
+ 'font-face',
328
+ 'font-face-format',
329
+ 'font-face-name',
330
+ 'font-face-src',
331
+ 'font-face-uri',
332
+ 'foreignObject',
333
+ 'g',
334
+ 'glyph',
335
+ 'glyphRef',
336
+ 'hkern',
337
+ 'image',
338
+ 'line',
339
+ 'linearGradient',
340
+ 'marker',
341
+ 'mask',
342
+ 'metadata',
343
+ 'missing-glyph',
344
+ 'mpath',
345
+ 'path',
346
+ 'pattern',
347
+ 'polygon',
348
+ 'polyline',
349
+ 'radialGradient',
350
+ 'rect',
351
+ 'set',
352
+ 'stop',
353
+ 'svg',
354
+ 'switch',
355
+ 'symbol',
356
+ 'text',
357
+ 'textPath',
358
+ 'tref',
359
+ 'tspan',
360
+ 'use',
361
+ 'view',
362
+ 'vkern',
363
+ ])
364
+
365
+ /**
366
+ * SVG attribute namespaces
367
+ */
368
+ export const SVGNamespace: Record<string, string> = {
369
+ xlink: 'http://www.w3.org/1999/xlink',
370
+ xml: 'http://www.w3.org/XML/1998/namespace',
371
+ }
372
+
373
+ // ============================================================================
374
+ // Unitless CSS Properties
375
+ // ============================================================================
376
+
377
+ /**
378
+ * CSS properties that don't need a unit (like 'px')
379
+ */
380
+ export const UnitlessStyles = new Set<string>([
381
+ 'animationIterationCount',
382
+ 'animation-iteration-count',
383
+ 'borderImageOutset',
384
+ 'border-image-outset',
385
+ 'borderImageSlice',
386
+ 'border-image-slice',
387
+ 'borderImageWidth',
388
+ 'border-image-width',
389
+ 'boxFlex',
390
+ 'box-flex',
391
+ 'boxFlexGroup',
392
+ 'box-flex-group',
393
+ 'boxOrdinalGroup',
394
+ 'box-ordinal-group',
395
+ 'columnCount',
396
+ 'column-count',
397
+ 'columns',
398
+ 'flex',
399
+ 'flexGrow',
400
+ 'flex-grow',
401
+ 'flexPositive',
402
+ 'flex-positive',
403
+ 'flexShrink',
404
+ 'flex-shrink',
405
+ 'flexNegative',
406
+ 'flex-negative',
407
+ 'flexOrder',
408
+ 'flex-order',
409
+ 'gridRow',
410
+ 'grid-row',
411
+ 'gridRowEnd',
412
+ 'grid-row-end',
413
+ 'gridRowSpan',
414
+ 'grid-row-span',
415
+ 'gridRowStart',
416
+ 'grid-row-start',
417
+ 'gridColumn',
418
+ 'grid-column',
419
+ 'gridColumnEnd',
420
+ 'grid-column-end',
421
+ 'gridColumnSpan',
422
+ 'grid-column-span',
423
+ 'gridColumnStart',
424
+ 'grid-column-start',
425
+ 'fontWeight',
426
+ 'font-weight',
427
+ 'lineClamp',
428
+ 'line-clamp',
429
+ 'lineHeight',
430
+ 'line-height',
431
+ 'opacity',
432
+ 'order',
433
+ 'orphans',
434
+ 'tabSize',
435
+ 'tab-size',
436
+ 'widows',
437
+ 'zIndex',
438
+ 'z-index',
439
+ 'zoom',
440
+ 'fillOpacity',
441
+ 'fill-opacity',
442
+ 'floodOpacity',
443
+ 'flood-opacity',
444
+ 'stopOpacity',
445
+ 'stop-opacity',
446
+ 'strokeDasharray',
447
+ 'stroke-dasharray',
448
+ 'strokeDashoffset',
449
+ 'stroke-dashoffset',
450
+ 'strokeMiterlimit',
451
+ 'stroke-miterlimit',
452
+ 'strokeOpacity',
453
+ 'stroke-opacity',
454
+ 'strokeWidth',
455
+ 'stroke-width',
456
+ ])
@@ -0,0 +1,134 @@
1
+ import { getDevtoolsHook } from './devtools'
2
+
3
+ export interface CycleProtectionOptions {
4
+ maxFlushCyclesPerMicrotask?: number
5
+ maxEffectRunsPerFlush?: number
6
+ windowSize?: number
7
+ highUsageRatio?: number
8
+ maxRootReentrantDepth?: number
9
+ enableWindowWarning?: boolean
10
+ devMode?: boolean
11
+ }
12
+
13
+ interface CycleWindowEntry {
14
+ used: number
15
+ budget: number
16
+ }
17
+
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
+ }
27
+
28
+ let options: Required<CycleProtectionOptions> = {
29
+ ...defaultOptions,
30
+ } as Required<CycleProtectionOptions>
31
+
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
38
+
39
+ export function setCycleProtectionOptions(opts: CycleProtectionOptions): void {
40
+ options = { ...options, ...opts }
41
+ }
42
+
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
+ }
52
+
53
+ export function beginFlushGuard(): void {
54
+ effectRunsThisFlush = 0
55
+ flushWarned = false
56
+ windowWarned = false
57
+ }
58
+
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 })
69
+ }
70
+ return false
71
+ }
72
+ return true
73
+ }
74
+
75
+ export function endFlushGuard(): void {
76
+ recordWindowUsage(effectRunsThisFlush, options.maxFlushCyclesPerMicrotask)
77
+ effectRunsThisFlush = 0
78
+ }
79
+
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)
86
+ }
87
+ if (!rootWarned) {
88
+ rootWarned = true
89
+ console.warn(message, { depth })
90
+ }
91
+ return false
92
+ }
93
+ rootDepth.set(root, depth)
94
+ return true
95
+ }
96
+
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)
104
+ }
105
+ }
106
+
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()
113
+ }
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
+ })
124
+ }
125
+ }
126
+
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 ?? '')
134
+ }
@@ -0,0 +1,17 @@
1
+ export interface FictDevtoolsHook {
2
+ registerSignal: (id: number, value: unknown) => void
3
+ updateSignal: (id: number, value: unknown) => void
4
+ registerEffect: (id: number) => void
5
+ effectRun: (id: number) => void
6
+ cycleDetected?: (payload: { reason: string; detail?: Record<string, unknown> }) => void
7
+ }
8
+
9
+ function getGlobalHook(): FictDevtoolsHook | undefined {
10
+ if (typeof globalThis === 'undefined') return undefined
11
+ return (globalThis as typeof globalThis & { __FICT_DEVTOOLS_HOOK__?: FictDevtoolsHook })
12
+ .__FICT_DEVTOOLS_HOOK__
13
+ }
14
+
15
+ export function getDevtoolsHook(): FictDevtoolsHook | undefined {
16
+ return getGlobalHook()
17
+ }