jskelet 0.6.3 → 0.6.5
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/AGENTS.md +136 -136
- package/CHANGELOG.md +633 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +700 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +351 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +708 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +355 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +965 -356
- package/src/server/og-raster.mjs +21 -0
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +51 -3
|
@@ -1,265 +1,265 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Framework'ün kendi hata sayfaları (404, 500, 503…).
|
|
3
|
-
*
|
|
4
|
-
* Uygulama kendi sayfasını vermediğinde ziyaretçinin Express'in düz metin
|
|
5
|
-
* "Internal Server Error" çıktısını görmesi istenmiyor; bu yüzden framework
|
|
6
|
-
* şablonsuz, tek dosyada duran minimal bir HTML üretir.
|
|
7
|
-
*
|
|
8
|
-
* Production metni bilinçli olarak yalın: yalnızca bir şeyin ters gittiğini
|
|
9
|
-
* söyler. Marka adı, ürün tanıtımı ya da hata ayrıntısı yok — hata sayfası
|
|
10
|
-
* ziyaretçiye bir şey satmaz ve sunucunun içini dışa açmaz. Development'ta
|
|
11
|
-
* 5xx için ayrıntılı teşhis sayfası üretilir (aşağıya bak).
|
|
12
|
-
*
|
|
13
|
-
* Ezme yolları (öncelik sırasıyla; development 5xx hariç):
|
|
14
|
-
* 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
|
|
15
|
-
* 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
|
|
16
|
-
* doğrudan HTML string döner.
|
|
17
|
-
* 3. Aşağıdaki gömülü HTML.
|
|
18
|
-
*/
|
|
19
|
-
import { getConfig, hook } from "../config/index.js";
|
|
20
|
-
import { esc } from "../views/helpers/html.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Durum koduna karşılık gelen sayfayı üretir. Hiçbir koşulda fırlatmaz:
|
|
24
|
-
* hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
|
|
25
|
-
* başarısızlık gömülü HTML'e düşer.
|
|
26
|
-
*
|
|
27
|
-
* Development'ta 5xx yanıtları `hooks.error` ve gömülü 500 sayfasını atlar;
|
|
28
|
-
* yığın izini içeren bir teşhis sayfası döner. Production'da ayrıntı
|
|
29
|
-
* gösterilmez — sunucu içi ziyaretçiye açılmaz.
|
|
30
|
-
*
|
|
31
|
-
* @param {number} status
|
|
32
|
-
* @param {{ error?: unknown }} [options]
|
|
33
|
-
* @returns {Promise<string>}
|
|
34
|
-
*/
|
|
35
|
-
export async function renderStatusPage(status, options = {}) {
|
|
36
|
-
if (process.env.NODE_ENV === "development" && status >= 500) {
|
|
37
|
-
return developmentErrorPage(status, options.error);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const page =
|
|
41
|
-
(status === 404 ? await hook("notFound", null) : null) ??
|
|
42
|
-
(await hook("error", null, { status, error: options.error }));
|
|
43
|
-
|
|
44
|
-
if (typeof page === "string") return page;
|
|
45
|
-
if (!page) return fallbackPage(status);
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
// Dinamik import: render.js bu modülü kendisi kullanıyor, statik ithal
|
|
49
|
-
// iki modül arasında döngü kurardı.
|
|
50
|
-
const { renderPage } = await import("./render.js");
|
|
51
|
-
return await renderPage({ pathname: `/${status}`, ...page });
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error(`[render] failed to render the ${status} page`, error);
|
|
54
|
-
return fallbackPage(status);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Bir hatadan HTTP durum kodu çıkarır. Uygulama kodu `error.statusCode` ya da
|
|
60
|
-
* `error.status` ile kendi kodunu bildirebilir; tanınmayan her şey 500'dür.
|
|
61
|
-
*
|
|
62
|
-
* @param {unknown} error
|
|
63
|
-
* @returns {number}
|
|
64
|
-
*/
|
|
65
|
-
export function statusFromError(error) {
|
|
66
|
-
const raw =
|
|
67
|
-
error && typeof error === "object"
|
|
68
|
-
? /** @type {{ statusCode?: unknown, status?: unknown }} */ (error).statusCode ??
|
|
69
|
-
/** @type {{ status?: unknown }} */ (error).status
|
|
70
|
-
: undefined;
|
|
71
|
-
|
|
72
|
-
const status = Number(raw);
|
|
73
|
-
return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Durum başlıkları. Yalnızca framework'ün kendi ürettiği yanıtlar için;
|
|
78
|
-
* uygulamanın diline ait metinler `hooks.error()` üzerinden gelir.
|
|
79
|
-
*
|
|
80
|
-
* @type {Record<string, Record<number | "4xx" | "5xx", [string, string]>>}
|
|
81
|
-
*/
|
|
82
|
-
const MESSAGES = {
|
|
83
|
-
tr: {
|
|
84
|
-
400: ["Geçersiz istek", "İstek anlaşılamadı."],
|
|
85
|
-
401: ["Yetki gerekiyor", "Bu sayfayı görmek için oturum açmanız gerekiyor."],
|
|
86
|
-
403: ["Erişim yok", "Bu sayfaya erişim izniniz yok."],
|
|
87
|
-
404: ["Sayfa bulunamadı", "Aradığınız sayfa burada değil."],
|
|
88
|
-
408: ["İstek zaman aşımına uğradı", "Lütfen tekrar deneyin."],
|
|
89
|
-
410: ["Sayfa kaldırıldı", "Bu sayfa artık yayında değil."],
|
|
90
|
-
429: ["Çok fazla istek", "Kısa bir süre sonra tekrar deneyin."],
|
|
91
|
-
500: ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
|
|
92
|
-
503: ["Servis kullanılamıyor", "Lütfen daha sonra tekrar deneyin."],
|
|
93
|
-
"4xx": ["İstek karşılanamadı", "Lütfen adresi kontrol edin."],
|
|
94
|
-
"5xx": ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
|
|
95
|
-
},
|
|
96
|
-
en: {
|
|
97
|
-
400: ["Bad request", "The request could not be understood."],
|
|
98
|
-
401: ["Sign in required", "You need to sign in to view this page."],
|
|
99
|
-
403: ["No access", "You do not have permission to view this page."],
|
|
100
|
-
404: ["Page not found", "The page you are looking for is not here."],
|
|
101
|
-
408: ["Request timed out", "Please try again."],
|
|
102
|
-
410: ["Page removed", "This page is no longer available."],
|
|
103
|
-
429: ["Too many requests", "Please try again in a moment."],
|
|
104
|
-
500: ["Something went wrong", "Please try again later."],
|
|
105
|
-
503: ["Service unavailable", "Please try again later."],
|
|
106
|
-
"4xx": ["Request failed", "Please check the address."],
|
|
107
|
-
"5xx": ["Something went wrong", "Please try again later."],
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @param {number} status
|
|
113
|
-
* @returns {{ lang: string, title: string, detail: string }}
|
|
114
|
-
*/
|
|
115
|
-
function statusText(status) {
|
|
116
|
-
// Config yüklenmeden de çağrılabilir (ör. loadConfig() patladıysa);
|
|
117
|
-
// hata sayfası bu yüzden getConfig()'in fırlatmasına dayanmaz.
|
|
118
|
-
let lang = "en";
|
|
119
|
-
try {
|
|
120
|
-
lang = getConfig().brand.lang ?? "en";
|
|
121
|
-
} catch {
|
|
122
|
-
/* varsayılan kalır */
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const table = MESSAGES[lang.slice(0, 2).toLowerCase()] ?? MESSAGES.en;
|
|
126
|
-
const [title, detail] =
|
|
127
|
-
table[status] ?? table[status >= 500 ? "5xx" : "4xx"];
|
|
128
|
-
|
|
129
|
-
return { lang, title, detail };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Şablonsuz, varlıksız hata sayfası: tek istekte biter, build çıktısına ve
|
|
134
|
-
* uygulamanın layout'una bağlı değildir. `noindex` bilinçli — hata sayfası
|
|
135
|
-
* arama sonuçlarında görünmemeli.
|
|
136
|
-
*
|
|
137
|
-
* @param {number} status
|
|
138
|
-
* @returns {string}
|
|
139
|
-
*/
|
|
140
|
-
function fallbackPage(status) {
|
|
141
|
-
const { lang, title, detail } = statusText(status);
|
|
142
|
-
|
|
143
|
-
return `<!DOCTYPE html>
|
|
144
|
-
<html lang="${esc(lang)}">
|
|
145
|
-
<head>
|
|
146
|
-
<meta charset="utf-8">
|
|
147
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
148
|
-
<meta name="robots" content="noindex, nofollow">
|
|
149
|
-
<title>${status} — ${esc(title)}</title>
|
|
150
|
-
<style>
|
|
151
|
-
:root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; }
|
|
152
|
-
@media (prefers-color-scheme: dark) {
|
|
153
|
-
:root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; }
|
|
154
|
-
}
|
|
155
|
-
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg); }
|
|
156
|
-
body {
|
|
157
|
-
display: grid; place-items: center; padding: 2rem; text-align: center;
|
|
158
|
-
font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
159
|
-
}
|
|
160
|
-
.code { font-size: 3.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
|
|
161
|
-
h1 { font-size: 1.125rem; font-weight: 600; margin: 0.75rem 0 0; }
|
|
162
|
-
p { margin: 0.375rem 0 0; color: var(--muted); }
|
|
163
|
-
</style>
|
|
164
|
-
</head>
|
|
165
|
-
<body>
|
|
166
|
-
<main>
|
|
167
|
-
<p class="code">${status}</p>
|
|
168
|
-
<h1>${esc(title)}</h1>
|
|
169
|
-
<p>${esc(detail)}</p>
|
|
170
|
-
</main>
|
|
171
|
-
</body>
|
|
172
|
-
</html>`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Development teşhis sayfası: mesaj + yığın izi. Yalnızca
|
|
177
|
-
* `NODE_ENV=development` iken `renderStatusPage` üzerinden çağrılır;
|
|
178
|
-
* production yolu buraya hiç girmez.
|
|
179
|
-
*
|
|
180
|
-
* @param {number} status
|
|
181
|
-
* @param {unknown} [error]
|
|
182
|
-
* @returns {string}
|
|
183
|
-
*/
|
|
184
|
-
function developmentErrorPage(status, error) {
|
|
185
|
-
const name =
|
|
186
|
-
error instanceof Error && error.name ? error.name : "Error";
|
|
187
|
-
const message =
|
|
188
|
-
error instanceof Error
|
|
189
|
-
? error.message || "(no message)"
|
|
190
|
-
: error == null
|
|
191
|
-
? "(no error object)"
|
|
192
|
-
: String(error);
|
|
193
|
-
const body = serializeError(error);
|
|
194
|
-
|
|
195
|
-
return `<!DOCTYPE html>
|
|
196
|
-
<html lang="en">
|
|
197
|
-
<head>
|
|
198
|
-
<meta charset="utf-8">
|
|
199
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
200
|
-
<meta name="robots" content="noindex, nofollow">
|
|
201
|
-
<title>${status} — ${esc(name)}: ${esc(message)}</title>
|
|
202
|
-
<style>
|
|
203
|
-
:root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; --panel: #fff; --border: #e4e4e7; }
|
|
204
|
-
@media (prefers-color-scheme: dark) {
|
|
205
|
-
:root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; --panel: #18181b; --border: #27272a; }
|
|
206
|
-
}
|
|
207
|
-
html, body { margin: 0; background: var(--bg); color: var(--fg); }
|
|
208
|
-
body {
|
|
209
|
-
padding: 1.5rem; max-width: 56rem; margin: 0 auto;
|
|
210
|
-
font: 1rem/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
211
|
-
}
|
|
212
|
-
.banner {
|
|
213
|
-
display: inline-block; margin: 0 0 1rem; padding: 0.2rem 0.5rem;
|
|
214
|
-
font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em;
|
|
215
|
-
text-transform: uppercase; color: var(--muted); border: 1px solid var(--border);
|
|
216
|
-
}
|
|
217
|
-
.code { font-size: 2.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
|
|
218
|
-
h1 { font-size: 1.25rem; font-weight: 600; margin: 0.5rem 0 0; word-break: break-word; }
|
|
219
|
-
pre {
|
|
220
|
-
margin: 1.25rem 0 0; padding: 1rem; overflow: auto;
|
|
221
|
-
background: var(--panel); border: 1px solid var(--border);
|
|
222
|
-
font: 0.8125rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
223
|
-
white-space: pre-wrap; word-break: break-word;
|
|
224
|
-
}
|
|
225
|
-
</style>
|
|
226
|
-
</head>
|
|
227
|
-
<body>
|
|
228
|
-
<p class="banner">Development only — not shown in production</p>
|
|
229
|
-
<p class="code">${status}</p>
|
|
230
|
-
<h1>${esc(name)}: ${esc(message)}</h1>
|
|
231
|
-
<pre>${esc(body)}</pre>
|
|
232
|
-
</body>
|
|
233
|
-
</html>`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Hata nesnesini yığın izi + `cause` zinciriyle metne çevirir.
|
|
238
|
-
*
|
|
239
|
-
* @param {unknown} error
|
|
240
|
-
* @returns {string}
|
|
241
|
-
*/
|
|
242
|
-
function serializeError(error) {
|
|
243
|
-
if (error == null) return "(no error object)";
|
|
244
|
-
|
|
245
|
-
/** @type {string[]} */
|
|
246
|
-
const parts = [];
|
|
247
|
-
/** @type {unknown} */
|
|
248
|
-
let current = error;
|
|
249
|
-
let depth = 0;
|
|
250
|
-
|
|
251
|
-
while (current != null && depth < 6) {
|
|
252
|
-
if (depth > 0) parts.push("\nCaused by:");
|
|
253
|
-
|
|
254
|
-
if (current instanceof Error) {
|
|
255
|
-
parts.push(current.stack || `${current.name}: ${current.message}`);
|
|
256
|
-
current = "cause" in current ? current.cause : undefined;
|
|
257
|
-
} else {
|
|
258
|
-
parts.push(String(current));
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
depth += 1;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return parts.join("\n");
|
|
265
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Framework'ün kendi hata sayfaları (404, 500, 503…).
|
|
3
|
+
*
|
|
4
|
+
* Uygulama kendi sayfasını vermediğinde ziyaretçinin Express'in düz metin
|
|
5
|
+
* "Internal Server Error" çıktısını görmesi istenmiyor; bu yüzden framework
|
|
6
|
+
* şablonsuz, tek dosyada duran minimal bir HTML üretir.
|
|
7
|
+
*
|
|
8
|
+
* Production metni bilinçli olarak yalın: yalnızca bir şeyin ters gittiğini
|
|
9
|
+
* söyler. Marka adı, ürün tanıtımı ya da hata ayrıntısı yok — hata sayfası
|
|
10
|
+
* ziyaretçiye bir şey satmaz ve sunucunun içini dışa açmaz. Development'ta
|
|
11
|
+
* 5xx için ayrıntılı teşhis sayfası üretilir (aşağıya bak).
|
|
12
|
+
*
|
|
13
|
+
* Ezme yolları (öncelik sırasıyla; development 5xx hariç):
|
|
14
|
+
* 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
|
|
15
|
+
* 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
|
|
16
|
+
* doğrudan HTML string döner.
|
|
17
|
+
* 3. Aşağıdaki gömülü HTML.
|
|
18
|
+
*/
|
|
19
|
+
import { getConfig, hook } from "../config/index.js";
|
|
20
|
+
import { esc } from "../views/helpers/html.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Durum koduna karşılık gelen sayfayı üretir. Hiçbir koşulda fırlatmaz:
|
|
24
|
+
* hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
|
|
25
|
+
* başarısızlık gömülü HTML'e düşer.
|
|
26
|
+
*
|
|
27
|
+
* Development'ta 5xx yanıtları `hooks.error` ve gömülü 500 sayfasını atlar;
|
|
28
|
+
* yığın izini içeren bir teşhis sayfası döner. Production'da ayrıntı
|
|
29
|
+
* gösterilmez — sunucu içi ziyaretçiye açılmaz.
|
|
30
|
+
*
|
|
31
|
+
* @param {number} status
|
|
32
|
+
* @param {{ error?: unknown }} [options]
|
|
33
|
+
* @returns {Promise<string>}
|
|
34
|
+
*/
|
|
35
|
+
export async function renderStatusPage(status, options = {}) {
|
|
36
|
+
if (process.env.NODE_ENV === "development" && status >= 500) {
|
|
37
|
+
return developmentErrorPage(status, options.error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const page =
|
|
41
|
+
(status === 404 ? await hook("notFound", null) : null) ??
|
|
42
|
+
(await hook("error", null, { status, error: options.error }));
|
|
43
|
+
|
|
44
|
+
if (typeof page === "string") return page;
|
|
45
|
+
if (!page) return fallbackPage(status);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Dinamik import: render.js bu modülü kendisi kullanıyor, statik ithal
|
|
49
|
+
// iki modül arasında döngü kurardı.
|
|
50
|
+
const { renderPage } = await import("./render.js");
|
|
51
|
+
return await renderPage({ pathname: `/${status}`, ...page });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(`[render] failed to render the ${status} page`, error);
|
|
54
|
+
return fallbackPage(status);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bir hatadan HTTP durum kodu çıkarır. Uygulama kodu `error.statusCode` ya da
|
|
60
|
+
* `error.status` ile kendi kodunu bildirebilir; tanınmayan her şey 500'dür.
|
|
61
|
+
*
|
|
62
|
+
* @param {unknown} error
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function statusFromError(error) {
|
|
66
|
+
const raw =
|
|
67
|
+
error && typeof error === "object"
|
|
68
|
+
? /** @type {{ statusCode?: unknown, status?: unknown }} */ (error).statusCode ??
|
|
69
|
+
/** @type {{ status?: unknown }} */ (error).status
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
72
|
+
const status = Number(raw);
|
|
73
|
+
return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Durum başlıkları. Yalnızca framework'ün kendi ürettiği yanıtlar için;
|
|
78
|
+
* uygulamanın diline ait metinler `hooks.error()` üzerinden gelir.
|
|
79
|
+
*
|
|
80
|
+
* @type {Record<string, Record<number | "4xx" | "5xx", [string, string]>>}
|
|
81
|
+
*/
|
|
82
|
+
const MESSAGES = {
|
|
83
|
+
tr: {
|
|
84
|
+
400: ["Geçersiz istek", "İstek anlaşılamadı."],
|
|
85
|
+
401: ["Yetki gerekiyor", "Bu sayfayı görmek için oturum açmanız gerekiyor."],
|
|
86
|
+
403: ["Erişim yok", "Bu sayfaya erişim izniniz yok."],
|
|
87
|
+
404: ["Sayfa bulunamadı", "Aradığınız sayfa burada değil."],
|
|
88
|
+
408: ["İstek zaman aşımına uğradı", "Lütfen tekrar deneyin."],
|
|
89
|
+
410: ["Sayfa kaldırıldı", "Bu sayfa artık yayında değil."],
|
|
90
|
+
429: ["Çok fazla istek", "Kısa bir süre sonra tekrar deneyin."],
|
|
91
|
+
500: ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
|
|
92
|
+
503: ["Servis kullanılamıyor", "Lütfen daha sonra tekrar deneyin."],
|
|
93
|
+
"4xx": ["İstek karşılanamadı", "Lütfen adresi kontrol edin."],
|
|
94
|
+
"5xx": ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
|
|
95
|
+
},
|
|
96
|
+
en: {
|
|
97
|
+
400: ["Bad request", "The request could not be understood."],
|
|
98
|
+
401: ["Sign in required", "You need to sign in to view this page."],
|
|
99
|
+
403: ["No access", "You do not have permission to view this page."],
|
|
100
|
+
404: ["Page not found", "The page you are looking for is not here."],
|
|
101
|
+
408: ["Request timed out", "Please try again."],
|
|
102
|
+
410: ["Page removed", "This page is no longer available."],
|
|
103
|
+
429: ["Too many requests", "Please try again in a moment."],
|
|
104
|
+
500: ["Something went wrong", "Please try again later."],
|
|
105
|
+
503: ["Service unavailable", "Please try again later."],
|
|
106
|
+
"4xx": ["Request failed", "Please check the address."],
|
|
107
|
+
"5xx": ["Something went wrong", "Please try again later."],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {number} status
|
|
113
|
+
* @returns {{ lang: string, title: string, detail: string }}
|
|
114
|
+
*/
|
|
115
|
+
function statusText(status) {
|
|
116
|
+
// Config yüklenmeden de çağrılabilir (ör. loadConfig() patladıysa);
|
|
117
|
+
// hata sayfası bu yüzden getConfig()'in fırlatmasına dayanmaz.
|
|
118
|
+
let lang = "en";
|
|
119
|
+
try {
|
|
120
|
+
lang = getConfig().brand.lang ?? "en";
|
|
121
|
+
} catch {
|
|
122
|
+
/* varsayılan kalır */
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const table = MESSAGES[lang.slice(0, 2).toLowerCase()] ?? MESSAGES.en;
|
|
126
|
+
const [title, detail] =
|
|
127
|
+
table[status] ?? table[status >= 500 ? "5xx" : "4xx"];
|
|
128
|
+
|
|
129
|
+
return { lang, title, detail };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Şablonsuz, varlıksız hata sayfası: tek istekte biter, build çıktısına ve
|
|
134
|
+
* uygulamanın layout'una bağlı değildir. `noindex` bilinçli — hata sayfası
|
|
135
|
+
* arama sonuçlarında görünmemeli.
|
|
136
|
+
*
|
|
137
|
+
* @param {number} status
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
140
|
+
function fallbackPage(status) {
|
|
141
|
+
const { lang, title, detail } = statusText(status);
|
|
142
|
+
|
|
143
|
+
return `<!DOCTYPE html>
|
|
144
|
+
<html lang="${esc(lang)}">
|
|
145
|
+
<head>
|
|
146
|
+
<meta charset="utf-8">
|
|
147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
148
|
+
<meta name="robots" content="noindex, nofollow">
|
|
149
|
+
<title>${status} — ${esc(title)}</title>
|
|
150
|
+
<style>
|
|
151
|
+
:root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; }
|
|
152
|
+
@media (prefers-color-scheme: dark) {
|
|
153
|
+
:root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; }
|
|
154
|
+
}
|
|
155
|
+
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg); }
|
|
156
|
+
body {
|
|
157
|
+
display: grid; place-items: center; padding: 2rem; text-align: center;
|
|
158
|
+
font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
159
|
+
}
|
|
160
|
+
.code { font-size: 3.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
|
|
161
|
+
h1 { font-size: 1.125rem; font-weight: 600; margin: 0.75rem 0 0; }
|
|
162
|
+
p { margin: 0.375rem 0 0; color: var(--muted); }
|
|
163
|
+
</style>
|
|
164
|
+
</head>
|
|
165
|
+
<body>
|
|
166
|
+
<main>
|
|
167
|
+
<p class="code">${status}</p>
|
|
168
|
+
<h1>${esc(title)}</h1>
|
|
169
|
+
<p>${esc(detail)}</p>
|
|
170
|
+
</main>
|
|
171
|
+
</body>
|
|
172
|
+
</html>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Development teşhis sayfası: mesaj + yığın izi. Yalnızca
|
|
177
|
+
* `NODE_ENV=development` iken `renderStatusPage` üzerinden çağrılır;
|
|
178
|
+
* production yolu buraya hiç girmez.
|
|
179
|
+
*
|
|
180
|
+
* @param {number} status
|
|
181
|
+
* @param {unknown} [error]
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function developmentErrorPage(status, error) {
|
|
185
|
+
const name =
|
|
186
|
+
error instanceof Error && error.name ? error.name : "Error";
|
|
187
|
+
const message =
|
|
188
|
+
error instanceof Error
|
|
189
|
+
? error.message || "(no message)"
|
|
190
|
+
: error == null
|
|
191
|
+
? "(no error object)"
|
|
192
|
+
: String(error);
|
|
193
|
+
const body = serializeError(error);
|
|
194
|
+
|
|
195
|
+
return `<!DOCTYPE html>
|
|
196
|
+
<html lang="en">
|
|
197
|
+
<head>
|
|
198
|
+
<meta charset="utf-8">
|
|
199
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
200
|
+
<meta name="robots" content="noindex, nofollow">
|
|
201
|
+
<title>${status} — ${esc(name)}: ${esc(message)}</title>
|
|
202
|
+
<style>
|
|
203
|
+
:root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; --panel: #fff; --border: #e4e4e7; }
|
|
204
|
+
@media (prefers-color-scheme: dark) {
|
|
205
|
+
:root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; --panel: #18181b; --border: #27272a; }
|
|
206
|
+
}
|
|
207
|
+
html, body { margin: 0; background: var(--bg); color: var(--fg); }
|
|
208
|
+
body {
|
|
209
|
+
padding: 1.5rem; max-width: 56rem; margin: 0 auto;
|
|
210
|
+
font: 1rem/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
211
|
+
}
|
|
212
|
+
.banner {
|
|
213
|
+
display: inline-block; margin: 0 0 1rem; padding: 0.2rem 0.5rem;
|
|
214
|
+
font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em;
|
|
215
|
+
text-transform: uppercase; color: var(--muted); border: 1px solid var(--border);
|
|
216
|
+
}
|
|
217
|
+
.code { font-size: 2.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
|
|
218
|
+
h1 { font-size: 1.25rem; font-weight: 600; margin: 0.5rem 0 0; word-break: break-word; }
|
|
219
|
+
pre {
|
|
220
|
+
margin: 1.25rem 0 0; padding: 1rem; overflow: auto;
|
|
221
|
+
background: var(--panel); border: 1px solid var(--border);
|
|
222
|
+
font: 0.8125rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
223
|
+
white-space: pre-wrap; word-break: break-word;
|
|
224
|
+
}
|
|
225
|
+
</style>
|
|
226
|
+
</head>
|
|
227
|
+
<body>
|
|
228
|
+
<p class="banner">Development only — not shown in production</p>
|
|
229
|
+
<p class="code">${status}</p>
|
|
230
|
+
<h1>${esc(name)}: ${esc(message)}</h1>
|
|
231
|
+
<pre>${esc(body)}</pre>
|
|
232
|
+
</body>
|
|
233
|
+
</html>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Hata nesnesini yığın izi + `cause` zinciriyle metne çevirir.
|
|
238
|
+
*
|
|
239
|
+
* @param {unknown} error
|
|
240
|
+
* @returns {string}
|
|
241
|
+
*/
|
|
242
|
+
function serializeError(error) {
|
|
243
|
+
if (error == null) return "(no error object)";
|
|
244
|
+
|
|
245
|
+
/** @type {string[]} */
|
|
246
|
+
const parts = [];
|
|
247
|
+
/** @type {unknown} */
|
|
248
|
+
let current = error;
|
|
249
|
+
let depth = 0;
|
|
250
|
+
|
|
251
|
+
while (current != null && depth < 6) {
|
|
252
|
+
if (depth > 0) parts.push("\nCaused by:");
|
|
253
|
+
|
|
254
|
+
if (current instanceof Error) {
|
|
255
|
+
parts.push(current.stack || `${current.name}: ${current.message}`);
|
|
256
|
+
current = "cause" in current ? current.cause : undefined;
|
|
257
|
+
} else {
|
|
258
|
+
parts.push(String(current));
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
depth += 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return parts.join("\n");
|
|
265
|
+
}
|