@znight/http 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.
- package/LICENSE +21 -0
- package/README.md +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
package/dist/path.mjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// src/path.ts
|
|
2
|
+
/** Name of the parameter holding the value matched by a trailing wildcard (`ctx.params['*']`). */
|
|
3
|
+
const WILDCARD_PARAM = "*";
|
|
4
|
+
/** Characters allowed in static segments. */
|
|
5
|
+
const STATIC_SEGMENT = /^[A-Za-z0-9\-._~!$&'+,;=@]+$/;
|
|
6
|
+
const PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7
|
+
const EDGE_SLASHES = /^\/+|\/+$/g;
|
|
8
|
+
/** Error thrown by {@link parseRoutePath}. */
|
|
9
|
+
var RoutePathError = class extends TypeError {
|
|
10
|
+
path;
|
|
11
|
+
issue;
|
|
12
|
+
constructor(path, issue) {
|
|
13
|
+
super(`Invalid route path "${path}": ${issue.message}`);
|
|
14
|
+
this.name = "RoutePathError";
|
|
15
|
+
this.path = path;
|
|
16
|
+
this.issue = issue;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Normalises a path: exactly one leading slash, no trailing slash (except the root `/`). Inner
|
|
21
|
+
* content is left untouched (`//` stays and is rejected by {@link parseRoutePath}).
|
|
22
|
+
*
|
|
23
|
+
* `normalizePath('users/')` → `/users`, `normalizePath('')` → `/`.
|
|
24
|
+
*/
|
|
25
|
+
function normalizePath(path) {
|
|
26
|
+
const trimmed = path.replace(EDGE_SLASHES, "");
|
|
27
|
+
return trimmed === "" ? "/" : `/${trimmed}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Joins path parts (global prefix, controller base path, route path) with single slashes and
|
|
31
|
+
* normalises the result. Empty parts and `/` are ignored.
|
|
32
|
+
*
|
|
33
|
+
* `joinPaths('/api', '/users/', ':id')` → `/api/users/:id`, `joinPaths('/', '')` → `/`.
|
|
34
|
+
*/
|
|
35
|
+
function joinPaths(...parts) {
|
|
36
|
+
let result = "";
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
const trimmed = part.replace(EDGE_SLASHES, "");
|
|
39
|
+
if (trimmed !== "") result += `/${trimmed}`;
|
|
40
|
+
}
|
|
41
|
+
return result === "" ? "/" : result;
|
|
42
|
+
}
|
|
43
|
+
/** Validates `path` (normalised first). Returns the first issue, or `undefined` when it is valid. */
|
|
44
|
+
function checkRoutePath(path) {
|
|
45
|
+
const result = analyse(path);
|
|
46
|
+
return "message" in result ? result : void 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parses and validates a route path (normalised first).
|
|
50
|
+
*
|
|
51
|
+
* @throws RoutePathError when the path violates the grammar.
|
|
52
|
+
*/
|
|
53
|
+
function parseRoutePath(path) {
|
|
54
|
+
const result = analyse(path);
|
|
55
|
+
if ("message" in result) throw new RoutePathError(path, result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function analyse(input) {
|
|
59
|
+
if (typeof input !== "string") return { message: "a route path must be a string" };
|
|
60
|
+
if (input.includes("?") || input.includes("#")) return { message: "a route path cannot contain \"?\" or \"#\" (query strings are not part of routes)" };
|
|
61
|
+
const path = normalizePath(input);
|
|
62
|
+
if (path === "/") return {
|
|
63
|
+
path,
|
|
64
|
+
segments: [],
|
|
65
|
+
params: [],
|
|
66
|
+
wildcard: false,
|
|
67
|
+
shape: "/"
|
|
68
|
+
};
|
|
69
|
+
const raw = path.slice(1).split("/");
|
|
70
|
+
const segments = [];
|
|
71
|
+
const params = [];
|
|
72
|
+
for (let index = 0; index < raw.length; index++) {
|
|
73
|
+
const segment = raw[index];
|
|
74
|
+
if (segment === "") return { message: "empty segment (\"//\")" };
|
|
75
|
+
if (segment === "*") {
|
|
76
|
+
if (index !== raw.length - 1) return {
|
|
77
|
+
message: "the wildcard \"*\" must be the last segment",
|
|
78
|
+
segment
|
|
79
|
+
};
|
|
80
|
+
segments.push({ kind: "wildcard" });
|
|
81
|
+
params.push("*");
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (segment.includes("*")) return {
|
|
85
|
+
message: `"*" must be a whole segment (found "${segment}")`,
|
|
86
|
+
segment
|
|
87
|
+
};
|
|
88
|
+
if (segment.startsWith(":")) {
|
|
89
|
+
const name = segment.slice(1);
|
|
90
|
+
if (!PARAM_NAME.test(name)) return {
|
|
91
|
+
message: `invalid parameter name "${name}" (expected letters, digits and "_", not starting with a digit)`,
|
|
92
|
+
segment
|
|
93
|
+
};
|
|
94
|
+
if (params.includes(name)) return {
|
|
95
|
+
message: `duplicate parameter ":${name}"`,
|
|
96
|
+
segment
|
|
97
|
+
};
|
|
98
|
+
segments.push({
|
|
99
|
+
kind: "param",
|
|
100
|
+
name
|
|
101
|
+
});
|
|
102
|
+
params.push(name);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (segment.includes(":")) return {
|
|
106
|
+
message: `":" is only allowed at the start of a segment (found "${segment}")`,
|
|
107
|
+
segment
|
|
108
|
+
};
|
|
109
|
+
if (segment === "." || segment === "..") return {
|
|
110
|
+
message: "dot segments (\".\" and \"..\") are not allowed",
|
|
111
|
+
segment
|
|
112
|
+
};
|
|
113
|
+
if (!STATIC_SEGMENT.test(segment)) {
|
|
114
|
+
const bad = [...segment].find((char) => !STATIC_SEGMENT.test(char)) ?? segment;
|
|
115
|
+
return {
|
|
116
|
+
message: `invalid character ${JSON.stringify(bad)} in "${segment}" (allowed: letters, digits and - . _ ~ ! $ & ' + , ; = @)`,
|
|
117
|
+
segment
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
segments.push({
|
|
121
|
+
kind: "static",
|
|
122
|
+
value: segment
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
path,
|
|
127
|
+
segments,
|
|
128
|
+
params,
|
|
129
|
+
wildcard: params.at(-1) === "*",
|
|
130
|
+
shape: shapeOf(segments)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function shapeOf(segments) {
|
|
134
|
+
let shape = "";
|
|
135
|
+
for (const segment of segments) if (segment.kind === "static") shape += `/${segment.value}`;
|
|
136
|
+
else if (segment.kind === "param") shape += "/:";
|
|
137
|
+
else shape += "/*";
|
|
138
|
+
return shape === "" ? "/" : shape;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Matches a raw request path (no query string) against a route. Returns the **raw**
|
|
142
|
+
* (percent-encoded) parameter values, or `undefined` when the path does not match.
|
|
143
|
+
*/
|
|
144
|
+
function matchRoutePath(route, path) {
|
|
145
|
+
if (path.charCodeAt(0) !== 47) return void 0;
|
|
146
|
+
const segments = route.segments;
|
|
147
|
+
if (segments.length === 0) return path === "/" ? {} : void 0;
|
|
148
|
+
const params = {};
|
|
149
|
+
let position = 1;
|
|
150
|
+
for (let index = 0; index < segments.length; index++) {
|
|
151
|
+
const segment = segments[index];
|
|
152
|
+
if (segment.kind === "wildcard") {
|
|
153
|
+
params["*"] = path.slice(position);
|
|
154
|
+
return params;
|
|
155
|
+
}
|
|
156
|
+
const slash = path.indexOf("/", position);
|
|
157
|
+
const end = slash === -1 ? path.length : slash;
|
|
158
|
+
const value = path.slice(position, end);
|
|
159
|
+
if (segment.kind === "static") {
|
|
160
|
+
if (value !== segment.value) return void 0;
|
|
161
|
+
} else {
|
|
162
|
+
if (value === "") return void 0;
|
|
163
|
+
params[segment.name] = value;
|
|
164
|
+
}
|
|
165
|
+
if (index === segments.length - 1) return end === path.length ? params : void 0;
|
|
166
|
+
if (slash === -1) return void 0;
|
|
167
|
+
position = slash + 1;
|
|
168
|
+
}
|
|
169
|
+
return params;
|
|
170
|
+
}
|
|
171
|
+
/** Decodes a raw parameter value. Returns `undefined` when it is not valid percent-encoding. */
|
|
172
|
+
function decodePathValue(value) {
|
|
173
|
+
if (!value.includes("%")) return value;
|
|
174
|
+
try {
|
|
175
|
+
return decodeURIComponent(value);
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const RANK = {
|
|
181
|
+
static: 0,
|
|
182
|
+
param: 1,
|
|
183
|
+
wildcard: 2
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Orders routes from the most to the least specific (static segments before parameters before the
|
|
187
|
+
* wildcard, compared segment by segment), then by path. Registering routes in this order makes
|
|
188
|
+
* first-match routers (Express, Hono) behave like specificity-based ones (Fastify, the built-in
|
|
189
|
+
* router).
|
|
190
|
+
*/
|
|
191
|
+
function compareRoutePaths(a, b) {
|
|
192
|
+
const length = Math.min(a.segments.length, b.segments.length);
|
|
193
|
+
for (let index = 0; index < length; index++) {
|
|
194
|
+
const left = a.segments[index];
|
|
195
|
+
const right = b.segments[index];
|
|
196
|
+
const rank = RANK[left.kind] - RANK[right.kind];
|
|
197
|
+
if (rank !== 0) return rank;
|
|
198
|
+
if (left.kind === "static" && right.kind === "static" && left.value !== right.value) return left.value < right.value ? -1 : 1;
|
|
199
|
+
}
|
|
200
|
+
if (a.segments.length !== b.segments.length) return a.segments.length - b.segments.length;
|
|
201
|
+
return compareStrings(a.path, b.path);
|
|
202
|
+
}
|
|
203
|
+
/** Ordinal string comparison (locale-independent, deterministic). */
|
|
204
|
+
function compareStrings(a, b) {
|
|
205
|
+
if (a === b) return 0;
|
|
206
|
+
return a < b ? -1 : 1;
|
|
207
|
+
}
|
|
208
|
+
/** Fastify / find-my-way pattern (`/users/:id`, `/files/*`). */
|
|
209
|
+
function toFastifyPath(route) {
|
|
210
|
+
return toColonPath(route, "*");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Hono pattern. The wildcard becomes a regexp parameter so that `/files/*` does not match `/files`
|
|
214
|
+
* (Hono's own `*` does).
|
|
215
|
+
*/
|
|
216
|
+
function toHonoPath(route) {
|
|
217
|
+
return toColonPath(route, ":__znight_wildcard{.*}");
|
|
218
|
+
}
|
|
219
|
+
function toColonPath(route, wildcard) {
|
|
220
|
+
if (route.segments.length === 0) return "/";
|
|
221
|
+
let result = "";
|
|
222
|
+
for (const segment of route.segments) if (segment.kind === "static") result += `/${segment.value}`;
|
|
223
|
+
else if (segment.kind === "param") result += `/:${segment.name}`;
|
|
224
|
+
else result += `/${wildcard}`;
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Anchored regular expression source matching the raw path of a route, without capturing groups
|
|
229
|
+
* (`^\/users\/[^/]+$`). Used by the Express adapter, which matches regular expressions against the
|
|
230
|
+
* raw path.
|
|
231
|
+
*/
|
|
232
|
+
function toRegExpSource(route) {
|
|
233
|
+
if (route.segments.length === 0) return "^\\/$";
|
|
234
|
+
let source = "^";
|
|
235
|
+
for (const segment of route.segments) if (segment.kind === "static") source += `\\/${escapeRegExp(segment.value)}`;
|
|
236
|
+
else if (segment.kind === "param") source += "\\/[^/]+";
|
|
237
|
+
else source += "\\/.*";
|
|
238
|
+
return `${source}$`;
|
|
239
|
+
}
|
|
240
|
+
function escapeRegExp(value) {
|
|
241
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
}
|
|
243
|
+
export { RoutePathError, WILDCARD_PARAM, checkRoutePath, compareRoutePaths, compareStrings, decodePathValue, joinPaths, matchRoutePath, normalizePath, parseRoutePath, toFastifyPath, toHonoPath, toRegExpSource };
|
|
244
|
+
|
|
245
|
+
//# sourceMappingURL=path.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.mjs","names":[],"sources":["../src/path.ts"],"sourcesContent":["/**\n * Route path grammar of `@znight/http`, shared by the runtime (adapters, router) and the compiler\n * extension (ZNIGHT007 diagnostics, `fullPath`). This module is **pure**: no imports, no I/O, no\n * global state, so the compiler can load it without loading the HTTP runtime.\n *\n * Grammar (after normalisation, see {@link normalizePath}):\n *\n * ```\n * path = \"/\" | ( \"/\" segment )+\n * segment = static | param | wildcard ; wildcard only as the last segment\n * static = 1*( ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\" / \"!\" / \"$\" / \"&\" / \"'\" / \"+\" / \",\" / \";\" / \"=\" / \"@\" )\n * param = \":\" name ; name = [A-Za-z_][A-Za-z0-9_]*, unique in the path\n * wildcard = \"*\" ; matches the rest of the path (may be empty)\n * ```\n *\n * Matching is case-sensitive and works on the raw (percent-encoded) request path. A `:param`\n * matches one non-empty segment; `/files/*` matches `/files/` and `/files/a/b` (not `/files`).\n */\n\n/** A segment of a parsed route path. */\nexport type RouteSegment =\n | { readonly kind: 'static'; readonly value: string }\n | { readonly kind: 'param'; readonly name: string }\n | { readonly kind: 'wildcard' }\n\n/** A validated, normalised route path. */\nexport interface RoutePath {\n /** Normalised path (`/users/:id`, `/` for the root). */\n readonly path: string\n readonly segments: readonly RouteSegment[]\n /** Parameter names, in order. The wildcard, when present, is last and named {@link WILDCARD_PARAM}. */\n readonly params: readonly string[]\n readonly wildcard: boolean\n /**\n * Matching shape: param names are erased (`/users/:` for `/users/:id`). Two routes with the same\n * method and shape conflict.\n */\n readonly shape: string\n}\n\n/** A grammar violation. */\nexport interface RoutePathIssue {\n readonly message: string\n /** The offending segment, when the issue concerns one. */\n readonly segment?: string\n}\n\n/** Name of the parameter holding the value matched by a trailing wildcard (`ctx.params['*']`). */\nexport const WILDCARD_PARAM = '*'\n\n/** Characters allowed in static segments. */\nconst STATIC_SEGMENT = /^[A-Za-z0-9\\-._~!$&'+,;=@]+$/\nconst PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/\nconst EDGE_SLASHES = /^\\/+|\\/+$/g\n\n/** Error thrown by {@link parseRoutePath}. */\nexport class RoutePathError extends TypeError {\n readonly path: string\n readonly issue: RoutePathIssue\n\n constructor(path: string, issue: RoutePathIssue) {\n super(`Invalid route path \"${path}\": ${issue.message}`)\n this.name = 'RoutePathError'\n this.path = path\n this.issue = issue\n }\n}\n\n/**\n * Normalises a path: exactly one leading slash, no trailing slash (except the root `/`). Inner\n * content is left untouched (`//` stays and is rejected by {@link parseRoutePath}).\n *\n * `normalizePath('users/')` → `/users`, `normalizePath('')` → `/`.\n */\nexport function normalizePath(path: string): string {\n const trimmed = path.replace(EDGE_SLASHES, '')\n return trimmed === '' ? '/' : `/${trimmed}`\n}\n\n/**\n * Joins path parts (global prefix, controller base path, route path) with single slashes and\n * normalises the result. Empty parts and `/` are ignored.\n *\n * `joinPaths('/api', '/users/', ':id')` → `/api/users/:id`, `joinPaths('/', '')` → `/`.\n */\nexport function joinPaths(...parts: readonly string[]): string {\n let result = ''\n for (const part of parts) {\n const trimmed = part.replace(EDGE_SLASHES, '')\n if (trimmed !== '') result += `/${trimmed}`\n }\n return result === '' ? '/' : result\n}\n\n/** Validates `path` (normalised first). Returns the first issue, or `undefined` when it is valid. */\nexport function checkRoutePath(path: string): RoutePathIssue | undefined {\n const result = analyse(path)\n return 'message' in result ? result : undefined\n}\n\n/**\n * Parses and validates a route path (normalised first).\n *\n * @throws RoutePathError when the path violates the grammar.\n */\nexport function parseRoutePath(path: string): RoutePath {\n const result = analyse(path)\n if ('message' in result) throw new RoutePathError(path, result)\n return result\n}\n\nfunction analyse(input: string): RoutePath | RoutePathIssue {\n if (typeof input !== 'string') return { message: 'a route path must be a string' }\n if (input.includes('?') || input.includes('#')) {\n return { message: 'a route path cannot contain \"?\" or \"#\" (query strings are not part of routes)' }\n }\n const path = normalizePath(input)\n if (path === '/') return { path, segments: [], params: [], wildcard: false, shape: '/' }\n const raw = path.slice(1).split('/')\n const segments: RouteSegment[] = []\n const params: string[] = []\n for (let index = 0; index < raw.length; index++) {\n const segment = raw[index] as string\n if (segment === '') return { message: 'empty segment (\"//\")' }\n if (segment === '*') {\n if (index !== raw.length - 1) {\n return { message: 'the wildcard \"*\" must be the last segment', segment }\n }\n segments.push({ kind: 'wildcard' })\n params.push(WILDCARD_PARAM)\n continue\n }\n if (segment.includes('*')) {\n return { message: `\"*\" must be a whole segment (found \"${segment}\")`, segment }\n }\n if (segment.startsWith(':')) {\n const name = segment.slice(1)\n if (!PARAM_NAME.test(name)) {\n return {\n message: `invalid parameter name \"${name}\" (expected letters, digits and \"_\", not starting with a digit)`,\n segment,\n }\n }\n if (params.includes(name)) return { message: `duplicate parameter \":${name}\"`, segment }\n segments.push({ kind: 'param', name })\n params.push(name)\n continue\n }\n if (segment.includes(':')) {\n return { message: `\":\" is only allowed at the start of a segment (found \"${segment}\")`, segment }\n }\n if (segment === '.' || segment === '..') return { message: 'dot segments (\".\" and \"..\") are not allowed', segment }\n if (!STATIC_SEGMENT.test(segment)) {\n const bad = [...segment].find(char => !STATIC_SEGMENT.test(char)) ?? segment\n return {\n message: `invalid character ${JSON.stringify(bad)} in \"${segment}\" (allowed: letters, digits and - . _ ~ ! $ & ' + , ; = @)`,\n segment,\n }\n }\n segments.push({ kind: 'static', value: segment })\n }\n return { path, segments, params, wildcard: params.at(-1) === WILDCARD_PARAM, shape: shapeOf(segments) }\n}\n\nfunction shapeOf(segments: readonly RouteSegment[]): string {\n let shape = ''\n for (const segment of segments) {\n if (segment.kind === 'static') shape += `/${segment.value}`\n else if (segment.kind === 'param') shape += '/:'\n else shape += '/*'\n }\n return shape === '' ? '/' : shape\n}\n\n/**\n * Matches a raw request path (no query string) against a route. Returns the **raw**\n * (percent-encoded) parameter values, or `undefined` when the path does not match.\n */\nexport function matchRoutePath(route: RoutePath, path: string): Record<string, string> | undefined {\n if (path.charCodeAt(0) !== 47 /* / */) return undefined\n const segments = route.segments\n if (segments.length === 0) return path === '/' ? {} : undefined\n const params: Record<string, string> = {}\n let position = 1\n for (let index = 0; index < segments.length; index++) {\n const segment = segments[index] as RouteSegment\n if (segment.kind === 'wildcard') {\n params[WILDCARD_PARAM] = path.slice(position)\n return params\n }\n const slash = path.indexOf('/', position)\n const end = slash === -1 ? path.length : slash\n const value = path.slice(position, end)\n if (segment.kind === 'static') {\n if (value !== segment.value) return undefined\n } else {\n if (value === '') return undefined\n params[segment.name] = value\n }\n if (index === segments.length - 1) return end === path.length ? params : undefined\n if (slash === -1) return undefined\n position = slash + 1\n }\n return params\n}\n\n/** Decodes a raw parameter value. Returns `undefined` when it is not valid percent-encoding. */\nexport function decodePathValue(value: string): string | undefined {\n if (!value.includes('%')) return value\n try {\n return decodeURIComponent(value)\n } catch {\n return undefined\n }\n}\n\nconst RANK = { static: 0, param: 1, wildcard: 2 } as const\n\n/**\n * Orders routes from the most to the least specific (static segments before parameters before the\n * wildcard, compared segment by segment), then by path. Registering routes in this order makes\n * first-match routers (Express, Hono) behave like specificity-based ones (Fastify, the built-in\n * router).\n */\nexport function compareRoutePaths(a: RoutePath, b: RoutePath): number {\n const length = Math.min(a.segments.length, b.segments.length)\n for (let index = 0; index < length; index++) {\n const left = a.segments[index] as RouteSegment\n const right = b.segments[index] as RouteSegment\n const rank = RANK[left.kind] - RANK[right.kind]\n if (rank !== 0) return rank\n if (left.kind === 'static' && right.kind === 'static' && left.value !== right.value) {\n return left.value < right.value ? -1 : 1\n }\n }\n if (a.segments.length !== b.segments.length) return a.segments.length - b.segments.length\n return compareStrings(a.path, b.path)\n}\n\n/** Ordinal string comparison (locale-independent, deterministic). */\nexport function compareStrings(a: string, b: string): number {\n if (a === b) return 0\n return a < b ? -1 : 1\n}\n\n/** Fastify / find-my-way pattern (`/users/:id`, `/files/*`). */\nexport function toFastifyPath(route: RoutePath): string {\n return toColonPath(route, '*')\n}\n\n/**\n * Hono pattern. The wildcard becomes a regexp parameter so that `/files/*` does not match `/files`\n * (Hono's own `*` does).\n */\nexport function toHonoPath(route: RoutePath): string {\n return toColonPath(route, ':__znight_wildcard{.*}')\n}\n\nfunction toColonPath(route: RoutePath, wildcard: string): string {\n if (route.segments.length === 0) return '/'\n let result = ''\n for (const segment of route.segments) {\n if (segment.kind === 'static') result += `/${segment.value}`\n else if (segment.kind === 'param') result += `/:${segment.name}`\n else result += `/${wildcard}`\n }\n return result\n}\n\n/**\n * Anchored regular expression source matching the raw path of a route, without capturing groups\n * (`^\\/users\\/[^/]+$`). Used by the Express adapter, which matches regular expressions against the\n * raw path.\n */\nexport function toRegExpSource(route: RoutePath): string {\n if (route.segments.length === 0) return '^\\\\/$'\n let source = '^'\n for (const segment of route.segments) {\n if (segment.kind === 'static') source += `\\\\/${escapeRegExp(segment.value)}`\n else if (segment.kind === 'param') source += '\\\\/[^/]+'\n else source += '\\\\/.*'\n }\n return `${source}$`\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// src/mime.ts
|
|
2
|
+
/**
|
|
3
|
+
* Media types of files: a small extension table for downloads and `type/*` patterns for upload
|
|
4
|
+
* filters. This module is **pure** (no imports, no I/O): the compiler extension validates the
|
|
5
|
+
* `mimeTypes` option of `@UploadedFile` with it.
|
|
6
|
+
*/
|
|
7
|
+
/** Common extensions → media type (text types get `charset=utf-8` in {@link contentTypeFor}). */
|
|
8
|
+
const TYPES = {
|
|
9
|
+
txt: "text/plain",
|
|
10
|
+
text: "text/plain",
|
|
11
|
+
log: "text/plain",
|
|
12
|
+
md: "text/markdown",
|
|
13
|
+
markdown: "text/markdown",
|
|
14
|
+
csv: "text/csv",
|
|
15
|
+
tsv: "text/tab-separated-values",
|
|
16
|
+
htm: "text/html",
|
|
17
|
+
html: "text/html",
|
|
18
|
+
css: "text/css",
|
|
19
|
+
xml: "application/xml",
|
|
20
|
+
js: "text/javascript",
|
|
21
|
+
mjs: "text/javascript",
|
|
22
|
+
cjs: "text/javascript",
|
|
23
|
+
json: "application/json",
|
|
24
|
+
map: "application/json",
|
|
25
|
+
jsonld: "application/ld+json",
|
|
26
|
+
webmanifest: "application/manifest+json",
|
|
27
|
+
yaml: "application/yaml",
|
|
28
|
+
yml: "application/yaml",
|
|
29
|
+
ics: "text/calendar",
|
|
30
|
+
vtt: "text/vtt",
|
|
31
|
+
png: "image/png",
|
|
32
|
+
jpg: "image/jpeg",
|
|
33
|
+
jpeg: "image/jpeg",
|
|
34
|
+
gif: "image/gif",
|
|
35
|
+
webp: "image/webp",
|
|
36
|
+
avif: "image/avif",
|
|
37
|
+
svg: "image/svg+xml",
|
|
38
|
+
ico: "image/x-icon",
|
|
39
|
+
bmp: "image/bmp",
|
|
40
|
+
tif: "image/tiff",
|
|
41
|
+
tiff: "image/tiff",
|
|
42
|
+
heic: "image/heic",
|
|
43
|
+
mp3: "audio/mpeg",
|
|
44
|
+
wav: "audio/wav",
|
|
45
|
+
ogg: "audio/ogg",
|
|
46
|
+
oga: "audio/ogg",
|
|
47
|
+
flac: "audio/flac",
|
|
48
|
+
aac: "audio/aac",
|
|
49
|
+
m4a: "audio/mp4",
|
|
50
|
+
opus: "audio/opus",
|
|
51
|
+
mp4: "video/mp4",
|
|
52
|
+
m4v: "video/mp4",
|
|
53
|
+
webm: "video/webm",
|
|
54
|
+
ogv: "video/ogg",
|
|
55
|
+
mov: "video/quicktime",
|
|
56
|
+
avi: "video/x-msvideo",
|
|
57
|
+
mkv: "video/x-matroska",
|
|
58
|
+
woff: "font/woff",
|
|
59
|
+
woff2: "font/woff2",
|
|
60
|
+
ttf: "font/ttf",
|
|
61
|
+
otf: "font/otf",
|
|
62
|
+
pdf: "application/pdf",
|
|
63
|
+
rtf: "application/rtf",
|
|
64
|
+
doc: "application/msword",
|
|
65
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
66
|
+
xls: "application/vnd.ms-excel",
|
|
67
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
68
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
69
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
70
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
71
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
72
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
73
|
+
epub: "application/epub+zip",
|
|
74
|
+
zip: "application/zip",
|
|
75
|
+
gz: "application/gzip",
|
|
76
|
+
tgz: "application/gzip",
|
|
77
|
+
tar: "application/x-tar",
|
|
78
|
+
bz2: "application/x-bzip2",
|
|
79
|
+
xz: "application/x-xz",
|
|
80
|
+
"7z": "application/x-7z-compressed",
|
|
81
|
+
rar: "application/vnd.rar",
|
|
82
|
+
wasm: "application/wasm",
|
|
83
|
+
bin: "application/octet-stream"
|
|
84
|
+
};
|
|
85
|
+
/** Default media type of files without a known extension. */
|
|
86
|
+
const OCTET_STREAM = "application/octet-stream";
|
|
87
|
+
/**
|
|
88
|
+
* Media type of a file name from its extension (`report.pdf` → `application/pdf`, text types with
|
|
89
|
+
* `; charset=utf-8`), `undefined` when the extension is unknown.
|
|
90
|
+
*/
|
|
91
|
+
function contentTypeFor(filename) {
|
|
92
|
+
const dot = filename.lastIndexOf(".");
|
|
93
|
+
if (dot === -1 || dot === filename.length - 1) return void 0;
|
|
94
|
+
if (dot < Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"))) return void 0;
|
|
95
|
+
const extension = filename.slice(dot + 1).toLowerCase();
|
|
96
|
+
if (!Object.hasOwn(TYPES, extension)) return void 0;
|
|
97
|
+
const type = TYPES[extension];
|
|
98
|
+
return isTextual(type) ? `${type}; charset=utf-8` : type;
|
|
99
|
+
}
|
|
100
|
+
function isTextual(type) {
|
|
101
|
+
return type.startsWith("text/") || type === "application/json" || type === "application/ld+json" || type === "application/manifest+json" || type === "application/xml" || type === "application/yaml" || type === "image/svg+xml";
|
|
102
|
+
}
|
|
103
|
+
/** `type/subtype` token characters (RFC 9110). */
|
|
104
|
+
const TOKEN = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+";
|
|
105
|
+
const MEDIA_TYPE = new RegExp(`^${TOKEN}/${TOKEN}$`);
|
|
106
|
+
/**
|
|
107
|
+
* Whether `pattern` is a valid media type filter: `type/subtype`, `type/*` or `*\/*`
|
|
108
|
+
* (case-insensitive, without parameters).
|
|
109
|
+
*/
|
|
110
|
+
function isMediaTypePattern(pattern) {
|
|
111
|
+
if (!MEDIA_TYPE.test(pattern)) return false;
|
|
112
|
+
const [type, subtype] = pattern.split("/");
|
|
113
|
+
if (type.includes("*") && type !== "*") return false;
|
|
114
|
+
if (subtype.includes("*") && subtype !== "*") return false;
|
|
115
|
+
return type !== "*" || subtype === "*";
|
|
116
|
+
}
|
|
117
|
+
/** The essence of a media type: lower-case `type/subtype` without parameters (`Text/Plain; charset=x` → `text/plain`). */
|
|
118
|
+
function mediaTypeEssence(type) {
|
|
119
|
+
const separator = type.indexOf(";");
|
|
120
|
+
return (separator === -1 ? type : type.slice(0, separator)).trim().toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
/** Whether the media type `type` (parameters ignored) matches one of `patterns` (`image/png`, `image/*`, `*\/*`). */
|
|
123
|
+
function matchesMediaType(type, patterns) {
|
|
124
|
+
const essence = mediaTypeEssence(type);
|
|
125
|
+
const slash = essence.indexOf("/");
|
|
126
|
+
if (slash <= 0 || slash === essence.length - 1) return false;
|
|
127
|
+
const major = essence.slice(0, slash);
|
|
128
|
+
for (const raw of patterns) {
|
|
129
|
+
const pattern = raw.toLowerCase();
|
|
130
|
+
if (pattern === "*/*" || pattern === essence) return true;
|
|
131
|
+
if (pattern.endsWith("/*") && pattern.slice(0, -2) === major) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// src/size.ts
|
|
136
|
+
const UNITS = {
|
|
137
|
+
b: 1,
|
|
138
|
+
k: 1024,
|
|
139
|
+
kb: 1024,
|
|
140
|
+
kib: 1024,
|
|
141
|
+
m: 1024 ** 2,
|
|
142
|
+
mb: 1024 ** 2,
|
|
143
|
+
mib: 1024 ** 2,
|
|
144
|
+
g: 1024 ** 3,
|
|
145
|
+
gb: 1024 ** 3,
|
|
146
|
+
gib: 1024 ** 3,
|
|
147
|
+
t: 1024 ** 4,
|
|
148
|
+
tb: 1024 ** 4,
|
|
149
|
+
tib: 1024 ** 4
|
|
150
|
+
};
|
|
151
|
+
const SIZE = /^(\d+(?:\.\d+)?|\.\d+)\s*([a-z]*)$/i;
|
|
152
|
+
/**
|
|
153
|
+
* Parses a {@link ByteSize}: `1024` → `1024`, `'10mb'` → `10485760`, `'1.5kb'` → `1536` (fractions
|
|
154
|
+
* are rounded down). Returns `undefined` for anything else (negative or fractional numbers,
|
|
155
|
+
* unknown units, `NaN`).
|
|
156
|
+
*/
|
|
157
|
+
function parseByteSize(value) {
|
|
158
|
+
if (typeof value === "number") {
|
|
159
|
+
if (value === Number.POSITIVE_INFINITY) return value;
|
|
160
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
161
|
+
}
|
|
162
|
+
if (typeof value !== "string") return void 0;
|
|
163
|
+
const match = SIZE.exec(value.trim());
|
|
164
|
+
if (match === null) return void 0;
|
|
165
|
+
const unit = match[2].toLowerCase();
|
|
166
|
+
const multiplier = unit === "" ? 1 : UNITS[unit];
|
|
167
|
+
if (multiplier === void 0) return void 0;
|
|
168
|
+
const bytes = Math.floor(Number(match[1]) * multiplier);
|
|
169
|
+
return Number.isSafeInteger(bytes) ? bytes : void 0;
|
|
170
|
+
}
|
|
171
|
+
/** Human-readable size for messages: `5 MiB`, `1.5 KiB`, `512 bytes`, `unlimited`. */
|
|
172
|
+
function formatByteSize(bytes) {
|
|
173
|
+
if (bytes === Number.POSITIVE_INFINITY) return "unlimited";
|
|
174
|
+
for (const [unit, size] of [
|
|
175
|
+
["TiB", 1024 ** 4],
|
|
176
|
+
["GiB", 1024 ** 3],
|
|
177
|
+
["MiB", 1024 ** 2],
|
|
178
|
+
["KiB", 1024]
|
|
179
|
+
]) if (bytes >= size) return `${Number((bytes / size).toFixed(2))} ${unit}`;
|
|
180
|
+
return bytes === 1 ? "1 byte" : `${bytes} bytes`;
|
|
181
|
+
}
|
|
182
|
+
export { isMediaTypePattern as a, contentTypeFor as i, parseByteSize as n, matchesMediaType as o, OCTET_STREAM as r, mediaTypeEssence as s, formatByteSize as t };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=size-D8rsQcV0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-D8rsQcV0.mjs","names":[],"sources":["../src/mime.ts","../src/size.ts"],"sourcesContent":["/**\n * Media types of files: a small extension table for downloads and `type/*` patterns for upload\n * filters. This module is **pure** (no imports, no I/O): the compiler extension validates the\n * `mimeTypes` option of `@UploadedFile` with it.\n */\n\n/** Common extensions → media type (text types get `charset=utf-8` in {@link contentTypeFor}). */\nconst TYPES: Readonly<Record<string, string>> = {\n // text\n txt: 'text/plain',\n text: 'text/plain',\n log: 'text/plain',\n md: 'text/markdown',\n markdown: 'text/markdown',\n csv: 'text/csv',\n tsv: 'text/tab-separated-values',\n htm: 'text/html',\n html: 'text/html',\n css: 'text/css',\n xml: 'application/xml',\n js: 'text/javascript',\n mjs: 'text/javascript',\n cjs: 'text/javascript',\n json: 'application/json',\n map: 'application/json',\n jsonld: 'application/ld+json',\n webmanifest: 'application/manifest+json',\n yaml: 'application/yaml',\n yml: 'application/yaml',\n ics: 'text/calendar',\n vtt: 'text/vtt',\n // images\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n ico: 'image/x-icon',\n bmp: 'image/bmp',\n tif: 'image/tiff',\n tiff: 'image/tiff',\n heic: 'image/heic',\n // audio / video\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n oga: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n m4a: 'audio/mp4',\n opus: 'audio/opus',\n mp4: 'video/mp4',\n m4v: 'video/mp4',\n webm: 'video/webm',\n ogv: 'video/ogg',\n mov: 'video/quicktime',\n avi: 'video/x-msvideo',\n mkv: 'video/x-matroska',\n // fonts\n woff: 'font/woff',\n woff2: 'font/woff2',\n ttf: 'font/ttf',\n otf: 'font/otf',\n // documents and archives\n pdf: 'application/pdf',\n rtf: 'application/rtf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n epub: 'application/epub+zip',\n zip: 'application/zip',\n gz: 'application/gzip',\n tgz: 'application/gzip',\n tar: 'application/x-tar',\n bz2: 'application/x-bzip2',\n xz: 'application/x-xz',\n '7z': 'application/x-7z-compressed',\n rar: 'application/vnd.rar',\n wasm: 'application/wasm',\n bin: 'application/octet-stream',\n}\n\n/** Default media type of files without a known extension. */\nexport const OCTET_STREAM = 'application/octet-stream'\n\n/**\n * Media type of a file name from its extension (`report.pdf` → `application/pdf`, text types with\n * `; charset=utf-8`), `undefined` when the extension is unknown.\n */\nexport function contentTypeFor(filename: string): string | undefined {\n const dot = filename.lastIndexOf('.')\n if (dot === -1 || dot === filename.length - 1) return undefined\n const separator = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\\\'))\n if (dot < separator) return undefined\n const extension = filename.slice(dot + 1).toLowerCase()\n // own keys only: `.constructor` / `.__proto__` must not resolve to `Object.prototype` members\n if (!Object.hasOwn(TYPES, extension)) return undefined\n const type = TYPES[extension] as string\n return isTextual(type) ? `${type}; charset=utf-8` : type\n}\n\nfunction isTextual(type: string): boolean {\n return (\n type.startsWith('text/') ||\n type === 'application/json' ||\n type === 'application/ld+json' ||\n type === 'application/manifest+json' ||\n type === 'application/xml' ||\n type === 'application/yaml' ||\n type === 'image/svg+xml'\n )\n}\n\n/** `type/subtype` token characters (RFC 9110). */\nconst TOKEN = \"[!#$%&'*+\\\\-.^_`|~0-9A-Za-z]+\"\nconst MEDIA_TYPE = new RegExp(`^${TOKEN}/${TOKEN}$`)\n\n/**\n * Whether `pattern` is a valid media type filter: `type/subtype`, `type/*` or `*\\/*`\n * (case-insensitive, without parameters).\n */\nexport function isMediaTypePattern(pattern: string): boolean {\n if (!MEDIA_TYPE.test(pattern)) return false\n const [type, subtype] = pattern.split('/') as [string, string]\n // `*` is only a wildcard on its own: `image/*`, `*/*` (`*/png` and `image/png*` are rejected)\n if (type.includes('*') && type !== '*') return false\n if (subtype.includes('*') && subtype !== '*') return false\n return type !== '*' || subtype === '*'\n}\n\n/** The essence of a media type: lower-case `type/subtype` without parameters (`Text/Plain; charset=x` → `text/plain`). */\nexport function mediaTypeEssence(type: string): string {\n const separator = type.indexOf(';')\n return (separator === -1 ? type : type.slice(0, separator)).trim().toLowerCase()\n}\n\n/** Whether the media type `type` (parameters ignored) matches one of `patterns` (`image/png`, `image/*`, `*\\/*`). */\nexport function matchesMediaType(type: string, patterns: readonly string[]): boolean {\n const essence = mediaTypeEssence(type)\n const slash = essence.indexOf('/')\n if (slash <= 0 || slash === essence.length - 1) return false\n const major = essence.slice(0, slash)\n for (const raw of patterns) {\n const pattern = raw.toLowerCase()\n if (pattern === '*/*' || pattern === essence) return true\n if (pattern.endsWith('/*') && pattern.slice(0, -2) === major) return true\n }\n return false\n}\n","/**\n * Byte sizes (`1048576`, `'512kb'`, `'10 MiB'`, `'1.5gb'`) used by body and upload limits. This\n * module is **pure** (no imports, no I/O): the compiler extension evaluates `@BodyLimit('10mb')` and\n * `maxSize` options with it at build time.\n *\n * Units are case-insensitive and binary, like the `bytes` package used by Express:\n * `b`, `kb`/`kib`/`k` = 1024, `mb`/`mib`/`m` = 1024², `gb`/`gib`/`g` = 1024³, `tb`/`tib`/`t` = 1024⁴.\n */\n\n/**\n * A size in bytes: a non-negative integer (`Infinity` disables the limit) or a string with a unit\n * (`'512kb'`, `'10mb'`, `'1.5 GiB'`; `kb` = 1024 bytes).\n */\nexport type ByteSize = number | string\n\nconst UNITS: Readonly<Record<string, number>> = {\n b: 1,\n k: 1024,\n kb: 1024,\n kib: 1024,\n m: 1024 ** 2,\n mb: 1024 ** 2,\n mib: 1024 ** 2,\n g: 1024 ** 3,\n gb: 1024 ** 3,\n gib: 1024 ** 3,\n t: 1024 ** 4,\n tb: 1024 ** 4,\n tib: 1024 ** 4,\n}\n\nconst SIZE = /^(\\d+(?:\\.\\d+)?|\\.\\d+)\\s*([a-z]*)$/i\n\n/**\n * Parses a {@link ByteSize}: `1024` → `1024`, `'10mb'` → `10485760`, `'1.5kb'` → `1536` (fractions\n * are rounded down). Returns `undefined` for anything else (negative or fractional numbers,\n * unknown units, `NaN`).\n */\nexport function parseByteSize(value: unknown): number | undefined {\n if (typeof value === 'number') {\n if (value === Number.POSITIVE_INFINITY) return value\n return Number.isSafeInteger(value) && value >= 0 ? value : undefined\n }\n if (typeof value !== 'string') return undefined\n const match = SIZE.exec(value.trim())\n if (match === null) return undefined\n const unit = (match[2] as string).toLowerCase()\n const multiplier = unit === '' ? 1 : UNITS[unit]\n if (multiplier === undefined) return undefined\n const bytes = Math.floor(Number(match[1]) * multiplier)\n return Number.isSafeInteger(bytes) ? bytes : undefined\n}\n\n/** Human-readable size for messages: `5 MiB`, `1.5 KiB`, `512 bytes`, `unlimited`. */\nexport function formatByteSize(bytes: number): string {\n if (bytes === Number.POSITIVE_INFINITY) return 'unlimited'\n for (const [unit, size] of [\n ['TiB', 1024 ** 4],\n ['GiB', 1024 ** 3],\n ['MiB', 1024 ** 2],\n ['KiB', 1024],\n ] as const) {\n if (bytes >= size) return `${Number((bytes / size).toFixed(2))} ${unit}`\n }\n return bytes === 1 ? '1 byte' : `${bytes} bytes`\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
/** Internal helpers (not part of the public API). */
|
|
3
|
+
/** Whether `value` is a promise-like object (has a callable `then`). */
|
|
4
|
+
function isThenable(value) {
|
|
5
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
6
|
+
}
|
|
7
|
+
/** Message of any thrown value. */
|
|
8
|
+
function errorMessage(error) {
|
|
9
|
+
if (error instanceof Error) return error.message;
|
|
10
|
+
if (typeof error === "string") return error;
|
|
11
|
+
try {
|
|
12
|
+
return String(error);
|
|
13
|
+
} catch {
|
|
14
|
+
return Object.prototype.toString.call(error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** A `Record` without prototype (safe for user-controlled keys such as `__proto__`). */
|
|
18
|
+
function emptyRecord() {
|
|
19
|
+
return Object.create(null);
|
|
20
|
+
}
|
|
21
|
+
/** Whether the code runs on Bun. */
|
|
22
|
+
function isBun() {
|
|
23
|
+
return typeof globalThis.Bun !== "undefined";
|
|
24
|
+
}
|
|
25
|
+
export { isThenable as i, errorMessage as n, isBun as r, emptyRecord as t };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=utils-DCbu3a4w.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-DCbu3a4w.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/** Internal helpers (not part of the public API). */\n\n/** Whether `value` is a promise-like object (has a callable `then`). */\nexport function isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === 'object' || typeof value === 'function') &&\n typeof (value as { then?: unknown }).then === 'function'\n )\n}\n\n/** Message of any thrown value. */\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message\n if (typeof error === 'string') return error\n try {\n return String(error)\n } catch {\n return Object.prototype.toString.call(error)\n }\n}\n\n/** A `Record` without prototype (safe for user-controlled keys such as `__proto__`). */\nexport function emptyRecord<V>(): Record<string, V> {\n return Object.create(null) as Record<string, V>\n}\n\n/** Whether the code runs on Bun. */\nexport function isBun(): boolean {\n return typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'\n}\n"],"mappings":";;;AAGA,SAAgB,WAAW,OAA+C;CACxE,OACE,UAAU,SACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;;AAGA,SAAgB,aAAa,OAAwB;CACnD,IAAI,iBAAiB,OAAO,OAAO,MAAM;CACzC,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,OAAO,OAAO,KAAK;CACrB,QAAQ;EACN,OAAO,OAAO,UAAU,SAAS,KAAK,KAAK;CAC7C;AACF;;AAGA,SAAgB,cAAoC;CAClD,OAAO,OAAO,OAAO,IAAI;AAC3B;;AAGA,SAAgB,QAAiB;CAC/B,OAAO,OAAQ,WAAiC,QAAQ;AAC1D"}
|