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