@coherent.js/core 1.1.0 → 2.0.0-rc.0
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 +60 -72
- package/dist/index.js +865 -509
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/types/index.d.ts +123 -21
package/dist/index.js
CHANGED
|
@@ -473,13 +473,25 @@ function createPerformanceMonitor(options = {}) {
|
|
|
473
473
|
if (startTime !== void 0) {
|
|
474
474
|
const duration = performance.now() - startTime;
|
|
475
475
|
_renderTimers.delete(id);
|
|
476
|
-
recordMetric("
|
|
476
|
+
recordMetric("renderTime", duration);
|
|
477
477
|
return duration;
|
|
478
478
|
}
|
|
479
479
|
return 0;
|
|
480
480
|
}
|
|
481
|
+
function recordRender(operation, duration, fromCache = false, metadata = {}) {
|
|
482
|
+
if (operation === "render") {
|
|
483
|
+
recordMetric("renderTime", duration, { fromCache, ...metadata });
|
|
484
|
+
} else {
|
|
485
|
+
recordMetric("componentCount", 1, { operation, fromCache });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function recordError(operation, error, metadata = {}) {
|
|
489
|
+
recordMetric("errorCount", 1, { operation, error: error?.message, ...metadata });
|
|
490
|
+
}
|
|
481
491
|
return {
|
|
482
492
|
recordMetric,
|
|
493
|
+
recordRender,
|
|
494
|
+
recordError,
|
|
483
495
|
measure,
|
|
484
496
|
measureAsync,
|
|
485
497
|
addMetric,
|
|
@@ -506,95 +518,98 @@ function createPerformanceMonitor(options = {}) {
|
|
|
506
518
|
var performanceMonitor = createPerformanceMonitor();
|
|
507
519
|
|
|
508
520
|
// src/core/html-utils.js
|
|
521
|
+
var HTML_ESCAPE_TEST = /[&<>"']/;
|
|
522
|
+
var HTML_ESCAPE = /[&<>"']/g;
|
|
523
|
+
var HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
509
524
|
function escapeHtml(text) {
|
|
510
525
|
if (typeof text !== "string") return text;
|
|
511
|
-
|
|
526
|
+
if (!HTML_ESCAPE_TEST.test(text)) return text;
|
|
527
|
+
return text.replace(HTML_ESCAPE, (ch) => HTML_ESCAPES[ch]);
|
|
528
|
+
}
|
|
529
|
+
var TRUSTED_CONTENT = /* @__PURE__ */ Symbol.for("coherent.js.trustedContent");
|
|
530
|
+
function createTrustedContent(content) {
|
|
531
|
+
const marker = { __html: String(content), __trusted: true };
|
|
532
|
+
Object.defineProperty(marker, TRUSTED_CONTENT, { value: true });
|
|
533
|
+
return Object.freeze(marker);
|
|
512
534
|
}
|
|
513
535
|
function isTrustedContent(value) {
|
|
514
|
-
return Boolean(value) && typeof value === "object" && value
|
|
536
|
+
return Boolean(value) && typeof value === "object" && value[TRUSTED_CONTENT] === true && typeof value.__html === "string";
|
|
537
|
+
}
|
|
538
|
+
var INVALID_ATTRIBUTE_NAME = /[\s"'<>/=\u0000-\u001F\u007F-\u009F]/;
|
|
539
|
+
function isValidAttributeName(name) {
|
|
540
|
+
return typeof name === "string" && name.length > 0 && !INVALID_ATTRIBUTE_NAME.test(name);
|
|
515
541
|
}
|
|
516
542
|
function isVoidElement(tagName) {
|
|
517
543
|
if (typeof tagName !== "string") {
|
|
518
544
|
return false;
|
|
519
545
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
"
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
"
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
"source",
|
|
533
|
-
"track",
|
|
534
|
-
"wbr"
|
|
535
|
-
]);
|
|
536
|
-
return voidElements.has(tagName.toLowerCase());
|
|
546
|
+
return voidElements.has(tagName) || voidElements.has(tagName.toLowerCase());
|
|
547
|
+
}
|
|
548
|
+
var ENUMERATED_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set(["spellcheck", "draggable", "contenteditable"]);
|
|
549
|
+
function normalizeClassValue(value) {
|
|
550
|
+
if (Array.isArray(value)) {
|
|
551
|
+
return value.map(normalizeClassValue).filter(Boolean).join(" ");
|
|
552
|
+
}
|
|
553
|
+
if (value && typeof value === "object") {
|
|
554
|
+
return Object.keys(value).filter((name) => value[name]).join(" ");
|
|
555
|
+
}
|
|
556
|
+
if (value === null || value === void 0 || value === false) return "";
|
|
557
|
+
return String(value);
|
|
537
558
|
}
|
|
538
|
-
function
|
|
559
|
+
function callAttribute(key, value) {
|
|
560
|
+
try {
|
|
561
|
+
return value();
|
|
562
|
+
} catch (_error) {
|
|
563
|
+
console.warn(`Error executing function for attribute '${key}':`, {
|
|
564
|
+
error: _error.message,
|
|
565
|
+
stack: _error.stack,
|
|
566
|
+
attributeKey: key
|
|
567
|
+
});
|
|
568
|
+
return "";
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
var ATTRIBUTE_NAMES = {
|
|
572
|
+
className: "class",
|
|
573
|
+
// Written as is, browsers read `htmlFor` as an unknown `htmlfor`
|
|
574
|
+
// attribute: the label was not associated with its control.
|
|
575
|
+
htmlFor: "for"
|
|
576
|
+
};
|
|
577
|
+
function styleToCss(style) {
|
|
578
|
+
return Object.entries(style).filter(([, val]) => val !== null && val !== void 0 && val !== false).map(([prop, val]) => {
|
|
579
|
+
const name = prop.startsWith("--") ? prop : prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
580
|
+
return `${name}: ${val}`;
|
|
581
|
+
}).join("; ");
|
|
582
|
+
}
|
|
583
|
+
function formatAttributes(props, skip) {
|
|
584
|
+
if (props && props.class !== void 0 && props.className !== void 0) {
|
|
585
|
+
const { className, ...rest } = props;
|
|
586
|
+
const resolve = (key, value) => normalizeClassValue(typeof value === "function" ? callAttribute(key, value) : value);
|
|
587
|
+
props = { ...rest, class: [resolve("class", props.class), resolve("className", className)].filter(Boolean).join(" ") };
|
|
588
|
+
}
|
|
539
589
|
let formatted = "";
|
|
540
590
|
for (const key in props) {
|
|
541
|
-
if (
|
|
591
|
+
if (Object.prototype.hasOwnProperty.call(props, key) && !(skip && skip.has(key))) {
|
|
542
592
|
let value = props[key];
|
|
543
|
-
const attributeName = key
|
|
593
|
+
const attributeName = ATTRIBUTE_NAMES[key] ?? key;
|
|
594
|
+
if (!isValidAttributeName(attributeName)) {
|
|
595
|
+
throw new Error(`Invalid attribute name ${JSON.stringify(key)}: attribute names cannot contain whitespace, quotes, '<', '>', '/', '=' or control characters`);
|
|
596
|
+
}
|
|
544
597
|
if (typeof value === "function") {
|
|
545
598
|
if (attributeName.startsWith("on")) {
|
|
546
|
-
const actionId = `__coherent_action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
547
|
-
const DEBUG = typeof process !== "undefined" && process && process.env && (process.env.COHERENT_DEBUG === "1" || true) || typeof window !== "undefined" && window && window.COHERENT_DEBUG === true;
|
|
548
|
-
if (typeof global !== "undefined") {
|
|
549
|
-
if (!global.__coherentActionRegistry) {
|
|
550
|
-
global.__coherentActionRegistry = {};
|
|
551
|
-
if (DEBUG) console.log("Initialized global action registry");
|
|
552
|
-
}
|
|
553
|
-
global.__coherentActionRegistry[actionId] = value;
|
|
554
|
-
if (DEBUG) console.log(`Added action ${actionId} to global registry, total: ${Object.keys(global.__coherentActionRegistry).length}`);
|
|
555
|
-
if (DEBUG) console.log(`Global registry keys: ${Object.keys(global.__coherentActionRegistry).join(", ")}`);
|
|
556
|
-
if (DEBUG) {
|
|
557
|
-
if (typeof global.__coherentActionRegistryLog === "undefined") {
|
|
558
|
-
global.__coherentActionRegistryLog = [];
|
|
559
|
-
}
|
|
560
|
-
global.__coherentActionRegistryLog.push({
|
|
561
|
-
action: "add",
|
|
562
|
-
actionId,
|
|
563
|
-
timestamp: Date.now(),
|
|
564
|
-
registrySize: Object.keys(global.__coherentActionRegistry).length
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
} else if (typeof window !== "undefined") {
|
|
568
|
-
if (!window.__coherentActionRegistry) {
|
|
569
|
-
window.__coherentActionRegistry = {};
|
|
570
|
-
if (DEBUG) console.log("Initialized window action registry");
|
|
571
|
-
}
|
|
572
|
-
window.__coherentActionRegistry[actionId] = value;
|
|
573
|
-
if (DEBUG) console.log(`Added action ${actionId} to window registry, total: ${Object.keys(window.__coherentActionRegistry).length}`);
|
|
574
|
-
if (DEBUG) console.log(`Window registry keys: ${Object.keys(window.__coherentActionRegistry).join(", ")}`);
|
|
575
|
-
}
|
|
576
|
-
const eventType = attributeName.substring(2);
|
|
577
|
-
formatted += ` data-action="${actionId}" data-event="${eventType}"`;
|
|
578
599
|
continue;
|
|
579
600
|
} else {
|
|
580
|
-
|
|
581
|
-
value = value();
|
|
582
|
-
} catch (_error) {
|
|
583
|
-
console.warn(`Error executing function for attribute '${key}':`, {
|
|
584
|
-
_error: _error.message,
|
|
585
|
-
stack: _error.stack,
|
|
586
|
-
attributeKey: key
|
|
587
|
-
});
|
|
588
|
-
value = "";
|
|
589
|
-
}
|
|
601
|
+
value = callAttribute(key, value);
|
|
590
602
|
}
|
|
591
603
|
}
|
|
604
|
+
if (attributeName === "class" && typeof value === "object" && value !== null) {
|
|
605
|
+
value = normalizeClassValue(value);
|
|
606
|
+
}
|
|
607
|
+
if (typeof value === "boolean" && (attributeName.startsWith("aria-") || ENUMERATED_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase()))) {
|
|
608
|
+
value = String(value);
|
|
609
|
+
}
|
|
592
610
|
if (attributeName === "style" && typeof value === "object" && value !== null) {
|
|
593
|
-
const cssString =
|
|
594
|
-
|
|
595
|
-
return `${kebabProp}: ${val}`;
|
|
596
|
-
}).join("; ");
|
|
597
|
-
formatted += ` ${attributeName}="${escapeHtml(cssString)}"`;
|
|
611
|
+
const cssString = styleToCss(value);
|
|
612
|
+
if (cssString) formatted += ` ${attributeName}="${escapeHtml(cssString)}"`;
|
|
598
613
|
} else if (value === true) {
|
|
599
614
|
formatted += ` ${attributeName}`;
|
|
600
615
|
} else if (value !== false && value !== null && value !== void 0) {
|
|
@@ -604,10 +619,81 @@ function formatAttributes(props) {
|
|
|
604
619
|
}
|
|
605
620
|
return formatted.trim();
|
|
606
621
|
}
|
|
622
|
+
function stripComments(html) {
|
|
623
|
+
let out = "";
|
|
624
|
+
let cursor = 0;
|
|
625
|
+
for (; ; ) {
|
|
626
|
+
const start = html.indexOf("<!--", cursor);
|
|
627
|
+
if (start === -1) return out + html.slice(cursor);
|
|
628
|
+
out += html.slice(cursor, start);
|
|
629
|
+
const end = html.indexOf("-->", start + 4);
|
|
630
|
+
if (end === -1) return out;
|
|
631
|
+
cursor = end + 3;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function endsInComment(html) {
|
|
635
|
+
let cursor = 0;
|
|
636
|
+
for (; ; ) {
|
|
637
|
+
const start = html.indexOf("<!--", cursor);
|
|
638
|
+
if (start === -1) return false;
|
|
639
|
+
const end = html.indexOf("-->", start + 4);
|
|
640
|
+
if (end === -1 || end + 3 === html.length) return true;
|
|
641
|
+
cursor = end + 3;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function createStreamMinifier() {
|
|
645
|
+
let pending = "";
|
|
646
|
+
let started = false;
|
|
647
|
+
const minifyPiece = (text, last) => {
|
|
648
|
+
let out = stripComments(started ? `>${text}` : text).replace(/\s+/g, " ").replace(/>\s+</g, "><");
|
|
649
|
+
if (started) {
|
|
650
|
+
out = out.slice(1);
|
|
651
|
+
} else {
|
|
652
|
+
out = out.trimStart();
|
|
653
|
+
}
|
|
654
|
+
if (last) out = out.trimEnd();
|
|
655
|
+
started = true;
|
|
656
|
+
return out;
|
|
657
|
+
};
|
|
658
|
+
return {
|
|
659
|
+
push(chunk) {
|
|
660
|
+
pending += chunk;
|
|
661
|
+
let cut = pending.lastIndexOf(">");
|
|
662
|
+
while (cut !== -1 && endsInComment(pending.slice(0, cut + 1))) {
|
|
663
|
+
cut = cut === 0 ? -1 : pending.lastIndexOf(">", cut - 1);
|
|
664
|
+
}
|
|
665
|
+
if (cut === -1) return "";
|
|
666
|
+
const head = pending.slice(0, cut + 1);
|
|
667
|
+
pending = pending.slice(cut + 1);
|
|
668
|
+
return minifyPiece(head, false);
|
|
669
|
+
},
|
|
670
|
+
end() {
|
|
671
|
+
const rest = pending;
|
|
672
|
+
pending = "";
|
|
673
|
+
return rest || !started ? minifyPiece(rest, true) : "";
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
607
677
|
function minifyHtml(html, options = {}) {
|
|
608
678
|
if (!options.minify) return html;
|
|
609
|
-
return html
|
|
679
|
+
return stripComments(html).replace(/\s+/g, " ").replace(/>\s+</g, "><").trim();
|
|
610
680
|
}
|
|
681
|
+
var voidElements = /* @__PURE__ */ new Set([
|
|
682
|
+
"area",
|
|
683
|
+
"base",
|
|
684
|
+
"br",
|
|
685
|
+
"col",
|
|
686
|
+
"embed",
|
|
687
|
+
"hr",
|
|
688
|
+
"img",
|
|
689
|
+
"input",
|
|
690
|
+
"link",
|
|
691
|
+
"meta",
|
|
692
|
+
"param",
|
|
693
|
+
"source",
|
|
694
|
+
"track",
|
|
695
|
+
"wbr"
|
|
696
|
+
]);
|
|
611
697
|
|
|
612
698
|
// src/core/object-utils.js
|
|
613
699
|
function deepClone(obj, seen = /* @__PURE__ */ new WeakMap()) {
|
|
@@ -694,6 +780,9 @@ function validateComponent(component, path = "root") {
|
|
|
694
780
|
if (typeof component === "function") {
|
|
695
781
|
return true;
|
|
696
782
|
}
|
|
783
|
+
if (isTrustedContent(component)) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
697
786
|
if (Array.isArray(component)) {
|
|
698
787
|
component.forEach((child, index) => {
|
|
699
788
|
validateComponent(child, `${path}[${index}]`);
|
|
@@ -735,22 +824,6 @@ function isCoherentObject(obj) {
|
|
|
735
824
|
return /^[a-zA-Z][a-zA-Z0-9-]*$/.test(key);
|
|
736
825
|
});
|
|
737
826
|
}
|
|
738
|
-
function extractProps(coherentObj) {
|
|
739
|
-
if (!isCoherentObject(coherentObj)) {
|
|
740
|
-
return {};
|
|
741
|
-
}
|
|
742
|
-
const props = {};
|
|
743
|
-
const keys = Object.keys(coherentObj);
|
|
744
|
-
keys.forEach((tag) => {
|
|
745
|
-
const value = coherentObj[tag];
|
|
746
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
747
|
-
props[tag] = { ...value };
|
|
748
|
-
} else {
|
|
749
|
-
props[tag] = { text: value };
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
return props;
|
|
753
|
-
}
|
|
754
827
|
function hasChildren(component) {
|
|
755
828
|
if (Array.isArray(component)) {
|
|
756
829
|
return component.length > 0;
|
|
@@ -772,6 +845,15 @@ function normalizeChildren(children) {
|
|
|
772
845
|
return [];
|
|
773
846
|
}
|
|
774
847
|
if (Array.isArray(children)) {
|
|
848
|
+
let clean = true;
|
|
849
|
+
for (let i = 0; i < children.length; i++) {
|
|
850
|
+
const child = children[i];
|
|
851
|
+
if (child === null || child === void 0 || Array.isArray(child)) {
|
|
852
|
+
clean = false;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (clean) return children;
|
|
775
857
|
return children.flat().filter((child) => child !== null && child !== void 0);
|
|
776
858
|
}
|
|
777
859
|
return [children];
|
|
@@ -785,7 +867,9 @@ var DEFAULT_RENDERER_CONFIG = {
|
|
|
785
867
|
enableMonitoring: false,
|
|
786
868
|
validateInput: true,
|
|
787
869
|
// HTML Renderer specific options
|
|
788
|
-
|
|
870
|
+
// Off by default: keying a render on its content costs a full walk of
|
|
871
|
+
// the tree, which only pays off when identical trees are re-rendered.
|
|
872
|
+
enableCache: false,
|
|
789
873
|
minify: false,
|
|
790
874
|
cacheSize: 1e3,
|
|
791
875
|
cacheTTL: 3e5,
|
|
@@ -871,7 +955,7 @@ var BaseRenderer = class {
|
|
|
871
955
|
return {
|
|
872
956
|
...baseConfig,
|
|
873
957
|
// HTML-specific defaults
|
|
874
|
-
enableCache: baseConfig.enableCache
|
|
958
|
+
enableCache: baseConfig.enableCache === true,
|
|
875
959
|
enableMonitoring: baseConfig.enableMonitoring !== false
|
|
876
960
|
};
|
|
877
961
|
case "streaming":
|
|
@@ -904,11 +988,14 @@ var BaseRenderer = class {
|
|
|
904
988
|
/**
|
|
905
989
|
* Check if component is valid for rendering
|
|
906
990
|
*/
|
|
907
|
-
isValidComponent(component) {
|
|
991
|
+
isValidComponent(component, depth = 0) {
|
|
908
992
|
if (component === null || component === void 0) return true;
|
|
909
993
|
if (typeof component === "string" || typeof component === "number") return true;
|
|
910
994
|
if (typeof component === "function") return true;
|
|
911
|
-
if (Array.isArray(component))
|
|
995
|
+
if (Array.isArray(component)) {
|
|
996
|
+
if (depth >= this.config.maxDepth) return true;
|
|
997
|
+
return component.every((child) => this.isValidComponent(child, depth + 1));
|
|
998
|
+
}
|
|
912
999
|
if (isCoherentObject(component)) return true;
|
|
913
1000
|
return false;
|
|
914
1001
|
}
|
|
@@ -930,7 +1017,10 @@ var BaseRenderer = class {
|
|
|
930
1017
|
if (typeof component === "string") {
|
|
931
1018
|
return { type: "text", value: component };
|
|
932
1019
|
}
|
|
933
|
-
if (typeof component === "
|
|
1020
|
+
if (typeof component === "boolean") {
|
|
1021
|
+
return { type: "empty", value: "" };
|
|
1022
|
+
}
|
|
1023
|
+
if (typeof component === "number") {
|
|
934
1024
|
return { type: "text", value: String(component) };
|
|
935
1025
|
}
|
|
936
1026
|
if (typeof component === "function") {
|
|
@@ -945,31 +1035,23 @@ var BaseRenderer = class {
|
|
|
945
1035
|
return { type: "unknown", value: component };
|
|
946
1036
|
}
|
|
947
1037
|
/**
|
|
948
|
-
* Execute function components
|
|
1038
|
+
* Execute function components. Errors propagate: they used to be
|
|
1039
|
+
* swallowed here (the component rendered as nothing, logged only when
|
|
1040
|
+
* NODE_ENV=development), so a broken component produced a partial page
|
|
1041
|
+
* with a 200 and error boundaries never saw nested failures.
|
|
949
1042
|
*/
|
|
950
1043
|
executeFunctionComponent(func, depth = 0) {
|
|
951
1044
|
try {
|
|
952
|
-
const
|
|
953
|
-
let result;
|
|
954
|
-
if (isContextProvider) {
|
|
955
|
-
result = func((children) => {
|
|
956
|
-
return this.renderComponent(children, this.config, depth + 1);
|
|
957
|
-
});
|
|
958
|
-
} else {
|
|
959
|
-
result = func();
|
|
960
|
-
}
|
|
1045
|
+
const result = func();
|
|
961
1046
|
if (typeof result === "function") {
|
|
962
1047
|
return this.executeFunctionComponent(result, depth);
|
|
963
1048
|
}
|
|
964
1049
|
return result;
|
|
965
|
-
} catch (
|
|
1050
|
+
} catch (error) {
|
|
966
1051
|
if (this.config.enableMonitoring) {
|
|
967
|
-
performanceMonitor.recordError("functionComponent",
|
|
968
|
-
}
|
|
969
|
-
if (typeof process !== "undefined" && process.env && true) {
|
|
970
|
-
console.warn("Coherent.js Function Component Error:", _error.message);
|
|
1052
|
+
performanceMonitor.recordError("functionComponent", error);
|
|
971
1053
|
}
|
|
972
|
-
|
|
1054
|
+
throw error;
|
|
973
1055
|
}
|
|
974
1056
|
}
|
|
975
1057
|
/**
|
|
@@ -1070,88 +1152,43 @@ var BaseRenderer = class {
|
|
|
1070
1152
|
throw new Error("render must be implemented by subclass");
|
|
1071
1153
|
}
|
|
1072
1154
|
};
|
|
1073
|
-
var
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
return true;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return false;
|
|
1109
|
-
},
|
|
1110
|
-
/**
|
|
1111
|
-
* Get element complexity score
|
|
1112
|
-
*/
|
|
1113
|
-
getElementComplexity(element) {
|
|
1114
|
-
if (!element || typeof element !== "object") return 1;
|
|
1115
|
-
let complexity = Object.keys(element).length;
|
|
1116
|
-
if (element.children && Array.isArray(element.children)) {
|
|
1117
|
-
complexity += element.children.reduce(
|
|
1118
|
-
(sum, child) => sum + RendererUtils.getElementComplexity(child),
|
|
1119
|
-
0
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
return complexity;
|
|
1123
|
-
},
|
|
1124
|
-
/**
|
|
1125
|
-
* Generate cache key for element
|
|
1126
|
-
*/
|
|
1127
|
-
generateCacheKey(tagName, element) {
|
|
1128
|
-
try {
|
|
1129
|
-
const keyData = {
|
|
1130
|
-
tag: tagName,
|
|
1131
|
-
props: extractProps(element),
|
|
1132
|
-
hasChildren: hasChildren(element),
|
|
1133
|
-
childrenType: Array.isArray(element.children) ? "array" : typeof element.children
|
|
1134
|
-
};
|
|
1135
|
-
return `element:${JSON.stringify(keyData)}`;
|
|
1136
|
-
} catch (_error) {
|
|
1137
|
-
if (typeof process !== "undefined" && process.env && true) {
|
|
1138
|
-
console.warn("Failed to generate cache key:", _error);
|
|
1139
|
-
}
|
|
1140
|
-
return null;
|
|
1155
|
+
var UNCACHEABLE = /* @__PURE__ */ Symbol("uncacheable");
|
|
1156
|
+
function serializeForCache(value) {
|
|
1157
|
+
try {
|
|
1158
|
+
return serialize(value, /* @__PURE__ */ new Set());
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
if (error === UNCACHEABLE) return null;
|
|
1161
|
+
throw error;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function serialize(value, ancestors) {
|
|
1165
|
+
switch (typeof value) {
|
|
1166
|
+
case "string":
|
|
1167
|
+
return JSON.stringify(value);
|
|
1168
|
+
case "number":
|
|
1169
|
+
return `n${value}`;
|
|
1170
|
+
case "boolean":
|
|
1171
|
+
return value ? "t" : "f";
|
|
1172
|
+
case "undefined":
|
|
1173
|
+
return "u";
|
|
1174
|
+
case "object": {
|
|
1175
|
+
if (value === null) return "z";
|
|
1176
|
+
if (isTrustedContent(value)) return `T${JSON.stringify(value.__html)}`;
|
|
1177
|
+
if (ancestors.has(value)) throw UNCACHEABLE;
|
|
1178
|
+
const isArray = Array.isArray(value);
|
|
1179
|
+
if (!isArray) {
|
|
1180
|
+
const proto = Object.getPrototypeOf(value);
|
|
1181
|
+
if (proto !== Object.prototype && proto !== null) throw UNCACHEABLE;
|
|
1182
|
+
}
|
|
1183
|
+
ancestors.add(value);
|
|
1184
|
+
const body = isArray ? value.map((item) => serialize(item, ancestors)).join(",") : Object.keys(value).map((key) => `${JSON.stringify(key)}:${serialize(value[key], ancestors)}`).join(",");
|
|
1185
|
+
ancestors.delete(value);
|
|
1186
|
+
return isArray ? `[${body}]` : `{${body}}`;
|
|
1141
1187
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
* Check if element is cacheable
|
|
1145
|
-
*/
|
|
1146
|
-
isCacheable(element, options) {
|
|
1147
|
-
if (!options.enableCache) return false;
|
|
1148
|
-
if (RendererUtils.hasFunctions(element)) return false;
|
|
1149
|
-
if (RendererUtils.getElementComplexity(element) > 1e3) return false;
|
|
1150
|
-
const cacheKey = RendererUtils.generateCacheKey(element.tagName || "unknown", element);
|
|
1151
|
-
if (!cacheKey) return false;
|
|
1152
|
-
return true;
|
|
1188
|
+
default:
|
|
1189
|
+
throw UNCACHEABLE;
|
|
1153
1190
|
}
|
|
1154
|
-
}
|
|
1191
|
+
}
|
|
1155
1192
|
|
|
1156
1193
|
// src/core/html-nesting-rules.js
|
|
1157
1194
|
var FORBIDDEN_CHILDREN = {
|
|
@@ -1237,12 +1274,13 @@ var HTMLNestingError = class extends Error {
|
|
|
1237
1274
|
// src/performance/cache-manager.js
|
|
1238
1275
|
function createCacheManager(options = {}) {
|
|
1239
1276
|
const {
|
|
1240
|
-
maxCacheSize = 1e3,
|
|
1241
1277
|
maxMemoryMB = 100,
|
|
1242
1278
|
ttlMs = 1e3 * 60 * 5,
|
|
1243
1279
|
// 5 minutes
|
|
1244
1280
|
enableStatistics = true
|
|
1245
1281
|
} = options;
|
|
1282
|
+
const maxCacheSize = options.maxCacheSize ?? options.maxSize ?? 1e3;
|
|
1283
|
+
const maxMemoryBytes = maxMemoryMB * 1024 * 1024;
|
|
1246
1284
|
const caches = {
|
|
1247
1285
|
static: /* @__PURE__ */ new Map(),
|
|
1248
1286
|
// Never-changing components
|
|
@@ -1285,51 +1323,57 @@ function createCacheManager(options = {}) {
|
|
|
1285
1323
|
return `${extractComponentName(component)}_${hash}`;
|
|
1286
1324
|
}
|
|
1287
1325
|
function get(key, type = "component") {
|
|
1288
|
-
const
|
|
1326
|
+
const cacheType = caches[type] ? type : "component";
|
|
1327
|
+
const cache = caches[cacheType];
|
|
1289
1328
|
const entry = cache.get(key);
|
|
1290
1329
|
if (!entry) {
|
|
1291
1330
|
stats.misses++;
|
|
1292
|
-
if (enableStatistics) stats.accessCount[
|
|
1331
|
+
if (enableStatistics) stats.accessCount[cacheType]++;
|
|
1293
1332
|
return null;
|
|
1294
1333
|
}
|
|
1295
|
-
if (Date.now() - entry.timestamp >
|
|
1334
|
+
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
1296
1335
|
cache.delete(key);
|
|
1297
1336
|
updateMemoryUsage(-entry.size);
|
|
1298
1337
|
stats.misses++;
|
|
1299
|
-
if (enableStatistics) stats.accessCount[
|
|
1338
|
+
if (enableStatistics) stats.accessCount[cacheType]++;
|
|
1300
1339
|
return null;
|
|
1301
1340
|
}
|
|
1341
|
+
cache.delete(key);
|
|
1342
|
+
cache.set(key, entry);
|
|
1302
1343
|
entry.lastAccess = Date.now();
|
|
1303
1344
|
entry.accessCount++;
|
|
1304
1345
|
stats.hits++;
|
|
1305
1346
|
if (enableStatistics) {
|
|
1306
|
-
stats.accessCount[
|
|
1307
|
-
stats.hitRate[
|
|
1347
|
+
stats.accessCount[cacheType]++;
|
|
1348
|
+
stats.hitRate[cacheType] = stats.hits / (stats.hits + stats.misses) * 100;
|
|
1308
1349
|
}
|
|
1309
1350
|
return entry.value;
|
|
1310
1351
|
}
|
|
1311
1352
|
function set(key, value, type = "component", metadata = {}) {
|
|
1312
|
-
const
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1353
|
+
const cacheType = caches[type] ? type : "component";
|
|
1354
|
+
const cache = caches[cacheType];
|
|
1355
|
+
const size = calculateSize(value) + calculateSize(key);
|
|
1356
|
+
if (size > maxMemoryBytes) return;
|
|
1357
|
+
const existing = cache.get(key);
|
|
1358
|
+
if (existing) {
|
|
1359
|
+
cache.delete(key);
|
|
1360
|
+
updateMemoryUsage(-existing.size);
|
|
1316
1361
|
}
|
|
1317
|
-
const
|
|
1362
|
+
const now = Date.now();
|
|
1363
|
+
cache.set(key, {
|
|
1318
1364
|
value,
|
|
1319
|
-
timestamp:
|
|
1320
|
-
lastAccess:
|
|
1365
|
+
timestamp: now,
|
|
1366
|
+
lastAccess: now,
|
|
1321
1367
|
size,
|
|
1322
1368
|
metadata,
|
|
1369
|
+
ttl: typeof metadata.ttlMs === "number" ? metadata.ttlMs : ttlMs,
|
|
1323
1370
|
accessCount: 0
|
|
1324
|
-
};
|
|
1325
|
-
const existing = cache.get(key);
|
|
1326
|
-
if (existing) {
|
|
1327
|
-
updateMemoryUsage(-existing.size);
|
|
1328
|
-
}
|
|
1329
|
-
cache.set(key, entry);
|
|
1371
|
+
});
|
|
1330
1372
|
updateMemoryUsage(size);
|
|
1331
|
-
|
|
1332
|
-
|
|
1373
|
+
evict(cache, () => cache.size > maxCacheSize);
|
|
1374
|
+
for (const other of Object.values(caches)) {
|
|
1375
|
+
if (memoryUsage <= maxMemoryBytes) break;
|
|
1376
|
+
evict(other, () => memoryUsage > maxMemoryBytes);
|
|
1333
1377
|
}
|
|
1334
1378
|
}
|
|
1335
1379
|
function remove(key, type) {
|
|
@@ -1356,11 +1400,14 @@ function createCacheManager(options = {}) {
|
|
|
1356
1400
|
if (type) {
|
|
1357
1401
|
const cache = caches[type];
|
|
1358
1402
|
if (cache) {
|
|
1403
|
+
for (const entry of cache.values()) {
|
|
1404
|
+
updateMemoryUsage(-entry.size);
|
|
1405
|
+
}
|
|
1359
1406
|
cache.clear();
|
|
1360
1407
|
}
|
|
1361
|
-
|
|
1362
|
-
Object.values(caches).forEach((cache) => cache.clear());
|
|
1408
|
+
return;
|
|
1363
1409
|
}
|
|
1410
|
+
Object.values(caches).forEach((cache) => cache.clear());
|
|
1364
1411
|
memoryUsage = 0;
|
|
1365
1412
|
}
|
|
1366
1413
|
function getStats() {
|
|
@@ -1379,7 +1426,7 @@ function createCacheManager(options = {}) {
|
|
|
1379
1426
|
let freed = 0;
|
|
1380
1427
|
for (const [, cache] of Object.entries(caches)) {
|
|
1381
1428
|
for (const [key, entry] of cache.entries()) {
|
|
1382
|
-
if (now - entry.timestamp >
|
|
1429
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
1383
1430
|
cache.delete(key);
|
|
1384
1431
|
updateMemoryUsage(-entry.size);
|
|
1385
1432
|
freed++;
|
|
@@ -1404,17 +1451,15 @@ function createCacheManager(options = {}) {
|
|
|
1404
1451
|
function updateMemoryUsage(delta) {
|
|
1405
1452
|
memoryUsage = Math.max(0, memoryUsage + delta);
|
|
1406
1453
|
}
|
|
1407
|
-
function
|
|
1408
|
-
const cache = caches[type] || caches.component;
|
|
1409
|
-
const entries = Array.from(cache.entries()).sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
|
|
1454
|
+
function evict(cache, shouldEvict) {
|
|
1410
1455
|
let freed = 0;
|
|
1411
|
-
for (const [key, entry] of
|
|
1412
|
-
if (
|
|
1456
|
+
for (const [key, entry] of cache) {
|
|
1457
|
+
if (!shouldEvict()) break;
|
|
1413
1458
|
cache.delete(key);
|
|
1414
1459
|
updateMemoryUsage(-entry.size);
|
|
1415
1460
|
freed += entry.size;
|
|
1416
1461
|
}
|
|
1417
|
-
return
|
|
1462
|
+
return freed;
|
|
1418
1463
|
}
|
|
1419
1464
|
function simpleHash(str) {
|
|
1420
1465
|
let hash = 0;
|
|
@@ -1569,7 +1614,7 @@ var ErrorHandler = class {
|
|
|
1569
1614
|
enableStackTrace: options.enableStackTrace !== false,
|
|
1570
1615
|
enableSuggestions: options.enableSuggestions !== false,
|
|
1571
1616
|
enableLogging: options.enableLogging ?? defaultEnableLogging,
|
|
1572
|
-
logLevel: options.logLevel || "
|
|
1617
|
+
logLevel: options.logLevel || "error",
|
|
1573
1618
|
maxErrorHistory: options.maxErrorHistory || 100,
|
|
1574
1619
|
...options
|
|
1575
1620
|
};
|
|
@@ -1720,7 +1765,7 @@ var ErrorHandler = class {
|
|
|
1720
1765
|
if (suggestions.length === 0) {
|
|
1721
1766
|
suggestions.push(
|
|
1722
1767
|
"Enable development tools for more detailed debugging",
|
|
1723
|
-
"Check browser console for additional
|
|
1768
|
+
"Check browser console for additional error details",
|
|
1724
1769
|
"Use component validation tools to identify issues"
|
|
1725
1770
|
);
|
|
1726
1771
|
}
|
|
@@ -1862,15 +1907,34 @@ var ErrorHandler = class {
|
|
|
1862
1907
|
var globalErrorHandler = new ErrorHandler();
|
|
1863
1908
|
|
|
1864
1909
|
// src/rendering/html-renderer.js
|
|
1910
|
+
var RESERVED_PROPS = /* @__PURE__ */ new Set(["children", "text", "key", "html"]);
|
|
1865
1911
|
var rendererCache = createCacheManager({
|
|
1866
|
-
|
|
1912
|
+
maxCacheSize: 1e3,
|
|
1867
1913
|
ttlMs: 3e5
|
|
1868
1914
|
// 5 minutes
|
|
1869
1915
|
});
|
|
1916
|
+
function assertNotThenable(value, path) {
|
|
1917
|
+
if (value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function") {
|
|
1918
|
+
throw new RenderingError(
|
|
1919
|
+
`Cannot render a Promise at ${path === "root" ? "root" : formatRenderPath(path)}: render() is synchronous. Await async components (and their data) before rendering.`,
|
|
1920
|
+
void 0,
|
|
1921
|
+
{ path: path === "root" ? "root" : formatRenderPath(path), renderer: "html" }
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function childPath(parent, segment) {
|
|
1926
|
+
return { parent, segment };
|
|
1927
|
+
}
|
|
1870
1928
|
function formatRenderPath(path) {
|
|
1871
|
-
|
|
1929
|
+
const segments = [];
|
|
1930
|
+
if (Array.isArray(path)) {
|
|
1931
|
+
segments.push(...path);
|
|
1932
|
+
} else {
|
|
1933
|
+
for (let node = path; node; node = node.parent) segments.push(node.segment);
|
|
1934
|
+
segments.reverse();
|
|
1935
|
+
}
|
|
1872
1936
|
let rendered = "root";
|
|
1873
|
-
for (const segment of
|
|
1937
|
+
for (const segment of segments) {
|
|
1874
1938
|
if (typeof segment !== "string" || segment.length === 0) continue;
|
|
1875
1939
|
if (segment.startsWith("[")) {
|
|
1876
1940
|
rendered += segment;
|
|
@@ -1883,15 +1947,15 @@ function formatRenderPath(path) {
|
|
|
1883
1947
|
var HTMLRenderer = class extends BaseRenderer {
|
|
1884
1948
|
constructor(options = {}) {
|
|
1885
1949
|
super({
|
|
1886
|
-
enableCache: options.enableCache !== false,
|
|
1887
1950
|
enableMonitoring: options.enableMonitoring !== false,
|
|
1888
1951
|
minify: options.minify || false,
|
|
1889
1952
|
streaming: options.streaming || false,
|
|
1890
1953
|
maxDepth: options.maxDepth || 100,
|
|
1891
|
-
...options
|
|
1954
|
+
...options,
|
|
1955
|
+
enableCache: options.enableCache === true
|
|
1892
1956
|
});
|
|
1893
|
-
if (this.config.enableCache
|
|
1894
|
-
this.cache = rendererCache;
|
|
1957
|
+
if (this.config.enableCache) {
|
|
1958
|
+
this.cache = options.cache || rendererCache;
|
|
1895
1959
|
}
|
|
1896
1960
|
}
|
|
1897
1961
|
/**
|
|
@@ -1919,15 +1983,30 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
1919
1983
|
const config = { ...this.config, ...options };
|
|
1920
1984
|
this.startTiming();
|
|
1921
1985
|
try {
|
|
1986
|
+
assertNotThenable(component, "root");
|
|
1922
1987
|
if (config.validateInput && !this.isValidComponent(component)) {
|
|
1923
1988
|
throw new Error("Invalid component structure");
|
|
1924
1989
|
}
|
|
1990
|
+
const cacheKey = this.cache && config.enableCache ? serializeForCache(component) : null;
|
|
1991
|
+
const fullKey = cacheKey === null ? null : `render:${config.minify ? "min" : "raw"}:${cacheKey}`;
|
|
1992
|
+
if (fullKey !== null) {
|
|
1993
|
+
const cached = this.cache.get(fullKey);
|
|
1994
|
+
if (cached !== null) {
|
|
1995
|
+
this.endTiming();
|
|
1996
|
+
return cached;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1925
1999
|
const renderOptions = {
|
|
1926
2000
|
...config,
|
|
1927
2001
|
seenObjects: /* @__PURE__ */ new WeakSet()
|
|
1928
2002
|
};
|
|
1929
|
-
const html = this.renderComponent(component, renderOptions, 0,
|
|
2003
|
+
const html = this.renderComponent(component, renderOptions, 0, null);
|
|
1930
2004
|
const finalHtml = config.minify ? minifyHtml(html, config) : html;
|
|
2005
|
+
if (fullKey !== null) {
|
|
2006
|
+
this.cache.set(fullKey, finalHtml, "component", {
|
|
2007
|
+
ttlMs: typeof config.cacheTTL === "number" ? config.cacheTTL : void 0
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
1931
2010
|
this.endTiming();
|
|
1932
2011
|
this.recordPerformance("render", this.metrics.startTime, false, {
|
|
1933
2012
|
cacheEnabled: config.enableCache
|
|
@@ -1947,7 +2026,7 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
1947
2026
|
/**
|
|
1948
2027
|
* Render a single component with full optimization pipeline
|
|
1949
2028
|
*/
|
|
1950
|
-
renderComponent(component, options, depth = 0, path =
|
|
2029
|
+
renderComponent(component, options, depth = 0, path = null) {
|
|
1951
2030
|
if (component === null || component === void 0) {
|
|
1952
2031
|
return "";
|
|
1953
2032
|
}
|
|
@@ -1957,8 +2036,13 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
1957
2036
|
if (isTrustedContent(component)) {
|
|
1958
2037
|
return component.__html;
|
|
1959
2038
|
}
|
|
1960
|
-
if (typeof component === "object" && component
|
|
1961
|
-
|
|
2039
|
+
if (typeof component === "object" && component.__isLazy === true && typeof component.evaluate === "function") {
|
|
2040
|
+
return this.renderComponent(component.evaluate(), options, depth + 1, childPath(path, "()"));
|
|
2041
|
+
}
|
|
2042
|
+
assertNotThenable(component, path);
|
|
2043
|
+
const tracked = options.seenObjects && typeof component === "object" && component !== null ? component : null;
|
|
2044
|
+
if (tracked) {
|
|
2045
|
+
if (options.seenObjects.has(tracked)) {
|
|
1962
2046
|
throw new RenderingError(
|
|
1963
2047
|
"Circular reference detected in component tree",
|
|
1964
2048
|
component,
|
|
@@ -1966,9 +2050,7 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
1966
2050
|
["Remove the circular reference", "Use lazy loading to break the cycle"]
|
|
1967
2051
|
);
|
|
1968
2052
|
}
|
|
1969
|
-
|
|
1970
|
-
options.seenObjects.add(component);
|
|
1971
|
-
}
|
|
2053
|
+
options.seenObjects.add(tracked);
|
|
1972
2054
|
}
|
|
1973
2055
|
this.validateDepth(depth);
|
|
1974
2056
|
try {
|
|
@@ -1979,8 +2061,8 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
1979
2061
|
case "text":
|
|
1980
2062
|
return escapeHtml(value);
|
|
1981
2063
|
case "function": {
|
|
1982
|
-
const result = this.
|
|
1983
|
-
return this.renderComponent(result, options, depth + 1,
|
|
2064
|
+
const result = this.runFunctionComponent(value, options, depth, path);
|
|
2065
|
+
return this.renderComponent(result, options, depth + 1, childPath(path, "()"));
|
|
1984
2066
|
}
|
|
1985
2067
|
case "array":
|
|
1986
2068
|
if (typeof process !== "undefined" && process.env && true && value.length > 1) {
|
|
@@ -1998,11 +2080,23 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
1998
2080
|
);
|
|
1999
2081
|
}
|
|
2000
2082
|
}
|
|
2001
|
-
|
|
2083
|
+
{
|
|
2084
|
+
let html = "";
|
|
2085
|
+
for (let index = 0; index < value.length; index++) {
|
|
2086
|
+
html += this.renderComponent(value[index], options, depth + 1, childPath(path, `[${index}]`));
|
|
2087
|
+
}
|
|
2088
|
+
return html;
|
|
2089
|
+
}
|
|
2002
2090
|
case "element": {
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2091
|
+
const tagNames = Object.keys(value);
|
|
2092
|
+
if (tagNames.length === 1) {
|
|
2093
|
+
return this.renderElement(tagNames[0], value[tagNames[0]], options, depth, childPath(path, tagNames[0]));
|
|
2094
|
+
}
|
|
2095
|
+
let html = "";
|
|
2096
|
+
for (const tagName of tagNames) {
|
|
2097
|
+
html += this.renderElement(tagName, value[tagName], options, depth, childPath(path, tagName));
|
|
2098
|
+
}
|
|
2099
|
+
return html;
|
|
2006
2100
|
}
|
|
2007
2101
|
default:
|
|
2008
2102
|
this.recordError("renderComponent", new Error(`Unknown component type: ${type}`));
|
|
@@ -2018,16 +2112,34 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
2018
2112
|
}
|
|
2019
2113
|
throw _error;
|
|
2020
2114
|
}
|
|
2021
|
-
|
|
2115
|
+
const wrapped = new RenderingError(_error.message, void 0, { path: renderPath, renderer: "html" });
|
|
2116
|
+
wrapped.cause = _error;
|
|
2117
|
+
throw wrapped;
|
|
2118
|
+
} finally {
|
|
2119
|
+
if (tracked) options.seenObjects.delete(tracked);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Run a function component, giving `options.onError` the chance to
|
|
2124
|
+
* replace a component that throws.
|
|
2125
|
+
*/
|
|
2126
|
+
runFunctionComponent(func, options, depth, path) {
|
|
2127
|
+
try {
|
|
2128
|
+
return this.executeFunctionComponent(func, depth);
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
if (typeof options.onError === "function") {
|
|
2131
|
+
return options.onError(error, { path: formatRenderPath(path) });
|
|
2132
|
+
}
|
|
2133
|
+
throw error;
|
|
2022
2134
|
}
|
|
2023
2135
|
}
|
|
2024
2136
|
/**
|
|
2025
2137
|
* Render an HTML element with advanced caching and optimization
|
|
2026
2138
|
*/
|
|
2027
|
-
renderElement(tagName, element, options, depth = 0, path =
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2030
|
-
if (options.seenObjects
|
|
2139
|
+
renderElement(tagName, element, options, depth = 0, path = null) {
|
|
2140
|
+
const tracked = options.seenObjects && element && typeof element === "object" && !Array.isArray(element) ? element : null;
|
|
2141
|
+
if (tracked) {
|
|
2142
|
+
if (options.seenObjects.has(tracked)) {
|
|
2031
2143
|
throw new RenderingError(
|
|
2032
2144
|
"Circular reference detected in component tree",
|
|
2033
2145
|
element,
|
|
@@ -2035,32 +2147,31 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
2035
2147
|
["Remove the circular reference", "Use lazy loading to break the cycle"]
|
|
2036
2148
|
);
|
|
2037
2149
|
}
|
|
2038
|
-
|
|
2039
|
-
options.seenObjects.add(element);
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
if (options.enableMonitoring && this.cache) {
|
|
2150
|
+
options.seenObjects.add(tracked);
|
|
2043
2151
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
if (cached) {
|
|
2049
|
-
this.recordPerformance(tagName, startTime, true);
|
|
2050
|
-
return cached.value;
|
|
2051
|
-
}
|
|
2052
|
-
} catch {
|
|
2053
|
-
}
|
|
2152
|
+
try {
|
|
2153
|
+
return this.renderElementContent(tagName, element, options, depth, path);
|
|
2154
|
+
} finally {
|
|
2155
|
+
if (tracked) options.seenObjects.delete(tracked);
|
|
2054
2156
|
}
|
|
2157
|
+
}
|
|
2158
|
+
renderElementContent(tagName, element, options, depth = 0, path = null) {
|
|
2159
|
+
const startTime = options.enableMonitoring ? performance.now() : 0;
|
|
2055
2160
|
if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
|
|
2056
2161
|
const html2 = isVoidElement(tagName) ? `<${tagName}>` : `<${tagName}>${escapeHtml(String(element))}</${tagName}>`;
|
|
2057
|
-
this.cacheIfStatic(tagName, element, html2, options);
|
|
2058
2162
|
this.recordPerformance(tagName, startTime, false);
|
|
2059
2163
|
return html2;
|
|
2060
2164
|
}
|
|
2061
2165
|
if (typeof element === "function") {
|
|
2062
|
-
|
|
2063
|
-
|
|
2166
|
+
let result;
|
|
2167
|
+
try {
|
|
2168
|
+
result = this.executeFunctionComponent(element, depth);
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
if (typeof options.onError !== "function") throw error;
|
|
2171
|
+
const replacement = options.onError(error, { path: formatRenderPath(path) });
|
|
2172
|
+
return this.renderComponent(replacement, options, depth + 1, childPath(path, "()"));
|
|
2173
|
+
}
|
|
2174
|
+
return this.renderElement(tagName, result, options, depth, childPath(path, "()"));
|
|
2064
2175
|
}
|
|
2065
2176
|
if (element && typeof element === "object") {
|
|
2066
2177
|
return this.renderObjectElement(tagName, element, options, depth, path);
|
|
@@ -2075,105 +2186,255 @@ var HTMLRenderer = class extends BaseRenderer {
|
|
|
2075
2186
|
return html;
|
|
2076
2187
|
}
|
|
2077
2188
|
/**
|
|
2078
|
-
*
|
|
2189
|
+
* Render complex object elements with attributes and children
|
|
2079
2190
|
*/
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2191
|
+
renderObjectElement(tagName, element, options, depth = 0, path = null) {
|
|
2192
|
+
const startTime = options.enableMonitoring ? performance.now() : 0;
|
|
2193
|
+
const parts = elementParts(tagName, element);
|
|
2194
|
+
let html = parts.open + parts.content;
|
|
2195
|
+
if (parts.children) {
|
|
2196
|
+
const forbidden = FORBIDDEN_CHILDREN[tagName.toLowerCase()];
|
|
2197
|
+
for (let index = 0; index < parts.children.length; index++) {
|
|
2198
|
+
const child = parts.children[index];
|
|
2199
|
+
const segment = childPath(path, `children[${index}]`);
|
|
2200
|
+
checkNesting(tagName, forbidden, child, segment);
|
|
2201
|
+
html += this.renderComponent(child, options, depth + 1, segment);
|
|
2091
2202
|
}
|
|
2092
2203
|
}
|
|
2204
|
+
html += parts.close;
|
|
2205
|
+
this.recordPerformance(tagName, startTime, false);
|
|
2206
|
+
return html;
|
|
2093
2207
|
}
|
|
2094
2208
|
/**
|
|
2095
|
-
*
|
|
2209
|
+
* Streaming counterpart of renderComponent: yields HTML pieces. Elements
|
|
2210
|
+
* with many children are streamed child by child; everything else goes
|
|
2211
|
+
* through the synchronous renderer, which is exact and faster, so the
|
|
2212
|
+
* streamed output is the same as render()'s by construction.
|
|
2096
2213
|
*/
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2214
|
+
async *streamComponent(component, options, depth = 0, path = null) {
|
|
2215
|
+
if (component === null || component === void 0) return;
|
|
2216
|
+
this.validateDepth(depth);
|
|
2217
|
+
if (typeof component === "function") {
|
|
2218
|
+
const result = this.runFunctionComponent(component, options, depth, path);
|
|
2219
|
+
yield* this.streamComponent(result, options, depth + 1, childPath(path, "()"));
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (Array.isArray(component)) {
|
|
2223
|
+
yield* this.streamTracked(component, options, path, async function* (renderer) {
|
|
2224
|
+
for (let index = 0; index < component.length; index++) {
|
|
2225
|
+
yield* renderer.streamComponent(component[index], options, depth + 1, childPath(path, `[${index}]`));
|
|
2106
2226
|
}
|
|
2227
|
+
});
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
if (typeof component === "object" && !isTrustedContent(component) && component.__isLazy !== true) {
|
|
2231
|
+
const { type, value } = this.processComponentType(component);
|
|
2232
|
+
if (type === "element") {
|
|
2233
|
+
yield* this.streamTracked(component, options, path, async function* (renderer) {
|
|
2234
|
+
for (const tagName of Object.keys(value)) {
|
|
2235
|
+
yield* renderer.streamElement(tagName, value[tagName], options, depth, childPath(path, tagName));
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
return;
|
|
2107
2239
|
}
|
|
2108
2240
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
if (
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2241
|
+
yield this.renderComponent(component, options, depth, path);
|
|
2242
|
+
}
|
|
2243
|
+
async *streamElement(tagName, element, options, depth, path) {
|
|
2244
|
+
if (!element || typeof element !== "object" || Array.isArray(element) || !shouldStream(element.children)) {
|
|
2245
|
+
yield this.renderElement(tagName, element, options, depth, path);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
yield* this.streamTracked(element, options, path, async function* (renderer) {
|
|
2249
|
+
const parts = elementParts(tagName, element);
|
|
2250
|
+
yield parts.open + parts.content;
|
|
2251
|
+
if (parts.children) {
|
|
2252
|
+
const forbidden = FORBIDDEN_CHILDREN[tagName.toLowerCase()];
|
|
2253
|
+
for (let index = 0; index < parts.children.length; index++) {
|
|
2254
|
+
const child = parts.children[index];
|
|
2255
|
+
const segment = childPath(path, `children[${index}]`);
|
|
2256
|
+
checkNesting(tagName, forbidden, child, segment);
|
|
2257
|
+
yield* renderer.streamComponent(child, options, depth + 1, segment);
|
|
2117
2258
|
}
|
|
2118
2259
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2260
|
+
yield parts.close;
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Run `body` with `value` on the ancestor path used for cycle detection.
|
|
2265
|
+
*/
|
|
2266
|
+
async *streamTracked(value, options, path, body) {
|
|
2267
|
+
if (options.seenObjects.has(value)) {
|
|
2268
|
+
throw new RenderingError(
|
|
2269
|
+
"Circular reference detected in component tree",
|
|
2270
|
+
value,
|
|
2271
|
+
{ path: formatRenderPath(path) },
|
|
2272
|
+
["Remove the circular reference", "Use lazy loading to break the cycle"]
|
|
2273
|
+
);
|
|
2121
2274
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2275
|
+
options.seenObjects.add(value);
|
|
2276
|
+
try {
|
|
2277
|
+
yield* body(this);
|
|
2278
|
+
} finally {
|
|
2279
|
+
options.seenObjects.delete(value);
|
|
2127
2280
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
var STREAM_MIN_CHILDREN = 8;
|
|
2284
|
+
function shouldStream(children) {
|
|
2285
|
+
if (children === void 0 || children === null) return false;
|
|
2286
|
+
const list = Array.isArray(children) ? children : [children];
|
|
2287
|
+
if (list.length >= STREAM_MIN_CHILDREN) return true;
|
|
2288
|
+
for (const child of list) {
|
|
2289
|
+
if (typeof child === "function" || Array.isArray(child)) return true;
|
|
2290
|
+
if (child && typeof child === "object") {
|
|
2291
|
+
for (const key in child) {
|
|
2292
|
+
const content = child[key];
|
|
2293
|
+
if (typeof content === "function") return true;
|
|
2294
|
+
if (content && typeof content === "object" && content.children !== void 0 && content.children !== null) return true;
|
|
2142
2295
|
}
|
|
2143
2296
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2297
|
+
}
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2300
|
+
function elementParts(tagName, element) {
|
|
2301
|
+
const { children, text: rawText, html: rawHtml } = element || {};
|
|
2302
|
+
const attributeString = formatAttributes(element, RESERVED_PROPS);
|
|
2303
|
+
const open = attributeString ? `<${tagName} ${attributeString}>` : `<${tagName}>`;
|
|
2304
|
+
if (isVoidElement(tagName)) {
|
|
2305
|
+
return { open, content: "", children: null, close: "" };
|
|
2306
|
+
}
|
|
2307
|
+
const close = `</${tagName}>`;
|
|
2308
|
+
const html = typeof rawHtml === "function" ? rawHtml() : rawHtml;
|
|
2309
|
+
if (html !== void 0 && html !== null) {
|
|
2310
|
+
return { open, content: isTrustedContent(html) ? html.__html : String(html), children: null, close };
|
|
2311
|
+
}
|
|
2312
|
+
const text = typeof rawText === "function" && !isTrustedContent(rawText) ? rawText() : rawText;
|
|
2313
|
+
if (isTrustedContent(text)) {
|
|
2314
|
+
return { open, content: text.__html, children: null, close };
|
|
2315
|
+
}
|
|
2316
|
+
let content = "";
|
|
2317
|
+
if (text !== void 0 && text !== null) {
|
|
2318
|
+
const raw = String(text);
|
|
2319
|
+
if (tagName === "script" || tagName === "style") {
|
|
2320
|
+
content = raw.replace(/<\/(script)/gi, "<\\/$1").replace(/<\/(style)/gi, "<\\/$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2321
|
+
} else {
|
|
2322
|
+
content = escapeHtml(raw);
|
|
2156
2323
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2324
|
+
}
|
|
2325
|
+
const normalized = children !== void 0 && children !== null ? normalizeChildren(children) : null;
|
|
2326
|
+
return { open, content, children: normalized && normalized.length > 0 ? normalized : null, close };
|
|
2327
|
+
}
|
|
2328
|
+
function checkNesting(tagName, forbidden, child, path) {
|
|
2329
|
+
if (forbidden && child && typeof child === "object" && !Array.isArray(child)) {
|
|
2330
|
+
const childTagName = Object.keys(child)[0];
|
|
2331
|
+
if (childTagName && forbidden.has(childTagName.toLowerCase())) {
|
|
2332
|
+
validateNesting(tagName, childTagName, formatRenderPath(path));
|
|
2163
2333
|
}
|
|
2164
|
-
this.recordPerformance(tagName, startTime, false);
|
|
2165
|
-
return html;
|
|
2166
2334
|
}
|
|
2167
|
-
}
|
|
2335
|
+
}
|
|
2168
2336
|
function render(component, options = {}) {
|
|
2169
2337
|
const mergedOptions = {
|
|
2170
|
-
enableCache: true,
|
|
2171
2338
|
enableMonitoring: false,
|
|
2172
2339
|
...options
|
|
2173
2340
|
};
|
|
2174
2341
|
const renderer = new HTMLRenderer(mergedOptions);
|
|
2175
2342
|
return renderer.render(component, mergedOptions);
|
|
2176
2343
|
}
|
|
2344
|
+
async function* renderToStream(component, options = {}) {
|
|
2345
|
+
const { chunkSize = 8192, ...renderOptions } = options;
|
|
2346
|
+
const renderer = new HTMLRenderer({ enableMonitoring: false, ...renderOptions, enableCache: false });
|
|
2347
|
+
const config = { ...renderer.config, seenObjects: /* @__PURE__ */ new WeakSet() };
|
|
2348
|
+
assertNotThenable(component, "root");
|
|
2349
|
+
if (config.validateInput && !renderer.isValidComponent(component)) {
|
|
2350
|
+
throw new Error("Invalid component structure");
|
|
2351
|
+
}
|
|
2352
|
+
const minifier = config.minify ? createStreamMinifier() : null;
|
|
2353
|
+
let buffer = "";
|
|
2354
|
+
for await (const piece of renderer.streamComponent(component, config, 0, null)) {
|
|
2355
|
+
buffer += minifier ? minifier.push(piece) : piece;
|
|
2356
|
+
if (buffer.length >= chunkSize) {
|
|
2357
|
+
yield buffer;
|
|
2358
|
+
buffer = "";
|
|
2359
|
+
await yieldToEventLoop();
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
if (minifier) buffer += minifier.end();
|
|
2363
|
+
if (buffer) yield buffer;
|
|
2364
|
+
}
|
|
2365
|
+
var yieldToEventLoop = typeof setImmediate === "function" ? () => new Promise((resolve) => setImmediate(resolve)) : () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
2366
|
+
function waitForDrain(response) {
|
|
2367
|
+
if (response.destroyed) return Promise.resolve(false);
|
|
2368
|
+
return new Promise((resolve) => {
|
|
2369
|
+
const settle = (drained) => () => {
|
|
2370
|
+
response.off("drain", onDrain);
|
|
2371
|
+
response.off("close", onClose);
|
|
2372
|
+
response.off("error", onClose);
|
|
2373
|
+
resolve(drained);
|
|
2374
|
+
};
|
|
2375
|
+
const onDrain = settle(true);
|
|
2376
|
+
const onClose = settle(false);
|
|
2377
|
+
response.on("drain", onDrain);
|
|
2378
|
+
response.on("close", onClose);
|
|
2379
|
+
response.on("error", onClose);
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
var streamingUtils = {
|
|
2383
|
+
/**
|
|
2384
|
+
* Collect all chunks into a single string
|
|
2385
|
+
*/
|
|
2386
|
+
async collectChunks(chunkGenerator) {
|
|
2387
|
+
let html = "";
|
|
2388
|
+
for await (const chunk of chunkGenerator) {
|
|
2389
|
+
html += chunk;
|
|
2390
|
+
}
|
|
2391
|
+
return html;
|
|
2392
|
+
},
|
|
2393
|
+
/**
|
|
2394
|
+
* Stream directly to a Node.js response, with backpressure.
|
|
2395
|
+
*
|
|
2396
|
+
* Resolves with the number of bytes written once the response has ended.
|
|
2397
|
+
* If the client disconnects first, rendering stops (the generator is
|
|
2398
|
+
* closed) and it resolves with the bytes written so far without ending
|
|
2399
|
+
* the response. If rendering fails, the response is destroyed and it
|
|
2400
|
+
* rejects with the rendering error.
|
|
2401
|
+
*/
|
|
2402
|
+
async streamToResponse(chunkGenerator, response) {
|
|
2403
|
+
let totalBytes = 0;
|
|
2404
|
+
if (!response.headersSent && !response.getHeader?.("Content-Type")) {
|
|
2405
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2406
|
+
}
|
|
2407
|
+
try {
|
|
2408
|
+
for await (const chunk of chunkGenerator) {
|
|
2409
|
+
if (response.destroyed) return totalBytes;
|
|
2410
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
2411
|
+
if (!response.write(chunk) && !await waitForDrain(response)) {
|
|
2412
|
+
return totalBytes;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
response.destroy(error);
|
|
2417
|
+
throw error;
|
|
2418
|
+
}
|
|
2419
|
+
response.end();
|
|
2420
|
+
return totalBytes;
|
|
2421
|
+
},
|
|
2422
|
+
/**
|
|
2423
|
+
* Stream with progress callback
|
|
2424
|
+
*/
|
|
2425
|
+
async *streamWithProgress(chunkGenerator, onProgress) {
|
|
2426
|
+
let totalBytes = 0;
|
|
2427
|
+
let chunkCount = 0;
|
|
2428
|
+
for await (const chunk of chunkGenerator) {
|
|
2429
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
2430
|
+
chunkCount++;
|
|
2431
|
+
if (onProgress) {
|
|
2432
|
+
onProgress({ chunkCount, totalBytes, chunk });
|
|
2433
|
+
}
|
|
2434
|
+
yield chunk;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2177
2438
|
|
|
2178
2439
|
// src/components/component-system.js
|
|
2179
2440
|
var COMPONENT_REGISTRY = /* @__PURE__ */ new Map();
|
|
@@ -2220,7 +2481,7 @@ var ComponentState = class {
|
|
|
2220
2481
|
try {
|
|
2221
2482
|
listener(newState, oldState);
|
|
2222
2483
|
} catch (_error) {
|
|
2223
|
-
console.error("State listener
|
|
2484
|
+
console.error("State listener error:", _error);
|
|
2224
2485
|
}
|
|
2225
2486
|
});
|
|
2226
2487
|
this.isUpdating = false;
|
|
@@ -2389,7 +2650,7 @@ var Component = class _Component {
|
|
|
2389
2650
|
return this.rendered;
|
|
2390
2651
|
} catch (_error) {
|
|
2391
2652
|
this.handleError(_error);
|
|
2392
|
-
return { div: { className: "component-
|
|
2653
|
+
return { div: { className: "component-error", text: `Error in ${this.name}` } };
|
|
2393
2654
|
}
|
|
2394
2655
|
}
|
|
2395
2656
|
/**
|
|
@@ -2635,7 +2896,7 @@ function lazy(factory, options = {}) {
|
|
|
2635
2896
|
if (onError) {
|
|
2636
2897
|
onError(_error);
|
|
2637
2898
|
} else {
|
|
2638
|
-
console.error("Lazy evaluation
|
|
2899
|
+
console.error("Lazy evaluation error:", _error);
|
|
2639
2900
|
}
|
|
2640
2901
|
return fallback;
|
|
2641
2902
|
} finally {
|
|
@@ -2738,27 +2999,32 @@ function evaluateWithTimeout(factory, timeout, args, fallback) {
|
|
|
2738
2999
|
}
|
|
2739
3000
|
}).catch(() => fallback);
|
|
2740
3001
|
}
|
|
3002
|
+
var functionIds = /* @__PURE__ */ new WeakMap();
|
|
3003
|
+
var nextFunctionId = 0;
|
|
3004
|
+
function functionId(fn) {
|
|
3005
|
+
let id = functionIds.get(fn);
|
|
3006
|
+
if (id === void 0) {
|
|
3007
|
+
id = ++nextFunctionId;
|
|
3008
|
+
functionIds.set(fn, id);
|
|
3009
|
+
}
|
|
3010
|
+
return id;
|
|
3011
|
+
}
|
|
3012
|
+
function serializeMemoKey(value) {
|
|
3013
|
+
return JSON.stringify(value, (_key, v) => typeof v === "function" ? `\0fn:${functionId(v)}` : v);
|
|
3014
|
+
}
|
|
2741
3015
|
function memo(fn, options = {}) {
|
|
2742
3016
|
const {
|
|
2743
3017
|
// Caching strategy
|
|
2744
3018
|
strategy = "lru",
|
|
2745
3019
|
// 'lru', 'ttl', 'weak', 'simple'
|
|
2746
3020
|
maxSize = 100,
|
|
2747
|
-
// Maximum cache entries
|
|
3021
|
+
// Maximum cache entries ('lru' and 'ttl')
|
|
2748
3022
|
ttl = null,
|
|
2749
3023
|
// Time to live in milliseconds
|
|
2750
3024
|
// Key generation
|
|
2751
3025
|
keyFn = null,
|
|
2752
3026
|
// Custom key function
|
|
2753
|
-
keySerializer =
|
|
2754
|
-
// Default serialization
|
|
2755
|
-
// Comparison
|
|
2756
|
-
// eslint-disable-next-line no-unused-vars
|
|
2757
|
-
compareFn = null,
|
|
2758
|
-
// Custom equality comparison
|
|
2759
|
-
// eslint-disable-next-line no-unused-vars
|
|
2760
|
-
shallow = false,
|
|
2761
|
-
// Shallow comparison for objects
|
|
3027
|
+
keySerializer = serializeMemoKey,
|
|
2762
3028
|
// Lifecycle hooks
|
|
2763
3029
|
onHit = null,
|
|
2764
3030
|
// Called on cache hit
|
|
@@ -2773,63 +3039,74 @@ function memo(fn, options = {}) {
|
|
|
2773
3039
|
debug = false
|
|
2774
3040
|
// Debug logging
|
|
2775
3041
|
} = options;
|
|
2776
|
-
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
case "weak":
|
|
2786
|
-
cache = /* @__PURE__ */ new WeakMap();
|
|
2787
|
-
break;
|
|
2788
|
-
default:
|
|
2789
|
-
cache = /* @__PURE__ */ new Map();
|
|
2790
|
-
}
|
|
3042
|
+
const statsData = { hits: 0, misses: 0, evictions: 0 };
|
|
3043
|
+
const expiresIn = strategy === "ttl" && ttl === null ? 5e3 : ttl;
|
|
3044
|
+
const store = strategy === "simple" ? /* @__PURE__ */ new Map() : new LRUCache(maxSize, {
|
|
3045
|
+
onEvict: (key, entry) => {
|
|
3046
|
+
statsData.evictions++;
|
|
3047
|
+
if (onEvict) onEvict(key, entry.value);
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
const weakStore = strategy === "weak" ? /* @__PURE__ */ new WeakMap() : null;
|
|
2791
3051
|
const generateKey = keyFn || ((...args) => {
|
|
2792
3052
|
if (args.length === 0) return "__empty__";
|
|
2793
3053
|
if (args.length === 1) return keySerializer(args[0]);
|
|
2794
3054
|
return keySerializer(args);
|
|
2795
3055
|
});
|
|
3056
|
+
const isFresh = (entry) => entry.expires === null || Date.now() < entry.expires;
|
|
3057
|
+
const lookup = (args) => {
|
|
3058
|
+
if (weakStore) {
|
|
3059
|
+
const [first] = args;
|
|
3060
|
+
const cacheable = first !== null && (typeof first === "object" || typeof first === "function");
|
|
3061
|
+
return { cacheable, key: first, entries: weakStore };
|
|
3062
|
+
}
|
|
3063
|
+
try {
|
|
3064
|
+
return { cacheable: true, key: generateKey(...args), entries: store };
|
|
3065
|
+
} catch {
|
|
3066
|
+
return { cacheable: false };
|
|
3067
|
+
}
|
|
3068
|
+
};
|
|
2796
3069
|
const memoizedFn = (...args) => {
|
|
2797
|
-
const key =
|
|
2798
|
-
if (
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3070
|
+
const { cacheable, key, entries } = lookup(args);
|
|
3071
|
+
if (!cacheable) return fn(...args);
|
|
3072
|
+
const cached = entries.get(key);
|
|
3073
|
+
if (cached !== void 0) {
|
|
3074
|
+
if (isFresh(cached)) {
|
|
3075
|
+
if (debug) console.log(`Memo cache hit for key: ${String(key)}`);
|
|
2802
3076
|
if (onHit) onHit(key, cached.value, args);
|
|
2803
|
-
|
|
2804
|
-
return cached.value
|
|
2805
|
-
} else {
|
|
2806
|
-
cache.delete(key);
|
|
3077
|
+
statsData.hits++;
|
|
3078
|
+
return cached.value;
|
|
2807
3079
|
}
|
|
3080
|
+
entries.delete(key);
|
|
2808
3081
|
}
|
|
2809
|
-
if (debug) console.log(`Memo cache miss for key: ${key}`);
|
|
3082
|
+
if (debug) console.log(`Memo cache miss for key: ${String(key)}`);
|
|
2810
3083
|
if (onMiss) onMiss(key, args);
|
|
2811
|
-
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3084
|
+
statsData.misses++;
|
|
3085
|
+
const value = fn(...args);
|
|
3086
|
+
entries.set(key, { value, expires: expiresIn ? Date.now() + expiresIn : null });
|
|
3087
|
+
return value;
|
|
3088
|
+
};
|
|
3089
|
+
memoizedFn.cache = weakStore || (store instanceof LRUCache ? store.cache : store);
|
|
3090
|
+
memoizedFn.clear = () => {
|
|
3091
|
+
if (!weakStore) store.clear();
|
|
3092
|
+
};
|
|
3093
|
+
memoizedFn.delete = (key) => (weakStore || store).delete(key);
|
|
3094
|
+
memoizedFn.has = (key) => {
|
|
3095
|
+
const entry = (weakStore || store).get(key);
|
|
3096
|
+
return entry !== void 0 && isFresh(entry);
|
|
2816
3097
|
};
|
|
2817
|
-
memoizedFn.
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
memoizedFn.has = (key) => cache.has(key);
|
|
2821
|
-
memoizedFn.size = () => cache.size;
|
|
2822
|
-
if (stats_data) {
|
|
2823
|
-
memoizedFn.stats = () => ({ ...stats_data });
|
|
3098
|
+
memoizedFn.size = () => weakStore ? void 0 : store.size;
|
|
3099
|
+
if (stats) {
|
|
3100
|
+
memoizedFn.stats = () => ({ ...statsData });
|
|
2824
3101
|
memoizedFn.resetStats = () => {
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3102
|
+
statsData.hits = 0;
|
|
3103
|
+
statsData.misses = 0;
|
|
3104
|
+
statsData.evictions = 0;
|
|
2828
3105
|
};
|
|
2829
3106
|
}
|
|
2830
3107
|
memoizedFn.refresh = (...args) => {
|
|
2831
|
-
const key =
|
|
2832
|
-
|
|
3108
|
+
const { cacheable, key, entries } = lookup(args);
|
|
3109
|
+
if (cacheable) entries.delete(key);
|
|
2833
3110
|
return memoizedFn(...args);
|
|
2834
3111
|
};
|
|
2835
3112
|
return memoizedFn;
|
|
@@ -2893,66 +3170,6 @@ var LRUCache = class {
|
|
|
2893
3170
|
return this.cache.size;
|
|
2894
3171
|
}
|
|
2895
3172
|
};
|
|
2896
|
-
var TTLCache = class {
|
|
2897
|
-
constructor(ttl, options = {}) {
|
|
2898
|
-
this.ttl = ttl;
|
|
2899
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
2900
|
-
this.timers = /* @__PURE__ */ new Map();
|
|
2901
|
-
this.onEvict = options.onEvict;
|
|
2902
|
-
}
|
|
2903
|
-
get(key) {
|
|
2904
|
-
if (this.cache.has(key)) {
|
|
2905
|
-
const entry = this.cache.get(key);
|
|
2906
|
-
if (Date.now() < entry.expires) {
|
|
2907
|
-
return entry.value;
|
|
2908
|
-
} else {
|
|
2909
|
-
this.delete(key);
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
return void 0;
|
|
2913
|
-
}
|
|
2914
|
-
set(key, value) {
|
|
2915
|
-
if (this.timers.has(key)) {
|
|
2916
|
-
clearTimeout(this.timers.get(key));
|
|
2917
|
-
}
|
|
2918
|
-
const expires = Date.now() + this.ttl;
|
|
2919
|
-
this.cache.set(key, { value, expires });
|
|
2920
|
-
const timer = setTimeout(() => {
|
|
2921
|
-
this.delete(key);
|
|
2922
|
-
}, this.ttl);
|
|
2923
|
-
this.timers.set(key, timer);
|
|
2924
|
-
}
|
|
2925
|
-
has(key) {
|
|
2926
|
-
if (this.cache.has(key)) {
|
|
2927
|
-
const entry = this.cache.get(key);
|
|
2928
|
-
return Date.now() < entry.expires;
|
|
2929
|
-
}
|
|
2930
|
-
return false;
|
|
2931
|
-
}
|
|
2932
|
-
delete(key) {
|
|
2933
|
-
const had = this.cache.has(key);
|
|
2934
|
-
if (had) {
|
|
2935
|
-
const entry = this.cache.get(key);
|
|
2936
|
-
this.cache.delete(key);
|
|
2937
|
-
if (this.timers.has(key)) {
|
|
2938
|
-
clearTimeout(this.timers.get(key));
|
|
2939
|
-
this.timers.delete(key);
|
|
2940
|
-
}
|
|
2941
|
-
if (this.onEvict) {
|
|
2942
|
-
this.onEvict(key, entry.value);
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
return had;
|
|
2946
|
-
}
|
|
2947
|
-
clear() {
|
|
2948
|
-
this.timers.forEach((timer) => clearTimeout(timer));
|
|
2949
|
-
this.timers.clear();
|
|
2950
|
-
this.cache.clear();
|
|
2951
|
-
}
|
|
2952
|
-
get size() {
|
|
2953
|
-
return this.cache.size;
|
|
2954
|
-
}
|
|
2955
|
-
};
|
|
2956
3173
|
function shallowEqual(a, b) {
|
|
2957
3174
|
if (a === b) return true;
|
|
2958
3175
|
if (!a || !b) return false;
|
|
@@ -3146,7 +3363,9 @@ function createStateContainer(initialState, options) {
|
|
|
3146
3363
|
storageKey,
|
|
3147
3364
|
storage,
|
|
3148
3365
|
reducer,
|
|
3149
|
-
|
|
3366
|
+
// Defaulted here: withStateUtils.shared() passes {}, and spreading
|
|
3367
|
+
// an undefined middleware list threw "middleware is not iterable".
|
|
3368
|
+
middleware = [],
|
|
3150
3369
|
validator,
|
|
3151
3370
|
onStateChange,
|
|
3152
3371
|
debug
|
|
@@ -3201,7 +3420,7 @@ function createStateContainer(initialState, options) {
|
|
|
3201
3420
|
try {
|
|
3202
3421
|
listener(state, prevState);
|
|
3203
3422
|
} catch (_error) {
|
|
3204
|
-
if (debug) console.error("State listener
|
|
3423
|
+
if (debug) console.error("State listener error:", _error);
|
|
3205
3424
|
}
|
|
3206
3425
|
});
|
|
3207
3426
|
if (onStateChange) {
|
|
@@ -3345,8 +3564,10 @@ var withStateUtils = {
|
|
|
3345
3564
|
}
|
|
3346
3565
|
}),
|
|
3347
3566
|
/**
|
|
3348
|
-
* State with loading/
|
|
3567
|
+
* State with loading/error handling
|
|
3349
3568
|
*/
|
|
3569
|
+
// `_loading` / `_error` are the documented state keys (docs/components/state.md).
|
|
3570
|
+
/* eslint-disable no-restricted-syntax */
|
|
3350
3571
|
withLoading: async (initialState) => withState({
|
|
3351
3572
|
...initialState,
|
|
3352
3573
|
_loading: false,
|
|
@@ -3376,6 +3597,7 @@ var withStateUtils = {
|
|
|
3376
3597
|
}
|
|
3377
3598
|
}
|
|
3378
3599
|
}),
|
|
3600
|
+
/* eslint-enable no-restricted-syntax */
|
|
3379
3601
|
/**
|
|
3380
3602
|
* State with undo/redo functionality
|
|
3381
3603
|
*/
|
|
@@ -3476,7 +3698,7 @@ var LIFECYCLE_PHASES = {
|
|
|
3476
3698
|
UPDATED: "updated",
|
|
3477
3699
|
BEFORE_UNMOUNT: "beforeUnmount",
|
|
3478
3700
|
UNMOUNTED: "unmounted",
|
|
3479
|
-
ERROR: "
|
|
3701
|
+
ERROR: "error"
|
|
3480
3702
|
};
|
|
3481
3703
|
var componentInstances = /* @__PURE__ */ new WeakMap();
|
|
3482
3704
|
var componentRegistry = /* @__PURE__ */ new Map();
|
|
@@ -3796,7 +4018,7 @@ var ComponentEventSystem = class {
|
|
|
3796
4018
|
handler(event);
|
|
3797
4019
|
} catch (_error) {
|
|
3798
4020
|
globalErrorHandler.handle(_error, {
|
|
3799
|
-
type: "event-handler-
|
|
4021
|
+
type: "event-handler-error",
|
|
3800
4022
|
context: { event, handler: handler.toString() }
|
|
3801
4023
|
});
|
|
3802
4024
|
}
|
|
@@ -3815,7 +4037,7 @@ var ComponentEventSystem = class {
|
|
|
3815
4037
|
handler(event);
|
|
3816
4038
|
} catch (_error) {
|
|
3817
4039
|
globalErrorHandler.handle(_error, {
|
|
3818
|
-
type: "global-event-handler-
|
|
4040
|
+
type: "global-event-handler-error",
|
|
3819
4041
|
context: { event, handler: handler.toString() }
|
|
3820
4042
|
});
|
|
3821
4043
|
}
|
|
@@ -4138,6 +4360,7 @@ var ComponentCache = class {
|
|
|
4138
4360
|
this.cleanupTimer = setInterval(() => {
|
|
4139
4361
|
this.cleanup();
|
|
4140
4362
|
}, this.options.cleanupInterval);
|
|
4363
|
+
this.cleanupTimer.unref?.();
|
|
4141
4364
|
}
|
|
4142
4365
|
}
|
|
4143
4366
|
/**
|
|
@@ -4427,6 +4650,37 @@ function memoize(component, keyGenerator, options = {}) {
|
|
|
4427
4650
|
}
|
|
4428
4651
|
|
|
4429
4652
|
// src/components/error-boundary.js
|
|
4653
|
+
function isBrowser() {
|
|
4654
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
4655
|
+
}
|
|
4656
|
+
function resolveNestedComponents(node, depth = 0) {
|
|
4657
|
+
if (depth > 1e3) return node;
|
|
4658
|
+
if (typeof node === "function") {
|
|
4659
|
+
return resolveNestedComponents(node(), depth + 1);
|
|
4660
|
+
}
|
|
4661
|
+
if (Array.isArray(node)) {
|
|
4662
|
+
let changed = false;
|
|
4663
|
+
const resolved = node.map((child) => {
|
|
4664
|
+
const result = resolveNestedComponents(child, depth + 1);
|
|
4665
|
+
if (result !== child) changed = true;
|
|
4666
|
+
return result;
|
|
4667
|
+
});
|
|
4668
|
+
return changed ? resolved : node;
|
|
4669
|
+
}
|
|
4670
|
+
if (node && typeof node === "object" && !isTrustedContent(node)) {
|
|
4671
|
+
const keys = Object.keys(node);
|
|
4672
|
+
if (keys.length !== 1) return node;
|
|
4673
|
+
const tag = keys[0];
|
|
4674
|
+
const props = node[tag];
|
|
4675
|
+
if (props && typeof props === "object" && !Array.isArray(props) && props.children !== void 0) {
|
|
4676
|
+
const children = resolveNestedComponents(props.children, depth + 1);
|
|
4677
|
+
if (children !== props.children) {
|
|
4678
|
+
return { [tag]: { ...props, children } };
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
return node;
|
|
4683
|
+
}
|
|
4430
4684
|
var ErrorBoundaryState = class {
|
|
4431
4685
|
constructor() {
|
|
4432
4686
|
this.hasError = false;
|
|
@@ -4458,19 +4712,23 @@ function createErrorBoundary(options = {}) {
|
|
|
4458
4712
|
maxErrors = Infinity,
|
|
4459
4713
|
resetTimeout = null
|
|
4460
4714
|
} = options;
|
|
4461
|
-
const
|
|
4715
|
+
const sharedState = new ErrorBoundaryState();
|
|
4462
4716
|
let previousProps = {};
|
|
4463
4717
|
let resetTimer = null;
|
|
4464
4718
|
return function errorBoundaryWrapper(component) {
|
|
4465
4719
|
return function wrappedComponent(props = {}) {
|
|
4720
|
+
const persistent = isBrowser();
|
|
4721
|
+
const state = persistent ? sharedState : new ErrorBoundaryState();
|
|
4466
4722
|
try {
|
|
4467
|
-
if (resetOnPropsChange && shouldReset(props, previousProps, resetKeys)) {
|
|
4723
|
+
if (persistent && resetOnPropsChange && shouldReset(props, previousProps, resetKeys)) {
|
|
4468
4724
|
state.reset();
|
|
4469
4725
|
if (onReset) {
|
|
4470
4726
|
onReset();
|
|
4471
4727
|
}
|
|
4472
4728
|
}
|
|
4473
|
-
|
|
4729
|
+
if (persistent) {
|
|
4730
|
+
previousProps = { ...props };
|
|
4731
|
+
}
|
|
4474
4732
|
if (state.hasError) {
|
|
4475
4733
|
if (state.errorCount >= maxErrors) {
|
|
4476
4734
|
return typeof fallback === "function" ? fallback(state.error, state.errorInfo, { permanent: true }) : fallback;
|
|
@@ -4487,7 +4745,7 @@ function createErrorBoundary(options = {}) {
|
|
|
4487
4745
|
return fallbackComponent;
|
|
4488
4746
|
}
|
|
4489
4747
|
const result = typeof component === "function" ? component(props) : component;
|
|
4490
|
-
return result;
|
|
4748
|
+
return resolveNestedComponents(result);
|
|
4491
4749
|
} catch (error) {
|
|
4492
4750
|
const errorInfo = {
|
|
4493
4751
|
componentStack: error.stack,
|
|
@@ -4502,7 +4760,7 @@ function createErrorBoundary(options = {}) {
|
|
|
4502
4760
|
console.error("Error in onError callback:", callbackError);
|
|
4503
4761
|
}
|
|
4504
4762
|
}
|
|
4505
|
-
if (resetTimeout && !resetTimer) {
|
|
4763
|
+
if (persistent && resetTimeout && !resetTimer) {
|
|
4506
4764
|
resetTimer = setTimeout(() => {
|
|
4507
4765
|
state.reset();
|
|
4508
4766
|
resetTimer = null;
|
|
@@ -4618,9 +4876,10 @@ function createAsyncErrorBoundary(options = {}) {
|
|
|
4618
4876
|
} = options;
|
|
4619
4877
|
return function asyncBoundaryWrapper(asyncComponent) {
|
|
4620
4878
|
return async function wrappedAsyncComponent(props = {}) {
|
|
4879
|
+
let timer;
|
|
4621
4880
|
try {
|
|
4622
4881
|
const timeoutPromise = new Promise((_, reject) => {
|
|
4623
|
-
setTimeout(() => reject(new Error("Component load timeout")), timeout);
|
|
4882
|
+
timer = setTimeout(() => reject(new Error("Component load timeout")), timeout);
|
|
4624
4883
|
});
|
|
4625
4884
|
const result = await Promise.race([
|
|
4626
4885
|
typeof asyncComponent === "function" ? asyncComponent(props) : asyncComponent,
|
|
@@ -4632,6 +4891,8 @@ function createAsyncErrorBoundary(options = {}) {
|
|
|
4632
4891
|
onError(error, { props, async: true });
|
|
4633
4892
|
}
|
|
4634
4893
|
return typeof errorFallback === "function" ? errorFallback(error, { props }) : errorFallback;
|
|
4894
|
+
} finally {
|
|
4895
|
+
clearTimeout(timer);
|
|
4635
4896
|
}
|
|
4636
4897
|
};
|
|
4637
4898
|
};
|
|
@@ -4726,7 +4987,7 @@ function renderWithTemplate(component, options = {}) {
|
|
|
4726
4987
|
template = "<!DOCTYPE html>\n{{content}}"
|
|
4727
4988
|
} = options;
|
|
4728
4989
|
const html = renderWithMonitoring(component, options);
|
|
4729
|
-
return template.replace("{{content}}", html);
|
|
4990
|
+
return template.replace("{{content}}", () => html);
|
|
4730
4991
|
}
|
|
4731
4992
|
async function renderComponentFactory(componentFactory, factoryArgs, options = {}) {
|
|
4732
4993
|
const component = await Promise.resolve(
|
|
@@ -4914,7 +5175,8 @@ function renderComponentContent(obj) {
|
|
|
4914
5175
|
if (value === true) return attrName;
|
|
4915
5176
|
return `${attrName}="${escapeHTML(String(value))}"`;
|
|
4916
5177
|
}).join(" ");
|
|
4917
|
-
const
|
|
5178
|
+
const attrsPart = attrsStr ? ` ${attrsStr}` : "";
|
|
5179
|
+
const openTag = `<${tagName}${attrsPart}>`;
|
|
4918
5180
|
if ([
|
|
4919
5181
|
"area",
|
|
4920
5182
|
"base",
|
|
@@ -4931,7 +5193,7 @@ function renderComponentContent(obj) {
|
|
|
4931
5193
|
"track",
|
|
4932
5194
|
"wbr"
|
|
4933
5195
|
].includes(tagName)) {
|
|
4934
|
-
return
|
|
5196
|
+
return `<${tagName}${attrsPart} />`;
|
|
4935
5197
|
}
|
|
4936
5198
|
let content = "";
|
|
4937
5199
|
if (text !== void 0) {
|
|
@@ -5263,6 +5525,29 @@ var EventBus = class {
|
|
|
5263
5525
|
}
|
|
5264
5526
|
return false;
|
|
5265
5527
|
}
|
|
5528
|
+
/**
|
|
5529
|
+
* A view of this bus whose event and action names are prefixed with
|
|
5530
|
+
* `${scope}:`. It shares listeners with the bus, so `scope:event` can
|
|
5531
|
+
* still be observed from the unscoped bus. withEventBus({ scope }) and
|
|
5532
|
+
* emitEvent(name, { scope }) called this, but it didn't exist, so any
|
|
5533
|
+
* scoped usage threw "createScope is not a function".
|
|
5534
|
+
*
|
|
5535
|
+
* @param {string} scope - Scope name
|
|
5536
|
+
* @returns {Object} Scoped event bus
|
|
5537
|
+
*/
|
|
5538
|
+
createScope(scope) {
|
|
5539
|
+
const prefix = `${scope}:`;
|
|
5540
|
+
return {
|
|
5541
|
+
emit: (event, data) => this.emit(prefix + event, data),
|
|
5542
|
+
emitSync: (event, data) => this.emitSync(prefix + event, data),
|
|
5543
|
+
on: (event, listener, options) => this.on(prefix + event, listener, options),
|
|
5544
|
+
once: (event, listener, options) => this.once(prefix + event, listener, options),
|
|
5545
|
+
off: (event, listenerId) => this.off(prefix + event, listenerId),
|
|
5546
|
+
registerAction: (action, handler) => this.registerAction(prefix + action, handler),
|
|
5547
|
+
handleAction: (action, element, event, data) => this.handleAction(prefix + action, element, event, data),
|
|
5548
|
+
createScope: (child) => this.createScope(prefix + child)
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5266
5551
|
/**
|
|
5267
5552
|
* Remove all listeners for an event
|
|
5268
5553
|
*/
|
|
@@ -5876,7 +6161,8 @@ function withEventBus(options = {}) {
|
|
|
5876
6161
|
function EventBusComponent(props = {}, state = {}, context = {}) {
|
|
5877
6162
|
const bus = scope ? eventBus.createScope(scope) : eventBus;
|
|
5878
6163
|
const listenerIds = /* @__PURE__ */ new Map();
|
|
5879
|
-
|
|
6164
|
+
const interactive = typeof window !== "undefined" && typeof document !== "undefined";
|
|
6165
|
+
if (interactive) Object.entries(events).forEach(([event, handler]) => {
|
|
5880
6166
|
const listenerId = bus.on(event, (data, eventName) => {
|
|
5881
6167
|
if (typeof handler === "function") {
|
|
5882
6168
|
handler.call(this, data, eventName, { props, state, context });
|
|
@@ -5884,7 +6170,7 @@ function withEventBus(options = {}) {
|
|
|
5884
6170
|
});
|
|
5885
6171
|
listenerIds.set(event, listenerId);
|
|
5886
6172
|
});
|
|
5887
|
-
Object.entries(actions).forEach(([action, handler]) => {
|
|
6173
|
+
if (interactive) Object.entries(actions).forEach(([action, handler]) => {
|
|
5888
6174
|
bus.registerAction(action, (actionContext) => {
|
|
5889
6175
|
if (typeof handler === "function") {
|
|
5890
6176
|
handler.call(this, actionContext, { props, state, context });
|
|
@@ -5928,7 +6214,10 @@ function withEventBus(options = {}) {
|
|
|
5928
6214
|
originalUnmount.call(this);
|
|
5929
6215
|
};
|
|
5930
6216
|
} else {
|
|
5931
|
-
result
|
|
6217
|
+
Object.defineProperty(result, "__eventBusCleanup", {
|
|
6218
|
+
value: eventUtils.cleanup,
|
|
6219
|
+
configurable: true
|
|
6220
|
+
});
|
|
5932
6221
|
}
|
|
5933
6222
|
}
|
|
5934
6223
|
return result;
|
|
@@ -6256,6 +6545,7 @@ var eventSystem2 = {
|
|
|
6256
6545
|
registerAction: globalEventBus.registerAction.bind(globalEventBus),
|
|
6257
6546
|
registerActions: globalEventBus.registerActions.bind(globalEventBus),
|
|
6258
6547
|
handleAction: globalEventBus.handleAction.bind(globalEventBus),
|
|
6548
|
+
createScope: globalEventBus.createScope.bind(globalEventBus),
|
|
6259
6549
|
// Statistics and debugging
|
|
6260
6550
|
getStats: globalEventBus.getStats.bind(globalEventBus),
|
|
6261
6551
|
resetStats: globalEventBus.resetStats.bind(globalEventBus),
|
|
@@ -6359,23 +6649,74 @@ var compose = {
|
|
|
6359
6649
|
};
|
|
6360
6650
|
|
|
6361
6651
|
// src/index.js
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6652
|
+
function generateScopeId(cssText) {
|
|
6653
|
+
let hash = 2166136261;
|
|
6654
|
+
for (let i = 0; i < cssText.length; i++) {
|
|
6655
|
+
hash ^= cssText.charCodeAt(i);
|
|
6656
|
+
hash = Math.imul(hash, 16777619);
|
|
6657
|
+
}
|
|
6658
|
+
return `coh-${(hash >>> 0).toString(36)}`;
|
|
6659
|
+
}
|
|
6660
|
+
var GROUPING_AT_RULES = /* @__PURE__ */ new Set(["media", "supports", "container", "layer", "document", "scope"]);
|
|
6661
|
+
function splitSelectorList(prelude) {
|
|
6662
|
+
const parts = [];
|
|
6663
|
+
let depth = 0;
|
|
6664
|
+
let start = 0;
|
|
6665
|
+
for (let i = 0; i < prelude.length; i++) {
|
|
6666
|
+
const ch = prelude[i];
|
|
6667
|
+
if (ch === "(" || ch === "[") depth++;
|
|
6668
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
6669
|
+
else if (ch === "," && depth === 0) {
|
|
6670
|
+
parts.push(prelude.slice(start, i));
|
|
6671
|
+
start = i + 1;
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
parts.push(prelude.slice(start));
|
|
6675
|
+
return parts;
|
|
6676
|
+
}
|
|
6677
|
+
function scopeSelector(selector, scopeId) {
|
|
6678
|
+
const trimmed = selector.trim();
|
|
6679
|
+
if (!trimmed) return selector;
|
|
6680
|
+
if (trimmed.includes(":")) {
|
|
6681
|
+
return trimmed.replace(/([^:]+)(:.*)?/, `$1[${scopeId}]$2`);
|
|
6682
|
+
}
|
|
6683
|
+
return `${trimmed}[${scopeId}]`;
|
|
6365
6684
|
}
|
|
6366
6685
|
function scopeCSS(css, scopeId) {
|
|
6367
6686
|
if (!css || typeof css !== "string") return css;
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6687
|
+
let result = "";
|
|
6688
|
+
let i = 0;
|
|
6689
|
+
while (i < css.length) {
|
|
6690
|
+
const open = css.indexOf("{", i);
|
|
6691
|
+
if (open === -1) {
|
|
6692
|
+
result += css.slice(i);
|
|
6693
|
+
break;
|
|
6694
|
+
}
|
|
6695
|
+
let depth = 1;
|
|
6696
|
+
let j = open + 1;
|
|
6697
|
+
while (j < css.length && depth > 0) {
|
|
6698
|
+
if (css[j] === "{") depth++;
|
|
6699
|
+
else if (css[j] === "}") depth--;
|
|
6700
|
+
j++;
|
|
6701
|
+
}
|
|
6702
|
+
const body = css.slice(open + 1, depth === 0 ? j - 1 : j);
|
|
6703
|
+
const rawPrelude = css.slice(i, open);
|
|
6704
|
+
const semicolon = rawPrelude.lastIndexOf(";");
|
|
6705
|
+
const lead = semicolon === -1 ? "" : rawPrelude.slice(0, semicolon + 1);
|
|
6706
|
+
const prelude = semicolon === -1 ? rawPrelude : rawPrelude.slice(semicolon + 1);
|
|
6707
|
+
const trimmed = prelude.trim();
|
|
6708
|
+
if (trimmed.startsWith("@")) {
|
|
6709
|
+
const name = trimmed.slice(1).split(/[\s({]/)[0].toLowerCase();
|
|
6710
|
+
const inner = GROUPING_AT_RULES.has(name) ? scopeCSS(body, scopeId) : body;
|
|
6711
|
+
result += `${lead}${prelude}{${inner}}`;
|
|
6712
|
+
} else {
|
|
6713
|
+
const leading = prelude.match(/^\s*/)[0];
|
|
6714
|
+
const scoped = splitSelectorList(prelude).map((part) => scopeSelector(part, scopeId)).join(", ");
|
|
6715
|
+
result += `${lead}${leading}${scoped} {${body}}`;
|
|
6716
|
+
}
|
|
6717
|
+
i = j;
|
|
6718
|
+
}
|
|
6719
|
+
return result;
|
|
6379
6720
|
}
|
|
6380
6721
|
function applyScopeToElement(element, scopeId) {
|
|
6381
6722
|
if (typeof element === "string" || typeof element === "number" || !element) {
|
|
@@ -6403,10 +6744,7 @@ function applyScopeToElement(element, scopeId) {
|
|
|
6403
6744
|
return element;
|
|
6404
6745
|
}
|
|
6405
6746
|
function dangerouslySetInnerContent(content) {
|
|
6406
|
-
return
|
|
6407
|
-
__html: content,
|
|
6408
|
-
__trusted: true
|
|
6409
|
-
};
|
|
6747
|
+
return createTrustedContent(content);
|
|
6410
6748
|
}
|
|
6411
6749
|
function injectHydrationAttributes(component, options) {
|
|
6412
6750
|
if (!component || typeof component !== "object" || Array.isArray(component)) {
|
|
@@ -6438,20 +6776,42 @@ function Island(componentFn) {
|
|
|
6438
6776
|
});
|
|
6439
6777
|
};
|
|
6440
6778
|
}
|
|
6441
|
-
function
|
|
6779
|
+
function prepareRender(obj, options) {
|
|
6442
6780
|
const scoped = options.scoped ?? options.encapsulate ?? false;
|
|
6443
6781
|
const { scoped: _scoped, encapsulate: _encapsulate, hydratable: _hydratable, island: _island, ...rendererOptions } = options;
|
|
6444
|
-
let component =
|
|
6445
|
-
if (
|
|
6446
|
-
component = component
|
|
6782
|
+
let component = typeof obj === "function" ? obj(options) : obj;
|
|
6783
|
+
if (scoped) {
|
|
6784
|
+
component = renderScopedComponent(component);
|
|
6447
6785
|
}
|
|
6448
6786
|
if (_hydratable || _island) {
|
|
6449
6787
|
component = injectHydrationAttributes(component, { hydratable: _hydratable, island: _island });
|
|
6450
6788
|
}
|
|
6789
|
+
return { component, rendererOptions };
|
|
6790
|
+
}
|
|
6791
|
+
function render2(obj, options = {}) {
|
|
6792
|
+
const { component, rendererOptions } = prepareRender(obj, options);
|
|
6451
6793
|
return render(component, rendererOptions);
|
|
6452
6794
|
}
|
|
6795
|
+
function renderToStream2(obj, options = {}) {
|
|
6796
|
+
const { component, rendererOptions } = prepareRender(obj, options);
|
|
6797
|
+
return renderToStream(component, rendererOptions);
|
|
6798
|
+
}
|
|
6799
|
+
function collectStyleText(element, out = []) {
|
|
6800
|
+
if (Array.isArray(element)) {
|
|
6801
|
+
element.forEach((item) => collectStyleText(item, out));
|
|
6802
|
+
} else if (element && typeof element === "object") {
|
|
6803
|
+
for (const [tagName, props] of Object.entries(element)) {
|
|
6804
|
+
if (tagName === "style" && props && typeof props === "object" && typeof props.text === "string") {
|
|
6805
|
+
out.push(props.text);
|
|
6806
|
+
} else if (props && typeof props === "object" && props.children) {
|
|
6807
|
+
collectStyleText(props.children, out);
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
return out;
|
|
6812
|
+
}
|
|
6453
6813
|
function renderScopedComponent(component) {
|
|
6454
|
-
const scopeId = generateScopeId();
|
|
6814
|
+
const scopeId = generateScopeId(collectStyleText(component).join("\n"));
|
|
6455
6815
|
function processScopedElement(element) {
|
|
6456
6816
|
if (!element || typeof element !== "object") {
|
|
6457
6817
|
return element;
|
|
@@ -6482,21 +6842,13 @@ function renderScopedComponent(component) {
|
|
|
6482
6842
|
const scopedComponent = applyScopeToElement(processedComponent, scopeId);
|
|
6483
6843
|
return scopedComponent;
|
|
6484
6844
|
}
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
const
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
const result = component(props);
|
|
6493
|
-
memoCache.set(key, result);
|
|
6494
|
-
if (memoCache.size > 100) {
|
|
6495
|
-
const firstKey = memoCache.keys().next().value;
|
|
6496
|
-
memoCache.delete(firstKey);
|
|
6497
|
-
}
|
|
6498
|
-
return result;
|
|
6499
|
-
};
|
|
6845
|
+
function memo2(component, options = {}) {
|
|
6846
|
+
const withDefaultProps = (props = {}, ...rest) => component(props, ...rest);
|
|
6847
|
+
if (typeof options === "function") {
|
|
6848
|
+
const keyGenerator = options;
|
|
6849
|
+
return memo(withDefaultProps, { keyFn: (props = {}, ...rest) => keyGenerator(props, ...rest) });
|
|
6850
|
+
}
|
|
6851
|
+
return memo(withDefaultProps, options);
|
|
6500
6852
|
}
|
|
6501
6853
|
function validateComponent2(obj) {
|
|
6502
6854
|
if (!obj || typeof obj !== "object") {
|
|
@@ -6517,7 +6869,7 @@ function deepClone2(obj) {
|
|
|
6517
6869
|
}
|
|
6518
6870
|
return cloned;
|
|
6519
6871
|
}
|
|
6520
|
-
var VERSION = true ? "
|
|
6872
|
+
var VERSION = true ? "2.0.0-rc.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
6521
6873
|
var fp = {
|
|
6522
6874
|
/**
|
|
6523
6875
|
* Curried map: fp.map(fn)(array)
|
|
@@ -6529,6 +6881,7 @@ var fp = {
|
|
|
6529
6881
|
var coherent = {
|
|
6530
6882
|
// Core rendering
|
|
6531
6883
|
render: render2,
|
|
6884
|
+
renderToStream: renderToStream2,
|
|
6532
6885
|
// Shadow DOM (client-side only)
|
|
6533
6886
|
shadowDOM: shadow_dom_exports,
|
|
6534
6887
|
// Component system
|
|
@@ -6646,6 +6999,7 @@ export {
|
|
|
6646
6999
|
isLazy,
|
|
6647
7000
|
isPeerDependencyAvailable,
|
|
6648
7001
|
isTrustedContent,
|
|
7002
|
+
isValidAttributeName,
|
|
6649
7003
|
isVoidElement,
|
|
6650
7004
|
lazy,
|
|
6651
7005
|
componentUtils as lifecycleUtils,
|
|
@@ -6661,9 +7015,11 @@ export {
|
|
|
6661
7015
|
registerComponent,
|
|
6662
7016
|
render2 as render,
|
|
6663
7017
|
renderComponentFactory,
|
|
7018
|
+
renderToStream2 as renderToStream,
|
|
6664
7019
|
renderWithMonitoring,
|
|
6665
7020
|
renderWithTemplate,
|
|
6666
7021
|
shadow_dom_exports as shadowDOM,
|
|
7022
|
+
streamingUtils,
|
|
6667
7023
|
useHooks,
|
|
6668
7024
|
validateComponent2 as validateComponent,
|
|
6669
7025
|
validateNesting,
|