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
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render başına upstream API hatalarını toplar.
|
|
3
|
-
*
|
|
4
|
-
* `render.js` her sayfayı bu bağlam içinde üretir. Böylece HTML önbelleği "bu
|
|
5
|
-
* çıktı eksik veriyle üretildi" bilgisine sahip olur ve bozuk sayfayı saklamaz;
|
|
6
|
-
* `notFound()` de geçici bir hataya denk geldiğinde 404 olmaktan çıkar.
|
|
7
|
-
*
|
|
8
|
-
* Bilgi iki yoldan gelir:
|
|
9
|
-
*
|
|
10
|
-
* 1. **Otomatik** — `trackUpstreamFetch()` `globalThis.fetch`i sarar ve
|
|
11
|
-
* geçici hataları (429, 5xx, ağ) kendiliğinden bildirir. `createApp()`
|
|
12
|
-
* bunu açılışta kurar, yani hiçbir uygulama kodu gerekmez.
|
|
13
|
-
* 2. **Elle** — `fetch` kullanmayan bir istemci (veritabanı sürücüsü, gRPC,
|
|
14
|
-
* SDK) için:
|
|
15
|
-
*
|
|
16
|
-
* import { reportUpstreamFailure } from "jskelet";
|
|
17
|
-
*
|
|
18
|
-
* if (!response.ok) {
|
|
19
|
-
* reportUpstreamFailure({ status: response.status, path: url });
|
|
20
|
-
* }
|
|
21
|
-
*
|
|
22
|
-
* İki yol aynı hatayı bildirirse tekilleştirilir.
|
|
23
|
-
*
|
|
24
|
-
* Sarmalayıcı aynı zamanda hız freninin durduğu yerdir
|
|
25
|
-
* (`upstream-limiter.js`): kotayı harcayan şey sayfa isteği değil, buradan
|
|
26
|
-
* geçen çağrı. Fren varsayılan olarak kapalı.
|
|
27
|
-
*/
|
|
28
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29
|
-
import { limitUpstream, noteUpstreamResponse } from "./upstream-limiter.js";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @typedef {{ status: number, path: string }} UpstreamFailure
|
|
33
|
-
* `status: 0` ağ hatası anlamına gelir (yanıt hiç gelmedi).
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/** @type {AsyncLocalStorage<{ failures: UpstreamFailure[] }>} */
|
|
37
|
-
const storage = new AsyncLocalStorage();
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {UpstreamFailure} failure
|
|
41
|
-
* @returns {void}
|
|
42
|
-
*/
|
|
43
|
-
export function reportUpstreamFailure(failure) {
|
|
44
|
-
const store = storage.getStore();
|
|
45
|
-
if (!store) return;
|
|
46
|
-
|
|
47
|
-
// Aynı hatayı hem otomatik sarmalayıcı hem uygulamanın istemcisi
|
|
48
|
-
// bildirebilir; aynı satırı iki kez loglamanın faydası yok.
|
|
49
|
-
const duplicate = store.failures.some(
|
|
50
|
-
(existing) => existing.status === failure.status && existing.path === failure.path,
|
|
51
|
-
);
|
|
52
|
-
if (!duplicate) store.failures.push(failure);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Geçici sayılan durumlar: tekrar denemekle düzelebilenler. Bu liste
|
|
57
|
-
* `render.js` ile paylaşılır — hangi hatanın önbelleği engellediği ve hangi
|
|
58
|
-
* hatanın `notFound()`u 404 olmaktan çıkardığı tek yerde tanımlı olsun.
|
|
59
|
-
*/
|
|
60
|
-
const TRANSIENT_STATUSES = new Set([0, 408, 425, 429]);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @param {number} status
|
|
64
|
-
* @returns {boolean}
|
|
65
|
-
*/
|
|
66
|
-
export function isTransientStatus(status) {
|
|
67
|
-
return TRANSIENT_STATUSES.has(status) || status >= 500;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* `globalThis.fetch`i sarıp **geçici** upstream hatalarını kendiliğinden
|
|
72
|
-
* bildirir.
|
|
73
|
-
*
|
|
74
|
-
* Gerekçesi pratik: `reportUpstreamFailure()` sözleşmesi uygulamanın HTTP
|
|
75
|
-
* istemcisine bir satır eklemeyi gerektiriyor ve o satır yazılmadığında
|
|
76
|
-
* framework rate limit'i hiç göremiyor — veri gelmediği için `notFound()`
|
|
77
|
-
* çağıran sayfa 404 olarak servis ediliyordu. Otomatik izleme bu bilgiyi
|
|
78
|
-
* varsayılan hâle getirir; elle çağrı hâlâ geçerli ve tekilleştirilir.
|
|
79
|
-
*
|
|
80
|
-
* Yalnızca geçici durumlar bildirilir. `404`/`403` gibi deterministik
|
|
81
|
-
* cevaplar birçok API'de "böyle bir kayıt yok" anlamına geliyor ve onları
|
|
82
|
-
* otomatik olarak "eksik veri" saymak her sayfada yanlış uyarı üretirdi.
|
|
83
|
-
*
|
|
84
|
-
* Kendi sunucumuza yapılan istekler atlanır: ısıtma turu ve sağlık kontrolü
|
|
85
|
-
* upstream değil.
|
|
86
|
-
*
|
|
87
|
-
* @returns {void}
|
|
88
|
-
*/
|
|
89
|
-
export function trackUpstreamFetch() {
|
|
90
|
-
const original = globalThis.fetch;
|
|
91
|
-
if (/** @type {any} */ (original).__jskeletUpstreamTracked) return;
|
|
92
|
-
|
|
93
|
-
/** @type {typeof fetch} */
|
|
94
|
-
const wrapped = async (input, init) => {
|
|
95
|
-
// İstek bir render bağlamı içinde değilse (script, zamanlayıcı) hiçbir
|
|
96
|
-
// şey yapılmaz: sarmalayıcının maliyeti bir `getStore()` çağrısı.
|
|
97
|
-
if (!storage.getStore()) return original(input, init);
|
|
98
|
-
|
|
99
|
-
const url = requestUrl(input);
|
|
100
|
-
if (isSelfRequest(url)) return original(input, init);
|
|
101
|
-
|
|
102
|
-
// Hız freni burada, çünkü kotayı harcayan şey sayfa değil bu çağrı.
|
|
103
|
-
// Kapalıysa (varsayılan) `null` döner ve tek maliyeti bir dal.
|
|
104
|
-
const permit = await limitUpstream(url);
|
|
105
|
-
|
|
106
|
-
if (permit?.blocked) {
|
|
107
|
-
// Devre kesici açık: 429 yiyeceğini bildiğimiz bir çağrıyı yapmıyoruz.
|
|
108
|
-
// Render tarafı bunu geçici hata olarak görür, yani sayfa önbelleğe
|
|
109
|
-
// yazılmaz ve bir sonraki istek yeniden dener.
|
|
110
|
-
reportUpstreamFailure({ status: 429, path: url });
|
|
111
|
-
return new Response(null, { status: 429, statusText: "Too Many Requests" });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const response = await original(input, init);
|
|
116
|
-
|
|
117
|
-
if (permit) {
|
|
118
|
-
noteUpstreamResponse(permit.host, response.status, response.headers.get("retry-after"));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!response.ok && isTransientStatus(response.status)) {
|
|
122
|
-
reportUpstreamFailure({ status: response.status, path: url });
|
|
123
|
-
}
|
|
124
|
-
return response;
|
|
125
|
-
} catch (error) {
|
|
126
|
-
// Yanıt hiç gelmedi: ağ hatası her zaman geçicidir.
|
|
127
|
-
if (permit) noteUpstreamResponse(permit.host, 0, null);
|
|
128
|
-
reportUpstreamFailure({ status: 0, path: url });
|
|
129
|
-
throw error;
|
|
130
|
-
} finally {
|
|
131
|
-
permit?.release();
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/** @type {any} */ (wrapped).__jskeletUpstreamTracked = true;
|
|
136
|
-
globalThis.fetch = wrapped;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* @param {RequestInfo | URL} input
|
|
141
|
-
* @returns {string}
|
|
142
|
-
*/
|
|
143
|
-
function requestUrl(input) {
|
|
144
|
-
if (typeof input === "string") return input;
|
|
145
|
-
if (input instanceof URL) return input.href;
|
|
146
|
-
return /** @type {Request} */ (input)?.url ?? String(input);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** @param {string} url */
|
|
150
|
-
function isSelfRequest(url) {
|
|
151
|
-
return /^https?:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:|\/|$)/i.test(url);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* @param {() => T} run
|
|
156
|
-
* @returns {T}
|
|
157
|
-
* @template T
|
|
158
|
-
*/
|
|
159
|
-
export function withUpstreamTracking(run) {
|
|
160
|
-
return storage.run({ failures: [] }, run);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** @returns {UpstreamFailure[]} */
|
|
164
|
-
export function getUpstreamFailures() {
|
|
165
|
-
return storage.getStore()?.failures ?? [];
|
|
166
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Render başına upstream API hatalarını toplar.
|
|
3
|
+
*
|
|
4
|
+
* `render.js` her sayfayı bu bağlam içinde üretir. Böylece HTML önbelleği "bu
|
|
5
|
+
* çıktı eksik veriyle üretildi" bilgisine sahip olur ve bozuk sayfayı saklamaz;
|
|
6
|
+
* `notFound()` de geçici bir hataya denk geldiğinde 404 olmaktan çıkar.
|
|
7
|
+
*
|
|
8
|
+
* Bilgi iki yoldan gelir:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Otomatik** — `trackUpstreamFetch()` `globalThis.fetch`i sarar ve
|
|
11
|
+
* geçici hataları (429, 5xx, ağ) kendiliğinden bildirir. `createApp()`
|
|
12
|
+
* bunu açılışta kurar, yani hiçbir uygulama kodu gerekmez.
|
|
13
|
+
* 2. **Elle** — `fetch` kullanmayan bir istemci (veritabanı sürücüsü, gRPC,
|
|
14
|
+
* SDK) için:
|
|
15
|
+
*
|
|
16
|
+
* import { reportUpstreamFailure } from "jskelet";
|
|
17
|
+
*
|
|
18
|
+
* if (!response.ok) {
|
|
19
|
+
* reportUpstreamFailure({ status: response.status, path: url });
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* İki yol aynı hatayı bildirirse tekilleştirilir.
|
|
23
|
+
*
|
|
24
|
+
* Sarmalayıcı aynı zamanda hız freninin durduğu yerdir
|
|
25
|
+
* (`upstream-limiter.js`): kotayı harcayan şey sayfa isteği değil, buradan
|
|
26
|
+
* geçen çağrı. Fren varsayılan olarak kapalı.
|
|
27
|
+
*/
|
|
28
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29
|
+
import { limitUpstream, noteUpstreamResponse } from "./upstream-limiter.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {{ status: number, path: string }} UpstreamFailure
|
|
33
|
+
* `status: 0` ağ hatası anlamına gelir (yanıt hiç gelmedi).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** @type {AsyncLocalStorage<{ failures: UpstreamFailure[] }>} */
|
|
37
|
+
const storage = new AsyncLocalStorage();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {UpstreamFailure} failure
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
export function reportUpstreamFailure(failure) {
|
|
44
|
+
const store = storage.getStore();
|
|
45
|
+
if (!store) return;
|
|
46
|
+
|
|
47
|
+
// Aynı hatayı hem otomatik sarmalayıcı hem uygulamanın istemcisi
|
|
48
|
+
// bildirebilir; aynı satırı iki kez loglamanın faydası yok.
|
|
49
|
+
const duplicate = store.failures.some(
|
|
50
|
+
(existing) => existing.status === failure.status && existing.path === failure.path,
|
|
51
|
+
);
|
|
52
|
+
if (!duplicate) store.failures.push(failure);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Geçici sayılan durumlar: tekrar denemekle düzelebilenler. Bu liste
|
|
57
|
+
* `render.js` ile paylaşılır — hangi hatanın önbelleği engellediği ve hangi
|
|
58
|
+
* hatanın `notFound()`u 404 olmaktan çıkardığı tek yerde tanımlı olsun.
|
|
59
|
+
*/
|
|
60
|
+
const TRANSIENT_STATUSES = new Set([0, 408, 425, 429]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {number} status
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export function isTransientStatus(status) {
|
|
67
|
+
return TRANSIENT_STATUSES.has(status) || status >= 500;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `globalThis.fetch`i sarıp **geçici** upstream hatalarını kendiliğinden
|
|
72
|
+
* bildirir.
|
|
73
|
+
*
|
|
74
|
+
* Gerekçesi pratik: `reportUpstreamFailure()` sözleşmesi uygulamanın HTTP
|
|
75
|
+
* istemcisine bir satır eklemeyi gerektiriyor ve o satır yazılmadığında
|
|
76
|
+
* framework rate limit'i hiç göremiyor — veri gelmediği için `notFound()`
|
|
77
|
+
* çağıran sayfa 404 olarak servis ediliyordu. Otomatik izleme bu bilgiyi
|
|
78
|
+
* varsayılan hâle getirir; elle çağrı hâlâ geçerli ve tekilleştirilir.
|
|
79
|
+
*
|
|
80
|
+
* Yalnızca geçici durumlar bildirilir. `404`/`403` gibi deterministik
|
|
81
|
+
* cevaplar birçok API'de "böyle bir kayıt yok" anlamına geliyor ve onları
|
|
82
|
+
* otomatik olarak "eksik veri" saymak her sayfada yanlış uyarı üretirdi.
|
|
83
|
+
*
|
|
84
|
+
* Kendi sunucumuza yapılan istekler atlanır: ısıtma turu ve sağlık kontrolü
|
|
85
|
+
* upstream değil.
|
|
86
|
+
*
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*/
|
|
89
|
+
export function trackUpstreamFetch() {
|
|
90
|
+
const original = globalThis.fetch;
|
|
91
|
+
if (/** @type {any} */ (original).__jskeletUpstreamTracked) return;
|
|
92
|
+
|
|
93
|
+
/** @type {typeof fetch} */
|
|
94
|
+
const wrapped = async (input, init) => {
|
|
95
|
+
// İstek bir render bağlamı içinde değilse (script, zamanlayıcı) hiçbir
|
|
96
|
+
// şey yapılmaz: sarmalayıcının maliyeti bir `getStore()` çağrısı.
|
|
97
|
+
if (!storage.getStore()) return original(input, init);
|
|
98
|
+
|
|
99
|
+
const url = requestUrl(input);
|
|
100
|
+
if (isSelfRequest(url)) return original(input, init);
|
|
101
|
+
|
|
102
|
+
// Hız freni burada, çünkü kotayı harcayan şey sayfa değil bu çağrı.
|
|
103
|
+
// Kapalıysa (varsayılan) `null` döner ve tek maliyeti bir dal.
|
|
104
|
+
const permit = await limitUpstream(url);
|
|
105
|
+
|
|
106
|
+
if (permit?.blocked) {
|
|
107
|
+
// Devre kesici açık: 429 yiyeceğini bildiğimiz bir çağrıyı yapmıyoruz.
|
|
108
|
+
// Render tarafı bunu geçici hata olarak görür, yani sayfa önbelleğe
|
|
109
|
+
// yazılmaz ve bir sonraki istek yeniden dener.
|
|
110
|
+
reportUpstreamFailure({ status: 429, path: url });
|
|
111
|
+
return new Response(null, { status: 429, statusText: "Too Many Requests" });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await original(input, init);
|
|
116
|
+
|
|
117
|
+
if (permit) {
|
|
118
|
+
noteUpstreamResponse(permit.host, response.status, response.headers.get("retry-after"));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!response.ok && isTransientStatus(response.status)) {
|
|
122
|
+
reportUpstreamFailure({ status: response.status, path: url });
|
|
123
|
+
}
|
|
124
|
+
return response;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Yanıt hiç gelmedi: ağ hatası her zaman geçicidir.
|
|
127
|
+
if (permit) noteUpstreamResponse(permit.host, 0, null);
|
|
128
|
+
reportUpstreamFailure({ status: 0, path: url });
|
|
129
|
+
throw error;
|
|
130
|
+
} finally {
|
|
131
|
+
permit?.release();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** @type {any} */ (wrapped).__jskeletUpstreamTracked = true;
|
|
136
|
+
globalThis.fetch = wrapped;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {RequestInfo | URL} input
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function requestUrl(input) {
|
|
144
|
+
if (typeof input === "string") return input;
|
|
145
|
+
if (input instanceof URL) return input.href;
|
|
146
|
+
return /** @type {Request} */ (input)?.url ?? String(input);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {string} url */
|
|
150
|
+
function isSelfRequest(url) {
|
|
151
|
+
return /^https?:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:|\/|$)/i.test(url);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {() => T} run
|
|
156
|
+
* @returns {T}
|
|
157
|
+
* @template T
|
|
158
|
+
*/
|
|
159
|
+
export function withUpstreamTracking(run) {
|
|
160
|
+
return storage.run({ failures: [] }, run);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @returns {UpstreamFailure[]} */
|
|
164
|
+
export function getUpstreamFailures() {
|
|
165
|
+
return storage.getStore()?.failures ?? [];
|
|
166
|
+
}
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paylaşımlı cookie Domain seçimi — sunucu ve istemci aynı kuralları kullanır.
|
|
3
|
-
*
|
|
4
|
-
* `brand.sharedCookieRoots` örn. `[".investvio.com", ".localhost"]`.
|
|
5
|
-
* Host bu köklerden birine uyuyorsa Domain olarak o kök yazılır.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {string} host Host veya Host:port.
|
|
10
|
-
* @returns {string} Lowercase, portsuz.
|
|
11
|
-
*/
|
|
12
|
-
export function stripHostPort(host) {
|
|
13
|
-
const raw = String(host ?? "")
|
|
14
|
-
.split(",")[0]
|
|
15
|
-
.trim()
|
|
16
|
-
.toLowerCase();
|
|
17
|
-
if (!raw) return "";
|
|
18
|
-
if (raw.startsWith("[")) {
|
|
19
|
-
const end = raw.indexOf("]");
|
|
20
|
-
return end === -1 ? raw : raw.slice(0, end + 1);
|
|
21
|
-
}
|
|
22
|
-
const colon = raw.lastIndexOf(":");
|
|
23
|
-
if (colon === -1) return raw;
|
|
24
|
-
if (raw.indexOf(":") !== colon) return raw;
|
|
25
|
-
return raw.slice(0, colon);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Kökü `Domain=` biçimine getirir (başında nokta).
|
|
30
|
-
*
|
|
31
|
-
* @param {unknown} root
|
|
32
|
-
* @returns {string | null}
|
|
33
|
-
*/
|
|
34
|
-
export function normalizeCookieRoot(root) {
|
|
35
|
-
if (typeof root !== "string") return null;
|
|
36
|
-
const trimmed = root.trim().toLowerCase();
|
|
37
|
-
if (!trimmed || trimmed === ".") return null;
|
|
38
|
-
const bare = trimmed.replace(/^\./, "");
|
|
39
|
-
if (!bare || bare.includes("/") || bare.includes(":")) return null;
|
|
40
|
-
return `.${bare}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {string} hostname Portsuz hostname.
|
|
45
|
-
* @param {Iterable<string>} roots `brand.sharedCookieRoots`.
|
|
46
|
-
* @returns {string | null} `Domain` değeri (başında nokta) ya da eşleşme yoksa null.
|
|
47
|
-
*/
|
|
48
|
-
export function resolveSharedCookieDomain(hostname, roots) {
|
|
49
|
-
const host = stripHostPort(hostname);
|
|
50
|
-
if (!host) return null;
|
|
51
|
-
|
|
52
|
-
for (const entry of roots ?? []) {
|
|
53
|
-
const root = normalizeCookieRoot(entry);
|
|
54
|
-
if (!root) continue;
|
|
55
|
-
const bare = root.slice(1);
|
|
56
|
-
if (host === bare || host.endsWith(`.${bare}`)) return root;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Paylaşımlı cookie için önerilen üst sınır. JWT / büyük token buraya
|
|
64
|
-
* sığmaz — kısa session id koyun; aksi halde handoff veya host-only cookie.
|
|
65
|
-
*/
|
|
66
|
-
export const SHARED_COOKIE_WARN_BYTES = 512;
|
|
1
|
+
/**
|
|
2
|
+
* Paylaşımlı cookie Domain seçimi — sunucu ve istemci aynı kuralları kullanır.
|
|
3
|
+
*
|
|
4
|
+
* `brand.sharedCookieRoots` örn. `[".investvio.com", ".localhost"]`.
|
|
5
|
+
* Host bu köklerden birine uyuyorsa Domain olarak o kök yazılır.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} host Host veya Host:port.
|
|
10
|
+
* @returns {string} Lowercase, portsuz.
|
|
11
|
+
*/
|
|
12
|
+
export function stripHostPort(host) {
|
|
13
|
+
const raw = String(host ?? "")
|
|
14
|
+
.split(",")[0]
|
|
15
|
+
.trim()
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
if (!raw) return "";
|
|
18
|
+
if (raw.startsWith("[")) {
|
|
19
|
+
const end = raw.indexOf("]");
|
|
20
|
+
return end === -1 ? raw : raw.slice(0, end + 1);
|
|
21
|
+
}
|
|
22
|
+
const colon = raw.lastIndexOf(":");
|
|
23
|
+
if (colon === -1) return raw;
|
|
24
|
+
if (raw.indexOf(":") !== colon) return raw;
|
|
25
|
+
return raw.slice(0, colon);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Kökü `Domain=` biçimine getirir (başında nokta).
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} root
|
|
32
|
+
* @returns {string | null}
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeCookieRoot(root) {
|
|
35
|
+
if (typeof root !== "string") return null;
|
|
36
|
+
const trimmed = root.trim().toLowerCase();
|
|
37
|
+
if (!trimmed || trimmed === ".") return null;
|
|
38
|
+
const bare = trimmed.replace(/^\./, "");
|
|
39
|
+
if (!bare || bare.includes("/") || bare.includes(":")) return null;
|
|
40
|
+
return `.${bare}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} hostname Portsuz hostname.
|
|
45
|
+
* @param {Iterable<string>} roots `brand.sharedCookieRoots`.
|
|
46
|
+
* @returns {string | null} `Domain` değeri (başında nokta) ya da eşleşme yoksa null.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveSharedCookieDomain(hostname, roots) {
|
|
49
|
+
const host = stripHostPort(hostname);
|
|
50
|
+
if (!host) return null;
|
|
51
|
+
|
|
52
|
+
for (const entry of roots ?? []) {
|
|
53
|
+
const root = normalizeCookieRoot(entry);
|
|
54
|
+
if (!root) continue;
|
|
55
|
+
const bare = root.slice(1);
|
|
56
|
+
if (host === bare || host.endsWith(`.${bare}`)) return root;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Paylaşımlı cookie için önerilen üst sınır. JWT / büyük token buraya
|
|
64
|
+
* sığmaz — kısa session id koyun; aksi halde handoff veya host-only cookie.
|
|
65
|
+
*/
|
|
66
|
+
export const SHARED_COOKIE_WARN_BYTES = 512;
|
package/src/start.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prod sunucu girişi. Build çıktısı yoksa önce üretir, sonra dinlemeye başlar.
|
|
3
|
-
*
|
|
4
|
-
* `jskelet start --murder` → dolu porttaki dinleyiciyi öldürüp başlar.
|
|
5
|
-
* Port kontrolü build'den önce: doluysa pahalı üretimi hiç başlatma.
|
|
6
|
-
*/
|
|
7
|
-
import process from "node:process";
|
|
8
|
-
import { ensurePortFree } from "./server/port-guard.js";
|
|
9
|
-
|
|
10
|
-
const port = Number(process.env.PORT ?? 3000);
|
|
11
|
-
const murder = process.argv.includes("--murder");
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
await ensurePortFree(port, { murder });
|
|
15
|
-
await import("./build/ensure-build.mjs");
|
|
16
|
-
const { startServer } = await import("./server/create-app.js");
|
|
17
|
-
await startServer({ murder, port });
|
|
18
|
-
} catch (error) {
|
|
19
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
-
process.stderr.write(`${message}\n`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Prod sunucu girişi. Build çıktısı yoksa önce üretir, sonra dinlemeye başlar.
|
|
3
|
+
*
|
|
4
|
+
* `jskelet start --murder` → dolu porttaki dinleyiciyi öldürüp başlar.
|
|
5
|
+
* Port kontrolü build'den önce: doluysa pahalı üretimi hiç başlatma.
|
|
6
|
+
*/
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { ensurePortFree } from "./server/port-guard.js";
|
|
9
|
+
|
|
10
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
11
|
+
const murder = process.argv.includes("--murder");
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await ensurePortFree(port, { murder });
|
|
15
|
+
await import("./build/ensure-build.mjs");
|
|
16
|
+
const { startServer } = await import("./server/create-app.js");
|
|
17
|
+
await startServer({ murder, port });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
process.stderr.write(`${message}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
package/src/templates/layout.ejs
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
JSkelet varsayılan layout'unun legacy EJS kopyası.
|
|
3
|
-
|
|
4
|
-
Yeni uygulamalar `jskelet/layout` (.jsk) kullanır. Bu dosya yalnızca EJS
|
|
5
|
-
peer'i kurulu eski şablonlar için `jskelet/layout/ejs` üzerinden erişilir.
|
|
6
|
-
Uygulama `views/layout.jsk` veya `views/layout.ejs` oluşturduğu anda
|
|
7
|
-
framework layout'u devre dışı kalır.
|
|
8
|
-
|
|
9
|
-
Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
|
|
10
|
-
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
|
|
11
|
-
asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
|
|
12
|
-
`hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
|
|
13
|
-
-%>
|
|
14
|
-
<!DOCTYPE html>
|
|
15
|
-
<html lang="<%= lang %>">
|
|
16
|
-
<head>
|
|
17
|
-
<meta charset="utf-8">
|
|
18
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
-
<%# Kaynak ipuçları en başta: preconnect ve LCP preload'ını geciktirmek
|
|
20
|
-
doğrudan LCP'ye yazılır. Sayfaya özel head buradan gelir. %>
|
|
21
|
-
<%- extraHead %>
|
|
22
|
-
<%- stylesheets({ styles }) %>
|
|
23
|
-
<%- headMeta %>
|
|
24
|
-
<%- jsonLd({ items: structuredData }) %>
|
|
25
|
-
</head>
|
|
26
|
-
<body class="<%= bodyClass %>">
|
|
27
|
-
<%- body %>
|
|
28
|
-
<%- bodyScripts({ entries, devtools, devBasePath }) %>
|
|
29
|
-
</body>
|
|
30
|
-
</html>
|
|
1
|
+
<%#
|
|
2
|
+
JSkelet varsayılan layout'unun legacy EJS kopyası.
|
|
3
|
+
|
|
4
|
+
Yeni uygulamalar `jskelet/layout` (.jsk) kullanır. Bu dosya yalnızca EJS
|
|
5
|
+
peer'i kurulu eski şablonlar için `jskelet/layout/ejs` üzerinden erişilir.
|
|
6
|
+
Uygulama `views/layout.jsk` veya `views/layout.ejs` oluşturduğu anda
|
|
7
|
+
framework layout'u devre dışı kalır.
|
|
8
|
+
|
|
9
|
+
Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
|
|
10
|
+
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
|
|
11
|
+
asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
|
|
12
|
+
`hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
|
|
13
|
+
-%>
|
|
14
|
+
<!DOCTYPE html>
|
|
15
|
+
<html lang="<%= lang %>">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
+
<%# Kaynak ipuçları en başta: preconnect ve LCP preload'ını geciktirmek
|
|
20
|
+
doğrudan LCP'ye yazılır. Sayfaya özel head buradan gelir. %>
|
|
21
|
+
<%- extraHead %>
|
|
22
|
+
<%- stylesheets({ styles }) %>
|
|
23
|
+
<%- headMeta %>
|
|
24
|
+
<%- jsonLd({ items: structuredData }) %>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="<%= bodyClass %>">
|
|
27
|
+
<%- body %>
|
|
28
|
+
<%- bodyScripts({ entries, devtools, devBasePath }) %>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
package/src/templates/layout.jsk
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
{#
|
|
2
|
-
JSkelet varsayılan layout'u.
|
|
3
|
-
|
|
4
|
-
Uygulama `views/layout.jsk` (veya legacy `views/layout.ejs`) oluşturduğu
|
|
5
|
-
anda bu dosya devre dışı kalır; amacı yeni bir projenin tek route ile
|
|
6
|
-
çalışabilmesi. Kopyalayıp başlangıç noktası olarak kullanın.
|
|
7
|
-
|
|
8
|
-
Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
|
|
9
|
-
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
|
|
10
|
-
asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
|
|
11
|
-
`hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
|
|
12
|
-
|
|
13
|
-
Stylesheets / JsonLd / BodyScripts: `.jsk` ifade dilinde fonksiyon çağrısı
|
|
14
|
-
olmadığı için asset döngüleri bu yerleşik etiketlere taşındı.
|
|
15
|
-
#}
|
|
16
|
-
<!DOCTYPE html>
|
|
17
|
-
<html :lang="lang">
|
|
18
|
-
<head>
|
|
19
|
-
<meta charset="utf-8">
|
|
20
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
21
|
-
{{{ extraHead }}}
|
|
22
|
-
<Stylesheets :styles="styles" />
|
|
23
|
-
{{{ headMeta }}}
|
|
24
|
-
<JsonLd :items="structuredData" />
|
|
25
|
-
</head>
|
|
26
|
-
<body :class="bodyClass">
|
|
27
|
-
{{{ body }}}
|
|
28
|
-
<BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
|
|
29
|
-
</body>
|
|
30
|
-
</html>
|
|
1
|
+
{#
|
|
2
|
+
JSkelet varsayılan layout'u.
|
|
3
|
+
|
|
4
|
+
Uygulama `views/layout.jsk` (veya legacy `views/layout.ejs`) oluşturduğu
|
|
5
|
+
anda bu dosya devre dışı kalır; amacı yeni bir projenin tek route ile
|
|
6
|
+
çalışabilmesi. Kopyalayıp başlangıç noktası olarak kullanın.
|
|
7
|
+
|
|
8
|
+
Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
|
|
9
|
+
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
|
|
10
|
+
asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
|
|
11
|
+
`hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
|
|
12
|
+
|
|
13
|
+
Stylesheets / JsonLd / BodyScripts: `.jsk` ifade dilinde fonksiyon çağrısı
|
|
14
|
+
olmadığı için asset döngüleri bu yerleşik etiketlere taşındı.
|
|
15
|
+
#}
|
|
16
|
+
<!DOCTYPE html>
|
|
17
|
+
<html :lang="lang">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="utf-8">
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
21
|
+
{{{ extraHead }}}
|
|
22
|
+
<Stylesheets :styles="styles" />
|
|
23
|
+
{{{ headMeta }}}
|
|
24
|
+
<JsonLd :items="structuredData" />
|
|
25
|
+
</head>
|
|
26
|
+
<body :class="bodyClass">
|
|
27
|
+
{{{ body }}}
|
|
28
|
+
<BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|