@server/next 0.28.3 → 0.28.5
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/package.json +1 -1
- package/src/jsx/jsx-runtime.js +44 -42
- package/src/jsx/jsx-runtime.test.jsx +46 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.5",
|
|
4
4
|
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
5
|
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "https://github.com/franciscop/server-next.git",
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -16,73 +16,63 @@ const SELFCLOSE = new Set(
|
|
|
16
16
|
"area,base,br,col,embed,hr,img,input,link,meta,source,track,wbr".split(","),
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
+
const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
20
|
+
|
|
19
21
|
const altAttrs = {
|
|
20
22
|
classname: "class",
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
//
|
|
24
|
-
const isValidChild = (child) => child || child === "" || child
|
|
25
|
+
// valid primitives only
|
|
26
|
+
const isValidChild = (child) => child === 0 || child === "" || !!child;
|
|
25
27
|
|
|
28
|
+
// CSS helpers
|
|
26
29
|
const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
|
|
27
30
|
|
|
28
31
|
const minifyCss = (str) =>
|
|
29
32
|
str
|
|
30
33
|
.replace(/\s+/g, " ")
|
|
31
|
-
.replace(
|
|
32
|
-
.replace(/(\w|\*) (\{)/g, "$1$2")
|
|
33
|
-
.replace(/(\}) (\w|\*)/g, "$1$2")
|
|
34
|
-
.replace(/(\{) (\w)/g, "$1$2")
|
|
35
|
-
.replace(/(\w)(:) /g, "$1$2")
|
|
36
|
-
.replace(/(;) (\})/g, "$1$2")
|
|
37
|
-
.replace(/(;) (\w)/g, "$1$2")
|
|
38
|
-
.replace(/;(\})/g, "$1")
|
|
39
|
-
.replace(/(\w), (\w)/g, "$1,$2")
|
|
40
|
-
.replace(/(\w), (\w)/g, "$1,$2")
|
|
41
|
-
.replace(/(\{) (\w)/g, "$1$2")
|
|
34
|
+
.replace(/\/\*[^*]*\*\//g, "")
|
|
42
35
|
.trim();
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
// React element detection (safe for your custom objects too)
|
|
46
38
|
const isReactElement = (val) =>
|
|
47
|
-
val !== null &&
|
|
48
|
-
typeof val === "object" &&
|
|
49
|
-
(val.$$typeof === REACT_ELEMENT_TYPE || // real React
|
|
50
|
-
("type" in val && "props" in val)); // your test objects
|
|
51
|
-
|
|
52
|
-
const resolve = (val) => {
|
|
53
|
-
while (typeof val === "function") val = val();
|
|
54
|
-
if (isReactElement(val)) {
|
|
55
|
-
val = jsx(val.type, val.props || {});
|
|
56
|
-
}
|
|
57
|
-
return val ?? "";
|
|
58
|
-
};
|
|
39
|
+
val !== null && typeof val === "object" && "type" in val && "props" in val;
|
|
59
40
|
|
|
41
|
+
// 🔥 CRITICAL: single-pass renderer (NO recursion cycles)
|
|
60
42
|
const renderChild = (child) => {
|
|
61
|
-
|
|
43
|
+
if (child == null || child === false) return "";
|
|
62
44
|
|
|
63
45
|
if (Array.isArray(child)) {
|
|
64
46
|
return child.map(renderChild).join("");
|
|
65
47
|
}
|
|
66
48
|
|
|
67
|
-
if (
|
|
68
|
-
return
|
|
49
|
+
if (typeof child === "function") {
|
|
50
|
+
return renderChild(child());
|
|
69
51
|
}
|
|
70
52
|
|
|
71
|
-
if (typeof child === "string") return
|
|
53
|
+
if (typeof child === "string") return child;
|
|
72
54
|
if (typeof child === "number") return String(child);
|
|
73
55
|
|
|
74
|
-
if (
|
|
56
|
+
if (isReactElement(child)) {
|
|
57
|
+
return jsx(child.type, child.props || {})();
|
|
58
|
+
}
|
|
75
59
|
|
|
76
60
|
console.warn("Unknown child:", child);
|
|
77
61
|
return "";
|
|
78
62
|
};
|
|
79
63
|
|
|
80
|
-
const jsx = (tag, { children, ...props }) => {
|
|
64
|
+
const jsx = (tag, { children, ...props } = {}) => {
|
|
65
|
+
// 🔥 Fragment (Symbol-safe)
|
|
66
|
+
if (tag === REACT_FRAGMENT_TYPE || tag === Fragment) {
|
|
67
|
+
return () => renderChild(children);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// function component
|
|
81
71
|
if (typeof tag === "function") {
|
|
82
72
|
return () => renderChild(tag({ children, ...props }));
|
|
83
73
|
}
|
|
84
74
|
|
|
85
|
-
//
|
|
75
|
+
// forwardRef-like objects
|
|
86
76
|
if (
|
|
87
77
|
typeof tag === "object" &&
|
|
88
78
|
tag !== null &&
|
|
@@ -91,48 +81,60 @@ const jsx = (tag, { children, ...props }) => {
|
|
|
91
81
|
return () => renderChild(tag.render({ children, ...props }, null));
|
|
92
82
|
}
|
|
93
83
|
|
|
84
|
+
// script special-case
|
|
94
85
|
if (tag === "script" && children) {
|
|
95
86
|
const src = children;
|
|
96
87
|
children = () => src;
|
|
97
88
|
}
|
|
98
89
|
|
|
99
|
-
|
|
90
|
+
// style special-case
|
|
91
|
+
if (tag === "style" && typeof children === "string") {
|
|
100
92
|
const src = minifyCss(escapeCSS(children));
|
|
101
93
|
children = () => src;
|
|
102
94
|
}
|
|
103
95
|
|
|
96
|
+
// dangerouslySetInnerHTML
|
|
104
97
|
if (props?.dangerouslySetInnerHTML) {
|
|
105
98
|
children = () => props.dangerouslySetInnerHTML.__html;
|
|
106
99
|
}
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
// normalize children
|
|
102
|
+
const flatChildren = (Array.isArray(children) ? children.flat() : [children])
|
|
103
|
+
.filter(isValidChild)
|
|
111
104
|
.map(renderChild)
|
|
112
|
-
.map((c) => (typeof c === "string" ? c : ""))
|
|
113
105
|
.join("");
|
|
114
106
|
|
|
115
|
-
|
|
107
|
+
// 🔥 CRITICAL SAFETY GATE (prevents Symbol/string crash)
|
|
108
|
+
if (!tag || typeof tag !== "string") {
|
|
109
|
+
return () => flatChildren;
|
|
110
|
+
}
|
|
116
111
|
|
|
112
|
+
// attributes
|
|
117
113
|
let attrStr = Object.entries(props || {})
|
|
118
114
|
.filter(([k]) => k !== "dangerouslySetInnerHTML")
|
|
119
115
|
.filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
|
|
120
116
|
.filter(([, v]) => v !== false)
|
|
121
117
|
.map(([k, v]) => {
|
|
122
118
|
const key = altAttrs[k.toLowerCase()] || encode(k);
|
|
119
|
+
|
|
123
120
|
if (v === true) return key;
|
|
121
|
+
|
|
124
122
|
const value =
|
|
125
123
|
typeof v === "string" || typeof v === "number" ? encode(v) : "";
|
|
124
|
+
|
|
126
125
|
return `${key}="${value}"`;
|
|
127
126
|
})
|
|
128
127
|
.join(" ");
|
|
129
128
|
|
|
130
129
|
if (attrStr) attrStr = ` ${attrStr}`;
|
|
131
130
|
|
|
132
|
-
if (SELFCLOSE.has(tag))
|
|
131
|
+
if (SELFCLOSE.has(tag)) {
|
|
132
|
+
return () => `<${tag}${attrStr} />`;
|
|
133
|
+
}
|
|
133
134
|
|
|
134
135
|
const doctype = tag === "html" ? "<!DOCTYPE html>" : "";
|
|
135
|
-
|
|
136
|
+
|
|
137
|
+
return () => `${doctype}<${tag}${attrStr}>${flatChildren}</${tag}>`;
|
|
136
138
|
};
|
|
137
139
|
|
|
138
140
|
const Fragment = "";
|
|
@@ -44,6 +44,50 @@ describe("jsx", () => {
|
|
|
44
44
|
`<!DOCTYPE html><html lang="en">Hello</html>`,
|
|
45
45
|
);
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
it("can render a pre with text", () => {
|
|
49
|
+
expect(<pre>hello</pre>).toRender("<pre>hello</pre>");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("can render a pre with newlines", () => {
|
|
53
|
+
expect(<pre>line1{"\n"}line2</pre>).toRender("<pre>line1\nline2</pre>");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("can render a pre with indented text", () => {
|
|
57
|
+
expect(<pre>{` indented`}</pre>).toRender("<pre> indented</pre>");
|
|
58
|
+
});
|
|
59
|
+
it("can render a pre inside a div", () => {
|
|
60
|
+
expect(
|
|
61
|
+
<div>
|
|
62
|
+
<pre>line1{"\n"}line2</pre>
|
|
63
|
+
</div>,
|
|
64
|
+
).toRender("<div><pre>line1\nline2</pre></div>");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("fragments", () => {
|
|
69
|
+
it("does not crash on symbol-based element types (React internals)", () => {
|
|
70
|
+
expect(
|
|
71
|
+
<>
|
|
72
|
+
<span>Hello</span> world
|
|
73
|
+
</>,
|
|
74
|
+
).toRender("<span>Hello</span> world");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders nested fragments", () => {
|
|
78
|
+
expect(
|
|
79
|
+
<>
|
|
80
|
+
<>
|
|
81
|
+
<span>A</span>
|
|
82
|
+
</>
|
|
83
|
+
B
|
|
84
|
+
</>,
|
|
85
|
+
).toRender("<span>A</span>B");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("handles empty fragments", () => {
|
|
89
|
+
expect(<></>).toRender("");
|
|
90
|
+
});
|
|
47
91
|
});
|
|
48
92
|
|
|
49
93
|
describe("React element interop", () => {
|
|
@@ -78,7 +122,8 @@ describe("React element interop", () => {
|
|
|
78
122
|
});
|
|
79
123
|
|
|
80
124
|
it("renders a React element with attributes", () => {
|
|
81
|
-
const Link = () =>
|
|
125
|
+
const Link = () =>
|
|
126
|
+
reactEl("a", { href: "https://example.com", children: "click" });
|
|
82
127
|
expect(<Link />).toRender(`<a href="https://example.com">click</a>`);
|
|
83
128
|
});
|
|
84
129
|
|