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.
- package/CHANGELOG.md +9 -0
- package/docs/02-mimari.md +8 -3
- package/docs/03-routing.md +2 -0
- package/docs/04-render-ve-sablonlar.md +11 -4
- package/docs/08-build.md +32 -4
- package/docs/09-dev-araclari.md +12 -6
- package/docs/en/02-architecture.md +8 -2
- package/docs/en/03-routing.md +2 -0
- package/docs/en/04-rendering.md +11 -4
- package/docs/en/08-build.md +37 -8
- package/docs/en/09-dev-tools.md +12 -7
- package/package.json +1 -1
- package/src/build/tasks/css.mjs +122 -13
- package/src/client/devtools/overlay.js +203 -9
- package/src/client/devtools/report.js +20 -0
- package/src/client/registry.js +8 -0
- package/src/http/request-context.js +4 -1
- package/src/server/dev/devtools.js +124 -7
- package/src/server/dev/report.js +131 -15
- package/src/server/render.js +14 -4
- package/src/templates/layout.ejs +10 -4
package/src/server/dev/report.js
CHANGED
|
@@ -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
|
-
* @
|
|
82
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
const call = {
|
|
119
167
|
url,
|
|
120
|
-
method
|
|
168
|
+
method,
|
|
121
169
|
status: 0,
|
|
122
170
|
ms: Date.now() - started,
|
|
123
171
|
bytes: 0,
|
|
124
|
-
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
|
-
/** @
|
|
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 {
|
package/src/server/render.js
CHANGED
|
@@ -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[],
|
|
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({
|
|
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({
|
|
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 ?? {},
|
package/src/templates/layout.ejs
CHANGED
|
@@ -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,
|
|
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
|
-
<%#
|
|
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>
|