jskelet 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +628 -620
  3. package/LICENSE +21 -21
  4. package/README.md +2 -0
  5. package/bin/jskelet.mjs +130 -130
  6. package/docs/01-baslangic.md +291 -291
  7. package/docs/02-mimari.md +310 -310
  8. package/docs/03-routing.md +515 -515
  9. package/docs/04-render-ve-sablonlar.md +667 -661
  10. package/docs/05-islands.md +486 -486
  11. package/docs/06-cache.md +1467 -1443
  12. package/docs/07-yapilandirma.md +1208 -1197
  13. package/docs/08-build.md +429 -429
  14. package/docs/09-dev-araclari.md +364 -364
  15. package/docs/10-dagitim.md +348 -338
  16. package/docs/12-panel-ve-oturum.md +479 -478
  17. package/docs/README.md +83 -83
  18. package/docs/en/01-getting-started.md +298 -298
  19. package/docs/en/02-architecture.md +329 -329
  20. package/docs/en/03-routing.md +531 -531
  21. package/docs/en/04-rendering.md +675 -669
  22. package/docs/en/05-islands.md +497 -497
  23. package/docs/en/06-caching.md +1476 -1453
  24. package/docs/en/07-configuration.md +1229 -1219
  25. package/docs/en/08-build.md +447 -447
  26. package/docs/en/09-dev-tools.md +373 -373
  27. package/docs/en/10-deployment.md +351 -340
  28. package/docs/en/11-migration.md +398 -398
  29. package/docs/en/12-dashboards-and-sessions.md +489 -488
  30. package/docs/en/README.md +87 -87
  31. package/package.json +137 -137
  32. package/src/build/ensure-build.mjs +19 -19
  33. package/src/build/paths.mjs +153 -153
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +349 -349
  36. package/src/build/tasks/css.mjs +235 -235
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +357 -357
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/build/tasks/templates.mjs +20 -20
  42. package/src/client/admin/i18n.js +764 -764
  43. package/src/client/admin/login.html +74 -74
  44. package/src/client/admin/panel.css +809 -809
  45. package/src/client/admin/panel.html +495 -495
  46. package/src/client/admin/panel.js +1251 -1251
  47. package/src/client/devtools/report.html +185 -185
  48. package/src/client/devtools/report.js +745 -745
  49. package/src/client/devtools/seo.js +628 -628
  50. package/src/client/dom.js +95 -95
  51. package/src/client/form.js +192 -192
  52. package/src/client/index.js +45 -45
  53. package/src/client/registry.js +305 -305
  54. package/src/client/safe-image.js +91 -91
  55. package/src/client/shared-cookie.js +225 -225
  56. package/src/client/store.js +36 -36
  57. package/src/client/swap.js +188 -188
  58. package/src/compile/codegen.js +336 -336
  59. package/src/compile/compile-all.js +149 -149
  60. package/src/compile/errors.js +66 -66
  61. package/src/compile/expr.js +409 -409
  62. package/src/compile/index.js +17 -17
  63. package/src/compile/parse.js +541 -541
  64. package/src/compile/resolve.js +211 -211
  65. package/src/compile/scan-exports.js +51 -51
  66. package/src/config/defaults.js +541 -534
  67. package/src/config/index.js +1500 -1469
  68. package/src/config/pattern.js +107 -107
  69. package/src/generate.mjs +163 -163
  70. package/src/http/control-flow.js +71 -71
  71. package/src/http/cookies-entry.js +21 -21
  72. package/src/http/cookies.js +277 -277
  73. package/src/http/request-cache.js +46 -46
  74. package/src/http/request-context.js +165 -165
  75. package/src/http/shared-cookie.js +178 -178
  76. package/src/index.js +101 -101
  77. package/src/init.mjs +232 -230
  78. package/src/migrate/apply.mjs +262 -262
  79. package/src/migrate/babel.mjs +79 -79
  80. package/src/migrate/classify.mjs +155 -155
  81. package/src/migrate/config.mjs +126 -126
  82. package/src/migrate/fs-walk.mjs +191 -191
  83. package/src/migrate/parse.mjs +26 -26
  84. package/src/migrate/scan.mjs +177 -177
  85. package/src/migrate/transform/expr-source.mjs +168 -168
  86. package/src/migrate/transform/island.mjs +67 -67
  87. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  88. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  89. package/src/migrate/transform/page-split.mjs +435 -435
  90. package/src/migrate/write.mjs +81 -81
  91. package/src/migrate.mjs +171 -171
  92. package/src/runtime/alias-hooks.mjs +119 -119
  93. package/src/runtime/register.mjs +4 -4
  94. package/src/server/admin/actions.js +229 -229
  95. package/src/server/admin/auth.js +125 -125
  96. package/src/server/admin/event-log.js +151 -151
  97. package/src/server/admin/gate.js +209 -209
  98. package/src/server/admin/inventory.js +188 -188
  99. package/src/server/admin/mount.js +56 -56
  100. package/src/server/admin/router.js +216 -216
  101. package/src/server/admin/snapshot.js +241 -241
  102. package/src/server/assets.js +147 -147
  103. package/src/server/auth/handoff.js +309 -309
  104. package/src/server/cache-blob.js +70 -70
  105. package/src/server/cache-control.js +45 -0
  106. package/src/server/cache-deps.js +42 -42
  107. package/src/server/cache-vary.js +113 -113
  108. package/src/server/cloudflare.js +607 -607
  109. package/src/server/create-app.js +366 -366
  110. package/src/server/data-cache.js +553 -553
  111. package/src/server/dev/report.js +485 -485
  112. package/src/server/dev/socket.js +170 -170
  113. package/src/server/dev/version-check.mjs +139 -139
  114. package/src/server/disk-cache.js +233 -233
  115. package/src/server/ejs-adapter.js +59 -59
  116. package/src/server/html-cache.js +1196 -1196
  117. package/src/server/image-optimizer.js +500 -500
  118. package/src/server/logs/access-middleware.js +66 -66
  119. package/src/server/logs/file-sink.js +193 -193
  120. package/src/server/logs/pipeline.js +165 -165
  121. package/src/server/logs/s3-put.js +214 -214
  122. package/src/server/logs/s3-sink.js +112 -112
  123. package/src/server/metadata.js +102 -102
  124. package/src/server/middleware/compression.js +205 -205
  125. package/src/server/middleware/csrf.js +134 -134
  126. package/src/server/middleware/dev-gate.js +75 -75
  127. package/src/server/middleware/headers.js +37 -37
  128. package/src/server/middleware/redirects.js +32 -32
  129. package/src/server/middleware/robots-txt.js +341 -341
  130. package/src/server/middleware/static-precompressed.js +121 -121
  131. package/src/server/middleware/trailing-slash.js +53 -53
  132. package/src/server/middleware/upstream-proxy.js +141 -141
  133. package/src/server/og-image.js +369 -356
  134. package/src/server/port-guard.js +255 -255
  135. package/src/server/prewarm.js +1082 -1082
  136. package/src/server/redis.js +588 -588
  137. package/src/server/render.js +910 -910
  138. package/src/server/router.js +157 -157
  139. package/src/server/status-page.js +265 -265
  140. package/src/server/upstream-limiter.js +376 -376
  141. package/src/server/upstream-tracking.js +166 -166
  142. package/src/shared/cookie-domain.js +66 -66
  143. package/src/start.mjs +22 -22
  144. package/src/templates/layout.ejs +30 -30
  145. package/src/templates/layout.jsk +30 -30
  146. package/src/version.mjs +31 -31
  147. package/src/views/components/loader.js +101 -101
  148. package/src/views/helpers/html.js +102 -102
  149. package/src/views/helpers/tags.js +375 -375
  150. package/types/config/defaults.d.ts +6 -0
  151. package/types/config/index.d.ts +6 -0
  152. package/types/server/cache-control.d.ts +28 -0
  153. package/types/server/og-image.d.ts +5 -0
@@ -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
+ }