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,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet.config.mjs` yükleyicisi ve çözümlenmiş proje durumu.
|
|
3
|
+
*
|
|
4
|
+
* Bu modül framework'ün **tek gerçek kaynağıdır**: proje kökü, dizin yolları,
|
|
5
|
+
* markalama, hook'lar ve `headers/redirects/rewrites/cache` kuralları burada
|
|
6
|
+
* normalize edilir. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
|
|
7
|
+
* Böylece framework `node_modules/` içine girdiğinde hiçbir dosyada
|
|
8
|
+
* `../..` sayma hatası oluşmaz.
|
|
9
|
+
*
|
|
10
|
+
* Config dosyası **zorunlu değildir**: yoksa ya da okunamıyorsa uyarı basılır
|
|
11
|
+
* ve sunucu varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi
|
|
12
|
+
* açılamaz hâle getirmemeli.
|
|
13
|
+
*
|
|
14
|
+
* Desteklenen bölümler (hepsi opsiyonel, hepsi `async` olabilir):
|
|
15
|
+
* headers() → [{ source, headers: [{ key, value }] }]
|
|
16
|
+
* redirects() → [{ source, destination, permanent?, statusCode? }]
|
|
17
|
+
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
|
+
* cache() → { html?: { [source]: saniye }, prewarm?: {...} }
|
|
19
|
+
*/
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import process from "node:process";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
|
+
import { compilePattern } from "./pattern.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_BRAND,
|
|
27
|
+
DEFAULT_DEV_GATE_BYPASS,
|
|
28
|
+
DEFAULT_DIRS,
|
|
29
|
+
DEFAULT_NAVIGATION,
|
|
30
|
+
DEFAULT_NAVIGATION_EXCLUDE,
|
|
31
|
+
DEFAULT_PREWARM,
|
|
32
|
+
DEFAULT_PREWARM_SKIP,
|
|
33
|
+
DEFAULT_STATIC,
|
|
34
|
+
} from "./defaults.js";
|
|
35
|
+
|
|
36
|
+
/** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
|
|
37
|
+
export const FRAMEWORK_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
38
|
+
|
|
39
|
+
const CONFIG_FILE = "jskelet.config.mjs";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {"conservative" | "moderate" | "eager"} Eagerness
|
|
43
|
+
*
|
|
44
|
+
* @typedef {object} NavigationConfig
|
|
45
|
+
* @property {false | Eagerness} prefetch
|
|
46
|
+
* @property {false | Eagerness} prerender
|
|
47
|
+
* @property {boolean} viewTransition
|
|
48
|
+
* @property {string[]} exclude Spekülasyon dışı bırakılan href desenleri.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {import('./pattern.js').CompiledPattern} CompiledPattern
|
|
53
|
+
*
|
|
54
|
+
* @typedef {object} ResolvedConfig
|
|
55
|
+
* @property {string} root Proje kökü (mutlak).
|
|
56
|
+
* @property {boolean} loaded Config dosyası okundu mu.
|
|
57
|
+
* @property {Record<string, string>} dirs Mutlak dizin yolları.
|
|
58
|
+
* @property {{ pattern: CompiledPattern, headers: { key: string, value: string }[] }[]} headers
|
|
59
|
+
* @property {{ pattern: CompiledPattern, destination: string, statusCode: number }[]} redirects
|
|
60
|
+
* @property {{ phase: "beforeFiles" | "afterFiles", pattern: CompiledPattern, destination: string }[]} rewrites
|
|
61
|
+
* @property {{ pattern: CompiledPattern, seconds: number }[]} html
|
|
62
|
+
* @property {Record<string, unknown>} prewarm
|
|
63
|
+
* @property {Record<string, unknown>} brand
|
|
64
|
+
* @property {Record<string, Function>} hooks
|
|
65
|
+
* @property {string} layout Layout `.ejs` dosyasının mutlak yolu.
|
|
66
|
+
* @property {string[] | null} routes Açık route modülü listesi.
|
|
67
|
+
* @property {{ extensions: Set<string>, prefixes: string[] }} static
|
|
68
|
+
* @property {string[]} devGateBypass
|
|
69
|
+
* @property {string[]} preconnect
|
|
70
|
+
* @property {NavigationConfig} navigation
|
|
71
|
+
* @property {string[]} prewarmSkip
|
|
72
|
+
* @property {string[]} watch Dev sunucusunun izlediği ek dizinler.
|
|
73
|
+
* @property {{ family: string, slug?: string, weights: number[] }[]} fonts
|
|
74
|
+
* @property {{ scan?: string[] } | false} icons
|
|
75
|
+
* @property {{ widths?: number[], quality?: number, skip?: string[] } | false} images
|
|
76
|
+
* @property {string[]} clientEnv Client bundle'a gömülecek env anahtarları.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/** @type {ResolvedConfig | null} */
|
|
80
|
+
let config = null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} value
|
|
84
|
+
* @param {string} label
|
|
85
|
+
* @returns {unknown[]}
|
|
86
|
+
*/
|
|
87
|
+
function asArray(value, label) {
|
|
88
|
+
if (value == null) return [];
|
|
89
|
+
if (Array.isArray(value)) return value;
|
|
90
|
+
console.warn(`[config] ${label} bir dizi döndürmeli, yok sayıldı`);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {unknown} raw
|
|
96
|
+
* @returns {ResolvedConfig["headers"]}
|
|
97
|
+
*/
|
|
98
|
+
function normalizeHeaders(raw) {
|
|
99
|
+
/** @type {ResolvedConfig["headers"]} */
|
|
100
|
+
const out = [];
|
|
101
|
+
|
|
102
|
+
for (const entry of asArray(raw, "headers()")) {
|
|
103
|
+
const pattern = compilePattern(entry?.source);
|
|
104
|
+
if (!pattern) continue;
|
|
105
|
+
|
|
106
|
+
const headers = asArray(entry?.headers, "headers()[].headers")
|
|
107
|
+
.filter((header) => header?.key && header?.value !== undefined)
|
|
108
|
+
.map((header) => ({ key: String(header.key), value: String(header.value) }));
|
|
109
|
+
|
|
110
|
+
if (headers.length) out.push({ pattern, headers });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {unknown} raw
|
|
118
|
+
* @returns {ResolvedConfig["redirects"]}
|
|
119
|
+
*/
|
|
120
|
+
function normalizeRedirects(raw) {
|
|
121
|
+
/** @type {ResolvedConfig["redirects"]} */
|
|
122
|
+
const out = [];
|
|
123
|
+
|
|
124
|
+
for (const entry of asArray(raw, "redirects()")) {
|
|
125
|
+
const pattern = compilePattern(entry?.source);
|
|
126
|
+
if (!pattern || typeof entry?.destination !== "string") continue;
|
|
127
|
+
|
|
128
|
+
// Next semantiği: permanent → 308, geçici → 307. Farklı bir kod isteyen
|
|
129
|
+
// `statusCode` verebilir (ör. eski kurulumlarla uyum için 301).
|
|
130
|
+
const statusCode = Number(entry.statusCode) || (entry.permanent ? 308 : 307);
|
|
131
|
+
|
|
132
|
+
out.push({ pattern, destination: entry.destination, statusCode });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {unknown} raw
|
|
140
|
+
* @returns {ResolvedConfig["rewrites"]}
|
|
141
|
+
*/
|
|
142
|
+
function normalizeRewrites(raw) {
|
|
143
|
+
/** @type {ResolvedConfig["rewrites"]} */
|
|
144
|
+
const out = [];
|
|
145
|
+
|
|
146
|
+
/** @type {[("beforeFiles" | "afterFiles"), unknown][]} */
|
|
147
|
+
const phases = Array.isArray(raw)
|
|
148
|
+
? [["afterFiles", raw]]
|
|
149
|
+
: [
|
|
150
|
+
["beforeFiles", raw?.beforeFiles],
|
|
151
|
+
["afterFiles", raw?.afterFiles],
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
for (const [phase, entries] of phases) {
|
|
155
|
+
for (const entry of asArray(entries, `rewrites().${phase}`)) {
|
|
156
|
+
const pattern = compilePattern(entry?.source);
|
|
157
|
+
if (!pattern || typeof entry?.destination !== "string") continue;
|
|
158
|
+
out.push({ phase, pattern, destination: entry.destination });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {unknown} raw
|
|
167
|
+
* @returns {{ html: ResolvedConfig["html"], prewarm: Record<string, unknown> }}
|
|
168
|
+
*/
|
|
169
|
+
function normalizeCache(raw) {
|
|
170
|
+
/** @type {ResolvedConfig["html"]} */
|
|
171
|
+
const html = [];
|
|
172
|
+
|
|
173
|
+
for (const [source, seconds] of Object.entries(raw?.html ?? {})) {
|
|
174
|
+
const pattern = compilePattern(source);
|
|
175
|
+
const value = Number(seconds);
|
|
176
|
+
if (!pattern || !Number.isFinite(value) || value < 0) continue;
|
|
177
|
+
html.push({ pattern, seconds: value });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { html, prewarm: { ...DEFAULT_PREWARM, ...(raw?.prewarm ?? {}) } };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Speculation Rules'un tanıdığı eagerness değerleri. */
|
|
184
|
+
const EAGERNESS = new Set(["conservative", "moderate", "eager"]);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `true` → varsayılan eagerness, `false` → kapalı, string → doğrulanır.
|
|
188
|
+
* Geçersiz bir değer siteyi düşürmemeli; uyarı basılıp varsayılana dönülür.
|
|
189
|
+
*
|
|
190
|
+
* @param {unknown} value
|
|
191
|
+
* @param {false | Eagerness} fallback
|
|
192
|
+
* @param {string} label
|
|
193
|
+
* @returns {false | Eagerness}
|
|
194
|
+
*/
|
|
195
|
+
function normalizeEagerness(value, fallback, label) {
|
|
196
|
+
if (value === undefined) return fallback;
|
|
197
|
+
if (value === false) return false;
|
|
198
|
+
if (value === true) return fallback === false ? "moderate" : fallback;
|
|
199
|
+
if (typeof value === "string" && EAGERNESS.has(value)) {
|
|
200
|
+
return /** @type {Eagerness} */ (value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.warn(
|
|
204
|
+
`[config] navigation.${label} geçersiz (${String(value)}), varsayılana dönüldü`,
|
|
205
|
+
);
|
|
206
|
+
return fallback;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {unknown} raw
|
|
211
|
+
* @param {Record<string, unknown>} brand
|
|
212
|
+
* @returns {NavigationConfig}
|
|
213
|
+
*/
|
|
214
|
+
function normalizeNavigation(raw, brand) {
|
|
215
|
+
const source = /** @type {Record<string, unknown>} */ (raw ?? {});
|
|
216
|
+
|
|
217
|
+
// Dev araçlarının yolu spekülasyona kapalı: overlay ve rapor uçları gerçek
|
|
218
|
+
// sayfa değil, önden getirilmelerinin hiçbir karşılığı yok.
|
|
219
|
+
const devBase = typeof brand.devBasePath === "string" ? brand.devBasePath : null;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
prefetch: normalizeEagerness(
|
|
223
|
+
source.prefetch,
|
|
224
|
+
DEFAULT_NAVIGATION.prefetch,
|
|
225
|
+
"prefetch",
|
|
226
|
+
),
|
|
227
|
+
prerender: normalizeEagerness(
|
|
228
|
+
source.prerender,
|
|
229
|
+
DEFAULT_NAVIGATION.prerender,
|
|
230
|
+
"prerender",
|
|
231
|
+
),
|
|
232
|
+
viewTransition: source.viewTransition === true,
|
|
233
|
+
exclude: [
|
|
234
|
+
...DEFAULT_NAVIGATION_EXCLUDE,
|
|
235
|
+
...(devBase ? [`${devBase}/*`] : []),
|
|
236
|
+
...asArray(source.exclude, "navigation.exclude").filter(
|
|
237
|
+
(entry) => typeof entry === "string",
|
|
238
|
+
),
|
|
239
|
+
].map(String),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Dizin adlarını mutlak yola çevirir. `styles` bir dosya yolu olduğu için
|
|
245
|
+
* de aynı çözümlemeden geçer; ayrı bir alan tutmaya değmez.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} root
|
|
248
|
+
* @param {Record<string, string>} [overrides]
|
|
249
|
+
* @returns {Record<string, string>}
|
|
250
|
+
*/
|
|
251
|
+
function resolveDirs(root, overrides) {
|
|
252
|
+
/** @type {Record<string, string>} */
|
|
253
|
+
const dirs = {};
|
|
254
|
+
const merged = { ...DEFAULT_DIRS, ...(overrides ?? {}) };
|
|
255
|
+
|
|
256
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
257
|
+
dirs[key] = path.resolve(root, value);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Build çıktısı `public/assets` altına yazılır; ayrı ayar gerektirmeyecek
|
|
261
|
+
// kadar sabit ama yol hesabı tek yerde kalsın.
|
|
262
|
+
dirs.assets = path.join(dirs.public, "assets");
|
|
263
|
+
dirs.fonts = path.join(dirs.public, "fonts");
|
|
264
|
+
|
|
265
|
+
return dirs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Uygulamanın layout'u yoksa framework'ün minimal layout'u kullanılır. Bu
|
|
270
|
+
* sayede yeni bir proje tek bir route ile çalışır hâle gelir.
|
|
271
|
+
*
|
|
272
|
+
* @param {Record<string, string>} dirs
|
|
273
|
+
* @param {string} [override]
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
function resolveLayout(dirs, override) {
|
|
277
|
+
if (override) return path.resolve(dirs.views, "..", override);
|
|
278
|
+
|
|
279
|
+
const appLayout = path.join(dirs.views, "layout.ejs");
|
|
280
|
+
if (fs.existsSync(appLayout)) return appLayout;
|
|
281
|
+
|
|
282
|
+
return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.ejs");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Config'i okur, normalize eder ve modül durumuna yazar. Sunucu ve build
|
|
287
|
+
* süreçleri açılışta bir kez çağırır.
|
|
288
|
+
*
|
|
289
|
+
* Aynı süreçte ikinci çağrı önbelleğe düşer: `jskelet start` hem
|
|
290
|
+
* `ensure-build` hem `createApp` üzerinden çağırıyor ve config'i iki kez
|
|
291
|
+
* okuyup iki kez loglamanın hiçbir faydası yok. Yeniden okumak gerekiyorsa
|
|
292
|
+
* `force: true`.
|
|
293
|
+
*
|
|
294
|
+
* @param {{ root?: string, configFile?: string, force?: boolean }} [options]
|
|
295
|
+
* @returns {Promise<ResolvedConfig>}
|
|
296
|
+
*/
|
|
297
|
+
export async function loadConfig(options = {}) {
|
|
298
|
+
if (config && !options.force) return config;
|
|
299
|
+
|
|
300
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
301
|
+
const configFile = options.configFile ?? CONFIG_FILE;
|
|
302
|
+
const configPath = path.join(root, configFile);
|
|
303
|
+
|
|
304
|
+
/** @type {Record<string, any>} */
|
|
305
|
+
let source = {};
|
|
306
|
+
let loaded = false;
|
|
307
|
+
|
|
308
|
+
if (!fs.existsSync(configPath)) {
|
|
309
|
+
console.warn(
|
|
310
|
+
`[config] ${configFile} bulunamadı — yerleşik varsayılanlarla devam ediliyor.`,
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
try {
|
|
314
|
+
// Windows'ta mutlak yol import'u için file:// şeması gerekir.
|
|
315
|
+
const module = await import(pathToFileURL(configPath).href);
|
|
316
|
+
source = module.default ?? module;
|
|
317
|
+
loaded = true;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn(`[config] ${configFile} yüklenemedi, yok sayıldı`, error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {string} name */
|
|
324
|
+
const section = async (name) => {
|
|
325
|
+
const value = source?.[name];
|
|
326
|
+
if (value == null) return null;
|
|
327
|
+
try {
|
|
328
|
+
return typeof value === "function" ? await value.call(source) : value;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.warn(`[config] ${name}() hata verdi, yok sayıldı`, error);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const [headers, redirects, rewrites, cache] = await Promise.all([
|
|
336
|
+
section("headers"),
|
|
337
|
+
section("redirects"),
|
|
338
|
+
section("rewrites"),
|
|
339
|
+
section("cache"),
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
const { html, prewarm } = normalizeCache(cache);
|
|
343
|
+
const dirs = resolveDirs(root, source.paths);
|
|
344
|
+
const brand = { ...DEFAULT_BRAND, ...(source.brand ?? {}) };
|
|
345
|
+
|
|
346
|
+
config = {
|
|
347
|
+
root,
|
|
348
|
+
loaded,
|
|
349
|
+
dirs,
|
|
350
|
+
headers: normalizeHeaders(headers),
|
|
351
|
+
redirects: normalizeRedirects(redirects),
|
|
352
|
+
rewrites: normalizeRewrites(rewrites),
|
|
353
|
+
html,
|
|
354
|
+
prewarm,
|
|
355
|
+
brand,
|
|
356
|
+
hooks: source.hooks ?? {},
|
|
357
|
+
layout: resolveLayout(dirs, source.layout),
|
|
358
|
+
routes: Array.isArray(source.routes) ? source.routes : null,
|
|
359
|
+
static: {
|
|
360
|
+
extensions: new Set(source.static?.extensions ?? DEFAULT_STATIC.extensions),
|
|
361
|
+
prefixes: source.static?.prefixes ?? DEFAULT_STATIC.prefixes,
|
|
362
|
+
},
|
|
363
|
+
devGateBypass: source.devGateBypass ?? DEFAULT_DEV_GATE_BYPASS,
|
|
364
|
+
preconnect: source.preconnect ?? [],
|
|
365
|
+
navigation: normalizeNavigation(source.navigation, brand),
|
|
366
|
+
prewarmSkip: source.prewarmSkip ?? DEFAULT_PREWARM_SKIP,
|
|
367
|
+
// `routes`, `views` ve `lib` zaten izlenir; buraya yalnızca ek dizinler.
|
|
368
|
+
watch: source.watch ?? [],
|
|
369
|
+
// Build tarafı ayarları. Sunucu bunları okumaz ama config tek dosya
|
|
370
|
+
// olsun diye aynı yerden geçer.
|
|
371
|
+
fonts: source.fonts ?? [],
|
|
372
|
+
icons: source.icons ?? {},
|
|
373
|
+
images: source.images ?? {},
|
|
374
|
+
clientEnv: source.clientEnv ?? [],
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Dev'de build ve sunucu ayrı alt süreçler; üçü de aynı özeti basınca satır
|
|
378
|
+
// banner'ın ve build bloğunun arasına üç kez giriyor. Özeti dış süreç basar.
|
|
379
|
+
if (loaded && !process.env.JSKELET_CHILD) {
|
|
380
|
+
const counts = [
|
|
381
|
+
config.headers.length && `${config.headers.length} header`,
|
|
382
|
+
config.redirects.length && `${config.redirects.length} redirect`,
|
|
383
|
+
config.rewrites.length && `${config.rewrites.length} rewrite`,
|
|
384
|
+
config.html.length && `${config.html.length} cache kuralı`,
|
|
385
|
+
].filter(Boolean);
|
|
386
|
+
|
|
387
|
+
if (counts.length) {
|
|
388
|
+
console.log(`[config] ${configFile} yüklendi — ${counts.join(", ")}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return config;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Çözümlenmiş config. `loadConfig()` çağrılmadan erişilirse boş bir proje
|
|
397
|
+
* kökü varsayımıyla çalışmak yerine hata verir: sessiz yanlış yol,
|
|
398
|
+
* "stylesheet neden yok" gibi teşhisi zor sorunlara dönüşüyor.
|
|
399
|
+
*
|
|
400
|
+
* @returns {ResolvedConfig}
|
|
401
|
+
*/
|
|
402
|
+
export function getConfig() {
|
|
403
|
+
if (!config) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"[config] loadConfig() çağrılmadan getConfig() kullanıldı. " +
|
|
406
|
+
"Sunucuyu `jskelet` CLI ile ya da createApp() üzerinden başlatın.",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return config;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Uygulamanın tanımladığı hook'u çalıştırır; yoksa `fallback` döner.
|
|
414
|
+
* Hook'un hata vermesi sayfayı düşürmemeli — framework kendi varsayılanına
|
|
415
|
+
* geri döner ve uyarır.
|
|
416
|
+
*
|
|
417
|
+
* @template T
|
|
418
|
+
* @param {string} name
|
|
419
|
+
* @param {T} fallback
|
|
420
|
+
* @param {unknown[]} args
|
|
421
|
+
* @returns {Promise<T>}
|
|
422
|
+
*/
|
|
423
|
+
export async function hook(name, fallback, ...args) {
|
|
424
|
+
const fn = getConfig().hooks?.[name];
|
|
425
|
+
if (typeof fn !== "function") return fallback;
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
return await fn(...args);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.warn(`[config] hooks.${name}() hata verdi, varsayılan kullanıldı`, error);
|
|
431
|
+
return fallback;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +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] geçersiz source (\`/\` ile başlamalı): ${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
|
+
}
|