@server/next 0.28.8 → 0.28.10
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 +2 -1
- package/readme.md +1 -1
- package/src/jsx/jsx-runtime.js +41 -12
- package/src/jsx/jsx-runtime.test.jsx +169 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.10",
|
|
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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@types/bun": "^1.3.0",
|
|
48
48
|
"@types/jest": "^30.0.0",
|
|
49
49
|
"@types/node": "^24.10.0",
|
|
50
|
+
"bun": "^1.3.13",
|
|
50
51
|
"check-dts": "^0.8.2",
|
|
51
52
|
"jest": "^29.7.0",
|
|
52
53
|
"polystore": "^0.18.0",
|
package/readme.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# Server JS
|
|
1
|
+
# Server JS [](https://github.com/franciscop/server-next/actions)
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const ENTITIES = {
|
|
2
2
|
"&": "&",
|
|
3
3
|
"<": "<",
|
|
4
4
|
">": ">",
|
|
@@ -6,23 +6,34 @@ const entities = {
|
|
|
6
6
|
"'": "'",
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const encode = (str = "") => {
|
|
10
|
-
if (typeof str === "number") str = String(str);
|
|
11
|
-
if (typeof str !== "string") return "";
|
|
12
|
-
return str.replace(/[&<>"']/g, (tag) => entities[tag]);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
9
|
const SELFCLOSE = new Set(
|
|
16
10
|
"area,base,br,col,embed,hr,img,input,link,meta,source,track,wbr".split(","),
|
|
17
11
|
);
|
|
18
12
|
|
|
13
|
+
const SVG_TAGS = new Set([
|
|
14
|
+
"svg",
|
|
15
|
+
"circle",
|
|
16
|
+
"rect",
|
|
17
|
+
"path",
|
|
18
|
+
"line",
|
|
19
|
+
"g",
|
|
20
|
+
"text",
|
|
21
|
+
"defs",
|
|
22
|
+
]);
|
|
23
|
+
|
|
19
24
|
const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
20
25
|
|
|
21
|
-
const
|
|
26
|
+
const ALT_NAMES = {
|
|
22
27
|
classname: "class",
|
|
23
28
|
htmlfor: "for",
|
|
24
29
|
};
|
|
25
30
|
|
|
31
|
+
const encode = (str = "") => {
|
|
32
|
+
if (typeof str === "number") str = String(str);
|
|
33
|
+
if (typeof str !== "string") return "";
|
|
34
|
+
return str.replace(/[&<>"']/g, (tag) => ENTITIES[tag]);
|
|
35
|
+
};
|
|
36
|
+
|
|
26
37
|
// valid primitives only — null, undefined, false, true are all skipped
|
|
27
38
|
const isValidChild = (child) =>
|
|
28
39
|
child != null && child !== false && child !== true;
|
|
@@ -53,7 +64,6 @@ const renderChild = (child) => {
|
|
|
53
64
|
|
|
54
65
|
if (typeof child === "function") {
|
|
55
66
|
const result = child();
|
|
56
|
-
// Strings returned by functions are pre-rendered HTML — don't re-encode them
|
|
57
67
|
if (result == null || result === false || result === true) return "";
|
|
58
68
|
if (typeof result === "string") return result;
|
|
59
69
|
if (typeof result === "number") return String(result);
|
|
@@ -63,7 +73,6 @@ const renderChild = (child) => {
|
|
|
63
73
|
if (typeof child === "string") return encode(child);
|
|
64
74
|
if (typeof child === "number") return String(child);
|
|
65
75
|
|
|
66
|
-
// Raw marker bypasses encoding (used by script, style, dangerouslySetInnerHTML)
|
|
67
76
|
if (typeof child === "object" && RAW in child) return child[RAW];
|
|
68
77
|
|
|
69
78
|
if (isReactElement(child)) {
|
|
@@ -99,7 +108,9 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
99
108
|
const parts = Array.isArray(children) ? children : [children];
|
|
100
109
|
const content = parts
|
|
101
110
|
.filter(isValidChild)
|
|
102
|
-
.map((c) =>
|
|
111
|
+
.map((c) =>
|
|
112
|
+
typeof c === "string" ? c : typeof c === "number" ? String(c) : "",
|
|
113
|
+
)
|
|
103
114
|
.join("");
|
|
104
115
|
children = raw(content);
|
|
105
116
|
}
|
|
@@ -131,10 +142,28 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
131
142
|
.filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
|
|
132
143
|
.filter(([, v]) => v !== false)
|
|
133
144
|
.map(([k, v]) => {
|
|
134
|
-
const
|
|
145
|
+
const preserveSvg = k === "viewBox" || k === "preserveAspectRatio";
|
|
146
|
+
|
|
147
|
+
const key =
|
|
148
|
+
ALT_NAMES[k.toLowerCase()] ||
|
|
149
|
+
(SVG_TAGS.has(tag)
|
|
150
|
+
? preserveSvg
|
|
151
|
+
? k
|
|
152
|
+
: k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
|
|
153
|
+
: encode(k));
|
|
135
154
|
|
|
136
155
|
if (v === true) return key;
|
|
137
156
|
|
|
157
|
+
if (k === "style" && v && typeof v === "object") {
|
|
158
|
+
v = Object.entries(v)
|
|
159
|
+
.filter(([, val]) => val != null)
|
|
160
|
+
.map(([prop, val]) => {
|
|
161
|
+
const cssKey = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
162
|
+
return `${cssKey}:${val}`;
|
|
163
|
+
})
|
|
164
|
+
.join(";");
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
const value =
|
|
139
168
|
typeof v === "string" || typeof v === "number" ? encode(String(v)) : "";
|
|
140
169
|
|
|
@@ -142,9 +142,13 @@ describe("text encoding", () => {
|
|
|
142
142
|
|
|
143
143
|
it("renders array children", () => {
|
|
144
144
|
const items = ["x", "y", "z"];
|
|
145
|
-
expect(
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
expect(
|
|
146
|
+
<span>
|
|
147
|
+
{items.map((i) => (
|
|
148
|
+
<b key={i}>{i}</b>
|
|
149
|
+
))}
|
|
150
|
+
</span>,
|
|
151
|
+
).toRender("<span><b>x</b><b>y</b><b>z</b></span>");
|
|
148
152
|
});
|
|
149
153
|
|
|
150
154
|
it("treats string returned from a function child as raw HTML", () => {
|
|
@@ -171,9 +175,7 @@ describe("attribute encoding", () => {
|
|
|
171
175
|
});
|
|
172
176
|
|
|
173
177
|
it("encodes ' in attribute values", () => {
|
|
174
|
-
expect(<div title={"it's"}></div>).toRender(
|
|
175
|
-
'<div title="it's"></div>',
|
|
176
|
-
);
|
|
178
|
+
expect(<div title={"it's"}></div>).toRender('<div title="it's"></div>');
|
|
177
179
|
});
|
|
178
180
|
|
|
179
181
|
it("maps className to class", () => {
|
|
@@ -316,9 +318,9 @@ describe("style tag", () => {
|
|
|
316
318
|
});
|
|
317
319
|
|
|
318
320
|
it("preserves child combinator in selectors", () => {
|
|
319
|
-
expect(
|
|
320
|
-
<style
|
|
321
|
-
)
|
|
321
|
+
expect(<style>{":not(pre) > code { background: none; }"}</style>).toRender(
|
|
322
|
+
"<style>:not(pre)>code{background:none}</style>",
|
|
323
|
+
);
|
|
322
324
|
});
|
|
323
325
|
|
|
324
326
|
it("preserves sibling combinators in selectors", () => {
|
|
@@ -328,21 +330,21 @@ describe("style tag", () => {
|
|
|
328
330
|
});
|
|
329
331
|
|
|
330
332
|
it("removes spaces around braces, colons, and semicolons", () => {
|
|
331
|
-
expect(
|
|
332
|
-
<style>
|
|
333
|
-
)
|
|
333
|
+
expect(<style>{"a { color : red ; font-size : 1em ; }"}</style>).toRender(
|
|
334
|
+
"<style>a{color:red;font-size:1em}</style>",
|
|
335
|
+
);
|
|
334
336
|
});
|
|
335
337
|
|
|
336
338
|
it("removes trailing semicolon before closing brace", () => {
|
|
337
|
-
expect(
|
|
338
|
-
<style>
|
|
339
|
-
)
|
|
339
|
+
expect(<style>{"p { margin: 0; padding: 0; }"}</style>).toRender(
|
|
340
|
+
"<style>p{margin:0;padding:0}</style>",
|
|
341
|
+
);
|
|
340
342
|
});
|
|
341
343
|
|
|
342
344
|
it("does not break </style> injection", () => {
|
|
343
|
-
expect(
|
|
344
|
-
<style>
|
|
345
|
-
)
|
|
345
|
+
expect(<style>{"a { content: '</style>'; }"}</style>).toRender(
|
|
346
|
+
"<style>a{content:'<\\/style>'}</style>",
|
|
347
|
+
);
|
|
346
348
|
});
|
|
347
349
|
});
|
|
348
350
|
|
|
@@ -368,9 +370,9 @@ describe("dangerouslySetInnerHTML", () => {
|
|
|
368
370
|
});
|
|
369
371
|
|
|
370
372
|
it("does not render dangerouslySetInnerHTML as an attribute", () => {
|
|
371
|
-
expect(
|
|
372
|
-
<div
|
|
373
|
-
)
|
|
373
|
+
expect(<div dangerouslySetInnerHTML={{ __html: "hi" }}></div>).toRender(
|
|
374
|
+
"<div>hi</div>",
|
|
375
|
+
);
|
|
374
376
|
});
|
|
375
377
|
});
|
|
376
378
|
|
|
@@ -411,18 +413,14 @@ describe("function components", () => {
|
|
|
411
413
|
|
|
412
414
|
it("encodes text returned by a component", () => {
|
|
413
415
|
const Unsafe = () => "<script>alert(1)</script>";
|
|
414
|
-
expect(<Unsafe />).toRender(
|
|
415
|
-
"<script>alert(1)</script>",
|
|
416
|
-
);
|
|
416
|
+
expect(<Unsafe />).toRender("<script>alert(1)</script>");
|
|
417
417
|
});
|
|
418
418
|
});
|
|
419
419
|
|
|
420
420
|
describe("forwardRef-like components", () => {
|
|
421
421
|
it("renders an object with a render function", () => {
|
|
422
422
|
const Button = { render: ({ children }) => <button>{children}</button> };
|
|
423
|
-
expect(<Button>click me</Button>).toRender(
|
|
424
|
-
"<button>click me</button>",
|
|
425
|
-
);
|
|
423
|
+
expect(<Button>click me</Button>).toRender("<button>click me</button>");
|
|
426
424
|
});
|
|
427
425
|
|
|
428
426
|
it("passes props to render function", () => {
|
|
@@ -487,3 +485,147 @@ describe("React element interop", () => {
|
|
|
487
485
|
expect(<Deep />).toRender("<div><ul><li>one</li><li>two</li></ul></div>");
|
|
488
486
|
});
|
|
489
487
|
});
|
|
488
|
+
describe("styles", () => {
|
|
489
|
+
it("stringifies style object into CSS", () => {
|
|
490
|
+
expect(
|
|
491
|
+
<div
|
|
492
|
+
style={{
|
|
493
|
+
display: "inline-block",
|
|
494
|
+
padding: "2px 8px",
|
|
495
|
+
borderRadius: "4px",
|
|
496
|
+
}}
|
|
497
|
+
/>,
|
|
498
|
+
).toRender(
|
|
499
|
+
'<div style="display:inline-block;padding:2px 8px;border-radius:4px"></div>',
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("handles numbers and mixed style values", () => {
|
|
504
|
+
expect(
|
|
505
|
+
<div
|
|
506
|
+
style={{
|
|
507
|
+
fontSize: 12,
|
|
508
|
+
lineHeight: 1.5,
|
|
509
|
+
fontWeight: "600",
|
|
510
|
+
}}
|
|
511
|
+
/>,
|
|
512
|
+
).toRender(
|
|
513
|
+
'<div style="font-size:12;line-height:1.5;font-weight:600"></div>',
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("converts camelCase CSS properties", () => {
|
|
518
|
+
expect(
|
|
519
|
+
<div
|
|
520
|
+
style={{
|
|
521
|
+
backgroundColor: "#fff",
|
|
522
|
+
marginTop: "10px",
|
|
523
|
+
}}
|
|
524
|
+
/>,
|
|
525
|
+
).toRender('<div style="background-color:#fff;margin-top:10px"></div>');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("ignores null and undefined style values", () => {
|
|
529
|
+
expect(
|
|
530
|
+
<div
|
|
531
|
+
style={{
|
|
532
|
+
color: "red",
|
|
533
|
+
padding: null,
|
|
534
|
+
margin: undefined,
|
|
535
|
+
}}
|
|
536
|
+
/>,
|
|
537
|
+
).toRender('<div style="color:red"></div>');
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe("svg attributes", () => {
|
|
542
|
+
it("converts strokeWidth to stroke-width", () => {
|
|
543
|
+
expect(
|
|
544
|
+
<svg>
|
|
545
|
+
<circle strokeWidth="2" />
|
|
546
|
+
</svg>,
|
|
547
|
+
).toRender('<svg><circle stroke-width="2"></circle></svg>');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("converts fillOpacity to fill-opacity", () => {
|
|
551
|
+
expect(
|
|
552
|
+
<svg>
|
|
553
|
+
<rect fillOpacity="0.5" />
|
|
554
|
+
</svg>,
|
|
555
|
+
).toRender('<svg><rect fill-opacity="0.5"></rect></svg>');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("converts strokeLinecap to stroke-linecap", () => {
|
|
559
|
+
expect(
|
|
560
|
+
<svg>
|
|
561
|
+
<line strokeLinecap="round" />
|
|
562
|
+
</svg>,
|
|
563
|
+
).toRender('<svg><line stroke-linecap="round"></line></svg>');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("does NOT break viewBox casing", () => {
|
|
567
|
+
expect(
|
|
568
|
+
<svg viewBox="0 0 100 100">
|
|
569
|
+
<rect />
|
|
570
|
+
</svg>,
|
|
571
|
+
).toRender('<svg viewBox="0 0 100 100"><rect></rect></svg>');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("handles mixed SVG attributes correctly", () => {
|
|
575
|
+
expect(
|
|
576
|
+
<svg viewBox="0 0 10 10">
|
|
577
|
+
<circle strokeWidth="1" fillOpacity="0.2" />
|
|
578
|
+
</svg>,
|
|
579
|
+
).toRender(
|
|
580
|
+
'<svg viewBox="0 0 10 10"><circle stroke-width="1" fill-opacity="0.2"></circle></svg>',
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("keeps viewBox unchanged", () => {
|
|
585
|
+
expect(
|
|
586
|
+
<svg viewBox="0 0 100 100">
|
|
587
|
+
<rect />
|
|
588
|
+
</svg>,
|
|
589
|
+
).toRender('<svg viewBox="0 0 100 100"><rect></rect></svg>');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("does NOT convert viewBox to view-box", () => {
|
|
593
|
+
expect(<svg viewBox="0 0 10 10"></svg>).toRender(
|
|
594
|
+
'<svg viewBox="0 0 10 10"></svg>',
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("converts strokeWidth to stroke-width", () => {
|
|
599
|
+
expect(
|
|
600
|
+
<svg>
|
|
601
|
+
<circle strokeWidth="2" />
|
|
602
|
+
</svg>,
|
|
603
|
+
).toRender('<svg><circle stroke-width="2"></circle></svg>');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("converts fillOpacity to fill-opacity", () => {
|
|
607
|
+
expect(
|
|
608
|
+
<svg>
|
|
609
|
+
<rect fillOpacity="0.5" />
|
|
610
|
+
</svg>,
|
|
611
|
+
).toRender('<svg><rect fill-opacity="0.5"></rect></svg>');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("converts strokeLinecap to stroke-linecap", () => {
|
|
615
|
+
expect(
|
|
616
|
+
<svg>
|
|
617
|
+
<line strokeLinecap="round" />
|
|
618
|
+
</svg>,
|
|
619
|
+
).toRender('<svg><line stroke-linecap="round"></line></svg>');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("handles mixed SVG attributes correctly", () => {
|
|
623
|
+
expect(
|
|
624
|
+
<svg viewBox="0 0 10 10">
|
|
625
|
+
<circle strokeWidth="1" fillOpacity="0.2" />
|
|
626
|
+
</svg>,
|
|
627
|
+
).toRender(
|
|
628
|
+
'<svg viewBox="0 0 10 10"><circle stroke-width="1" fill-opacity="0.2"></circle></svg>',
|
|
629
|
+
);
|
|
630
|
+
});
|
|
631
|
+
});
|