@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.28.3",
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",
@@ -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
- // "" and 0 are valid children, false and null and undefined are not
24
- const isValidChild = (child) => child || child === "" || child === 0;
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(/(?!<")\/\*[^*]+\*\/(?!")/g, "")
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
- const REACT_ELEMENT_TYPE = Symbol.for("react.element");
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
- while (typeof child === "function") child = child();
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 (isReactElement(child)) {
68
- return resolve(jsx(child.type, child.props || {}));
49
+ if (typeof child === "function") {
50
+ return renderChild(child());
69
51
  }
70
52
 
71
- if (typeof child === "string") return encode(child);
53
+ if (typeof child === "string") return child;
72
54
  if (typeof child === "number") return String(child);
73
55
 
74
- if (!isValidChild(child)) return "";
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
- // Handle React forwardRef objects: { render: fn }
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
- if (tag === "style" && children && typeof children === "string") {
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
- if (!isValidChild(children)) children = [];
109
-
110
- children = (Array.isArray(children) ? children.flat() : [children])
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
- if (!tag) return () => children;
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)) return () => `<${tag}${attrStr} />`;
131
+ if (SELFCLOSE.has(tag)) {
132
+ return () => `<${tag}${attrStr} />`;
133
+ }
133
134
 
134
135
  const doctype = tag === "html" ? "<!DOCTYPE html>" : "";
135
- return () => `${doctype}<${tag}${attrStr}>${children}</${tag}>`;
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 = () => reactEl("a", { href: "https://example.com", children: "click" });
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