jskelet 0.5.5 → 0.6.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 +19 -15
- package/CHANGELOG.md +165 -15
- package/README.md +16 -21
- package/bin/jskelet.mjs +23 -9
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +10 -4
- package/docs/03-routing.md +14 -7
- package/docs/04-render-ve-sablonlar.md +60 -43
- package/docs/05-islands.md +12 -8
- package/docs/06-cache.md +18 -7
- package/docs/07-yapilandirma.md +69 -27
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +22 -8
- package/docs/10-dagitim.md +14 -13
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +10 -33
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +12 -6
- package/docs/en/03-routing.md +15 -8
- package/docs/en/04-rendering.md +71 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/06-caching.md +21 -7
- package/docs/en/07-configuration.md +69 -29
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +24 -8
- package/docs/en/10-deployment.md +14 -14
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +10 -35
- package/package.json +48 -13
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +11 -1
- package/src/client/index.js +2 -2
- package/src/compile/codegen.js +4 -0
- package/src/compile/compile-all.js +12 -21
- package/src/compile/expr.js +5 -0
- package/src/compile/parse.js +64 -8
- package/src/compile/resolve.js +3 -0
- package/src/config/defaults.js +48 -5
- package/src/config/index.js +138 -27
- package/src/dev-server.mjs +26 -3
- package/src/http/cookies-entry.js +1 -0
- package/src/http/cookies.js +18 -0
- package/src/logo.png +0 -0
- package/src/migrate/apply.mjs +262 -0
- package/src/migrate/babel.mjs +79 -0
- package/src/migrate/classify.mjs +155 -0
- package/src/migrate/config.mjs +126 -0
- package/src/migrate/fs-walk.mjs +191 -0
- package/src/migrate/parse.mjs +26 -0
- package/src/migrate/scan.mjs +177 -0
- package/src/migrate/transform/expr-source.mjs +168 -0
- package/src/migrate/transform/island.mjs +67 -0
- package/src/migrate/transform/jsx-to-component.mjs +302 -0
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
- package/src/migrate/transform/page-split.mjs +435 -0
- package/src/migrate/write.mjs +81 -0
- package/src/migrate.mjs +171 -0
- package/src/server/auth/handoff.js +94 -11
- package/src/server/create-app.js +37 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/html-cache.js +178 -32
- package/src/server/image-optimizer.js +94 -26
- package/src/server/middleware/dev-gate.js +21 -8
- package/src/server/middleware/robots-txt.js +341 -0
- package/src/server/port-guard.js +255 -0
- package/src/server/prewarm.js +137 -51
- package/src/server/render.js +30 -10
- package/src/server/status-page.js +105 -4
- package/src/start.mjs +18 -3
- package/src/templates/layout.ejs +8 -28
- package/src/templates/layout.jsk +30 -0
- package/src/templates/layout.render.js +41 -0
- package/src/views/helpers/tags.js +86 -3
- package/types/build/resolve-peer.d.mts +13 -0
- package/types/client/dom.d.ts +55 -0
- package/types/client/form.d.ts +19 -0
- package/types/client/index.d.ts +20 -0
- package/types/client/registry.d.ts +53 -0
- package/types/client/safe-image.d.ts +19 -0
- package/types/client/shared-cookie.d.ts +82 -0
- package/types/client/store.d.ts +18 -0
- package/types/client/swap.d.ts +46 -0
- package/types/compile/codegen.d.ts +32 -0
- package/types/compile/compile-all.d.ts +42 -0
- package/types/compile/errors.d.ts +30 -0
- package/types/compile/expr.d.ts +67 -0
- package/types/compile/index.d.ts +10 -0
- package/types/compile/parse.d.ts +82 -0
- package/types/compile/resolve.d.ts +46 -0
- package/types/compile/scan-exports.d.ts +9 -0
- package/types/config/defaults.d.ts +477 -0
- package/types/config/index.d.ts +304 -0
- package/types/config/pattern.d.ts +38 -0
- package/types/http/control-flow.d.ts +45 -0
- package/types/http/cookies-entry.d.ts +5 -0
- package/types/http/cookies.d.ts +113 -0
- package/types/http/request-cache.d.ts +13 -0
- package/types/http/request-context.d.ts +67 -0
- package/types/http/shared-cookie.d.ts +73 -0
- package/types/index.d.ts +30 -0
- package/types/log.d.mts +153 -0
- package/types/server/admin/actions.d.ts +16 -0
- package/types/server/admin/auth.d.ts +52 -0
- package/types/server/admin/event-log.d.ts +38 -0
- package/types/server/admin/gate.d.ts +43 -0
- package/types/server/admin/inventory.d.ts +40 -0
- package/types/server/admin/mount.d.ts +6 -0
- package/types/server/admin/router.d.ts +6 -0
- package/types/server/admin/snapshot.d.ts +6 -0
- package/types/server/assets.d.ts +47 -0
- package/types/server/auth/handoff.d.ts +12 -0
- package/types/server/cache-deps.d.ts +16 -0
- package/types/server/cache-vary.d.ts +30 -0
- package/types/server/cloudflare.d.ts +163 -0
- package/types/server/create-app.d.ts +25 -0
- package/types/server/data-cache.d.ts +116 -0
- package/types/server/dev/devtools.d.ts +44 -0
- package/types/server/dev/report.d.ts +229 -0
- package/types/server/dev/socket.d.ts +17 -0
- package/types/server/dev/version-check.d.mts +15 -0
- package/types/server/ejs-adapter.d.ts +11 -0
- package/types/server/head-hints.d.ts +40 -0
- package/types/server/html-cache.d.ts +207 -0
- package/types/server/image-optimizer.d.ts +68 -0
- package/types/server/logs/access-middleware.d.ts +7 -0
- package/types/server/logs/file-sink.d.ts +17 -0
- package/types/server/logs/pipeline.d.ts +37 -0
- package/types/server/logs/s3-put.d.ts +85 -0
- package/types/server/logs/s3-sink.d.ts +26 -0
- package/types/server/metadata.d.ts +38 -0
- package/types/server/middleware/compression.d.ts +17 -0
- package/types/server/middleware/csrf.d.ts +4 -0
- package/types/server/middleware/dev-gate.d.ts +2 -0
- package/types/server/middleware/headers.d.ts +2 -0
- package/types/server/middleware/redirects.d.ts +2 -0
- package/types/server/middleware/robots-txt.d.ts +33 -0
- package/types/server/middleware/static-precompressed.d.ts +5 -0
- package/types/server/middleware/trailing-slash.d.ts +11 -0
- package/types/server/middleware/upstream-proxy.d.ts +21 -0
- package/types/server/og-image.d.ts +149 -0
- package/types/server/port-guard.d.ts +50 -0
- package/types/server/prewarm.d.ts +131 -0
- package/types/server/redis.d.ts +163 -0
- package/types/server/render.d.ts +101 -0
- package/types/server/router.d.ts +5 -0
- package/types/server/status-page.d.ts +24 -0
- package/types/server/upstream-limiter.d.ts +123 -0
- package/types/server/upstream-tracking.d.ts +42 -0
- package/types/shared/cookie-domain.d.ts +29 -0
- package/types/templates/layout.render.d.ts +7 -0
- package/types/version.d.mts +10 -0
- package/types/views/components/loader.d.ts +5 -0
- package/types/views/helpers/html.d.ts +39 -0
- package/types/views/helpers/tags.d.ts +127 -0
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
* @param {{ quiet?: boolean }} [options]
|
|
20
20
|
* @returns {Promise<{ count: number, manifest: Record<string, string>, outDir: string }>}
|
|
21
21
|
*/
|
|
22
|
-
export async function compileAll(config,
|
|
22
|
+
export async function compileAll(config, _options = {}) {
|
|
23
23
|
const outDir = path.join(config.dirs.generated, "templates");
|
|
24
24
|
const viewRoots = getViewRoots(config);
|
|
25
25
|
const jskFiles = discoverJskFiles(viewRoots);
|
|
@@ -32,7 +32,7 @@ export async function compileAll(config, options = {}) {
|
|
|
32
32
|
|
|
33
33
|
/** @type {Record<string, string>} */
|
|
34
34
|
const manifest = {};
|
|
35
|
-
/** @type {Map<string, { code: string, includes: string[] }>} */
|
|
35
|
+
/** @type {Map<string, { code: string, includes: string[], includeEntries: { id: string, index: number }[], source: string, relative: string }>} */
|
|
36
36
|
const compiled = new Map();
|
|
37
37
|
|
|
38
38
|
for (const [viewId, file] of jskFiles) {
|
|
@@ -47,34 +47,25 @@ export async function compileAll(config, options = {}) {
|
|
|
47
47
|
throw error;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const { code, includes,
|
|
50
|
+
const { code, includes, includeEntries } = codegen(ast, {
|
|
51
51
|
viewId,
|
|
52
52
|
file: relative,
|
|
53
|
-
knownComponents
|
|
53
|
+
knownComponents,
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
if (knownComponents.has(name)) continue;
|
|
58
|
-
const camel = name.charAt(0).toLowerCase() + name.slice(1);
|
|
59
|
-
if (knownComponents.has(camel)) continue;
|
|
60
|
-
if (!options.quiet) {
|
|
61
|
-
console.warn(`[jsk] ${relative}: component "${name}" was not found at compile time`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
compiled.set(viewId, { code, includes });
|
|
56
|
+
compiled.set(viewId, { code, includes, includeEntries, source, relative });
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
// Include hedefleri mevcut mu?
|
|
69
|
-
for (const [
|
|
70
|
-
for (const id of
|
|
60
|
+
for (const [, { includeEntries, source, relative }] of compiled) {
|
|
61
|
+
for (const { id, index } of includeEntries) {
|
|
71
62
|
const normalized = normalizeIncludeId(id);
|
|
72
63
|
if (!compiled.has(normalized) && !jskFiles.has(normalized)) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
throw new CompileError(`Unknown include "${id}"`, {
|
|
65
|
+
file: relative,
|
|
66
|
+
source,
|
|
67
|
+
index,
|
|
68
|
+
});
|
|
78
69
|
}
|
|
79
70
|
}
|
|
80
71
|
}
|
package/src/compile/expr.js
CHANGED
|
@@ -278,6 +278,11 @@ class ExprParser {
|
|
|
278
278
|
let node = this.parsePrimary();
|
|
279
279
|
for (;;) {
|
|
280
280
|
this.skipWs();
|
|
281
|
+
if (this.peek() === "(") {
|
|
282
|
+
throw this.error(
|
|
283
|
+
"Function calls are not allowed in .jsk; move logic to a JS component or controller",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
281
286
|
if (this.match("?.")) {
|
|
282
287
|
this.skipWs();
|
|
283
288
|
if (this.peek() === "[") {
|
package/src/compile/parse.js
CHANGED
|
@@ -64,6 +64,8 @@ class TemplateParser {
|
|
|
64
64
|
this.source = source;
|
|
65
65
|
this.file = file;
|
|
66
66
|
this.i = 0;
|
|
67
|
+
/** @type {string[]} Açık {#if}/{#each} kapanışları — EOF'ta öncelikli hata. */
|
|
68
|
+
this.blockStack = [];
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -121,6 +123,13 @@ class TemplateParser {
|
|
|
121
123
|
break;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// `<!DOCTYPE …>` / `<![CDATA[…]]>` — etiket değil; metin olarak bas.
|
|
127
|
+
// İlerletmezsek `<` + `!` dalı parseText'te de takılıp sonsuz döngü olur.
|
|
128
|
+
if (this.startsWith("<!")) {
|
|
129
|
+
nodes.push(this.parseDeclaration());
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
|
|
125
134
|
nodes.push(this.parseTag());
|
|
126
135
|
continue;
|
|
@@ -131,6 +140,35 @@ class TemplateParser {
|
|
|
131
140
|
return nodes;
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
/**
|
|
144
|
+
* `<!DOCTYPE html>` gibi bildirimler. Kapanış `>` ilk tırnak dışı geçiş.
|
|
145
|
+
* @returns {TextNode}
|
|
146
|
+
*/
|
|
147
|
+
parseDeclaration() {
|
|
148
|
+
const start = this.i;
|
|
149
|
+
this.i += 2; // <!
|
|
150
|
+
let quote = null;
|
|
151
|
+
while (!this.eof()) {
|
|
152
|
+
const ch = this.peek();
|
|
153
|
+
if (quote) {
|
|
154
|
+
if (ch === quote) quote = null;
|
|
155
|
+
this.i += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (ch === '"' || ch === "'") {
|
|
159
|
+
quote = ch;
|
|
160
|
+
this.i += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === ">") {
|
|
164
|
+
this.i += 1;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
this.i += 1;
|
|
168
|
+
}
|
|
169
|
+
return { type: "Text", value: this.source.slice(start, this.i) };
|
|
170
|
+
}
|
|
171
|
+
|
|
134
172
|
skipComment() {
|
|
135
173
|
const end = this.source.indexOf("-->", this.i + 4);
|
|
136
174
|
if (end === -1) throw this.error("Unterminated HTML comment");
|
|
@@ -217,13 +255,19 @@ class TemplateParser {
|
|
|
217
255
|
});
|
|
218
256
|
this.i = closeBrace + 1;
|
|
219
257
|
|
|
220
|
-
|
|
258
|
+
this.blockStack.push("{/if}");
|
|
259
|
+
/** @type {AstNode[]} */
|
|
260
|
+
let consequent;
|
|
221
261
|
/** @type {AstNode[]} */
|
|
222
262
|
let alternate = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
263
|
+
try {
|
|
264
|
+
consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
|
|
265
|
+
if (this.startsWith("{#else}")) {
|
|
266
|
+
this.i += "{#else}".length;
|
|
267
|
+
alternate = this.parseNodesUntil(["{/if}"]);
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
this.blockStack.pop();
|
|
227
271
|
}
|
|
228
272
|
|
|
229
273
|
if (!this.startsWith("{/if}")) {
|
|
@@ -263,6 +307,10 @@ class TemplateParser {
|
|
|
263
307
|
continue;
|
|
264
308
|
}
|
|
265
309
|
if (this.startsWith("</")) break;
|
|
310
|
+
if (this.startsWith("<!")) {
|
|
311
|
+
nodes.push(this.parseDeclaration());
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
266
314
|
if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
|
|
267
315
|
nodes.push(this.parseTag());
|
|
268
316
|
continue;
|
|
@@ -308,7 +356,14 @@ class TemplateParser {
|
|
|
308
356
|
templateIndex: this.i,
|
|
309
357
|
});
|
|
310
358
|
this.i = closeBrace + 1;
|
|
311
|
-
|
|
359
|
+
this.blockStack.push("{/each}");
|
|
360
|
+
/** @type {AstNode[]} */
|
|
361
|
+
let children;
|
|
362
|
+
try {
|
|
363
|
+
children = this.parseNodesUntil(["{/each}"]);
|
|
364
|
+
} finally {
|
|
365
|
+
this.blockStack.pop();
|
|
366
|
+
}
|
|
312
367
|
if (!this.startsWith("{/each}")) {
|
|
313
368
|
throw this.error("Expected {/each}", this.i);
|
|
314
369
|
}
|
|
@@ -369,9 +424,10 @@ class TemplateParser {
|
|
|
369
424
|
let children = [];
|
|
370
425
|
if (!selfClosing && !voidEl) {
|
|
371
426
|
children = this.parseNodesUntil([`</${name}>`]);
|
|
372
|
-
// Also allow parseNodes style — check closing tag
|
|
373
427
|
if (!this.startsWith(`</${name}>`)) {
|
|
374
|
-
|
|
428
|
+
if (this.eof() && this.blockStack.length) {
|
|
429
|
+
throw this.error(`Expected ${this.blockStack.at(-1)}`, this.i);
|
|
430
|
+
}
|
|
375
431
|
throw this.error(`Expected </${name}>`, this.i);
|
|
376
432
|
}
|
|
377
433
|
this.i += `</${name}>`.length;
|
package/src/compile/resolve.js
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -106,18 +106,34 @@ export const DEFAULT_PREWARM_ON_VISIT = {
|
|
|
106
106
|
/** Sayfa başına üstten alta en fazla kaç link kuyruğa alınır. */
|
|
107
107
|
perPage: 20,
|
|
108
108
|
/**
|
|
109
|
-
* Paralel işçi
|
|
110
|
-
* (
|
|
109
|
+
* Paralel işçi. `null` yalnızca kapalı modda durur; açıkken tavan
|
|
110
|
+
* (`ON_VISIT_CONCURRENCY_CEILING`) uygulanır. Klasik prewarm'daki
|
|
111
|
+
* prod 4 burada geçerli değil — onVisit sürekli çalışır.
|
|
111
112
|
* @type {number | null}
|
|
112
113
|
*/
|
|
113
114
|
concurrency: null,
|
|
114
115
|
/**
|
|
115
|
-
* Saniyedeki istek tavan
|
|
116
|
+
* Saniyedeki istek tavanı. `null` yalnızca kapalı modda durur; açıkken
|
|
117
|
+
* `0` (sınırsız) dahil her şey `ON_VISIT_RPS_CEILING` ile kesilir.
|
|
116
118
|
* @type {number | null}
|
|
117
119
|
*/
|
|
118
120
|
rps: null,
|
|
119
121
|
};
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* onVisit sürekli tur olduğu için klasik prewarm'daki "sınırsız" burada yok.
|
|
125
|
+
* Config daha yükseğini yazsa da çözümlenen değer bu tavanları geçemez.
|
|
126
|
+
*/
|
|
127
|
+
export const ON_VISIT_PER_PAGE_CEILING = 20;
|
|
128
|
+
export const ON_VISIT_RPS_CEILING = 2;
|
|
129
|
+
export const ON_VISIT_CONCURRENCY_CEILING = 2;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Bekleyen onVisit yolları. Bir sayfa düzinelerce link basınca kuyruk
|
|
133
|
+
* birikmesin; taşan link bu turda alınmaz.
|
|
134
|
+
*/
|
|
135
|
+
export const ON_VISIT_QUEUE_MAX = 64;
|
|
136
|
+
|
|
121
137
|
/** `onVisit` açıkken `cache().prewarm` kökünde yasak olan klasik alanlar. */
|
|
122
138
|
export const CLASSIC_PREWARM_KEYS = [
|
|
123
139
|
"enabled",
|
|
@@ -136,9 +152,26 @@ export const CLASSIC_PREWARM_KEYS = [
|
|
|
136
152
|
* HTML önbelleğinin girdi sınırı. 500 girdi ortalama bir sayfa boyutunda
|
|
137
153
|
* yaklaşık 100-200 MB tutar; uzun kuyruklu siteler bunu yükseltmek yerine
|
|
138
154
|
* veri önbelleğine yaslanmalı (bkz. `DEFAULT_DATA_CACHE`).
|
|
155
|
+
*
|
|
156
|
+
* Config daha yükseğini istese de girdi sayısı `HTML_CACHE_MAX_ENTRIES_CEILING`
|
|
157
|
+
* değerini geçemez. Asıl bellek freni bayt bütçesidir: şişman sayfa ve
|
|
158
|
+
* `vary.host` kopyası sayı tavanının altında da RSS'i şişirir.
|
|
139
159
|
*/
|
|
140
160
|
export const DEFAULT_HTML_CACHE_MAX_ENTRIES = 500;
|
|
141
161
|
|
|
162
|
+
/** `cache().maxEntries` için sert tavan. Üstü uyarıyla bu değere çekilir. */
|
|
163
|
+
export const HTML_CACHE_MAX_ENTRIES_CEILING = 800;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Süreç içi HTML string + sıkıştırılmış gövde tavanı (256 MB).
|
|
167
|
+
* `install()` bunu uygular; config yükseltemez. Tek sayfa bütçeden büyükse
|
|
168
|
+
* saklanmaz, yanıt yine gider.
|
|
169
|
+
*/
|
|
170
|
+
export const HTML_CACHE_BYTE_BUDGET = 256 * 1024 * 1024;
|
|
171
|
+
|
|
172
|
+
/** `cache().data.maxEntries` için sert tavan. Uzun kuyruk burada durur, HTML'de değil. */
|
|
173
|
+
export const DATA_CACHE_MAX_ENTRIES_CEILING = 20_000;
|
|
174
|
+
|
|
142
175
|
/**
|
|
143
176
|
* `notFound()` geçici bir upstream hatasına denk geldiğinde sayfanın kaç kez
|
|
144
177
|
* daha denenmesi gerektiği.
|
|
@@ -467,9 +500,19 @@ export const DEFAULT_BRAND = {
|
|
|
467
500
|
*/
|
|
468
501
|
export const DEFAULT_AUTH = {
|
|
469
502
|
/**
|
|
470
|
-
* `true` veya `{ ttlSeconds?, path?, maxValueBytes
|
|
503
|
+
* `true` veya `{ allowedCookieNames, ttlSeconds?, path?, maxValueBytes?,
|
|
504
|
+
* maxPendingTickets?, maxMintsPerIpPerMinute? }`.
|
|
471
505
|
* Açıkken `POST /_jskelet/auth/handoff` ve `?handoff=` redeem middleware'i.
|
|
472
|
-
*
|
|
506
|
+
* Mint için `allowedCookieNames` dolu olmalı (aksi halde 400).
|
|
507
|
+
* @type {boolean | {
|
|
508
|
+
* enabled?: boolean,
|
|
509
|
+
* allowedCookieNames?: string[],
|
|
510
|
+
* ttlSeconds?: number,
|
|
511
|
+
* path?: string,
|
|
512
|
+
* maxValueBytes?: number,
|
|
513
|
+
* maxPendingTickets?: number,
|
|
514
|
+
* maxMintsPerIpPerMinute?: number,
|
|
515
|
+
* }}
|
|
473
516
|
*/
|
|
474
517
|
crossSubdomainHandoff: false,
|
|
475
518
|
};
|
package/src/config/index.js
CHANGED
|
@@ -37,10 +37,15 @@ import {
|
|
|
37
37
|
DEFAULT_AUTH,
|
|
38
38
|
DEFAULT_BRAND,
|
|
39
39
|
DEFAULT_CLOUDFLARE,
|
|
40
|
+
DATA_CACHE_MAX_ENTRIES_CEILING,
|
|
40
41
|
DEFAULT_DATA_CACHE,
|
|
41
42
|
DEFAULT_DEV_GATE_BYPASS,
|
|
42
43
|
DEFAULT_DIRS,
|
|
43
44
|
DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
45
|
+
HTML_CACHE_MAX_ENTRIES_CEILING,
|
|
46
|
+
ON_VISIT_CONCURRENCY_CEILING,
|
|
47
|
+
ON_VISIT_PER_PAGE_CEILING,
|
|
48
|
+
ON_VISIT_RPS_CEILING,
|
|
44
49
|
DEFAULT_IMAGES,
|
|
45
50
|
DEFAULT_LOGS,
|
|
46
51
|
DEFAULT_NAVIGATION,
|
|
@@ -134,6 +139,8 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
134
139
|
* @property {string[] | null} routes Açık route modülü listesi.
|
|
135
140
|
* @property {boolean} trailingSlash URL'ler `/` ile bitsin mi (Next `trailingSlash`).
|
|
136
141
|
* @property {{ extensions: Set<string>, prefixes: string[] }} static
|
|
142
|
+
* @property {boolean} devGate `DEV_TOKEN` tek başına siteyi kilitlemez; gate
|
|
143
|
+
* ancak bu bayrak veya `DEV_GATE=1` ile açılır.
|
|
137
144
|
* @property {string[]} devGateBypass
|
|
138
145
|
* @property {string[]} preconnect
|
|
139
146
|
* @property {NavigationConfig} navigation
|
|
@@ -348,6 +355,22 @@ function normalizeUpstream(raw) {
|
|
|
348
355
|
};
|
|
349
356
|
}
|
|
350
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Dev gate. `DEV_TOKEN` ortamda durması siteyi kilitlemez: paylaşılan bir
|
|
360
|
+
* task tanımı production'a da aynı değişkeni taşır ve herkese 404 olur.
|
|
361
|
+
* Gate ancak `devGate: true` ya da `DEV_GATE=1` ile açılır. `DEV_GATE=0`
|
|
362
|
+
* config'teki açığı da kapatır.
|
|
363
|
+
*
|
|
364
|
+
* @param {Record<string, any>} source
|
|
365
|
+
* @returns {boolean}
|
|
366
|
+
*/
|
|
367
|
+
function normalizeDevGate(source) {
|
|
368
|
+
const env = process.env.DEV_GATE;
|
|
369
|
+
if (env === "0" || env === "false") return false;
|
|
370
|
+
if (env === "1" || env === "true") return true;
|
|
371
|
+
return source.devGate === true;
|
|
372
|
+
}
|
|
373
|
+
|
|
351
374
|
/**
|
|
352
375
|
* Yönetim paneli. `enabled` yalnızca açıkça `true` verildiğinde ya da
|
|
353
376
|
* `JSKELET_ADMIN` ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla
|
|
@@ -701,16 +724,24 @@ function normalizeCache(raw) {
|
|
|
701
724
|
const prewarm = normalizePrewarm(raw?.prewarm);
|
|
702
725
|
const queryRules = normalizeQueryRules(raw?.query);
|
|
703
726
|
const maxEntries = Number(raw?.maxEntries);
|
|
727
|
+
let htmlMaxEntries =
|
|
728
|
+
Number.isFinite(maxEntries) && maxEntries > 0
|
|
729
|
+
? Math.floor(maxEntries)
|
|
730
|
+
: DEFAULT_HTML_CACHE_MAX_ENTRIES;
|
|
731
|
+
if (htmlMaxEntries > HTML_CACHE_MAX_ENTRIES_CEILING) {
|
|
732
|
+
console.warn(
|
|
733
|
+
`[config] cache().maxEntries ${htmlMaxEntries} exceeds the ceiling of ` +
|
|
734
|
+
`${HTML_CACHE_MAX_ENTRIES_CEILING}; using ${HTML_CACHE_MAX_ENTRIES_CEILING}`,
|
|
735
|
+
);
|
|
736
|
+
htmlMaxEntries = HTML_CACHE_MAX_ENTRIES_CEILING;
|
|
737
|
+
}
|
|
704
738
|
|
|
705
739
|
return {
|
|
706
740
|
html,
|
|
707
741
|
cacheQuery: queryRules,
|
|
708
742
|
cacheVary: normalizeVary(raw?.vary),
|
|
709
|
-
htmlMaxEntries
|
|
710
|
-
|
|
711
|
-
? Math.floor(maxEntries)
|
|
712
|
-
: DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
713
|
-
data: { ...DEFAULT_DATA_CACHE, ...(raw?.data ?? {}) },
|
|
743
|
+
htmlMaxEntries,
|
|
744
|
+
data: normalizeDataCache(raw?.data),
|
|
714
745
|
// Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
|
|
715
746
|
// bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
|
|
716
747
|
trackUpstream: raw?.trackUpstream !== false,
|
|
@@ -732,14 +763,110 @@ function normalizeCache(raw) {
|
|
|
732
763
|
};
|
|
733
764
|
}
|
|
734
765
|
|
|
766
|
+
/**
|
|
767
|
+
* İstenen pozitif tamsayı tavanı aşıyorsa uyarı basıp tavana çeker.
|
|
768
|
+
*
|
|
769
|
+
* @param {string} field
|
|
770
|
+
* @param {number} requested
|
|
771
|
+
* @param {number} ceiling
|
|
772
|
+
* @returns {number}
|
|
773
|
+
*/
|
|
774
|
+
function clampCeiling(field, requested, ceiling) {
|
|
775
|
+
if (requested <= ceiling) return requested;
|
|
776
|
+
console.warn(
|
|
777
|
+
`[config] ${field} ${requested} exceeds the ceiling of ${ceiling}; using ${ceiling}`,
|
|
778
|
+
);
|
|
779
|
+
return ceiling;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Veri önbelleği JSON tutar; sınır HTML'den yüksek olabilir ama sonsuz değil.
|
|
784
|
+
*
|
|
785
|
+
* @param {unknown} raw
|
|
786
|
+
* @returns {{ maxEntries: number, staleFactor: number }}
|
|
787
|
+
*/
|
|
788
|
+
function normalizeDataCache(raw) {
|
|
789
|
+
const source =
|
|
790
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
791
|
+
? /** @type {Record<string, unknown>} */ (raw)
|
|
792
|
+
: {};
|
|
793
|
+
const requested = Number(source.maxEntries);
|
|
794
|
+
let maxEntries =
|
|
795
|
+
Number.isFinite(requested) && requested > 0
|
|
796
|
+
? Math.floor(requested)
|
|
797
|
+
: DEFAULT_DATA_CACHE.maxEntries;
|
|
798
|
+
maxEntries = clampCeiling(
|
|
799
|
+
"cache().data.maxEntries",
|
|
800
|
+
maxEntries,
|
|
801
|
+
DATA_CACHE_MAX_ENTRIES_CEILING,
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
const stale = Number(source.staleFactor);
|
|
805
|
+
return {
|
|
806
|
+
maxEntries,
|
|
807
|
+
staleFactor:
|
|
808
|
+
Number.isFinite(stale) && stale >= 0 ? stale : DEFAULT_DATA_CACHE.staleFactor,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* onVisit sürekli çalışır. Klasik turdaki `rps: 0` (sınırsız) burada her
|
|
814
|
+
* ziyaretçide yeniden crawl demek; boş, `0` ve tavanın üstü 2'ye çekilir.
|
|
815
|
+
*
|
|
816
|
+
* @param {Record<string, unknown>} source
|
|
817
|
+
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
818
|
+
*/
|
|
819
|
+
function resolveOnVisitLimits(source) {
|
|
820
|
+
const perPageRaw = Number(source.perPage);
|
|
821
|
+
const perPage = clampCeiling(
|
|
822
|
+
"cache().prewarm.onVisit.perPage",
|
|
823
|
+
Number.isFinite(perPageRaw) && perPageRaw > 0
|
|
824
|
+
? Math.floor(perPageRaw)
|
|
825
|
+
: DEFAULT_PREWARM_ON_VISIT.perPage,
|
|
826
|
+
ON_VISIT_PER_PAGE_CEILING,
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const concurrencyRaw = Number(source.concurrency);
|
|
830
|
+
const concurrency = clampCeiling(
|
|
831
|
+
"cache().prewarm.onVisit.concurrency",
|
|
832
|
+
Number.isFinite(concurrencyRaw) && concurrencyRaw > 0
|
|
833
|
+
? Math.floor(concurrencyRaw)
|
|
834
|
+
: ON_VISIT_CONCURRENCY_CEILING,
|
|
835
|
+
ON_VISIT_CONCURRENCY_CEILING,
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
const rpsRaw = Number(source.rps);
|
|
839
|
+
let rps = ON_VISIT_RPS_CEILING;
|
|
840
|
+
if (source.rps != null && source.rps !== "") {
|
|
841
|
+
if (!Number.isFinite(rpsRaw) || rpsRaw <= 0) {
|
|
842
|
+
console.warn(
|
|
843
|
+
`[config] cache().prewarm.onVisit.rps ${source.rps} is not a positive rate; ` +
|
|
844
|
+
`using ${ON_VISIT_RPS_CEILING}`,
|
|
845
|
+
);
|
|
846
|
+
} else {
|
|
847
|
+
rps = clampCeiling(
|
|
848
|
+
"cache().prewarm.onVisit.rps",
|
|
849
|
+
rpsRaw,
|
|
850
|
+
ON_VISIT_RPS_CEILING,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
...DEFAULT_PREWARM_ON_VISIT,
|
|
857
|
+
enabled: source.enabled !== false,
|
|
858
|
+
perPage,
|
|
859
|
+
concurrency,
|
|
860
|
+
rps,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
735
864
|
/**
|
|
736
865
|
* @param {unknown} raw
|
|
737
866
|
* @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
|
|
738
867
|
*/
|
|
739
868
|
function normalizeOnVisit(raw) {
|
|
740
|
-
if (raw === true) {
|
|
741
|
-
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: true };
|
|
742
|
-
}
|
|
869
|
+
if (raw === true) return resolveOnVisitLimits({});
|
|
743
870
|
|
|
744
871
|
if (raw == null || raw === false) {
|
|
745
872
|
return { ...DEFAULT_PREWARM_ON_VISIT, enabled: false };
|
|
@@ -751,24 +878,7 @@ function normalizeOnVisit(raw) {
|
|
|
751
878
|
);
|
|
752
879
|
}
|
|
753
880
|
|
|
754
|
-
|
|
755
|
-
const perPage = Number(source.perPage);
|
|
756
|
-
const concurrency = Number(source.concurrency);
|
|
757
|
-
const rps = Number(source.rps);
|
|
758
|
-
|
|
759
|
-
return {
|
|
760
|
-
...DEFAULT_PREWARM_ON_VISIT,
|
|
761
|
-
enabled: source.enabled !== false,
|
|
762
|
-
perPage:
|
|
763
|
-
Number.isFinite(perPage) && perPage > 0
|
|
764
|
-
? Math.floor(perPage)
|
|
765
|
-
: DEFAULT_PREWARM_ON_VISIT.perPage,
|
|
766
|
-
concurrency:
|
|
767
|
-
Number.isFinite(concurrency) && concurrency > 0
|
|
768
|
-
? Math.floor(concurrency)
|
|
769
|
-
: null,
|
|
770
|
-
rps: Number.isFinite(rps) && rps >= 0 ? rps : null,
|
|
771
|
-
};
|
|
881
|
+
return resolveOnVisitLimits(/** @type {Record<string, unknown>} */ (raw));
|
|
772
882
|
}
|
|
773
883
|
|
|
774
884
|
/**
|
|
@@ -1138,7 +1248,7 @@ function resolveLayout(dirs, override) {
|
|
|
1138
1248
|
const appLayout = path.join(dirs.views, "layout.ejs");
|
|
1139
1249
|
if (fs.existsSync(appLayout)) return appLayout;
|
|
1140
1250
|
|
|
1141
|
-
return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.
|
|
1251
|
+
return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
|
|
1142
1252
|
}
|
|
1143
1253
|
|
|
1144
1254
|
/**
|
|
@@ -1255,6 +1365,7 @@ export async function loadConfig(options = {}) {
|
|
|
1255
1365
|
extensions: new Set(source.static?.extensions ?? DEFAULT_STATIC.extensions),
|
|
1256
1366
|
prefixes: source.static?.prefixes ?? DEFAULT_STATIC.prefixes,
|
|
1257
1367
|
},
|
|
1368
|
+
devGate: normalizeDevGate(source),
|
|
1258
1369
|
devGateBypass: source.devGateBypass ?? DEFAULT_DEV_GATE_BYPASS,
|
|
1259
1370
|
preconnect: source.preconnect ?? [],
|
|
1260
1371
|
navigation: normalizeNavigation(source.navigation, brand),
|
package/src/dev-server.mjs
CHANGED
|
@@ -138,6 +138,18 @@ function escapeRegExp(value) {
|
|
|
138
138
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Bekleyen hata kutusunu hemen bas. Alt süreç exit/close olduğunda 60 ms
|
|
143
|
+
* zamanlayıcıya güvenilmez: parent `shutdown` ile ölürse satır hiç görünmez.
|
|
144
|
+
*/
|
|
145
|
+
function flushErrorNow() {
|
|
146
|
+
if (errorTimer) {
|
|
147
|
+
clearTimeout(errorTimer);
|
|
148
|
+
errorTimer = null;
|
|
149
|
+
}
|
|
150
|
+
flushError();
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
/**
|
|
142
154
|
* @param {string} line
|
|
143
155
|
* @param {boolean} isError
|
|
@@ -174,8 +186,9 @@ function onServerLine(line, isError) {
|
|
|
174
186
|
|
|
175
187
|
if (!line.trim()) return;
|
|
176
188
|
|
|
177
|
-
//
|
|
178
|
-
|
|
189
|
+
// Yığın / Error: kutuya. Düz stderr (port dolu vb.) anında — süreç hemen
|
|
190
|
+
// exit 1 ile öldüğü için gecikmeli flush'a kalırsa satır kayboluyor.
|
|
191
|
+
if (/^\s+at\s/.test(line) || /Error\b/.test(line)) {
|
|
179
192
|
errorBuffer.push(
|
|
180
193
|
line.replace(/^\[(?:uncaughtException|unhandledRejection)\]\s*/, ""),
|
|
181
194
|
);
|
|
@@ -183,6 +196,11 @@ function onServerLine(line, isError) {
|
|
|
183
196
|
return;
|
|
184
197
|
}
|
|
185
198
|
|
|
199
|
+
if (isError) {
|
|
200
|
+
log.error(line.trim());
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
186
204
|
log.event({ scope: "server", message: line });
|
|
187
205
|
}
|
|
188
206
|
|
|
@@ -205,9 +223,12 @@ function run(args, label, passthrough) {
|
|
|
205
223
|
passthrough ? onBuildLine(line) : onServerLine(line, true),
|
|
206
224
|
);
|
|
207
225
|
|
|
208
|
-
|
|
226
|
+
// `close`: stdio boşaldıktan sonra. `exit` ile yarışınca port hatası
|
|
227
|
+
// stderr'de kalıp "server exited" dışında bir şey görünmüyordu.
|
|
228
|
+
child.on("close", (code) => {
|
|
209
229
|
if (code === 0 || code === null) return;
|
|
210
230
|
if (child[STOPPING]) return; // yeniden başlatma: beklenen çıkış
|
|
231
|
+
if (!passthrough) flushErrorNow();
|
|
211
232
|
log.error(`${label} exited (code ${code})`);
|
|
212
233
|
shutdown(code);
|
|
213
234
|
});
|
|
@@ -249,6 +270,7 @@ let server = null;
|
|
|
249
270
|
|
|
250
271
|
// `--import` modül belirteci bekler: Windows'ta `H:\…` yolu `h:` şemalı URL
|
|
251
272
|
// sanılıp reddediliyor, bu yüzden file:// URL'e çevrilir.
|
|
273
|
+
// `--murder` sunucu sürecine iletilir: dolu portta dinleyen öldürülüp başlanır.
|
|
252
274
|
const SERVER_ARGS = [
|
|
253
275
|
// `.env` yoksa bayrak hiç geçilmez: `--env-file-if-exists` dosya yokken bir
|
|
254
276
|
// bildirim satırı basıyor ve bu satır hata kutusuna dönüşüyordu.
|
|
@@ -256,6 +278,7 @@ const SERVER_ARGS = [
|
|
|
256
278
|
"--import",
|
|
257
279
|
pathToFileURL(path.join(SRC, "runtime", "register.mjs")).href,
|
|
258
280
|
path.join(SRC, "start.mjs"),
|
|
281
|
+
...(process.argv.includes("--murder") ? ["--murder"] : []),
|
|
259
282
|
];
|
|
260
283
|
|
|
261
284
|
function startServerProcess() {
|
package/src/http/cookies.js
CHANGED
|
@@ -148,6 +148,17 @@ export function parseCookies(req) {
|
|
|
148
148
|
* @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
|
|
149
149
|
*/
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* RFC 6265 cookie-name (token). `;`, boşluk, CRLF gibi karakterler
|
|
153
|
+
* Set-Cookie enjeksiyonuna yol açardı — reddedilir.
|
|
154
|
+
*
|
|
155
|
+
* @param {unknown} name
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function isValidCookieName(name) {
|
|
159
|
+
return typeof name === "string" && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
/**
|
|
152
163
|
* Varsayılanlar bilinçli olarak kısıtlayıcı: `HttpOnly` ile JS okuyamaz,
|
|
153
164
|
* `SameSite=Lax` ile çapraz site POST'larında gönderilmez (CSRF'nin büyük
|
|
@@ -159,6 +170,13 @@ export function parseCookies(req) {
|
|
|
159
170
|
* @returns {string}
|
|
160
171
|
*/
|
|
161
172
|
export function serializeCookie(name, value, options = {}) {
|
|
173
|
+
if (!isValidCookieName(name)) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`[cookies] invalid cookie name ${JSON.stringify(name)}; ` +
|
|
176
|
+
"names must be RFC 6265 tokens (no spaces, semicolons, or control chars)",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
163
181
|
|
|
164
182
|
parts.push(`Path=${options.path ?? "/"}`);
|
package/src/logo.png
CHANGED
|
Binary file
|