@unsetsoft/ryunixjs 1.2.5-canary.6 → 1.2.5-canary.7

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/dist/Ryunix.js DELETED
@@ -1,1521 +0,0 @@
1
- ;(function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined'
3
- ? factory(exports)
4
- : typeof define === 'function' && define.amd
5
- ? define(['exports'], factory)
6
- : ((global =
7
- typeof globalThis !== 'undefined' ? globalThis : global || self),
8
- factory((global.Ryunix = {})))
9
- })(this, function (exports) {
10
- 'use strict'
11
-
12
- // Improved state management - avoid global mutable object
13
- // Instead, create a state manager that can be instantiated per render tree
14
-
15
- const createRenderState = () => ({
16
- containerRoot: null,
17
- nextUnitOfWork: null,
18
- currentRoot: null,
19
- wipRoot: null,
20
- deletions: [],
21
- wipFiber: null,
22
- hookIndex: 0,
23
- effects: [],
24
- })
25
-
26
- // Singleton for backward compatibility, but allows testing with isolated instances
27
- let globalState = createRenderState()
28
-
29
- const getState = () => globalState
30
-
31
- // Use const for regex to prevent accidental modification
32
- const CAMEL_TO_KEBAB_REGEX = /[A-Z]/g
33
-
34
- const RYUNIX_TYPES = Object.freeze({
35
- TEXT_ELEMENT: Symbol.for('ryunix.text.element'),
36
- RYUNIX_ELEMENT: Symbol.for('ryunix.element'),
37
- RYUNIX_EFFECT: Symbol.for('ryunix.effect'),
38
- RYUNIX_MEMO: Symbol.for('ryunix.memo'),
39
- RYUNIX_URL_QUERY: Symbol.for('ryunix.urlQuery'),
40
- RYUNIX_REF: Symbol.for('ryunix.ref'),
41
- RYUNIX_STORE: Symbol.for('ryunix.store'),
42
- RYUNIX_REDUCE: Symbol.for('ryunix.reduce'),
43
- RYUNIX_FRAGMENT: Symbol.for('ryunix.fragment'),
44
- RYUNIX_CONTEXT: Symbol.for('ryunix.context'),
45
- })
46
-
47
- const STRINGS = Object.freeze({
48
- OBJECT: 'object',
49
- FUNCTION: 'function',
50
- STYLE: 'ryunix-style',
51
- CLASS_NAME: 'ryunix-class',
52
- CHILDREN: 'children',
53
- BOOLEAN: 'boolean',
54
- STRING: 'string',
55
- UNDEFINED: 'undefined',
56
- })
57
-
58
- const OLD_STRINGS = Object.freeze({
59
- STYLE: 'style',
60
- CLASS_NAME: 'className',
61
- })
62
-
63
- const EFFECT_TAGS = Object.freeze({
64
- PLACEMENT: Symbol.for('ryunix.reconciler.status.placement'),
65
- UPDATE: Symbol.for('ryunix.reconciler.status.update'),
66
- DELETION: Symbol.for('ryunix.reconciler.status.deletion'),
67
- NO_EFFECT: Symbol.for('ryunix.reconciler.status.no_effect'),
68
- })
69
-
70
- /**
71
- * Type checking utilities
72
- */
73
- const is = {
74
- object: (val) => val !== null && typeof val === STRINGS.OBJECT,
75
- function: (val) => typeof val === STRINGS.FUNCTION,
76
- string: (val) => typeof val === STRINGS.STRING,
77
- undefined: (val) => typeof val === STRINGS.UNDEFINED,
78
- null: (val) => val === null,
79
- array: (val) => Array.isArray(val),
80
- promise: (val) => val instanceof Promise,
81
- }
82
-
83
- /**
84
- * Create text element
85
- */
86
- const createTextElement = (text) => {
87
- return {
88
- type: RYUNIX_TYPES.TEXT_ELEMENT,
89
- props: {
90
- nodeValue: text,
91
- children: [],
92
- },
93
- }
94
- }
95
-
96
- /**
97
- * Create element
98
- */
99
- const createElement = (type, props, ...children) => {
100
- const safeProps = props || {}
101
-
102
- return {
103
- type,
104
- props: {
105
- ...safeProps,
106
- children: children
107
- .flat()
108
- .map((child) =>
109
- typeof child === STRINGS.OBJECT ? child : createTextElement(child),
110
- ),
111
- },
112
- }
113
- }
114
-
115
- /**
116
- * Fragment component
117
- */
118
- const Fragment = (props) => {
119
- const children = Array.isArray(props.children)
120
- ? props.children
121
- : [props.children]
122
- return createElement(RYUNIX_TYPES.RYUNIX_FRAGMENT, {}, ...children)
123
- }
124
-
125
- /**
126
- * Check if a key is an event handler
127
- * @param {string} key - Prop key
128
- * @returns {boolean}
129
- */
130
- const isEvent = (key) => key.startsWith('on')
131
-
132
- /**
133
- * Check if a key is a property (not children or event)
134
- * @param {string} key - Prop key
135
- * @returns {boolean}
136
- */
137
- const isProperty = (key) => key !== STRINGS.CHILDREN && !isEvent(key)
138
-
139
- /**
140
- * Check if a property is new or changed
141
- * @param {Object} prev - Previous props
142
- * @param {Object} next - Next props
143
- * @returns {Function}
144
- */
145
- const isNew = (prev, next) => (key) => {
146
- // Use Object.is for better comparison (handles NaN, -0, +0)
147
- return !Object.is(prev[key], next[key])
148
- }
149
-
150
- /**
151
- * Check if a property was removed
152
- * @param {Object} next - Next props
153
- * @returns {Function}
154
- */
155
- const isGone = (next) => (key) => !(key in next)
156
-
157
- /**
158
- * Cancel effects for a single fiber
159
- * @param {Object} fiber - Fiber node
160
- */
161
- const cancelEffects = (fiber) => {
162
- if (!fiber?.hooks?.length) return
163
-
164
- fiber.hooks
165
- .filter(
166
- (hook) =>
167
- hook.type === RYUNIX_TYPES.RYUNIX_EFFECT && is.function(hook.cancel),
168
- )
169
- .forEach((hook) => {
170
- try {
171
- hook.cancel()
172
- hook.cancel = null // Clear reference to prevent memory leaks
173
- } catch (error) {
174
- if (process.env.NODE_ENV !== 'production') {
175
- console.error('Error in effect cleanup:', error)
176
- }
177
- }
178
- })
179
- }
180
-
181
- /**
182
- * Recursively cancel effects in fiber tree
183
- * @param {Object} fiber - Root fiber node
184
- */
185
- const cancelEffectsDeep = (fiber) => {
186
- if (!fiber) return
187
-
188
- // Cancel effects for current fiber
189
- if (fiber.hooks?.length > 0) {
190
- fiber.hooks
191
- .filter(
192
- (hook) =>
193
- hook.type === RYUNIX_TYPES.RYUNIX_EFFECT &&
194
- is.function(hook.cancel),
195
- )
196
- .forEach((hook) => {
197
- try {
198
- hook.cancel()
199
- hook.cancel = null // Clear reference
200
- } catch (error) {
201
- if (process.env.NODE_ENV !== 'production') {
202
- console.error('Error in deep effect cleanup:', error)
203
- }
204
- }
205
- })
206
- }
207
-
208
- // Recursively process children
209
- if (fiber.child) cancelEffectsDeep(fiber.child)
210
- if (fiber.sibling) cancelEffectsDeep(fiber.sibling)
211
- }
212
-
213
- /**
214
- * Run effects for a fiber
215
- * @param {Object} fiber - Fiber node
216
- */
217
- const runEffects = (fiber) => {
218
- if (!fiber?.hooks?.length) return
219
-
220
- for (let i = 0; i < fiber.hooks.length; i++) {
221
- const hook = fiber.hooks[i]
222
-
223
- if (
224
- hook.type === RYUNIX_TYPES.RYUNIX_EFFECT &&
225
- is.function(hook.effect)
226
- ) {
227
- // Cancel previous cleanup if exists
228
- if (is.function(hook.cancel)) {
229
- try {
230
- hook.cancel()
231
- } catch (error) {
232
- if (process.env.NODE_ENV !== 'production') {
233
- console.error('Error in effect cleanup:', error)
234
- }
235
- }
236
- }
237
-
238
- // Run new effect
239
- try {
240
- const cleanup = hook.effect()
241
-
242
- // Store cleanup function if returned
243
- if (is.function(cleanup)) {
244
- hook.cancel = cleanup
245
- } else {
246
- hook.cancel = null
247
- }
248
- } catch (error) {
249
- if (process.env.NODE_ENV !== 'production') {
250
- console.error('Error in effect:', error)
251
- }
252
- hook.cancel = null
253
- }
254
-
255
- // Clear effect reference after running
256
- hook.effect = null
257
- }
258
- }
259
- }
260
-
261
- /**
262
- * Convert camelCase to kebab-case for CSS properties
263
- * @param {string} camelCase - CamelCase string
264
- * @returns {string} Kebab-case string
265
- */
266
- const camelToKebab = (camelCase) => {
267
- return camelCase.replace(
268
- CAMEL_TO_KEBAB_REGEX,
269
- (match) => `-${match.toLowerCase()}`,
270
- )
271
- }
272
-
273
- /**
274
- * Apply styles to DOM element
275
- * @param {HTMLElement} dom - DOM element
276
- * @param {Object} styleObj - Style object
277
- */
278
- const applyStyles = (dom, styleObj) => {
279
- if (!is.object(styleObj) || is.null(styleObj)) {
280
- dom.style.cssText = ''
281
- return
282
- }
283
-
284
- try {
285
- const cssText = Object.entries(styleObj)
286
- .filter(([_, value]) => value != null) // Filter out null/undefined
287
- .map(([key, value]) => {
288
- const kebabKey = camelToKebab(key)
289
- return `${kebabKey}: ${value}`
290
- })
291
- .join('; ')
292
-
293
- dom.style.cssText = cssText
294
- } catch (error) {
295
- if (process.env.NODE_ENV !== 'production') {
296
- console.error('Error applying styles:', error)
297
- }
298
- }
299
- }
300
-
301
- /**
302
- * Apply CSS classes to DOM element
303
- * @param {HTMLElement} dom - DOM element
304
- * @param {string} prevClasses - Previous class string
305
- * @param {string} nextClasses - Next class string
306
- */
307
- const applyClasses = (dom, prevClasses, nextClasses) => {
308
- // Allow empty/undefined - just remove classes
309
- if (!nextClasses || nextClasses.trim() === '') {
310
- if (prevClasses) {
311
- const oldClasses = prevClasses.split(/\s+/).filter(Boolean)
312
- dom.classList.remove(...oldClasses)
313
- }
314
- return
315
- }
316
-
317
- // Remove old classes
318
- if (prevClasses) {
319
- const oldClasses = prevClasses.split(/\s+/).filter(Boolean)
320
- dom.classList.remove(...oldClasses)
321
- }
322
-
323
- // Add new classes
324
- const newClasses = nextClasses.split(/\s+/).filter(Boolean)
325
- if (newClasses.length > 0) {
326
- dom.classList.add(...newClasses)
327
- }
328
- }
329
-
330
- /**
331
- * Create a DOM element from fiber
332
- * @param {Object} fiber - Fiber node
333
- * @returns {HTMLElement|Text|null}
334
- */
335
- const createDom = (fiber) => {
336
- // Fragments don't create real DOM nodes
337
- if (fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
338
- return null
339
- }
340
-
341
- let dom
342
-
343
- try {
344
- if (fiber.type === RYUNIX_TYPES.TEXT_ELEMENT) {
345
- dom = document.createTextNode('')
346
- } else if (is.string(fiber.type)) {
347
- dom = document.createElement(fiber.type)
348
- } else {
349
- if (process.env.NODE_ENV !== 'production') {
350
- console.warn(
351
- 'Attempted to create DOM for non-host component:',
352
- fiber.type,
353
- )
354
- }
355
- return null
356
- }
357
-
358
- updateDom(dom, {}, fiber.props)
359
- return dom
360
- } catch (error) {
361
- if (process.env.NODE_ENV !== 'production') {
362
- console.error('Error creating DOM element:', error, fiber)
363
- }
364
- return null
365
- }
366
- }
367
-
368
- /**
369
- * Update DOM element with new props
370
- * @param {HTMLElement|Text} dom - DOM element
371
- * @param {Object} prevProps - Previous props
372
- * @param {Object} nextProps - Next props
373
- */
374
- const updateDom = (dom, prevProps = {}, nextProps = {}) => {
375
- // Remove old event listeners
376
- Object.keys(prevProps)
377
- .filter(isEvent)
378
- .filter(
379
- (key) => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key),
380
- )
381
- .forEach((name) => {
382
- const eventType = name.toLowerCase().substring(2)
383
- try {
384
- dom.removeEventListener(eventType, prevProps[name])
385
- } catch (error) {
386
- if (process.env.NODE_ENV !== 'production') {
387
- console.warn('Error removing event listener:', error)
388
- }
389
- }
390
- })
391
-
392
- // Remove old properties
393
- Object.keys(prevProps)
394
- .filter(isProperty)
395
- .filter(isGone(nextProps))
396
- .forEach((name) => {
397
- // Skip special properties
398
- if (
399
- [
400
- STRINGS.STYLE,
401
- OLD_STRINGS.STYLE,
402
- STRINGS.CLASS_NAME,
403
- OLD_STRINGS.CLASS_NAME,
404
- ].includes(name)
405
- ) {
406
- return
407
- }
408
- dom[name] = ''
409
- })
410
-
411
- // Set new properties
412
- Object.keys(nextProps)
413
- .filter(isProperty)
414
- .filter(isNew(prevProps, nextProps))
415
- .forEach((name) => {
416
- try {
417
- // Handle style properties
418
- if (name === STRINGS.STYLE || name === OLD_STRINGS.STYLE) {
419
- const styleValue = nextProps[name]
420
- applyStyles(dom, styleValue)
421
- }
422
- // Handle className properties
423
- else if (name === STRINGS.CLASS_NAME) {
424
- applyClasses(
425
- dom,
426
- prevProps[STRINGS.CLASS_NAME],
427
- nextProps[STRINGS.CLASS_NAME],
428
- )
429
- } else if (name === OLD_STRINGS.CLASS_NAME) {
430
- applyClasses(
431
- dom,
432
- prevProps[OLD_STRINGS.CLASS_NAME],
433
- nextProps[OLD_STRINGS.CLASS_NAME],
434
- )
435
- }
436
- // Handle other properties
437
- else {
438
- // Special handling for value and checked (controlled components)
439
- if (name === 'value' || name === 'checked') {
440
- if (dom[name] !== nextProps[name]) {
441
- dom[name] = nextProps[name]
442
- }
443
- } else {
444
- dom[name] = nextProps[name]
445
- }
446
- }
447
- } catch (error) {
448
- if (process.env.NODE_ENV !== 'production') {
449
- console.warn(`Error setting property ${name}:`, error)
450
- }
451
- }
452
- })
453
-
454
- // Add new event listeners
455
- Object.keys(nextProps)
456
- .filter(isEvent)
457
- .filter(isNew(prevProps, nextProps))
458
- .forEach((name) => {
459
- const eventType = name.toLowerCase().substring(2)
460
- try {
461
- dom.addEventListener(eventType, nextProps[name])
462
- } catch (error) {
463
- if (process.env.NODE_ENV !== 'production') {
464
- console.warn('Error adding event listener:', error)
465
- }
466
- }
467
- })
468
- }
469
-
470
- const commitRoot = () => {
471
- const state = getState()
472
- state.deletions.forEach(commitWork)
473
- commitWork(state.wipRoot.child)
474
- state.currentRoot = state.wipRoot
475
- state.wipRoot = null
476
- }
477
-
478
- const commitWork = (fiber) => {
479
- if (!fiber) {
480
- return
481
- }
482
-
483
- let domParentFiber = fiber.parent
484
- while (!domParentFiber.dom) {
485
- domParentFiber = domParentFiber.parent
486
- }
487
- const domParent = domParentFiber.dom
488
-
489
- if (fiber.effectTag === EFFECT_TAGS.PLACEMENT) {
490
- if (fiber.dom != null) {
491
- domParent.appendChild(fiber.dom)
492
- }
493
- runEffects(fiber)
494
- } else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
495
- cancelEffects(fiber)
496
- if (fiber.dom != null) {
497
- updateDom(fiber.dom, fiber.alternate.props, fiber.props)
498
- }
499
- runEffects(fiber)
500
- } else if (fiber.effectTag === EFFECT_TAGS.DELETION) {
501
- cancelEffectsDeep(fiber)
502
- commitDeletion(fiber, domParent)
503
- return
504
- }
505
-
506
- commitWork(fiber.child)
507
- commitWork(fiber.sibling)
508
- }
509
-
510
- const commitDeletion = (fiber, domParent) => {
511
- if (fiber.dom) {
512
- domParent.removeChild(fiber.dom)
513
- } else {
514
- let child = fiber.child
515
- while (child) {
516
- commitDeletion(child, domParent)
517
- child = child.sibling
518
- }
519
- }
520
- }
521
-
522
- /**
523
- * Reconcile children with key optimization
524
- */
525
- const reconcileChildren = (wipFiber, elements) => {
526
- const state = getState()
527
- let index = 0
528
- let prevSibling
529
-
530
- // Build map of old fibers by key/index
531
- const oldFiberMap = new Map()
532
- let oldFiber = wipFiber.alternate?.child
533
- let position = 0
534
-
535
- while (oldFiber) {
536
- const key = oldFiber.key ?? `__index_${position}__`
537
- oldFiberMap.set(key, oldFiber)
538
- oldFiber = oldFiber.sibling
539
- position++
540
- }
541
-
542
- // Process new elements
543
- while (index < elements.length) {
544
- const element = elements[index]
545
- if (!element) {
546
- index++
547
- continue
548
- }
549
-
550
- const key = element.key ?? `__index_${index}__`
551
- const matchedFiber = oldFiberMap.get(key)
552
-
553
- let newFiber
554
- const sameType = matchedFiber && element.type === matchedFiber.type
555
-
556
- if (sameType) {
557
- // Update existing fiber
558
- newFiber = {
559
- type: matchedFiber.type,
560
- props: element.props,
561
- dom: matchedFiber.dom,
562
- parent: wipFiber,
563
- alternate: matchedFiber,
564
- effectTag: EFFECT_TAGS.UPDATE,
565
- hooks: matchedFiber.hooks,
566
- key: element.key,
567
- }
568
- oldFiberMap.delete(key)
569
- } else {
570
- // Create new fiber
571
- newFiber = {
572
- type: element.type,
573
- props: element.props,
574
- dom: null,
575
- parent: wipFiber,
576
- alternate: null,
577
- effectTag: EFFECT_TAGS.PLACEMENT,
578
- key: element.key,
579
- }
580
-
581
- // Mark matched fiber for deletion if exists
582
- if (matchedFiber) {
583
- matchedFiber.effectTag = EFFECT_TAGS.DELETION
584
- state.deletions.push(matchedFiber)
585
- oldFiberMap.delete(key)
586
- }
587
- }
588
-
589
- // Link fibers
590
- if (index === 0) {
591
- wipFiber.child = newFiber
592
- } else if (newFiber) {
593
- prevSibling.sibling = newFiber
594
- }
595
-
596
- prevSibling = newFiber
597
- index++
598
- }
599
-
600
- // Delete remaining old fibers
601
- oldFiberMap.forEach((fiber) => {
602
- fiber.effectTag = EFFECT_TAGS.DELETION
603
- state.deletions.push(fiber)
604
- })
605
- }
606
-
607
- const updateFunctionComponent = (fiber) => {
608
- const state = getState()
609
- state.wipFiber = fiber
610
- state.hookIndex = 0
611
- state.wipFiber.hooks = []
612
-
613
- const children = [fiber.type(fiber.props)]
614
-
615
- if (fiber.type._contextId && fiber.props.value !== undefined) {
616
- fiber._contextId = fiber.type._contextId
617
- fiber._contextValue = fiber.props.value
618
- }
619
-
620
- reconcileChildren(fiber, children)
621
- }
622
-
623
- const updateHostComponent = (fiber) => {
624
- if (!fiber.dom) {
625
- fiber.dom = createDom(fiber)
626
- }
627
- const children = fiber.props?.children || []
628
- reconcileChildren(fiber, children)
629
- }
630
-
631
- const Image = ({ src, ...props }) => {
632
- return createElement('img', { ...props, src })
633
- }
634
-
635
- /**
636
- * Priority levels for updates
637
- */
638
- const Priority = {
639
- IMMEDIATE: 1, // User input (clicks, typing)
640
- USER_BLOCKING: 2, // Hover, scroll
641
- NORMAL: 3, // Data fetching
642
- LOW: 4, // Analytics
643
- IDLE: 5, // Background tasks
644
- }
645
-
646
- Priority.NORMAL
647
-
648
- /**
649
- * Performance profiler for Ryunix
650
- */
651
- class Profiler {
652
- constructor() {
653
- this.enabled = process.env.NODE_ENV !== 'production'
654
- this.measures = new Map()
655
- this.renderTimes = []
656
- this.maxSamples = 100
657
- }
658
-
659
- startMeasure(name) {
660
- if (!this.enabled) return
661
- this.measures.set(name, performance.now())
662
- }
663
-
664
- endMeasure(name) {
665
- if (!this.enabled) return
666
- const start = this.measures.get(name)
667
- if (!start) return
668
-
669
- const duration = performance.now() - start
670
- this.measures.delete(name)
671
-
672
- return duration
673
- }
674
-
675
- recordRender(componentName, duration) {
676
- if (!this.enabled) return
677
-
678
- this.renderTimes.push({
679
- component: componentName,
680
- duration,
681
- timestamp: Date.now(),
682
- })
683
-
684
- if (this.renderTimes.length > this.maxSamples) {
685
- this.renderTimes.shift()
686
- }
687
- }
688
-
689
- getStats() {
690
- if (!this.enabled) return null
691
-
692
- const total = this.renderTimes.reduce((sum, r) => sum + r.duration, 0)
693
- const avg = total / this.renderTimes.length
694
- const max = Math.max(...this.renderTimes.map((r) => r.duration))
695
- const min = Math.min(...this.renderTimes.map((r) => r.duration))
696
-
697
- return { total, avg, max, min, count: this.renderTimes.length }
698
- }
699
-
700
- getSlowestComponents(limit = 10) {
701
- if (!this.enabled) return []
702
-
703
- const byComponent = new Map()
704
-
705
- this.renderTimes.forEach(({ component, duration }) => {
706
- if (!byComponent.has(component)) {
707
- byComponent.set(component, { total: 0, count: 0, max: 0 })
708
- }
709
- const stats = byComponent.get(component)
710
- stats.total += duration
711
- stats.count++
712
- stats.max = Math.max(stats.max, duration)
713
- })
714
-
715
- return Array.from(byComponent.entries())
716
- .map(([name, stats]) => ({
717
- name,
718
- avg: stats.total / stats.count,
719
- max: stats.max,
720
- count: stats.count,
721
- }))
722
- .sort((a, b) => b.avg - a.avg)
723
- .slice(0, limit)
724
- }
725
-
726
- logStats() {
727
- if (!this.enabled) return
728
-
729
- const stats = this.getStats()
730
- if (!stats) return
731
-
732
- console.group('🔍 Ryunix Performance Stats')
733
- console.log(`Total renders: ${stats.count}`)
734
- console.log(`Avg render time: ${stats.avg.toFixed(2)}ms`)
735
- console.log(
736
- `Min: ${stats.min.toFixed(2)}ms | Max: ${stats.max.toFixed(2)}ms`,
737
- )
738
-
739
- const slowest = this.getSlowestComponents(5)
740
- if (slowest.length > 0) {
741
- console.log('\n⚠️ Slowest components:')
742
- slowest.forEach((comp, i) => {
743
- console.log(
744
- `${i + 1}. ${comp.name}: ${comp.avg.toFixed(2)}ms avg (${comp.count} renders)`,
745
- )
746
- })
747
- }
748
- console.groupEnd()
749
- }
750
-
751
- clear() {
752
- this.renderTimes = []
753
- this.measures.clear()
754
- }
755
-
756
- enable() {
757
- this.enabled = true
758
- }
759
-
760
- disable() {
761
- this.enabled = false
762
- }
763
- }
764
-
765
- // Global profiler instance
766
- const profiler = new Profiler()
767
-
768
- /**
769
- * Hook to profile component render
770
- */
771
- const useProfiler = (componentName) => {
772
- const startTime = performance.now()
773
-
774
- return () => {
775
- const duration = performance.now() - startTime
776
- profiler.recordRender(componentName, duration)
777
- }
778
- }
779
-
780
- /**
781
- * HOC to profile component
782
- */
783
- const withProfiler = (Component, name) => {
784
- return (props) => {
785
- profiler.startMeasure(name)
786
- const result = Component(props)
787
- const duration = profiler.endMeasure(name)
788
- if (duration) profiler.recordRender(name, duration)
789
- return result
790
- }
791
- }
792
-
793
- const workLoop = (deadline) => {
794
- const state = getState()
795
- let shouldYield = false
796
-
797
- while (state.nextUnitOfWork && !shouldYield) {
798
- state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork)
799
- shouldYield = deadline.timeRemaining() < 1
800
- }
801
-
802
- if (!state.nextUnitOfWork && state.wipRoot) {
803
- commitRoot()
804
- }
805
-
806
- requestIdleCallback(workLoop)
807
- }
808
-
809
- requestIdleCallback(workLoop)
810
-
811
- const performUnitOfWork = (fiber) => {
812
- const componentName =
813
- fiber.type?.name || fiber.type?.displayName || 'Unknown'
814
-
815
- profiler.startMeasure(componentName)
816
-
817
- const isFunctionComponent = fiber.type instanceof Function
818
- if (isFunctionComponent) {
819
- updateFunctionComponent(fiber)
820
- } else {
821
- updateHostComponent(fiber)
822
- }
823
-
824
- const duration = profiler.endMeasure(componentName)
825
- if (duration) profiler.recordRender(componentName, duration)
826
-
827
- if (fiber.child) {
828
- return fiber.child
829
- }
830
- let nextFiber = fiber
831
- while (nextFiber) {
832
- if (nextFiber.sibling) {
833
- return nextFiber.sibling
834
- }
835
- nextFiber = nextFiber.parent
836
- }
837
- }
838
-
839
- const scheduleWork = (root, priority = Priority.NORMAL) => {
840
- const state = getState()
841
- state.nextUnitOfWork = root
842
- state.wipRoot = root
843
- state.deletions = []
844
- state.hookIndex = 0
845
- state.effects = []
846
-
847
- // Higher priority = faster scheduling
848
- if (priority <= Priority.USER_BLOCKING) {
849
- requestIdleCallback(workLoop)
850
- } else {
851
- setTimeout(() => requestIdleCallback(workLoop), 0)
852
- }
853
- }
854
-
855
- const render = (element, container) => {
856
- const state = getState()
857
- state.wipRoot = {
858
- dom: container,
859
- props: {
860
- children: [element],
861
- },
862
- alternate: state.currentRoot,
863
- }
864
-
865
- state.nextUnitOfWork = state.wipRoot
866
- state.deletions = []
867
- scheduleWork(state.wipRoot)
868
- return state.wipRoot
869
- }
870
-
871
- const init = (MainElement, root = '__ryunix') => {
872
- const state = getState()
873
- state.containerRoot = document.getElementById(root)
874
- const renderProcess = render(MainElement, state.containerRoot)
875
- return renderProcess
876
- }
877
-
878
- const safeRender = (component, props, onError) => {
879
- try {
880
- return component(props)
881
- } catch (error) {
882
- if (process.env.NODE_ENV !== 'production') {
883
- console.error('Component error:', error)
884
- }
885
- if (onError) onError(error)
886
- return null
887
- }
888
- }
889
-
890
- const validateHookCall = () => {
891
- const state = getState()
892
- if (!state.wipFiber) {
893
- throw new Error(
894
- 'Hooks can only be called inside the body of a function component.',
895
- )
896
- }
897
- if (!Array.isArray(state.wipFiber.hooks)) {
898
- state.wipFiber.hooks = []
899
- }
900
- }
901
-
902
- const haveDepsChanged = (oldDeps, newDeps) => {
903
- if (!oldDeps || !newDeps) return true
904
- if (oldDeps.length !== newDeps.length) return true
905
- return oldDeps.some((dep, i) => !Object.is(dep, newDeps[i]))
906
- }
907
-
908
- const useStore = (initialState) => {
909
- const reducer = (state, action) =>
910
- is.function(action) ? action(state) : action
911
- return useReducer(reducer, initialState)
912
- }
913
-
914
- const useReducer = (reducer, initialState, init) => {
915
- validateHookCall()
916
-
917
- const state = getState()
918
- const { wipFiber, hookIndex } = state
919
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex]
920
-
921
- const hook = {
922
- hookID: hookIndex,
923
- type: RYUNIX_TYPES.RYUNIX_STORE,
924
- state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
925
- queue: [], // Siempre nueva cola vacía
926
- }
927
-
928
- // Procesar acciones del render anterior
929
- if (oldHook?.queue) {
930
- oldHook.queue.forEach((action) => {
931
- try {
932
- hook.state = reducer(hook.state, action)
933
- } catch (error) {
934
- if (process.env.NODE_ENV !== 'production') {
935
- console.error('Error in reducer:', error)
936
- }
937
- }
938
- })
939
- }
940
-
941
- const dispatch = (action) => {
942
- if (action === undefined) {
943
- if (process.env.NODE_ENV !== 'production') {
944
- console.warn('dispatch called with undefined action')
945
- }
946
- return
947
- }
948
-
949
- hook.queue.push(action)
950
-
951
- const currentState = getState()
952
- currentState.wipRoot = {
953
- dom: currentState.currentRoot.dom,
954
- props: currentState.currentRoot.props,
955
- alternate: currentState.currentRoot,
956
- }
957
- currentState.deletions = []
958
- currentState.hookIndex = 0
959
- scheduleWork(currentState.wipRoot)
960
- }
961
-
962
- wipFiber.hooks[hookIndex] = hook
963
- state.hookIndex++
964
- return [hook.state, dispatch]
965
- }
966
-
967
- const useEffect = (callback, deps) => {
968
- validateHookCall()
969
-
970
- if (!is.function(callback)) {
971
- throw new Error('useEffect callback must be a function')
972
- }
973
- if (deps !== undefined && !Array.isArray(deps)) {
974
- throw new Error('useEffect dependencies must be an array or undefined')
975
- }
976
-
977
- const state = getState()
978
- const { wipFiber, hookIndex } = state
979
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex]
980
- const hasChanged = haveDepsChanged(oldHook?.deps, deps)
981
-
982
- const hook = {
983
- hookID: hookIndex,
984
- type: RYUNIX_TYPES.RYUNIX_EFFECT,
985
- deps,
986
- effect: hasChanged ? callback : null,
987
- cancel: oldHook?.cancel,
988
- }
989
-
990
- wipFiber.hooks[hookIndex] = hook
991
- state.hookIndex++
992
- }
993
-
994
- const useRef = (initialValue) => {
995
- validateHookCall()
996
-
997
- const state = getState()
998
- const { wipFiber, hookIndex } = state
999
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex]
1000
-
1001
- const hook = {
1002
- hookID: hookIndex,
1003
- type: RYUNIX_TYPES.RYUNIX_REF,
1004
- value: oldHook ? oldHook.value : { current: initialValue },
1005
- }
1006
-
1007
- wipFiber.hooks[hookIndex] = hook
1008
- state.hookIndex++
1009
- return hook.value
1010
- }
1011
-
1012
- const useMemo = (compute, deps) => {
1013
- validateHookCall()
1014
-
1015
- if (!is.function(compute)) {
1016
- throw new Error('useMemo callback must be a function')
1017
- }
1018
- if (!Array.isArray(deps)) {
1019
- throw new Error('useMemo requires a dependencies array')
1020
- }
1021
-
1022
- const state = getState()
1023
- const { wipFiber, hookIndex } = state
1024
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex]
1025
-
1026
- let value
1027
- if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
1028
- value = oldHook.value
1029
- } else {
1030
- try {
1031
- value = compute()
1032
- } catch (error) {
1033
- if (process.env.NODE_ENV !== 'production') {
1034
- console.error('Error in useMemo computation:', error)
1035
- }
1036
- value = undefined
1037
- }
1038
- }
1039
-
1040
- const hook = {
1041
- hookID: hookIndex,
1042
- type: RYUNIX_TYPES.RYUNIX_MEMO,
1043
- value,
1044
- deps,
1045
- }
1046
-
1047
- wipFiber.hooks[hookIndex] = hook
1048
- state.hookIndex++
1049
- return value
1050
- }
1051
-
1052
- const useCallback = (callback, deps) => {
1053
- if (!is.function(callback)) {
1054
- throw new Error('useCallback requires a function as first argument')
1055
- }
1056
- return useMemo(() => callback, deps)
1057
- }
1058
-
1059
- const createContext = (
1060
- contextId = RYUNIX_TYPES.RYUNIX_CONTEXT,
1061
- defaultValue = {},
1062
- ) => {
1063
- const Provider = ({ children, value }) => {
1064
- const element = Fragment({ children })
1065
- element._contextId = contextId
1066
- element._contextValue = value
1067
- return element
1068
- }
1069
-
1070
- Provider._contextId = contextId
1071
-
1072
- const useContext = (ctxID = contextId) => {
1073
- validateHookCall()
1074
-
1075
- const state = getState()
1076
- let fiber = state.wipFiber
1077
-
1078
- while (fiber) {
1079
- if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
1080
- return fiber._contextValue
1081
- }
1082
- if (
1083
- fiber.type?._contextId === ctxID &&
1084
- fiber.props?.value !== undefined
1085
- ) {
1086
- return fiber.props.value
1087
- }
1088
- fiber = fiber.parent
1089
- }
1090
- return defaultValue
1091
- }
1092
-
1093
- return { Provider, useContext }
1094
- }
1095
-
1096
- const useQuery = () => {
1097
- if (typeof window === 'undefined') return {}
1098
-
1099
- const searchParams = new URLSearchParams(window.location.search)
1100
- const query = {}
1101
- for (const [key, value] of searchParams.entries()) {
1102
- query[key] = value
1103
- }
1104
- return query
1105
- }
1106
-
1107
- const useHash = () => {
1108
- if (typeof window === 'undefined') return ''
1109
-
1110
- const [hash, setHash] = useStore(window.location.hash)
1111
- useEffect(() => {
1112
- const onHashChange = () => setHash(window.location.hash)
1113
- window.addEventListener('hashchange', onHashChange)
1114
- return () => window.removeEventListener('hashchange', onHashChange)
1115
- }, [])
1116
- return hash
1117
- }
1118
-
1119
- const useMetadata = (tags = {}, options = {}) => {
1120
- useEffect(() => {
1121
- if (typeof document === 'undefined') return
1122
-
1123
- let finalTitle = 'Ryunix App'
1124
- const template = options.title?.template
1125
- const defaultTitle = options.title?.prefix || 'Ryunix App'
1126
- const pageTitle = tags.pageTitle || tags.title
1127
-
1128
- if (is.string(pageTitle) && pageTitle.trim()) {
1129
- finalTitle = template?.includes('%s')
1130
- ? template.replace('%s', pageTitle)
1131
- : pageTitle
1132
- } else {
1133
- finalTitle = defaultTitle
1134
- }
1135
-
1136
- document.title = finalTitle
1137
-
1138
- if (tags.canonical) {
1139
- let link = document.querySelector('link[rel="canonical"]')
1140
- if (!link) {
1141
- link = document.createElement('link')
1142
- link.setAttribute('rel', 'canonical')
1143
- document.head.appendChild(link)
1144
- }
1145
- link.setAttribute('href', tags.canonical)
1146
- }
1147
-
1148
- Object.entries(tags).forEach(([key, value]) => {
1149
- if (['title', 'pageTitle', 'canonical'].includes(key)) return
1150
-
1151
- const isProperty = key.startsWith('og:') || key.startsWith('twitter:')
1152
- const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`
1153
- let meta = document.head.querySelector(selector)
1154
-
1155
- if (!meta) {
1156
- meta = document.createElement('meta')
1157
- meta.setAttribute(isProperty ? 'property' : 'name', key)
1158
- document.head.appendChild(meta)
1159
- }
1160
- meta.setAttribute('content', value)
1161
- })
1162
- }, [JSON.stringify(tags), JSON.stringify(options)])
1163
- }
1164
-
1165
- // Router Context
1166
- const RouterContext = createContext('ryunix.navigation', {
1167
- location: '/',
1168
- params: {},
1169
- query: {},
1170
- navigate: (path) => {},
1171
- route: null,
1172
- })
1173
-
1174
- const findRoute = (routes, path) => {
1175
- const pathname = path.split('?')[0].split('#')[0]
1176
- const notFoundRoute = routes.find((route) => route.NotFound)
1177
- const notFound = notFoundRoute
1178
- ? { route: { component: notFoundRoute.NotFound }, params: {} }
1179
- : { route: { component: null }, params: {} }
1180
-
1181
- for (const route of routes) {
1182
- if (route.subRoutes) {
1183
- const childRoute = findRoute(route.subRoutes, path)
1184
- if (childRoute) return childRoute
1185
- }
1186
- if (route.path === '*') return notFound
1187
- if (!route.path || typeof route.path !== 'string') continue
1188
-
1189
- const keys = []
1190
- const pattern = new RegExp(
1191
- `^${route.path.replace(/:\w+/g, (match) => {
1192
- keys.push(match.substring(1))
1193
- return '([^/]+)'
1194
- })}$`,
1195
- )
1196
-
1197
- const match = pathname.match(pattern)
1198
- if (match) {
1199
- const params = keys.reduce((acc, key, index) => {
1200
- acc[key] = match[index + 1]
1201
- return acc
1202
- }, {})
1203
- return { route, params }
1204
- }
1205
- }
1206
- return notFound
1207
- }
1208
-
1209
- const RouterProvider = ({ routes, children }) => {
1210
- const [location, setLocation] = useStore(window.location.pathname)
1211
-
1212
- useEffect(() => {
1213
- const update = () => setLocation(window.location.pathname)
1214
- window.addEventListener('popstate', update)
1215
- window.addEventListener('hashchange', update)
1216
- return () => {
1217
- window.removeEventListener('popstate', update)
1218
- window.removeEventListener('hashchange', update)
1219
- }
1220
- }, [location])
1221
-
1222
- const navigate = (path) => {
1223
- window.history.pushState({}, '', path)
1224
- setLocation(path)
1225
- }
1226
-
1227
- const currentRouteData = findRoute(routes, location) || {}
1228
- const query = useQuery()
1229
-
1230
- const contextValue = {
1231
- location,
1232
- params: currentRouteData.params || {},
1233
- query,
1234
- navigate,
1235
- route: currentRouteData.route,
1236
- }
1237
-
1238
- return createElement(
1239
- RouterContext.Provider,
1240
- { value: contextValue },
1241
- Fragment({ children }),
1242
- )
1243
- }
1244
-
1245
- const useRouter = () => {
1246
- return RouterContext.useContext('ryunix.navigation')
1247
- }
1248
-
1249
- const Children = () => {
1250
- const { route, params, query, location } = useRouter()
1251
- if (!route || !route.component) return null
1252
- const hash = useHash()
1253
-
1254
- useEffect(() => {
1255
- if (hash) {
1256
- const id = hash.slice(1)
1257
- const el = document.getElementById(id)
1258
- if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' })
1259
- }
1260
- }, [hash])
1261
-
1262
- return createElement(route.component, {
1263
- key: location,
1264
- params,
1265
- query,
1266
- hash,
1267
- })
1268
- }
1269
-
1270
- const NavLink = ({ to, exact = false, ...props }) => {
1271
- const { location, navigate } = useRouter()
1272
- const isActive = exact ? location === to : location.startsWith(to)
1273
-
1274
- const resolveClass = (cls) =>
1275
- typeof cls === 'function' ? cls({ isActive }) : cls || ''
1276
-
1277
- const handleClick = (e) => {
1278
- e.preventDefault()
1279
- navigate(to)
1280
- }
1281
-
1282
- const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className'
1283
- const classAttrValue = resolveClass(
1284
- props['ryunix-class'] || props['className'],
1285
- )
1286
-
1287
- const {
1288
- ['ryunix-class']: _omitRyunix,
1289
- className: _omitClassName,
1290
- ...cleanedProps
1291
- } = props
1292
-
1293
- return createElement(
1294
- 'a',
1295
- {
1296
- href: to,
1297
- onClick: handleClick,
1298
- [classAttrName]: classAttrValue,
1299
- ...cleanedProps,
1300
- },
1301
- props.children,
1302
- )
1303
- }
1304
-
1305
- /**
1306
- * useStore with priority support
1307
- */
1308
- const useStorePriority = (initialState) => {
1309
- const reducer = (state, action) =>
1310
- typeof action === 'function' ? action.value(state) : action.value
1311
-
1312
- const [state, baseDispatch] = useReducer(reducer, initialState)
1313
-
1314
- const dispatch = (action, priority = Priority.NORMAL) => {
1315
- const wrappedAction = {
1316
- value: action,
1317
- priority,
1318
- }
1319
-
1320
- baseDispatch(wrappedAction)
1321
- }
1322
-
1323
- return [state, dispatch]
1324
- }
1325
-
1326
- /**
1327
- * useTransition - Mark updates as non-urgent
1328
- */
1329
- const useTransition = () => {
1330
- const [isPending, setIsPending] = useStorePriority(false)
1331
-
1332
- const startTransition = (callback) => {
1333
- setIsPending(true, Priority.IMMEDIATE)
1334
-
1335
- setTimeout(() => {
1336
- callback()
1337
- setIsPending(false, Priority.IMMEDIATE)
1338
- }, 0)
1339
- }
1340
-
1341
- return [isPending, startTransition]
1342
- }
1343
-
1344
- /**
1345
- * useDeferredValue - Defer value updates
1346
- */
1347
- const useDeferredValue = (value) => {
1348
- const [deferredValue, setDeferredValue] = useStorePriority(value)
1349
-
1350
- useEffect(() => {
1351
- const timeout = setTimeout(() => {
1352
- setDeferredValue(value, Priority.LOW)
1353
- }, 100)
1354
-
1355
- return () => clearTimeout(timeout)
1356
- }, [value])
1357
-
1358
- return deferredValue
1359
- }
1360
-
1361
- var hooks = /*#__PURE__*/ Object.freeze({
1362
- __proto__: null,
1363
- Children: Children,
1364
- NavLink: NavLink,
1365
- RouterProvider: RouterProvider,
1366
- createContext: createContext,
1367
- useCallback: useCallback,
1368
- useDeferredValue: useDeferredValue,
1369
- useEffect: useEffect,
1370
- useHash: useHash,
1371
- useMemo: useMemo,
1372
- useMetadata: useMetadata,
1373
- useQuery: useQuery,
1374
- useReducer: useReducer,
1375
- useRef: useRef,
1376
- useRouter: useRouter,
1377
- useStore: useStore,
1378
- useStorePriority: useStorePriority,
1379
- useTransition: useTransition,
1380
- })
1381
-
1382
- /**
1383
- * memo - Memoize component to prevent unnecessary re-renders
1384
- */
1385
- const memo = (Component, arePropsEqual) => {
1386
- return (props) => {
1387
- const memoizedElement = useMemo(() => {
1388
- return Component(props)
1389
- }, [
1390
- // Default comparison: shallow props comparison
1391
- ...Object.values(props),
1392
- ])
1393
-
1394
- return memoizedElement
1395
- }
1396
- }
1397
-
1398
- /**
1399
- * Lazy load component
1400
- */
1401
- const lazy = (importFn) => {
1402
- let Component = null
1403
- let promise = null
1404
- let error = null
1405
-
1406
- return (props) => {
1407
- const [, forceUpdate] = useStore(0)
1408
-
1409
- useEffect(() => {
1410
- if (Component || error) return
1411
-
1412
- if (!promise) {
1413
- promise = importFn()
1414
- .then((module) => {
1415
- Component = module.default || module
1416
- forceUpdate((x) => x + 1)
1417
- })
1418
- .catch((err) => {
1419
- error = err
1420
- forceUpdate((x) => x + 1)
1421
- })
1422
- }
1423
- }, [])
1424
-
1425
- if (error) throw error
1426
- if (!Component) return null
1427
- return createElement(Component, props)
1428
- }
1429
- }
1430
-
1431
- /**
1432
- * Suspense component (basic implementation)
1433
- */
1434
- const Suspense = ({ fallback, children }) => {
1435
- const [isLoading, setIsLoading] = useStore(true)
1436
-
1437
- useEffect(() => {
1438
- setIsLoading(false)
1439
- }, [])
1440
-
1441
- if (isLoading && fallback) {
1442
- return fallback
1443
- }
1444
-
1445
- return children
1446
- }
1447
-
1448
- let isBatching = false
1449
- let pendingUpdates = []
1450
-
1451
- /**
1452
- * Batch multiple state updates into single render
1453
- */
1454
- const batchUpdates = (callback) => {
1455
- const wasBatching = isBatching
1456
- isBatching = true
1457
-
1458
- try {
1459
- callback()
1460
- } finally {
1461
- isBatching = wasBatching
1462
-
1463
- if (!isBatching && pendingUpdates.length > 0) {
1464
- flushUpdates()
1465
- }
1466
- }
1467
- }
1468
-
1469
- /**
1470
- * Flush all pending updates
1471
- */
1472
- const flushUpdates = () => {
1473
- if (pendingUpdates.length === 0) return
1474
-
1475
- const updates = pendingUpdates
1476
- pendingUpdates = []
1477
-
1478
- // Execute all updates
1479
- updates.forEach((update) => update())
1480
- }
1481
-
1482
- var Ryunix = /*#__PURE__*/ Object.freeze({
1483
- __proto__: null,
1484
- Fragment: Fragment,
1485
- Hooks: hooks,
1486
- Priority: Priority,
1487
- Suspense: Suspense,
1488
- batchUpdates: batchUpdates,
1489
- createElement: createElement,
1490
- init: init,
1491
- lazy: lazy,
1492
- memo: memo,
1493
- profiler: profiler,
1494
- render: render,
1495
- safeRender: safeRender,
1496
- useProfiler: useProfiler,
1497
- withProfiler: withProfiler,
1498
- })
1499
-
1500
- window.Ryunix = Ryunix
1501
-
1502
- exports.Fragment = Fragment
1503
- exports.Hooks = hooks
1504
- exports.Image = Image
1505
- exports.Priority = Priority
1506
- exports.Suspense = Suspense
1507
- exports.batchUpdates = batchUpdates
1508
- exports.createElement = createElement
1509
- exports.default = Ryunix
1510
- exports.init = init
1511
- exports.lazy = lazy
1512
- exports.memo = memo
1513
- exports.profiler = profiler
1514
- exports.render = render
1515
- exports.safeRender = safeRender
1516
- exports.useProfiler = useProfiler
1517
- exports.withProfiler = withProfiler
1518
-
1519
- Object.defineProperty(exports, '__esModule', { value: true })
1520
- })
1521
- //# sourceMappingURL=Ryunix.js.map