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