@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 CHANGED
@@ -1,19 +1,91 @@
1
- <img src="https://raw.githubusercontent.com/UnSetSoft/Ryunixjs/canary/assets/logo.png" width="200" height="200" style="
2
- display: block;
3
- margin: 0 auto;" />
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
- ## RyunixJS [![npm version](https://img.shields.io/npm/v/@unsetsoft/ryunixjs.svg?style=flat)](https://www.npmjs.com/package/@unsetsoft/ryunixjs)[![npm version](https://img.shields.io/npm/v/@unsetsoft/ryunixjs/canary.svg?style=flat)](https://www.npmjs.com/package/@unsetsoft/ryunixjs/v/canary)
5
+ <h1 align="center">@unsetsoft/ryunixjs</h1>
6
6
 
7
- ### What is RyunixJS?
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
- Like React, NextJS, Preact, Vite. Ryunix allows you to build static websites from JavaScript in a similar way to the aforementioned frameworks. However, Ryunix is planned to be completely standalone, i.e. without including React internally. This way allowing it to be more manageable and moldable for each developer. The reactivity of Ryunix is similar to Preact, however, it does not pretend to follow any standard of React or any similar Framework, but to allow to generate an SPA in its own way.
16
+ ---
10
17
 
11
- ### Usage
18
+ ## 🧠 What is the Core?
12
19
 
13
- `npx @unsetsoft/cra@latest <my-app>`
20
+ `@unsetsoft/ryunixjs` is the **foundational engine** of the RyunixJS framework. It's a lightweight, high-performance library responsible for:
14
21
 
15
- ### Do you want to contribute?
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
- You can make any change as long as it does not affect the `canary` branches. Make changes that are important, necessary or to add something new if you see it necessary, include your proposal before in an issue and then create a PR referencing that issue.
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
- To be able to work more comfortably you should create a branch with this name `gh/[user]/[branch name]`, all changes should always go to the canary version. Once the changes are applied and no problems are detected, they will become part of the nightly version for further testing and finally the final version will be released. make each change with a simple descriptive message, and remember not to change the version of the mono repo or packages, such changes are made manually when a new update is about to be made.
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).
@@ -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, nextProps[name]);
589
+ dom.setAttribute(attrName, validatedValue);
571
590
  } else {
572
- dom[name] = nextProps[name];
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, nextProps[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 props = JSON.parse(container.getAttribute('data-props') || '{}');
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(value)}"`;
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
- attributes += ` ${toSvgAttrName(key)}="${escapeHtml(value)}"`;
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
- push(`<script>${RC_SCRIPT}</script>`);
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
  });