@senrijs/senri 0.0.0-stage → 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -2
  3. package/dist/adapters/node/index.d.ts +14 -0
  4. package/dist/adapters/node/index.js +152 -0
  5. package/dist/adapters/serve/index.d.ts +28 -0
  6. package/dist/adapters/serve/index.js +137 -0
  7. package/dist/adapters/static/index.d.ts +9 -0
  8. package/dist/adapters/static/index.js +120 -0
  9. package/dist/src/app.d.ts +49 -0
  10. package/dist/src/app.js +375 -0
  11. package/dist/src/body.d.ts +3 -0
  12. package/dist/src/body.js +45 -0
  13. package/dist/src/compose.d.ts +22 -0
  14. package/dist/src/compose.js +95 -0
  15. package/dist/src/connection.d.ts +3 -0
  16. package/dist/src/connection.js +49 -0
  17. package/dist/src/context.d.ts +60 -0
  18. package/dist/src/context.js +254 -0
  19. package/dist/src/cookie.d.ts +13 -0
  20. package/dist/src/cookie.js +61 -0
  21. package/dist/src/dev/dashboard/access.d.ts +6 -0
  22. package/dist/src/dev/dashboard/access.js +46 -0
  23. package/dist/src/dev/dashboard/index.d.ts +27 -0
  24. package/dist/src/dev/dashboard/index.js +51 -0
  25. package/dist/src/dev/dashboard/links.d.ts +3 -0
  26. package/dist/src/dev/dashboard/links.js +8 -0
  27. package/dist/src/dev/dashboard/log.d.ts +20 -0
  28. package/dist/src/dev/dashboard/log.js +53 -0
  29. package/dist/src/dev/dashboard/mask.d.ts +4 -0
  30. package/dist/src/dev/dashboard/mask.js +43 -0
  31. package/dist/src/dev/dashboard/record.d.ts +37 -0
  32. package/dist/src/dev/dashboard/record.js +50 -0
  33. package/dist/src/dev/dashboard/routes.d.ts +7 -0
  34. package/dist/src/dev/dashboard/routes.js +121 -0
  35. package/dist/src/dev/dashboard/sse.d.ts +7 -0
  36. package/dist/src/dev/dashboard/sse.js +73 -0
  37. package/dist/src/dev/editor-url.d.ts +14 -0
  38. package/dist/src/dev/editor-url.js +61 -0
  39. package/dist/src/dev/editor.d.ts +6 -0
  40. package/dist/src/dev/editor.js +14 -0
  41. package/dist/src/dev/error-page.d.ts +3 -0
  42. package/dist/src/dev/error-page.js +69 -0
  43. package/dist/src/dev/fallback.d.ts +3 -0
  44. package/dist/src/dev/fallback.js +40 -0
  45. package/dist/src/dev/hints.d.ts +6 -0
  46. package/dist/src/dev/hints.js +82 -0
  47. package/dist/src/dev/html.d.ts +1 -0
  48. package/dist/src/dev/html.js +14 -0
  49. package/dist/src/dev/index.d.ts +6 -0
  50. package/dist/src/dev/index.js +11 -0
  51. package/dist/src/dev/messages.d.ts +2 -0
  52. package/dist/src/dev/messages.js +6 -0
  53. package/dist/src/dev/not-found-page.d.ts +7 -0
  54. package/dist/src/dev/not-found-page.js +33 -0
  55. package/dist/src/dev/page-data.d.ts +95 -0
  56. package/dist/src/dev/page-data.js +0 -0
  57. package/dist/src/dev/page.generated.d.ts +1 -0
  58. package/dist/src/dev/page.generated.js +3 -0
  59. package/dist/src/dev/pages.d.ts +4 -0
  60. package/dist/src/dev/pages.js +59 -0
  61. package/dist/src/dev/render.d.ts +5 -0
  62. package/dist/src/dev/render.js +42 -0
  63. package/dist/src/dev/runtime.d.ts +3 -0
  64. package/dist/src/dev/runtime.js +45 -0
  65. package/dist/src/dev/server-timing.d.ts +6 -0
  66. package/dist/src/dev/server-timing.js +45 -0
  67. package/dist/src/dev/source.d.ts +20 -0
  68. package/dist/src/dev/source.js +106 -0
  69. package/dist/src/dev/stack.d.ts +12 -0
  70. package/dist/src/dev/stack.js +93 -0
  71. package/dist/src/dev/suggest.d.ts +12 -0
  72. package/dist/src/dev/suggest.js +95 -0
  73. package/dist/src/errors.d.ts +23 -0
  74. package/dist/src/errors.js +136 -0
  75. package/dist/src/group.d.ts +38 -0
  76. package/dist/src/group.js +83 -0
  77. package/dist/src/health.d.ts +9 -0
  78. package/dist/src/health.js +40 -0
  79. package/dist/src/index.d.ts +13 -0
  80. package/dist/src/index.js +10 -0
  81. package/dist/src/logger.d.ts +18 -0
  82. package/dist/src/logger.js +110 -0
  83. package/dist/src/matcher.d.ts +16 -0
  84. package/dist/src/matcher.js +86 -0
  85. package/dist/src/middleware/body-limit.d.ts +6 -0
  86. package/dist/src/middleware/body-limit.js +47 -0
  87. package/dist/src/middleware/cors.d.ts +10 -0
  88. package/dist/src/middleware/cors.js +79 -0
  89. package/dist/src/middleware/response.d.ts +2 -0
  90. package/dist/src/middleware/response.js +28 -0
  91. package/dist/src/middleware/secure-headers.d.ts +19 -0
  92. package/dist/src/middleware/secure-headers.js +46 -0
  93. package/dist/src/router.d.ts +30 -0
  94. package/dist/src/router.js +177 -0
  95. package/dist/src/runtime.d.ts +4 -0
  96. package/dist/src/runtime.js +49 -0
  97. package/dist/src/validate.d.ts +47 -0
  98. package/dist/src/validate.js +87 -0
  99. package/package.json +74 -4
