@fluixi/start 0.1.0-alpha.73 → 0.1.0-alpha.74
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/dist/adapter.cjs +748 -0
- package/dist/adapter.mjs +717 -0
- package/dist/adapters/entry.cjs +122 -0
- package/dist/adapters/entry.mjs +95 -0
- package/dist/adapters/platforms.cjs +311 -0
- package/dist/adapters/platforms.mjs +284 -0
- package/dist/api-RH6E5UTH.mjs +114 -0
- package/dist/api.cjs +145 -0
- package/dist/api.mjs +116 -0
- package/dist/commands/api-23MJFSYL.mjs +17 -0
- package/dist/commands/api-4IYNQRKG.mjs +16 -0
- package/dist/commands/api-RH6E5UTH.mjs +114 -0
- package/dist/commands/build.cjs +870 -0
- package/dist/commands/build.mjs +710 -0
- package/dist/commands/chunk-NS5GR2GX.mjs +115 -0
- package/dist/commands/chunk-OCPCK4ZJ.mjs +117 -0
- package/dist/commands/dev.cjs +663 -0
- package/dist/commands/dev.mjs +505 -0
- package/dist/commands/index.cjs +1138 -0
- package/dist/commands/index.mjs +975 -0
- package/dist/commands/prerender.cjs +402 -0
- package/dist/commands/prerender.mjs +375 -0
- package/dist/commands/start.cjs +461 -0
- package/dist/commands/start.mjs +426 -0
- package/dist/config.cjs +144 -0
- package/dist/config.mjs +108 -0
- package/dist/di.cjs +200 -0
- package/dist/di.mjs +169 -0
- package/dist/document.cjs +115 -0
- package/dist/document.mjs +86 -0
- package/dist/generated-api.cjs +43 -0
- package/dist/generated-api.mjs +18 -0
- package/dist/generated-server-fns.cjs +43 -0
- package/dist/generated-server-fns.mjs +18 -0
- package/dist/handler-core.cjs +211 -0
- package/dist/handler-core.mjs +185 -0
- package/dist/handler.cjs +181 -0
- package/dist/handler.mjs +150 -0
- package/dist/head.cjs +25 -0
- package/dist/head.mjs +4 -0
- package/dist/image.cjs +146 -0
- package/dist/image.mjs +120 -0
- package/dist/index.cjs +1036 -0
- package/dist/index.mjs +968 -0
- package/dist/interceptors.cjs +71 -0
- package/dist/interceptors.mjs +42 -0
- package/dist/internal.cjs +388 -0
- package/dist/internal.mjs +218 -0
- package/dist/middleware.cjs +58 -0
- package/dist/middleware.mjs +31 -0
- package/dist/preload.cjs +42 -0
- package/dist/preload.mjs +18 -0
- package/dist/router.cjs +25 -0
- package/dist/router.mjs +4 -0
- package/dist/server-fn-setup.cjs +46 -0
- package/dist/server-fn-setup.mjs +22 -0
- package/dist/server-fn.cjs +109 -0
- package/dist/server-fn.mjs +79 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/ui.cjs +123 -0
- package/dist/ui.mjs +93 -0
- package/package.json +45 -25
package/dist/handler.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/handler.ts
|
|
2
|
+
import { createServer as createHttpServer } from "node:http";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { join, extname } from "node:path";
|
|
7
|
+
function streamDocument(head, body, tail) {
|
|
8
|
+
const enc = new TextEncoder();
|
|
9
|
+
const stream = new ReadableStream({
|
|
10
|
+
async start(controller) {
|
|
11
|
+
try {
|
|
12
|
+
controller.enqueue(enc.encode(head));
|
|
13
|
+
const reader = body.getReader();
|
|
14
|
+
for (; ; ) {
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done) break;
|
|
17
|
+
controller.enqueue(value);
|
|
18
|
+
}
|
|
19
|
+
controller.enqueue(enc.encode(tail));
|
|
20
|
+
controller.close();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
controller.error(err);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return new Response(stream, {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function createRequestHandler(render) {
|
|
32
|
+
return async (request) => {
|
|
33
|
+
const url = new URL(request.url);
|
|
34
|
+
try {
|
|
35
|
+
const html = await render(url.pathname + url.search, request);
|
|
36
|
+
return new Response(html, {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return new Response(String(e?.stack || e), {
|
|
42
|
+
status: 500,
|
|
43
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function nodeToRequest(req) {
|
|
49
|
+
const host = req.headers.host ?? "localhost";
|
|
50
|
+
const url = `http://${host}${req.url ?? "/"}`;
|
|
51
|
+
const headers = new Headers();
|
|
52
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
53
|
+
if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));
|
|
54
|
+
else if (v != null) headers.set(k, v);
|
|
55
|
+
}
|
|
56
|
+
const method = req.method ?? "GET";
|
|
57
|
+
const init = { method, headers };
|
|
58
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
59
|
+
init.body = req;
|
|
60
|
+
init.duplex = "half";
|
|
61
|
+
}
|
|
62
|
+
return new Request(url, init);
|
|
63
|
+
}
|
|
64
|
+
async function sendResponse(res, response) {
|
|
65
|
+
res.statusCode = response.status;
|
|
66
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
67
|
+
const body = response.body;
|
|
68
|
+
if (!body) {
|
|
69
|
+
res.end(await response.text().catch(() => ""));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const reader = body.getReader();
|
|
73
|
+
try {
|
|
74
|
+
for (; ; ) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done) break;
|
|
77
|
+
res.write(value);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
res.end();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function toNodeHandler(handler) {
|
|
84
|
+
return async (req, res) => {
|
|
85
|
+
const response = await handler(nodeToRequest(req));
|
|
86
|
+
await sendResponse(res, response);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function serveNode(handler, opts = {}) {
|
|
90
|
+
const port = opts.port ?? 3e3;
|
|
91
|
+
const node = toNodeHandler(handler);
|
|
92
|
+
const server = createHttpServer((req, res) => {
|
|
93
|
+
node(req, res).catch((e) => {
|
|
94
|
+
res.statusCode = 500;
|
|
95
|
+
res.end(String(e?.stack || e));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
server.listen(
|
|
99
|
+
port,
|
|
100
|
+
() => opts.onListen ? opts.onListen(port) : console.log(` fluixi → http://localhost:${port}`)
|
|
101
|
+
);
|
|
102
|
+
return server;
|
|
103
|
+
}
|
|
104
|
+
var MIME = {
|
|
105
|
+
".js": "text/javascript",
|
|
106
|
+
".mjs": "text/javascript",
|
|
107
|
+
".css": "text/css",
|
|
108
|
+
".html": "text/html",
|
|
109
|
+
".json": "application/json",
|
|
110
|
+
".svg": "image/svg+xml",
|
|
111
|
+
".png": "image/png",
|
|
112
|
+
".jpg": "image/jpeg",
|
|
113
|
+
".jpeg": "image/jpeg",
|
|
114
|
+
".webp": "image/webp",
|
|
115
|
+
".ico": "image/x-icon",
|
|
116
|
+
".woff": "font/woff",
|
|
117
|
+
".woff2": "font/woff2",
|
|
118
|
+
".map": "application/json"
|
|
119
|
+
};
|
|
120
|
+
function withStaticFiles(handler, dir) {
|
|
121
|
+
return async (request) => {
|
|
122
|
+
const { pathname } = new URL(request.url);
|
|
123
|
+
if (pathname !== "/" && extname(pathname)) {
|
|
124
|
+
const file = join(dir, decodeURIComponent(pathname));
|
|
125
|
+
try {
|
|
126
|
+
const s = await stat(file);
|
|
127
|
+
if (s.isFile()) {
|
|
128
|
+
const body = Readable.toWeb(createReadStream(file));
|
|
129
|
+
return new Response(body, {
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": MIME[extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
132
|
+
"content-length": String(s.size)
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return handler(request);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
createRequestHandler,
|
|
144
|
+
nodeToRequest,
|
|
145
|
+
sendResponse,
|
|
146
|
+
serveNode,
|
|
147
|
+
streamDocument,
|
|
148
|
+
toNodeHandler,
|
|
149
|
+
withStaticFiles
|
|
150
|
+
};
|
package/dist/head.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*! @fluixi/start v0.1.0-alpha.74 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __copyProps = (to, from, except, desc) => {
|
|
8
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
9
|
+
for (let key of __getOwnPropNames(from))
|
|
10
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
11
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
12
|
+
}
|
|
13
|
+
return to;
|
|
14
|
+
};
|
|
15
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
16
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
17
|
+
|
|
18
|
+
// src/head.ts
|
|
19
|
+
var head_exports = {};
|
|
20
|
+
module.exports = __toCommonJS(head_exports);
|
|
21
|
+
__reExport(head_exports, require("@fluixi/head"), module.exports);
|
|
22
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
23
|
+
0 && (module.exports = {
|
|
24
|
+
...require("@fluixi/head")
|
|
25
|
+
});
|
package/dist/head.mjs
ADDED
package/dist/image.cjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/image.ts
|
|
21
|
+
var image_exports = {};
|
|
22
|
+
__export(image_exports, {
|
|
23
|
+
createImageHandler: () => createImageHandler,
|
|
24
|
+
imageLoader: () => imageLoader
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(image_exports);
|
|
27
|
+
var import_promises = require("node:fs/promises");
|
|
28
|
+
var import_node_path = require("node:path");
|
|
29
|
+
var DEFAULT_SIZES = [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
30
|
+
var MIME = {
|
|
31
|
+
".jpg": "image/jpeg",
|
|
32
|
+
".jpeg": "image/jpeg",
|
|
33
|
+
".png": "image/png",
|
|
34
|
+
".webp": "image/webp",
|
|
35
|
+
".avif": "image/avif",
|
|
36
|
+
".gif": "image/gif",
|
|
37
|
+
".svg": "image/svg+xml"
|
|
38
|
+
};
|
|
39
|
+
var sharpModule;
|
|
40
|
+
async function loadSharp() {
|
|
41
|
+
if (sharpModule !== void 0) return sharpModule;
|
|
42
|
+
try {
|
|
43
|
+
const name = "sharp";
|
|
44
|
+
sharpModule = (await import(
|
|
45
|
+
/* @vite-ignore */
|
|
46
|
+
name
|
|
47
|
+
)).default;
|
|
48
|
+
} catch {
|
|
49
|
+
sharpModule = null;
|
|
50
|
+
}
|
|
51
|
+
return sharpModule;
|
|
52
|
+
}
|
|
53
|
+
var defaultTransform = async (input, { width, quality, format }) => {
|
|
54
|
+
const sharp = await loadSharp();
|
|
55
|
+
if (!sharp) return null;
|
|
56
|
+
const pipe = sharp(input).rotate().resize({ width, withoutEnlargement: true });
|
|
57
|
+
if (format === "avif") return { data: new Uint8Array(await pipe.avif({ quality }).toBuffer()), contentType: "image/avif" };
|
|
58
|
+
if (format === "webp") return { data: new Uint8Array(await pipe.webp({ quality }).toBuffer()), contentType: "image/webp" };
|
|
59
|
+
if (format === "png") return { data: new Uint8Array(await pipe.png().toBuffer()), contentType: "image/png" };
|
|
60
|
+
return { data: new Uint8Array(await pipe.jpeg({ quality, mozjpeg: true }).toBuffer()), contentType: "image/jpeg" };
|
|
61
|
+
};
|
|
62
|
+
function imageResponse(body, contentType, maxAge) {
|
|
63
|
+
return new Response(body, {
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": contentType,
|
|
66
|
+
"cache-control": `public, max-age=${maxAge}, immutable`,
|
|
67
|
+
vary: "Accept"
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function createImageHandler(options = {}) {
|
|
72
|
+
const root = (0, import_node_path.resolve)(options.root ?? (0, import_node_path.join)(process.cwd(), "public"));
|
|
73
|
+
const domains = new Set((options.domains ?? []).map((d) => d.toLowerCase()));
|
|
74
|
+
const sizes = options.sizes ?? DEFAULT_SIZES;
|
|
75
|
+
const maxQuality = options.quality ?? 80;
|
|
76
|
+
const maxAge = options.cacheMaxAge ?? 60 * 60 * 24 * 365;
|
|
77
|
+
const transform = options.transform ?? defaultTransform;
|
|
78
|
+
const cacheMax = options.cache === false ? 0 : typeof options.cache === "number" ? options.cache : 100;
|
|
79
|
+
const cache = /* @__PURE__ */ new Map();
|
|
80
|
+
return async (request) => {
|
|
81
|
+
const url = new URL(request.url);
|
|
82
|
+
const src = url.searchParams.get("url") ?? url.searchParams.get("src");
|
|
83
|
+
const width = Number(url.searchParams.get("w"));
|
|
84
|
+
const quality = Math.min(Math.max(Number(url.searchParams.get("q")) || maxQuality, 1), 100);
|
|
85
|
+
if (!src) return new Response('missing "url"', { status: 400 });
|
|
86
|
+
if (!Number.isInteger(width) || !sizes.includes(width)) return new Response('invalid "w"', { status: 400 });
|
|
87
|
+
const accept = request.headers.get("accept") ?? "";
|
|
88
|
+
const format = accept.includes("image/avif") ? "avif" : accept.includes("image/webp") ? "webp" : "original";
|
|
89
|
+
const key = `${src}|${width}|${quality}|${format}`;
|
|
90
|
+
const hit = cache.get(key);
|
|
91
|
+
if (hit) return imageResponse(hit.body, hit.contentType, maxAge);
|
|
92
|
+
let input;
|
|
93
|
+
let sourceType;
|
|
94
|
+
if (/^https?:\/\//i.test(src)) {
|
|
95
|
+
let host;
|
|
96
|
+
try {
|
|
97
|
+
host = new URL(src).hostname.toLowerCase();
|
|
98
|
+
} catch {
|
|
99
|
+
return new Response("bad url", { status: 400 });
|
|
100
|
+
}
|
|
101
|
+
if (!domains.has(host)) return new Response("domain not allowed", { status: 403 });
|
|
102
|
+
const res = await fetch(src);
|
|
103
|
+
if (!res.ok) return new Response("upstream error", { status: 502 });
|
|
104
|
+
input = new Uint8Array(await res.arrayBuffer());
|
|
105
|
+
sourceType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
106
|
+
} else {
|
|
107
|
+
const filePath = (0, import_node_path.resolve)(root, src.startsWith("/") ? src.slice(1) : src);
|
|
108
|
+
if (filePath !== root && !filePath.startsWith(root + import_node_path.sep)) return new Response("forbidden", { status: 403 });
|
|
109
|
+
try {
|
|
110
|
+
input = new Uint8Array(await (0, import_promises.readFile)(filePath));
|
|
111
|
+
} catch {
|
|
112
|
+
return new Response("not found", { status: 404 });
|
|
113
|
+
}
|
|
114
|
+
sourceType = MIME[(0, import_node_path.extname)(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
115
|
+
}
|
|
116
|
+
if (sourceType === "image/svg+xml" || sourceType === "image/gif") {
|
|
117
|
+
return imageResponse(input, sourceType, maxAge);
|
|
118
|
+
}
|
|
119
|
+
let out = null;
|
|
120
|
+
try {
|
|
121
|
+
out = await transform(input, { width, quality, format, contentType: sourceType });
|
|
122
|
+
} catch {
|
|
123
|
+
out = null;
|
|
124
|
+
}
|
|
125
|
+
const body = out?.data ?? input;
|
|
126
|
+
const contentType = out?.contentType ?? sourceType;
|
|
127
|
+
if (cacheMax > 0) {
|
|
128
|
+
if (cache.size >= cacheMax) cache.delete(cache.keys().next().value);
|
|
129
|
+
cache.set(key, { body, contentType });
|
|
130
|
+
}
|
|
131
|
+
return imageResponse(body, contentType, maxAge);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function imageLoader(options = {}) {
|
|
135
|
+
const path = options.path ?? "/api/image";
|
|
136
|
+
return ({ src, width, quality }) => {
|
|
137
|
+
const params = new URLSearchParams({ url: src, w: String(width) });
|
|
138
|
+
if (quality != null) params.set("q", String(quality));
|
|
139
|
+
return `${path}?${params.toString()}`;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
143
|
+
0 && (module.exports = {
|
|
144
|
+
createImageHandler,
|
|
145
|
+
imageLoader
|
|
146
|
+
});
|
package/dist/image.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/image.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve as resolvePath, join, extname, sep } from "node:path";
|
|
4
|
+
var DEFAULT_SIZES = [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
5
|
+
var MIME = {
|
|
6
|
+
".jpg": "image/jpeg",
|
|
7
|
+
".jpeg": "image/jpeg",
|
|
8
|
+
".png": "image/png",
|
|
9
|
+
".webp": "image/webp",
|
|
10
|
+
".avif": "image/avif",
|
|
11
|
+
".gif": "image/gif",
|
|
12
|
+
".svg": "image/svg+xml"
|
|
13
|
+
};
|
|
14
|
+
var sharpModule;
|
|
15
|
+
async function loadSharp() {
|
|
16
|
+
if (sharpModule !== void 0) return sharpModule;
|
|
17
|
+
try {
|
|
18
|
+
const name = "sharp";
|
|
19
|
+
sharpModule = (await import(
|
|
20
|
+
/* @vite-ignore */
|
|
21
|
+
name
|
|
22
|
+
)).default;
|
|
23
|
+
} catch {
|
|
24
|
+
sharpModule = null;
|
|
25
|
+
}
|
|
26
|
+
return sharpModule;
|
|
27
|
+
}
|
|
28
|
+
var defaultTransform = async (input, { width, quality, format }) => {
|
|
29
|
+
const sharp = await loadSharp();
|
|
30
|
+
if (!sharp) return null;
|
|
31
|
+
const pipe = sharp(input).rotate().resize({ width, withoutEnlargement: true });
|
|
32
|
+
if (format === "avif") return { data: new Uint8Array(await pipe.avif({ quality }).toBuffer()), contentType: "image/avif" };
|
|
33
|
+
if (format === "webp") return { data: new Uint8Array(await pipe.webp({ quality }).toBuffer()), contentType: "image/webp" };
|
|
34
|
+
if (format === "png") return { data: new Uint8Array(await pipe.png().toBuffer()), contentType: "image/png" };
|
|
35
|
+
return { data: new Uint8Array(await pipe.jpeg({ quality, mozjpeg: true }).toBuffer()), contentType: "image/jpeg" };
|
|
36
|
+
};
|
|
37
|
+
function imageResponse(body, contentType, maxAge) {
|
|
38
|
+
return new Response(body, {
|
|
39
|
+
headers: {
|
|
40
|
+
"content-type": contentType,
|
|
41
|
+
"cache-control": `public, max-age=${maxAge}, immutable`,
|
|
42
|
+
vary: "Accept"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function createImageHandler(options = {}) {
|
|
47
|
+
const root = resolvePath(options.root ?? join(process.cwd(), "public"));
|
|
48
|
+
const domains = new Set((options.domains ?? []).map((d) => d.toLowerCase()));
|
|
49
|
+
const sizes = options.sizes ?? DEFAULT_SIZES;
|
|
50
|
+
const maxQuality = options.quality ?? 80;
|
|
51
|
+
const maxAge = options.cacheMaxAge ?? 60 * 60 * 24 * 365;
|
|
52
|
+
const transform = options.transform ?? defaultTransform;
|
|
53
|
+
const cacheMax = options.cache === false ? 0 : typeof options.cache === "number" ? options.cache : 100;
|
|
54
|
+
const cache = /* @__PURE__ */ new Map();
|
|
55
|
+
return async (request) => {
|
|
56
|
+
const url = new URL(request.url);
|
|
57
|
+
const src = url.searchParams.get("url") ?? url.searchParams.get("src");
|
|
58
|
+
const width = Number(url.searchParams.get("w"));
|
|
59
|
+
const quality = Math.min(Math.max(Number(url.searchParams.get("q")) || maxQuality, 1), 100);
|
|
60
|
+
if (!src) return new Response('missing "url"', { status: 400 });
|
|
61
|
+
if (!Number.isInteger(width) || !sizes.includes(width)) return new Response('invalid "w"', { status: 400 });
|
|
62
|
+
const accept = request.headers.get("accept") ?? "";
|
|
63
|
+
const format = accept.includes("image/avif") ? "avif" : accept.includes("image/webp") ? "webp" : "original";
|
|
64
|
+
const key = `${src}|${width}|${quality}|${format}`;
|
|
65
|
+
const hit = cache.get(key);
|
|
66
|
+
if (hit) return imageResponse(hit.body, hit.contentType, maxAge);
|
|
67
|
+
let input;
|
|
68
|
+
let sourceType;
|
|
69
|
+
if (/^https?:\/\//i.test(src)) {
|
|
70
|
+
let host;
|
|
71
|
+
try {
|
|
72
|
+
host = new URL(src).hostname.toLowerCase();
|
|
73
|
+
} catch {
|
|
74
|
+
return new Response("bad url", { status: 400 });
|
|
75
|
+
}
|
|
76
|
+
if (!domains.has(host)) return new Response("domain not allowed", { status: 403 });
|
|
77
|
+
const res = await fetch(src);
|
|
78
|
+
if (!res.ok) return new Response("upstream error", { status: 502 });
|
|
79
|
+
input = new Uint8Array(await res.arrayBuffer());
|
|
80
|
+
sourceType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
81
|
+
} else {
|
|
82
|
+
const filePath = resolvePath(root, src.startsWith("/") ? src.slice(1) : src);
|
|
83
|
+
if (filePath !== root && !filePath.startsWith(root + sep)) return new Response("forbidden", { status: 403 });
|
|
84
|
+
try {
|
|
85
|
+
input = new Uint8Array(await readFile(filePath));
|
|
86
|
+
} catch {
|
|
87
|
+
return new Response("not found", { status: 404 });
|
|
88
|
+
}
|
|
89
|
+
sourceType = MIME[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
90
|
+
}
|
|
91
|
+
if (sourceType === "image/svg+xml" || sourceType === "image/gif") {
|
|
92
|
+
return imageResponse(input, sourceType, maxAge);
|
|
93
|
+
}
|
|
94
|
+
let out = null;
|
|
95
|
+
try {
|
|
96
|
+
out = await transform(input, { width, quality, format, contentType: sourceType });
|
|
97
|
+
} catch {
|
|
98
|
+
out = null;
|
|
99
|
+
}
|
|
100
|
+
const body = out?.data ?? input;
|
|
101
|
+
const contentType = out?.contentType ?? sourceType;
|
|
102
|
+
if (cacheMax > 0) {
|
|
103
|
+
if (cache.size >= cacheMax) cache.delete(cache.keys().next().value);
|
|
104
|
+
cache.set(key, { body, contentType });
|
|
105
|
+
}
|
|
106
|
+
return imageResponse(body, contentType, maxAge);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function imageLoader(options = {}) {
|
|
110
|
+
const path = options.path ?? "/api/image";
|
|
111
|
+
return ({ src, width, quality }) => {
|
|
112
|
+
const params = new URLSearchParams({ url: src, w: String(width) });
|
|
113
|
+
if (quality != null) params.set("q", String(quality));
|
|
114
|
+
return `${path}?${params.toString()}`;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
createImageHandler,
|
|
119
|
+
imageLoader
|
|
120
|
+
};
|