@fynixorg/ui 1.0.12 → 1.0.14

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/runtime.js CHANGED
@@ -22,6 +22,387 @@ import { nixRef } from "./hooks/nixRef";
22
22
  import { nixState } from "./hooks/nixState";
23
23
  import { nixStore } from "./hooks/nixStore";
24
24
  import createFynix from "./router/router";
25
+ class SimplePriorityQueue {
26
+ constructor() {
27
+ this.items = [];
28
+ this.priorityOrder = {
29
+ immediate: 0,
30
+ high: 1,
31
+ normal: 2,
32
+ low: 3,
33
+ idle: 4,
34
+ };
35
+ }
36
+ push(item, priority) {
37
+ this.items.push({ item, priority });
38
+ this.items.sort((a, b) => this.priorityOrder[a.priority] - this.priorityOrder[b.priority]);
39
+ }
40
+ pop() {
41
+ return this.items.shift()?.item;
42
+ }
43
+ peek() {
44
+ return this.items[0]?.item;
45
+ }
46
+ size() {
47
+ return this.items.length;
48
+ }
49
+ isEmpty() {
50
+ return this.items.length === 0;
51
+ }
52
+ }
53
+ class FynixScheduler {
54
+ constructor() {
55
+ this.updateQueue = new SimplePriorityQueue();
56
+ this.batchedUpdates = new Set();
57
+ this.isScheduled = false;
58
+ this.isWorking = false;
59
+ this.currentPriority = "normal";
60
+ this.updateIdCounter = 0;
61
+ }
62
+ schedule(update, priority = "normal") {
63
+ update.id = `update_${this.updateIdCounter++}`;
64
+ update.priority = priority;
65
+ update.timestamp = performance.now();
66
+ if (priority === "immediate") {
67
+ this.flushUpdate(update);
68
+ }
69
+ else {
70
+ this.updateQueue.push(update, priority);
71
+ this.scheduleWork();
72
+ }
73
+ }
74
+ batchUpdates(updates) {
75
+ updates.forEach((update) => this.batchedUpdates.add(update));
76
+ this.scheduleWork();
77
+ }
78
+ timeSlice(deadline) {
79
+ const startTime = performance.now();
80
+ const previousPriority = this.currentPriority;
81
+ while (!this.updateQueue.isEmpty() &&
82
+ performance.now() - startTime < deadline) {
83
+ const update = this.updateQueue.pop();
84
+ if (update) {
85
+ if (this.shouldYield() && update.priority !== "immediate") {
86
+ this.updateQueue.push(update, update.priority);
87
+ break;
88
+ }
89
+ this.flushUpdate(update);
90
+ }
91
+ }
92
+ this.currentPriority = previousPriority;
93
+ return this.updateQueue.isEmpty();
94
+ }
95
+ flush() {
96
+ if (this.isWorking)
97
+ return;
98
+ this.isWorking = true;
99
+ try {
100
+ while (!this.updateQueue.isEmpty()) {
101
+ const update = this.updateQueue.peek();
102
+ if (update && update.priority === "immediate") {
103
+ this.flushUpdate(this.updateQueue.pop());
104
+ }
105
+ else {
106
+ break;
107
+ }
108
+ }
109
+ this.batchedUpdates.forEach((update) => this.flushUpdate(update));
110
+ this.batchedUpdates.clear();
111
+ }
112
+ finally {
113
+ this.isWorking = false;
114
+ this.isScheduled = false;
115
+ }
116
+ }
117
+ flushUpdate(update) {
118
+ const previousPriority = this.currentPriority;
119
+ this.currentPriority = update.priority;
120
+ try {
121
+ update.callback();
122
+ }
123
+ catch (error) {
124
+ console.error("[FynixScheduler] Update error:", error);
125
+ showErrorOverlay(error);
126
+ }
127
+ finally {
128
+ this.currentPriority = previousPriority;
129
+ }
130
+ }
131
+ scheduleWork() {
132
+ if (this.isScheduled)
133
+ return;
134
+ this.isScheduled = true;
135
+ const nextUpdate = this.updateQueue.peek();
136
+ if (nextUpdate) {
137
+ if (nextUpdate.priority === "high") {
138
+ requestAnimationFrame(() => this.workLoop(16.67));
139
+ }
140
+ else {
141
+ if ("requestIdleCallback" in window) {
142
+ requestIdleCallback((deadline) => {
143
+ this.workLoop(deadline.timeRemaining());
144
+ });
145
+ }
146
+ else {
147
+ setTimeout(() => this.workLoop(5), 0);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ workLoop(deadline) {
153
+ const hasMoreWork = !this.timeSlice(deadline);
154
+ if (hasMoreWork) {
155
+ this.isScheduled = false;
156
+ this.scheduleWork();
157
+ }
158
+ else {
159
+ this.flush();
160
+ }
161
+ }
162
+ getCurrentPriority() {
163
+ return this.currentPriority;
164
+ }
165
+ shouldYield() {
166
+ const nextUpdate = this.updateQueue.peek();
167
+ if (!nextUpdate)
168
+ return false;
169
+ const currentPriorityLevel = this.getPriorityLevel(this.currentPriority);
170
+ const nextPriorityLevel = this.getPriorityLevel(nextUpdate.priority);
171
+ return nextPriorityLevel < currentPriorityLevel;
172
+ }
173
+ getPriorityLevel(priority) {
174
+ const levels = { immediate: 0, high: 1, normal: 2, low: 3, idle: 4 };
175
+ return levels[priority];
176
+ }
177
+ }
178
+ const scheduler = new FynixScheduler();
179
+ class FiberRenderer {
180
+ constructor() {
181
+ this.workInProgressRoot = null;
182
+ this.nextUnitOfWork = null;
183
+ this.currentRoot = null;
184
+ this.deletions = [];
185
+ }
186
+ scheduleWork(fiber) {
187
+ this.workInProgressRoot = {
188
+ ...fiber,
189
+ alternate: this.currentRoot,
190
+ };
191
+ this.nextUnitOfWork = this.workInProgressRoot;
192
+ this.deletions = [];
193
+ scheduler.schedule({
194
+ id: "",
195
+ type: "layout",
196
+ priority: "high",
197
+ callback: () => this.workLoop(5),
198
+ timestamp: performance.now(),
199
+ }, "high");
200
+ }
201
+ workLoop(deadline) {
202
+ const startTime = performance.now();
203
+ while (this.nextUnitOfWork && performance.now() - startTime < deadline) {
204
+ this.nextUnitOfWork = this.performUnitOfWork(this.nextUnitOfWork);
205
+ }
206
+ if (!this.nextUnitOfWork && this.workInProgressRoot) {
207
+ this.commitRoot();
208
+ }
209
+ else if (this.nextUnitOfWork) {
210
+ scheduler.schedule({
211
+ id: "",
212
+ type: "layout",
213
+ priority: "normal",
214
+ callback: () => this.workLoop(5),
215
+ timestamp: performance.now(),
216
+ }, "normal");
217
+ }
218
+ }
219
+ performUnitOfWork(fiber) {
220
+ this.reconcileChildren(fiber, fiber.props?.children || []);
221
+ if (fiber.child) {
222
+ return fiber.child;
223
+ }
224
+ let nextFiber = fiber;
225
+ while (nextFiber) {
226
+ if (nextFiber.sibling) {
227
+ return nextFiber.sibling;
228
+ }
229
+ nextFiber = nextFiber.parent;
230
+ }
231
+ return null;
232
+ }
233
+ reconcileChildren(wipFiber, elements) {
234
+ let index = 0;
235
+ let oldFiber = wipFiber.alternate?.child;
236
+ let prevSibling = null;
237
+ while (index < elements.length || oldFiber != null) {
238
+ const element = elements[index];
239
+ let newFiber = null;
240
+ const sameType = oldFiber && element && element.type === oldFiber.type;
241
+ if (sameType && oldFiber) {
242
+ newFiber = {
243
+ type: oldFiber.type,
244
+ props: element.props,
245
+ key: element.key,
246
+ _domNode: oldFiber._domNode,
247
+ parent: wipFiber,
248
+ alternate: oldFiber,
249
+ effectTag: "UPDATE",
250
+ updatePriority: "normal",
251
+ child: null,
252
+ sibling: null,
253
+ _rendered: null,
254
+ };
255
+ }
256
+ if (element && !sameType) {
257
+ newFiber = {
258
+ type: element.type,
259
+ props: element.props,
260
+ key: element.key,
261
+ _domNode: null,
262
+ parent: wipFiber,
263
+ alternate: null,
264
+ effectTag: "PLACEMENT",
265
+ updatePriority: "normal",
266
+ child: null,
267
+ sibling: null,
268
+ _rendered: null,
269
+ };
270
+ }
271
+ if (oldFiber && !sameType) {
272
+ oldFiber.effectTag = "DELETION";
273
+ this.deletions.push(oldFiber);
274
+ }
275
+ if (oldFiber) {
276
+ oldFiber = oldFiber.sibling;
277
+ }
278
+ if (index === 0) {
279
+ wipFiber.child = newFiber;
280
+ }
281
+ else if (newFiber && prevSibling) {
282
+ prevSibling.sibling = newFiber;
283
+ }
284
+ prevSibling = newFiber;
285
+ index++;
286
+ }
287
+ }
288
+ commitRoot() {
289
+ this.deletions.forEach((fiber) => this.commitWork(fiber));
290
+ if (this.workInProgressRoot?.child) {
291
+ this.commitWork(this.workInProgressRoot.child);
292
+ }
293
+ this.currentRoot = this.workInProgressRoot;
294
+ this.workInProgressRoot = null;
295
+ }
296
+ commitWork(fiber) {
297
+ if (!fiber)
298
+ return;
299
+ let domParentFiber = fiber.parent;
300
+ while (!domParentFiber?._domNode) {
301
+ domParentFiber = domParentFiber?.parent || null;
302
+ }
303
+ const domParent = domParentFiber?._domNode;
304
+ if (fiber.effectTag === "PLACEMENT" && fiber._domNode && domParent) {
305
+ domParent.appendChild(fiber._domNode);
306
+ }
307
+ else if (fiber.effectTag === "UPDATE" && fiber._domNode) {
308
+ this.updateDom(fiber._domNode, fiber.alternate?.props || {}, fiber.props);
309
+ }
310
+ else if (fiber.effectTag === "DELETION" && domParent) {
311
+ this.commitDeletion(fiber, domParent);
312
+ }
313
+ this.commitWork(fiber.child);
314
+ this.commitWork(fiber.sibling);
315
+ }
316
+ commitDeletion(fiber, domParent) {
317
+ if (fiber._domNode) {
318
+ domParent.removeChild(fiber._domNode);
319
+ }
320
+ else if (fiber.child) {
321
+ this.commitDeletion(fiber.child, domParent);
322
+ }
323
+ }
324
+ updateDom(dom, prevProps, nextProps) {
325
+ Object.keys(prevProps)
326
+ .filter((key) => key !== "children" && !(key in nextProps))
327
+ .forEach((name) => {
328
+ if (name.startsWith("on")) {
329
+ const eventType = name.toLowerCase().substring(2);
330
+ dom.removeEventListener(eventType, prevProps[name]);
331
+ }
332
+ else {
333
+ dom[name] = "";
334
+ }
335
+ });
336
+ Object.keys(nextProps)
337
+ .filter((key) => key !== "children")
338
+ .forEach((name) => {
339
+ if (prevProps[name] !== nextProps[name]) {
340
+ if (name.startsWith("on")) {
341
+ const eventType = name.toLowerCase().substring(2);
342
+ dom.addEventListener(eventType, nextProps[name]);
343
+ }
344
+ else {
345
+ dom[name] = nextProps[name];
346
+ }
347
+ }
348
+ });
349
+ }
350
+ }
351
+ const fiberRenderer = new FiberRenderer();
352
+ export function useFiberRenderer() {
353
+ return fiberRenderer;
354
+ }
355
+ class HierarchicalStore {
356
+ constructor() {
357
+ this.root = new Map();
358
+ this.selectorCache = new Map();
359
+ this.stateSnapshot = {};
360
+ }
361
+ select(selector) {
362
+ const selectorKey = selector.toString();
363
+ if (this.selectorCache.has(selectorKey)) {
364
+ return this.selectorCache.get(selectorKey);
365
+ }
366
+ const result = selector(this.getState());
367
+ this.selectorCache.set(selectorKey, result);
368
+ return result;
369
+ }
370
+ optimisticUpdate(path, update, rollback) {
371
+ const original = this.get(path);
372
+ this.set(path, update);
373
+ return {
374
+ commit: () => this.clearRollback(path),
375
+ rollback: () => {
376
+ this.set(path, original);
377
+ rollback?.();
378
+ },
379
+ };
380
+ }
381
+ getState() {
382
+ return this.stateSnapshot;
383
+ }
384
+ get(path) {
385
+ return this.root.get(path)?.value;
386
+ }
387
+ set(path, value) {
388
+ const node = this.root.get(path);
389
+ if (node) {
390
+ node.value = value;
391
+ this.stateSnapshot = { ...this.stateSnapshot, [path]: value };
392
+ this.invalidateSelectors();
393
+ }
394
+ }
395
+ clearRollback(path) {
396
+ console.log(`[HierarchicalStore] Optimistic update committed for path: ${path}`);
397
+ }
398
+ invalidateSelectors() {
399
+ this.selectorCache.clear();
400
+ }
401
+ }
402
+ const hierarchicalStore = new HierarchicalStore();
403
+ export function useHierarchicalStore() {
404
+ return hierarchicalStore;
405
+ }
25
406
  export const TEXT = Symbol("text");
26
407
  export const Fragment = Symbol("Fragment");
27
408
  const BOOLEAN_ATTRS = new Set([
@@ -52,6 +433,31 @@ const DOM_PROPERTIES = new Set([
52
433
  "textContent",
53
434
  "innerText",
54
435
  ]);
436
+ const DANGEROUS_HTML_PROPS = new Set([
437
+ "innerHTML",
438
+ "outerHTML",
439
+ "insertAdjacentHTML",
440
+ "srcdoc",
441
+ ]);
442
+ const DANGEROUS_PROTOCOLS = new Set([
443
+ "javascript:",
444
+ "data:",
445
+ "vbscript:",
446
+ "file:",
447
+ "about:",
448
+ ]);
449
+ const SAFE_PROTOCOLS = new Set([
450
+ "http:",
451
+ "https:",
452
+ "ftp:",
453
+ "ftps:",
454
+ "mailto:",
455
+ "tel:",
456
+ "#",
457
+ "/",
458
+ "./",
459
+ "../",
460
+ ]);
55
461
  export function createTextVNode(text) {
56
462
  if (text == null || text === false) {
57
463
  return { type: TEXT, props: { nodeValue: "" }, key: null };
@@ -302,7 +708,7 @@ export function renderComponent(Component, props = {}) {
302
708
  catch (err) {
303
709
  console.error("[Fynix] Component render error:", err);
304
710
  showErrorOverlay(err);
305
- return h("div", { style: "color:red" }, `Error: ${err.message}`);
711
+ return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
306
712
  }
307
713
  finally {
308
714
  endComponent();
@@ -346,6 +752,45 @@ function registerDelegatedHandler(el, eventName, fn) {
346
752
  }
347
753
  });
348
754
  }
755
+ function sanitizeText(text) {
756
+ if (typeof text !== "string")
757
+ return String(text);
758
+ return text
759
+ .replace(/[<>"'&]/g, (match) => {
760
+ const entityMap = {
761
+ "<": "&lt;",
762
+ ">": "&gt;",
763
+ '"': "&quot;",
764
+ "'": "&#x27;",
765
+ "&": "&amp;",
766
+ };
767
+ return entityMap[match] || match;
768
+ })
769
+ .replace(/javascript:/gi, "blocked:")
770
+ .replace(/data:.*?base64/gi, "blocked:");
771
+ }
772
+ function sanitizeAttributeValue(value) {
773
+ if (typeof value !== "string")
774
+ return String(value);
775
+ return value
776
+ .replace(/["'<>]/g, (match) => {
777
+ const entityMap = {
778
+ '"': "&quot;",
779
+ "'": "&#x27;",
780
+ "<": "&lt;",
781
+ ">": "&gt;",
782
+ };
783
+ return entityMap[match] || match;
784
+ })
785
+ .replace(/javascript:/gi, "blocked:")
786
+ .replace(/on\w+=/gi, "blocked=");
787
+ }
788
+ function sanitizeErrorMessage(error) {
789
+ if (!error)
790
+ return "Unknown error";
791
+ const message = error.message || error.toString() || "Unknown error";
792
+ return sanitizeText(String(message)).slice(0, 200);
793
+ }
349
794
  function setProperty(el, key, value) {
350
795
  const k = key.toLowerCase();
351
796
  if (key === "r-class" || key === "rc") {
@@ -370,8 +815,40 @@ function setProperty(el, key, value) {
370
815
  Object.assign(el.style, value);
371
816
  return;
372
817
  }
373
- if (key === "innerHTML" || key === "outerHTML") {
374
- console.warn("[Fynix] Security: innerHTML/outerHTML not allowed. Use textContent or children.");
818
+ if (DANGEROUS_HTML_PROPS.has(key)) {
819
+ console.error(`[Fynix] Security: ${key} is blocked for security reasons. Use textContent or children instead.`);
820
+ return;
821
+ }
822
+ if ((key === "href" ||
823
+ key === "src" ||
824
+ key === "action" ||
825
+ key === "formaction") &&
826
+ typeof value === "string") {
827
+ const normalizedValue = value.trim().toLowerCase();
828
+ for (const protocol of DANGEROUS_PROTOCOLS) {
829
+ if (normalizedValue.startsWith(protocol)) {
830
+ console.error(`[Fynix] Security: ${protocol} protocol blocked in ${key}`);
831
+ return;
832
+ }
833
+ }
834
+ if (normalizedValue.includes(":")) {
835
+ const protocol = normalizedValue.split(":")[0] + ":";
836
+ if (!SAFE_PROTOCOLS.has(protocol) &&
837
+ !SAFE_PROTOCOLS.has(normalizedValue.charAt(0))) {
838
+ console.error(`[Fynix] Security: Protocol '${protocol}' not in safe list for ${key}`);
839
+ return;
840
+ }
841
+ }
842
+ if (normalizedValue.startsWith("data:")) {
843
+ if (normalizedValue.includes("javascript") ||
844
+ normalizedValue.includes("<script")) {
845
+ console.error(`[Fynix] Security: Suspicious data: URL blocked in ${key}`);
846
+ return;
847
+ }
848
+ }
849
+ }
850
+ if (key.toLowerCase().startsWith("on") && key !== "open") {
851
+ console.error(`[Fynix] Security: Inline event handler '${key}' blocked. Use r-${key.slice(2)} instead.`);
375
852
  return;
376
853
  }
377
854
  if (BOOLEAN_ATTRS.has(k)) {
@@ -385,25 +862,24 @@ function setProperty(el, key, value) {
385
862
  }
386
863
  return;
387
864
  }
388
- if (DOM_PROPERTIES.has(key)) {
389
- el[key] = value ?? "";
865
+ if (DOM_PROPERTIES.has(key) && !DANGEROUS_HTML_PROPS.has(key)) {
866
+ if (key === "textContent" || key === "innerText") {
867
+ el[key] = sanitizeText(value ?? "");
868
+ }
869
+ else {
870
+ el[key] = value ?? "";
871
+ }
390
872
  return;
391
873
  }
392
874
  if (key.startsWith("data-") || key.startsWith("aria-")) {
393
875
  if (value != null && value !== false) {
394
- el.setAttribute(key, value);
876
+ el.setAttribute(key, sanitizeAttributeValue(String(value)));
395
877
  }
396
878
  else {
397
879
  el.removeAttribute(key);
398
880
  }
399
881
  return;
400
882
  }
401
- if ((key === "href" || key === "src") && typeof value === "string") {
402
- if (value.trim().toLowerCase().startsWith("javascript:")) {
403
- console.warn("[Fynix] Security: javascript: protocol blocked in", key);
404
- return;
405
- }
406
- }
407
883
  if (value != null && value !== false) {
408
884
  el.setAttribute(key, value);
409
885
  }
@@ -489,7 +965,7 @@ async function renderMaybeAsyncComponent(Component, props, vnode) {
489
965
  showErrorOverlay(err);
490
966
  ctx._isMounted = false;
491
967
  endComponent();
492
- return h("div", { style: "color:red" }, `Error: ${err.message}`);
968
+ return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
493
969
  }
494
970
  }
495
971
  export async function patch(parent, newVNode, oldVNode) {