@server/next 0.44.1 → 0.45.0
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 +15 -17
- package/index.js +73 -160
- package/package.json +4 -2
- package/readme.md +12 -4
- package/src/jsx/jsx-runtime.d.ts +26 -8
- package/src/jsx/jsx-runtime.js +8 -5
- package/src/jsx/jsx-runtime.test.jsx +0 -694
package/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
2
3
|
export { default as kv } from 'polystore';
|
|
3
4
|
export { default as bucket } from 'bucket';
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ type LimitOptions = {
|
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
type CookieOptions = string | string[] | Cookie | Cookie[] | null;
|
|
13
|
+
type SendBody = SerializableValue | JSX.Element | Uint8Array | ReadableStream | Readable | Response | Reply$1 | BucketFile | Promise<SendBody>;
|
|
12
14
|
interface ResponseData {
|
|
13
15
|
headers: Headers;
|
|
14
16
|
status?: number;
|
|
@@ -22,10 +24,10 @@ declare class Reply$1 {
|
|
|
22
24
|
headers(key: string | Record<string, string | string[]>, value?: string | string[]): this;
|
|
23
25
|
cache(value: CacheOption): this;
|
|
24
26
|
cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
|
|
25
|
-
json(body: unknown): Response
|
|
26
|
-
redirect(path: string): Response
|
|
27
|
+
json(body: unknown): Promise<Response>;
|
|
28
|
+
redirect(path: string): Promise<Response>;
|
|
27
29
|
file(path: string | BucketFile): Promise<Response>;
|
|
28
|
-
send(
|
|
30
|
+
send(input?: SendBody): Promise<Response>;
|
|
29
31
|
}
|
|
30
32
|
type Params<K extends keyof Reply$1> = Reply$1[K] extends (...args: infer A) => any ? A : never;
|
|
31
33
|
declare const status: (...args: Params<"status">) => Reply$1;
|
|
@@ -34,25 +36,23 @@ declare const type: (...args: Params<"type">) => Reply$1;
|
|
|
34
36
|
declare const cache: (...args: Params<"cache">) => Reply$1;
|
|
35
37
|
declare const download: (...args: Params<"download">) => Reply$1;
|
|
36
38
|
declare const cookies: (...args: Params<"cookies">) => Reply$1;
|
|
37
|
-
declare const send: (...args: Params<"send">) => Response
|
|
38
|
-
declare const json: (...args: Params<"json">) => Response
|
|
39
|
+
declare const send: (...args: Params<"send">) => Promise<Response>;
|
|
40
|
+
declare const json: (...args: Params<"json">) => Promise<Response>;
|
|
39
41
|
declare const file: (...args: Params<"file">) => Promise<Response>;
|
|
40
|
-
declare const redirect: (...args: Params<"redirect">) => Response
|
|
42
|
+
declare const redirect: (...args: Params<"redirect">) => Promise<Response>;
|
|
41
43
|
|
|
42
44
|
type Reply = ReturnType<typeof status>;
|
|
43
45
|
type Method = "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "socket";
|
|
44
46
|
type ContextTypes = {
|
|
45
|
-
session?: any;
|
|
46
47
|
user?: any;
|
|
47
48
|
params?: any;
|
|
48
49
|
query?: any;
|
|
49
50
|
body?: any;
|
|
50
51
|
};
|
|
51
52
|
type Field<C, K extends keyof ContextTypes, Fallback> = K extends keyof C ? SchemaOutput<C[K], C[K]> : Fallback;
|
|
52
|
-
declare namespace JSX {
|
|
53
|
+
declare namespace JSX$1 {
|
|
53
54
|
interface Element {
|
|
54
|
-
|
|
55
|
-
props: any;
|
|
55
|
+
(): string;
|
|
56
56
|
}
|
|
57
57
|
interface IntrinsicElements {
|
|
58
58
|
[elem: string]: any;
|
|
@@ -203,6 +203,7 @@ type AuthOption = `${Strategy}:${Provider}` | {
|
|
|
203
203
|
strategy: Strategy;
|
|
204
204
|
providers?: Provider | Provider[];
|
|
205
205
|
users?: StoreSource;
|
|
206
|
+
sessions?: StoreSource;
|
|
206
207
|
redirect?: string;
|
|
207
208
|
onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
|
|
208
209
|
onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
@@ -214,6 +215,7 @@ type AuthSettings = {
|
|
|
214
215
|
providers: Provider[];
|
|
215
216
|
strategy: Strategy;
|
|
216
217
|
users: KVStore;
|
|
218
|
+
sessions: KVStore;
|
|
217
219
|
onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
|
|
218
220
|
onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
219
221
|
onUser: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
|
|
@@ -256,7 +258,6 @@ type Options = {
|
|
|
256
258
|
secret?: string;
|
|
257
259
|
public?: string | Bucket;
|
|
258
260
|
uploads?: string | Bucket | UploadOptions;
|
|
259
|
-
sessions?: StoreSource;
|
|
260
261
|
cors?: CorsOptions;
|
|
261
262
|
auth?: AuthOption;
|
|
262
263
|
openapi?: boolean | string | {
|
|
@@ -279,8 +280,6 @@ type Settings = {
|
|
|
279
280
|
uploads?: ({
|
|
280
281
|
bucket: Bucket;
|
|
281
282
|
} & LimitOptions) | null;
|
|
282
|
-
sessions: KVStore;
|
|
283
|
-
sessionsDefault?: boolean;
|
|
284
283
|
cors?: CorsSettings;
|
|
285
284
|
auth?: AuthSettings;
|
|
286
285
|
openapi?: {
|
|
@@ -339,7 +338,6 @@ type Context<C extends ContextTypes = {}> = {
|
|
|
339
338
|
time?: Time;
|
|
340
339
|
socket?: WebSocket;
|
|
341
340
|
sockets?: WebSocket[];
|
|
342
|
-
session: Field<C, "session", Record<string, any>>;
|
|
343
341
|
user?: Field<C, "user", Record<string, any>>;
|
|
344
342
|
init: number;
|
|
345
343
|
app: Server;
|
|
@@ -351,7 +349,7 @@ type Context<C extends ContextTypes = {}> = {
|
|
|
351
349
|
type InlineReply = Response | Reply | BucketFile | {
|
|
352
350
|
body: string;
|
|
353
351
|
headers?: Headers;
|
|
354
|
-
} | SerializableValue | JSX.Element | Buffer | ReadableStream;
|
|
352
|
+
} | SerializableValue | JSX$1.Element | Buffer | ReadableStream;
|
|
355
353
|
type Body = InlineReply;
|
|
356
354
|
type Middleware<C extends ContextTypes = {}> = (ctx: Context<C>) => InlineReply | Promise<InlineReply> | void | Promise<void>;
|
|
357
355
|
|
|
@@ -373,7 +371,7 @@ declare class ServerError extends Error {
|
|
|
373
371
|
declare const TypedServerError: typeof ServerError & ServerErrorConstructor;
|
|
374
372
|
|
|
375
373
|
declare global {
|
|
376
|
-
var env: Record<string,
|
|
374
|
+
var env: Record<string, string | undefined>;
|
|
377
375
|
}
|
|
378
376
|
|
|
379
377
|
type Fn<C extends ContextTypes> = (ctx: Context<C>) => ReturnType<Middleware>;
|
|
@@ -440,7 +438,7 @@ declare class ValidationError extends StatusError {
|
|
|
440
438
|
declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
441
439
|
settings: Settings;
|
|
442
440
|
platform: Platform;
|
|
443
|
-
sockets:
|
|
441
|
+
sockets: WebSocket[];
|
|
444
442
|
websocket: any;
|
|
445
443
|
port?: number;
|
|
446
444
|
constructor(options?: Options);
|
package/index.js
CHANGED
|
@@ -49,8 +49,6 @@ ServerError_default.extend({
|
|
|
49
49
|
status: 400,
|
|
50
50
|
message: "Missing the OAuth 'code' in the request body"
|
|
51
51
|
},
|
|
52
|
-
SESSION_JWT: "The `jwt` strategy is stateless, so there is no `ctx.session` (tried '{key}'). Use the `token` strategy for server-side sessions, or `cookie` for browsers",
|
|
53
|
-
SESSION_GUEST: "No `ctx.session` for this request (tried '{key}'): the `token` strategy carries the session in the Authorization header, and this request has none. Sign in first, or use the `cookie` strategy for guest sessions",
|
|
54
52
|
AUTH_INVALID_HEADER: {
|
|
55
53
|
status: 401,
|
|
56
54
|
message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
|
|
@@ -799,7 +797,7 @@ function isReadableStream(obj) {
|
|
|
799
797
|
|
|
800
798
|
// src/reply.ts
|
|
801
799
|
var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
|
|
802
|
-
var Reply = class {
|
|
800
|
+
var Reply = class _Reply {
|
|
803
801
|
res;
|
|
804
802
|
constructor() {
|
|
805
803
|
this.res = {
|
|
@@ -870,10 +868,12 @@ var Reply = class {
|
|
|
870
868
|
}
|
|
871
869
|
async file(path) {
|
|
872
870
|
if (typeof path !== "string") {
|
|
873
|
-
if (!await path.exists()) return
|
|
871
|
+
if (!await path.exists()) return new Response(null, { status: 404 });
|
|
874
872
|
return this.type(fileType(path)).send(path.stream());
|
|
875
873
|
}
|
|
876
|
-
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path))
|
|
874
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) {
|
|
875
|
+
return new Response(null, { status: 404 });
|
|
876
|
+
}
|
|
877
877
|
try {
|
|
878
878
|
const fs = await import("fs");
|
|
879
879
|
const ext = path.split(".").pop();
|
|
@@ -882,23 +882,55 @@ var Reply = class {
|
|
|
882
882
|
return this.type(ext).send(stream);
|
|
883
883
|
} catch (error) {
|
|
884
884
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
885
|
-
return
|
|
885
|
+
return new Response(null, { status: 404 });
|
|
886
886
|
}
|
|
887
887
|
throw error;
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
|
-
send(
|
|
890
|
+
// Accepts everything a route can return, so `send(x)` and `return x` agree.
|
|
891
|
+
// Async because a bucket file has to be read before its status is known;
|
|
892
|
+
// routes await whatever they return, so this is invisible in normal use.
|
|
893
|
+
async send(input = "") {
|
|
891
894
|
const { status: status2 = 200, headers: headers2 } = this.res;
|
|
895
|
+
let body = input;
|
|
892
896
|
if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
|
|
893
897
|
return new Response(null, { status: status2, headers: headers2 });
|
|
894
898
|
}
|
|
895
899
|
if (body === null) body = "";
|
|
900
|
+
if (typeof body?.then === "function") body = await body;
|
|
896
901
|
if (typeof body === "function") body = body();
|
|
897
902
|
if (typeof body?.then === "function") {
|
|
898
903
|
throw new Error(
|
|
899
|
-
"
|
|
904
|
+
"Cannot render an async component: components must be synchronous. Await the data before rendering and pass it in as props."
|
|
900
905
|
);
|
|
901
906
|
}
|
|
907
|
+
if (body instanceof _Reply) body = await body.send();
|
|
908
|
+
if (body instanceof Response) {
|
|
909
|
+
const merged = new Headers(body.headers);
|
|
910
|
+
for (const [key, value] of headers2) {
|
|
911
|
+
if (key === "set-cookie") continue;
|
|
912
|
+
merged.set(key, value);
|
|
913
|
+
}
|
|
914
|
+
for (const cookie of headers2.getSetCookie?.() ?? []) {
|
|
915
|
+
merged.append("set-cookie", cookie);
|
|
916
|
+
}
|
|
917
|
+
if (body.url && /^(br|gzip)$/.test(merged.get("content-encoding") || "")) {
|
|
918
|
+
merged.delete("content-encoding");
|
|
919
|
+
}
|
|
920
|
+
return new Response(body.body, {
|
|
921
|
+
status: this.res.status ?? body.status,
|
|
922
|
+
headers: merged
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (body && typeof body.stream === "function" && typeof body.bytes === "function" && typeof body.exists === "function" && typeof body.name === "string") {
|
|
926
|
+
return this.file(body);
|
|
927
|
+
}
|
|
928
|
+
if (body instanceof Blob) {
|
|
929
|
+
if (!headers2.get("content-type") && body.type) {
|
|
930
|
+
headers2.set("content-type", body.type);
|
|
931
|
+
}
|
|
932
|
+
return new Response(body, { status: status2, headers: headers2 });
|
|
933
|
+
}
|
|
902
934
|
if (typeof body === "string") {
|
|
903
935
|
if (!headers2.get("content-type")) {
|
|
904
936
|
headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
|
|
@@ -924,6 +956,12 @@ var Reply = class {
|
|
|
924
956
|
if (isReadableStream(body)) {
|
|
925
957
|
return new Response(toWeb(body), { status: status2, headers: headers2 });
|
|
926
958
|
}
|
|
959
|
+
if (!Array.isArray(body) && body?.[Symbol.iterator]) {
|
|
960
|
+
return new Response(iteratorToReadable(body), { status: status2, headers: headers2 });
|
|
961
|
+
}
|
|
962
|
+
if (body?.[Symbol.asyncIterator]) {
|
|
963
|
+
return new Response(iteratorAsyncToReadable(body), { status: status2, headers: headers2 });
|
|
964
|
+
}
|
|
927
965
|
if (!headers2.get("content-type")) {
|
|
928
966
|
headers2.set("content-type", "application/json");
|
|
929
967
|
}
|
|
@@ -1039,48 +1077,11 @@ function findSessionId(ctx) {
|
|
|
1039
1077
|
return ctx.cookies.session || void 0;
|
|
1040
1078
|
}
|
|
1041
1079
|
|
|
1042
|
-
// src/middle/session.ts
|
|
1043
|
-
var loaded = /* @__PURE__ */ new WeakMap();
|
|
1044
|
-
function noSession(error) {
|
|
1045
|
-
const target = {};
|
|
1046
|
-
return new Proxy(target, {
|
|
1047
|
-
get(target2, key) {
|
|
1048
|
-
if (typeof key === "symbol" || key === "then") return target2[key];
|
|
1049
|
-
throw error(String(key));
|
|
1050
|
-
},
|
|
1051
|
-
set(target2, key, value) {
|
|
1052
|
-
if (typeof key === "symbol") {
|
|
1053
|
-
target2[key] = value;
|
|
1054
|
-
return true;
|
|
1055
|
-
}
|
|
1056
|
-
throw error(String(key));
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
async function session(ctx) {
|
|
1061
|
-
const strategy = ctx.options.auth?.strategy;
|
|
1062
|
-
if (strategy?.includes("jwt")) {
|
|
1063
|
-
ctx.session = noSession((key) => ServerError_default.SESSION_JWT({ key }));
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
const id = findSessionId(ctx);
|
|
1067
|
-
if (!id && strategy?.includes("token")) {
|
|
1068
|
-
ctx.session = noSession((key) => ServerError_default.SESSION_GUEST({ key }));
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
ctx.session = id && await ctx.options.sessions.get(id) || {};
|
|
1072
|
-
loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
1080
|
// src/auth/finishLogin.ts
|
|
1076
1081
|
async function finishLogin(ctx, input, opts = {}) {
|
|
1077
1082
|
const settings = ctx.options.auth;
|
|
1078
1083
|
const { strategy, onLogin, onUser, onToken } = settings;
|
|
1079
1084
|
const key = String(input.key);
|
|
1080
|
-
if (!strategy.includes("jwt") && !loaded.has(ctx)) {
|
|
1081
|
-
ctx.session = {};
|
|
1082
|
-
loaded.set(ctx, { id: void 0, data: "{}" });
|
|
1083
|
-
}
|
|
1084
1085
|
const auth2 = {
|
|
1085
1086
|
user: key,
|
|
1086
1087
|
provider: input.provider,
|
|
@@ -1106,12 +1107,10 @@ async function finishLogin(ctx, input, opts = {}) {
|
|
|
1106
1107
|
assertUser(exposed, "onUser");
|
|
1107
1108
|
return status(201).json({ ...exposed, token });
|
|
1108
1109
|
}
|
|
1109
|
-
const prev =
|
|
1110
|
-
if (prev
|
|
1110
|
+
const prev = findSessionId(ctx);
|
|
1111
|
+
if (prev) await settings.sessions.del(prev);
|
|
1111
1112
|
const id = createId();
|
|
1112
|
-
|
|
1113
|
-
await ctx.options.sessions.set(id, ctx.session);
|
|
1114
|
-
loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
|
|
1113
|
+
await settings.sessions.set(id, auth2);
|
|
1115
1114
|
if (strategy.includes("token")) {
|
|
1116
1115
|
const exposed = await onUser(user, ctx);
|
|
1117
1116
|
assertUser(exposed, "onUser");
|
|
@@ -1631,6 +1630,7 @@ function parseAuthOptions(auth2) {
|
|
|
1631
1630
|
const onUser = auth2.onUser || defaultOnUser;
|
|
1632
1631
|
const onToken = auth2.onToken || defaultOnUser;
|
|
1633
1632
|
const users = auth2.users ? toStore(auth2.users) : null;
|
|
1633
|
+
const sessions = auth2.sessions ? toStoreExpiring(auth2.sessions, "1w") : null;
|
|
1634
1634
|
return {
|
|
1635
1635
|
strategy,
|
|
1636
1636
|
providers: list,
|
|
@@ -1640,7 +1640,8 @@ function parseAuthOptions(auth2) {
|
|
|
1640
1640
|
onUser,
|
|
1641
1641
|
onToken,
|
|
1642
1642
|
onLogout,
|
|
1643
|
-
users
|
|
1643
|
+
users,
|
|
1644
|
+
sessions
|
|
1644
1645
|
};
|
|
1645
1646
|
}
|
|
1646
1647
|
|
|
@@ -1830,7 +1831,8 @@ function config(options = {}) {
|
|
|
1830
1831
|
const level = raw === true ? "info" : raw === false ? void 0 : raw;
|
|
1831
1832
|
const log = createLogger(level);
|
|
1832
1833
|
const settings = {
|
|
1833
|
-
|
|
1834
|
+
// `env.PORT` is a string, so coerce it: `settings.port` is a number
|
|
1835
|
+
port: options.port || Number(env2.PORT) || 3e3,
|
|
1834
1836
|
secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
|
|
1835
1837
|
log,
|
|
1836
1838
|
// How request bodies are read: parsed into ctx.body by default; `raw` keeps
|
|
@@ -1838,11 +1840,7 @@ function config(options = {}) {
|
|
|
1838
1840
|
parser: options.parser ?? "parse",
|
|
1839
1841
|
// Secure-by-default response headers + trustProxy for ctx.ip. `false` turns
|
|
1840
1842
|
// the added headers off; see resolveSecurity for the defaults.
|
|
1841
|
-
security: resolveSecurity(options.security)
|
|
1842
|
-
// Sessions: one record per device, exposed as ctx.session. Anything
|
|
1843
|
-
// polystore accepts works; raw sources (a Map, a Redis client) get a 1w
|
|
1844
|
-
// expiry, a built store is honored as-is, prefix and expiry included.
|
|
1845
|
-
sessions: toStoreExpiring(options.sessions ?? /* @__PURE__ */ new Map(), "1w")
|
|
1843
|
+
security: resolveSecurity(options.security)
|
|
1846
1844
|
};
|
|
1847
1845
|
if (options.cache !== void 0) settings.cache = options.cache;
|
|
1848
1846
|
options.cors = options.cors || env2.CORS || null;
|
|
@@ -1885,10 +1883,10 @@ function config(options = {}) {
|
|
|
1885
1883
|
settings.public = publicDir ? bucket(publicDir) : null;
|
|
1886
1884
|
settings.uploads = resolveUploads(options.uploads);
|
|
1887
1885
|
const production = env2.NODE_ENV === "production";
|
|
1888
|
-
const defaulted = options.sessions == null;
|
|
1889
|
-
settings.sessionsDefault = defaulted;
|
|
1890
1886
|
if (options.auth || env2.AUTH) {
|
|
1891
|
-
settings.auth = parseAuthOptions(
|
|
1887
|
+
settings.auth = parseAuthOptions(
|
|
1888
|
+
options.auth || env2.AUTH || null
|
|
1889
|
+
);
|
|
1892
1890
|
}
|
|
1893
1891
|
if (settings.auth) {
|
|
1894
1892
|
if (!settings.auth.users) {
|
|
@@ -1899,10 +1897,13 @@ function config(options = {}) {
|
|
|
1899
1897
|
}
|
|
1900
1898
|
settings.auth.users = toStore(/* @__PURE__ */ new Map());
|
|
1901
1899
|
}
|
|
1902
|
-
if (
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1900
|
+
if (!settings.auth.sessions && !settings.auth.strategy.includes("jwt")) {
|
|
1901
|
+
if (production) {
|
|
1902
|
+
throw new Error(
|
|
1903
|
+
"Auth in production needs a persistent `sessions` store, like auth: { ..., sessions: kv(redis).prefix('session:') }."
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
settings.auth.sessions = toStoreExpiring(/* @__PURE__ */ new Map(), "1w");
|
|
1906
1907
|
}
|
|
1907
1908
|
}
|
|
1908
1909
|
if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
|
|
@@ -1928,7 +1929,6 @@ function config(options = {}) {
|
|
|
1928
1929
|
}
|
|
1929
1930
|
if (settings.public) log.message("public", loc(options.public));
|
|
1930
1931
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
1931
|
-
if (options.sessions) log.message("sessions", "enabled");
|
|
1932
1932
|
if (settings.cors) {
|
|
1933
1933
|
const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
|
|
1934
1934
|
log.message("cors", origin);
|
|
@@ -2038,74 +2038,17 @@ function getMachine() {
|
|
|
2038
2038
|
}
|
|
2039
2039
|
|
|
2040
2040
|
// src/parseResponse.ts
|
|
2041
|
-
var warned = false;
|
|
2042
|
-
var warnDefault = () => {
|
|
2043
|
-
if (warned) return;
|
|
2044
|
-
warned = true;
|
|
2045
|
-
console.warn(
|
|
2046
|
-
"[server:sessions] Using the default in-memory session store in production: sessions are lost on restart and not shared across instances. Configure one with sessions: kv(redis).prefix('session:')."
|
|
2047
|
-
);
|
|
2048
|
-
};
|
|
2049
2041
|
async function parseResponse(out, ctx) {
|
|
2050
2042
|
if (!out && typeof out !== "string") return null;
|
|
2051
2043
|
if (typeof out === "function") {
|
|
2052
2044
|
out = await out(ctx);
|
|
2053
|
-
|
|
2054
|
-
if (out && typeof out.send === "function" && out.res?.headers instanceof Headers) {
|
|
2055
|
-
out = out.send();
|
|
2056
|
-
}
|
|
2057
|
-
if (out instanceof Blob) {
|
|
2058
|
-
out = new Response(out, { headers: { "Content-Type": out.type } });
|
|
2059
|
-
}
|
|
2060
|
-
if (out && typeof out.stream === "function" && typeof out.bytes === "function" && typeof out.exists === "function" && typeof out.name === "string") {
|
|
2061
|
-
if (!await out.exists()) {
|
|
2062
|
-
out = new Response(null, { status: 404 });
|
|
2063
|
-
} else {
|
|
2064
|
-
const type2 = fileType(out);
|
|
2065
|
-
out = new Response(
|
|
2066
|
-
out.stream(),
|
|
2067
|
-
type2 ? { headers: { "content-type": type2 } } : void 0
|
|
2068
|
-
);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
if (out instanceof ReadableStream) {
|
|
2072
|
-
out = new Response(out);
|
|
2073
|
-
}
|
|
2074
|
-
if (out instanceof Uint8Array) {
|
|
2075
|
-
out = new Response(out);
|
|
2045
|
+
if (!out && typeof out !== "string") return null;
|
|
2076
2046
|
}
|
|
2077
2047
|
if (typeof out === "number") {
|
|
2078
|
-
out = new Response(
|
|
2048
|
+
out = new Response(null, { status: out });
|
|
2079
2049
|
}
|
|
2080
|
-
if (
|
|
2081
|
-
|
|
2082
|
-
out = new Response(out, {
|
|
2083
|
-
headers: {
|
|
2084
|
-
"content-type": type2,
|
|
2085
|
-
"content-length": String(Buffer.byteLength(out))
|
|
2086
|
-
}
|
|
2087
|
-
});
|
|
2088
|
-
}
|
|
2089
|
-
if (out?.constructor === Object || Array.isArray(out)) {
|
|
2090
|
-
out = json(out);
|
|
2091
|
-
}
|
|
2092
|
-
if (out[Symbol.iterator]) {
|
|
2093
|
-
out = new Response(iteratorToReadable(out));
|
|
2094
|
-
}
|
|
2095
|
-
if (out[Symbol.asyncIterator] && !(out instanceof Response)) {
|
|
2096
|
-
out = new Response(iteratorAsyncToReadable(out));
|
|
2097
|
-
}
|
|
2098
|
-
if (out instanceof Response && out.url && out.body) {
|
|
2099
|
-
out = new Response(out.body, {
|
|
2100
|
-
status: out.status,
|
|
2101
|
-
headers: out.headers
|
|
2102
|
-
});
|
|
2103
|
-
if (/^(br|gzip)$/.test(out.headers.get("content-encoding") || "")) {
|
|
2104
|
-
out.headers.delete("content-encoding");
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
if (!(out instanceof Response)) {
|
|
2108
|
-
throw new Error(`Invalid response type ${out}`);
|
|
2050
|
+
if (!(out instanceof Response) || out.url) {
|
|
2051
|
+
out = await send(out);
|
|
2109
2052
|
}
|
|
2110
2053
|
applyCors(out, ctx);
|
|
2111
2054
|
applySecurity(out, ctx);
|
|
@@ -2113,27 +2056,6 @@ async function parseResponse(out, ctx) {
|
|
|
2113
2056
|
if (ctx.time?.times?.length > 1) {
|
|
2114
2057
|
out.headers.set("Server-Timing", ctx.time.headers());
|
|
2115
2058
|
}
|
|
2116
|
-
const prev = loaded.get(ctx);
|
|
2117
|
-
if (prev && JSON.stringify(ctx.session ?? {}) !== prev.data) {
|
|
2118
|
-
if (ctx.options.sessionsDefault && ctx.platform.production) {
|
|
2119
|
-
warnDefault();
|
|
2120
|
-
}
|
|
2121
|
-
let id = prev.id;
|
|
2122
|
-
if (!id) {
|
|
2123
|
-
id = createId();
|
|
2124
|
-
out.headers.append(
|
|
2125
|
-
"set-cookie",
|
|
2126
|
-
createCookies("session", {
|
|
2127
|
-
value: id,
|
|
2128
|
-
path: "/",
|
|
2129
|
-
httpOnly: true,
|
|
2130
|
-
secure: ctx.platform.production,
|
|
2131
|
-
sameSite: "Lax"
|
|
2132
|
-
})
|
|
2133
|
-
);
|
|
2134
|
-
}
|
|
2135
|
-
ctx.options.sessions.set(id, ctx.session);
|
|
2136
|
-
}
|
|
2137
2059
|
return out;
|
|
2138
2060
|
}
|
|
2139
2061
|
|
|
@@ -2472,14 +2394,10 @@ async function getJwtUser(ctx) {
|
|
|
2472
2394
|
return exposed;
|
|
2473
2395
|
}
|
|
2474
2396
|
async function getAuthSession(ctx) {
|
|
2475
|
-
if (loaded.has(ctx)) {
|
|
2476
|
-
const session3 = ctx.session;
|
|
2477
|
-
return session3?.user ? session3 : void 0;
|
|
2478
|
-
}
|
|
2479
2397
|
const id = findSessionId(ctx);
|
|
2480
2398
|
if (!id) return;
|
|
2481
|
-
const
|
|
2482
|
-
return
|
|
2399
|
+
const session = await ctx.options.auth.sessions.get(id);
|
|
2400
|
+
return session?.user ? session : void 0;
|
|
2483
2401
|
}
|
|
2484
2402
|
async function getUser(ctx) {
|
|
2485
2403
|
if (!ctx.options.auth) return;
|
|
@@ -2504,10 +2422,8 @@ async function getUser(ctx) {
|
|
|
2504
2422
|
async function logout(ctx) {
|
|
2505
2423
|
const { strategy } = ctx.options.auth;
|
|
2506
2424
|
if (!strategy.includes("jwt")) {
|
|
2507
|
-
const prev =
|
|
2508
|
-
if (prev
|
|
2509
|
-
ctx.session = {};
|
|
2510
|
-
loaded.set(ctx, { id: void 0, data: "{}" });
|
|
2425
|
+
const prev = findSessionId(ctx);
|
|
2426
|
+
if (prev) await ctx.options.auth.sessions.del(prev);
|
|
2511
2427
|
}
|
|
2512
2428
|
if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
|
|
2513
2429
|
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
@@ -3034,7 +2950,6 @@ async function createNode(req, app, signal = new AbortController().signal) {
|
|
|
3034
2950
|
headers: headers2,
|
|
3035
2951
|
cookies: cookies2,
|
|
3036
2952
|
signal,
|
|
3037
|
-
session: {},
|
|
3038
2953
|
init,
|
|
3039
2954
|
app,
|
|
3040
2955
|
ip: clientIp(headers2, {
|
|
@@ -3075,7 +2990,6 @@ async function createWinter(req, app, server2) {
|
|
|
3075
2990
|
headers: headers2,
|
|
3076
2991
|
cookies: cookies2,
|
|
3077
2992
|
signal: req.signal,
|
|
3078
|
-
session: {},
|
|
3079
2993
|
init,
|
|
3080
2994
|
app,
|
|
3081
2995
|
ip: clientIp(headers2, {
|
|
@@ -3326,7 +3240,6 @@ var Server = class extends Router {
|
|
|
3326
3240
|
app.use(timer);
|
|
3327
3241
|
if (this.settings.cors) app.use(preflight);
|
|
3328
3242
|
app.use(assets);
|
|
3329
|
-
app.use(session);
|
|
3330
3243
|
if (this.settings.auth) {
|
|
3331
3244
|
auth(app);
|
|
3332
3245
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"types": "index.d.ts",
|
|
26
26
|
"files": [
|
|
27
27
|
"index.d.ts",
|
|
28
|
-
"src/jsx
|
|
28
|
+
"src/jsx/*.js",
|
|
29
|
+
"src/jsx/jsx-runtime.d.ts"
|
|
29
30
|
],
|
|
30
31
|
"exports": {
|
|
31
32
|
".": "./index.js",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"menu": {
|
|
46
47
|
"Documentation": "/documentation",
|
|
47
48
|
"Tutorials": "/tutorials",
|
|
49
|
+
"Donate": "https://www.paypal.com/paypalme/franciscopresencia/19",
|
|
48
50
|
"Github": "https://github.com/franciscop/server-next"
|
|
49
51
|
},
|
|
50
52
|
"documentation": {
|
package/readme.md
CHANGED
|
@@ -15,16 +15,24 @@ export default server({ uploads: './uploads' })
|
|
|
15
15
|
.post('/avatar', (ctx) => ctx.body.avatar.path);
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Key-value stores and file storage come included, so
|
|
18
|
+
Key-value stores and file storage come included, so logins work out of the box and `uploads` takes a folder path. For Redis, S3 and the rest, pass the client straight in:
|
|
19
19
|
|
|
20
20
|
```js
|
|
21
|
-
import server, { bucket } from '@server/next';
|
|
21
|
+
import server, { bucket, kv } from '@server/next';
|
|
22
22
|
import { createClient } from 'redis';
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const redis = kv(createClient({ url }));
|
|
25
25
|
const uploads = bucket.S3('my-bucket', { id, key });
|
|
26
26
|
|
|
27
|
-
export default server({
|
|
27
|
+
export default server({
|
|
28
|
+
uploads,
|
|
29
|
+
auth: {
|
|
30
|
+
strategy: 'cookie',
|
|
31
|
+
providers: ['github'],
|
|
32
|
+
users: redis.prefix('user:'),
|
|
33
|
+
sessions: redis.prefix('session:'),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
28
36
|
```
|
|
29
37
|
|
|
30
38
|
See the [full documentation](https://serverjs.io/documentation).
|
package/src/jsx/jsx-runtime.d.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// What a JSX tag can be: an intrinsic name, a Fragment, a function component,
|
|
2
|
+
// or an object exposing `render` (the shape forwardRef-style components take)
|
|
3
|
+
type Tag =
|
|
4
|
+
| string
|
|
5
|
+
| symbol
|
|
6
|
+
| ((props: any) => unknown)
|
|
7
|
+
| { render: (props: any, ref: unknown) => unknown };
|
|
8
|
+
|
|
9
|
+
export declare function jsx(
|
|
10
|
+
type: Tag,
|
|
11
|
+
props?: any,
|
|
12
|
+
key?: string | number,
|
|
13
|
+
): JSX.Element;
|
|
14
|
+
export declare function jsxs(
|
|
15
|
+
type: Tag,
|
|
16
|
+
props?: any,
|
|
17
|
+
key?: string | number,
|
|
18
|
+
): JSX.Element;
|
|
19
|
+
export declare function jsxDEV(
|
|
20
|
+
type: Tag,
|
|
21
|
+
props?: any,
|
|
22
|
+
key?: string | number,
|
|
23
|
+
): JSX.Element;
|
|
24
|
+
export declare const Fragment: symbol;
|
|
5
25
|
|
|
6
26
|
export declare namespace JSX {
|
|
7
27
|
interface Element {
|
|
8
|
-
|
|
9
|
-
props: any;
|
|
28
|
+
(): string;
|
|
10
29
|
}
|
|
11
30
|
interface IntrinsicElements {
|
|
12
31
|
[elem: string]: any;
|
|
@@ -16,8 +35,7 @@ export declare namespace JSX {
|
|
|
16
35
|
declare global {
|
|
17
36
|
namespace JSX {
|
|
18
37
|
interface Element {
|
|
19
|
-
|
|
20
|
-
props: any;
|
|
38
|
+
(): string;
|
|
21
39
|
}
|
|
22
40
|
interface IntrinsicElements {
|
|
23
41
|
[elem: string]: any;
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -21,8 +21,6 @@ const SVG_TAGS = new Set([
|
|
|
21
21
|
"defs",
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
-
const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
25
|
-
|
|
26
24
|
const ALT_NAMES = {
|
|
27
25
|
classname: "class",
|
|
28
26
|
htmlfor: "for",
|
|
@@ -100,8 +98,9 @@ const renderChild = (child) => {
|
|
|
100
98
|
};
|
|
101
99
|
|
|
102
100
|
const jsx = (tag, { children, ...props } = {}) => {
|
|
103
|
-
// Fragment
|
|
104
|
-
|
|
101
|
+
// Fragment. It is the registered React symbol, so a compiled React library
|
|
102
|
+
// matches whether it imports Fragment or inlines Symbol.for("react.fragment")
|
|
103
|
+
if (tag === Fragment) {
|
|
105
104
|
return () => renderChild(children);
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -158,7 +157,9 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
158
157
|
// `minify` drives <style>, it isn't an HTML attribute
|
|
159
158
|
.filter(([k]) => !(tag === "style" && k === "minify"))
|
|
160
159
|
.filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
|
|
161
|
-
|
|
160
|
+
// `false`, `null` and `undefined` drop the attribute (as React does),
|
|
161
|
+
// rather than rendering it empty
|
|
162
|
+
.filter(([, v]) => v !== false && v != null)
|
|
162
163
|
.map(([k, v]) => {
|
|
163
164
|
const preserveSvg = k === "viewBox" || k === "preserveAspectRatio";
|
|
164
165
|
|
|
@@ -180,6 +181,7 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
180
181
|
return `${cssKey}:${val}`;
|
|
181
182
|
})
|
|
182
183
|
.join(";");
|
|
184
|
+
if (!v) return null; // an empty style object adds nothing
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
const value =
|
|
@@ -187,6 +189,7 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
187
189
|
|
|
188
190
|
return `${key}="${value}"`;
|
|
189
191
|
})
|
|
192
|
+
.filter((attr) => attr !== null)
|
|
190
193
|
.join(" ");
|
|
191
194
|
|
|
192
195
|
if (attrStr) attrStr = ` ${attrStr}`;
|
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource . */
|
|
2
|
-
|
|
3
|
-
expect.extend({
|
|
4
|
-
toRender(fn, rendered) {
|
|
5
|
-
const html = fn();
|
|
6
|
-
if (this.isNot) {
|
|
7
|
-
const msg = `Expected "${html}" to render differently`;
|
|
8
|
-
return { pass: html === rendered, message: () => msg };
|
|
9
|
-
}
|
|
10
|
-
const msg = `Expected "${html}" to be rendered as "${rendered}"`;
|
|
11
|
-
return { pass: html === rendered, message: () => msg };
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("jsx", () => {
|
|
16
|
-
it("can render a div", () => {
|
|
17
|
-
expect(<div>Hello</div>).toRender("<div>Hello</div>");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("can render an input with attributes", () => {
|
|
21
|
-
expect(<input name="hello" />).toRender(`<input name="hello" />`);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("will stringify an attribute", () => {
|
|
25
|
-
expect(<input value={5.2} />).toRender('<input value="5.2" />');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("will ignore a boolean attribute", () => {
|
|
29
|
-
expect(<input disabled />).toRender("<input disabled />");
|
|
30
|
-
expect(<input disabled={true} />).toRender("<input disabled />");
|
|
31
|
-
expect(<input disabled={false} />).toRender("<input />");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("will remove events", () => {
|
|
35
|
-
expect(<input onChange={() => console.log("hello")} />).toRender(
|
|
36
|
-
"<input />",
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("will inject the doctype for html", () => {
|
|
41
|
-
// biome-ignore lint/a11y/useHtmlLang: This is an example, not user code
|
|
42
|
-
expect(<html>Hello</html>).toRender("<!DOCTYPE html><html>Hello</html>");
|
|
43
|
-
expect(<html lang="en">Hello</html>).toRender(
|
|
44
|
-
`<!DOCTYPE html><html lang="en">Hello</html>`,
|
|
45
|
-
);
|
|
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
|
-
|
|
60
|
-
it("can render a pre inside a div", () => {
|
|
61
|
-
expect(
|
|
62
|
-
<div>
|
|
63
|
-
<pre>line1{"\n"}line2</pre>
|
|
64
|
-
</div>,
|
|
65
|
-
).toRender("<div><pre>line1\nline2</pre></div>");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("does NOT render the <pre>", () => {
|
|
69
|
-
expect(<code>{"<pre>"}</code>).toRender("<code><pre></code>");
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("text encoding", () => {
|
|
74
|
-
it("encodes < and > to prevent XSS", () => {
|
|
75
|
-
expect(<div>{"<script>alert(1)</script>"}</div>).toRender(
|
|
76
|
-
"<div><script>alert(1)</script></div>",
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("encodes & in text content", () => {
|
|
81
|
-
expect(<div>{"a & b"}</div>).toRender("<div>a & b</div>");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('encodes " in text content', () => {
|
|
85
|
-
expect(<div>{'say "hi"'}</div>).toRender("<div>say "hi"</div>");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("encodes ' in text content", () => {
|
|
89
|
-
expect(<div>{"it's"}</div>).toRender("<div>it's</div>");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("renders 0 as a child", () => {
|
|
93
|
-
expect(<div>{0}</div>).toRender("<div>0</div>");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("renders positive integers", () => {
|
|
97
|
-
expect(<div>{42}</div>).toRender("<div>42</div>");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("renders floats", () => {
|
|
101
|
-
expect(<div>{3.14}</div>).toRender("<div>3.14</div>");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("renders negative numbers", () => {
|
|
105
|
-
expect(<div>{-1}</div>).toRender("<div>-1</div>");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("renders nothing for null", () => {
|
|
109
|
-
expect(<div>{null}</div>).toRender("<div></div>");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("renders nothing for undefined", () => {
|
|
113
|
-
expect(<div>{undefined}</div>).toRender("<div></div>");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("renders nothing for false", () => {
|
|
117
|
-
expect(<div>{false}</div>).toRender("<div></div>");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("renders nothing for true", () => {
|
|
121
|
-
expect(<div>{true}</div>).toRender("<div></div>");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("renders conditional content", () => {
|
|
125
|
-
const show = true;
|
|
126
|
-
expect(<div>{show && <span>yes</span>}</div>).toRender(
|
|
127
|
-
"<div><span>yes</span></div>",
|
|
128
|
-
);
|
|
129
|
-
const hide = false;
|
|
130
|
-
expect(<div>{hide && <span>no</span>}</div>).toRender("<div></div>");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("renders multiple children in order", () => {
|
|
134
|
-
expect(
|
|
135
|
-
<ul>
|
|
136
|
-
<li>a</li>
|
|
137
|
-
<li>b</li>
|
|
138
|
-
<li>c</li>
|
|
139
|
-
</ul>,
|
|
140
|
-
).toRender("<ul><li>a</li><li>b</li><li>c</li></ul>");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("renders array children", () => {
|
|
144
|
-
const items = ["x", "y", "z"];
|
|
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>");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("treats string returned from a function child as raw HTML", () => {
|
|
155
|
-
const getHtml = () => "<b>bold</b>";
|
|
156
|
-
expect(<div>{getHtml}</div>).toRender("<div><b>bold</b></div>");
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("attribute encoding", () => {
|
|
161
|
-
it("encodes < and > in attribute values", () => {
|
|
162
|
-
expect(<div title="<b>hi</b>"></div>).toRender(
|
|
163
|
-
'<div title="<b>hi</b>"></div>',
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("encodes & in attribute values", () => {
|
|
168
|
-
expect(<div title="a & b"></div>).toRender('<div title="a & b"></div>');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('encodes " in attribute values', () => {
|
|
172
|
-
expect(<div title={`say "hi"`}></div>).toRender(
|
|
173
|
-
'<div title="say "hi""></div>',
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("encodes ' in attribute values", () => {
|
|
178
|
-
expect(<div title={"it's"}></div>).toRender('<div title="it's"></div>');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("maps className to class", () => {
|
|
182
|
-
expect(<div className="foo bar"></div>).toRender(
|
|
183
|
-
'<div class="foo bar"></div>',
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("maps htmlFor to for", () => {
|
|
188
|
-
expect(<label htmlFor="email"></label>).toRender(
|
|
189
|
-
'<label for="email"></label>',
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("supports data-* attributes", () => {
|
|
194
|
-
expect(<div data-id="42" data-name="test"></div>).toRender(
|
|
195
|
-
'<div data-id="42" data-name="test"></div>',
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("supports aria-* attributes", () => {
|
|
200
|
-
expect(<button aria-label="close" aria-expanded={false}></button>).toRender(
|
|
201
|
-
'<button aria-label="close"></button>',
|
|
202
|
-
);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("supports numeric attribute values", () => {
|
|
206
|
-
expect(<input tabIndex={3} />).toRender('<input tabIndex="3" />');
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("strips function-valued attributes", () => {
|
|
210
|
-
expect(<div onFoo={() => {}} ref={() => {}}></div>).toRender("<div></div>");
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe("self-closing tags", () => {
|
|
215
|
-
it("renders br", () => {
|
|
216
|
-
expect(<br />).toRender("<br />");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("renders hr", () => {
|
|
220
|
-
expect(<hr />).toRender("<hr />");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("renders img with attributes", () => {
|
|
224
|
-
expect(<img src="cat.png" alt="A cat" />).toRender(
|
|
225
|
-
'<img src="cat.png" alt="A cat" />',
|
|
226
|
-
);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("renders input with type and name", () => {
|
|
230
|
-
expect(<input type="text" name="q" />).toRender(
|
|
231
|
-
'<input type="text" name="q" />',
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("renders link tag", () => {
|
|
236
|
-
expect(<link rel="stylesheet" href="style.css" />).toRender(
|
|
237
|
-
'<link rel="stylesheet" href="style.css" />',
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("renders meta tag", () => {
|
|
242
|
-
expect(<meta name="description" content="hello" />).toRender(
|
|
243
|
-
'<meta name="description" content="hello" />',
|
|
244
|
-
);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("renders source tag", () => {
|
|
248
|
-
expect(<source src="video.mp4" type="video/mp4" />).toRender(
|
|
249
|
-
'<source src="video.mp4" type="video/mp4" />',
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describe("fragments", () => {
|
|
255
|
-
it("does not crash on symbol-based element types (React internals)", () => {
|
|
256
|
-
expect(
|
|
257
|
-
<>
|
|
258
|
-
<span>Hello</span> world
|
|
259
|
-
</>,
|
|
260
|
-
).toRender("<span>Hello</span> world");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("renders nested fragments", () => {
|
|
264
|
-
expect(
|
|
265
|
-
<>
|
|
266
|
-
<>
|
|
267
|
-
<span>A</span>
|
|
268
|
-
</>
|
|
269
|
-
B
|
|
270
|
-
</>,
|
|
271
|
-
).toRender("<span>A</span>B");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("handles empty fragments", () => {
|
|
275
|
-
expect(<></>).toRender("");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("encodes text inside a fragment", () => {
|
|
279
|
-
expect(<>{"<b>"}</>).toRender("<b>");
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("script tag", () => {
|
|
284
|
-
it("renders script content without encoding", () => {
|
|
285
|
-
expect(<script>{"const x = 1 < 2 && 3 > 0;"}</script>).toRender(
|
|
286
|
-
"<script>const x = 1 < 2 && 3 > 0;</script>",
|
|
287
|
-
);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("renders script with src attribute and no children", () => {
|
|
291
|
-
expect(<script src="app.js"></script>).toRender(
|
|
292
|
-
'<script src="app.js"></script>',
|
|
293
|
-
);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("renders empty script tag", () => {
|
|
297
|
-
expect(<script></script>).toRender("<script></script>");
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
describe("style tag", () => {
|
|
302
|
-
it("renders the CSS as written", () => {
|
|
303
|
-
expect(<style>{"body { color: red; }"}</style>).toRender(
|
|
304
|
-
"<style>body { color: red; }</style>",
|
|
305
|
-
);
|
|
306
|
-
});
|
|
307
|
-
|
|
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>",
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
|
|
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
|
-
);
|
|
319
|
-
});
|
|
320
|
-
|
|
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>",
|
|
327
|
-
);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("never renders `minify` as an attribute", () => {
|
|
331
|
-
expect(<style minify>{"a { color: red; }"}</style>).toRender(
|
|
332
|
-
"<style>a{color:red}</style>",
|
|
333
|
-
);
|
|
334
|
-
});
|
|
335
|
-
|
|
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
|
-
});
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
describe("dangerouslySetInnerHTML", () => {
|
|
415
|
-
it("renders raw HTML without encoding", () => {
|
|
416
|
-
expect(
|
|
417
|
-
<div dangerouslySetInnerHTML={{ __html: "<b>bold</b>" }}></div>,
|
|
418
|
-
).toRender("<div><b>bold</b></div>");
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it("does not double-encode HTML entities", () => {
|
|
422
|
-
expect(
|
|
423
|
-
<div dangerouslySetInnerHTML={{ __html: "& <" }}></div>,
|
|
424
|
-
).toRender("<div>& <</div>");
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it("preserves script tags in raw HTML", () => {
|
|
428
|
-
expect(
|
|
429
|
-
<div
|
|
430
|
-
dangerouslySetInnerHTML={{ __html: "<script>alert(1)</script>" }}
|
|
431
|
-
></div>,
|
|
432
|
-
).toRender("<div><script>alert(1)</script></div>");
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it("does not render dangerouslySetInnerHTML as an attribute", () => {
|
|
436
|
-
expect(<div dangerouslySetInnerHTML={{ __html: "hi" }}></div>).toRender(
|
|
437
|
-
"<div>hi</div>",
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
describe("function components", () => {
|
|
443
|
-
it("renders a simple function component", () => {
|
|
444
|
-
const Greeting = ({ name }) => <p>Hello, {name}!</p>;
|
|
445
|
-
expect(<Greeting name="world" />).toRender("<p>Hello, world!</p>");
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("renders a component with children", () => {
|
|
449
|
-
const Box = ({ children }) => <div className="box">{children}</div>;
|
|
450
|
-
expect(
|
|
451
|
-
<Box>
|
|
452
|
-
<span>inside</span>
|
|
453
|
-
</Box>,
|
|
454
|
-
).toRender('<div class="box"><span>inside</span></div>');
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it("renders nested components", () => {
|
|
458
|
-
const Inner = () => <em>inner</em>;
|
|
459
|
-
const Outer = () => (
|
|
460
|
-
<div>
|
|
461
|
-
<Inner />
|
|
462
|
-
</div>
|
|
463
|
-
);
|
|
464
|
-
expect(<Outer />).toRender("<div><em>inner</em></div>");
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it("renders a component returning null", () => {
|
|
468
|
-
const Empty = () => null;
|
|
469
|
-
expect(<Empty />).toRender("");
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it("renders a component returning a string", () => {
|
|
473
|
-
const Text = () => "hello";
|
|
474
|
-
expect(<Text />).toRender("hello");
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("encodes text returned by a component", () => {
|
|
478
|
-
const Unsafe = () => "<script>alert(1)</script>";
|
|
479
|
-
expect(<Unsafe />).toRender("<script>alert(1)</script>");
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
describe("forwardRef-like components", () => {
|
|
484
|
-
it("renders an object with a render function", () => {
|
|
485
|
-
const Button = { render: ({ children }) => <button>{children}</button> };
|
|
486
|
-
expect(<Button>click me</Button>).toRender("<button>click me</button>");
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it("passes props to render function", () => {
|
|
490
|
-
const Input = {
|
|
491
|
-
render: ({ type, placeholder }) => (
|
|
492
|
-
<input type={type} placeholder={placeholder} />
|
|
493
|
-
),
|
|
494
|
-
};
|
|
495
|
-
expect(<Input type="email" placeholder="you@example.com" />).toRender(
|
|
496
|
-
'<input type="email" placeholder="you@example.com" />',
|
|
497
|
-
);
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
describe("React element interop", () => {
|
|
502
|
-
// Simulate a component built with React's jsx runtime, which returns plain
|
|
503
|
-
// objects { type, props } instead of @server/next functions
|
|
504
|
-
const reactEl = (type, props = {}) => ({ type, props });
|
|
505
|
-
|
|
506
|
-
it("renders a React element returned from a function component", () => {
|
|
507
|
-
const Greeting = () => reactEl("h1", { children: "Hello" });
|
|
508
|
-
expect(<Greeting />).toRender("<h1>Hello</h1>");
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it("renders nested React elements", () => {
|
|
512
|
-
const Card = () =>
|
|
513
|
-
reactEl("div", {
|
|
514
|
-
children: [
|
|
515
|
-
reactEl("h2", { children: "Title" }),
|
|
516
|
-
reactEl("p", { children: "Body" }),
|
|
517
|
-
],
|
|
518
|
-
});
|
|
519
|
-
expect(<Card />).toRender("<div><h2>Title</h2><p>Body</p></div>");
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it("renders React elements as children of a native element", () => {
|
|
523
|
-
const child = reactEl("strong", { children: "bold" });
|
|
524
|
-
expect(<p>{child}</p>).toRender("<p><strong>bold</strong></p>");
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
it("renders a mix of React elements and plain strings as children", () => {
|
|
528
|
-
const em = reactEl("em", { children: "world" });
|
|
529
|
-
expect(<p>Hello {em}!</p>).toRender("<p>Hello <em>world</em>!</p>");
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it("renders a React element with attributes", () => {
|
|
533
|
-
const Link = () =>
|
|
534
|
-
reactEl("a", { href: "https://example.com", children: "click" });
|
|
535
|
-
expect(<Link />).toRender(`<a href="https://example.com">click</a>`);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("renders deeply nested React elements", () => {
|
|
539
|
-
const Deep = () =>
|
|
540
|
-
reactEl("div", {
|
|
541
|
-
children: reactEl("ul", {
|
|
542
|
-
children: [
|
|
543
|
-
reactEl("li", { children: "one" }),
|
|
544
|
-
reactEl("li", { children: "two" }),
|
|
545
|
-
],
|
|
546
|
-
}),
|
|
547
|
-
});
|
|
548
|
-
expect(<Deep />).toRender("<div><ul><li>one</li><li>two</li></ul></div>");
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
describe("styles", () => {
|
|
552
|
-
it("stringifies style object into CSS", () => {
|
|
553
|
-
expect(
|
|
554
|
-
<div
|
|
555
|
-
style={{
|
|
556
|
-
display: "inline-block",
|
|
557
|
-
padding: "2px 8px",
|
|
558
|
-
borderRadius: "4px",
|
|
559
|
-
}}
|
|
560
|
-
/>,
|
|
561
|
-
).toRender(
|
|
562
|
-
'<div style="display:inline-block;padding:2px 8px;border-radius:4px"></div>',
|
|
563
|
-
);
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it("handles numbers and mixed style values", () => {
|
|
567
|
-
expect(
|
|
568
|
-
<div
|
|
569
|
-
style={{
|
|
570
|
-
fontSize: 12,
|
|
571
|
-
lineHeight: 1.5,
|
|
572
|
-
fontWeight: "600",
|
|
573
|
-
}}
|
|
574
|
-
/>,
|
|
575
|
-
).toRender(
|
|
576
|
-
'<div style="font-size:12;line-height:1.5;font-weight:600"></div>',
|
|
577
|
-
);
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
it("converts camelCase CSS properties", () => {
|
|
581
|
-
expect(
|
|
582
|
-
<div
|
|
583
|
-
style={{
|
|
584
|
-
backgroundColor: "#fff",
|
|
585
|
-
marginTop: "10px",
|
|
586
|
-
}}
|
|
587
|
-
/>,
|
|
588
|
-
).toRender('<div style="background-color:#fff;margin-top:10px"></div>');
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it("ignores null and undefined style values", () => {
|
|
592
|
-
expect(
|
|
593
|
-
<div
|
|
594
|
-
style={{
|
|
595
|
-
color: "red",
|
|
596
|
-
padding: null,
|
|
597
|
-
margin: undefined,
|
|
598
|
-
}}
|
|
599
|
-
/>,
|
|
600
|
-
).toRender('<div style="color:red"></div>');
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
describe("svg attributes", () => {
|
|
605
|
-
it("converts strokeWidth to stroke-width", () => {
|
|
606
|
-
expect(
|
|
607
|
-
<svg>
|
|
608
|
-
<circle strokeWidth="2" />
|
|
609
|
-
</svg>,
|
|
610
|
-
).toRender('<svg><circle stroke-width="2"></circle></svg>');
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it("converts fillOpacity to fill-opacity", () => {
|
|
614
|
-
expect(
|
|
615
|
-
<svg>
|
|
616
|
-
<rect fillOpacity="0.5" />
|
|
617
|
-
</svg>,
|
|
618
|
-
).toRender('<svg><rect fill-opacity="0.5"></rect></svg>');
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it("converts strokeLinecap to stroke-linecap", () => {
|
|
622
|
-
expect(
|
|
623
|
-
<svg>
|
|
624
|
-
<line strokeLinecap="round" />
|
|
625
|
-
</svg>,
|
|
626
|
-
).toRender('<svg><line stroke-linecap="round"></line></svg>');
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it("does NOT break viewBox casing", () => {
|
|
630
|
-
expect(
|
|
631
|
-
<svg viewBox="0 0 100 100">
|
|
632
|
-
<rect />
|
|
633
|
-
</svg>,
|
|
634
|
-
).toRender('<svg viewBox="0 0 100 100"><rect></rect></svg>');
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it("handles mixed SVG attributes correctly", () => {
|
|
638
|
-
expect(
|
|
639
|
-
<svg viewBox="0 0 10 10">
|
|
640
|
-
<circle strokeWidth="1" fillOpacity="0.2" />
|
|
641
|
-
</svg>,
|
|
642
|
-
).toRender(
|
|
643
|
-
'<svg viewBox="0 0 10 10"><circle stroke-width="1" fill-opacity="0.2"></circle></svg>',
|
|
644
|
-
);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
it("keeps viewBox unchanged", () => {
|
|
648
|
-
expect(
|
|
649
|
-
<svg viewBox="0 0 100 100">
|
|
650
|
-
<rect />
|
|
651
|
-
</svg>,
|
|
652
|
-
).toRender('<svg viewBox="0 0 100 100"><rect></rect></svg>');
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("does NOT convert viewBox to view-box", () => {
|
|
656
|
-
expect(<svg viewBox="0 0 10 10"></svg>).toRender(
|
|
657
|
-
'<svg viewBox="0 0 10 10"></svg>',
|
|
658
|
-
);
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it("converts strokeWidth to stroke-width", () => {
|
|
662
|
-
expect(
|
|
663
|
-
<svg>
|
|
664
|
-
<circle strokeWidth="2" />
|
|
665
|
-
</svg>,
|
|
666
|
-
).toRender('<svg><circle stroke-width="2"></circle></svg>');
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it("converts fillOpacity to fill-opacity", () => {
|
|
670
|
-
expect(
|
|
671
|
-
<svg>
|
|
672
|
-
<rect fillOpacity="0.5" />
|
|
673
|
-
</svg>,
|
|
674
|
-
).toRender('<svg><rect fill-opacity="0.5"></rect></svg>');
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
it("converts strokeLinecap to stroke-linecap", () => {
|
|
678
|
-
expect(
|
|
679
|
-
<svg>
|
|
680
|
-
<line strokeLinecap="round" />
|
|
681
|
-
</svg>,
|
|
682
|
-
).toRender('<svg><line stroke-linecap="round"></line></svg>');
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it("handles mixed SVG attributes correctly", () => {
|
|
686
|
-
expect(
|
|
687
|
-
<svg viewBox="0 0 10 10">
|
|
688
|
-
<circle strokeWidth="1" fillOpacity="0.2" />
|
|
689
|
-
</svg>,
|
|
690
|
-
).toRender(
|
|
691
|
-
'<svg viewBox="0 0 10 10"><circle stroke-width="1" fill-opacity="0.2"></circle></svg>',
|
|
692
|
-
);
|
|
693
|
-
});
|
|
694
|
-
});
|