@@ -0,0 +1,30 @@
1
+ import type { Handler } from "./context.ts";
2
+ export type Route = {
3
+ path: string;
4
+ segments: string[];
5
+ paramNames: string[];
6
+ method: string;
7
+ handler: Handler;
8
+ };
9
+ type Segment<Path extends string> = Path extends `${infer Head}/${infer Rest}` ? Head | Segment<Rest> : Path;
10
+ type ParamName<S extends string> = S extends `:${infer Name}` ? Name : never;
11
+ export type ExtractParams<Path extends string> = string extends Path ? Record<string, string> : {
12
+ [K in ParamName<Segment<Path>>]: string;
13
+ };
14
+ export type Simplify<T> = {
15
+ [K in keyof T]: T[K];
16
+ } & {};
17
+ export type RouteTable<K extends string, V extends object = any> = {
18
+ [Key in K]: Key extends `${string} ${infer Path}` ? Handler<Simplify<ExtractParams<Path>>, V> : 'ルートの書き方が違います。"GET /hello" のように、メソッドとパスの間にスペースを入れてください';
19
+ };
20
+ export declare function splitPattern(pattern: string): string[];
21
+ export declare function splitPath(pathname: string): string[];
22
+ export declare function requestPathname(req: Request): string;
23
+ export declare function matchPath(pattern: readonly string[], path: readonly string[]): Record<string, string> | null;
24
+ export declare function routeShape(segments: readonly string[]): string;
25
+ export declare function checkParamNames(path: string, segments: readonly string[]): void;
26
+ export declare function normalizeMethod(method: string): string;
27
+ export declare function allowedMethods(routes: readonly Route[]): string;
28
+ export declare function startsWithSegments(path: readonly string[], prefix: readonly string[]): boolean;
29
+ export declare function compareRoutes(a: Route, b: Route): number;
30
+ export {};
@@ -0,0 +1,177 @@
1
+ import { HttpError } from "./errors.js";
2
+ // ルートのパターン("/users/:id")を分割する。末尾スラッシュや連続スラッシュは無視
3
+ export function splitPattern(pattern) {
4
+ return pattern.split("/").filter(Boolean);
5
+ }
6
+ // リクエストのパス(URL の pathname)を分割して、セグメントごとにデコードする
7
+ // - 末尾スラッシュや連続スラッシュは無視する("/a//b/" は "/a/b" と同じ)
8
+ // - "/" ごとに分割してからデコードするので、%2F が区切りと混ざることはないが、
9
+ // 紛らわしいので %2F(デコードすると "/" を含む)は 400 にする
10
+ // - デコードすると "." や ".." になるものは、パストラバーサルの元なので 400 にする
11
+ // (ふつうは "%2E%2E" も含めて URL の仕様で解決済みなので、ここは念のための防御)
12
+ export function splitPath(pathname) {
13
+ const segments = [];
14
+ for (const raw of pathname.split("/")) {
15
+ if (raw === "")
16
+ continue;
17
+ // "%" がなければデコードしても同じなので、しない(ほとんどのリクエストはこちら)
18
+ // デコードしなければ "/" は含まれない(split で分けたので)
19
+ let segment = raw;
20
+ if (raw.includes("%")) {
21
+ try {
22
+ segment = decodeURIComponent(raw);
23
+ }
24
+ catch {
25
+ throw new HttpError(400, "400 Bad Request");
26
+ }
27
+ if (segment.includes("/"))
28
+ throw new HttpError(400, "400 Bad Request");
29
+ }
30
+ if (segment === "." || segment === "..") {
31
+ throw new HttpError(400, "400 Bad Request");
32
+ }
33
+ segments.push(segment);
34
+ }
35
+ return segments;
36
+ }
37
+ // URL の区切り文字や空白を含む Host は、ふつうのクライアントは送らない
38
+ const BAD_HOST = /[\s/?#@\\]/;
39
+ // リクエストの pathname を取り出す。おかしなリクエストは HttpError(400) にする
40
+ // - ランタイムが Host ヘッダーをつないで URL を作ることがある(Deno)。
41
+ // "Host: x/admin#" だと http://x/admin#/public になり、パスが /admin にすり替わるので弾く
42
+ // - Bun は Host が不正だと req.url を "/public" のような相対 URL にするので、new URL() が失敗する
43
+ // - 速さのため、ふつうは new URL() を使わず req.url の文字列から切り出す。
44
+ // ただし Deno は req.url を正規化しない("/a/../b" のまま来る)ので、"." のセグメント・%2e・\ を含むときだけ
45
+ // new URL() に任せて、URL の仕様どおりに解決させる(どちらで取り出しても、同じ結果になるようにしている)
46
+ export function requestPathname(req) {
47
+ const host = req.headers.get("host");
48
+ if (host !== null && BAD_HOST.test(host)) {
49
+ throw new HttpError(400, "400 Bad Request");
50
+ }
51
+ const url = req.url;
52
+ const scheme = url.indexOf("://");
53
+ if (scheme !== -1) {
54
+ const start = url.indexOf("/", scheme + 3);
55
+ if (start === -1)
56
+ return "/";
57
+ // "?" か "#" までがパス。1回の走査で、new URL() に任せるべきもの("/." と "%2e" と "\")も探す
58
+ let needsUrl = false;
59
+ let i = start;
60
+ for (; i < url.length; i++) {
61
+ const ch = url.charCodeAt(i);
62
+ if (ch === 63 || ch === 35)
63
+ break; // "?" "#"
64
+ if (ch === 92)
65
+ needsUrl = true; // "\"
66
+ else if (ch === 46 && url.charCodeAt(i - 1) === 47)
67
+ needsUrl = true; // "/."
68
+ else if (ch === 37 && url.charCodeAt(i + 1) === 50 && (url.charCodeAt(i + 2) | 32) === 101)
69
+ needsUrl = true; // "%2e" "%2E"
70
+ }
71
+ if (!needsUrl)
72
+ return url.slice(start, i);
73
+ }
74
+ try {
75
+ return new URL(url).pathname;
76
+ }
77
+ catch {
78
+ throw new HttpError(400, "400 Bad Request");
79
+ }
80
+ }
81
+ export function matchPath(pattern, path) {
82
+ // 個数が違ったら一致しない
83
+ if (pattern.length !== path.length) {
84
+ return null;
85
+ }
86
+ // 1個ずつ比較する
87
+ const params = {};
88
+ for (let i = 0; i < pattern.length; i++) {
89
+ const p = pattern[i];
90
+ const actual = path[i];
91
+ if (p.startsWith(":")) {
92
+ params[p.slice(1)] = actual;
93
+ }
94
+ else if (p !== actual) {
95
+ return null;
96
+ }
97
+ }
98
+ return params;
99
+ }
100
+ // パターンの「形」。パラメーター名は区別しない("/users/:id" と "/users/:name" は同じ "users/:")
101
+ export function routeShape(segments) {
102
+ return segments.map((s) => (s.startsWith(":") ? ":" : s)).join("/");
103
+ }
104
+ // パラメーター名が空だったり、1つのパターンで2回使われていたりしたらエラー
105
+ // (2回目の値で1回目が黙って上書きされてしまうので)
106
+ export function checkParamNames(path, segments) {
107
+ const seen = new Set();
108
+ for (const s of segments) {
109
+ if (!s.startsWith(":"))
110
+ continue;
111
+ const name = s.slice(1);
112
+ if (name === "") {
113
+ throw new Error(`パラメーター名が空です: "${path}"`);
114
+ }
115
+ if (seen.has(name)) {
116
+ throw new Error(`パラメーター名 "${name}" が2回使われています: "${path}"`);
117
+ }
118
+ seen.add(name);
119
+ }
120
+ }
121
+ // メソッド名に使える文字(RFC 9110 の token)
122
+ const METHOD = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
123
+ // 登録するメソッド名を確かめて、大文字にする("propfind" → "PROPFIND")
124
+ // - CONNECT はプロキシ用、TRACE / TRACK はリクエストをそのまま返すので Cookie を盗む攻撃(XST)に使われる。どれも扱わない
125
+ // - "ALL" は app.all() の印として使っているので、app.on() からは登録させない
126
+ export function normalizeMethod(method) {
127
+ if (!METHOD.test(method))
128
+ throw new Error(`メソッド名に使えない文字があります: ${JSON.stringify(method)}`);
129
+ const upper = method.toUpperCase();
130
+ if (upper === "CONNECT" || upper === "TRACE" || upper === "TRACK") {
131
+ throw new Error(`${upper} メソッドは扱いません(Senri では CONNECT / TRACE / TRACK を登録できません)`);
132
+ }
133
+ return upper;
134
+ }
135
+ // Allow ヘッダーに入れるメソッドの一覧("GET, HEAD, POST, OPTIONS")
136
+ // GET があれば HEAD も使える(GET にフォールバックするので)。OPTIONS はいつでも使える
137
+ export function allowedMethods(routes) {
138
+ const methods = [];
139
+ for (const r of routes) {
140
+ if (!methods.includes(r.method))
141
+ methods.push(r.method);
142
+ if (r.method === "GET" && !methods.includes("HEAD"))
143
+ methods.push("HEAD");
144
+ }
145
+ if (!methods.includes("OPTIONS"))
146
+ methods.push("OPTIONS");
147
+ return methods.join(", ");
148
+ }
149
+ // path の先頭が prefix と一致するか。セグメント単位で比べるので "/admin" は "/administrator" にはマッチしない
150
+ // プレフィックスの ":id" のようなパラメーターは、どんなセグメントにも合う(matchPath と同じ判定)
151
+ export function startsWithSegments(path, prefix) {
152
+ if (path.length < prefix.length)
153
+ return false;
154
+ return prefix.every((p, i) => p.startsWith(":") || p === path[i]);
155
+ }
156
+ // sort 用の比較関数。どの2つを比べても答えが矛盾しない(=並び順が一意に決まる)ようにする
157
+ export function compareRoutes(a, b) {
158
+ const aParts = a.segments;
159
+ const bParts = b.segments;
160
+ // まずセグメント数で分ける。数が違うルート同士はそもそも同じパスにマッチしないので、
161
+ // ここで順番を決めてしまっても優先順位には影響しない
162
+ // (以前はここで 0 = 「同じ」と答えていたため、sort の結果が登録順に左右されていた)
163
+ if (aParts.length !== bParts.length) {
164
+ return aParts.length - bParts.length;
165
+ }
166
+ for (let i = 0; i < aParts.length; i++) {
167
+ const aIsParam = aParts[i].startsWith(":");
168
+ const bIsParam = bParts[i].startsWith(":");
169
+ // aがパラメータでbがパラメータでない場合、bを優先する
170
+ if (aIsParam && !bIsParam)
171
+ return 1;
172
+ // aがパラメータでなくbがパラメータの場合、aを優先する
173
+ if (!aIsParam && bIsParam)
174
+ return -1;
175
+ }
176
+ return 0;
177
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isWorkers(): boolean;
2
+ export declare function cwd(): string | null;
3
+ export declare function env(name: string): string | undefined;
4
+ export declare function canRead(path: string): boolean;
@@ -0,0 +1,49 @@
1
+ // ランタイムごとに違う処理(カレントディレクトリ、環境変数、ファイルの読み込み権限)。開発用ページと serve() で使う
2
+ //
3
+ // Deno は、許可されていない環境変数やファイルを読もうとすると、ターミナルで「許可しますか?」と聞いてきて
4
+ // その間リクエストが止まってしまう。なので先に permissions.querySync() で「許可済みか」だけを確かめ、
5
+ // 許可されていなければ読まずにあきらめる(聞き返しはしない)
6
+ const g = globalThis;
7
+ // Cloudflare Workers(workerd)で動いているか。Workers の navigator.userAgent は決まった値になる
8
+ export function isWorkers() {
9
+ return g.navigator?.userAgent === "Cloudflare-Workers";
10
+ }
11
+ function denoAllows(desc) {
12
+ try {
13
+ return g.Deno.permissions.querySync(desc).state === "granted";
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ // スラッシュ区切りのカレントディレクトリ。分からなければ null
20
+ export function cwd() {
21
+ try {
22
+ // Deno 2 には process もあるが、権限の確認をしたいので Deno を先に見る
23
+ // 全体({ name: "read" })で聞くと、--allow-read=. のように範囲を絞ったときに "prompt" になってしまう。
24
+ // カレントディレクトリが読めればよいので、"." で聞く
25
+ if (g.Deno)
26
+ return denoAllows({ name: "read", path: "." }) ? g.Deno.cwd().replaceAll("\\", "/") : null;
27
+ if (g.process)
28
+ return g.process.cwd().replaceAll("\\", "/");
29
+ }
30
+ catch {
31
+ // 読めなければあきらめる
32
+ }
33
+ return null;
34
+ }
35
+ export function env(name) {
36
+ try {
37
+ if (g.Deno)
38
+ return denoAllows({ name: "env", variable: name }) ? g.Deno.env.get(name) : undefined;
39
+ return g.process?.env[name];
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ export function canRead(path) {
46
+ if (g.Deno)
47
+ return denoAllows({ name: "read", path });
48
+ return true;
49
+ }
@@ -0,0 +1,47 @@
1
+ import { type Handler } from "./context.ts";
2
+ import { HttpError } from "./errors.ts";
3
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
4
+ readonly "~standard": {
5
+ readonly version: 1;
6
+ readonly vendor: string;
7
+ readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
8
+ readonly types?: {
9
+ readonly input: Input;
10
+ readonly output: Output;
11
+ } | undefined;
12
+ };
13
+ }
14
+ type StandardResult<Output> = {
15
+ readonly value: Output;
16
+ readonly issues?: undefined;
17
+ } | {
18
+ readonly issues: readonly StandardIssue[];
19
+ };
20
+ export type StandardIssue = {
21
+ readonly message: string;
22
+ readonly path?: readonly (PropertyKey | {
23
+ readonly key: PropertyKey;
24
+ })[] | undefined;
25
+ };
26
+ export type InferOutput<S> = S extends StandardSchemaV1<any, infer Output> ? Output : never;
27
+ export type RouteSchemas = {
28
+ params?: StandardSchemaV1;
29
+ query?: StandardSchemaV1;
30
+ body?: StandardSchemaV1;
31
+ };
32
+ export type Validated<S extends RouteSchemas> = {
33
+ [K in keyof S & ("query" | "body")]: InferOutput<S[K]>;
34
+ };
35
+ export type ValidatedParams<S extends RouteSchemas, Fallback> = S extends {
36
+ params: StandardSchemaV1;
37
+ } ? InferOutput<S["params"]> : Fallback;
38
+ export type ValidationTarget = "params" | "query" | "body";
39
+ export declare class ValidationError extends HttpError {
40
+ readonly target: ValidationTarget;
41
+ readonly issues: readonly StandardIssue[];
42
+ constructor(target: ValidationTarget, issues: readonly StandardIssue[]);
43
+ describe(): string;
44
+ }
45
+ export declare function checkSchemas(path: string, schemas: RouteSchemas): void;
46
+ export declare function withValidation(schemas: RouteSchemas, handler: Handler): Handler;
47
+ export {};
@@ -0,0 +1,87 @@
1
+ // Standard Schema(https://standardschema.dev)によるバリデーション
2
+ // Zod・Valibot・ArkType など、Standard Schema に対応したライブラリのスキーマをそのまま使える(Senri はどれにも依存しない)
3
+ //
4
+ // app.post("/users/:id", { params, query, body }, handler) のように、ルートにスキーマを渡す
5
+ // - params: パスパラメーター。通ればハンドラーの第1引数(と c.params)が変換後の値になる
6
+ // - query: c.query() の結果。通れば c.valid("query") で読める
7
+ // - body: c.body() の結果(Content-Type に合わせて読んだもの)。通れば c.valid("body") で読める
8
+ // どれかが通らなければ、ハンドラーは呼ばずに 400 にする(ValidationError)
9
+ import { setValidated } from "./context.js";
10
+ import { HttpError } from "./errors.js";
11
+ // バリデーションに通らなかった。issues はスキーマのライブラリが返したもの
12
+ // app.onError で受けて、自分の形(JSON など)で返すこともできる
13
+ export class ValidationError extends HttpError {
14
+ target;
15
+ issues;
16
+ constructor(target, issues) {
17
+ // 本番の本文(message)には中身を出さない(スキーマの作りをそのまま外に見せないため)
18
+ super(400, "400 Bad Request");
19
+ this.name = "ValidationError";
20
+ this.target = target;
21
+ this.issues = issues;
22
+ }
23
+ // 開発モードで返す、どこが違うかの一覧
24
+ describe() {
25
+ const lines = this.issues.map((issue) => {
26
+ const path = issuePath(issue);
27
+ return `- ${path ? `${path}: ` : ""}${issue.message}`;
28
+ });
29
+ return [`400 Bad Request: ${LABEL[this.target]}が、スキーマに合いません`, ...lines].join("\n");
30
+ }
31
+ }
32
+ const LABEL = {
33
+ params: "パスパラメーター",
34
+ query: "クエリ",
35
+ body: "ボディ",
36
+ };
37
+ // ["user", 0, "name"] → "user[0].name"
38
+ function issuePath(issue) {
39
+ let out = "";
40
+ for (const part of issue.path ?? []) {
41
+ const key = typeof part === "object" ? part.key : part;
42
+ if (typeof key === "number")
43
+ out += `[${key}]`;
44
+ else
45
+ out += out ? `.${String(key)}` : String(key);
46
+ }
47
+ return out;
48
+ }
49
+ // ArkType のスキーマは関数なので、オブジェクトでも関数でもよい
50
+ function isSchema(value) {
51
+ return (typeof value === "object" || typeof value === "function") && value !== null && "~standard" in value;
52
+ }
53
+ // スキーマを確かめておく(書き間違いに、登録した時点で気づけるように)
54
+ export function checkSchemas(path, schemas) {
55
+ if (typeof schemas !== "object" || schemas === null) {
56
+ throw new TypeError(`${path}: 2つ目の引数には { params, query, body } のスキーマかハンドラーを渡してください`);
57
+ }
58
+ for (const [key, schema] of Object.entries(schemas)) {
59
+ if (key !== "params" && key !== "query" && key !== "body") {
60
+ throw new TypeError(`${path}: スキーマに渡せるのは params / query / body だけです("${key}" は使えません)`);
61
+ }
62
+ if (schema !== undefined && !isSchema(schema)) {
63
+ throw new TypeError(`${path}: ${key} が Standard Schema のスキーマではありません(Zod・Valibot・ArkType などのスキーマを渡してください)`);
64
+ }
65
+ }
66
+ }
67
+ async function run(schema, target, value) {
68
+ const result = await schema["~standard"].validate(value);
69
+ if (result.issues)
70
+ throw new ValidationError(target, result.issues);
71
+ return result.value;
72
+ }
73
+ // スキーマで確かめてからハンドラーを呼ぶハンドラーにする。params → query → body の順に確かめる
74
+ export function withValidation(schemas, handler) {
75
+ return async (params, c) => {
76
+ let validParams = params;
77
+ if (schemas.params) {
78
+ validParams = (await run(schemas.params, "params", params));
79
+ c.params = validParams;
80
+ }
81
+ if (schemas.query)
82
+ setValidated(c, "query", await run(schemas.query, "query", c.query()));
83
+ if (schemas.body)
84
+ setValidated(c, "body", await run(schemas.body, "body", await c.body()));
85
+ return handler(validParams, c);
86
+ };
87
+ }
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@senrijs/senri",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Web 標準の Request / Response で書かれた、小さな Web フレームワーク。Bun・Deno・Node.js・Cloudflare Workers で動く",
5
+ "license": "MIT",
6
+ "author": "ibuki-hum4",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Senrijs/Senri.git"
10
+ },
11
+ "homepage": "https://github.com/Senrijs/Senri#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Senrijs/Senri/issues"
14
+ },
15
+ "keywords": [
16
+ "web",
17
+ "framework",
18
+ "router",
19
+ "http",
20
+ "fetch",
21
+ "bun",
22
+ "deno",
23
+ "node",
24
+ "cloudflare-workers",
25
+ "standard-schema"
26
+ ],
27
+ "type": "module",
28
+ "private": false,
29
+ "sideEffects": false,
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/src/index.d.ts",
36
+ "import": "./dist/src/index.js"
37
+ },
38
+ "./node": {
39
+ "types": "./dist/adapters/node/index.d.ts",
40
+ "import": "./dist/adapters/node/index.js"
41
+ },
42
+ "./serve": {
43
+ "types": "./dist/adapters/serve/index.d.ts",
44
+ "import": "./dist/adapters/serve/index.js"
45
+ },
46
+ "./static": {
47
+ "types": "./dist/adapters/static/index.d.ts",
48
+ "import": "./dist/adapters/static/index.js"
49
+ }
50
+ },
51
+ "files": [
52
+ "dist"
53
+ ],
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.build.json",
56
+ "build:pages": "vite build -c ui/vite.config.ts && node scripts/build-pages.ts",
57
+ "typecheck": "bun run build && bun check && bun check -p test && bun check -p test/node && bun check -p adapters/node && bun check -p examples/bun && bun check -p examples/node && bun check -p scripts && deno check examples/deno/index.ts && bun check -p adapters/serve && bun check -p adapters/static && bun check -p ui",
58
+ "dev": "bun --watch examples/bun/index.ts",
59
+ "test": "bun test",
60
+ "test:node": "bun run build && node --test --test-timeout=5000 \"test/node/**/*.test.ts\"",
61
+ "smoke": "node scripts/smoke.ts"
62
+ },
63
+ "devDependencies": {
64
+ "@types/bun": "latest",
65
+ "@types/node": "^26.6.5",
66
+ "@vitejs/plugin-vue": "6.0.10",
67
+ "arktype": "2.2.8",
68
+ "typescript": "^7",
69
+ "valibot": "1.5.0",
70
+ "vite": "8.3.5",
71
+ "vite-plugin-singlefile": "2.3.4",
72
+ "vue": "3.5.44",
73
+ "workerd": "1.20261010.1",
74
+ "zod": "4.6.6"
75
+ }
76
+ }