@orochibraru/svelte-smol 1.8.0 → 1.8.2

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.
@@ -0,0 +1,133 @@
1
+ import { env_prefix } from "MANIFEST";
2
+ import process from "node:process";
3
+
4
+ const expected = new Set([
5
+ "SOCKET_PATH",
6
+ "HOST",
7
+ "PORT",
8
+ "REUSE_PORT",
9
+ "IPV6_ONLY",
10
+ "CONNECTION_IDLE_TIMEOUT",
11
+ "BODY_SIZE_LIMIT",
12
+ "SHUTDOWN_TIMEOUT",
13
+ "DEVELOPMENT",
14
+ "XFF_DEPTH",
15
+ "ADDRESS_HEADER",
16
+ "PROTOCOL_HEADER",
17
+ "HOST_HEADER",
18
+ "PORT_HEADER",
19
+ "HEALTHCHECK_PATH",
20
+ "HEALTHCHECK_TIMEOUT",
21
+ ]);
22
+
23
+ if (env_prefix) {
24
+ for (const name in process.env) {
25
+ if (
26
+ name.startsWith(env_prefix) &&
27
+ !expected.has(name.slice(env_prefix.length))
28
+ ) {
29
+ throw new Error(
30
+ `You should change envPrefix (${env_prefix}) to avoid conflicts with existing environment variables — unexpectedly saw ${name}`,
31
+ );
32
+ }
33
+ }
34
+ }
35
+
36
+ function parsing_error(name: string, value: string, expected: string): never {
37
+ throw new Error(
38
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected ${expected})`,
39
+ );
40
+ }
41
+
42
+ export function env<T extends string | undefined = undefined>(
43
+ name: string,
44
+ fallback?: T,
45
+ ): string | T {
46
+ const prefixed = env_prefix + name;
47
+ return prefixed in process.env
48
+ ? (process.env[prefixed] as string)
49
+ : (fallback as T);
50
+ }
51
+
52
+ const BOOLEANS: Record<string, boolean> = {
53
+ 1: true,
54
+ true: true,
55
+ yes: true,
56
+ on: true,
57
+ 0: false,
58
+ false: false,
59
+ no: false,
60
+ off: false,
61
+ };
62
+
63
+ export function boolean_env<T extends boolean | undefined = undefined>(
64
+ name: string,
65
+ fallback?: T,
66
+ ): boolean | T {
67
+ const value = env(name);
68
+ if (value === undefined) return fallback as T;
69
+ return (
70
+ BOOLEANS[value.toLowerCase()] ?? parsing_error(name, value, "a boolean")
71
+ );
72
+ }
73
+
74
+ export function number_env<T extends number | undefined = undefined>(
75
+ name: string,
76
+ fallback?: T,
77
+ limits: { min?: number; max?: number } = {},
78
+ ): number | T {
79
+ const value = env(name);
80
+ if (value === undefined) return fallback as T;
81
+ if (!/^\d+$/.test(value)) {
82
+ parsing_error(name, value, "a non-negative integer");
83
+ }
84
+
85
+ const number = Number(value);
86
+ if (
87
+ !Number.isSafeInteger(number) ||
88
+ number < (limits.min ?? 0) ||
89
+ number > (limits.max ?? Number.POSITIVE_INFINITY)
90
+ ) {
91
+ const range =
92
+ limits.max === undefined
93
+ ? `at least ${limits.min ?? 0}`
94
+ : `between ${limits.min ?? 0} and ${limits.max}`;
95
+ parsing_error(name, value, `an integer ${range}`);
96
+ }
97
+
98
+ return number;
99
+ }
100
+
101
+ export function bytes_env<T extends number | undefined = undefined>(
102
+ name: string,
103
+ fallback?: T,
104
+ ): number | T {
105
+ const value = env(name);
106
+ if (value === undefined) return fallback as T;
107
+ // adapter-node documents Infinity as the value that disables the limit
108
+ if (value === "Infinity") return Number.POSITIVE_INFINITY;
109
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:[KMG])?$/i.test(value)) {
110
+ parsing_error(
111
+ name,
112
+ value,
113
+ "a non-negative number with an optional K, M, or G suffix, or Infinity",
114
+ );
115
+ }
116
+
117
+ const suffix = value.at(-1)?.toUpperCase() as string;
118
+ const multiplier =
119
+ (
120
+ { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024 } as Record<
121
+ string,
122
+ number
123
+ >
124
+ )[suffix] ?? 1;
125
+ const number =
126
+ Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier;
127
+
128
+ if (!Number.isSafeInteger(number)) {
129
+ parsing_error(name, value, "a non-negative number of whole bytes");
130
+ }
131
+
132
+ return number;
133
+ }
@@ -0,0 +1,135 @@
1
+ import { app_dir, base, env_prefix, origin } from "MANIFEST";
2
+ import { server_assets } from "ROUTES";
3
+ import { server } from "SERVER";
4
+ import type { Server as BunServer } from "bun";
5
+ import { env, number_env } from "./env.ts";
6
+
7
+ const address_header = env("ADDRESS_HEADER", "").toLowerCase();
8
+ const protocol_header = env("PROTOCOL_HEADER", "").toLowerCase();
9
+ const host_header = env("HOST_HEADER", "").toLowerCase();
10
+ const port_header = env("PORT_HEADER", "").toLowerCase();
11
+ const xff_depth = number_env("XFF_DEPTH", 1, { min: 1 });
12
+
13
+ await server.init({
14
+ env: Bun.env as Record<string, string>,
15
+ read: (file) => server_assets.get(file)?.stream() ?? null,
16
+ });
17
+
18
+ /** The Bun-native SvelteKit request handler used by the generated server. */
19
+ export async function handler(
20
+ request: Request,
21
+ bun_server: BunServer<undefined>,
22
+ ): Promise<Response> {
23
+ const normalized_request = normalize_request(request);
24
+ if (normalized_request instanceof Response) return normalized_request;
25
+
26
+ const response = await server.respond(normalized_request, {
27
+ platform: { server: bun_server },
28
+ getClientAddress: () => get_client_address(request, bun_server),
29
+ });
30
+
31
+ if (response.headers.get("content-type")?.startsWith("text/event-stream")) {
32
+ bun_server.timeout(request, 0);
33
+ response.headers.set("x-accel-buffering", "no");
34
+ }
35
+
36
+ return response;
37
+ }
38
+
39
+ /** Rewrites the request onto the public origin the user actually requested. */
40
+ function normalize_request(request: Request): Request | Response {
41
+ try {
42
+ // an empty Host header makes request.url relative, so parsing belongs in the try
43
+ const url = new URL(request.url);
44
+ const request_origin =
45
+ same_host_remote_origin(request, url) ||
46
+ origin ||
47
+ get_origin(request, url);
48
+ return request_origin === url.origin
49
+ ? request
50
+ : new Request(request_origin + url.pathname + url.search, request);
51
+ } catch (error) {
52
+ console.error(
53
+ `Could not determine request origin: ${error instanceof Error ? error.message : String(error)}`,
54
+ );
55
+ return new Response("Bad Request", { status: 400 });
56
+ }
57
+ }
58
+
59
+ function get_origin(request: Request, url: URL): string {
60
+ // assume TLS terminates upstream, like adapter-node; an http origin would fail CSRF checks
61
+ const protocol = decodeURIComponent(
62
+ (protocol_header && request.headers.get(protocol_header)) || "https",
63
+ );
64
+ if (!/^https?$/i.test(protocol)) {
65
+ throw new Error(
66
+ `The ${protocol_header} header specified ${protocol} which is an invalid protocol scheme. It should only contain the protocol scheme (e.g. \`https\`)`,
67
+ );
68
+ }
69
+
70
+ const host =
71
+ (host_header && request.headers.get(host_header)) ||
72
+ (request.headers.get("host") ?? url.host);
73
+ if (!host) {
74
+ throw new Error(
75
+ `Could not determine host from the ${host_header ? `${host_header} or ` : ""}host header`,
76
+ );
77
+ }
78
+
79
+ const port = port_header ? request.headers.get(port_header) : null;
80
+ if (port && Number.isNaN(+port)) {
81
+ throw new Error(
82
+ `The ${port_header} header specified ${port} which is an invalid port because it is not a number. The value should only contain the port number (e.g. 443)`,
83
+ );
84
+ }
85
+
86
+ // canonicalized so the caller's comparison with url.origin matches (case, default ports)
87
+ return new URL(`${protocol}://${host}${port ? `:${port}` : ""}`).origin;
88
+ }
89
+
90
+ function get_client_address(
91
+ request: Request,
92
+ bun_server: BunServer<undefined>,
93
+ ): string {
94
+ if (!address_header) {
95
+ // requestIP() is null over unix sockets; undefined matches adapter-node
96
+ return bun_server.requestIP(request)?.address as string;
97
+ }
98
+
99
+ const value = request.headers.get(address_header);
100
+ if (value === null) {
101
+ throw new Error(
102
+ `Address header was specified with ${env_prefix}ADDRESS_HEADER=${address_header} but is absent from request`,
103
+ );
104
+ }
105
+ if (address_header !== "x-forwarded-for") return value;
106
+
107
+ const addresses = value.split(",");
108
+ if (xff_depth > addresses.length) {
109
+ throw new Error(
110
+ `${env_prefix}XFF_DEPTH is ${xff_depth}, but only found ${addresses.length} addresses`,
111
+ );
112
+ }
113
+ return (addresses[addresses.length - xff_depth] as string).trim();
114
+ }
115
+
116
+ // Remote function calls are CSRF-checked against the request origin. When the
117
+ // browser's Origin header matches the Host, use it instead of `paths.origin`.
118
+ function same_host_remote_origin(request: Request, url: URL) {
119
+ const request_origin = request.headers.get("origin");
120
+ if (
121
+ !request_origin ||
122
+ !url.pathname.startsWith(`${base}/${app_dir}/remote/`)
123
+ ) {
124
+ return null;
125
+ }
126
+ try {
127
+ const parsed = new URL(request_origin);
128
+ const host =
129
+ (host_header && request.headers.get(host_header)) ||
130
+ request.headers.get("host");
131
+ return parsed.host === host ? parsed.origin : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
@@ -0,0 +1,31 @@
1
+ import { healthcheck_path } from "MANIFEST";
2
+ import server_options from "SERVER_OPTIONS";
3
+ import process from "node:process";
4
+ import { env, number_env } from "./env.ts";
5
+
6
+ const unix = env("SOCKET_PATH", server_options.unix);
7
+ const timeout = number_env("HEALTHCHECK_TIMEOUT", 2000);
8
+ const path = env("HEALTHCHECK_PATH", healthcheck_path || "/_health");
9
+
10
+ // a server bound to a wildcard address is reached over loopback
11
+ const raw_host = env("HOST", server_options.hostname) ?? "";
12
+ const host = ["", "0.0.0.0", "::"].includes(raw_host) ? "127.0.0.1" : raw_host;
13
+ const port = env("PORT", server_options.port?.toString()) ?? 3000;
14
+
15
+ const url = unix
16
+ ? `http://localhost${path}`
17
+ : `http://${host.includes(":") ? `[${host}]` : host}:${port}${path}`;
18
+
19
+ try {
20
+ const response = await fetch(url, {
21
+ signal: AbortSignal.timeout(timeout),
22
+ ...(unix ? { unix } : {}),
23
+ });
24
+ if (!response.ok) throw new Error(`status ${response.status}`);
25
+ process.exit(0);
26
+ } catch (error) {
27
+ console.error(
28
+ `unhealthy: ${url} -> ${error instanceof Error ? error.message : String(error)}`,
29
+ );
30
+ process.exit(1);
31
+ }
@@ -0,0 +1,114 @@
1
+ import { healthcheck_path } from "MANIFEST";
2
+ import { routes } from "ROUTES";
3
+ import server_options from "SERVER_OPTIONS";
4
+ import fs from "node:fs";
5
+ import process from "node:process";
6
+ import type { Serve } from "bun";
7
+ import { boolean_env, bytes_env, env, number_env } from "./env.ts";
8
+ import { handler } from "./handler.ts";
9
+
10
+ // nothing enforces `engines.bun` at install time, and the build may have run on a newer Bun.
11
+ // order() rather than satisfies(): a range would reject canary builds such as 1.5.0-canary.1
12
+ if (Bun.semver.order(Bun.version, "1.4.0") < 0) {
13
+ throw new Error(
14
+ `@orochibraru/svelte-smol requires Bun 1.4 or newer, but this is Bun ${Bun.version}`,
15
+ );
16
+ }
17
+
18
+ const options = { ...server_options } as Serve.Options<undefined, string>;
19
+
20
+ const unix = env("SOCKET_PATH", options.unix);
21
+
22
+ if (unix) {
23
+ options.unix = unix;
24
+ delete options.hostname;
25
+ delete options.port;
26
+ delete options.reusePort;
27
+ delete options.ipv6Only;
28
+
29
+ // an unclean shutdown leaves the socket file behind and the next listen would
30
+ // fail with EADDRINUSE; the zero-size check (same heuristic as adapter-node)
31
+ // avoids deleting a regular file that happens to sit at this path
32
+ try {
33
+ if (fs.statSync(unix).size === 0) fs.rmSync(unix);
34
+ } catch {
35
+ // ignore
36
+ }
37
+ } else {
38
+ delete options.unix;
39
+ options.hostname = env("HOST", options.hostname);
40
+ // always set an explicit port: left undefined, Bun.serve reads the unprefixed
41
+ // BUN_PORT/PORT/NODE_PORT itself, bypassing envPrefix isolation
42
+ options.port = env("PORT", options.port?.toString()) ?? 3000;
43
+ options.reusePort = boolean_env("REUSE_PORT", options.reusePort);
44
+ options.ipv6Only = boolean_env("IPV6_ONLY", options.ipv6Only);
45
+ }
46
+
47
+ // not IDLE_TIMEOUT: that name means idle-shutdown seconds on adapter-node, and a
48
+ // carried-over value would crash on the 255 cap or silently kill slow requests
49
+ options.idleTimeout = number_env(
50
+ "CONNECTION_IDLE_TIMEOUT",
51
+ options.idleTimeout,
52
+ { max: 255 },
53
+ );
54
+ options.development =
55
+ boolean_env("DEVELOPMENT") ?? options.development ?? false;
56
+ options.maxRequestBodySize = bytes_env(
57
+ "BODY_SIZE_LIMIT",
58
+ options.maxRequestBodySize ?? 512 * 1024,
59
+ );
60
+
61
+ const shutdown_timeout = number_env("SHUTDOWN_TIMEOUT", 30);
62
+
63
+ options.fetch = handler;
64
+ options.routes = healthcheck_path
65
+ ? {
66
+ ...routes,
67
+ [healthcheck_path]: {
68
+ GET: () =>
69
+ Response.json(
70
+ { status: "ok", uptime: Math.round(process.uptime()) },
71
+ { headers: { "cache-control": "no-store" } },
72
+ ),
73
+ },
74
+ }
75
+ : routes;
76
+
77
+ const server = Bun.serve(options);
78
+
79
+ console.log(unix ? `Listening on ${unix}` : `Listening on ${server.url}`);
80
+
81
+ let shutting_down = false;
82
+
83
+ async function graceful_shutdown(reason: "SIGINT" | "SIGTERM") {
84
+ if (shutting_down) return process.exit(1);
85
+ shutting_down = true;
86
+
87
+ if (server.pendingRequests !== 0) {
88
+ console.log(
89
+ `Waiting for ${server.pendingRequests} requests to finish before shutting down...\n` +
90
+ "Press Ctrl+C again to force shutdown.",
91
+ );
92
+ }
93
+
94
+ // stop() waits for in-flight requests, and an open event stream is one forever,
95
+ // so race the drain against the deadline. The timer must stay referenced: a
96
+ // draining server no longer holds the event loop open on its own
97
+ let deadline: ReturnType<typeof setTimeout> | undefined;
98
+ const drained = await Promise.race([
99
+ server.stop().then(() => true),
100
+ new Promise<boolean>((resolve) => {
101
+ deadline = setTimeout(() => resolve(false), shutdown_timeout * 1000);
102
+ }),
103
+ ]);
104
+ clearTimeout(deadline);
105
+
106
+ // force-close aborts the in-flight handlers, so shutdown listeners do not tear
107
+ // down resources those handlers still hold
108
+ if (!drained) await server.stop(true);
109
+
110
+ process.emit("sveltekit:shutdown" as "exit", reason as never);
111
+ }
112
+
113
+ process.on("SIGTERM", () => graceful_shutdown("SIGTERM"));
114
+ process.on("SIGINT", () => graceful_shutdown("SIGINT"));
@@ -0,0 +1,207 @@
1
+ import { app_dir, base, embed } from "MANIFEST";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { BunRequest, Serve } from "bun";
5
+
6
+ type RouteHandler = Serve.Routes<undefined, string>[string];
7
+ type AssetMeta = { hash: string; mtime: number; br?: boolean; gz?: boolean };
8
+
9
+ // not Bun.main: when the built server is imported from a wrapper script rather than
10
+ // run directly, Bun.main is the wrapper and every asset path resolves wrong
11
+ const dir = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ /** Embedded assets are imported by identity path; on-disk assets live next to this module. */
14
+ function resolve_file(subdir: string, filename: string) {
15
+ return embed ? filename : path.resolve(dir, subdir, filename);
16
+ }
17
+
18
+ const CONTENT_ENCODING = { br: "br", gz: "gzip" };
19
+
20
+ // the URL Standard's path percent-encode set, plus `%` and `\` so they stay literal
21
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are part of the encode set
22
+ const ESCAPED_PATH_CHAR = /[\u0000-\u001f\u007f-\u{10ffff} "#<>?`{}%\\]/gu;
23
+
24
+ /**
25
+ * WHATWG path serialization: the exact bytes user agents put on the wire, because
26
+ * Bun matches route keys against the raw request pathname.
27
+ */
28
+ function encode_pathname(pathname: string) {
29
+ return pathname.replace(ESCAPED_PATH_CHAR, (char) =>
30
+ encodeURIComponent(char),
31
+ );
32
+ }
33
+
34
+ /** Registers a path both as user agents send it and fully percent-encoded. */
35
+ function route_paths(pathname: string) {
36
+ const minimal = encode_pathname(pathname);
37
+ const full = pathname.split("/").map(encodeURIComponent).join("/");
38
+ return minimal === full ? [minimal] : [minimal, full];
39
+ }
40
+
41
+ function to_paths(url: string) {
42
+ return route_paths(path.posix.join(base, url));
43
+ }
44
+
45
+ function to_directory_paths(url: string) {
46
+ const directory = `${path.posix.join(base, url).replace(/\/$/, "")}/`;
47
+ const paths = route_paths(directory);
48
+ if (directory !== "/") {
49
+ paths.push(...route_paths(directory.slice(0, -1)));
50
+ }
51
+ return paths;
52
+ }
53
+
54
+ /**
55
+ * If-None-Match takes precedence over If-Modified-Since (RFC 9110 §13.1.3);
56
+ * dates compare at whole-second precision because HTTP dates have none finer.
57
+ */
58
+ function is_fresh(request: Request, etag: string, mtime: number) {
59
+ const header = request.headers.get("if-none-match");
60
+ if (header !== null) {
61
+ return header
62
+ .split(",")
63
+ .some((value) => ["*", etag].includes(value.trim().replace(/^W\//, "")));
64
+ }
65
+
66
+ const since = Date.parse(request.headers.get("if-modified-since") ?? "");
67
+ return (
68
+ Number.isFinite(since) &&
69
+ Math.trunc(mtime / 1000) <= Math.trunc(since / 1000)
70
+ );
71
+ }
72
+
73
+ function negotiate(accept: string | null, meta: AssetMeta): "br" | "gz" | null {
74
+ if (accept === null || (!meta.br && !meta.gz)) return null;
75
+
76
+ const accepted = new Set<string>();
77
+ for (const part of accept.split(",")) {
78
+ const [name = "", ...params] = part.trim().toLowerCase().split(";");
79
+ if (params.some((param) => /^q=0(\.0*)?$/.test(param.trim()))) continue;
80
+ accepted.add(name.trim());
81
+ }
82
+
83
+ if (meta.br && (accepted.has("br") || accepted.has("*"))) return "br";
84
+ if (meta.gz && (accepted.has("gzip") || accepted.has("*"))) return "gz";
85
+ return null;
86
+ }
87
+
88
+ function route_entries(
89
+ paths: string[],
90
+ route: RouteHandler,
91
+ ): Array<[string, RouteHandler]> {
92
+ return paths.map((route_path) => [route_path, route]);
93
+ }
94
+
95
+ /** Serves one file with its build-time validator and precompressed variants. */
96
+ function file_route(
97
+ file: string,
98
+ meta: AssetMeta,
99
+ extra_headers: Record<string, string> = {},
100
+ ): RouteHandler {
101
+ const content_type = Bun.file(file).type;
102
+ const last_modified = new Date(meta.mtime).toUTCString();
103
+
104
+ const handler = (request: BunRequest) => {
105
+ // Bun serializes Range itself for file bodies; ranges apply to the identity representation
106
+ const encoding =
107
+ request.headers.get("range") === null
108
+ ? negotiate(request.headers.get("accept-encoding"), meta)
109
+ : null;
110
+ const etag =
111
+ encoding === null ? `"${meta.hash}"` : `"${meta.hash}-${encoding}"`;
112
+
113
+ const response_headers: Record<string, string> = {
114
+ "content-type": content_type,
115
+ ...extra_headers,
116
+ etag,
117
+ "last-modified": last_modified,
118
+ };
119
+ if (meta.br || meta.gz) response_headers.vary = "accept-encoding";
120
+
121
+ if (is_fresh(request, etag, meta.mtime)) {
122
+ return new Response(null, { status: 304, headers: response_headers });
123
+ }
124
+
125
+ let body_file = file;
126
+ if (encoding !== null) {
127
+ response_headers["content-encoding"] = CONTENT_ENCODING[encoding];
128
+ body_file = `${file}.${encoding}`;
129
+ }
130
+ return new Response(Bun.file(body_file), { headers: response_headers });
131
+ };
132
+
133
+ return { GET: handler };
134
+ }
135
+
136
+ export function client_asset(
137
+ url: string,
138
+ filename: string | undefined,
139
+ meta: AssetMeta,
140
+ ) {
141
+ const immutable = url.startsWith(`${app_dir}/immutable/`);
142
+ const route = file_route(
143
+ resolve_file("client", filename ?? url),
144
+ meta,
145
+ immutable ? { "cache-control": "public,max-age=31536000,immutable" } : {},
146
+ );
147
+
148
+ const paths = to_paths(url);
149
+ if (url.endsWith("/index.html") || url === "index.html") {
150
+ paths.push(...to_directory_paths(url.slice(0, -"index.html".length)));
151
+ } else if (url.endsWith(".html")) {
152
+ // sirv also serves `page.html` at `/page`
153
+ paths.push(...to_paths(url.slice(0, -".html".length)));
154
+ }
155
+
156
+ return route_entries(paths, route);
157
+ }
158
+
159
+ export function server_asset(url: string, filename = url) {
160
+ return Bun.file(resolve_file("client", filename));
161
+ }
162
+
163
+ export function prerendered_asset(
164
+ url: string,
165
+ filename: string | undefined,
166
+ meta: AssetMeta,
167
+ ) {
168
+ const route = file_route(resolve_file("prerendered", filename ?? url), meta);
169
+ return route_entries(to_paths(url), route);
170
+ }
171
+
172
+ export function prerendered_page(
173
+ url: string,
174
+ filename: string,
175
+ meta: AssetMeta,
176
+ ) {
177
+ const route = file_route(resolve_file("prerendered", filename), meta);
178
+ // path already contains base, no need to add it here
179
+ const entries = route_entries(route_paths(url), route);
180
+
181
+ const inverted = url.endsWith("/") ? url.slice(0, -1) : `${url}/`;
182
+ if (inverted) {
183
+ const canonical = encode_pathname(url);
184
+ entries.push(
185
+ ...route_entries(route_paths(inverted), {
186
+ GET: (req: BunRequest) => {
187
+ const location = canonical + new URL(req.url).search;
188
+ return new Response(null, { status: 308, headers: { location } });
189
+ },
190
+ }),
191
+ );
192
+ }
193
+
194
+ return entries;
195
+ }
196
+
197
+ export function prerendered_redirect(
198
+ url: string,
199
+ status: number,
200
+ location: string,
201
+ ) {
202
+ const route = {
203
+ GET: new Response(null, { status, headers: { location } }),
204
+ };
205
+ // path already contains base, no need to add it here
206
+ return route_entries(route_paths(url), route);
207
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orochibraru/svelte-smol",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "keywords": [
@@ -33,32 +33,26 @@
33
33
  "access": "public"
34
34
  },
35
35
  "scripts": {
36
- "build": "rm -rf dist && tsc -p tsconfig.build.json && cp -R templates dist/templates",
36
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp -R kit2/templates dist/kit2/templates && cp -R kit3/templates dist/kit3/templates",
37
37
  "prepack": "bun run build",
38
- "typecheck": "tsc --noEmit -p tsconfig.json",
38
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p kit2 && tsc --noEmit -p kit3",
39
39
  "test": "bun test index.test.ts",
40
- "test:integration": "bun test test/integration.test.ts",
40
+ "test:integration": "bun test integration",
41
41
  "lint": "biome check .",
42
42
  "lint:fix": "biome check --write .",
43
- "release": "semantic-release",
44
- "release:dry": "semantic-release --dry-run --no-ci",
45
43
  "prepare": "husky"
46
44
  },
47
45
  "devDependencies": {
48
46
  "@biomejs/biome": "^2.5.11",
49
- "@semantic-release/github": "^12.0.9",
50
- "@semantic-release/npm": "^13.1.5",
51
47
  "@sveltejs/kit": "^2.70.3",
52
48
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
53
49
  "@types/bun": "^1.4.0",
54
- "conventional-changelog-conventionalcommits": "^8",
55
50
  "husky": "^9.1.7",
56
- "semantic-release": "^25.0.9",
57
51
  "svelte": "^5.56.10",
58
52
  "typescript": "^7",
59
53
  "vite": "^8.2.2"
60
54
  },
61
55
  "peerDependencies": {
62
- "@sveltejs/kit": "^2.0.0"
56
+ "@sveltejs/kit": "^2.0.0 || ^3.0.0-next.0"
63
57
  }
64
58
  }
File without changes
File without changes
File without changes