@server/next 0.28.6 → 0.28.8
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/index.d.ts +25 -3
- package/index.js +102 -77
- package/package.json +1 -1
- package/src/jsx/jsx-runtime.js +6 -6
- package/src/jsx/jsx-runtime.test.jsx +33 -3
package/index.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
type LimitOptions = {
|
|
2
|
+
maxSize?: number | string;
|
|
3
|
+
minSize?: number | string;
|
|
4
|
+
fileType?: string[];
|
|
5
|
+
};
|
|
6
|
+
declare class UploadPipeline {
|
|
7
|
+
private _bucket;
|
|
8
|
+
private _limits;
|
|
9
|
+
constructor(bucket?: Bucket | null);
|
|
10
|
+
limit(options: LimitOptions): this;
|
|
11
|
+
store(bucket: Bucket): this;
|
|
12
|
+
processFile(originalName: string, data: Buffer, contentType: string): Promise<UploadedFile>;
|
|
13
|
+
}
|
|
14
|
+
declare function upload(bucket?: Bucket | null): UploadPipeline;
|
|
15
|
+
|
|
1
16
|
type Method = "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "socket";
|
|
2
17
|
type ServerConfig<Session = {}, User = {}> = {
|
|
3
18
|
Session?: Session;
|
|
@@ -28,6 +43,13 @@ type Bucket = {
|
|
|
28
43
|
write: (path: string, data: string | Buffer) => Promise<void | string>;
|
|
29
44
|
delete: (path: string) => Promise<boolean>;
|
|
30
45
|
};
|
|
46
|
+
type UploadedFile = {
|
|
47
|
+
name: string;
|
|
48
|
+
id: string;
|
|
49
|
+
path: string;
|
|
50
|
+
type: string;
|
|
51
|
+
size: number;
|
|
52
|
+
};
|
|
31
53
|
type CorsSettings = {
|
|
32
54
|
origin: string | boolean;
|
|
33
55
|
methods: string;
|
|
@@ -88,7 +110,7 @@ type Options = {
|
|
|
88
110
|
secret?: string;
|
|
89
111
|
views?: string | Bucket;
|
|
90
112
|
public?: string | Bucket;
|
|
91
|
-
uploads?: string | Bucket;
|
|
113
|
+
uploads?: string | Bucket | UploadPipeline;
|
|
92
114
|
store?: KVStore;
|
|
93
115
|
cookies?: KVStore;
|
|
94
116
|
session?: KVStore | {
|
|
@@ -104,7 +126,7 @@ type Settings = {
|
|
|
104
126
|
secret: string;
|
|
105
127
|
views?: Bucket;
|
|
106
128
|
public?: Bucket;
|
|
107
|
-
uploads?: Bucket;
|
|
129
|
+
uploads?: Bucket | UploadPipeline;
|
|
108
130
|
store?: KVStore;
|
|
109
131
|
cookies?: KVStore;
|
|
110
132
|
session?: {
|
|
@@ -402,4 +424,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
402
424
|
}
|
|
403
425
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
404
426
|
|
|
405
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
|
|
427
|
+
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
|
package/index.js
CHANGED
|
@@ -197,7 +197,7 @@ var Reply = class {
|
|
|
197
197
|
}
|
|
198
198
|
download(name) {
|
|
199
199
|
const ext = name?.split(".").pop();
|
|
200
|
-
if (
|
|
200
|
+
if (ext && !this.res.headers.get("content-type")) this.type(ext);
|
|
201
201
|
const filename = name ? `; filename="${encodeURIComponent(name)}"` : "";
|
|
202
202
|
return this.headers("content-disposition", `attachment${filename}`);
|
|
203
203
|
}
|
|
@@ -542,59 +542,83 @@ function createId(source, size = 16) {
|
|
|
542
542
|
return randomId(size);
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
// src/helpers/
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
blue: 34,
|
|
558
|
-
magenta: 35,
|
|
559
|
-
cyan: 36,
|
|
560
|
-
white: 37,
|
|
561
|
-
bgblack: 40,
|
|
562
|
-
bgred: 41,
|
|
563
|
-
bggreen: 42,
|
|
564
|
-
bgyellow: 43,
|
|
565
|
-
bgblue: 44,
|
|
566
|
-
bgmagenta: 45,
|
|
567
|
-
bgcyan: 46,
|
|
568
|
-
bgwhite: 47
|
|
569
|
-
};
|
|
570
|
-
var replace = (k) => {
|
|
571
|
-
if (process.env.NO_COLOR) return "";
|
|
572
|
-
if (!(k in map)) throw new Error(`"{${k}}" is not a valid color`);
|
|
573
|
-
return `\x1B[${map[k]}m`;
|
|
574
|
-
};
|
|
575
|
-
function color(str, ...vals) {
|
|
576
|
-
if (typeof str === "string") {
|
|
577
|
-
return str.replace(/\{(\w+)\}/g, (_m, k) => replace(k)).replace(/\{\/\w*\}/g, () => replace("reset"));
|
|
578
|
-
}
|
|
579
|
-
return color(str[0] + vals.map((v, i) => v + str[i + 1]).join(""));
|
|
545
|
+
// src/helpers/upload.ts
|
|
546
|
+
function parseBytes(value) {
|
|
547
|
+
if (typeof value === "number") return value;
|
|
548
|
+
const units = {
|
|
549
|
+
b: 1,
|
|
550
|
+
kb: 1024,
|
|
551
|
+
mb: 1024 ** 2,
|
|
552
|
+
gb: 1024 ** 3
|
|
553
|
+
};
|
|
554
|
+
const match = value.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
555
|
+
if (!match) throw new Error(`Invalid size: "${value}"`);
|
|
556
|
+
return parseFloat(match[1]) * (units[match[2]] ?? 1);
|
|
580
557
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
558
|
+
function getExt(filename) {
|
|
559
|
+
const i = filename.lastIndexOf(".");
|
|
560
|
+
if (i <= 0) return ".bin";
|
|
561
|
+
return filename.slice(i).toLowerCase();
|
|
562
|
+
}
|
|
563
|
+
async function saveFileToBucket(originalName, data, bucket, contentType) {
|
|
564
|
+
const ext = getExt(originalName);
|
|
565
|
+
const id = `${createId()}${ext}`;
|
|
566
|
+
const path2 = await bucket.write(id, data);
|
|
567
|
+
return { name: originalName, id, path: path2, type: contentType, size: data.length };
|
|
568
|
+
}
|
|
569
|
+
var UploadPipeline = class {
|
|
570
|
+
_bucket;
|
|
571
|
+
_limits = {};
|
|
572
|
+
constructor(bucket) {
|
|
573
|
+
this._bucket = bucket ?? null;
|
|
574
|
+
}
|
|
575
|
+
limit(options) {
|
|
576
|
+
this._limits = { ...this._limits, ...options };
|
|
577
|
+
return this;
|
|
589
578
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
579
|
+
store(bucket) {
|
|
580
|
+
this._bucket = bucket;
|
|
581
|
+
return this;
|
|
582
|
+
}
|
|
583
|
+
async processFile(originalName, data, contentType) {
|
|
584
|
+
const { maxSize, minSize, fileType } = this._limits;
|
|
585
|
+
if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (minSize !== void 0 && data.length < parseBytes(minSize)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (fileType && fileType.length > 0) {
|
|
596
|
+
const ext = getExt(originalName);
|
|
597
|
+
const mime = contentType.toLowerCase();
|
|
598
|
+
const allowed = fileType.some(
|
|
599
|
+
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext
|
|
600
|
+
);
|
|
601
|
+
if (!allowed) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (!this._bucket) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`No destination configured \u2014 pass a bucket to upload() or call .store()`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
function upload(bucket) {
|
|
616
|
+
return new UploadPipeline(bucket);
|
|
593
617
|
}
|
|
594
618
|
|
|
595
619
|
// src/helpers/config.ts
|
|
596
|
-
var env2 = globalThis.env;
|
|
597
620
|
function config(options = {}) {
|
|
621
|
+
const env2 = globalThis.env;
|
|
598
622
|
const settings = {
|
|
599
623
|
port: options.port || env2.PORT || 3e3,
|
|
600
624
|
secret: options.secret || env2.SECRET || `unsafe-${createId()}`
|
|
@@ -633,27 +657,16 @@ function config(options = {}) {
|
|
|
633
657
|
settings.cors = cors2;
|
|
634
658
|
}
|
|
635
659
|
settings.views = options.views ? bucket_default(options.views) : null;
|
|
636
|
-
debugInfo(options, "views", (views) => views?.location || "true", "\u{1F4C2}");
|
|
637
660
|
settings.public = options.public ? bucket_default(options.public) : null;
|
|
638
|
-
|
|
639
|
-
settings.uploads = options.uploads ? bucket_default(options.uploads) : null;
|
|
640
|
-
debugInfo(options, "uploads", (ups) => ups?.location || "true", "\u{1F4C2}");
|
|
661
|
+
settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
|
|
641
662
|
settings.store = options.store ?? null;
|
|
642
|
-
debugInfo(options, "store", (store) => store?.name || "working", "\u{1F4E6}");
|
|
643
663
|
settings.cookies = options.cookies ?? null;
|
|
644
|
-
debugInfo(options, "cookies", (cookies2) => cookies2?.name || "working", "\u{1F36A}");
|
|
645
664
|
if (options.session) {
|
|
646
665
|
settings.session = "store" in options.session ? options.session : { store: options.session };
|
|
647
666
|
}
|
|
648
667
|
if (options.store && !options.session) {
|
|
649
668
|
settings.session = { store: options.store.prefix("session:") };
|
|
650
669
|
}
|
|
651
|
-
debugInfo(
|
|
652
|
-
options,
|
|
653
|
-
"session",
|
|
654
|
-
(session2) => session2?.store?.name || "working",
|
|
655
|
-
"\u{1F510}"
|
|
656
|
-
);
|
|
657
670
|
if (options.auth || env2.AUTH) {
|
|
658
671
|
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
|
|
659
672
|
}
|
|
@@ -795,7 +808,7 @@ function getMachine() {
|
|
|
795
808
|
|
|
796
809
|
// src/parseResponse.ts
|
|
797
810
|
async function parseResponse(out, ctx) {
|
|
798
|
-
if (!out && typeof out !== "string") return;
|
|
811
|
+
if (!out && typeof out !== "string") return null;
|
|
799
812
|
if (typeof out === "function") {
|
|
800
813
|
out = await out(ctx);
|
|
801
814
|
}
|
|
@@ -1071,12 +1084,6 @@ function getMatching(string, regex) {
|
|
|
1071
1084
|
const matches = string.match(regex);
|
|
1072
1085
|
return matches?.[1] ?? "";
|
|
1073
1086
|
}
|
|
1074
|
-
var saveFile = async (name, value, bucket) => {
|
|
1075
|
-
const ext = name.split(".").pop();
|
|
1076
|
-
const id = `${createId()}.${ext}`;
|
|
1077
|
-
await bucket.write(id, value);
|
|
1078
|
-
return id;
|
|
1079
|
-
};
|
|
1080
1087
|
function splitBuffer(buffer, delimiter) {
|
|
1081
1088
|
const result = [];
|
|
1082
1089
|
let start = 0;
|
|
@@ -1090,6 +1097,7 @@ function splitBuffer(buffer, delimiter) {
|
|
|
1090
1097
|
return result;
|
|
1091
1098
|
}
|
|
1092
1099
|
var BREAK_BUFFER = Buffer.from("\r\n\r\n");
|
|
1100
|
+
var END_BUFFER = Buffer.from("--\r\n");
|
|
1093
1101
|
function isProbablyText(buffer) {
|
|
1094
1102
|
for (let i = 0; i < Math.min(buffer.length, 512); i++) {
|
|
1095
1103
|
const byte = buffer[i];
|
|
@@ -1100,7 +1108,7 @@ function isProbablyText(buffer) {
|
|
|
1100
1108
|
}
|
|
1101
1109
|
async function parseBody(raw, contentType, bucket) {
|
|
1102
1110
|
const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
1103
|
-
if (!raw) return {};
|
|
1111
|
+
if (!raw || raw.length === 0) return {};
|
|
1104
1112
|
if (!contentTypeStr || /^text\//.test(contentTypeStr)) {
|
|
1105
1113
|
return raw.toString("utf-8");
|
|
1106
1114
|
}
|
|
@@ -1113,7 +1121,7 @@ async function parseBody(raw, contentType, bucket) {
|
|
|
1113
1121
|
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
1114
1122
|
const parts = splitBuffer(raw, boundaryBuffer);
|
|
1115
1123
|
for (const part of parts) {
|
|
1116
|
-
if (part.length === 0 || part.equals(
|
|
1124
|
+
if (part.length === 0 || part.equals(END_BUFFER)) continue;
|
|
1117
1125
|
const idx = part.indexOf(BREAK_BUFFER);
|
|
1118
1126
|
if (idx === -1) continue;
|
|
1119
1127
|
const headerStr = part.slice(0, idx).toString("utf-8");
|
|
@@ -1122,8 +1130,24 @@ async function parseBody(raw, contentType, bucket) {
|
|
|
1122
1130
|
if (!name) continue;
|
|
1123
1131
|
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
1124
1132
|
if (filename) {
|
|
1125
|
-
|
|
1126
|
-
|
|
1133
|
+
const partContentType = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
|
|
1134
|
+
if (!bucket) {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (bucket instanceof UploadPipeline) {
|
|
1138
|
+
body[name] = await bucket.processFile(
|
|
1139
|
+
filename,
|
|
1140
|
+
contentBuf,
|
|
1141
|
+
partContentType
|
|
1142
|
+
);
|
|
1143
|
+
} else {
|
|
1144
|
+
body[name] = await saveFileToBucket(
|
|
1145
|
+
filename,
|
|
1146
|
+
contentBuf,
|
|
1147
|
+
bucket,
|
|
1148
|
+
partContentType
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1127
1151
|
} else {
|
|
1128
1152
|
const value = isProbablyText(contentBuf) ? contentBuf.toString("utf-8").trim() : contentBuf;
|
|
1129
1153
|
if (body[name]) {
|
|
@@ -1749,9 +1773,9 @@ async function createWinter(req, app) {
|
|
|
1749
1773
|
}
|
|
1750
1774
|
|
|
1751
1775
|
// src/context/handlers.ts
|
|
1752
|
-
var Winter = async (app, request,
|
|
1753
|
-
if (
|
|
1754
|
-
Object.assign(globalThis.env,
|
|
1776
|
+
var Winter = async (app, request, env2) => {
|
|
1777
|
+
if (env2?.upgrade(request)) return;
|
|
1778
|
+
Object.assign(globalThis.env, env2);
|
|
1755
1779
|
const ctx = await createWinter(request, app);
|
|
1756
1780
|
const res = await handleRequest(app.handlers, ctx);
|
|
1757
1781
|
ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
|
|
@@ -1965,8 +1989,8 @@ var Server = class extends Router {
|
|
|
1965
1989
|
node() {
|
|
1966
1990
|
return Node(this);
|
|
1967
1991
|
}
|
|
1968
|
-
fetch(request,
|
|
1969
|
-
return Winter(this, request,
|
|
1992
|
+
fetch(request, env2) {
|
|
1993
|
+
return Winter(this, request, env2);
|
|
1970
1994
|
}
|
|
1971
1995
|
callback(request, context) {
|
|
1972
1996
|
return Netlify(this, request, context);
|
|
@@ -1991,5 +2015,6 @@ export {
|
|
|
1991
2015
|
router,
|
|
1992
2016
|
send,
|
|
1993
2017
|
status,
|
|
1994
|
-
type
|
|
2018
|
+
type,
|
|
2019
|
+
upload
|
|
1995
2020
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.8",
|
|
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
|
@@ -27,13 +27,13 @@ const altAttrs = {
|
|
|
27
27
|
const isValidChild = (child) =>
|
|
28
28
|
child != null && child !== false && child !== true;
|
|
29
29
|
|
|
30
|
-
// CSS helpers
|
|
31
|
-
const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
|
|
32
|
-
|
|
33
30
|
const minifyCss = (str) =>
|
|
34
31
|
str
|
|
35
|
-
.replace(/\s+/g, " ")
|
|
36
32
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
33
|
+
.replace(/\s+/g, " ")
|
|
34
|
+
.replace(/\s*([{}:;,>~+])\s*/g, "$1")
|
|
35
|
+
.replace(/;}/g, "}")
|
|
36
|
+
.replace(/<\/style>/gi, "<\\/style>")
|
|
37
37
|
.trim();
|
|
38
38
|
|
|
39
39
|
// React element detection (safe for custom objects too)
|
|
@@ -104,9 +104,9 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
104
104
|
children = raw(content);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// style: minified
|
|
107
|
+
// style: minified raw content
|
|
108
108
|
if (tag === "style" && typeof children === "string") {
|
|
109
|
-
children = raw(minifyCss(
|
|
109
|
+
children = raw(minifyCss(children));
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// dangerouslySetInnerHTML
|
|
@@ -299,20 +299,50 @@ describe("script tag", () => {
|
|
|
299
299
|
describe("style tag", () => {
|
|
300
300
|
it("renders style content", () => {
|
|
301
301
|
expect(<style>{"body { color: red; }"}</style>).toRender(
|
|
302
|
-
"<style>body
|
|
302
|
+
"<style>body{color:red}</style>",
|
|
303
303
|
);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
306
|
it("minifies whitespace in style content", () => {
|
|
307
307
|
expect(
|
|
308
308
|
<style>{"body {\n color: red;\n margin: 0;\n}"}</style>,
|
|
309
|
-
).toRender("<style>body
|
|
309
|
+
).toRender("<style>body{color:red;margin:0}</style>");
|
|
310
310
|
});
|
|
311
311
|
|
|
312
312
|
it("strips CSS comments", () => {
|
|
313
313
|
expect(
|
|
314
314
|
<style>{"/* reset */ body { margin: 0; } /* end */"}</style>,
|
|
315
|
-
).toRender("<style>body
|
|
315
|
+
).toRender("<style>body{margin:0}</style>");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("preserves child combinator in selectors", () => {
|
|
319
|
+
expect(
|
|
320
|
+
<style>{":not(pre) > code { background: none; }"}</style>,
|
|
321
|
+
).toRender("<style>:not(pre)>code{background:none}</style>");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("preserves sibling combinators in selectors", () => {
|
|
325
|
+
expect(
|
|
326
|
+
<style>{"h2 + p { margin: 0; } h2 ~ p { color: red; }"}</style>,
|
|
327
|
+
).toRender("<style>h2+p{margin:0}h2~p{color:red}</style>");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("removes spaces around braces, colons, and semicolons", () => {
|
|
331
|
+
expect(
|
|
332
|
+
<style>{"a { color : red ; font-size : 1em ; }"}</style>,
|
|
333
|
+
).toRender("<style>a{color:red;font-size:1em}</style>");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("removes trailing semicolon before closing brace", () => {
|
|
337
|
+
expect(
|
|
338
|
+
<style>{"p { margin: 0; padding: 0; }"}</style>,
|
|
339
|
+
).toRender("<style>p{margin:0;padding:0}</style>");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("does not break </style> injection", () => {
|
|
343
|
+
expect(
|
|
344
|
+
<style>{"a { content: '</style>'; }"}</style>,
|
|
345
|
+
).toRender("<style>a{content:'<\\/style>'}</style>");
|
|
316
346
|
});
|
|
317
347
|
});
|
|
318
348
|
|