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.
- package/AGENTS.md +136 -136
- package/CHANGELOG.md +628 -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 +667 -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 +348 -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 +675 -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 +351 -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 +369 -356
- 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 +5 -0
package/src/config/index.js
CHANGED
|
@@ -1,1469 +1,1500 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jskelet.config.mjs` yükleyicisi ve çözümlenmiş proje durumu.
|
|
3
|
-
*
|
|
4
|
-
* Bu modül framework'ün **tek gerçek kaynağıdır**: proje kökü, dizin yolları,
|
|
5
|
-
* markalama, hook'lar ve `headers/redirects/rewrites/cache` kuralları burada
|
|
6
|
-
* normalize edilir. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
|
|
7
|
-
* Böylece framework `node_modules/` içine girdiğinde hiçbir dosyada
|
|
8
|
-
* `../..` sayma hatası oluşmaz.
|
|
9
|
-
*
|
|
10
|
-
* Config dosyası **zorunlu değildir**: yoksa ya da okunamıyorsa uyarı basılır
|
|
11
|
-
* ve sunucu varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi
|
|
12
|
-
* açılamaz hâle getirmemeli.
|
|
13
|
-
*
|
|
14
|
-
* Desteklenen bölümler (hepsi opsiyonel, hepsi `async` olabilir):
|
|
15
|
-
* headers() → [{ source, headers: [{ key, value }] }]
|
|
16
|
-
* redirects() → [{ source, destination, permanent?, statusCode? }]
|
|
17
|
-
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
|
-
* cache() → { html?: { [source]: saniye },
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
import
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* @typedef {
|
|
73
|
-
*
|
|
74
|
-
* @
|
|
75
|
-
* @property {
|
|
76
|
-
* @property {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* @
|
|
83
|
-
* @property {
|
|
84
|
-
* @property {string}
|
|
85
|
-
* @property {
|
|
86
|
-
* @property {
|
|
87
|
-
* @property {boolean}
|
|
88
|
-
* @property {boolean}
|
|
89
|
-
* @property {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* @typedef {
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* @property {
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* @property {
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* @typedef {
|
|
113
|
-
*
|
|
114
|
-
* @
|
|
115
|
-
* @property {
|
|
116
|
-
* @property {
|
|
117
|
-
* @property {
|
|
118
|
-
* @property {{
|
|
119
|
-
* @property {{ pattern: CompiledPattern,
|
|
120
|
-
* @property {{ pattern: CompiledPattern,
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* @property {
|
|
130
|
-
* @property {
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* @property {
|
|
134
|
-
* @property {
|
|
135
|
-
* @property {
|
|
136
|
-
* @property {
|
|
137
|
-
* @property {
|
|
138
|
-
* @property {
|
|
139
|
-
* @property {
|
|
140
|
-
* @property {
|
|
141
|
-
* @property {
|
|
142
|
-
* @property {string}
|
|
143
|
-
* @property {string
|
|
144
|
-
* @property {
|
|
145
|
-
* @property {{
|
|
146
|
-
* @property {
|
|
147
|
-
*
|
|
148
|
-
* @property {string[]}
|
|
149
|
-
* @property {
|
|
150
|
-
* @property {
|
|
151
|
-
* @property {
|
|
152
|
-
*
|
|
153
|
-
* @property {string[]}
|
|
154
|
-
* @property {
|
|
155
|
-
* @property {
|
|
156
|
-
* @property {
|
|
157
|
-
* @property {string[]}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
* @
|
|
162
|
-
* @property {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
* @
|
|
167
|
-
* @property {
|
|
168
|
-
* @property {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* @
|
|
173
|
-
* @property {number
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
envText(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
/** @type {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
.
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
*
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const source = /** @type {Record<string, unknown>} */ (raw);
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
* `
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
* @
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
.
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
* @param {
|
|
1252
|
-
* @param {string} [
|
|
1253
|
-
* @returns {string}
|
|
1254
|
-
*/
|
|
1255
|
-
function
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
)
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet.config.mjs` yükleyicisi ve çözümlenmiş proje durumu.
|
|
3
|
+
*
|
|
4
|
+
* Bu modül framework'ün **tek gerçek kaynağıdır**: proje kökü, dizin yolları,
|
|
5
|
+
* markalama, hook'lar ve `headers/redirects/rewrites/cache` kuralları burada
|
|
6
|
+
* normalize edilir. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
|
|
7
|
+
* Böylece framework `node_modules/` içine girdiğinde hiçbir dosyada
|
|
8
|
+
* `../..` sayma hatası oluşmaz.
|
|
9
|
+
*
|
|
10
|
+
* Config dosyası **zorunlu değildir**: yoksa ya da okunamıyorsa uyarı basılır
|
|
11
|
+
* ve sunucu varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi
|
|
12
|
+
* açılamaz hâle getirmemeli.
|
|
13
|
+
*
|
|
14
|
+
* Desteklenen bölümler (hepsi opsiyonel, hepsi `async` olabilir):
|
|
15
|
+
* headers() → [{ source, headers: [{ key, value }] }]
|
|
16
|
+
* redirects() → [{ source, destination, permanent?, statusCode? }]
|
|
17
|
+
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
|
+
* cache() → { html?: { [source]: saniye },
|
|
19
|
+
* staleWhileRevalidate?: number,
|
|
20
|
+
* query?: { [source]: string[] | true },
|
|
21
|
+
* vary?: { host?: boolean, headers?: string[], fn?: Function },
|
|
22
|
+
* maxEntries?: number,
|
|
23
|
+
* data?: {...}, redis?: {...}, prewarm?: {...} }
|
|
24
|
+
* admin() → { enabled?, basePath?, allowIps?, blockBots?, … }
|
|
25
|
+
* auth → { crossSubdomainHandoff?: boolean | object }
|
|
26
|
+
* logs → { console?, kinds?, file?, s3? }
|
|
27
|
+
*
|
|
28
|
+
* Fonksiyon olmayan bölümler (`brand`, `security`, `static`, `navigation`…)
|
|
29
|
+
* düz nesne olarak okunur. `logs` fonksiyon ya da düz nesne olabilir.
|
|
30
|
+
*/
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import process from "node:process";
|
|
34
|
+
import { pathToFileURL } from "node:url";
|
|
35
|
+
import { compilePattern, matchPattern } from "./pattern.js";
|
|
36
|
+
import {
|
|
37
|
+
DEFAULT_ADMIN,
|
|
38
|
+
DEFAULT_AUTH,
|
|
39
|
+
DEFAULT_BRAND,
|
|
40
|
+
DEFAULT_CLOUDFLARE,
|
|
41
|
+
DATA_CACHE_MAX_ENTRIES_CEILING,
|
|
42
|
+
DEFAULT_DATA_CACHE,
|
|
43
|
+
DEFAULT_DEV_GATE_BYPASS,
|
|
44
|
+
DEFAULT_DIRS,
|
|
45
|
+
DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
46
|
+
HTML_CACHE_MAX_ENTRIES_CEILING,
|
|
47
|
+
ON_VISIT_CONCURRENCY_CEILING,
|
|
48
|
+
ON_VISIT_PER_PAGE_CEILING,
|
|
49
|
+
ON_VISIT_RPS_CEILING,
|
|
50
|
+
DEFAULT_IMAGES,
|
|
51
|
+
DEFAULT_LOGS,
|
|
52
|
+
DEFAULT_NAVIGATION,
|
|
53
|
+
DEFAULT_NAVIGATION_EXCLUDE,
|
|
54
|
+
DEFAULT_PREWARM,
|
|
55
|
+
DEFAULT_PREWARM_ON_VISIT,
|
|
56
|
+
DEFAULT_PREWARM_SKIP,
|
|
57
|
+
CLASSIC_PREWARM_KEYS,
|
|
58
|
+
DEFAULT_REDIS,
|
|
59
|
+
DEFAULT_SECURITY,
|
|
60
|
+
DEFAULT_STALE_WHILE_REVALIDATE,
|
|
61
|
+
DEFAULT_STATIC,
|
|
62
|
+
DEFAULT_TRANSIENT_RETRY,
|
|
63
|
+
DEFAULT_UPSTREAM_LIMIT,
|
|
64
|
+
} from "./defaults.js";
|
|
65
|
+
|
|
66
|
+
/** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
|
|
67
|
+
export const FRAMEWORK_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
68
|
+
|
|
69
|
+
const CONFIG_FILE = "jskelet.config.mjs";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {"conservative" | "moderate" | "eager"} Eagerness
|
|
73
|
+
*
|
|
74
|
+
* @typedef {object} NavigationConfig
|
|
75
|
+
* @property {false | Eagerness} prefetch
|
|
76
|
+
* @property {false | Eagerness} prerender
|
|
77
|
+
* @property {boolean} viewTransition
|
|
78
|
+
* @property {string[]} exclude Spekülasyon dışı bırakılan href desenleri.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {object} RedisConfig
|
|
83
|
+
* @property {boolean} enabled
|
|
84
|
+
* @property {string | null} url
|
|
85
|
+
* @property {string} namespace
|
|
86
|
+
* @property {string} keyPrefix
|
|
87
|
+
* @property {boolean} html HTML gövdeleri paylaşılsın mı.
|
|
88
|
+
* @property {boolean} data Veri önbelleği paylaşılsın mı.
|
|
89
|
+
* @property {boolean} storeEncoded Sıkıştırılmış gövdeler de paylaşılsın mı.
|
|
90
|
+
* @property {boolean} events pub/sub invalidation yayını.
|
|
91
|
+
* @property {number} commandTimeoutMs
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @typedef {"http" | "event" | "error"} LogKind
|
|
96
|
+
*
|
|
97
|
+
* @typedef {object} LogsConfig
|
|
98
|
+
* @property {boolean} console Runtime http/event/error satırları stdout'a
|
|
99
|
+
* basılsın mı (banner/build satırları etkilenmez).
|
|
100
|
+
* @property {LogKind[]} kinds Sink'lere giden kayıt türleri.
|
|
101
|
+
* @property {{ enabled: boolean, dir: string, rotate: "daily" }} file
|
|
102
|
+
* `rotate` durur; dosya parçaları en fazla 5 dakika tutulur.
|
|
103
|
+
* @property {import('../server/logs/file-sink.js').DrainLog | null} drainLog
|
|
104
|
+
* Mühürlenen zstd parçasını uygulamanın seçtiği yere aktarır. Hata
|
|
105
|
+
* siteyi düşürmez.
|
|
106
|
+
* @property {{ enabled: boolean, bucket: string | null, prefix: string,
|
|
107
|
+
* region: string | null, endpoint: string | null, flushIntervalMs: number,
|
|
108
|
+
* maxBatch: number }} s3
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @typedef {import('./pattern.js').CompiledPattern} CompiledPattern
|
|
113
|
+
*
|
|
114
|
+
* @typedef {object} ResolvedConfig
|
|
115
|
+
* @property {string} root Proje kökü (mutlak).
|
|
116
|
+
* @property {boolean} loaded Config dosyası okundu mu.
|
|
117
|
+
* @property {Record<string, string>} dirs Mutlak dizin yolları.
|
|
118
|
+
* @property {{ pattern: CompiledPattern, headers: { key: string, value: string }[] }[]} headers
|
|
119
|
+
* @property {{ pattern: CompiledPattern, destination: string, statusCode: number }[]} redirects
|
|
120
|
+
* @property {{ phase: "beforeFiles" | "afterFiles", pattern: CompiledPattern, destination: string }[]} rewrites
|
|
121
|
+
* @property {{ pattern: CompiledPattern, seconds: number }[]} html
|
|
122
|
+
* @property {{ pattern: CompiledPattern, allow: true | string[] }[]} cacheQuery
|
|
123
|
+
* Yol deseni başına, HTML cache anahtarına girmesine izin verilen query
|
|
124
|
+
* parametreleri. Eşleşen kural yoksa query'li istek cache'lenmez.
|
|
125
|
+
* @property {{ host: boolean, headers: string[],
|
|
126
|
+
* fn: ((req: import('express').Request) => string | null | undefined) | null }} cacheVary
|
|
127
|
+
* Anahtara eklenen sabit parçalar (query allowlist'ten bağımsız). Host'tan
|
|
128
|
+
* locale üreten sitelerde `host: true` zorunlu.
|
|
129
|
+
* @property {number} htmlMaxEntries HTML önbelleğinin girdi sınırı.
|
|
130
|
+
* @property {number} staleWhileRevalidate Edge taze penceresi bittikten sonra
|
|
131
|
+
* eski HTML'in sunulacağı süre (saniye). 0 ise direktif basılmaz. Süreç içi
|
|
132
|
+
* HTML cache'in stale penceresinden bağımsızdır.
|
|
133
|
+
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
134
|
+
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
135
|
+
* @property {boolean} trackDependencies Render'ın okuduğu veri anahtarları kaydedilsin mi.
|
|
136
|
+
* @property {{ attempts: number, delayMs: number }} transientRetry
|
|
137
|
+
* @property {RedisConfig} redis Opsiyonel Redis ikinci kademesi.
|
|
138
|
+
* @property {typeof DEFAULT_UPSTREAM_LIMIT} upstream Upstream hız freni.
|
|
139
|
+
* @property {LogsConfig} logs Kalıcı log sink'leri (dosya + S3).
|
|
140
|
+
* @property {typeof DEFAULT_ADMIN} admin Framework yönetim paneli.
|
|
141
|
+
* @property {typeof DEFAULT_CLOUDFLARE} cloudflare Cloudflare cache yüzeyi.
|
|
142
|
+
* @property {Record<string, unknown>} prewarm
|
|
143
|
+
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
144
|
+
* @property {Record<string, unknown>} brand
|
|
145
|
+
* @property {{ crossSubdomainHandoff: boolean | Record<string, unknown> }} auth
|
|
146
|
+
* @property {Record<string, Function>} hooks
|
|
147
|
+
* @property {string} layout Layout `.ejs` dosyasının mutlak yolu.
|
|
148
|
+
* @property {string[] | null} routes Açık route modülü listesi.
|
|
149
|
+
* @property {boolean} trailingSlash URL'ler `/` ile bitsin mi (Next `trailingSlash`).
|
|
150
|
+
* @property {{ extensions: Set<string>, prefixes: string[] }} static
|
|
151
|
+
* @property {boolean} devGate `DEV_TOKEN` tek başına siteyi kilitlemez; gate
|
|
152
|
+
* ancak bu bayrak veya `DEV_GATE=1` ile açılır.
|
|
153
|
+
* @property {string[]} devGateBypass
|
|
154
|
+
* @property {string[]} preconnect
|
|
155
|
+
* @property {NavigationConfig} navigation
|
|
156
|
+
* @property {SecurityConfig} security
|
|
157
|
+
* @property {string[]} prewarmSkip
|
|
158
|
+
* @property {string[]} watch Dev sunucusunun izlediği ek dizinler.
|
|
159
|
+
* @property {{ family: string, slug?: string, weights: number[] }[]} fonts
|
|
160
|
+
* @property {{ scan?: string[], dir: string } | false} icons
|
|
161
|
+
* @property {ImagesConfig | false} images
|
|
162
|
+
* @property {string[]} clientEnv Client bundle'a gömülecek env anahtarları.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @typedef {object} ImagesRemoteConfig
|
|
167
|
+
* @property {boolean} enabled
|
|
168
|
+
* @property {string[]} allowHosts
|
|
169
|
+
* @property {string} path
|
|
170
|
+
* @property {number} maxWidth
|
|
171
|
+
* @property {number} cacheMaxAge
|
|
172
|
+
* @property {number} fetchTimeoutMs
|
|
173
|
+
* @property {number} maxBytes
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @typedef {object} ImagesConfig
|
|
178
|
+
* @property {number[]} widths
|
|
179
|
+
* @property {number} quality
|
|
180
|
+
* @property {string[]} skip
|
|
181
|
+
* @property {ImagesRemoteConfig | false} remote
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
/** @type {ResolvedConfig | null} */
|
|
185
|
+
let config = null;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {unknown} value
|
|
189
|
+
* @param {string} label
|
|
190
|
+
* @returns {unknown[]}
|
|
191
|
+
*/
|
|
192
|
+
function asArray(value, label) {
|
|
193
|
+
if (value == null) return [];
|
|
194
|
+
if (Array.isArray(value)) return value;
|
|
195
|
+
console.warn(`[config] ${label} must return an array, ignoring it`);
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown} raw
|
|
201
|
+
* @returns {ResolvedConfig["headers"]}
|
|
202
|
+
*/
|
|
203
|
+
function normalizeHeaders(raw) {
|
|
204
|
+
/** @type {ResolvedConfig["headers"]} */
|
|
205
|
+
const out = [];
|
|
206
|
+
|
|
207
|
+
for (const entry of asArray(raw, "headers()")) {
|
|
208
|
+
const pattern = compilePattern(entry?.source);
|
|
209
|
+
if (!pattern) continue;
|
|
210
|
+
|
|
211
|
+
const headers = asArray(entry?.headers, "headers()[].headers")
|
|
212
|
+
.filter((header) => header?.key && header?.value !== undefined)
|
|
213
|
+
.map((header) => ({ key: String(header.key), value: String(header.value) }));
|
|
214
|
+
|
|
215
|
+
if (headers.length) out.push({ pattern, headers });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {unknown} raw
|
|
223
|
+
* @returns {ResolvedConfig["redirects"]}
|
|
224
|
+
*/
|
|
225
|
+
function normalizeRedirects(raw) {
|
|
226
|
+
/** @type {ResolvedConfig["redirects"]} */
|
|
227
|
+
const out = [];
|
|
228
|
+
|
|
229
|
+
for (const entry of asArray(raw, "redirects()")) {
|
|
230
|
+
const pattern = compilePattern(entry?.source);
|
|
231
|
+
if (!pattern || typeof entry?.destination !== "string") continue;
|
|
232
|
+
|
|
233
|
+
// Next semantiği: permanent → 308, geçici → 307. Farklı bir kod isteyen
|
|
234
|
+
// `statusCode` verebilir (ör. eski kurulumlarla uyum için 301).
|
|
235
|
+
const statusCode = Number(entry.statusCode) || (entry.permanent ? 308 : 307);
|
|
236
|
+
|
|
237
|
+
out.push({ pattern, destination: entry.destination, statusCode });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {unknown} raw
|
|
245
|
+
* @returns {ResolvedConfig["rewrites"]}
|
|
246
|
+
*/
|
|
247
|
+
function normalizeRewrites(raw) {
|
|
248
|
+
/** @type {ResolvedConfig["rewrites"]} */
|
|
249
|
+
const out = [];
|
|
250
|
+
|
|
251
|
+
/** @type {[("beforeFiles" | "afterFiles"), unknown][]} */
|
|
252
|
+
const phases = Array.isArray(raw)
|
|
253
|
+
? [["afterFiles", raw]]
|
|
254
|
+
: [
|
|
255
|
+
["beforeFiles", raw?.beforeFiles],
|
|
256
|
+
["afterFiles", raw?.afterFiles],
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
for (const [phase, entries] of phases) {
|
|
260
|
+
for (const entry of asArray(entries, `rewrites().${phase}`)) {
|
|
261
|
+
const pattern = compilePattern(entry?.source);
|
|
262
|
+
if (!pattern || typeof entry?.destination !== "string") continue;
|
|
263
|
+
out.push({ phase, pattern, destination: entry.destination });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Isıtma sırası desenleri. İki biçim kabul edilir: config'in her yerinde
|
|
272
|
+
* geçerli olan `/haber/:slug` sözdizimi ve doğrudan `RegExp` — ikincisi
|
|
273
|
+
* "sonu `-yorumlar` ile bitenler" gibi desen sözdiziminin karşılamadığı
|
|
274
|
+
* kuralları yazabilmek için.
|
|
275
|
+
*
|
|
276
|
+
* @param {unknown} raw
|
|
277
|
+
* @returns {ResolvedConfig["prewarmPriority"]}
|
|
278
|
+
*/
|
|
279
|
+
function normalizePriority(raw) {
|
|
280
|
+
/** @type {ResolvedConfig["prewarmPriority"]} */
|
|
281
|
+
const out = [];
|
|
282
|
+
|
|
283
|
+
for (const entry of asArray(raw, "cache().prewarm.priority")) {
|
|
284
|
+
if (entry instanceof RegExp) {
|
|
285
|
+
out.push({ source: String(entry), test: (pathname) => entry.test(pathname) });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const pattern = compilePattern(entry);
|
|
290
|
+
if (!pattern) continue;
|
|
291
|
+
out.push({
|
|
292
|
+
source: pattern.source,
|
|
293
|
+
test: (pathname) => matchPattern(pattern, pathname) !== null,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Redis bölümü. Bozuk bir değer sunucuyu düşürmemeli: her alan tipine
|
|
302
|
+
* zorlanır ve `enabled` yalnızca açıkça `true` verildiğinde açılır.
|
|
303
|
+
*
|
|
304
|
+
* @param {unknown} raw
|
|
305
|
+
* @returns {RedisConfig}
|
|
306
|
+
*/
|
|
307
|
+
function normalizeRedis(raw) {
|
|
308
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
309
|
+
const timeout = Number(source.commandTimeoutMs);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
enabled: source.enabled === true,
|
|
313
|
+
url: typeof source.url === "string" && source.url ? source.url : null,
|
|
314
|
+
namespace: String(source.namespace ?? DEFAULT_REDIS.namespace),
|
|
315
|
+
keyPrefix: String(source.keyPrefix ?? DEFAULT_REDIS.keyPrefix),
|
|
316
|
+
html: source.html !== false,
|
|
317
|
+
data: source.data !== false,
|
|
318
|
+
storeEncoded: source.storeEncoded === true,
|
|
319
|
+
events: source.events !== false,
|
|
320
|
+
commandTimeoutMs:
|
|
321
|
+
Number.isFinite(timeout) && timeout > 0
|
|
322
|
+
? Math.floor(timeout)
|
|
323
|
+
: DEFAULT_REDIS.commandTimeoutMs,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Upstream hız freni. Sayısal alanlar tipine zorlanır; bozuk bir değer freni
|
|
329
|
+
* yanlış ayarlamak yerine varsayılana döner.
|
|
330
|
+
*
|
|
331
|
+
* @param {unknown} raw
|
|
332
|
+
* @returns {typeof DEFAULT_UPSTREAM_LIMIT}
|
|
333
|
+
*/
|
|
334
|
+
function normalizeUpstream(raw) {
|
|
335
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
336
|
+
const merged = { ...DEFAULT_UPSTREAM_LIMIT, ...source };
|
|
337
|
+
|
|
338
|
+
/** @param {string} key */
|
|
339
|
+
const positive = (key) => {
|
|
340
|
+
const value = Number(merged[key]);
|
|
341
|
+
return Number.isFinite(value) && value >= 0
|
|
342
|
+
? value
|
|
343
|
+
: /** @type {any} */ (DEFAULT_UPSTREAM_LIMIT)[key];
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** @type {Record<string, Record<string, number>>} */
|
|
347
|
+
const hosts = {};
|
|
348
|
+
for (const [host, override] of Object.entries(merged.hosts ?? {})) {
|
|
349
|
+
if (override && typeof override === "object") hosts[host] = override;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...merged,
|
|
354
|
+
rate: positive("rate"),
|
|
355
|
+
burst: positive("burst"),
|
|
356
|
+
concurrency: Math.max(1, Math.floor(positive("concurrency"))),
|
|
357
|
+
minRate: positive("minRate"),
|
|
358
|
+
increaseStep: positive("increaseStep"),
|
|
359
|
+
increaseIntervalMs: positive("increaseIntervalMs"),
|
|
360
|
+
decreaseIntervalMs: positive("decreaseIntervalMs"),
|
|
361
|
+
breakerFailures: Math.floor(positive("breakerFailures")),
|
|
362
|
+
breakerCooldownMs: positive("breakerCooldownMs"),
|
|
363
|
+
hosts,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Dev gate. `DEV_TOKEN` ortamda durması siteyi kilitlemez: paylaşılan bir
|
|
369
|
+
* task tanımı production'a da aynı değişkeni taşır ve herkese 404 olur.
|
|
370
|
+
* Gate ancak `devGate: true` ya da `DEV_GATE=1` ile açılır. `DEV_GATE=0`
|
|
371
|
+
* config'teki açığı da kapatır.
|
|
372
|
+
*
|
|
373
|
+
* @param {Record<string, any>} source
|
|
374
|
+
* @returns {boolean}
|
|
375
|
+
*/
|
|
376
|
+
function normalizeDevGate(source) {
|
|
377
|
+
const env = process.env.DEV_GATE;
|
|
378
|
+
if (env === "0" || env === "false") return false;
|
|
379
|
+
if (env === "1" || env === "true") return true;
|
|
380
|
+
return source.devGate === true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Yönetim paneli. `enabled` yalnızca açıkça `true` verildiğinde ya da
|
|
385
|
+
* `JSKELET_ADMIN` ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla
|
|
386
|
+
* açmanın bedeli, önbelleği boşaltabilen bir ucu internete koymak.
|
|
387
|
+
*
|
|
388
|
+
* Ortam değişkeni config'in **üstünde** duruyor, çünkü paneli genelde bir
|
|
389
|
+
* arıza sırasında tek seferlik açmak isteniyor ve o an config dosyasını
|
|
390
|
+
* değiştirip yeniden dağıtmak istenmiyor. `JSKELET_ADMIN=0` aynı mantıkla
|
|
391
|
+
* config'te açık olan paneli kapatır.
|
|
392
|
+
*
|
|
393
|
+
* @param {unknown} raw
|
|
394
|
+
* @returns {typeof DEFAULT_ADMIN}
|
|
395
|
+
*/
|
|
396
|
+
function normalizeAdmin(raw) {
|
|
397
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
398
|
+
const env = process.env.JSKELET_ADMIN;
|
|
399
|
+
|
|
400
|
+
const basePath =
|
|
401
|
+
typeof source.basePath === "string" && source.basePath.startsWith("/")
|
|
402
|
+
? source.basePath.replace(/\/+$/, "")
|
|
403
|
+
: DEFAULT_ADMIN.basePath;
|
|
404
|
+
|
|
405
|
+
/** @param {string} key @param {number} min */
|
|
406
|
+
const positive = (key, min) => {
|
|
407
|
+
const value = Number(source[key]);
|
|
408
|
+
return Number.isFinite(value) && value >= min
|
|
409
|
+
? value
|
|
410
|
+
: /** @type {any} */ (DEFAULT_ADMIN)[key];
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const allowIps = Array.isArray(source.allowIps)
|
|
414
|
+
? source.allowIps
|
|
415
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
416
|
+
.map((entry) => entry.trim())
|
|
417
|
+
: [...DEFAULT_ADMIN.allowIps];
|
|
418
|
+
|
|
419
|
+
const logSize = Number(source.logSize);
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
enabled:
|
|
423
|
+
env === undefined
|
|
424
|
+
? source.enabled === true
|
|
425
|
+
: env !== "0" && env !== "false" && env !== "",
|
|
426
|
+
basePath: basePath || DEFAULT_ADMIN.basePath,
|
|
427
|
+
allowIps,
|
|
428
|
+
blockBots: source.blockBots !== false,
|
|
429
|
+
banAttempts: Math.floor(positive("banAttempts", 1)),
|
|
430
|
+
banHours: positive("banHours", 0),
|
|
431
|
+
sessionHours: positive("sessionHours", 0),
|
|
432
|
+
logSize:
|
|
433
|
+
Number.isFinite(logSize) && logSize >= 50
|
|
434
|
+
? Math.min(5000, Math.floor(logSize))
|
|
435
|
+
: DEFAULT_ADMIN.logSize,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Cloudflare bölümü. Token burada da verilebiliyor ama önerilen yol env;
|
|
441
|
+
* normalizasyon sadece tipleri sabitler, sırrı okumak `cloudflare.js`'in işi.
|
|
442
|
+
*
|
|
443
|
+
* @param {unknown} raw
|
|
444
|
+
* @returns {typeof DEFAULT_CLOUDFLARE}
|
|
445
|
+
*/
|
|
446
|
+
function normalizeCloudflare(raw) {
|
|
447
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
448
|
+
const hours = Number(source.analyticsHours);
|
|
449
|
+
|
|
450
|
+
/** @param {unknown} value */
|
|
451
|
+
const text = (value) => (typeof value === "string" && value ? value : null);
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
enabled: source.enabled !== false,
|
|
455
|
+
zoneId: text(source.zoneId),
|
|
456
|
+
apiToken: text(source.apiToken),
|
|
457
|
+
// Şema yazılırsa purge URL'i `https://https://…` olur; baştaki şema atılır.
|
|
458
|
+
hostname: text(source.hostname)?.replace(/^https?:\/\//, "") ?? null,
|
|
459
|
+
analyticsHours:
|
|
460
|
+
Number.isFinite(hours) && hours > 0
|
|
461
|
+
? Math.min(72, Math.floor(hours))
|
|
462
|
+
: DEFAULT_CLOUDFLARE.analyticsHours,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const LOG_KINDS = new Set(["http", "event", "error"]);
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* `bucket`, `JSKELET_LOG_BUCKET` veya `JSKELET_S3_BUCKET` değeri
|
|
470
|
+
* `ayberkenis/jskelet/logs` gibi bir yol olabilir: ilk segment bucket adı,
|
|
471
|
+
* kalanı nesne öneki. Böylece tek env ile hem kova hem klasör verilmiş olur.
|
|
472
|
+
*
|
|
473
|
+
* @param {string | null} value
|
|
474
|
+
* @returns {{ bucket: string | null, prefix: string | null }}
|
|
475
|
+
* `prefix` null → yol öneki taşımıyor; config/varsayılan kalsın.
|
|
476
|
+
*/
|
|
477
|
+
export function splitS3BucketPath(value) {
|
|
478
|
+
if (!value) return { bucket: null, prefix: null };
|
|
479
|
+
|
|
480
|
+
const trimmed = value.replace(/^\/+|\/+$/g, "");
|
|
481
|
+
if (!trimmed) return { bucket: null, prefix: null };
|
|
482
|
+
|
|
483
|
+
const slash = trimmed.indexOf("/");
|
|
484
|
+
if (slash < 0) return { bucket: trimmed, prefix: null };
|
|
485
|
+
|
|
486
|
+
const bucket = trimmed.slice(0, slash);
|
|
487
|
+
const rest = trimmed.slice(slash + 1).replace(/^\/+|\/+$/g, "");
|
|
488
|
+
if (!bucket) return { bucket: null, prefix: null };
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
bucket,
|
|
492
|
+
prefix: rest ? `${rest}/` : null,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* @param {unknown} value
|
|
498
|
+
* @returns {string | null}
|
|
499
|
+
*/
|
|
500
|
+
function envText(value) {
|
|
501
|
+
return typeof value === "string" && value ? value : null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* @returns {{ accessKeyId: string, secretAccessKey: string,
|
|
506
|
+
* sessionToken: string | null } | null}
|
|
507
|
+
*/
|
|
508
|
+
export function readS3CredentialsFromEnv() {
|
|
509
|
+
const accessKeyId = envText(process.env.JSKELET_S3_ACCESS_KEY_ID);
|
|
510
|
+
const secretAccessKey =
|
|
511
|
+
envText(process.env.JSKELET_S3_SECRET_ACCESS_KEY) ??
|
|
512
|
+
envText(process.env.JSKELET_S3_ACCESS_SECRET);
|
|
513
|
+
if (!accessKeyId || !secretAccessKey) return null;
|
|
514
|
+
return {
|
|
515
|
+
accessKeyId,
|
|
516
|
+
secretAccessKey,
|
|
517
|
+
sessionToken: envText(process.env.JSKELET_S3_SESSION_TOKEN),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Log hedefi. Öncelik: `JSKELET_LOG_BUCKET` → `JSKELET_S3_BUCKET`
|
|
523
|
+
* (+ isteğe bağlı `JSKELET_S3_KEY_PREFIX`).
|
|
524
|
+
*
|
|
525
|
+
* @returns {string | null}
|
|
526
|
+
*/
|
|
527
|
+
function resolveLogBucketEnv() {
|
|
528
|
+
const logPath = envText(process.env.JSKELET_LOG_BUCKET);
|
|
529
|
+
if (logPath) return logPath;
|
|
530
|
+
|
|
531
|
+
const bucket = envText(process.env.JSKELET_S3_BUCKET);
|
|
532
|
+
if (!bucket) return null;
|
|
533
|
+
|
|
534
|
+
const prefix = envText(process.env.JSKELET_S3_KEY_PREFIX);
|
|
535
|
+
return prefix ? `${bucket}/${prefix}` : bucket;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Kalıcı log sink'leri. Bozuk bir `kinds` listesi siteyi düşürmemeli —
|
|
540
|
+
* bilinmeyen girdiler atılır; hiç geçerli tür kalmazsa varsayılana dönülür.
|
|
541
|
+
*
|
|
542
|
+
* @param {unknown} raw
|
|
543
|
+
* @returns {LogsConfig}
|
|
544
|
+
*/
|
|
545
|
+
export function normalizeLogs(raw) {
|
|
546
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
547
|
+
const fileRaw = /** @type {Record<string, any>} */ (source.file ?? {});
|
|
548
|
+
const s3Raw = /** @type {Record<string, any>} */ (source.s3 ?? {});
|
|
549
|
+
|
|
550
|
+
/** @type {LogKind[]} */
|
|
551
|
+
let kinds = DEFAULT_LOGS.kinds;
|
|
552
|
+
if (Array.isArray(source.kinds)) {
|
|
553
|
+
const filtered = source.kinds.filter(
|
|
554
|
+
(entry) => typeof entry === "string" && LOG_KINDS.has(entry),
|
|
555
|
+
);
|
|
556
|
+
if (filtered.length) kinds = /** @type {LogKind[]} */ ([...new Set(filtered)]);
|
|
557
|
+
else {
|
|
558
|
+
console.warn(
|
|
559
|
+
"[config] logs.kinds has no valid entries (http|event|error), using defaults",
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
} else if (source.kinds != null) {
|
|
563
|
+
console.warn("[config] logs.kinds must be an array, using defaults");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const flush = Number(s3Raw.flushIntervalMs);
|
|
567
|
+
const batch = Number(s3Raw.maxBatch);
|
|
568
|
+
|
|
569
|
+
const bucketPath = splitS3BucketPath(
|
|
570
|
+
resolveLogBucketEnv() ?? envText(s3Raw.bucket),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const configPrefix =
|
|
574
|
+
typeof s3Raw.prefix === "string" && s3Raw.prefix
|
|
575
|
+
? s3Raw.prefix.endsWith("/")
|
|
576
|
+
? s3Raw.prefix
|
|
577
|
+
: `${s3Raw.prefix}/`
|
|
578
|
+
: DEFAULT_LOGS.s3.prefix;
|
|
579
|
+
|
|
580
|
+
const endpoint =
|
|
581
|
+
envText(process.env.JSKELET_S3_API_URL) ?? envText(s3Raw.endpoint);
|
|
582
|
+
|
|
583
|
+
// Uyumlu API'lerde (Cloudflare R2 vb.) imza bölgesi çoğu zaman `auto`.
|
|
584
|
+
// Region hiçbir kurulumda zorunlu değil.
|
|
585
|
+
const region =
|
|
586
|
+
envText(s3Raw.region) ??
|
|
587
|
+
envText(process.env.JSKELET_S3_REGION) ??
|
|
588
|
+
"auto";
|
|
589
|
+
|
|
590
|
+
/** @type {import('../server/logs/file-sink.js').DrainLog | null} */
|
|
591
|
+
let drainLog = null;
|
|
592
|
+
if (typeof source.drainLog === "function") {
|
|
593
|
+
drainLog = source.drainLog;
|
|
594
|
+
} else if (source.drainLog != null) {
|
|
595
|
+
console.warn("[config] logs.drainLog must be a function, ignoring it");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const credentials = readS3CredentialsFromEnv();
|
|
599
|
+
// `JSKELET_LOG_BUCKET` (veya S3 bucket) + credential varsa config'te
|
|
600
|
+
// `enabled: true` unutulmuş olsa bile aç. Açık `enabled: false` ezer.
|
|
601
|
+
const envWantsLogs = Boolean(
|
|
602
|
+
envText(process.env.JSKELET_LOG_BUCKET) ||
|
|
603
|
+
envText(process.env.JSKELET_S3_BUCKET),
|
|
604
|
+
);
|
|
605
|
+
const enabled =
|
|
606
|
+
s3Raw.enabled === true ||
|
|
607
|
+
(s3Raw.enabled !== false &&
|
|
608
|
+
envWantsLogs &&
|
|
609
|
+
Boolean(credentials) &&
|
|
610
|
+
Boolean(bucketPath.bucket));
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
console: source.console !== false,
|
|
614
|
+
kinds,
|
|
615
|
+
file: {
|
|
616
|
+
enabled: fileRaw.enabled === true,
|
|
617
|
+
dir:
|
|
618
|
+
typeof fileRaw.dir === "string" && fileRaw.dir.trim()
|
|
619
|
+
? fileRaw.dir.trim()
|
|
620
|
+
: DEFAULT_LOGS.file.dir,
|
|
621
|
+
rotate: "daily",
|
|
622
|
+
},
|
|
623
|
+
drainLog,
|
|
624
|
+
s3: {
|
|
625
|
+
enabled,
|
|
626
|
+
bucket: bucketPath.bucket,
|
|
627
|
+
// Yoldaki önek tek env ile klasör vermeyi mümkün kılar; yoksa config.
|
|
628
|
+
prefix: bucketPath.prefix ?? configPrefix,
|
|
629
|
+
region,
|
|
630
|
+
endpoint,
|
|
631
|
+
flushIntervalMs:
|
|
632
|
+
Number.isFinite(flush) && flush >= 500
|
|
633
|
+
? Math.min(60_000, Math.floor(flush))
|
|
634
|
+
: DEFAULT_LOGS.s3.flushIntervalMs,
|
|
635
|
+
maxBatch:
|
|
636
|
+
Number.isFinite(batch) && batch >= 1
|
|
637
|
+
? Math.min(5000, Math.floor(batch))
|
|
638
|
+
: DEFAULT_LOGS.s3.maxBatch,
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* `cache().query` → yol deseni başına, cache anahtarına girmesine izin verilen
|
|
645
|
+
* query parametreleri.
|
|
646
|
+
*
|
|
647
|
+
* Varsayılan bilinçli olarak "query varsa sayfa dinamik": bir yolun bütün
|
|
648
|
+
* query varyantlarını cache'lemek, `?utm_source=…` gibi sonsuz sayıda anahtar
|
|
649
|
+
* üretip LRU'daki gerçek sayfaları dışarı atıyor. Hangi parametrenin çıktıyı
|
|
650
|
+
* gerçekten değiştirdiğini yalnızca uygulama bilir, o yüzden izin listesi
|
|
651
|
+
* config'ten gelir.
|
|
652
|
+
*
|
|
653
|
+
* Bir desen `true` ile eşlenirse bütün parametreler anahtara girer (eski
|
|
654
|
+
* davranış), `[]` ile eşlenirse hiçbiri girmez — yani query yok sayılır ve
|
|
655
|
+
* bütün varyantlar query'siz sürümün HTML'ini paylaşır.
|
|
656
|
+
*
|
|
657
|
+
* @param {unknown} raw
|
|
658
|
+
* @returns {ResolvedConfig["cacheQuery"]}
|
|
659
|
+
*/
|
|
660
|
+
function normalizeQueryRules(raw) {
|
|
661
|
+
/** @type {ResolvedConfig["cacheQuery"]} */
|
|
662
|
+
const out = [];
|
|
663
|
+
|
|
664
|
+
for (const [source, value] of Object.entries(raw ?? {})) {
|
|
665
|
+
const pattern = compilePattern(source);
|
|
666
|
+
if (!pattern) continue;
|
|
667
|
+
|
|
668
|
+
if (value === true) {
|
|
669
|
+
out.push({ pattern, allow: true });
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (value === false) continue;
|
|
673
|
+
|
|
674
|
+
const allow = asArray(
|
|
675
|
+
typeof value === "string" ? [value] : value,
|
|
676
|
+
`cache().query["${source}"]`,
|
|
677
|
+
)
|
|
678
|
+
.filter((name) => typeof name === "string" && name)
|
|
679
|
+
.map(String);
|
|
680
|
+
out.push({ pattern, allow });
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `cache().vary` → HTML anahtarına host / header / özel fn parçası.
|
|
688
|
+
*
|
|
689
|
+
* @param {unknown} raw
|
|
690
|
+
* @returns {ResolvedConfig["cacheVary"]}
|
|
691
|
+
*/
|
|
692
|
+
function normalizeVary(raw) {
|
|
693
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
694
|
+
return { host: false, headers: [], fn: null };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const source = /** @type {Record<string, unknown>} */ (raw);
|
|
698
|
+
const headers = asArray(source.headers, "cache().vary.headers")
|
|
699
|
+
.filter((name) => typeof name === "string" && name)
|
|
700
|
+
.map((name) => String(name).toLowerCase());
|
|
701
|
+
|
|
702
|
+
/** @type {ResolvedConfig["cacheVary"]["fn"]} */
|
|
703
|
+
let fn = null;
|
|
704
|
+
if (typeof source.fn === "function") {
|
|
705
|
+
fn = /** @type {ResolvedConfig["cacheVary"]["fn"]} */ (source.fn);
|
|
706
|
+
} else if (source.fn != null) {
|
|
707
|
+
console.warn("[config] cache().vary.fn must be a function, ignoring it");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
host: source.host === true,
|
|
712
|
+
headers,
|
|
713
|
+
fn,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @param {unknown} raw
|
|
719
|
+
* @returns {{ html: ResolvedConfig["html"],
|
|
720
|
+
* cacheQuery: ResolvedConfig["cacheQuery"],
|
|
721
|
+
* cacheVary: ResolvedConfig["cacheVary"], htmlMaxEntries: number,
|
|
722
|
+
* staleWhileRevalidate: number,
|
|
723
|
+
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
724
|
+
* trackDependencies: boolean,
|
|
725
|
+
* transientRetry: { attempts: number, delayMs: number },
|
|
726
|
+
* redis: RedisConfig,
|
|
727
|
+
* upstream: typeof DEFAULT_UPSTREAM_LIMIT,
|
|
728
|
+
* cloudflare: typeof DEFAULT_CLOUDFLARE,
|
|
729
|
+
* prewarm: Record<string, unknown>,
|
|
730
|
+
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
731
|
+
*/
|
|
732
|
+
function normalizeCache(raw) {
|
|
733
|
+
/** @type {ResolvedConfig["html"]} */
|
|
734
|
+
const html = [];
|
|
735
|
+
|
|
736
|
+
for (const [source, seconds] of Object.entries(raw?.html ?? {})) {
|
|
737
|
+
const pattern = compilePattern(source);
|
|
738
|
+
const value = Number(seconds);
|
|
739
|
+
if (!pattern || !Number.isFinite(value) || value < 0) continue;
|
|
740
|
+
html.push({ pattern, seconds: value });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const prewarm = normalizePrewarm(raw?.prewarm);
|
|
744
|
+
const queryRules = normalizeQueryRules(raw?.query);
|
|
745
|
+
const maxEntries = Number(raw?.maxEntries);
|
|
746
|
+
let htmlMaxEntries =
|
|
747
|
+
Number.isFinite(maxEntries) && maxEntries > 0
|
|
748
|
+
? Math.floor(maxEntries)
|
|
749
|
+
: DEFAULT_HTML_CACHE_MAX_ENTRIES;
|
|
750
|
+
if (htmlMaxEntries > HTML_CACHE_MAX_ENTRIES_CEILING) {
|
|
751
|
+
console.warn(
|
|
752
|
+
`[config] cache().maxEntries ${htmlMaxEntries} exceeds the ceiling of ` +
|
|
753
|
+
`${HTML_CACHE_MAX_ENTRIES_CEILING}; using ${HTML_CACHE_MAX_ENTRIES_CEILING}`,
|
|
754
|
+
);
|
|
755
|
+
htmlMaxEntries = HTML_CACHE_MAX_ENTRIES_CEILING;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
html,
|
|
760
|
+
cacheQuery: queryRules,
|
|
761
|
+
cacheVary: normalizeVary(raw?.vary),
|
|
762
|
+
htmlMaxEntries,
|
|
763
|
+
staleWhileRevalidate: normalizeStaleWhileRevalidate(raw?.staleWhileRevalidate),
|
|
764
|
+
data: normalizeDataCache(raw?.data),
|
|
765
|
+
// Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
|
|
766
|
+
// bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
|
|
767
|
+
trackUpstream: raw?.trackUpstream !== false,
|
|
768
|
+
// Hangi sayfanın hangi veri anahtarını okuduğu kaydedilsin mi.
|
|
769
|
+
// `withDataCache` kullanmayan bir uygulamada kaydedilecek bir şey yok;
|
|
770
|
+
// kapatmak bağlam kurma maliyetini de kaldırır.
|
|
771
|
+
trackDependencies: raw?.trackDependencies !== false,
|
|
772
|
+
transientRetry:
|
|
773
|
+
raw?.transientRetry === false
|
|
774
|
+
? { attempts: 0, delayMs: 0 }
|
|
775
|
+
: { ...DEFAULT_TRANSIENT_RETRY, ...(raw?.transientRetry ?? {}) },
|
|
776
|
+
redis: normalizeRedis(raw?.redis),
|
|
777
|
+
upstream: normalizeUpstream(raw?.upstream),
|
|
778
|
+
cloudflare: normalizeCloudflare(raw?.cloudflare),
|
|
779
|
+
// Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
|
|
780
|
+
// ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
|
|
781
|
+
prewarm,
|
|
782
|
+
prewarmPriority: normalizePriority(prewarm.priority),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Edge stale penceresi. Boş değer varsayılan 60'tır; `0` direktifi kapatır.
|
|
788
|
+
* Negatif veya sonlu olmayan değer uyarıyla varsayılana döner.
|
|
789
|
+
*
|
|
790
|
+
* @param {unknown} raw
|
|
791
|
+
* @returns {number}
|
|
792
|
+
*/
|
|
793
|
+
function normalizeStaleWhileRevalidate(raw) {
|
|
794
|
+
if (raw == null || raw === "") return DEFAULT_STALE_WHILE_REVALIDATE;
|
|
795
|
+
|
|
796
|
+
const value = Number(raw);
|
|
797
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
798
|
+
console.warn(
|
|
799
|
+
`[config] cache().staleWhileRevalidate ${raw} is not a non-negative number; ` +
|
|
800
|
+
`using ${DEFAULT_STALE_WHILE_REVALIDATE}`,
|
|
801
|
+
);
|
|
802
|
+
return DEFAULT_STALE_WHILE_REVALIDATE;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* İstenen pozitif tamsayı tavanı aşıyorsa uyarı basıp tavana çeker.
|
|
810
|
+
*
|
|
811
|
+
* @param {string} field
|
|
812
|
+
* @param {number} requested
|
|
813
|
+
* @param {number} ceiling
|
|
814
|
+
* @returns {number}
|
|
815
|
+
*/
|
|
816
|
+
function clampCeiling(field, requested, ceiling) {
|
|
817
|
+
if (requested <= ceiling) return requested;
|
|
818
|
+
console.warn(
|
|
819
|
+
`[config] ${field} ${requested} exceeds the ceiling of ${ceiling}; using ${ceiling}`,
|
|
820
|
+
);
|
|
821
|
+
return ceiling;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Veri önbelleği JSON tutar; sınır HTML'den yüksek olabilir ama sonsuz değil.
|
|
826
|
+
*
|
|
827
|
+
* @param {unknown} raw
|
|
828
|
+
* @returns {{ maxEntries: number, staleFactor: number }}
|
|
829
|
+
*/
|
|
830
|
+
function normalizeDataCache(raw) {
|
|
831
|
+
const source =
|
|
832
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
833
|
+
? /** @type {Record<string, unknown>} */ (raw)
|
|
834
|
+
: {};
|
|
835
|
+
const requested = Number(source.maxEntries);
|
|
836
|
+
let maxEntries =
|
|
837
|
+
Number.isFinite(requested) && requested > 0
|
|
838
|
+
? Math.floor(requested)
|
|
839
|
+
: DEFAULT_DATA_CACHE.maxEntries;
|
|
840
|
+
maxEntries = clampCeiling(
|
|
841
|
+
"cache().data.maxEntries",
|
|
842
|
+
maxEntries,
|
|
843
|
+
DATA_CACHE_MAX_ENTRIES_CEILING,
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
const stale = Number(source.staleFactor);
|
|
847
|
+
return {
|
|
848
|
+
maxEntries,
|
|
849
|
+
staleFactor:
|
|
850
|
+
Number.isFinite(stale) && stale >= 0 ? stale : DEFAULT_DATA_CACHE.staleFactor,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* onVisit sürekli çalışır. Klasik turdaki `rps: 0` (sınırsız) burada her
|
|
856
|
+
* ziyaretçide yeniden crawl demek; boş, `0` ve tavanın üstü 2'ye çekilir.
|
|
857
|
+
*
|
|
858
|
+
* @param {Record<string, unknown>} source
|
|
859
|
+
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
860
|
+
*/
|
|
861
|
+
function resolveOnVisitLimits(source) {
|
|
862
|
+
const perPageRaw = Number(source.perPage);
|
|
863
|
+
const perPage = clampCeiling(
|
|
864
|
+
"cache().prewarm.onVisit.perPage",
|
|
865
|
+
Number.isFinite(perPageRaw) && perPageRaw > 0
|
|
866
|
+
? Math.floor(perPageRaw)
|
|
867
|
+
: DEFAULT_PREWARM_ON_VISIT.perPage,
|
|
868
|
+
ON_VISIT_PER_PAGE_CEILING,
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const concurrencyRaw = Number(source.concurrency);
|
|
872
|
+
const concurrency = clampCeiling(
|
|
873
|
+
"cache().prewarm.onVisit.concurrency",
|
|
874
|
+
Number.isFinite(concurrencyRaw) && concurrencyRaw > 0
|
|
875
|
+
? Math.floor(concurrencyRaw)
|
|
876
|
+
: ON_VISIT_CONCURRENCY_CEILING,
|
|
877
|
+
ON_VISIT_CONCURRENCY_CEILING,
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
const rpsRaw = Number(source.rps);
|
|
881
|
+
let rps = ON_VISIT_RPS_CEILING;
|
|
882
|
+
if (source.rps != null && source.rps !== "") {
|
|
883
|
+
if (!Number.isFinite(rpsRaw) || rpsRaw <= 0) {
|
|
884
|
+
console.warn(
|
|
885
|
+
`[config] cache().prewarm.onVisit.rps ${source.rps} is not a positive rate; ` +
|
|
886
|
+
`using ${ON_VISIT_RPS_CEILING}`,
|
|
887
|
+
);
|
|
888
|
+
} else {
|
|
889
|
+
rps = clampCeiling(
|
|
890
|
+
"cache().prewarm.onVisit.rps",
|
|
891
|
+
rpsRaw,
|
|
892
|
+
ON_VISIT_RPS_CEILING,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
...DEFAULT_PREWARM_ON_VISIT,
|
|
899
|
+
enabled: source.enabled !== false,
|
|
900
|
+
perPage,
|
|
901
|
+
concurrency,
|
|
902
|
+
rps,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* @param {unknown} raw
|
|
908
|
+
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
909
|
+
*/
|
|
910
|
+
function normalizeOnVisit(raw) {
|
|
911
|
+
if (raw === true) return resolveOnVisitLimits({});
|
|
912
|
+
|
|
913
|
+
if (raw == null || raw === false) {
|
|
914
|
+
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: false };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
"[config] cache().prewarm.onVisit must be true, false, or an object",
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return resolveOnVisitLimits(/** @type {Record<string, unknown>} */ (raw));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Klasik liste ısıtması ile `onVisit` karşılıklı dışlayıcıdır. İkisini birden
|
|
928
|
+
* yazmak sessizce yanlış moda düşmesin diye yüklemede hata verir.
|
|
929
|
+
*
|
|
930
|
+
* @param {unknown} raw
|
|
931
|
+
* @returns {Record<string, unknown>}
|
|
932
|
+
*/
|
|
933
|
+
function normalizePrewarm(raw) {
|
|
934
|
+
const source =
|
|
935
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
936
|
+
? /** @type {Record<string, unknown>} */ ({ ...raw })
|
|
937
|
+
: {};
|
|
938
|
+
|
|
939
|
+
const onVisit = normalizeOnVisit(source.onVisit);
|
|
940
|
+
|
|
941
|
+
if (onVisit.enabled) {
|
|
942
|
+
const conflicts = Object.keys(source).filter(
|
|
943
|
+
(key) => key !== "onVisit" && CLASSIC_PREWARM_KEYS.includes(key),
|
|
944
|
+
);
|
|
945
|
+
if (conflicts.length) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
"[config] cache().prewarm.onVisit cannot be combined with classic " +
|
|
948
|
+
`prewarm settings (${conflicts.join(", ")}). Use either onVisit or ` +
|
|
949
|
+
"classic settings (max, priority, rotate, …), not both.",
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const unknown = Object.keys(source).filter((key) => key !== "onVisit");
|
|
954
|
+
if (unknown.length) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
"[config] cache().prewarm.onVisit cannot be combined with " +
|
|
957
|
+
`${unknown.join(", ")}. On-visit mode only accepts the onVisit object.`,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
...DEFAULT_PREWARM,
|
|
963
|
+
enabled: true,
|
|
964
|
+
onVisit,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Klasik mod: `onVisit: false` yazılmış olabilir; diğer alanlar varsayılanlarla
|
|
969
|
+
// birleşir. `onVisit` anahtarı çözülmüş nesnede her zaman durur.
|
|
970
|
+
const classic = { ...source };
|
|
971
|
+
delete classic.onVisit;
|
|
972
|
+
|
|
973
|
+
const origins = asArray(classic.origins, "cache().prewarm.origins")
|
|
974
|
+
.filter((value) => typeof value === "string" && /^https?:\/\//i.test(value))
|
|
975
|
+
.map(String);
|
|
976
|
+
classic.origins = origins;
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
...DEFAULT_PREWARM,
|
|
980
|
+
...classic,
|
|
981
|
+
onVisit,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** Speculation Rules'un tanıdığı eagerness değerleri. */
|
|
986
|
+
const EAGERNESS = new Set(["conservative", "moderate", "eager"]);
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* `true` → varsayılan eagerness, `false` → kapalı, string → doğrulanır.
|
|
990
|
+
* Geçersiz bir değer siteyi düşürmemeli; uyarı basılıp varsayılana dönülür.
|
|
991
|
+
*
|
|
992
|
+
* @param {unknown} value
|
|
993
|
+
* @param {false | Eagerness} fallback
|
|
994
|
+
* @param {string} label
|
|
995
|
+
* @returns {false | Eagerness}
|
|
996
|
+
*/
|
|
997
|
+
function normalizeEagerness(value, fallback, label) {
|
|
998
|
+
if (value === undefined) return fallback;
|
|
999
|
+
if (value === false) return false;
|
|
1000
|
+
if (value === true) return fallback === false ? "moderate" : fallback;
|
|
1001
|
+
if (typeof value === "string" && EAGERNESS.has(value)) {
|
|
1002
|
+
return /** @type {Eagerness} */ (value);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
console.warn(
|
|
1006
|
+
`[config] navigation.${label} is invalid (${String(value)}), falling back to the default`,
|
|
1007
|
+
);
|
|
1008
|
+
return fallback;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* @param {unknown} raw
|
|
1013
|
+
* @param {Record<string, unknown>} brand
|
|
1014
|
+
* @returns {NavigationConfig}
|
|
1015
|
+
*/
|
|
1016
|
+
function normalizeNavigation(raw, brand) {
|
|
1017
|
+
const source = /** @type {Record<string, unknown>} */ (raw ?? {});
|
|
1018
|
+
|
|
1019
|
+
// Dev araçlarının yolu spekülasyona kapalı: overlay ve rapor uçları gerçek
|
|
1020
|
+
// sayfa değil, önden getirilmelerinin hiçbir karşılığı yok.
|
|
1021
|
+
const devBase = typeof brand.devBasePath === "string" ? brand.devBasePath : null;
|
|
1022
|
+
|
|
1023
|
+
return {
|
|
1024
|
+
prefetch: normalizeEagerness(
|
|
1025
|
+
source.prefetch,
|
|
1026
|
+
DEFAULT_NAVIGATION.prefetch,
|
|
1027
|
+
"prefetch",
|
|
1028
|
+
),
|
|
1029
|
+
prerender: normalizeEagerness(
|
|
1030
|
+
source.prerender,
|
|
1031
|
+
DEFAULT_NAVIGATION.prerender,
|
|
1032
|
+
"prerender",
|
|
1033
|
+
),
|
|
1034
|
+
viewTransition: source.viewTransition === true,
|
|
1035
|
+
exclude: [
|
|
1036
|
+
...DEFAULT_NAVIGATION_EXCLUDE,
|
|
1037
|
+
...(devBase ? [`${devBase}/*`] : []),
|
|
1038
|
+
...asArray(source.exclude, "navigation.exclude").filter(
|
|
1039
|
+
(entry) => typeof entry === "string",
|
|
1040
|
+
),
|
|
1041
|
+
].map(String),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* @typedef {object} SecurityConfig
|
|
1047
|
+
* @property {boolean} trustProxy
|
|
1048
|
+
* @property {string | null} cookieSecret
|
|
1049
|
+
* @property {{ enabled: boolean, token: boolean, allowedOrigins: string[],
|
|
1050
|
+
* exclude: CompiledPattern[], cookieName: string, fieldName: string,
|
|
1051
|
+
* headerName: string }} csrf
|
|
1052
|
+
*/
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* @param {unknown} raw
|
|
1056
|
+
* @returns {Record<string, unknown>}
|
|
1057
|
+
*/
|
|
1058
|
+
function normalizeBrand(raw) {
|
|
1059
|
+
const source = /** @type {Record<string, unknown>} */ (raw ?? {});
|
|
1060
|
+
const roots = asArray(
|
|
1061
|
+
source.sharedCookieRoots ?? DEFAULT_BRAND.sharedCookieRoots,
|
|
1062
|
+
"brand.sharedCookieRoots",
|
|
1063
|
+
)
|
|
1064
|
+
.filter((entry) => typeof entry === "string")
|
|
1065
|
+
.map((entry) => {
|
|
1066
|
+
const trimmed = String(entry).trim().toLowerCase();
|
|
1067
|
+
if (!trimmed) return null;
|
|
1068
|
+
return trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
|
|
1069
|
+
})
|
|
1070
|
+
.filter((entry) => entry !== null);
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
...DEFAULT_BRAND,
|
|
1074
|
+
...source,
|
|
1075
|
+
sharedCookieRoots: roots,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* @param {unknown} raw
|
|
1081
|
+
* @returns {{ crossSubdomainHandoff: boolean | Record<string, unknown> }}
|
|
1082
|
+
*/
|
|
1083
|
+
function normalizeAuth(raw) {
|
|
1084
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1085
|
+
return { ...DEFAULT_AUTH };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const source = /** @type {Record<string, unknown>} */ (raw);
|
|
1089
|
+
const handoff = source.crossSubdomainHandoff;
|
|
1090
|
+
|
|
1091
|
+
if (handoff === true || handoff === false || handoff == null) {
|
|
1092
|
+
return {
|
|
1093
|
+
crossSubdomainHandoff: handoff === true,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (typeof handoff === "object" && !Array.isArray(handoff)) {
|
|
1098
|
+
return { crossSubdomainHandoff: { ...handoff } };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
console.warn(
|
|
1102
|
+
"[config] auth.crossSubdomainHandoff must be boolean or object, ignoring it",
|
|
1103
|
+
);
|
|
1104
|
+
return { ...DEFAULT_AUTH };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Güvenlik bölümü. `csrf.exclude` desenleri burada derlenir: her istekte
|
|
1109
|
+
* yeniden derlemek gereksiz, ve bozuk bir desen sunucuyu düşürmemeli.
|
|
1110
|
+
*
|
|
1111
|
+
* @param {unknown} raw
|
|
1112
|
+
* @returns {SecurityConfig}
|
|
1113
|
+
*/
|
|
1114
|
+
function normalizeSecurity(raw) {
|
|
1115
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
1116
|
+
const csrf = { ...DEFAULT_SECURITY.csrf, ...(source.csrf ?? {}) };
|
|
1117
|
+
|
|
1118
|
+
const exclude = asArray(csrf.exclude, "security.csrf.exclude")
|
|
1119
|
+
.map((entry) => compilePattern(entry))
|
|
1120
|
+
.filter((pattern) => pattern !== null);
|
|
1121
|
+
|
|
1122
|
+
return {
|
|
1123
|
+
trustProxy: source.trustProxy !== false,
|
|
1124
|
+
cookieSecret:
|
|
1125
|
+
typeof source.cookieSecret === "string" && source.cookieSecret
|
|
1126
|
+
? source.cookieSecret
|
|
1127
|
+
: null,
|
|
1128
|
+
csrf: {
|
|
1129
|
+
enabled: csrf.enabled !== false,
|
|
1130
|
+
token: csrf.token === true,
|
|
1131
|
+
allowedOrigins: asArray(csrf.allowedOrigins, "security.csrf.allowedOrigins")
|
|
1132
|
+
.filter((entry) => typeof entry === "string")
|
|
1133
|
+
.map(String),
|
|
1134
|
+
exclude: /** @type {CompiledPattern[]} */ (exclude),
|
|
1135
|
+
cookieName: String(csrf.cookieName ?? DEFAULT_SECURITY.csrf.cookieName),
|
|
1136
|
+
fieldName: String(csrf.fieldName ?? DEFAULT_SECURITY.csrf.fieldName),
|
|
1137
|
+
headerName: String(csrf.headerName ?? DEFAULT_SECURITY.csrf.headerName).toLowerCase(),
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* İkon sprite ayarları. `false` → adım atlanır. `dir` varsayılanı `"icons"`:
|
|
1144
|
+
* o dizin varsa yalnızca yerel SVG'ler; yoksa Phosphor.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {unknown} raw
|
|
1147
|
+
* @returns {{ scan?: string[], dir: string } | false}
|
|
1148
|
+
*/
|
|
1149
|
+
function normalizeIcons(raw) {
|
|
1150
|
+
if (raw === false) return false;
|
|
1151
|
+
|
|
1152
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
1153
|
+
const dir =
|
|
1154
|
+
typeof source.dir === "string" && source.dir.trim()
|
|
1155
|
+
? source.dir.trim()
|
|
1156
|
+
: "icons";
|
|
1157
|
+
|
|
1158
|
+
/** @type {{ scan?: string[], dir: string }} */
|
|
1159
|
+
const icons = { dir };
|
|
1160
|
+
|
|
1161
|
+
if (source.scan != null) {
|
|
1162
|
+
icons.scan = asArray(source.scan, "icons.scan")
|
|
1163
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
1164
|
+
.map((entry) => String(entry).trim());
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return icons;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Build + runtime görsel ayarları. `false` → her iki yüzey de kapalı.
|
|
1172
|
+
* `remote.allowHosts` boşsa remote kapalı kalır (açık proxy olmasın).
|
|
1173
|
+
*
|
|
1174
|
+
* @param {unknown} raw
|
|
1175
|
+
* @returns {ImagesConfig | false}
|
|
1176
|
+
*/
|
|
1177
|
+
function normalizeImages(raw) {
|
|
1178
|
+
if (raw === false) return false;
|
|
1179
|
+
|
|
1180
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
1181
|
+
const widths = asArray(source.widths ?? DEFAULT_IMAGES.widths, "images.widths")
|
|
1182
|
+
.map((entry) => Number(entry))
|
|
1183
|
+
.filter((entry) => Number.isFinite(entry) && entry > 0)
|
|
1184
|
+
.map((entry) => Math.round(entry));
|
|
1185
|
+
|
|
1186
|
+
const quality = Number(source.quality ?? DEFAULT_IMAGES.quality);
|
|
1187
|
+
const skip = asArray(source.skip ?? DEFAULT_IMAGES.skip, "images.skip")
|
|
1188
|
+
.filter((entry) => typeof entry === "string")
|
|
1189
|
+
.map(String);
|
|
1190
|
+
|
|
1191
|
+
/** @type {ImagesRemoteConfig | false} */
|
|
1192
|
+
let remote = false;
|
|
1193
|
+
if (source.remote !== false && source.remote != null) {
|
|
1194
|
+
const rem = /** @type {Record<string, any>} */ (
|
|
1195
|
+
source.remote === true ? {} : source.remote
|
|
1196
|
+
);
|
|
1197
|
+
const allowHosts = asArray(
|
|
1198
|
+
rem.allowHosts ?? DEFAULT_IMAGES.remote.allowHosts,
|
|
1199
|
+
"images.remote.allowHosts",
|
|
1200
|
+
)
|
|
1201
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
1202
|
+
.map((entry) => String(entry).trim().toLowerCase());
|
|
1203
|
+
|
|
1204
|
+
if (allowHosts.length === 0) {
|
|
1205
|
+
if (source.remote === true || rem.allowHosts != null) {
|
|
1206
|
+
console.warn(
|
|
1207
|
+
"[config] images.remote needs a non-empty allowHosts list; remote optimizer disabled",
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
remote = {
|
|
1212
|
+
enabled: true,
|
|
1213
|
+
allowHosts,
|
|
1214
|
+
path: String(rem.path ?? DEFAULT_IMAGES.remote.path),
|
|
1215
|
+
maxWidth: Math.max(
|
|
1216
|
+
1,
|
|
1217
|
+
Number(rem.maxWidth ?? DEFAULT_IMAGES.remote.maxWidth) ||
|
|
1218
|
+
DEFAULT_IMAGES.remote.maxWidth,
|
|
1219
|
+
),
|
|
1220
|
+
cacheMaxAge: Math.max(
|
|
1221
|
+
0,
|
|
1222
|
+
Number(rem.cacheMaxAge ?? DEFAULT_IMAGES.remote.cacheMaxAge) ||
|
|
1223
|
+
DEFAULT_IMAGES.remote.cacheMaxAge,
|
|
1224
|
+
),
|
|
1225
|
+
fetchTimeoutMs: Math.max(
|
|
1226
|
+
1000,
|
|
1227
|
+
Number(rem.fetchTimeoutMs ?? DEFAULT_IMAGES.remote.fetchTimeoutMs) ||
|
|
1228
|
+
DEFAULT_IMAGES.remote.fetchTimeoutMs,
|
|
1229
|
+
),
|
|
1230
|
+
maxBytes: Math.max(
|
|
1231
|
+
1024,
|
|
1232
|
+
Number(rem.maxBytes ?? DEFAULT_IMAGES.remote.maxBytes) ||
|
|
1233
|
+
DEFAULT_IMAGES.remote.maxBytes,
|
|
1234
|
+
),
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return {
|
|
1240
|
+
widths: widths.length ? widths : [...DEFAULT_IMAGES.widths],
|
|
1241
|
+
quality: Number.isFinite(quality) && quality > 0 ? quality : DEFAULT_IMAGES.quality,
|
|
1242
|
+
skip,
|
|
1243
|
+
remote,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Dizin adlarını mutlak yola çevirir. `styles` bir dosya yolu olduğu için
|
|
1249
|
+
* de aynı çözümlemeden geçer; ayrı bir alan tutmaya değmez.
|
|
1250
|
+
*
|
|
1251
|
+
* @param {string} root
|
|
1252
|
+
* @param {Record<string, string>} [overrides]
|
|
1253
|
+
* @returns {Record<string, string>}
|
|
1254
|
+
*/
|
|
1255
|
+
function resolveDirs(root, overrides) {
|
|
1256
|
+
/** @type {Record<string, string>} */
|
|
1257
|
+
const dirs = {};
|
|
1258
|
+
const merged = { ...DEFAULT_DIRS, ...(overrides ?? {}) };
|
|
1259
|
+
|
|
1260
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
1261
|
+
dirs[key] = path.resolve(root, value);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Build çıktısı `public/assets` altına yazılır; ayrı ayar gerektirmeyecek
|
|
1265
|
+
// kadar sabit ama yol hesabı tek yerde kalsın.
|
|
1266
|
+
dirs.assets = path.join(dirs.public, "assets");
|
|
1267
|
+
dirs.fonts = path.join(dirs.public, "fonts");
|
|
1268
|
+
|
|
1269
|
+
return dirs;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Uygulamanın layout'u yoksa framework'ün minimal layout'u kullanılır. Bu
|
|
1274
|
+
* sayede yeni bir proje tek bir route ile çalışır hâle gelir.
|
|
1275
|
+
*
|
|
1276
|
+
* Öncelik: config `layout` → `layout.jsk` (derlenmiş) → `layout.ejs` →
|
|
1277
|
+
* framework varsayılanı. Dönüş değeri kaynak dosya yoludur; `.jsk` için
|
|
1278
|
+
* render katmanı derlenmiş modülü kullanır.
|
|
1279
|
+
*
|
|
1280
|
+
* @param {Record<string, string>} dirs
|
|
1281
|
+
* @param {string} [override]
|
|
1282
|
+
* @returns {string}
|
|
1283
|
+
*/
|
|
1284
|
+
function resolveLayout(dirs, override) {
|
|
1285
|
+
if (override) return path.resolve(dirs.views, "..", override);
|
|
1286
|
+
|
|
1287
|
+
const jskLayout = path.join(dirs.views, "layout.jsk");
|
|
1288
|
+
if (fs.existsSync(jskLayout)) return jskLayout;
|
|
1289
|
+
|
|
1290
|
+
const appLayout = path.join(dirs.views, "layout.ejs");
|
|
1291
|
+
if (fs.existsSync(appLayout)) return appLayout;
|
|
1292
|
+
|
|
1293
|
+
return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Config'i okur, normalize eder ve modül durumuna yazar. Sunucu ve build
|
|
1298
|
+
* süreçleri açılışta bir kez çağırır.
|
|
1299
|
+
*
|
|
1300
|
+
* Aynı süreçte ikinci çağrı önbelleğe düşer: `jskelet start` hem
|
|
1301
|
+
* `ensure-build` hem `createApp` üzerinden çağırıyor ve config'i iki kez
|
|
1302
|
+
* okuyup iki kez loglamanın hiçbir faydası yok. Yeniden okumak gerekiyorsa
|
|
1303
|
+
* `force: true`.
|
|
1304
|
+
*
|
|
1305
|
+
* @param {{ root?: string, configFile?: string, force?: boolean }} [options]
|
|
1306
|
+
* @returns {Promise<ResolvedConfig>}
|
|
1307
|
+
*/
|
|
1308
|
+
export async function loadConfig(options = {}) {
|
|
1309
|
+
if (config && !options.force) return config;
|
|
1310
|
+
|
|
1311
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
1312
|
+
const configFile = options.configFile ?? CONFIG_FILE;
|
|
1313
|
+
const configPath = path.join(root, configFile);
|
|
1314
|
+
|
|
1315
|
+
/** @type {Record<string, any>} */
|
|
1316
|
+
let source = {};
|
|
1317
|
+
let loaded = false;
|
|
1318
|
+
|
|
1319
|
+
if (!fs.existsSync(configPath)) {
|
|
1320
|
+
console.warn(
|
|
1321
|
+
`[config] ${configFile} not found — continuing with built-in defaults.`,
|
|
1322
|
+
);
|
|
1323
|
+
} else {
|
|
1324
|
+
try {
|
|
1325
|
+
// Windows'ta mutlak yol import'u için file:// şeması gerekir.
|
|
1326
|
+
const module = await import(pathToFileURL(configPath).href);
|
|
1327
|
+
source = module.default ?? module;
|
|
1328
|
+
loaded = true;
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
console.warn(`[config] ${configFile} failed to load, ignoring it`, error);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** @param {string} name */
|
|
1335
|
+
const section = async (name) => {
|
|
1336
|
+
const value = source?.[name];
|
|
1337
|
+
if (value == null) return null;
|
|
1338
|
+
try {
|
|
1339
|
+
return typeof value === "function" ? await value.call(source) : value;
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
console.warn(`[config] ${name}() threw, ignoring it`, error);
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
const [headers, redirects, rewrites, cache, admin, logs] = await Promise.all([
|
|
1347
|
+
section("headers"),
|
|
1348
|
+
section("redirects"),
|
|
1349
|
+
section("rewrites"),
|
|
1350
|
+
section("cache"),
|
|
1351
|
+
section("admin"),
|
|
1352
|
+
section("logs"),
|
|
1353
|
+
]);
|
|
1354
|
+
|
|
1355
|
+
const {
|
|
1356
|
+
html,
|
|
1357
|
+
cacheQuery,
|
|
1358
|
+
cacheVary,
|
|
1359
|
+
htmlMaxEntries,
|
|
1360
|
+
staleWhileRevalidate,
|
|
1361
|
+
data,
|
|
1362
|
+
trackUpstream,
|
|
1363
|
+
trackDependencies,
|
|
1364
|
+
transientRetry,
|
|
1365
|
+
redis,
|
|
1366
|
+
upstream,
|
|
1367
|
+
cloudflare,
|
|
1368
|
+
prewarm,
|
|
1369
|
+
prewarmPriority,
|
|
1370
|
+
} = normalizeCache(cache);
|
|
1371
|
+
const dirs = resolveDirs(root, source.paths);
|
|
1372
|
+
const brand = normalizeBrand(source.brand);
|
|
1373
|
+
const auth = normalizeAuth(source.auth);
|
|
1374
|
+
|
|
1375
|
+
config = {
|
|
1376
|
+
root,
|
|
1377
|
+
loaded,
|
|
1378
|
+
dirs,
|
|
1379
|
+
headers: normalizeHeaders(headers),
|
|
1380
|
+
redirects: normalizeRedirects(redirects),
|
|
1381
|
+
rewrites: normalizeRewrites(rewrites),
|
|
1382
|
+
html,
|
|
1383
|
+
cacheQuery,
|
|
1384
|
+
cacheVary,
|
|
1385
|
+
htmlMaxEntries,
|
|
1386
|
+
staleWhileRevalidate,
|
|
1387
|
+
data,
|
|
1388
|
+
trackUpstream,
|
|
1389
|
+
trackDependencies,
|
|
1390
|
+
transientRetry,
|
|
1391
|
+
redis,
|
|
1392
|
+
upstream,
|
|
1393
|
+
logs: normalizeLogs(logs),
|
|
1394
|
+
admin: normalizeAdmin(admin),
|
|
1395
|
+
cloudflare,
|
|
1396
|
+
prewarm,
|
|
1397
|
+
prewarmPriority,
|
|
1398
|
+
brand,
|
|
1399
|
+
auth,
|
|
1400
|
+
hooks: source.hooks ?? {},
|
|
1401
|
+
layout: resolveLayout(dirs, source.layout),
|
|
1402
|
+
routes: Array.isArray(source.routes) ? source.routes : null,
|
|
1403
|
+
// Varsayılan kapalı: açıkken `/hakkinda` → 308 `/hakkinda/` ve kanonik
|
|
1404
|
+
// yanıt 200'dir. Kapalıyken slash dayatılmaz — Express'in non-strict
|
|
1405
|
+
// eşleşmesi her iki biçimi de 200 ile servis eder (Next'in varsayılan
|
|
1406
|
+
// "slash'ı kırp" davranışından bilinçli fark).
|
|
1407
|
+
trailingSlash: source.trailingSlash === true,
|
|
1408
|
+
static: {
|
|
1409
|
+
extensions: new Set(source.static?.extensions ?? DEFAULT_STATIC.extensions),
|
|
1410
|
+
prefixes: source.static?.prefixes ?? DEFAULT_STATIC.prefixes,
|
|
1411
|
+
},
|
|
1412
|
+
devGate: normalizeDevGate(source),
|
|
1413
|
+
devGateBypass: source.devGateBypass ?? DEFAULT_DEV_GATE_BYPASS,
|
|
1414
|
+
preconnect: source.preconnect ?? [],
|
|
1415
|
+
navigation: normalizeNavigation(source.navigation, brand),
|
|
1416
|
+
security: normalizeSecurity(source.security),
|
|
1417
|
+
prewarmSkip: source.prewarmSkip ?? DEFAULT_PREWARM_SKIP,
|
|
1418
|
+
// `routes`, `views` ve `lib` zaten izlenir; buraya yalnızca ek dizinler.
|
|
1419
|
+
watch: source.watch ?? [],
|
|
1420
|
+
// Build tarafı ayarları. Sunucu bunları okumaz ama config tek dosya
|
|
1421
|
+
// olsun diye aynı yerden geçer.
|
|
1422
|
+
fonts: source.fonts ?? [],
|
|
1423
|
+
icons: normalizeIcons(source.icons),
|
|
1424
|
+
images: normalizeImages(source.images),
|
|
1425
|
+
clientEnv: source.clientEnv ?? [],
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
if (
|
|
1429
|
+
config.prewarm?.onVisit?.enabled &&
|
|
1430
|
+
typeof config.hooks?.prewarmPaths === "function"
|
|
1431
|
+
) {
|
|
1432
|
+
throw new Error(
|
|
1433
|
+
"[config] hooks.prewarmPaths() cannot be used with cache().prewarm.onVisit. " +
|
|
1434
|
+
"On-visit mode warms links from each response; classic mode uses prewarmPaths. " +
|
|
1435
|
+
"Choose one.",
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Dev'de build ve sunucu ayrı alt süreçler; üçü de aynı özeti basınca satır
|
|
1440
|
+
// banner'ın ve build bloğunun arasına üç kez giriyor. Özeti dış süreç basar.
|
|
1441
|
+
if (loaded && !process.env.JSKELET_CHILD) {
|
|
1442
|
+
/** @param {number} count @param {string} singular @param {string} plural */
|
|
1443
|
+
const label = (count, singular, plural) =>
|
|
1444
|
+
`${count} ${count === 1 ? singular : plural}`;
|
|
1445
|
+
|
|
1446
|
+
const counts = [
|
|
1447
|
+
config.headers.length && label(config.headers.length, "header", "headers"),
|
|
1448
|
+
config.redirects.length &&
|
|
1449
|
+
label(config.redirects.length, "redirect", "redirects"),
|
|
1450
|
+
config.rewrites.length && label(config.rewrites.length, "rewrite", "rewrites"),
|
|
1451
|
+
config.html.length && label(config.html.length, "cache rule", "cache rules"),
|
|
1452
|
+
].filter(Boolean);
|
|
1453
|
+
|
|
1454
|
+
if (counts.length) {
|
|
1455
|
+
console.log(`[config] ${configFile} loaded — ${counts.join(", ")}`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
return config;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Çözümlenmiş config. `loadConfig()` çağrılmadan erişilirse boş bir proje
|
|
1464
|
+
* kökü varsayımıyla çalışmak yerine hata verir: sessiz yanlış yol,
|
|
1465
|
+
* "stylesheet neden yok" gibi teşhisi zor sorunlara dönüşüyor.
|
|
1466
|
+
*
|
|
1467
|
+
* @returns {ResolvedConfig}
|
|
1468
|
+
*/
|
|
1469
|
+
export function getConfig() {
|
|
1470
|
+
if (!config) {
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
"[config] getConfig() was used before loadConfig(). " +
|
|
1473
|
+
"Start the server with the `jskelet` CLI or through createApp().",
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
return config;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Uygulamanın tanımladığı hook'u çalıştırır; yoksa `fallback` döner.
|
|
1481
|
+
* Hook'un hata vermesi sayfayı düşürmemeli — framework kendi varsayılanına
|
|
1482
|
+
* geri döner ve uyarır.
|
|
1483
|
+
*
|
|
1484
|
+
* @template T
|
|
1485
|
+
* @param {string} name
|
|
1486
|
+
* @param {T} fallback
|
|
1487
|
+
* @param {unknown[]} args
|
|
1488
|
+
* @returns {Promise<T>}
|
|
1489
|
+
*/
|
|
1490
|
+
export async function hook(name, fallback, ...args) {
|
|
1491
|
+
const fn = getConfig().hooks?.[name];
|
|
1492
|
+
if (typeof fn !== "function") return fallback;
|
|
1493
|
+
|
|
1494
|
+
try {
|
|
1495
|
+
return await fn(...args);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
console.warn(`[config] hooks.${name}() threw, using the default`, error);
|
|
1498
|
+
return fallback;
|
|
1499
|
+
}
|
|
1500
|
+
}
|