@mandujs/core 0.42.0 → 0.43.1
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/package.json +21 -4
- package/src/auth/__tests__/login.test.ts +420 -419
- package/src/auth/__tests__/reset.test.ts +296 -296
- package/src/brain/adapters/anthropic-oauth.ts +421 -420
- package/src/brain/adapters/index.ts +2 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/adapters/openai-oauth.ts +534 -533
- package/src/brain/brain.ts +2 -1
- package/src/brain/redactor.ts +196 -196
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
- package/src/bundler/__tests__/cold-start.test.ts +504 -504
- package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
- package/src/bundler/__tests__/hdr.test.ts +1 -1
- package/src/bundler/analyzer.ts +958 -958
- package/src/bundler/build.ts +104 -14
- package/src/bundler/dev.ts +125 -0
- package/src/bundler/hmr-types.ts +1 -0
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
- package/src/bundler/plugins/index.ts +14 -0
- package/src/bundler/plugins/react-compiler-lint.ts +253 -0
- package/src/bundler/plugins/react-compiler.ts +162 -0
- package/src/bundler/types.ts +12 -0
- package/src/change/integrity.ts +2 -1
- package/src/client/index.ts +10 -0
- package/src/client/island.ts +38 -11
- package/src/client/router.ts +6 -1
- package/src/config/mandu.ts +57 -0
- package/src/config/validate.ts +42 -0
- package/src/content/collection.ts +844 -809
- package/src/content/content-layer.ts +316 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/digest.ts +133 -133
- package/src/content/generate-types.ts +168 -168
- package/src/content/index.ts +6 -1
- package/src/content/llms-txt.ts +277 -277
- package/src/contract/define.ts +474 -474
- package/src/contract/route-helpers.ts +2 -1
- package/src/contract/zod-utils.ts +158 -155
- package/src/db/index.ts +513 -513
- package/src/desktop/__tests__/smoke.test.ts +100 -100
- package/src/desktop/webview-fallback.ts +583 -583
- package/src/desktop/window.ts +3 -1
- package/src/dev-error-overlay/overlay-client.ts +300 -300
- package/src/devtools/ai/mcp-connector.ts +499 -498
- package/src/devtools/client/components/kitchen-root.tsx +7 -2
- package/src/email/resend.ts +163 -163
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/graph.ts +898 -898
- package/src/guard/index.ts +16 -0
- package/src/guard/statistics.ts +578 -578
- package/src/guard/tsgolint-bridge.ts +512 -0
- package/src/i18n/locale-resolver.ts +214 -214
- package/src/id/__tests__/id.test.ts +120 -120
- package/src/intent/index.ts +321 -321
- package/src/island/index.ts +39 -23
- package/src/kitchen/api/contract-api.ts +15 -8
- package/src/kitchen/kitchen-ui.ts +2137 -2137
- package/src/lockfile/index.ts +3 -2
- package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
- package/src/middleware/secure/index.ts +417 -417
- package/src/observability/metrics.ts +334 -334
- package/src/observability/tracing.ts +694 -694
- package/src/openapi/generator.ts +1 -1
- package/src/perf/user-marks.ts +553 -553
- package/src/plugins/registry.ts +387 -387
- package/src/resource/ddl/diff.ts +392 -392
- package/src/resource/ddl/snapshot.ts +448 -447
- package/src/resource/generator-schema.ts +477 -476
- package/src/resource/parser.ts +4 -2
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.ts +422 -422
- package/src/runtime/fast-refresh-types.ts +126 -128
- package/src/runtime/handlers.ts +300 -0
- package/src/runtime/image-handler.ts +206 -195
- package/src/runtime/index.ts +4 -0
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +36 -19
- package/src/runtime/session-key.ts +328 -328
- package/src/scheduler/__tests__/scheduler.test.ts +514 -514
- package/src/seo/resolve/index.ts +353 -353
- package/src/spec/load.ts +1 -1
- package/src/testing/reporter.ts +676 -676
- package/src/testing/server.ts +196 -196
- package/src/testing/snapshot.ts +444 -444
- package/src/utils/__tests__/lru-cache.test.ts +186 -186
- package/src/utils/bun.ts +8 -8
|
@@ -1,195 +1,206 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Image Handler
|
|
3
|
-
* /_mandu/image?url=...&w=...&q=... 엔드포인트
|
|
4
|
-
* 온디맨드 리사이즈 + WebP/AVIF 포맷 협상 + 캐시
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import path from "path";
|
|
8
|
-
|
|
9
|
-
// ========== Types ==========
|
|
10
|
-
|
|
11
|
-
interface ImageOptions {
|
|
12
|
-
width: number;
|
|
13
|
-
quality: number;
|
|
14
|
-
format: "webp" | "jpeg" | "png" | "avif";
|
|
15
|
-
sourceContentType?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ProcessedImageResult {
|
|
19
|
-
data: Uint8Array;
|
|
20
|
-
contentType: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ========== Cache ==========
|
|
24
|
-
|
|
25
|
-
const imageCache = new Map<string, { data: Uint8Array; contentType: string }>();
|
|
26
|
-
const MAX_IMAGE_CACHE = 500;
|
|
27
|
-
|
|
28
|
-
// ========== Handler ==========
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 이미지 최적화 요청 처리
|
|
32
|
-
* /_mandu/image?url=/photos/hero.jpg&w=800&q=80
|
|
33
|
-
*/
|
|
34
|
-
export async function handleImageRequest(
|
|
35
|
-
request: Request,
|
|
36
|
-
rootDir: string,
|
|
37
|
-
publicDir: string = "public"
|
|
38
|
-
): Promise<Response | null> {
|
|
39
|
-
const url = new URL(request.url);
|
|
40
|
-
if (url.pathname !== "/_mandu/image") return null;
|
|
41
|
-
|
|
42
|
-
const src = url.searchParams.get("url");
|
|
43
|
-
const width = Number(url.searchParams.get("w") ?? 800);
|
|
44
|
-
const quality = Number(url.searchParams.get("q") ?? 80);
|
|
45
|
-
|
|
46
|
-
if (!src || width < 1 || width > 4096 || quality < 1 || quality > 100) {
|
|
47
|
-
return new Response("Invalid image parameters", { status: 400 });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 보안: src가 /로 시작하고 traversal/null byte 없는지 확인
|
|
51
|
-
if (!src.startsWith("/") || src.includes("..") || src.includes("\0")) {
|
|
52
|
-
return new Response("Invalid image path", { status: 400 });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 포맷 협상 (Accept 헤더 기반)
|
|
56
|
-
const format = negotiateFormat(request);
|
|
57
|
-
const cacheKey = `${src}:${width}:${quality}:${format}`;
|
|
58
|
-
|
|
59
|
-
// 캐시 확인
|
|
60
|
-
const cached = imageCache.get(cacheKey);
|
|
61
|
-
if (cached) {
|
|
62
|
-
return new Response(cached.data as unknown as BodyInit, {
|
|
63
|
-
headers: {
|
|
64
|
-
"Content-Type": cached.contentType,
|
|
65
|
-
"Cache-Control": "public, max-age=31536000, immutable",
|
|
66
|
-
"Vary": "Accept",
|
|
67
|
-
"X-Mandu-Image-Cache": "HIT",
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 원본 파일 경로 해석 + symlink traversal 방지
|
|
73
|
-
const allowedBaseDir = path.resolve(rootDir, publicDir);
|
|
74
|
-
const filePath = path.join(allowedBaseDir, src.slice(1));
|
|
75
|
-
|
|
76
|
-
// realpath로 symlink를 해석한 후 allowedBaseDir 내부인지 검증
|
|
77
|
-
let resolvedPath: string;
|
|
78
|
-
try {
|
|
79
|
-
const realFs = require("fs") as typeof import("fs");
|
|
80
|
-
resolvedPath = realFs.realpathSync(filePath);
|
|
81
|
-
const resolvedBase = realFs.realpathSync(allowedBaseDir);
|
|
82
|
-
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
83
|
-
return new Response("Forbidden", { status: 403 });
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
// realpath 실패 (broken symlink, 파일 없음) → 안전하게 404 반환
|
|
87
|
-
return new Response("Image not found", { status: 404 });
|
|
88
|
-
}
|
|
89
|
-
const file = Bun.file(resolvedPath);
|
|
90
|
-
|
|
91
|
-
if (!await file.exists()) {
|
|
92
|
-
return new Response("Image not found", { status: 404 });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const original = await file.arrayBuffer();
|
|
97
|
-
const processed = await processImage(new Uint8Array(original), {
|
|
98
|
-
width,
|
|
99
|
-
quality,
|
|
100
|
-
format,
|
|
101
|
-
sourceContentType: getMimeForExtension(src),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// 캐시 저장 (LRU)
|
|
105
|
-
if (imageCache.size >= MAX_IMAGE_CACHE) {
|
|
106
|
-
const oldest = imageCache.keys().next().value;
|
|
107
|
-
if (oldest !== undefined) imageCache.delete(oldest);
|
|
108
|
-
}
|
|
109
|
-
imageCache.set(cacheKey, {
|
|
110
|
-
data: processed.data,
|
|
111
|
-
contentType: processed.contentType,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
return new Response(processed.data as unknown as BodyInit, {
|
|
115
|
-
headers: {
|
|
116
|
-
"Content-Type": processed.contentType,
|
|
117
|
-
"Cache-Control": "public, max-age=31536000, immutable",
|
|
118
|
-
"Vary": "Accept",
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error(`[Mandu Image] Processing failed for ${src}:`, error);
|
|
123
|
-
// 원본 파일 그대로 반환 (fallback)
|
|
124
|
-
return new Response(file, {
|
|
125
|
-
headers: {
|
|
126
|
-
"Content-Type": getMimeForExtension(src),
|
|
127
|
-
"Cache-Control": "public, max-age=86400",
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ========== Format Negotiation ==========
|
|
134
|
-
|
|
135
|
-
function negotiateFormat(request: Request): "webp" | "jpeg" | "png" | "avif" {
|
|
136
|
-
const accept = request.headers.get("Accept") ?? "";
|
|
137
|
-
if (accept.includes("image/avif")) return "avif";
|
|
138
|
-
if (accept.includes("image/webp")) return "webp";
|
|
139
|
-
return "jpeg";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function getMimeForExtension(src: string): string {
|
|
143
|
-
const ext = path.extname(src).toLowerCase();
|
|
144
|
-
const map: Record<string, string> = {
|
|
145
|
-
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
146
|
-
".png": "image/png", ".gif": "image/gif",
|
|
147
|
-
".webp": "image/webp", ".avif": "image/avif",
|
|
148
|
-
".svg": "image/svg+xml",
|
|
149
|
-
};
|
|
150
|
-
return map[ext] ?? "application/octet-stream";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ========== Image Processing ==========
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 이미지 리사이즈 + 포맷 변환
|
|
157
|
-
* Bun의 내장 sharp 미지원 시 원본 반환 (graceful degradation)
|
|
158
|
-
*/
|
|
159
|
-
async function processImage(
|
|
160
|
-
data: Uint8Array,
|
|
161
|
-
options: ImageOptions
|
|
162
|
-
): Promise<ProcessedImageResult> {
|
|
163
|
-
// sharp 사용 시도 (선택적 의존성)
|
|
164
|
-
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Image Handler
|
|
3
|
+
* /_mandu/image?url=...&w=...&q=... 엔드포인트
|
|
4
|
+
* 온디맨드 리사이즈 + WebP/AVIF 포맷 협상 + 캐시
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
// ========== Types ==========
|
|
10
|
+
|
|
11
|
+
interface ImageOptions {
|
|
12
|
+
width: number;
|
|
13
|
+
quality: number;
|
|
14
|
+
format: "webp" | "jpeg" | "png" | "avif";
|
|
15
|
+
sourceContentType?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ProcessedImageResult {
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
contentType: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ========== Cache ==========
|
|
24
|
+
|
|
25
|
+
const imageCache = new Map<string, { data: Uint8Array; contentType: string }>();
|
|
26
|
+
const MAX_IMAGE_CACHE = 500;
|
|
27
|
+
|
|
28
|
+
// ========== Handler ==========
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 이미지 최적화 요청 처리
|
|
32
|
+
* /_mandu/image?url=/photos/hero.jpg&w=800&q=80
|
|
33
|
+
*/
|
|
34
|
+
export async function handleImageRequest(
|
|
35
|
+
request: Request,
|
|
36
|
+
rootDir: string,
|
|
37
|
+
publicDir: string = "public"
|
|
38
|
+
): Promise<Response | null> {
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
if (url.pathname !== "/_mandu/image") return null;
|
|
41
|
+
|
|
42
|
+
const src = url.searchParams.get("url");
|
|
43
|
+
const width = Number(url.searchParams.get("w") ?? 800);
|
|
44
|
+
const quality = Number(url.searchParams.get("q") ?? 80);
|
|
45
|
+
|
|
46
|
+
if (!src || width < 1 || width > 4096 || quality < 1 || quality > 100) {
|
|
47
|
+
return new Response("Invalid image parameters", { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 보안: src가 /로 시작하고 traversal/null byte 없는지 확인
|
|
51
|
+
if (!src.startsWith("/") || src.includes("..") || src.includes("\0")) {
|
|
52
|
+
return new Response("Invalid image path", { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 포맷 협상 (Accept 헤더 기반)
|
|
56
|
+
const format = negotiateFormat(request);
|
|
57
|
+
const cacheKey = `${src}:${width}:${quality}:${format}`;
|
|
58
|
+
|
|
59
|
+
// 캐시 확인
|
|
60
|
+
const cached = imageCache.get(cacheKey);
|
|
61
|
+
if (cached) {
|
|
62
|
+
return new Response(cached.data as unknown as BodyInit, {
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": cached.contentType,
|
|
65
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
66
|
+
"Vary": "Accept",
|
|
67
|
+
"X-Mandu-Image-Cache": "HIT",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 원본 파일 경로 해석 + symlink traversal 방지
|
|
73
|
+
const allowedBaseDir = path.resolve(rootDir, publicDir);
|
|
74
|
+
const filePath = path.join(allowedBaseDir, src.slice(1));
|
|
75
|
+
|
|
76
|
+
// realpath로 symlink를 해석한 후 allowedBaseDir 내부인지 검증
|
|
77
|
+
let resolvedPath: string;
|
|
78
|
+
try {
|
|
79
|
+
const realFs = require("fs") as typeof import("fs");
|
|
80
|
+
resolvedPath = realFs.realpathSync(filePath);
|
|
81
|
+
const resolvedBase = realFs.realpathSync(allowedBaseDir);
|
|
82
|
+
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
83
|
+
return new Response("Forbidden", { status: 403 });
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// realpath 실패 (broken symlink, 파일 없음) → 안전하게 404 반환
|
|
87
|
+
return new Response("Image not found", { status: 404 });
|
|
88
|
+
}
|
|
89
|
+
const file = Bun.file(resolvedPath);
|
|
90
|
+
|
|
91
|
+
if (!await file.exists()) {
|
|
92
|
+
return new Response("Image not found", { status: 404 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const original = await file.arrayBuffer();
|
|
97
|
+
const processed = await processImage(new Uint8Array(original), {
|
|
98
|
+
width,
|
|
99
|
+
quality,
|
|
100
|
+
format,
|
|
101
|
+
sourceContentType: getMimeForExtension(src),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 캐시 저장 (LRU)
|
|
105
|
+
if (imageCache.size >= MAX_IMAGE_CACHE) {
|
|
106
|
+
const oldest = imageCache.keys().next().value;
|
|
107
|
+
if (oldest !== undefined) imageCache.delete(oldest);
|
|
108
|
+
}
|
|
109
|
+
imageCache.set(cacheKey, {
|
|
110
|
+
data: processed.data,
|
|
111
|
+
contentType: processed.contentType,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return new Response(processed.data as unknown as BodyInit, {
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": processed.contentType,
|
|
117
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
118
|
+
"Vary": "Accept",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`[Mandu Image] Processing failed for ${src}:`, error);
|
|
123
|
+
// 원본 파일 그대로 반환 (fallback)
|
|
124
|
+
return new Response(file, {
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": getMimeForExtension(src),
|
|
127
|
+
"Cache-Control": "public, max-age=86400",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ========== Format Negotiation ==========
|
|
134
|
+
|
|
135
|
+
function negotiateFormat(request: Request): "webp" | "jpeg" | "png" | "avif" {
|
|
136
|
+
const accept = request.headers.get("Accept") ?? "";
|
|
137
|
+
if (accept.includes("image/avif")) return "avif";
|
|
138
|
+
if (accept.includes("image/webp")) return "webp";
|
|
139
|
+
return "jpeg";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getMimeForExtension(src: string): string {
|
|
143
|
+
const ext = path.extname(src).toLowerCase();
|
|
144
|
+
const map: Record<string, string> = {
|
|
145
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
146
|
+
".png": "image/png", ".gif": "image/gif",
|
|
147
|
+
".webp": "image/webp", ".avif": "image/avif",
|
|
148
|
+
".svg": "image/svg+xml",
|
|
149
|
+
};
|
|
150
|
+
return map[ext] ?? "application/octet-stream";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ========== Image Processing ==========
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 이미지 리사이즈 + 포맷 변환
|
|
157
|
+
* Bun의 내장 sharp 미지원 시 원본 반환 (graceful degradation)
|
|
158
|
+
*/
|
|
159
|
+
async function processImage(
|
|
160
|
+
data: Uint8Array,
|
|
161
|
+
options: ImageOptions
|
|
162
|
+
): Promise<ProcessedImageResult> {
|
|
163
|
+
// sharp 사용 시도 (선택적 의존성)
|
|
164
|
+
try {
|
|
165
|
+
// sharp is a heavyweight optional native dep. Type it minimally to the
|
|
166
|
+
// tiny surface we actually call — `resize/webp/avif/jpeg/png/toBuffer`.
|
|
167
|
+
interface SharpPipeline {
|
|
168
|
+
resize(width: number): SharpPipeline;
|
|
169
|
+
webp(opts: { quality: number }): SharpPipeline;
|
|
170
|
+
avif(opts: { quality: number }): SharpPipeline;
|
|
171
|
+
jpeg(opts: { quality: number }): SharpPipeline;
|
|
172
|
+
png(opts: { quality: number }): SharpPipeline;
|
|
173
|
+
toBuffer(): Promise<Buffer>;
|
|
174
|
+
}
|
|
175
|
+
type SharpFn = (input: Buffer) => SharpPipeline;
|
|
176
|
+
const sharp = require("sharp") as SharpFn;
|
|
177
|
+
let pipeline = sharp(Buffer.from(data)).resize(options.width);
|
|
178
|
+
|
|
179
|
+
switch (options.format) {
|
|
180
|
+
case "webp":
|
|
181
|
+
pipeline = pipeline.webp({ quality: options.quality });
|
|
182
|
+
break;
|
|
183
|
+
case "avif":
|
|
184
|
+
pipeline = pipeline.avif({ quality: options.quality });
|
|
185
|
+
break;
|
|
186
|
+
case "jpeg":
|
|
187
|
+
pipeline = pipeline.jpeg({ quality: options.quality });
|
|
188
|
+
break;
|
|
189
|
+
case "png":
|
|
190
|
+
pipeline = pipeline.png({ quality: options.quality });
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await pipeline.toBuffer();
|
|
195
|
+
return {
|
|
196
|
+
data: new Uint8Array(result),
|
|
197
|
+
contentType: `image/${options.format}`,
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
// sharp 미설치 시 원본 반환
|
|
201
|
+
return {
|
|
202
|
+
data,
|
|
203
|
+
contentType: options.sourceContentType ?? "application/octet-stream",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -3,6 +3,10 @@ export * from "./streaming-ssr";
|
|
|
3
3
|
export { extractShellHtml, createPPRResponse, PPR_SHELL_MARKER } from "./ppr";
|
|
4
4
|
export * from "./router";
|
|
5
5
|
export * from "./server";
|
|
6
|
+
export {
|
|
7
|
+
registerManifestHandlers,
|
|
8
|
+
type RegisterHandlersOptions,
|
|
9
|
+
} from "./handlers";
|
|
6
10
|
export { redirect, isManduRedirect, isRedirectResponse, REDIRECT_BRAND } from "./redirect";
|
|
7
11
|
export type { RedirectStatus, RedirectOptions } from "./redirect";
|
|
8
12
|
export { notFound, isNotFoundResponse, NOT_FOUND_BRAND } from "./not-found";
|