jskelet 0.1.1
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 +127 -0
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +342 -0
- package/bin/jskelet.mjs +104 -0
- package/docs/01-baslangic.md +285 -0
- package/docs/02-mimari.md +287 -0
- package/docs/03-routing.md +437 -0
- package/docs/04-render-ve-sablonlar.md +490 -0
- package/docs/05-islands.md +429 -0
- package/docs/06-cache.md +409 -0
- package/docs/07-yapilandirma.md +673 -0
- package/docs/08-build.md +366 -0
- package/docs/09-dev-araclari.md +302 -0
- package/docs/10-dagitim.md +329 -0
- package/docs/11-tasima.md +352 -0
- package/docs/README.md +82 -0
- package/package.json +97 -0
- package/src/build/build.mjs +138 -0
- package/src/build/ensure-build.mjs +15 -0
- package/src/build/paths.mjs +118 -0
- package/src/build/resolve-peer.mjs +36 -0
- package/src/build/tasks/client.mjs +268 -0
- package/src/build/tasks/css.mjs +124 -0
- package/src/build/tasks/fonts.mjs +146 -0
- package/src/build/tasks/icons.mjs +224 -0
- package/src/build/tasks/images.mjs +244 -0
- package/src/build/tasks/precompress.mjs +78 -0
- package/src/client/devtools/overlay.js +1763 -0
- package/src/client/devtools/report.html +185 -0
- package/src/client/devtools/report.js +712 -0
- package/src/client/dom.js +95 -0
- package/src/client/index.js +26 -0
- package/src/client/registry.js +223 -0
- package/src/client/safe-image.js +91 -0
- package/src/client/store.js +36 -0
- package/src/config/defaults.js +102 -0
- package/src/config/index.js +433 -0
- package/src/config/pattern.js +107 -0
- package/src/dev-server.mjs +383 -0
- package/src/http/control-flow.js +56 -0
- package/src/http/request-cache.js +46 -0
- package/src/index.js +35 -0
- package/src/init.mjs +220 -0
- package/src/log.mjs +332 -0
- package/src/logo.png +0 -0
- package/src/runtime/alias-hooks.mjs +119 -0
- package/src/runtime/register.mjs +4 -0
- package/src/server/assets.js +119 -0
- package/src/server/create-app.js +167 -0
- package/src/server/dev/devtools.js +383 -0
- package/src/server/dev/report.js +351 -0
- package/src/server/head-hints.js +132 -0
- package/src/server/html-cache.js +166 -0
- package/src/server/metadata.js +102 -0
- package/src/server/middleware/compression.js +205 -0
- package/src/server/middleware/dev-gate.js +62 -0
- package/src/server/middleware/headers.js +37 -0
- package/src/server/middleware/redirects.js +32 -0
- package/src/server/middleware/static-precompressed.js +100 -0
- package/src/server/middleware/upstream-proxy.js +141 -0
- package/src/server/prewarm.js +283 -0
- package/src/server/render.js +356 -0
- package/src/server/router.js +121 -0
- package/src/server/status-page.js +164 -0
- package/src/server/upstream-tracking.js +51 -0
- package/src/start.mjs +7 -0
- package/src/templates/layout.ejs +44 -0
- package/src/version.mjs +17 -0
- package/src/views/components/loader.js +85 -0
- package/src/views/helpers/html.js +102 -0
- package/src/views/helpers/tags.js +193 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geliştirme akışı: build watch + sunucu, tek terminalde.
|
|
3
|
+
*
|
|
4
|
+
* Alt süreçlerin çıktısı olduğu gibi akmaz. Build satırları başlangıç
|
|
5
|
+
* bloğunda görünür; sonrası zaman damgalı tek satırlık olaylara indirgenir ve
|
|
6
|
+
* hata yığınları çerçeveli kutuya dönüşür. Kendi framework'ünü geliştirirken
|
|
7
|
+
* hatanın akış içinde kaybolmaması, gerçekten fark yaratan ayrıntı.
|
|
8
|
+
*
|
|
9
|
+
* NODE_ENV ataması da platformdan bağımsız olarak burada yapılır
|
|
10
|
+
* (`cross-env` gerekmez).
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import process from "node:process";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import * as log from "./log.mjs";
|
|
18
|
+
import { loadConfig } from "./config/index.js";
|
|
19
|
+
import { FRAMEWORK_VERSION } from "./version.mjs";
|
|
20
|
+
|
|
21
|
+
const ROOT = process.cwd();
|
|
22
|
+
const SRC = import.meta.dirname;
|
|
23
|
+
|
|
24
|
+
const config = await loadConfig();
|
|
25
|
+
|
|
26
|
+
const env = {
|
|
27
|
+
...process.env,
|
|
28
|
+
NODE_ENV: "development",
|
|
29
|
+
JSKELET_CHILD: "1",
|
|
30
|
+
// Alt süreçler boruya yazdığı için renk algılaması kapanır; burada zorlanır.
|
|
31
|
+
...(process.stdout.isTTY && !process.env.NO_COLOR ? { JSKELET_COLOR: "1" } : {}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const started = Date.now();
|
|
35
|
+
let ready = false;
|
|
36
|
+
let buildReady = false;
|
|
37
|
+
/** @type {string | null} */
|
|
38
|
+
let serverUrl = null;
|
|
39
|
+
let restartedAt = 0;
|
|
40
|
+
let restarting = false;
|
|
41
|
+
|
|
42
|
+
log.banner(`v${FRAMEWORK_VERSION}`, "development", ROOT);
|
|
43
|
+
|
|
44
|
+
/** @type {import('node:child_process').ChildProcess[]} */
|
|
45
|
+
const children = [];
|
|
46
|
+
|
|
47
|
+
/** Yeniden başlatma sırasındaki beklenen çıkışı işaretler. */
|
|
48
|
+
const STOPPING = Symbol("jskelet.stopping");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Satır tamponu: parça parça gelen çıktıyı tam satırlara böler.
|
|
52
|
+
*
|
|
53
|
+
* @param {import('node:stream').Readable} stream
|
|
54
|
+
* @param {(line: string) => void} onLine
|
|
55
|
+
*/
|
|
56
|
+
function readLines(stream, onLine) {
|
|
57
|
+
let buffer = "";
|
|
58
|
+
|
|
59
|
+
stream.setEncoding("utf8");
|
|
60
|
+
stream.on("data", (chunk) => {
|
|
61
|
+
buffer += chunk;
|
|
62
|
+
const lines = buffer.split("\n");
|
|
63
|
+
buffer = lines.pop() ?? "";
|
|
64
|
+
for (const line of lines) onLine(line);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build alt süreci kendi biçimini bastığı için satırları olduğu gibi geçer.
|
|
70
|
+
* @param {string} line
|
|
71
|
+
*/
|
|
72
|
+
function onBuildLine(line) {
|
|
73
|
+
if (line.includes("[jskelet:build-ready]")) {
|
|
74
|
+
buildReady = true;
|
|
75
|
+
announceReady();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (line.trim()) process.stdout.write(`${line}\n`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* "Ready" özeti hem build hem sunucu hazır olduğunda basılır; aksi hâlde
|
|
84
|
+
* özet, arkadan gelen build satırlarının arasında kalıyor.
|
|
85
|
+
*/
|
|
86
|
+
function announceReady() {
|
|
87
|
+
if (ready || !buildReady || !serverUrl) return;
|
|
88
|
+
ready = true;
|
|
89
|
+
|
|
90
|
+
log.ready({ elapsed: Date.now() - started, url: serverUrl, watching: true });
|
|
91
|
+
log.event({
|
|
92
|
+
symbol: log.symbols.cycle,
|
|
93
|
+
scope: "devtools",
|
|
94
|
+
message: "overlay ready",
|
|
95
|
+
note: "Alt+D",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ------------------------------------------------------- sunucu çıktısı */
|
|
100
|
+
|
|
101
|
+
/** @type {string[]} */
|
|
102
|
+
let errorBuffer = [];
|
|
103
|
+
/** @type {NodeJS.Timeout | null} */
|
|
104
|
+
let errorTimer = null;
|
|
105
|
+
|
|
106
|
+
/** Yığın satırları parça parça gelir; kısa bir sessizlikten sonra basılır. */
|
|
107
|
+
function scheduleErrorFlush() {
|
|
108
|
+
if (errorTimer) clearTimeout(errorTimer);
|
|
109
|
+
errorTimer = setTimeout(flushError, 60);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function flushError() {
|
|
113
|
+
const lines = errorBuffer;
|
|
114
|
+
errorBuffer = [];
|
|
115
|
+
errorTimer = null;
|
|
116
|
+
if (!lines.length) return;
|
|
117
|
+
|
|
118
|
+
const header = lines.find((line) => /^[\w.]*Error\b/.test(line)) ?? lines[0];
|
|
119
|
+
const match = header.match(/^([\w.]*Error)\b:?\s*(.*)$/);
|
|
120
|
+
|
|
121
|
+
const frames = lines
|
|
122
|
+
.filter((line) => line.trim().startsWith("at "))
|
|
123
|
+
.slice(0, 3)
|
|
124
|
+
.map((line) => line.trim().replace(new RegExp(escapeRegExp(ROOT), "g"), "."));
|
|
125
|
+
|
|
126
|
+
log.errorBox({
|
|
127
|
+
title: "Server Error",
|
|
128
|
+
name: match?.[1] ?? "Error",
|
|
129
|
+
message: match?.[2] || header,
|
|
130
|
+
lines: frames,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
log.event({ symbol: log.symbols.cycle, scope: "server", message: "watching" });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @param {string} value */
|
|
137
|
+
function escapeRegExp(value) {
|
|
138
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} line
|
|
143
|
+
* @param {boolean} isError
|
|
144
|
+
*/
|
|
145
|
+
function onServerLine(line, isError) {
|
|
146
|
+
// Sunucunun dev araçları zaten bu modülün biçimini kullanıyor; renk kodları
|
|
147
|
+
// satır başında olabildiği için karşılaştırma ANSI'siz yapılır.
|
|
148
|
+
// eslint-disable-next-line no-control-regex -- ANSI escape'i eşlemenin yolu bu.
|
|
149
|
+
const plain = line.replace(/\u001b\[\d+m/g, "");
|
|
150
|
+
if (/^(?:\d{2}:\d{2}:\d{2}\s|[┌│└])/.test(plain)) {
|
|
151
|
+
process.stdout.write(`${line}\n`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const listening = line.match(/^jskelet → (\S+)/);
|
|
156
|
+
if (listening) {
|
|
157
|
+
serverUrl = listening[1];
|
|
158
|
+
|
|
159
|
+
if (!ready) {
|
|
160
|
+
announceReady();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!restarting) return;
|
|
165
|
+
|
|
166
|
+
restarting = false;
|
|
167
|
+
log.event({
|
|
168
|
+
scope: "server",
|
|
169
|
+
message: "restarted",
|
|
170
|
+
time: restartedAt ? Date.now() - restartedAt : null,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!line.trim()) return;
|
|
176
|
+
|
|
177
|
+
// Hata gövdesi ve yığın satırları kutuya toplanır.
|
|
178
|
+
if (isError || /^\s+at\s/.test(line) || /Error\b/.test(line)) {
|
|
179
|
+
errorBuffer.push(
|
|
180
|
+
line.replace(/^\[(?:uncaughtException|unhandledRejection)\]\s*/, ""),
|
|
181
|
+
);
|
|
182
|
+
scheduleErrorFlush();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
log.event({ scope: "server", message: line });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string[]} args
|
|
191
|
+
* @param {string} label
|
|
192
|
+
* @param {boolean} passthrough
|
|
193
|
+
*/
|
|
194
|
+
function run(args, label, passthrough) {
|
|
195
|
+
const child = spawn(process.execPath, args, {
|
|
196
|
+
cwd: ROOT,
|
|
197
|
+
env,
|
|
198
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
readLines(child.stdout, (line) =>
|
|
202
|
+
passthrough ? onBuildLine(line) : onServerLine(line, false),
|
|
203
|
+
);
|
|
204
|
+
readLines(child.stderr, (line) =>
|
|
205
|
+
passthrough ? onBuildLine(line) : onServerLine(line, true),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
child.on("exit", (code) => {
|
|
209
|
+
if (code === 0 || code === null) return;
|
|
210
|
+
if (child[STOPPING]) return; // yeniden başlatma: beklenen çıkış
|
|
211
|
+
log.error(`${label} exited (code ${code})`);
|
|
212
|
+
shutdown(code);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
children.push(child);
|
|
216
|
+
return child;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ---------------------------------------------------------------- watcher */
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Sunucu yeniden başlatmayı `node --watch` yerine kendimiz yönetiyoruz.
|
|
223
|
+
*
|
|
224
|
+
* `--watch-path` verilse bile Node bu kurulumda proje kökünü izliyordu; build
|
|
225
|
+
* çıktısı (`public/assets`, `manifest.json`) ya da dev araçlarının günlüğü
|
|
226
|
+
* yazıldığında sunucu boşuna yeniden başlıyor, hatta kendini besleyen bir
|
|
227
|
+
* döngü kuruluyordu. Kendi watcher'ımız yalnızca sunucu kaynaklarını izler,
|
|
228
|
+
* değişiklikleri birleştirir ve hangi dosyaların değiştiğini bildirir.
|
|
229
|
+
*
|
|
230
|
+
* `views` de izlenir: bileşenlerin çoğu `views/components/**.js` içinde ve bu
|
|
231
|
+
* modüller sunucuya bir kez import edildiği için, restart olmadan yapılan
|
|
232
|
+
* değişiklik tarayıcıya hiç yansımıyordu (şablon düzenleyip "hiçbir şey
|
|
233
|
+
* değişmedi" hissi buradan geliyor). `client/` ve `styles/` burada **yok**;
|
|
234
|
+
* onları esbuild ve CSS watcher'ları kendi içinde hallediyor.
|
|
235
|
+
*/
|
|
236
|
+
const WATCH_DIRS = [
|
|
237
|
+
config.dirs.routes,
|
|
238
|
+
config.dirs.views,
|
|
239
|
+
path.join(ROOT, "lib"),
|
|
240
|
+
...(config.watch ?? []).map((dir) => path.resolve(ROOT, dir)),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const WATCH_EXTENSIONS = /\.(?:js|mjs|json|ejs)$/;
|
|
244
|
+
|
|
245
|
+
/** @type {import('node:child_process').ChildProcess | null} */
|
|
246
|
+
let server = null;
|
|
247
|
+
|
|
248
|
+
// `--import` modül belirteci bekler: Windows'ta `H:\…` yolu `h:` şemalı URL
|
|
249
|
+
// sanılıp reddediliyor, bu yüzden file:// URL'e çevrilir.
|
|
250
|
+
const SERVER_ARGS = [
|
|
251
|
+
// `.env` yoksa bayrak hiç geçilmez: `--env-file-if-exists` dosya yokken bir
|
|
252
|
+
// bildirim satırı basıyor ve bu satır hata kutusuna dönüşüyordu.
|
|
253
|
+
...(fs.existsSync(path.join(ROOT, ".env")) ? ["--env-file=.env"] : []),
|
|
254
|
+
"--import",
|
|
255
|
+
pathToFileURL(path.join(SRC, "runtime", "register.mjs")).href,
|
|
256
|
+
path.join(SRC, "start.mjs"),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
function startServerProcess() {
|
|
260
|
+
server = run(SERVER_ARGS, "server", false);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** @type {Set<string>} */
|
|
264
|
+
const changed = new Set();
|
|
265
|
+
/** @type {NodeJS.Timeout | null} */
|
|
266
|
+
let restartTimer = null;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Bilinen değişiklik zamanları. Windows'ta `fs.watch` bir dosya yazıldığında
|
|
270
|
+
* komşuları için de olay üretebiliyor; gerçekten değişmeyenleri elemezsek tek
|
|
271
|
+
* kaydetme iki restart'a dönüşüyor.
|
|
272
|
+
*
|
|
273
|
+
* @type {Map<string, number>}
|
|
274
|
+
*/
|
|
275
|
+
const mtimes = new Map();
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {string} file Mutlak yol.
|
|
279
|
+
* @returns {boolean} içerik zamanı gerçekten değiştiyse
|
|
280
|
+
*/
|
|
281
|
+
function touched(file) {
|
|
282
|
+
let mtime = 0;
|
|
283
|
+
try {
|
|
284
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
285
|
+
} catch {
|
|
286
|
+
// Silinmiş dosya: kaydı düşür ve değişiklik say.
|
|
287
|
+
return mtimes.delete(file);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (mtimes.get(file) === mtime) return false;
|
|
291
|
+
mtimes.set(file, mtime);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @param {string} file Mutlak yol. */
|
|
296
|
+
function onSourceChange(file) {
|
|
297
|
+
if (!touched(file)) return;
|
|
298
|
+
|
|
299
|
+
changed.add(path.relative(ROOT, file).split(path.sep).join("/"));
|
|
300
|
+
|
|
301
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
302
|
+
restartTimer = setTimeout(restartServer, 250);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function restartServer() {
|
|
306
|
+
restartTimer = null;
|
|
307
|
+
|
|
308
|
+
const files = [...changed];
|
|
309
|
+
changed.clear();
|
|
310
|
+
|
|
311
|
+
restarting = true;
|
|
312
|
+
restartedAt = Date.now();
|
|
313
|
+
|
|
314
|
+
log.event({
|
|
315
|
+
symbol: log.symbols.cycle,
|
|
316
|
+
scope: "server",
|
|
317
|
+
message: "restarting…",
|
|
318
|
+
note: files.length > 1 ? `${files.length} files` : files[0],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (process.env.JSKELET_VERBOSE === "1" && files.length > 1) {
|
|
322
|
+
for (const file of files) log.line(file);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (server) {
|
|
326
|
+
server[STOPPING] = true;
|
|
327
|
+
server.kill();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
startServerProcess();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function watchSources() {
|
|
334
|
+
for (const target of WATCH_DIRS) {
|
|
335
|
+
if (!fs.existsSync(target)) continue;
|
|
336
|
+
|
|
337
|
+
// Mevcut zamanlar önden okunur; ilk sahte olay da böylece elenir.
|
|
338
|
+
for (const entry of fs.readdirSync(target, {
|
|
339
|
+
recursive: true,
|
|
340
|
+
withFileTypes: true,
|
|
341
|
+
})) {
|
|
342
|
+
if (!entry.isFile() || !WATCH_EXTENSIONS.test(entry.name)) continue;
|
|
343
|
+
touched(path.join(entry.parentPath ?? entry.path, entry.name));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
fs.watch(target, { recursive: true }, (event, name) => {
|
|
348
|
+
if (!name || !WATCH_EXTENSIONS.test(name)) return;
|
|
349
|
+
onSourceChange(path.join(target, name));
|
|
350
|
+
});
|
|
351
|
+
} catch {
|
|
352
|
+
log.warn(
|
|
353
|
+
`${path.relative(ROOT, target)} izlenemedi; bu dizinde otomatik restart olmayacak.`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Config değişince sunucu da build de yeni ayarlarla açılmalı.
|
|
359
|
+
const configFile = path.join(ROOT, "jskelet.config.mjs");
|
|
360
|
+
if (fs.existsSync(configFile)) {
|
|
361
|
+
touched(configFile);
|
|
362
|
+
fs.watch(configFile, () => onSourceChange(configFile));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** @param {number} code */
|
|
367
|
+
function shutdown(code) {
|
|
368
|
+
for (const child of children) {
|
|
369
|
+
if (!child.killed) child.kill();
|
|
370
|
+
}
|
|
371
|
+
process.exit(code);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
375
|
+
process.on(signal, () => {
|
|
376
|
+
log.event({ symbol: log.symbols.cycle, scope: "dev", message: "shutting down" });
|
|
377
|
+
shutdown(0);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
run([path.join(SRC, "build", "build.mjs"), "--watch"], "build", true);
|
|
382
|
+
startServerProcess();
|
|
383
|
+
watchSources();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `next/navigation` içindeki notFound/redirect kontrol akışının karşılığı.
|
|
3
|
+
* Derinlerdeki bir fonksiyon throw eder, Express error handler yakalar.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class NotFoundError extends Error {
|
|
7
|
+
constructor(message = "Not Found") {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "NotFoundError";
|
|
10
|
+
this.statusCode = 404;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class RedirectError extends Error {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} location
|
|
17
|
+
* @param {301 | 302 | 307 | 308} [statusCode]
|
|
18
|
+
*/
|
|
19
|
+
constructor(location, statusCode = 307) {
|
|
20
|
+
super(`Redirect to ${location}`);
|
|
21
|
+
this.name = "RedirectError";
|
|
22
|
+
this.location = location;
|
|
23
|
+
this.statusCode = statusCode;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns {never} */
|
|
28
|
+
export function notFound() {
|
|
29
|
+
throw new NotFoundError();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} location
|
|
34
|
+
* @returns {never}
|
|
35
|
+
*/
|
|
36
|
+
export function permanentRedirect(location) {
|
|
37
|
+
throw new RedirectError(location, 308);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} location
|
|
42
|
+
* @returns {never}
|
|
43
|
+
*/
|
|
44
|
+
export function redirect(location) {
|
|
45
|
+
throw new RedirectError(location, 307);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} error */
|
|
49
|
+
export function isNotFoundError(error) {
|
|
50
|
+
return error instanceof NotFoundError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {unknown} error */
|
|
54
|
+
export function isRedirectError(error) {
|
|
55
|
+
return error instanceof RedirectError;
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React `cache()` fonksiyonunun karşılığı: aynı istek içinde aynı argümanlarla
|
|
3
|
+
* yapılan çağrılar tek kez çalışır. İstek bağlamı yoksa (script, client)
|
|
4
|
+
* memoizasyon devre dışı kalır ve fonksiyon doğrudan çağrılır.
|
|
5
|
+
*/
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
|
|
8
|
+
/** @type {AsyncLocalStorage<Map<unknown, Map<string, unknown>>>} */
|
|
9
|
+
const storage = new AsyncLocalStorage();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* İsteği bir memo bağlamı içinde çalıştırır.
|
|
13
|
+
* @param {() => T} run
|
|
14
|
+
* @returns {T}
|
|
15
|
+
* @template T
|
|
16
|
+
*/
|
|
17
|
+
export function withRequestCache(run) {
|
|
18
|
+
return storage.run(new Map(), run);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {F} fn
|
|
23
|
+
* @returns {F}
|
|
24
|
+
* @template {(...args: any[]) => any} F
|
|
25
|
+
*/
|
|
26
|
+
export function cache(fn) {
|
|
27
|
+
const wrapped = (...args) => {
|
|
28
|
+
const contextStore = storage.getStore();
|
|
29
|
+
if (!contextStore) return fn(...args);
|
|
30
|
+
|
|
31
|
+
let perFn = contextStore.get(fn);
|
|
32
|
+
if (!perFn) {
|
|
33
|
+
perFn = new Map();
|
|
34
|
+
contextStore.set(fn, perFn);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const key = args.length === 0 ? "" : JSON.stringify(args);
|
|
38
|
+
if (perFn.has(key)) return perFn.get(key);
|
|
39
|
+
|
|
40
|
+
const result = fn(...args);
|
|
41
|
+
perFn.set(key, result);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return /** @type {F} */ (wrapped);
|
|
46
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSkelet sunucu tarafı genel API'si.
|
|
3
|
+
*
|
|
4
|
+
* `import { route, notFound } from "jskelet"` — route dosyaları ve
|
|
5
|
+
* controller'lar bu yüzeyi kullanır. Alt yollardan (`jskelet/server`)
|
|
6
|
+
* ithal etmek de mümkün; buradaki liste "kararlı" sayılan yüzeydir.
|
|
7
|
+
*/
|
|
8
|
+
export { route, renderPage, renderView, renderNotFound } from "./server/render.js";
|
|
9
|
+
export { renderStatusPage, statusFromError } from "./server/status-page.js";
|
|
10
|
+
export { createApp, startServer } from "./server/create-app.js";
|
|
11
|
+
export {
|
|
12
|
+
notFound,
|
|
13
|
+
redirect,
|
|
14
|
+
permanentRedirect,
|
|
15
|
+
isNotFoundError,
|
|
16
|
+
isRedirectError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
RedirectError,
|
|
19
|
+
} from "./http/control-flow.js";
|
|
20
|
+
export { cache, withRequestCache } from "./http/request-cache.js";
|
|
21
|
+
export { reportUpstreamFailure } from "./server/upstream-tracking.js";
|
|
22
|
+
export { asset, hasAsset, optimizedImage, getSpriteIds } from "./server/assets.js";
|
|
23
|
+
export { headHints } from "./server/head-hints.js";
|
|
24
|
+
export { renderHeadMeta } from "./server/metadata.js";
|
|
25
|
+
export {
|
|
26
|
+
clearHtmlCache,
|
|
27
|
+
getHtmlCacheEntries,
|
|
28
|
+
getHtmlCacheSize,
|
|
29
|
+
withHtmlCache,
|
|
30
|
+
} from "./server/html-cache.js";
|
|
31
|
+
export { prewarm, prewarmProgress } from "./server/prewarm.js";
|
|
32
|
+
export { createProxy } from "./server/middleware/upstream-proxy.js";
|
|
33
|
+
export { getConfig, loadConfig } from "./config/index.js";
|
|
34
|
+
export { attrs, cn, cx, esc, jsonScript } from "./views/helpers/html.js";
|
|
35
|
+
export { icon, image, link, preloadImage } from "./views/helpers/tags.js";
|