@server/next 0.40.0 → 0.40.1
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 -2
- package/src/jsx/jsx-runtime.js +24 -6
- package/src/jsx/jsx-runtime.test.jsx +94 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.1",
|
|
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": "github:franciscop/server-next",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"bucket": "^0.7.1",
|
|
68
|
-
"polystore": "^0.
|
|
68
|
+
"polystore": "^0.24.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/bun": "^1.3.0",
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -38,14 +38,30 @@ const encode = (str = "") => {
|
|
|
38
38
|
const isValidChild = (child) =>
|
|
39
39
|
child != null && child !== false && child !== true;
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// A `</style>` inside the CSS would close the element early and let whatever
|
|
42
|
+
// follows run as HTML. The content is never encoded, so this runs on every
|
|
43
|
+
// <style>, minified or not.
|
|
44
|
+
const escapeCss = (str) => str.replace(/<\/style>/gi, "<\\/style>");
|
|
45
|
+
|
|
46
|
+
const minifyCss = (str) => {
|
|
47
|
+
// Quoted values are content, not formatting: the spaces and delimiters in
|
|
48
|
+
// `content: ", "` have to survive. They're set aside while the rest is
|
|
49
|
+
// squeezed, then put back. Comments go first and unconditionally, so one
|
|
50
|
+
// written inside a string is stripped along with the rest.
|
|
51
|
+
const quoted = [];
|
|
52
|
+
return str
|
|
43
53
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
54
|
+
.replace(
|
|
55
|
+
/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
|
|
56
|
+
(match) => `\0${quoted.push(match) - 1}\0`,
|
|
57
|
+
)
|
|
44
58
|
.replace(/\s+/g, " ")
|
|
45
|
-
|
|
59
|
+
// `+` is left out on purpose for `calc(100% + 10px)`
|
|
60
|
+
.replace(/\s*([{}:;,>~])\s*/g, "$1")
|
|
46
61
|
.replace(/;}/g, "}")
|
|
47
|
-
.replace(
|
|
62
|
+
.replace(/\0(\d+)\0/g, (_, i) => quoted[i])
|
|
48
63
|
.trim();
|
|
64
|
+
};
|
|
49
65
|
|
|
50
66
|
// React element detection (safe for custom objects too)
|
|
51
67
|
const isReactElement = (val) =>
|
|
@@ -115,9 +131,9 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
115
131
|
children = raw(content);
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
// style: minified
|
|
134
|
+
// style: raw content, minified only when asked for with <style minify>
|
|
119
135
|
if (tag === "style" && typeof children === "string") {
|
|
120
|
-
children = raw(minifyCss(children));
|
|
136
|
+
children = raw(escapeCss(props?.minify ? minifyCss(children) : children));
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
// dangerouslySetInnerHTML
|
|
@@ -139,6 +155,8 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
139
155
|
// attributes
|
|
140
156
|
let attrStr = Object.entries(props || {})
|
|
141
157
|
.filter(([k]) => k !== "dangerouslySetInnerHTML")
|
|
158
|
+
// `minify` drives <style>, it isn't an HTML attribute
|
|
159
|
+
.filter(([k]) => !(tag === "style" && k === "minify"))
|
|
142
160
|
.filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
|
|
143
161
|
.filter(([, v]) => v !== false)
|
|
144
162
|
.map(([k, v]) => {
|
|
@@ -299,52 +299,115 @@ describe("script tag", () => {
|
|
|
299
299
|
});
|
|
300
300
|
|
|
301
301
|
describe("style tag", () => {
|
|
302
|
-
it("renders
|
|
302
|
+
it("renders the CSS as written", () => {
|
|
303
303
|
expect(<style>{"body { color: red; }"}</style>).toRender(
|
|
304
|
-
"<style>body{color:red}</style>",
|
|
304
|
+
"<style>body { color: red; }</style>",
|
|
305
305
|
);
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
-
it("
|
|
309
|
-
expect(
|
|
310
|
-
<style
|
|
311
|
-
).toRender("<style>body{color:red;margin:0}</style>");
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("strips CSS comments", () => {
|
|
315
|
-
expect(
|
|
316
|
-
<style>{"/* reset */ body { margin: 0; } /* end */"}</style>,
|
|
317
|
-
).toRender("<style>body{margin:0}</style>");
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("preserves child combinator in selectors", () => {
|
|
321
|
-
expect(<style>{":not(pre) > code { background: none; }"}</style>).toRender(
|
|
322
|
-
"<style>:not(pre)>code{background:none}</style>",
|
|
308
|
+
it("keeps comments and whitespace without `minify`", () => {
|
|
309
|
+
expect(<style>{"/* reset */ body {\n margin: 0;\n}"}</style>).toRender(
|
|
310
|
+
"<style>/* reset */ body {\n margin: 0;\n}</style>",
|
|
323
311
|
);
|
|
324
312
|
});
|
|
325
313
|
|
|
326
|
-
it("
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
314
|
+
it("sends the content raw, like <script>", () => {
|
|
315
|
+
// Encoding it would break the CSS, so user input must never reach here
|
|
316
|
+
expect(<style>{"a::after { content: '>'; }"}</style>).toRender(
|
|
317
|
+
"<style>a::after { content: '>'; }</style>",
|
|
318
|
+
);
|
|
330
319
|
});
|
|
331
320
|
|
|
332
|
-
it("
|
|
333
|
-
expect(<style>{"a {
|
|
334
|
-
"<style>a{
|
|
321
|
+
it("escapes </style> either way, so it can't close the tag early", () => {
|
|
322
|
+
expect(<style>{"a { content: '</style>'; }"}</style>).toRender(
|
|
323
|
+
"<style>a { content: '<\\/style>'; }</style>",
|
|
324
|
+
);
|
|
325
|
+
expect(<style minify>{"a { content: '</style>'; }"}</style>).toRender(
|
|
326
|
+
"<style>a{content:'<\\/style>'}</style>",
|
|
335
327
|
);
|
|
336
328
|
});
|
|
337
329
|
|
|
338
|
-
it("
|
|
339
|
-
expect(<style>{"
|
|
340
|
-
"<style>
|
|
330
|
+
it("never renders `minify` as an attribute", () => {
|
|
331
|
+
expect(<style minify>{"a { color: red; }"}</style>).toRender(
|
|
332
|
+
"<style>a{color:red}</style>",
|
|
341
333
|
);
|
|
342
334
|
});
|
|
343
335
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
336
|
+
describe("with `minify`", () => {
|
|
337
|
+
it("minifies whitespace in style content", () => {
|
|
338
|
+
expect(
|
|
339
|
+
<style minify>{"body {\n color: red;\n margin: 0;\n}"}</style>,
|
|
340
|
+
).toRender("<style>body{color:red;margin:0}</style>");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("strips CSS comments", () => {
|
|
344
|
+
expect(
|
|
345
|
+
<style minify>{"/* reset */ body { margin: 0; } /* end */"}</style>,
|
|
346
|
+
).toRender("<style>body{margin:0}</style>");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("preserves child combinator in selectors", () => {
|
|
350
|
+
expect(
|
|
351
|
+
<style minify>{":not(pre) > code { background: none; }"}</style>,
|
|
352
|
+
).toRender("<style>:not(pre)>code{background:none}</style>");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("preserves sibling combinators in selectors", () => {
|
|
356
|
+
// `+` keeps a single space, since removing it breaks calc() below
|
|
357
|
+
expect(
|
|
358
|
+
<style minify>
|
|
359
|
+
{"h2 + p { margin: 0; } h2 ~ p { color: red; }"}
|
|
360
|
+
</style>,
|
|
361
|
+
).toRender("<style>h2 + p{margin:0}h2~p{color:red}</style>");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("keeps the spaces calc() needs around + and -", () => {
|
|
365
|
+
// `calc(100%+10px)` is invalid CSS: the operators need their whitespace
|
|
366
|
+
expect(<style minify>{"a { top: calc(100% + 10px); }"}</style>).toRender(
|
|
367
|
+
"<style>a{top:calc(100% + 10px)}</style>",
|
|
368
|
+
);
|
|
369
|
+
expect(<style minify>{"a { top: calc(100% - 10px); }"}</style>).toRender(
|
|
370
|
+
"<style>a{top:calc(100% - 10px)}</style>",
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("removes spaces around braces, colons, and semicolons", () => {
|
|
375
|
+
expect(
|
|
376
|
+
<style minify>{"a { color : red ; font-size : 1em ; }"}</style>,
|
|
377
|
+
).toRender("<style>a{color:red;font-size:1em}</style>");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("removes trailing semicolon before closing brace", () => {
|
|
381
|
+
expect(<style minify>{"p { margin: 0; padding: 0; }"}</style>).toRender(
|
|
382
|
+
"<style>p{margin:0;padding:0}</style>",
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("leaves quoted values alone", () => {
|
|
387
|
+
// The spaces and delimiters in here are content, not formatting
|
|
388
|
+
expect(<style minify>{'a::after { content: ", "; }'}</style>).toRender(
|
|
389
|
+
'<style>a::after{content:", "}</style>',
|
|
390
|
+
);
|
|
391
|
+
expect(
|
|
392
|
+
<style minify>{'a::after { content: "a; b: c"; }'}</style>,
|
|
393
|
+
).toRender('<style>a::after{content:"a; b: c"}</style>');
|
|
394
|
+
expect(
|
|
395
|
+
<style minify>{"a::after { content: 'x y'; }"}</style>,
|
|
396
|
+
).toRender("<style>a::after{content:'x y'}</style>");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("handles an escaped quote inside a value", () => {
|
|
400
|
+
expect(
|
|
401
|
+
<style minify>{'a::after { content: "say \\" hi"; }'}</style>,
|
|
402
|
+
).toRender('<style>a::after{content:"say \\" hi"}</style>');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("still strips a comment written inside a value", () => {
|
|
406
|
+
// Comments are stripped first, unconditionally
|
|
407
|
+
expect(
|
|
408
|
+
<style minify>{'a::after { content: "/* hi */"; }'}</style>,
|
|
409
|
+
).toRender('<style>a::after{content:""}</style>');
|
|
410
|
+
});
|
|
348
411
|
});
|
|
349
412
|
});
|
|
350
413
|
|