@orochibraru/svelte-smol 1.8.3 → 1.8.4
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/README.md +1 -0
- package/dist/kit3/index.js +7 -2
- package/dist/kit3/templates/routes-util.ts +80 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ implementation. Upgrading SvelteKit is enough to switch.
|
|
|
42
42
|
| ------------------- | --------------------------------------- | ---------------------------------------------- |
|
|
43
43
|
| Adapter config | `svelte.config.js` | `vite.config.js`, passed to `sveltekit()` |
|
|
44
44
|
| Static assets | `client/` and `prerendered/` beside it | embedded in the executable |
|
|
45
|
+
| `Range` requests | left to Bun, `If-Range` is ignored | `206` / `416` and `If-Range`, embedded or not |
|
|
45
46
|
| Build options | top level (`compile`, `target`, …) | under `buildOptions` |
|
|
46
47
|
| `Bun.serve` options | `serveOptions` | `serverOptions` (JSON-serializable only) |
|
|
47
48
|
| Public origin | `ORIGIN` env var | `ORIGIN` env var, else `paths.origin` |
|
package/dist/kit3/index.js
CHANGED
|
@@ -44,11 +44,16 @@ async function hash_file(file) {
|
|
|
44
44
|
file_waiters.shift()?.();
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Bun only generates ETags for in-memory static routes, so ship our own; the
|
|
49
|
+
* size is what byte ranges resolve against.
|
|
50
|
+
*/
|
|
48
51
|
async function asset_meta(file, precompress = false) {
|
|
52
|
+
const asset = Bun.file(file);
|
|
49
53
|
const meta = {
|
|
50
54
|
hash: await hash_file(file),
|
|
51
|
-
mtime:
|
|
55
|
+
mtime: asset.lastModified,
|
|
56
|
+
size: asset.size,
|
|
52
57
|
};
|
|
53
58
|
if (precompress) {
|
|
54
59
|
if (fs.existsSync(`${file}.br`))
|
|
@@ -4,7 +4,13 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import type { BunRequest, Serve } from "bun";
|
|
5
5
|
|
|
6
6
|
type RouteHandler = Serve.Routes<undefined, string>[string];
|
|
7
|
-
type AssetMeta = {
|
|
7
|
+
type AssetMeta = {
|
|
8
|
+
hash: string;
|
|
9
|
+
mtime: number;
|
|
10
|
+
size: number;
|
|
11
|
+
br?: boolean;
|
|
12
|
+
gz?: boolean;
|
|
13
|
+
};
|
|
8
14
|
|
|
9
15
|
// not Bun.main: when the built server is imported from a wrapper script rather than
|
|
10
16
|
// run directly, Bun.main is the wrapper and every asset path resolves wrong
|
|
@@ -85,6 +91,46 @@ function negotiate(accept: string | null, meta: AssetMeta): "br" | "gz" | null {
|
|
|
85
91
|
return null;
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
/**
|
|
95
|
+
* One byte range, clamped to the file (RFC 9110 §14.1.2). `null` means the header
|
|
96
|
+
* is ignored: malformed, another unit, or several ranges (no multipart/byteranges).
|
|
97
|
+
*/
|
|
98
|
+
export function parse_range(
|
|
99
|
+
header: string,
|
|
100
|
+
size: number,
|
|
101
|
+
): { start: number; end: number } | "unsatisfiable" | null {
|
|
102
|
+
const match = /^bytes=(\d*)-(\d*)$/i.exec(header.trim());
|
|
103
|
+
if (!match) return null;
|
|
104
|
+
const [, first = "", last = ""] = match;
|
|
105
|
+
|
|
106
|
+
if (first === "") {
|
|
107
|
+
if (last === "") return null;
|
|
108
|
+
const length = Number(last);
|
|
109
|
+
if (length === 0 || size === 0) return "unsatisfiable";
|
|
110
|
+
return { start: Math.max(0, size - length), end: size - 1 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const start = Number(first);
|
|
114
|
+
if (last !== "" && Number(last) < start) return null;
|
|
115
|
+
if (start >= size) return "unsatisfiable";
|
|
116
|
+
return {
|
|
117
|
+
start,
|
|
118
|
+
end: last === "" ? size - 1 : Math.min(Number(last), size - 1),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** If-Range takes a strong ETag or an HTTP date, never a weak validator (RFC 9110 §13.1.5). */
|
|
123
|
+
function range_applies(request: Request, etag: string, mtime: number) {
|
|
124
|
+
const header = request.headers.get("if-range");
|
|
125
|
+
if (header === null) return true;
|
|
126
|
+
if (header.startsWith('"') || header.startsWith("W/")) return header === etag;
|
|
127
|
+
|
|
128
|
+
const date = Date.parse(header);
|
|
129
|
+
return (
|
|
130
|
+
Number.isFinite(date) && Math.trunc(mtime / 1000) <= Math.trunc(date / 1000)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
88
134
|
function route_entries(
|
|
89
135
|
paths: string[],
|
|
90
136
|
route: RouteHandler,
|
|
@@ -102,9 +148,10 @@ function file_route(
|
|
|
102
148
|
const last_modified = new Date(meta.mtime).toUTCString();
|
|
103
149
|
|
|
104
150
|
const handler = (request: BunRequest) => {
|
|
105
|
-
//
|
|
151
|
+
// ranges apply to the identity representation
|
|
152
|
+
const range_header = request.headers.get("range");
|
|
106
153
|
const encoding =
|
|
107
|
-
|
|
154
|
+
range_header === null
|
|
108
155
|
? negotiate(request.headers.get("accept-encoding"), meta)
|
|
109
156
|
: null;
|
|
110
157
|
const etag =
|
|
@@ -115,6 +162,7 @@ function file_route(
|
|
|
115
162
|
...extra_headers,
|
|
116
163
|
etag,
|
|
117
164
|
"last-modified": last_modified,
|
|
165
|
+
"accept-ranges": "bytes",
|
|
118
166
|
};
|
|
119
167
|
if (meta.br || meta.gz) response_headers.vary = "accept-encoding";
|
|
120
168
|
|
|
@@ -122,12 +170,38 @@ function file_route(
|
|
|
122
170
|
return new Response(null, { status: 304, headers: response_headers });
|
|
123
171
|
}
|
|
124
172
|
|
|
125
|
-
let body_file = file;
|
|
126
173
|
if (encoding !== null) {
|
|
127
174
|
response_headers["content-encoding"] = CONTENT_ENCODING[encoding];
|
|
128
|
-
|
|
175
|
+
return new Response(Bun.file(`${file}.${encoding}`), {
|
|
176
|
+
headers: response_headers,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (range_header === null) {
|
|
180
|
+
return new Response(Bun.file(file), { headers: response_headers });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const range = range_applies(request, etag, meta.mtime)
|
|
184
|
+
? parse_range(range_header, meta.size)
|
|
185
|
+
: null;
|
|
186
|
+
if (range === null) {
|
|
187
|
+
// Bun applies Range itself to an on-disk file body (not to an embedded one) and
|
|
188
|
+
// knows nothing of If-Range: piping hides the file so the header stays ignored
|
|
189
|
+
return new Response(
|
|
190
|
+
Bun.file(file).stream().pipeThrough(new TransformStream()),
|
|
191
|
+
{ headers: response_headers },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (range === "unsatisfiable") {
|
|
195
|
+
response_headers["content-range"] = `bytes */${meta.size}`;
|
|
196
|
+
return new Response(null, { status: 416, headers: response_headers });
|
|
129
197
|
}
|
|
130
|
-
|
|
198
|
+
// an explicit 206 with Content-Range is sent as is, Content-Length being the slice's
|
|
199
|
+
response_headers["content-range"] =
|
|
200
|
+
`bytes ${range.start}-${range.end}/${meta.size}`;
|
|
201
|
+
return new Response(Bun.file(file).slice(range.start, range.end + 1), {
|
|
202
|
+
status: 206,
|
|
203
|
+
headers: response_headers,
|
|
204
|
+
});
|
|
131
205
|
};
|
|
132
206
|
|
|
133
207
|
return { GET: handler };
|