jskelet 0.2.5 → 0.3.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/AGENTS.md +132 -132
- package/CHANGELOG.md +403 -383
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +103 -103
- package/docs/01-baslangic.md +285 -285
- package/docs/02-mimari.md +293 -287
- package/docs/03-routing.md +486 -480
- package/docs/04-render-ve-sablonlar.md +490 -490
- package/docs/05-islands.md +482 -482
- package/docs/06-cache.md +1231 -1209
- package/docs/07-yapilandirma.md +44 -21
- package/docs/08-build.md +366 -366
- package/docs/09-dev-araclari.md +335 -335
- package/docs/10-dagitim.md +329 -329
- package/docs/11-tasima.md +1 -0
- package/docs/12-panel-ve-oturum.md +384 -384
- package/docs/README.md +105 -105
- package/docs/en/01-getting-started.md +292 -292
- package/docs/en/02-architecture.md +311 -305
- package/docs/en/03-routing.md +503 -497
- package/docs/en/04-rendering.md +504 -504
- package/docs/en/05-islands.md +492 -492
- package/docs/en/06-caching.md +1197 -1198
- package/docs/en/07-configuration.md +1009 -986
- package/docs/en/08-build.md +383 -383
- package/docs/en/09-dev-tools.md +342 -342
- package/docs/en/10-deployment.md +332 -332
- package/docs/en/11-migration.md +360 -359
- package/docs/en/12-dashboards-and-sessions.md +392 -392
- package/docs/en/README.md +112 -112
- package/package.json +102 -102
- package/src/build/ensure-build.mjs +15 -15
- package/src/build/paths.mjs +143 -143
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +268 -268
- package/src/build/tasks/css.mjs +124 -124
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +224 -224
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/client/{cache-panel → admin}/i18n.js +756 -670
- package/src/client/{cache-panel → admin}/login.html +74 -74
- package/src/client/{cache-panel → admin}/panel.css +804 -756
- package/src/client/admin/panel.html +486 -0
- package/src/client/{cache-panel → admin}/panel.js +1242 -915
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +725 -725
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +35 -35
- package/src/client/registry.js +297 -297
- package/src/client/safe-image.js +91 -91
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/config/defaults.js +16 -7
- package/src/config/index.js +38 -22
- package/src/config/pattern.js +107 -107
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies.js +257 -257
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +162 -162
- package/src/index.js +83 -83
- package/src/init.mjs +221 -221
- package/src/log.mjs +58 -0
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -0
- package/src/server/admin/auth.js +125 -0
- package/src/server/admin/event-log.js +151 -0
- package/src/server/admin/gate.js +209 -0
- package/src/server/admin/inventory.js +188 -0
- package/src/server/admin/mount.js +56 -0
- package/src/server/admin/router.js +216 -0
- package/src/server/admin/snapshot.js +126 -0
- package/src/server/assets.js +147 -147
- package/src/server/cache-deps.js +42 -42
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +295 -291
- package/src/server/data-cache.js +462 -462
- package/src/server/dev/report.js +369 -369
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/html-cache.js +817 -817
- 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 +62 -62
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/static-precompressed.js +100 -100
- package/src/server/middleware/trailing-slash.js +53 -0
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/prewarm.js +601 -601
- package/src/server/redis.js +569 -569
- package/src/server/router.js +128 -128
- package/src/server/status-page.js +164 -164
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/start.mjs +7 -7
- package/src/templates/layout.ejs +44 -44
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +85 -85
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +245 -245
- package/src/client/cache-panel/panel.html +0 -308
- package/src/server/cache-panel.js +0 -759
package/src/config/index.js
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
18
|
* cache() → { html?: { [source]: saniye },
|
|
19
19
|
* query?: { [source]: string[] | true }, maxEntries?: number,
|
|
20
|
-
* data?: {...}, redis?: {...}, prewarm?: {...}
|
|
21
|
-
*
|
|
20
|
+
* data?: {...}, redis?: {...}, prewarm?: {...} }
|
|
21
|
+
* admin() → { enabled?, basePath?, allowIps?, blockBots?, … }
|
|
22
22
|
*
|
|
23
23
|
* Fonksiyon olmayan bölümler (`brand`, `security`, `static`, `navigation`…)
|
|
24
24
|
* düz nesne olarak okunur.
|
|
@@ -29,8 +29,8 @@ import process from "node:process";
|
|
|
29
29
|
import { pathToFileURL } from "node:url";
|
|
30
30
|
import { compilePattern, matchPattern } from "./pattern.js";
|
|
31
31
|
import {
|
|
32
|
+
DEFAULT_ADMIN,
|
|
32
33
|
DEFAULT_BRAND,
|
|
33
|
-
DEFAULT_CACHE_PANEL,
|
|
34
34
|
DEFAULT_CLOUDFLARE,
|
|
35
35
|
DEFAULT_DATA_CACHE,
|
|
36
36
|
DEFAULT_DEV_GATE_BYPASS,
|
|
@@ -96,7 +96,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
96
96
|
* @property {{ attempts: number, delayMs: number }} transientRetry
|
|
97
97
|
* @property {RedisConfig} redis Opsiyonel Redis ikinci kademesi.
|
|
98
98
|
* @property {typeof DEFAULT_UPSTREAM_LIMIT} upstream Upstream hız freni.
|
|
99
|
-
* @property {typeof
|
|
99
|
+
* @property {typeof DEFAULT_ADMIN} admin Framework yönetim paneli.
|
|
100
100
|
* @property {typeof DEFAULT_CLOUDFLARE} cloudflare Cloudflare cache yüzeyi.
|
|
101
101
|
* @property {Record<string, unknown>} prewarm
|
|
102
102
|
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
@@ -104,6 +104,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
104
104
|
* @property {Record<string, Function>} hooks
|
|
105
105
|
* @property {string} layout Layout `.ejs` dosyasının mutlak yolu.
|
|
106
106
|
* @property {string[] | null} routes Açık route modülü listesi.
|
|
107
|
+
* @property {boolean} trailingSlash URL'ler `/` ile bitsin mi (Next `trailingSlash`).
|
|
107
108
|
* @property {{ extensions: Set<string>, prefixes: string[] }} static
|
|
108
109
|
* @property {string[]} devGateBypass
|
|
109
110
|
* @property {string[]} preconnect
|
|
@@ -301,46 +302,58 @@ function normalizeUpstream(raw) {
|
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla açmanın bedeli,
|
|
308
|
-
* önbelleği boşaltabilen bir ucu internete koymak.
|
|
305
|
+
* Yönetim paneli. `enabled` yalnızca açıkça `true` verildiğinde ya da
|
|
306
|
+
* `JSKELET_ADMIN` ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla
|
|
307
|
+
* açmanın bedeli, önbelleği boşaltabilen bir ucu internete koymak.
|
|
309
308
|
*
|
|
310
309
|
* Ortam değişkeni config'in **üstünde** duruyor, çünkü paneli genelde bir
|
|
311
310
|
* arıza sırasında tek seferlik açmak isteniyor ve o an config dosyasını
|
|
312
|
-
* değiştirip yeniden dağıtmak istenmiyor. `
|
|
313
|
-
*
|
|
311
|
+
* değiştirip yeniden dağıtmak istenmiyor. `JSKELET_ADMIN=0` aynı mantıkla
|
|
312
|
+
* config'te açık olan paneli kapatır.
|
|
314
313
|
*
|
|
315
314
|
* @param {unknown} raw
|
|
316
|
-
* @returns {typeof
|
|
315
|
+
* @returns {typeof DEFAULT_ADMIN}
|
|
317
316
|
*/
|
|
318
|
-
function
|
|
317
|
+
function normalizeAdmin(raw) {
|
|
319
318
|
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
320
|
-
const env = process.env.
|
|
319
|
+
const env = process.env.JSKELET_ADMIN;
|
|
321
320
|
|
|
322
321
|
const basePath =
|
|
323
322
|
typeof source.basePath === "string" && source.basePath.startsWith("/")
|
|
324
323
|
? source.basePath.replace(/\/+$/, "")
|
|
325
|
-
:
|
|
324
|
+
: DEFAULT_ADMIN.basePath;
|
|
326
325
|
|
|
327
326
|
/** @param {string} key @param {number} min */
|
|
328
327
|
const positive = (key, min) => {
|
|
329
328
|
const value = Number(source[key]);
|
|
330
329
|
return Number.isFinite(value) && value >= min
|
|
331
330
|
? value
|
|
332
|
-
: /** @type {any} */ (
|
|
331
|
+
: /** @type {any} */ (DEFAULT_ADMIN)[key];
|
|
333
332
|
};
|
|
334
333
|
|
|
334
|
+
const allowIps = Array.isArray(source.allowIps)
|
|
335
|
+
? source.allowIps
|
|
336
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
337
|
+
.map((entry) => entry.trim())
|
|
338
|
+
: [...DEFAULT_ADMIN.allowIps];
|
|
339
|
+
|
|
340
|
+
const logSize = Number(source.logSize);
|
|
341
|
+
|
|
335
342
|
return {
|
|
336
343
|
enabled:
|
|
337
344
|
env === undefined
|
|
338
345
|
? source.enabled === true
|
|
339
346
|
: env !== "0" && env !== "false" && env !== "",
|
|
340
|
-
basePath: basePath ||
|
|
347
|
+
basePath: basePath || DEFAULT_ADMIN.basePath,
|
|
348
|
+
allowIps,
|
|
349
|
+
blockBots: source.blockBots !== false,
|
|
341
350
|
banAttempts: Math.floor(positive("banAttempts", 1)),
|
|
342
351
|
banHours: positive("banHours", 0),
|
|
343
352
|
sessionHours: positive("sessionHours", 0),
|
|
353
|
+
logSize:
|
|
354
|
+
Number.isFinite(logSize) && logSize >= 50
|
|
355
|
+
? Math.min(5000, Math.floor(logSize))
|
|
356
|
+
: DEFAULT_ADMIN.logSize,
|
|
344
357
|
};
|
|
345
358
|
}
|
|
346
359
|
|
|
@@ -423,7 +436,6 @@ function normalizeQueryRules(raw) {
|
|
|
423
436
|
* transientRetry: { attempts: number, delayMs: number },
|
|
424
437
|
* redis: RedisConfig,
|
|
425
438
|
* upstream: typeof DEFAULT_UPSTREAM_LIMIT,
|
|
426
|
-
* cachePanel: typeof DEFAULT_CACHE_PANEL,
|
|
427
439
|
* cloudflare: typeof DEFAULT_CLOUDFLARE,
|
|
428
440
|
* prewarm: Record<string, unknown>,
|
|
429
441
|
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
@@ -464,7 +476,6 @@ function normalizeCache(raw) {
|
|
|
464
476
|
: { ...DEFAULT_TRANSIENT_RETRY, ...(raw?.transientRetry ?? {}) },
|
|
465
477
|
redis: normalizeRedis(raw?.redis),
|
|
466
478
|
upstream: normalizeUpstream(raw?.upstream),
|
|
467
|
-
cachePanel: normalizeCachePanel(raw?.panel),
|
|
468
479
|
cloudflare: normalizeCloudflare(raw?.cloudflare),
|
|
469
480
|
// Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
|
|
470
481
|
// ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
|
|
@@ -669,11 +680,12 @@ export async function loadConfig(options = {}) {
|
|
|
669
680
|
}
|
|
670
681
|
};
|
|
671
682
|
|
|
672
|
-
const [headers, redirects, rewrites, cache] = await Promise.all([
|
|
683
|
+
const [headers, redirects, rewrites, cache, admin] = await Promise.all([
|
|
673
684
|
section("headers"),
|
|
674
685
|
section("redirects"),
|
|
675
686
|
section("rewrites"),
|
|
676
687
|
section("cache"),
|
|
688
|
+
section("admin"),
|
|
677
689
|
]);
|
|
678
690
|
|
|
679
691
|
const {
|
|
@@ -686,7 +698,6 @@ export async function loadConfig(options = {}) {
|
|
|
686
698
|
transientRetry,
|
|
687
699
|
redis,
|
|
688
700
|
upstream,
|
|
689
|
-
cachePanel,
|
|
690
701
|
cloudflare,
|
|
691
702
|
prewarm,
|
|
692
703
|
prewarmPriority,
|
|
@@ -710,7 +721,7 @@ export async function loadConfig(options = {}) {
|
|
|
710
721
|
transientRetry,
|
|
711
722
|
redis,
|
|
712
723
|
upstream,
|
|
713
|
-
|
|
724
|
+
admin: normalizeAdmin(admin),
|
|
714
725
|
cloudflare,
|
|
715
726
|
prewarm,
|
|
716
727
|
prewarmPriority,
|
|
@@ -718,6 +729,11 @@ export async function loadConfig(options = {}) {
|
|
|
718
729
|
hooks: source.hooks ?? {},
|
|
719
730
|
layout: resolveLayout(dirs, source.layout),
|
|
720
731
|
routes: Array.isArray(source.routes) ? source.routes : null,
|
|
732
|
+
// Varsayılan kapalı: açıkken `/hakkinda` → 308 `/hakkinda/` ve kanonik
|
|
733
|
+
// yanıt 200'dir. Kapalıyken slash dayatılmaz — Express'in non-strict
|
|
734
|
+
// eşleşmesi her iki biçimi de 200 ile servis eder (Next'in varsayılan
|
|
735
|
+
// "slash'ı kırp" davranışından bilinçli fark).
|
|
736
|
+
trailingSlash: source.trailingSlash === true,
|
|
721
737
|
static: {
|
|
722
738
|
extensions: new Set(source.static?.extensions ?? DEFAULT_STATIC.extensions),
|
|
723
739
|
prefixes: source.static?.prefixes ?? DEFAULT_STATIC.prefixes,
|
package/src/config/pattern.js
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `next.config` `source`/`destination` sözdiziminin küçük bir derleyicisi.
|
|
3
|
-
*
|
|
4
|
-
* Desteklenen biçimler:
|
|
5
|
-
* `/haber/:slug` → tek segment yakalar
|
|
6
|
-
* `/:path*` → sıfır veya daha fazla segment yakalar
|
|
7
|
-
* `/:path*.svg` → joker + sabit son ek (uzantı kuralları böyle yazılır)
|
|
8
|
-
* `/etiket-:slug` → segment ortasında parametre
|
|
9
|
-
*
|
|
10
|
-
* Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
|
|
11
|
-
* Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen kullanılan alt
|
|
12
|
-
* küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce literal
|
|
13
|
-
* kabul edilmez, uyarı üretir.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/** @typedef {{ regex: RegExp, keys: string[], source: string }} CompiledPattern */
|
|
17
|
-
|
|
18
|
-
const PARAM = /^:([A-Za-z_][A-Za-z0-9_]*)(\*)?/;
|
|
19
|
-
|
|
20
|
-
/** @param {string} text */
|
|
21
|
-
function escapeLiteral(text) {
|
|
22
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {string} source
|
|
27
|
-
* @returns {CompiledPattern | null}
|
|
28
|
-
*/
|
|
29
|
-
export function compilePattern(source) {
|
|
30
|
-
if (typeof source !== "string" || !source.startsWith("/")) {
|
|
31
|
-
console.warn(`[config] invalid source (must start with \`/\`): ${source}`);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** @type {string[]} */
|
|
36
|
-
const keys = [];
|
|
37
|
-
let pattern = "";
|
|
38
|
-
let index = 0;
|
|
39
|
-
|
|
40
|
-
while (index < source.length) {
|
|
41
|
-
const char = source[index];
|
|
42
|
-
|
|
43
|
-
if (char !== ":") {
|
|
44
|
-
pattern += escapeLiteral(char);
|
|
45
|
-
index += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const match = source.slice(index).match(PARAM);
|
|
50
|
-
if (!match) {
|
|
51
|
-
pattern += escapeLiteral(char);
|
|
52
|
-
index += 1;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const [raw, name, wildcard] = match;
|
|
57
|
-
keys.push(name);
|
|
58
|
-
|
|
59
|
-
if (!wildcard) {
|
|
60
|
-
// Tek segmentli parametre segment sınırını aşmaz.
|
|
61
|
-
pattern += "([^/]+)";
|
|
62
|
-
index += raw.length;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Joker parametre segment sınırlarını aşar ve **sıfır** segment de
|
|
67
|
-
// yakalar: `/hesabim/:path*` kuralı `/hesabim`i de kapsamalı, aksi hâlde
|
|
68
|
-
// bir bölümün tamamını kapatmak isteyen kural kök yolu atlıyor. Bunun için
|
|
69
|
-
// hemen öncesindeki `/` de opsiyonele alınır.
|
|
70
|
-
const trailingSlash = pattern.endsWith("/");
|
|
71
|
-
pattern = trailingSlash ? `${pattern.slice(0, -1)}(?:/(.*))?` : `${pattern}(.*)`;
|
|
72
|
-
index += raw.length;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { regex: new RegExp(`^${pattern}$`), keys, source };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {CompiledPattern} compiled
|
|
80
|
-
* @param {string} pathname
|
|
81
|
-
* @returns {Record<string, string> | null}
|
|
82
|
-
*/
|
|
83
|
-
export function matchPattern(compiled, pathname) {
|
|
84
|
-
const match = compiled.regex.exec(pathname);
|
|
85
|
-
if (!match) return null;
|
|
86
|
-
|
|
87
|
-
/** @type {Record<string, string>} */
|
|
88
|
-
const params = {};
|
|
89
|
-
compiled.keys.forEach((key, i) => {
|
|
90
|
-
params[key] = match[i + 1] ?? "";
|
|
91
|
-
});
|
|
92
|
-
return params;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* `destination` içindeki `:param` yer tutucularını doldurur.
|
|
97
|
-
*
|
|
98
|
-
* @param {string} destination
|
|
99
|
-
* @param {Record<string, string>} params
|
|
100
|
-
* @returns {string}
|
|
101
|
-
*/
|
|
102
|
-
export function fillDestination(destination, params) {
|
|
103
|
-
return destination.replace(
|
|
104
|
-
/:([A-Za-z_][A-Za-z0-9_]*)(\*)?/g,
|
|
105
|
-
(raw, name) => params[name] ?? raw,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `next.config` `source`/`destination` sözdiziminin küçük bir derleyicisi.
|
|
3
|
+
*
|
|
4
|
+
* Desteklenen biçimler:
|
|
5
|
+
* `/haber/:slug` → tek segment yakalar
|
|
6
|
+
* `/:path*` → sıfır veya daha fazla segment yakalar
|
|
7
|
+
* `/:path*.svg` → joker + sabit son ek (uzantı kuralları böyle yazılır)
|
|
8
|
+
* `/etiket-:slug` → segment ortasında parametre
|
|
9
|
+
*
|
|
10
|
+
* Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
|
|
11
|
+
* Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen kullanılan alt
|
|
12
|
+
* küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce literal
|
|
13
|
+
* kabul edilmez, uyarı üretir.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @typedef {{ regex: RegExp, keys: string[], source: string }} CompiledPattern */
|
|
17
|
+
|
|
18
|
+
const PARAM = /^:([A-Za-z_][A-Za-z0-9_]*)(\*)?/;
|
|
19
|
+
|
|
20
|
+
/** @param {string} text */
|
|
21
|
+
function escapeLiteral(text) {
|
|
22
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} source
|
|
27
|
+
* @returns {CompiledPattern | null}
|
|
28
|
+
*/
|
|
29
|
+
export function compilePattern(source) {
|
|
30
|
+
if (typeof source !== "string" || !source.startsWith("/")) {
|
|
31
|
+
console.warn(`[config] invalid source (must start with \`/\`): ${source}`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const keys = [];
|
|
37
|
+
let pattern = "";
|
|
38
|
+
let index = 0;
|
|
39
|
+
|
|
40
|
+
while (index < source.length) {
|
|
41
|
+
const char = source[index];
|
|
42
|
+
|
|
43
|
+
if (char !== ":") {
|
|
44
|
+
pattern += escapeLiteral(char);
|
|
45
|
+
index += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const match = source.slice(index).match(PARAM);
|
|
50
|
+
if (!match) {
|
|
51
|
+
pattern += escapeLiteral(char);
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [raw, name, wildcard] = match;
|
|
57
|
+
keys.push(name);
|
|
58
|
+
|
|
59
|
+
if (!wildcard) {
|
|
60
|
+
// Tek segmentli parametre segment sınırını aşmaz.
|
|
61
|
+
pattern += "([^/]+)";
|
|
62
|
+
index += raw.length;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Joker parametre segment sınırlarını aşar ve **sıfır** segment de
|
|
67
|
+
// yakalar: `/hesabim/:path*` kuralı `/hesabim`i de kapsamalı, aksi hâlde
|
|
68
|
+
// bir bölümün tamamını kapatmak isteyen kural kök yolu atlıyor. Bunun için
|
|
69
|
+
// hemen öncesindeki `/` de opsiyonele alınır.
|
|
70
|
+
const trailingSlash = pattern.endsWith("/");
|
|
71
|
+
pattern = trailingSlash ? `${pattern.slice(0, -1)}(?:/(.*))?` : `${pattern}(.*)`;
|
|
72
|
+
index += raw.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { regex: new RegExp(`^${pattern}$`), keys, source };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {CompiledPattern} compiled
|
|
80
|
+
* @param {string} pathname
|
|
81
|
+
* @returns {Record<string, string> | null}
|
|
82
|
+
*/
|
|
83
|
+
export function matchPattern(compiled, pathname) {
|
|
84
|
+
const match = compiled.regex.exec(pathname);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
|
|
87
|
+
/** @type {Record<string, string>} */
|
|
88
|
+
const params = {};
|
|
89
|
+
compiled.keys.forEach((key, i) => {
|
|
90
|
+
params[key] = match[i + 1] ?? "";
|
|
91
|
+
});
|
|
92
|
+
return params;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `destination` içindeki `:param` yer tutucularını doldurur.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} destination
|
|
99
|
+
* @param {Record<string, string>} params
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function fillDestination(destination, params) {
|
|
103
|
+
return destination.replace(
|
|
104
|
+
/:([A-Za-z_][A-Za-z0-9_]*)(\*)?/g,
|
|
105
|
+
(raw, name) => params[name] ?? raw,
|
|
106
|
+
);
|
|
107
|
+
}
|
package/src/http/control-flow.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
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 | 303 | 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
|
-
/**
|
|
49
|
-
* POST sonrası yönlendirme (303 See Other).
|
|
50
|
-
*
|
|
51
|
-
* `redirect()` 307 kullanır ve 307 **metodu korur**: bir POST handler'ından
|
|
52
|
-
* çağrıldığında tarayıcı hedefe yeniden POST eder. Form gönderiminden sonra
|
|
53
|
-
* sayfayı GET olarak açmak — yani geri tuşunun formu yeniden göndermediği
|
|
54
|
-
* klasik "post/redirect/get" akışı — 303 gerektiriyor.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} location
|
|
57
|
-
* @returns {never}
|
|
58
|
-
*/
|
|
59
|
-
export function seeOther(location) {
|
|
60
|
-
throw new RedirectError(location, 303);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** @param {unknown} error */
|
|
64
|
-
export function isNotFoundError(error) {
|
|
65
|
-
return error instanceof NotFoundError;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** @param {unknown} error */
|
|
69
|
-
export function isRedirectError(error) {
|
|
70
|
-
return error instanceof RedirectError;
|
|
71
|
-
}
|
|
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 | 303 | 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
|
+
/**
|
|
49
|
+
* POST sonrası yönlendirme (303 See Other).
|
|
50
|
+
*
|
|
51
|
+
* `redirect()` 307 kullanır ve 307 **metodu korur**: bir POST handler'ından
|
|
52
|
+
* çağrıldığında tarayıcı hedefe yeniden POST eder. Form gönderiminden sonra
|
|
53
|
+
* sayfayı GET olarak açmak — yani geri tuşunun formu yeniden göndermediği
|
|
54
|
+
* klasik "post/redirect/get" akışı — 303 gerektiriyor.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} location
|
|
57
|
+
* @returns {never}
|
|
58
|
+
*/
|
|
59
|
+
export function seeOther(location) {
|
|
60
|
+
throw new RedirectError(location, 303);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {unknown} error */
|
|
64
|
+
export function isNotFoundError(error) {
|
|
65
|
+
return error instanceof NotFoundError;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {unknown} error */
|
|
69
|
+
export function isRedirectError(error) {
|
|
70
|
+
return error instanceof RedirectError;
|
|
71
|
+
}
|