@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 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` |
@@ -44,11 +44,16 @@ async function hash_file(file) {
44
44
  file_waiters.shift()?.();
45
45
  }
46
46
  }
47
- /** Bun only generates ETags for in-memory static routes, so ship our own. */
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: Bun.file(file).lastModified,
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 = { hash: string; mtime: number; br?: boolean; gz?: boolean };
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
- // Bun serializes Range itself for file bodies; ranges apply to the identity representation
151
+ // ranges apply to the identity representation
152
+ const range_header = request.headers.get("range");
106
153
  const encoding =
107
- request.headers.get("range") === null
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
- body_file = `${file}.${encoding}`;
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
- return new Response(Bun.file(body_file), { headers: response_headers });
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orochibraru/svelte-smol",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "keywords": [