jskelet 0.4.7 → 0.5.0

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.
@@ -18,6 +18,7 @@ import { getRedisStatus } from "../redis.js";
18
18
  import { getUpstreamLimiterStatus } from "../upstream-limiter.js";
19
19
  import { prewarmProgress } from "../prewarm.js";
20
20
  import { getConfig } from "../../config/index.js";
21
+ import { getRequestContext } from "../../http/request-context.js";
21
22
 
22
23
  /** Yollar config'ten: framework paket içine taşındığında `../..` sayan her hesap bozulur. */
23
24
  const ROOT = getConfig().root;
@@ -78,61 +79,176 @@ export function clearPageReports() {
78
79
  /* --------------------------------------------------- sunucu tarafı API çağrıları */
79
80
 
80
81
  /**
81
- * @type {{ url: string, host: string, method: string, status: number, ms: number,
82
- * bytes: number, at: number, error: string | null }[]}
82
+ * @typedef {{
83
+ * url: string,
84
+ * host: string,
85
+ * method: string,
86
+ * status: number,
87
+ * ms: number,
88
+ * bytes: number,
89
+ * at: number,
90
+ * error: string | null,
91
+ * page: string | null,
92
+ * details: unknown,
93
+ * }} ServerApiCall
83
94
  */
95
+
96
+ /** @type {ServerApiCall[]} */
84
97
  const serverApiCalls = [];
85
98
  const MAX_API_CALLS = 300;
86
99
 
100
+ /** Başarısız çağrı gövdesinin overlay'e taşınacak üst sınırı. */
101
+ const DETAILS_MAX = 4_000;
102
+
103
+ /**
104
+ * @typedef {{
105
+ * url: string,
106
+ * method: string,
107
+ * status: number,
108
+ * ms: number,
109
+ * bytes: number,
110
+ * error: string | null,
111
+ * page: string | null,
112
+ * details: unknown,
113
+ * }} ApiFailure
114
+ */
115
+
87
116
  /**
88
117
  * SSR sırasında yapılan dış çağrıları ölçer. `globalThis.fetch` sarılır;
89
118
  * yalnızca dev'de çağrıldığı için üretim yolu dokunulmaz kalır.
119
+ *
120
+ * Başarısız cevaplar (4xx/5xx ya da ağ) isteğe bağlı `onFailure` ile
121
+ * overlay hata günlüğüne de düşer — uygulama kendi logger'ıyla stderr'e
122
+ * yazsa bile panel "hangi sayfa hangi API" bilgisini görsün.
123
+ *
124
+ * @param {{ onFailure?: (call: ApiFailure) => void }} [options]
90
125
  */
91
- export function trackServerFetch() {
126
+ export function trackServerFetch(options = {}) {
92
127
  const original = globalThis.fetch;
93
- if (original.__jskeletWrapped) return;
128
+ if (/** @type {any} */ (original).__jskeletWrapped) return;
129
+
130
+ const { onFailure } = options;
94
131
 
95
132
  /** @type {typeof fetch} */
96
133
  const wrapped = async (input, init) => {
97
134
  const url = typeof input === "string" ? input : (input?.url ?? String(input));
135
+ const method = String(init?.method ?? "GET").toUpperCase();
98
136
  const started = Date.now();
137
+ const page = currentPage();
99
138
 
100
139
  // Kendi sunucumuza yapılan istekler (ısıtma, sağlık kontrolü) API sayılmaz.
101
- const isSelf = /^https?:\/\/(127\.0\.0\.1|localhost)/i.test(url);
140
+ const isSelf = /^https?:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:|\/|$)/i.test(url);
102
141
 
103
142
  try {
104
143
  const response = await original(input, init);
105
144
  if (!isSelf) {
106
- push({
145
+ const details = response.ok
146
+ ? null
147
+ : await readFailureDetails(response);
148
+ const error = response.ok ? null : summarizeFailure(response.status, details);
149
+ const call = {
107
150
  url,
108
- method: init?.method ?? "GET",
151
+ method,
109
152
  status: response.status,
110
153
  ms: Date.now() - started,
111
154
  bytes: Number(response.headers.get("content-length") ?? 0),
112
- error: response.ok ? null : `HTTP ${response.status}`,
113
- });
155
+ error,
156
+ page,
157
+ details,
158
+ };
159
+ push(call);
160
+ if (error) onFailure?.(call);
114
161
  }
115
162
  return response;
116
163
  } catch (error) {
117
164
  if (!isSelf) {
118
- push({
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ const call = {
119
167
  url,
120
- method: init?.method ?? "GET",
168
+ method,
121
169
  status: 0,
122
170
  ms: Date.now() - started,
123
171
  bytes: 0,
124
- error: error instanceof Error ? error.message : String(error),
125
- });
172
+ error: message,
173
+ page,
174
+ details: null,
175
+ };
176
+ push(call);
177
+ onFailure?.(call);
126
178
  }
127
179
  throw error;
128
180
  }
129
181
  };
130
182
 
131
- wrapped.__jskeletWrapped = true;
183
+ /** @type {any} */ (wrapped).__jskeletWrapped = true;
132
184
  globalThis.fetch = wrapped;
133
185
  }
134
186
 
135
- /** @param {{ url: string, method: string, status: number, ms: number, bytes: number, error: string | null }} call */
187
+ /** @returns {string | null} */
188
+ function currentPage() {
189
+ return getRequestContext()?.pathname ?? null;
190
+ }
191
+
192
+ /**
193
+ * @param {Response} response
194
+ * @returns {Promise<unknown>}
195
+ */
196
+ async function readFailureDetails(response) {
197
+ try {
198
+ const text = (await response.clone().text()).slice(0, DETAILS_MAX);
199
+ if (!text) return null;
200
+ try {
201
+ return JSON.parse(text);
202
+ } catch {
203
+ return text;
204
+ }
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Overlay başlığı: durum + varsa API'nin kendi doğrulama mesajı.
212
+ *
213
+ * @param {number} status
214
+ * @param {unknown} details
215
+ * @returns {string}
216
+ */
217
+ function summarizeFailure(status, details) {
218
+ const label = `HTTP ${status}`;
219
+ const detail = failureMessage(details);
220
+ return detail ? `${label}: ${detail}` : label;
221
+ }
222
+
223
+ /**
224
+ * Upstream gövdesinden kısa, okunabilir bir cümle çıkarır. `[object Object]`
225
+ * basmamak için nesneleri bilinçli dolaşır.
226
+ *
227
+ * @param {unknown} details
228
+ * @returns {string | null}
229
+ */
230
+ function failureMessage(details) {
231
+ if (details == null) return null;
232
+ if (typeof details === "string") return details.slice(0, 280);
233
+ if (typeof details !== "object") return String(details);
234
+
235
+ const record = /** @type {Record<string, unknown>} */ (details);
236
+ for (const key of ["details", "detail", "message", "error", "title"]) {
237
+ const value = record[key];
238
+ if (typeof value === "string" && value.trim()) return value.slice(0, 280);
239
+ if (value && typeof value === "object") {
240
+ const nested = failureMessage(value);
241
+ if (nested) return nested;
242
+ }
243
+ }
244
+ try {
245
+ return JSON.stringify(details).slice(0, 280);
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+
251
+ /** @param {Omit<ServerApiCall, "host" | "at">} call */
136
252
  function push(call) {
137
253
  let host = "—";
138
254
  try {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Controller sözleşmesi:
5
5
  * async (ctx) => { view, data?, metadata?, status?, revalidate?, head?,
6
- * bodyClass?, entries? }
6
+ * bodyClass?, entries?, styles? }
7
7
  * `ctx` → { params, query, pathname, req }
8
8
  *
9
9
  * `route()` üç kapsamı belirli bir sırayla iç içe kurar:
@@ -218,7 +218,8 @@ export async function renderView(view, data = {}) {
218
218
  * Sayfayı layout içinde render eder.
219
219
  *
220
220
  * @param {{ view: string, data?: object, metadata?: object, head?: string,
221
- * bodyClass?: string, entries?: string[], pathname?: string }} page
221
+ * bodyClass?: string, entries?: string[], styles?: string[],
222
+ * pathname?: string }} page
222
223
  * @returns {Promise<string>}
223
224
  */
224
225
  export async function renderPage(page) {
@@ -258,6 +259,7 @@ export async function renderPage(page) {
258
259
  (context.extraHead ?? ""),
259
260
  bodyClass: page.bodyClass ?? context.bodyClass ?? "",
260
261
  entries: page.entries ?? [],
262
+ styles: page.styles ?? [],
261
263
  devtools: isDev,
262
264
  devBasePath: config.brand.devBasePath,
263
265
  body,
@@ -304,7 +306,11 @@ export function route(controller, options = {}) {
304
306
  const isPrivate = options.private === true;
305
307
 
306
308
  return async (req, res, next) => {
307
- const context = createRequestContext({ private: isPrivate, res });
309
+ const context = createRequestContext({
310
+ private: isPrivate,
311
+ res,
312
+ pathname: req.path,
313
+ });
308
314
  const ctx = {
309
315
  params: req.params ?? {},
310
316
  query: req.query ?? {},
@@ -442,7 +448,11 @@ export function route(controller, options = {}) {
442
448
  */
443
449
  export function fragment(controller) {
444
450
  return async (req, res, next) => {
445
- const context = createRequestContext({ private: true, res });
451
+ const context = createRequestContext({
452
+ private: true,
453
+ res,
454
+ pathname: req.path,
455
+ });
446
456
  const ctx = {
447
457
  params: req.params ?? {},
448
458
  query: req.query ?? {},
@@ -6,8 +6,8 @@
6
6
  noktası olarak kullanın.
7
7
 
8
8
  Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
9
- body, bodyClass, entries, pathname, lang, devtools, devBasePath, asset,
10
- hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
9
+ body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
10
+ asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
11
11
  `hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
12
12
  -%>
13
13
  <!DOCTYPE html>
@@ -18,10 +18,16 @@
18
18
  <%# Kaynak ipuçları en başta: preconnect ve LCP preload'ını geciktirmek
19
19
  doğrudan LCP'ye yazılır. Sayfaya özel head buradan gelir. %>
20
20
  <%- extraHead %>
21
- <%# Tek, render-blocking stylesheet — build çalışmadıysa hiç basılmaz. %>
21
+ <%# Global sheet + controller `styles: [...]` sayfa sheet'leri.
22
+ data-jskelet-css: dev hot-swap doğru link'i bulsun diye. %>
22
23
  <% if (hasAsset('app.css')) { %>
23
- <link rel="stylesheet" href="<%= asset('app.css') %>">
24
+ <link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
24
25
  <% } %>
26
+ <% styles.forEach(function (sheet) { %>
27
+ <% if (hasAsset(sheet)) { %>
28
+ <link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
29
+ <% } %>
30
+ <% }); %>
25
31
  <%- headMeta %>
26
32
  <% structuredData.forEach(function (item) { %>
27
33
  <script type="application/ld+json"><%- jsonScript(item) %></script>