@server/next 0.23.0 → 0.24.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/package.json +5 -5
- package/src/auth/index.js +32 -8
- package/src/auth/providers/email.js +10 -5
- package/src/auth/providers/github.js +1 -1
- package/src/helpers/config.js +7 -0
- package/src/helpers/createId.js +1 -2
- package/src/helpers/jsx.js +2 -1
- package/{index.d.ts → src/index.d.ts} +87 -21
- package/src/index.js +5 -2
- package/src/index.types.ts +28 -0
- package/src/middle/index.js +2 -1
- package/src/middle/openapi.js +146 -0
- package/src/parseResponse.js +1 -3
- package/src/pathPattern.js +14 -1
- package/src/pathPattern.test.js +27 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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": "https://github.com/franciscop/server-next.git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun test --watch",
|
|
22
|
-
"lint": "npx @biomejs/biome lint ./src --skip=lint/style/noParameterAssign",
|
|
23
|
-
"test": "bun test",
|
|
22
|
+
"lint": "npx @biomejs/biome lint ./src --skip=lint/style/noParameterAssign --skip=lint/suspicious/noExplicitAny --skip=lint/suspicious/noConfusingVoidType",
|
|
23
|
+
"test": "bun test && check-dts ./src/**",
|
|
24
24
|
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
"server.js"
|
|
30
30
|
],
|
|
31
31
|
"type": "module",
|
|
32
|
-
"types": "index.d.ts",
|
|
33
32
|
"main": "src/index.js",
|
|
33
|
+
"types": "src/index.d.ts",
|
|
34
34
|
"files": [
|
|
35
35
|
"src/",
|
|
36
|
-
"index.d.ts",
|
|
37
36
|
"jsx-dev-runtime.js"
|
|
38
37
|
],
|
|
39
38
|
"devDependencies": {
|
|
40
39
|
"argon2": "^0.40.3",
|
|
40
|
+
"check-dts": "^0.8.2",
|
|
41
41
|
"jest": "^29.7.0",
|
|
42
42
|
"polystore": "^0.14.1"
|
|
43
43
|
},
|
package/src/auth/index.js
CHANGED
|
@@ -15,17 +15,41 @@ const middle = async (ctx) => {
|
|
|
15
15
|
if (ctx.options.auth.provider.includes("github")) {
|
|
16
16
|
if (!env.GITHUB_ID) throw new Error("GITHUB_ID not defined");
|
|
17
17
|
if (!env.GITHUB_SECRET) throw new Error("GITHUB_SECRET not defined");
|
|
18
|
-
ctx.app.get(
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
ctx.app.get(
|
|
19
|
+
"/auth/logout",
|
|
20
|
+
{ tags: "Auth", title: "Github logout" },
|
|
21
|
+
logout,
|
|
22
|
+
);
|
|
23
|
+
ctx.app.get(
|
|
24
|
+
"/auth/login/github",
|
|
25
|
+
{ tags: "Auth" },
|
|
26
|
+
providers.github.login,
|
|
27
|
+
);
|
|
28
|
+
ctx.app.get(
|
|
29
|
+
"/auth/callback/github",
|
|
30
|
+
{ tags: "Auth", title: "Github callback" },
|
|
31
|
+
providers.github.callback,
|
|
32
|
+
);
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
if (ctx.options.auth.provider.includes("email")) {
|
|
24
|
-
ctx.app.post("/auth/logout", logout);
|
|
25
|
-
ctx.app.post(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
ctx.app.post("/auth/logout", { tags: "Auth" }, logout);
|
|
37
|
+
ctx.app.post(
|
|
38
|
+
"/auth/register/email",
|
|
39
|
+
{ tags: "Auth" },
|
|
40
|
+
providers.email.register,
|
|
41
|
+
);
|
|
42
|
+
ctx.app.post(
|
|
43
|
+
"/auth/login/email",
|
|
44
|
+
{ tags: "Auth" },
|
|
45
|
+
providers.email.login,
|
|
46
|
+
);
|
|
47
|
+
ctx.app.put(
|
|
48
|
+
"/auth/password/email",
|
|
49
|
+
{ tags: "Auth" },
|
|
50
|
+
providers.email.password,
|
|
51
|
+
);
|
|
52
|
+
ctx.app.put("/auth/reset/email", { tags: "Auth" }, providers.email.reset);
|
|
29
53
|
}
|
|
30
54
|
}
|
|
31
55
|
};
|
|
@@ -61,7 +61,7 @@ const createSession = async (user, ctx) => {
|
|
|
61
61
|
throw new Error("Unknown auth type");
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
async function
|
|
64
|
+
async function emailLogin(ctx) {
|
|
65
65
|
const { email, password } = ctx.body;
|
|
66
66
|
if (!email) throw ServerError.LOGIN_NO_EMAIL();
|
|
67
67
|
if (!/@/.test(email)) throw ServerError.LOGIN_INVALID_EMAIL();
|
|
@@ -78,7 +78,7 @@ async function login(ctx) {
|
|
|
78
78
|
return createSession(user, ctx);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async function
|
|
81
|
+
async function emailRegister(ctx) {
|
|
82
82
|
const { email, password, ...data } = ctx.body;
|
|
83
83
|
if (!email) throw ServerError.REGISTER_NO_EMAIL();
|
|
84
84
|
if (!/@/.test(email)) throw ServerError.REGISTER_INVALID_EMAIL();
|
|
@@ -101,7 +101,7 @@ async function register(ctx) {
|
|
|
101
101
|
return createSession(user, ctx);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
async function
|
|
104
|
+
async function emailResetPassword(ctx) {
|
|
105
105
|
// const reset = ctx.options.store.prefix("reset:");
|
|
106
106
|
// // Already resetting
|
|
107
107
|
// if (ctx.body.token) {
|
|
@@ -123,7 +123,7 @@ async function reset(ctx) {
|
|
|
123
123
|
// }
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async function
|
|
126
|
+
async function emailUpdatePassword(ctx) {
|
|
127
127
|
const { previous, updated } = ctx.body;
|
|
128
128
|
|
|
129
129
|
const fullUser = await ctx.options.auth.store.get(ctx.auth.user);
|
|
@@ -137,4 +137,9 @@ async function password(ctx) {
|
|
|
137
137
|
return 200;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
export default {
|
|
140
|
+
export default {
|
|
141
|
+
login: emailLogin,
|
|
142
|
+
register: emailRegister,
|
|
143
|
+
reset: emailResetPassword,
|
|
144
|
+
password: emailUpdatePassword,
|
|
145
|
+
};
|
package/src/helpers/config.js
CHANGED
package/src/helpers/createId.js
CHANGED
package/src/helpers/jsx.js
CHANGED
|
@@ -16,6 +16,7 @@ const altAttrs = {
|
|
|
16
16
|
// "" and 0 are valid children, false and null and undefined are not
|
|
17
17
|
const isValidChild = (child) => child || child === "" || child === 0;
|
|
18
18
|
|
|
19
|
+
const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
|
|
19
20
|
const minifyCss = (str) =>
|
|
20
21
|
str
|
|
21
22
|
.replace(/\s+/g, " ")
|
|
@@ -42,7 +43,7 @@ export const jsx = (tag, { children, ...props }) => {
|
|
|
42
43
|
children = () => src;
|
|
43
44
|
}
|
|
44
45
|
if (tag === "style" && children && typeof children === "string") {
|
|
45
|
-
const src = minifyCss(children);
|
|
46
|
+
const src = minifyCss(escapeCSS(children));
|
|
46
47
|
children = () => src;
|
|
47
48
|
}
|
|
48
49
|
if (props.dangerouslySetInnerHTML)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
type Bucket = {};
|
|
2
|
-
|
|
3
1
|
type Method =
|
|
4
2
|
| "socket"
|
|
5
3
|
| "get"
|
|
@@ -10,25 +8,85 @@ type Method =
|
|
|
10
8
|
| "delete"
|
|
11
9
|
| "options";
|
|
12
10
|
|
|
11
|
+
type Store = {
|
|
12
|
+
get: (key: string) => Promise<any>;
|
|
13
|
+
set: (
|
|
14
|
+
key: string,
|
|
15
|
+
value: any,
|
|
16
|
+
{ expires }?: { expires?: string | number },
|
|
17
|
+
) => Promise<any>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Bucket = any;
|
|
21
|
+
|
|
22
|
+
type Auth = {
|
|
23
|
+
type: "cookie" | "token";
|
|
24
|
+
provider: "github" | "email";
|
|
25
|
+
};
|
|
26
|
+
type AuthString = `${Auth["type"]}:${Auth["provider"]}`;
|
|
27
|
+
|
|
28
|
+
type Domain = `https://${string}/`;
|
|
29
|
+
type Origin = boolean | "*" | Domain | Domain[];
|
|
30
|
+
type Cors = {
|
|
31
|
+
origin: Origin;
|
|
32
|
+
methods: string;
|
|
33
|
+
headers: string;
|
|
34
|
+
credentials?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
13
37
|
type ServerOptions = {
|
|
14
38
|
port?: number;
|
|
15
39
|
views?: string | Bucket;
|
|
16
40
|
public?: string | Bucket;
|
|
17
41
|
uploads?: string | Bucket;
|
|
18
|
-
|
|
42
|
+
cors?: boolean | Origin | Cors;
|
|
43
|
+
auth?: AuthString | Auth;
|
|
44
|
+
store?: Store;
|
|
19
45
|
};
|
|
20
46
|
|
|
21
47
|
type ExtractPathParams<Path extends string> =
|
|
22
|
-
Path extends `${string}:${infer Param}
|
|
23
|
-
? Param | ExtractPathParams<`/${Rest}`>
|
|
24
|
-
: Path extends `${string}:${infer Param}
|
|
25
|
-
? Param
|
|
26
|
-
:
|
|
48
|
+
Path extends `${string}:${infer Param}(${infer Type})?/${infer Rest}`
|
|
49
|
+
? `${Param}:${Type}?` | ExtractPathParams<`/${Rest}`>
|
|
50
|
+
: Path extends `${string}:${infer Param}(${infer Type})?`
|
|
51
|
+
? `${Param}:${Type}?`
|
|
52
|
+
: Path extends `${string}:${infer Param}(${infer Type})/${infer Rest}`
|
|
53
|
+
? `${Param}:${Type}` | ExtractPathParams<`/${Rest}`>
|
|
54
|
+
: Path extends `${string}:${infer Param}(${infer Type})`
|
|
55
|
+
? `${Param}:${Type}`
|
|
56
|
+
: Path extends `${string}:${infer Param}?/${infer Rest}`
|
|
57
|
+
? `${Param}?` | ExtractPathParams<`/${Rest}`>
|
|
58
|
+
: Path extends `${string}:${infer Param}?`
|
|
59
|
+
? `${Param}?`
|
|
60
|
+
: Path extends `${string}:${infer Param}/${infer Rest}`
|
|
61
|
+
? Param | ExtractPathParams<`/${Rest}`>
|
|
62
|
+
: Path extends `${string}:${infer Param}`
|
|
63
|
+
? Param
|
|
64
|
+
: never;
|
|
65
|
+
|
|
66
|
+
type ParamTypeMap = {
|
|
67
|
+
string: string;
|
|
68
|
+
number: number;
|
|
69
|
+
boolean: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type InferParamType<T extends string> = T extends keyof ParamTypeMap
|
|
73
|
+
? ParamTypeMap[T]
|
|
74
|
+
: string;
|
|
27
75
|
|
|
28
76
|
type ParamsToObject<Params extends string> = {
|
|
29
|
-
[K in Params as K extends `${infer Key}?`
|
|
77
|
+
[K in Params as K extends `${infer Key}:${infer Type}?`
|
|
30
78
|
? Key
|
|
31
|
-
: K
|
|
79
|
+
: K extends `${infer Key}:${infer Type}`
|
|
80
|
+
? Key
|
|
81
|
+
: K extends `${infer Key}?`
|
|
82
|
+
? Key
|
|
83
|
+
: K]: K extends `${infer Key}:${infer Type}?`
|
|
84
|
+
? InferParamType<Type> | undefined
|
|
85
|
+
: K extends `${infer Key}:${infer Type}`
|
|
86
|
+
? InferParamType<Type>
|
|
87
|
+
: K extends `${infer Key}?`
|
|
88
|
+
? string | undefined
|
|
89
|
+
: string;
|
|
32
90
|
};
|
|
33
91
|
|
|
34
92
|
type PathToParams<Path extends string> = ParamsToObject<
|
|
@@ -44,7 +102,7 @@ type Context<Path extends string = string> = {
|
|
|
44
102
|
body?: any;
|
|
45
103
|
url: URL & {
|
|
46
104
|
params: Simplify<PathToParams<Path>>; // Simplify here
|
|
47
|
-
query: {};
|
|
105
|
+
query: { [key: string]: string };
|
|
48
106
|
};
|
|
49
107
|
options: ServerOptions;
|
|
50
108
|
};
|
|
@@ -55,18 +113,11 @@ type ContentType = "application/json" | "text/plain" | (string & {});
|
|
|
55
113
|
|
|
56
114
|
// (src: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
|
|
57
115
|
type Headers = {
|
|
58
|
-
"Cache-Control"?: string;
|
|
59
|
-
"Content-Type"?: ContentType;
|
|
60
|
-
Server?: string;
|
|
61
|
-
"Set-Cookie"?: string;
|
|
62
|
-
"Content-Length"?: string;
|
|
63
|
-
Location?: string;
|
|
64
|
-
|
|
65
116
|
"cache-control"?: string;
|
|
66
117
|
"content-type"?: ContentType;
|
|
67
|
-
server?: string;
|
|
68
118
|
"set-cookie"?: string;
|
|
69
119
|
"content-length"?: string;
|
|
120
|
+
server?: string;
|
|
70
121
|
location?: string;
|
|
71
122
|
|
|
72
123
|
[key: string]: string | undefined;
|
|
@@ -77,11 +128,11 @@ type InlineReply =
|
|
|
77
128
|
| { body: Body; headers?: Headers }
|
|
78
129
|
| string
|
|
79
130
|
| number
|
|
80
|
-
|
|
|
131
|
+
| undefined;
|
|
81
132
|
|
|
82
133
|
type Middleware<Path extends string = string> = (
|
|
83
134
|
ctx: Context<Path>,
|
|
84
|
-
) => InlineReply;
|
|
135
|
+
) => InlineReply | void;
|
|
85
136
|
|
|
86
137
|
declare interface Router {
|
|
87
138
|
get<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
@@ -94,6 +145,21 @@ declare interface Router {
|
|
|
94
145
|
}
|
|
95
146
|
|
|
96
147
|
declare interface Server {
|
|
148
|
+
/**
|
|
149
|
+
* Launch the server with the optional configuration:
|
|
150
|
+
*
|
|
151
|
+
* ```js
|
|
152
|
+
* export default server({
|
|
153
|
+
* port: 3000,
|
|
154
|
+
* public: './public',
|
|
155
|
+
* store: kv(new Map()),
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* **[→ Getting Started](https://react-test.dev/documentation#attr)**
|
|
160
|
+
*
|
|
161
|
+
* **[→ Options Docs](https://react-test.dev/documentation#attr)**
|
|
162
|
+
*/
|
|
97
163
|
(options?: ServerOptions): this;
|
|
98
164
|
|
|
99
165
|
socket(path: string, ...middle: Middleware[]): this;
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
parseHeaders,
|
|
12
12
|
} from "./helpers/index.js";
|
|
13
13
|
|
|
14
|
-
import { assets, auth, timer } from "./middle/index.js";
|
|
14
|
+
import { assets, auth, timer, openapi } from "./middle/index.js";
|
|
15
15
|
|
|
16
16
|
// Export the reply helpers
|
|
17
17
|
export * from "./reply.js";
|
|
@@ -21,7 +21,6 @@ export { default as ServerError } from "./ServerError.js";
|
|
|
21
21
|
// Allow to create a sub-router
|
|
22
22
|
export { default as router } from "./router.js";
|
|
23
23
|
|
|
24
|
-
// #region server()
|
|
25
24
|
export default function server(options = {}) {
|
|
26
25
|
// Make it so that the exported one is a prototype of function()
|
|
27
26
|
if (!(this instanceof server)) {
|
|
@@ -76,6 +75,10 @@ export default function server(options = {}) {
|
|
|
76
75
|
|
|
77
76
|
this.use(timer);
|
|
78
77
|
this.use(assets);
|
|
78
|
+
if (this.opts.openapi) {
|
|
79
|
+
const path = this.opts.openapi.path || "/docs";
|
|
80
|
+
this.get(path, openapi);
|
|
81
|
+
}
|
|
79
82
|
if (this.opts.auth) {
|
|
80
83
|
this.use(auth({ options: this.opts, app: this }));
|
|
81
84
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import server, { headers, router } from ".";
|
|
2
|
+
|
|
3
|
+
const users = router
|
|
4
|
+
.post("/users", (ctx) => {
|
|
5
|
+
console.log(ctx.url.pathname);
|
|
6
|
+
console.log(ctx.url.query);
|
|
7
|
+
console.log(ctx.url.params); // No .id
|
|
8
|
+
})
|
|
9
|
+
.get("/users/:id?", (ctx) => {
|
|
10
|
+
console.log(ctx.url.params.id);
|
|
11
|
+
})
|
|
12
|
+
.del("/users/:id", (ctx) => {
|
|
13
|
+
console.log(ctx.url.params.id);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
server()
|
|
17
|
+
.get("/", (ctx) => {
|
|
18
|
+
console.log(ctx.method);
|
|
19
|
+
console.log(ctx.url);
|
|
20
|
+
console.log(ctx.headers);
|
|
21
|
+
console.log(ctx.cookies);
|
|
22
|
+
console.log(ctx.body);
|
|
23
|
+
return headers();
|
|
24
|
+
})
|
|
25
|
+
.router(users)
|
|
26
|
+
.post("/", () => {
|
|
27
|
+
return 201;
|
|
28
|
+
});
|
package/src/middle/index.js
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const entities = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
4
|
+
const encode = (str = "") => {
|
|
5
|
+
if (typeof str === "number") str = String(str);
|
|
6
|
+
if (typeof str !== "string") return ""; // nullify not-strings
|
|
7
|
+
return str.replace(/[&<>"]/g, (tag) => entities[tag]);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const getConfig = (routes) => {
|
|
11
|
+
const config = routes.find(
|
|
12
|
+
(r) =>
|
|
13
|
+
typeof r !== "string" && typeof r !== "function" && typeof r === "object",
|
|
14
|
+
);
|
|
15
|
+
if (!config) return {};
|
|
16
|
+
if (config.tags) {
|
|
17
|
+
if (typeof config.tags === "string") {
|
|
18
|
+
config.tags = config.tags.split(/\s*\,\s*/g);
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(config.tags)) {
|
|
21
|
+
throw new Error("invalid tags", config.tags);
|
|
22
|
+
}
|
|
23
|
+
config.tags = config.tags.map((t) => t.trim());
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const pkg = await fsp
|
|
29
|
+
.readFile("package.json", "utf-8")
|
|
30
|
+
.then((data) => JSON.parse(data))
|
|
31
|
+
.catch(() => ({}));
|
|
32
|
+
|
|
33
|
+
const getTag = (name, fn) => {
|
|
34
|
+
const found = fn
|
|
35
|
+
.toString()
|
|
36
|
+
.split("\n")
|
|
37
|
+
.filter((l) => /\s+\/\/\s/.test(l))
|
|
38
|
+
.map((l) => l.trim().replace("// ", ""))
|
|
39
|
+
.find((l) => l.startsWith(name));
|
|
40
|
+
if (!found) return "";
|
|
41
|
+
return encode(found.replace(name, "").trim());
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getDescription = (fn) => getTag("@description", fn) || "";
|
|
45
|
+
const getReturn = (fn) => getTag("@returns", fn) || "200 OK";
|
|
46
|
+
|
|
47
|
+
const generateOpenApiPaths = (handlers) => {
|
|
48
|
+
const paths = {};
|
|
49
|
+
|
|
50
|
+
for (const [method, routes] of Object.entries(handlers)) {
|
|
51
|
+
for (const route of routes) {
|
|
52
|
+
const [_, path, fn, meta] = [
|
|
53
|
+
route[0],
|
|
54
|
+
route[1],
|
|
55
|
+
route.find((p) => typeof p === "function"),
|
|
56
|
+
route.find((p) => typeof p === "object"),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const config = getConfig(route);
|
|
60
|
+
|
|
61
|
+
if (typeof path !== "string" || path === "*" || !fn) continue;
|
|
62
|
+
|
|
63
|
+
// Normalize path (convert ":id" to "{id}" for OpenAPI)
|
|
64
|
+
const normalizedPath = path.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
65
|
+
|
|
66
|
+
if (!paths[normalizedPath]) {
|
|
67
|
+
paths[normalizedPath] = {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const getTitle = (fn) => {
|
|
71
|
+
if (!fn.name) return null;
|
|
72
|
+
// Well, we shouldn't really rely on these, e.g. automatic names from export default
|
|
73
|
+
const wrongNames = ["default"];
|
|
74
|
+
if (wrongNames.includes(fn.name)) return null;
|
|
75
|
+
if (fn.name.length <= 3) return null;
|
|
76
|
+
if (fn.name.includes("_")) return fn.name.replaceAll("_", " ");
|
|
77
|
+
const name = fn.name
|
|
78
|
+
.split(/(?=[A-Z])/)
|
|
79
|
+
.join(" ")
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
return name[0].toUpperCase() + name.slice(1);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
paths[normalizedPath][method] = {
|
|
85
|
+
tags: config.tags,
|
|
86
|
+
summary:
|
|
87
|
+
config.title ||
|
|
88
|
+
getTitle(fn) ||
|
|
89
|
+
getTag("@title", fn) ||
|
|
90
|
+
`${method.toUpperCase()} ${path}`,
|
|
91
|
+
description: getDescription(fn),
|
|
92
|
+
responses: {
|
|
93
|
+
200: {
|
|
94
|
+
description: getReturn(fn),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
...(meta
|
|
98
|
+
? {
|
|
99
|
+
parameters: Object.entries(meta).map(([key, value]) => ({
|
|
100
|
+
name: key,
|
|
101
|
+
in: "query",
|
|
102
|
+
required: false,
|
|
103
|
+
schema: { type: typeof value },
|
|
104
|
+
example: value,
|
|
105
|
+
})),
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return paths;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default async (ctx) => {
|
|
116
|
+
const domain = pkg.homepage || ctx.url.origin;
|
|
117
|
+
const openApi = {
|
|
118
|
+
openapi: "3.0.0",
|
|
119
|
+
info: {
|
|
120
|
+
title: pkg.name || "API Documentation",
|
|
121
|
+
version: pkg.version || "1.0.0",
|
|
122
|
+
description: pkg.description || "",
|
|
123
|
+
},
|
|
124
|
+
servers: domain ? [{ url: domain }] : [],
|
|
125
|
+
paths: generateOpenApiPaths(ctx.app.handlers),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const configuration = ctx.options.openapi.scalar || {};
|
|
129
|
+
|
|
130
|
+
return `
|
|
131
|
+
<!doctype html>
|
|
132
|
+
<html>
|
|
133
|
+
<head>
|
|
134
|
+
<title>API Reference</title>
|
|
135
|
+
<meta charset="utf-8" />
|
|
136
|
+
<meta
|
|
137
|
+
name="viewport"
|
|
138
|
+
content="width=device-width, initial-scale=1" />
|
|
139
|
+
<style>.open-api-client-button {display: none!important;}</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
<script id="api-reference" type="application/json" data-configuration="${encode(JSON.stringify(configuration))}">${JSON.stringify(openApi, null, 2)}</script>
|
|
143
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
144
|
+
</body>
|
|
145
|
+
</html> `;
|
|
146
|
+
};
|
package/src/parseResponse.js
CHANGED
package/src/pathPattern.js
CHANGED
|
@@ -15,11 +15,24 @@ export default function pathPattern(pattern, path) {
|
|
|
15
15
|
const patt = pattParts[i] || "";
|
|
16
16
|
const part = pathParts[i] || "";
|
|
17
17
|
const last = pattParts[pattParts.length - 1];
|
|
18
|
-
const key = patt
|
|
18
|
+
const key = patt
|
|
19
|
+
.replace(/^:/, "")
|
|
20
|
+
.replace(/\?$/, "")
|
|
21
|
+
.replace(/\(\w*\)/, "");
|
|
19
22
|
if (patt === part) continue;
|
|
20
23
|
if (patt.endsWith("?") && !part) continue;
|
|
21
24
|
if (patt.startsWith(":")) {
|
|
22
25
|
params[key] = part;
|
|
26
|
+
if (/\(\w*\)/.test(patt)) {
|
|
27
|
+
if (patt.includes("(number)")) {
|
|
28
|
+
const value = Number(part);
|
|
29
|
+
params[key] = Number.isNaN(value) ? undefined : value;
|
|
30
|
+
}
|
|
31
|
+
if (patt.includes("(date)")) {
|
|
32
|
+
const value = new Date(part);
|
|
33
|
+
params[key] = Number.isNaN(value) ? undefined : value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
continue;
|
|
24
37
|
}
|
|
25
38
|
if ((!patt && last === "*" && part) || (patt === "*" && part)) {
|
package/src/pathPattern.test.js
CHANGED
|
@@ -25,20 +25,45 @@ describe("pathPattern.js", () => {
|
|
|
25
25
|
expect(pathPattern("/hello/", "/hello/John")).toEqual(null);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it("can capture simple
|
|
28
|
+
it("can capture simple params", () => {
|
|
29
29
|
expect(pathPattern("/:hello", "/john")).toEqual({ hello: "john" });
|
|
30
30
|
expect(pathPattern("/hello/:there", "/hello/john")).toEqual({
|
|
31
31
|
there: "john",
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it("will parse the params as numbers", () => {
|
|
36
|
+
expect(pathPattern("/:id(number)", "/25")).toEqual({ id: 25 });
|
|
37
|
+
expect(pathPattern("/:id(number)", "/25.5")).toEqual({ id: 25.5 });
|
|
38
|
+
expect(pathPattern("/users/:id(number)", "/users/25")).toEqual({
|
|
39
|
+
id: 25,
|
|
40
|
+
});
|
|
41
|
+
expect(pathPattern("/:id(date)", "/2015-01-01")).toEqual({
|
|
42
|
+
id: new Date("2015-01-01"),
|
|
43
|
+
});
|
|
44
|
+
expect(pathPattern("/report/:id(date)", "/report/2015-01-01")).toEqual({
|
|
45
|
+
id: new Date("2015-01-01"),
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("will still match, but not parse it if it's an invalid number", () => {
|
|
50
|
+
expect(pathPattern("/:id(number)", "/hi")).toEqual({});
|
|
51
|
+
expect(pathPattern("/users/:id(number)", "/users/hi")).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
35
54
|
it("requires a part for the asterisk", () => {
|
|
36
55
|
expect(pathPattern("/hello/:there/*", "/hello/John")).toEqual(null);
|
|
37
56
|
});
|
|
38
57
|
|
|
39
58
|
it("can make a part optional", () => {
|
|
40
|
-
|
|
59
|
+
expect(pathPattern("/:name?", "/")).toEqual({});
|
|
41
60
|
expect(pathPattern("/hello/:name?", "/hello/")).toEqual({});
|
|
61
|
+
expect(pathPattern("/:id(number)?", "/")).toEqual({});
|
|
62
|
+
expect(pathPattern("/:id(date)?", "/")).toEqual({});
|
|
63
|
+
expect(pathPattern("/:id(number)?", "/25")).toEqual({ id: 25 });
|
|
64
|
+
expect(pathPattern("/:id(date)?", "/2015-01-01")).toEqual({
|
|
65
|
+
id: new Date("2015-01-01"),
|
|
66
|
+
});
|
|
42
67
|
expect(pathPattern("/:name?", "/john")).toEqual({ name: "john" });
|
|
43
68
|
expect(pathPattern("/:name/*?", "/john")).toEqual({ name: "john" });
|
|
44
69
|
});
|