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.
Files changed (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. 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";