@unsetsoft/ryunixjs 1.2.4-canary.17 → 1.2.4-canary.18
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/README.md +83 -11
- package/dist/Ryunix.esm.js +46 -11
- package/dist/Ryunix.esm.js.map +1 -1
- package/dist/Ryunix.umd.js +46 -11
- package/dist/Ryunix.umd.js.map +1 -1
- package/dist/Ryunix.umd.min.js +1 -1
- package/dist/Ryunix.umd.min.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,91 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/UnSetSoft/Ryunixjs/canary/assets/logo.png" width="200" height="200" alt="RyunixJS Logo" />
|
|
3
|
+
</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<h1 align="center">@unsetsoft/ryunixjs</h1>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@unsetsoft/ryunixjs">
|
|
9
|
+
<img src="https://img.shields.io/npm/v/@unsetsoft/ryunixjs.svg?style=flat-square" alt="npm version" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@unsetsoft/ryunixjs/v/canary">
|
|
12
|
+
<img src="https://img.shields.io/npm/v/@unsetsoft/ryunixjs/canary.svg?style=flat-square&label=canary" alt="canary version" />
|
|
13
|
+
</a>
|
|
14
|
+
</p>
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
---
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
## 🧠 What is the Core?
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
`@unsetsoft/ryunixjs` is the **foundational engine** of the RyunixJS framework. It's a lightweight, high-performance library responsible for:
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
- **Reconciliation & Fiber Architecture**: Efficiently updating the DOM by comparing Virtual DOM trees.
|
|
23
|
+
- **Hooks System**: Providing the core state and lifecycle management (e.g., `useStore`, `useEffect`).
|
|
24
|
+
- **Concurrent Rendering**: Support for prioritized updates and transitions.
|
|
25
|
+
- **SSR & Hydration**: Server-Side Rendering engines for both Node.js and Edge environments.
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
While you can use it standalone for custom integrations, it is typically used via `@unsetsoft/ryunix-presets` and our official CLI.
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
## 🚀 Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @unsetsoft/ryunixjs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 🛠️ API Reference
|
|
36
|
+
|
|
37
|
+
### Core Functions
|
|
38
|
+
|
|
39
|
+
- `createElement(type, props, ...children)`: Creates a Vitual DOM element.
|
|
40
|
+
- `render(element, container)`: Renders a Ryunix element into the DOM.
|
|
41
|
+
- `hydrate(element, container)`: Hydrates server-rendered HTML.
|
|
42
|
+
- `init()`: Initializes the Ryunix state.
|
|
43
|
+
|
|
44
|
+
### Hooks
|
|
45
|
+
|
|
46
|
+
| Hook | Description |
|
|
47
|
+
| :--- | :--- |
|
|
48
|
+
| `useStore(initial)` | State management (Ryunix equivalent of `useState`). |
|
|
49
|
+
| `useReducer(reducer, initial)` | Advanced state management with reducers. |
|
|
50
|
+
| `useEffect(cb, deps)` | Side effects management. |
|
|
51
|
+
| `useLayoutEffect(cb, deps)` | Synchronous side effects before browser paint. |
|
|
52
|
+
| `useRef(initial)` | Persistent reference across renders. |
|
|
53
|
+
| `useMemo(cb, deps)` | Memoized values. |
|
|
54
|
+
| `useCallback(cb, deps)` | Memoized functions. |
|
|
55
|
+
| `useContext(id)` | Consumes a context value. |
|
|
56
|
+
| `useId()` | Generates unique, stable IDs for SSR. |
|
|
57
|
+
|
|
58
|
+
### Specialized Hooks
|
|
59
|
+
|
|
60
|
+
- `usePersistentStore(key, initial)`: Automatically syncs state with `localStorage`.
|
|
61
|
+
- `useSwitch(initial)`: Optimized toggle state hook.
|
|
62
|
+
- `useDebounce(value, delay)`: Debounces a value update.
|
|
63
|
+
- `useThrottle(value, interval)`: Throttles value updates.
|
|
64
|
+
- `useQuery()` / `useHash()`: Reactive URL parameters and hash.
|
|
65
|
+
|
|
66
|
+
### Concurrency & Server-Side
|
|
67
|
+
|
|
68
|
+
- **Prioritized Updates**: Use `useTransition` and `useDeferredValue` to manage non-urgent UI updates.
|
|
69
|
+
- **SSR Engine**: `renderToString` and `renderToReadableStream` for flexible server rendering.
|
|
70
|
+
- **Boundaries**: `ServerBoundary` and `ErrorBoundary` for resilient applications.
|
|
71
|
+
|
|
72
|
+
## 🏗️ Example Usage
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
import { render, useStore, createElement } from "@unsetsoft/ryunixjs";
|
|
76
|
+
|
|
77
|
+
function App() {
|
|
78
|
+
const [count, setCount] = useStore(0);
|
|
79
|
+
|
|
80
|
+
return createElement("div", null,
|
|
81
|
+
createElement("h1", null, `Count: ${count}`),
|
|
82
|
+
createElement("button", { onClick: () => setCount(count + 1) }, "Increment")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(createElement(App), document.getElementById("root"));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 📄 License
|
|
90
|
+
|
|
91
|
+
RyunixJS is [MIT Licensed](file:///e:/proyects/Ryunixjs/LICENSE).
|
package/dist/Ryunix.esm.js
CHANGED
|
@@ -477,6 +477,24 @@ const createDom = (fiber) => {
|
|
|
477
477
|
}
|
|
478
478
|
};
|
|
479
479
|
|
|
480
|
+
const validateUri = (name, value) => {
|
|
481
|
+
if (typeof value !== 'string') return value
|
|
482
|
+
const attr = name.toLowerCase();
|
|
483
|
+
if (attr !== 'href' && attr !== 'src' && attr !== 'action' && attr !== 'formaction') {
|
|
484
|
+
return value
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const normalized = value.replace(/\s+/g, '').toLowerCase();
|
|
488
|
+
if (normalized.startsWith('javascript:') || normalized.startsWith('vbscript:')) {
|
|
489
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
490
|
+
console.warn(`[Ryunix Security] Blocked dangerous ${name} URI: ${value}`);
|
|
491
|
+
}
|
|
492
|
+
return 'javascript:void(0)'
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return value
|
|
496
|
+
};
|
|
497
|
+
|
|
480
498
|
/**
|
|
481
499
|
* Update DOM element with new props
|
|
482
500
|
* @param {HTMLElement|Text} dom - DOM element
|
|
@@ -566,13 +584,15 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
|
|
|
566
584
|
const isSvgNode = dom instanceof SVGElement;
|
|
567
585
|
if (isSvgNode) {
|
|
568
586
|
const attrName = toSvgAttrName(name);
|
|
587
|
+
const validatedValue = validateUri(attrName, nextProps[name]);
|
|
569
588
|
// viewBox is case sensitive, we respect the camelCase for it.
|
|
570
|
-
dom.setAttribute(attrName,
|
|
589
|
+
dom.setAttribute(attrName, validatedValue);
|
|
571
590
|
} else {
|
|
572
|
-
|
|
591
|
+
const validatedValue = validateUri(name, nextProps[name]);
|
|
592
|
+
dom[name] = validatedValue;
|
|
573
593
|
// Best effort: set html attributes if it's not a primitive component property
|
|
574
594
|
if (typeof nextProps[name] !== 'object' && typeof nextProps[name] !== 'function') {
|
|
575
|
-
dom.setAttribute(name,
|
|
595
|
+
dom.setAttribute(name, validatedValue);
|
|
576
596
|
}
|
|
577
597
|
}
|
|
578
598
|
}
|
|
@@ -2603,6 +2623,16 @@ const hydrate = (element, container) => {
|
|
|
2603
2623
|
return root
|
|
2604
2624
|
};
|
|
2605
2625
|
|
|
2626
|
+
const sanitizeProps = (props) => {
|
|
2627
|
+
if (!props || typeof props !== 'object' || Array.isArray(props)) return props
|
|
2628
|
+
const sanitized = {};
|
|
2629
|
+
for (const key in props) {
|
|
2630
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
|
|
2631
|
+
sanitized[key] = props[key];
|
|
2632
|
+
}
|
|
2633
|
+
return sanitized
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2606
2636
|
/**
|
|
2607
2637
|
* The `init` function initializes a rendering process for a main element within a specified container
|
|
2608
2638
|
* root element.
|
|
@@ -2635,7 +2665,8 @@ const hydrateIslands = (components = {}, hasMainElement = false) => {
|
|
|
2635
2665
|
return
|
|
2636
2666
|
}
|
|
2637
2667
|
try {
|
|
2638
|
-
const
|
|
2668
|
+
const rawProps = JSON.parse(container.getAttribute('data-props') || '{}');
|
|
2669
|
+
const props = sanitizeProps(rawProps);
|
|
2639
2670
|
hydrate(createElement(Component, props), container);
|
|
2640
2671
|
} catch (e) {
|
|
2641
2672
|
console.error(`[Ryunix Islands] Error hydrating island "${id}":`, e);
|
|
@@ -2800,8 +2831,8 @@ const renderToStringImpl = (element) => {
|
|
|
2800
2831
|
if (value) attributes += ` ${key}=""`;
|
|
2801
2832
|
} else if (value != null) {
|
|
2802
2833
|
let attrName = toSvgAttrName(key);
|
|
2803
|
-
|
|
2804
|
-
attributes += ` ${attrName}="${escapeHtml(
|
|
2834
|
+
let validatedValue = validateUri(attrName, value);
|
|
2835
|
+
attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
|
|
2805
2836
|
}
|
|
2806
2837
|
}
|
|
2807
2838
|
});
|
|
@@ -2982,7 +3013,9 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
|
|
|
2982
3013
|
if (typeof value === 'boolean') {
|
|
2983
3014
|
if (value) attributes += ` ${key}=""`;
|
|
2984
3015
|
} else if (value != null) {
|
|
2985
|
-
|
|
3016
|
+
const attrName = toSvgAttrName(key);
|
|
3017
|
+
const validatedValue = validateUri(attrName, value);
|
|
3018
|
+
attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
|
|
2986
3019
|
}
|
|
2987
3020
|
}
|
|
2988
3021
|
});
|
|
@@ -3006,7 +3039,7 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
|
|
|
3006
3039
|
}
|
|
3007
3040
|
};
|
|
3008
3041
|
|
|
3009
|
-
const renderToReadableStream = (element) => {
|
|
3042
|
+
const renderToReadableStream = (element, options = {}) => {
|
|
3010
3043
|
const state = getState();
|
|
3011
3044
|
const encoder = new TextEncoder();
|
|
3012
3045
|
|
|
@@ -3021,7 +3054,8 @@ const renderToReadableStream = (element) => {
|
|
|
3021
3054
|
|
|
3022
3055
|
try {
|
|
3023
3056
|
// 0. Inject RC helper script first
|
|
3024
|
-
|
|
3057
|
+
const nonceAttr = options.nonce ? ` nonce="${options.nonce}"` : '';
|
|
3058
|
+
push(`<script${nonceAttr}>${RC_SCRIPT}</script>`);
|
|
3025
3059
|
|
|
3026
3060
|
// 1. Render initial tree (with fallbacks)
|
|
3027
3061
|
await renderToStreamImpl(element, push, suspenseTasks);
|
|
@@ -3034,7 +3068,7 @@ const renderToReadableStream = (element) => {
|
|
|
3034
3068
|
const res = await task;
|
|
3035
3069
|
if (res.success) {
|
|
3036
3070
|
push(`<template id="P:${res.id}">${res.content}</template>`);
|
|
3037
|
-
push(`<script>$RC("S:${res.id}", "P:${res.id}")</script>`);
|
|
3071
|
+
push(`<script${nonceAttr}>$RC("S:${res.id}", "P:${res.id}")</script>`);
|
|
3038
3072
|
}
|
|
3039
3073
|
}
|
|
3040
3074
|
|
|
@@ -3048,7 +3082,7 @@ const renderToReadableStream = (element) => {
|
|
|
3048
3082
|
})
|
|
3049
3083
|
};
|
|
3050
3084
|
|
|
3051
|
-
const renderToString = (element) => {
|
|
3085
|
+
const renderToString = (element, options = {}) => {
|
|
3052
3086
|
const state = getState();
|
|
3053
3087
|
const wasServerRendering = state.isServerRendering;
|
|
3054
3088
|
state.isServerRendering = true;
|
|
@@ -3305,6 +3339,7 @@ function createActionProxy(actionId) {
|
|
|
3305
3339
|
method: 'POST',
|
|
3306
3340
|
headers: {
|
|
3307
3341
|
'Content-Type': 'application/json',
|
|
3342
|
+
'X-Ryunix-Action': 'true',
|
|
3308
3343
|
},
|
|
3309
3344
|
body: JSON.stringify({ actionId, args }),
|
|
3310
3345
|
});
|