jskelet 0.7.1 → 0.7.3
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/CHANGELOG.md +41 -0
- package/docs/02-mimari.md +11 -3
- package/docs/04-render-ve-sablonlar.md +39 -9
- package/docs/05-islands.md +8 -4
- package/docs/06-cache.md +18 -16
- package/docs/07-yapilandirma.md +8 -7
- package/docs/08-build.md +5 -2
- package/docs/en/02-architecture.md +10 -3
- package/docs/en/04-rendering.md +44 -10
- package/docs/en/05-islands.md +8 -4
- package/docs/en/06-caching.md +18 -16
- package/docs/en/07-configuration.md +8 -6
- package/docs/en/08-build.md +7 -4
- package/package.json +1 -1
- package/src/build/build.mjs +23 -8
- package/src/build/tasks/css.mjs +2 -1
- package/src/build/tasks/precompress.mjs +51 -11
- package/src/client/registry.js +13 -3
- package/src/compile/compile-all.js +14 -8
- package/src/config/defaults.js +9 -5
- package/src/config/index.js +3 -3
- package/src/http/request-cache.js +19 -1
- package/src/index.js +2 -0
- package/src/server/cache-blob.js +33 -0
- package/src/server/create-app.js +5 -0
- package/src/server/data-cache.js +77 -11
- package/src/server/disk-cache.js +2 -2
- package/src/server/head-hints.js +21 -9
- package/src/server/html-cache.js +169 -39
- package/src/server/image-optimizer.js +72 -28
- package/src/server/redis.js +2 -2
- package/src/server/render.js +165 -53
- package/src/views/helpers/tags.js +38 -9
- package/types/client/registry.d.ts +2 -0
- package/types/config/defaults.d.ts +8 -4
- package/types/config/index.d.ts +1 -0
- package/types/index.d.ts +1 -1
- package/types/server/cache-blob.d.ts +8 -0
- package/types/server/data-cache.d.ts +1 -0
- package/types/server/html-cache.d.ts +4 -5
- package/types/server/render.d.ts +19 -0
- package/types/views/helpers/tags.d.ts +7 -1
package/src/server/render.js
CHANGED
|
@@ -58,16 +58,22 @@ const isDev = process.env.NODE_ENV === "development";
|
|
|
58
58
|
* taraması dosya sistemine dokunduğu için bir kez yapılır; config yüklenmeden
|
|
59
59
|
* hesaplanamaz, bu yüzden ilk render'da kurulur.
|
|
60
60
|
*
|
|
61
|
-
*
|
|
61
|
+
* Sayfa `.jsk` şablonları lazy yüklenir; layout + `components/*.jsk` eager.
|
|
62
|
+
*
|
|
63
|
+
* @typedef {{ helpers: Record<string, unknown>, options: Record<string, unknown>,
|
|
62
64
|
* viewsDir: string, viewRoots: string[], layout: string,
|
|
63
65
|
* compiled: Map<string, (data: object, helpers: object) => string>,
|
|
66
|
+
* loading: Map<string, Promise<(data: object, helpers: object) => string | null>>,
|
|
67
|
+
* manifest: Record<string, string>, templatesDir: string, bust: string,
|
|
64
68
|
* layoutRender: ((data: object, helpers: object) => string) | null
|
|
65
|
-
* }
|
|
69
|
+
* }} RenderEngine
|
|
66
70
|
*/
|
|
71
|
+
|
|
72
|
+
/** @type {RenderEngine | null} */
|
|
67
73
|
let engine = null;
|
|
68
74
|
|
|
69
75
|
/**
|
|
70
|
-
* @returns {Promise<
|
|
76
|
+
* @returns {Promise<RenderEngine>}
|
|
71
77
|
*/
|
|
72
78
|
async function getEngine() {
|
|
73
79
|
if (engine) return engine;
|
|
@@ -104,14 +110,11 @@ async function getEngine() {
|
|
|
104
110
|
if (!(pascal in helpers)) helpers[pascal] = value;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
const
|
|
108
|
-
config,
|
|
109
|
-
helpers,
|
|
110
|
-
);
|
|
113
|
+
const loaded = await loadCompiledTemplates(config, helpers);
|
|
111
114
|
|
|
112
115
|
// Yalnızca framework varsayılan layout'una düşüldüğünde hazır modülü kullan;
|
|
113
116
|
// uygulamanın kendi layout.jsk'si derlenmemişse sessizce framework'e kayma.
|
|
114
|
-
let layoutRender =
|
|
117
|
+
let layoutRender = loaded.layoutRender;
|
|
115
118
|
if (!layoutRender) {
|
|
116
119
|
const fwLayout = path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
|
|
117
120
|
if (path.resolve(config.layout) === path.resolve(fwLayout)) {
|
|
@@ -125,7 +128,11 @@ async function getEngine() {
|
|
|
125
128
|
viewRoots,
|
|
126
129
|
layout: config.layout,
|
|
127
130
|
helpers,
|
|
128
|
-
compiled,
|
|
131
|
+
compiled: loaded.compiled,
|
|
132
|
+
loading: new Map(),
|
|
133
|
+
manifest: loaded.manifest,
|
|
134
|
+
templatesDir: loaded.templatesDir,
|
|
135
|
+
bust: loaded.bust,
|
|
129
136
|
layoutRender,
|
|
130
137
|
options: {
|
|
131
138
|
// `include('partials/header')` gibi çağrılar views kökünden çözülür.
|
|
@@ -141,11 +148,48 @@ async function getEngine() {
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
/**
|
|
144
|
-
*
|
|
151
|
+
* Manifest'ten tek bir derlenmiş view yükler. Include'lar modül grafiğinde
|
|
152
|
+
* statik import ile gelir.
|
|
153
|
+
*
|
|
154
|
+
* @param {RenderEngine} eng
|
|
155
|
+
* @param {string} viewId
|
|
156
|
+
* @returns {Promise<((data: object, helpers: object) => string) | null>}
|
|
157
|
+
*/
|
|
158
|
+
async function ensureCompiledView(eng, viewId) {
|
|
159
|
+
const hit = eng.compiled.get(viewId);
|
|
160
|
+
if (hit) return hit;
|
|
161
|
+
|
|
162
|
+
const rel = eng.manifest[viewId];
|
|
163
|
+
if (!rel) return null;
|
|
164
|
+
|
|
165
|
+
const pending = eng.loading.get(viewId);
|
|
166
|
+
if (pending) return pending;
|
|
167
|
+
|
|
168
|
+
const task = (async () => {
|
|
169
|
+
const file = path.join(eng.templatesDir, rel);
|
|
170
|
+
try {
|
|
171
|
+
const mod = await import(pathToFileURL(file).href + eng.bust);
|
|
172
|
+
if (typeof mod.render !== "function") return null;
|
|
173
|
+
eng.compiled.set(viewId, mod.render);
|
|
174
|
+
return mod.render;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
} finally {
|
|
178
|
+
eng.loading.delete(viewId);
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
|
|
182
|
+
eng.loading.set(viewId, task);
|
|
183
|
+
return task;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Layout + `components/*.jsk` eager; sayfa şablonları ilk `renderView`'da.
|
|
145
188
|
*
|
|
146
189
|
* @param {import('../config/index.js').ResolvedConfig} config
|
|
147
190
|
* @param {Record<string, unknown>} helpers
|
|
148
|
-
* @returns {Promise<{ compiled: Map<string, Function>, layoutRender: Function | null
|
|
191
|
+
* @returns {Promise<{ compiled: Map<string, Function>, layoutRender: Function | null,
|
|
192
|
+
* manifest: Record<string, string>, templatesDir: string, bust: string }>}
|
|
149
193
|
*/
|
|
150
194
|
async function loadCompiledTemplates(config, helpers) {
|
|
151
195
|
/** @type {Map<string, (data: object, helpers: object) => string>} */
|
|
@@ -154,16 +198,26 @@ async function loadCompiledTemplates(config, helpers) {
|
|
|
154
198
|
const manifestPath = path.join(templatesDir, "manifest.json");
|
|
155
199
|
|
|
156
200
|
if (!fs.existsSync(manifestPath)) {
|
|
157
|
-
return {
|
|
201
|
+
return {
|
|
202
|
+
compiled,
|
|
203
|
+
layoutRender: null,
|
|
204
|
+
manifest: {},
|
|
205
|
+
templatesDir,
|
|
206
|
+
bust: "",
|
|
207
|
+
};
|
|
158
208
|
}
|
|
159
209
|
|
|
160
210
|
/** @type {Record<string, string>} */
|
|
161
211
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
162
212
|
const bust = isDev ? `?t=${Date.now()}` : "";
|
|
163
213
|
|
|
164
|
-
|
|
214
|
+
const eagerIds = Object.keys(manifest).filter(
|
|
215
|
+
(id) => id === "layout" || id.startsWith("components/"),
|
|
216
|
+
);
|
|
217
|
+
|
|
165
218
|
const entries = await Promise.all(
|
|
166
|
-
|
|
219
|
+
eagerIds.map(async (viewId) => {
|
|
220
|
+
const rel = manifest[viewId];
|
|
167
221
|
const file = path.join(templatesDir, rel);
|
|
168
222
|
try {
|
|
169
223
|
const mod = await import(pathToFileURL(file).href + bust);
|
|
@@ -178,7 +232,6 @@ async function loadCompiledTemplates(config, helpers) {
|
|
|
178
232
|
if (!mod || typeof mod.render !== "function") continue;
|
|
179
233
|
compiled.set(viewId, mod.render);
|
|
180
234
|
|
|
181
|
-
// `components/*.jsk` → helpers.PascalName
|
|
182
235
|
if (viewId.startsWith("components/")) {
|
|
183
236
|
const base = viewId.slice("components/".length).split("/").pop() ?? "";
|
|
184
237
|
const pascal = base
|
|
@@ -193,7 +246,24 @@ async function loadCompiledTemplates(config, helpers) {
|
|
|
193
246
|
}
|
|
194
247
|
|
|
195
248
|
const layoutRender = compiled.get("layout") ?? null;
|
|
196
|
-
return { compiled, layoutRender };
|
|
249
|
+
return { compiled, layoutRender, manifest, templatesDir, bust };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Listen öncesi: motor + asset manifest ısınır (sayfa prewarm değil).
|
|
254
|
+
*
|
|
255
|
+
* @returns {Promise<void>}
|
|
256
|
+
*/
|
|
257
|
+
export async function warmRenderEngine() {
|
|
258
|
+
await getEngine();
|
|
259
|
+
hasAsset("app.css");
|
|
260
|
+
hasAsset("main.js");
|
|
261
|
+
try {
|
|
262
|
+
const { getBuildId } = await import("./assets.js");
|
|
263
|
+
getBuildId();
|
|
264
|
+
} catch {
|
|
265
|
+
/* yoksa sessiz */
|
|
266
|
+
}
|
|
197
267
|
}
|
|
198
268
|
|
|
199
269
|
/**
|
|
@@ -232,8 +302,13 @@ export function resetRenderEngine() {
|
|
|
232
302
|
* @returns {Promise<string>}
|
|
233
303
|
*/
|
|
234
304
|
export async function renderView(view, data = {}) {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
305
|
+
const eng = await getEngine();
|
|
306
|
+
const { helpers, options, viewRoots, viewsDir } = eng;
|
|
307
|
+
|
|
308
|
+
let renderFn = eng.compiled.get(view) ?? null;
|
|
309
|
+
if (!renderFn && eng.manifest[view]) {
|
|
310
|
+
renderFn = await ensureCompiledView(eng, view);
|
|
311
|
+
}
|
|
237
312
|
if (renderFn) {
|
|
238
313
|
return renderFn({ ...data }, helpers);
|
|
239
314
|
}
|
|
@@ -256,17 +331,31 @@ export async function renderPage(page) {
|
|
|
256
331
|
const { helpers, options, layout, layoutRender } = await getEngine();
|
|
257
332
|
const config = getConfig();
|
|
258
333
|
|
|
259
|
-
|
|
334
|
+
// Gövde `page.metadata` ile başlar (hook beklemez). Head/layout birleşik
|
|
335
|
+
// metadata'yı alır — siteName/titleTemplate gibi hook alanları layout'ta.
|
|
336
|
+
const pageMeta = page.metadata ?? {};
|
|
337
|
+
const metadataPromise = (async () => ({
|
|
260
338
|
...(await hook("metadata", {}, page)),
|
|
261
|
-
...
|
|
262
|
-
};
|
|
339
|
+
...pageMeta,
|
|
340
|
+
}))();
|
|
263
341
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
342
|
+
const bodyPromise = renderView(page.view, {
|
|
343
|
+
...(page.data ?? {}),
|
|
344
|
+
metadata: pageMeta,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// metadata → layoutContext zinciri gövdeyle paralel.
|
|
348
|
+
const layoutPromise = metadataPromise.then(async (metadata) => {
|
|
349
|
+
const context = await hook("layoutContext", {}, {
|
|
350
|
+
pathname: page.pathname ?? "",
|
|
351
|
+
metadata,
|
|
352
|
+
});
|
|
353
|
+
return { metadata, context };
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const [body, { metadata, context }] = await Promise.all([
|
|
357
|
+
bodyPromise,
|
|
358
|
+
layoutPromise,
|
|
270
359
|
]);
|
|
271
360
|
|
|
272
361
|
// Layout local'leri. `.jsk` `(data, helpers)` aldığı için helpers'ı data'ya
|
|
@@ -694,31 +783,28 @@ async function sendHtml(req, res, body, encoded, options = {}) {
|
|
|
694
783
|
const encoding =
|
|
695
784
|
req.method === "HEAD" ? null : negotiateEncoding(req.headers["accept-encoding"]);
|
|
696
785
|
|
|
697
|
-
if (!encoding
|
|
786
|
+
if (!encoding) {
|
|
698
787
|
if (options.etag === false) {
|
|
699
|
-
// Sıkıştırma middleware'i devreye girerse Content-Length'i kendisi
|
|
700
|
-
// kaldırır; girmezse doğru uzunlukla gider.
|
|
701
788
|
res.setHeader("Content-Length", String(Buffer.byteLength(body)));
|
|
702
789
|
res.end(req.method === "HEAD" ? undefined : body);
|
|
703
790
|
return;
|
|
704
791
|
}
|
|
705
|
-
|
|
706
792
|
res.send(body);
|
|
707
793
|
return;
|
|
708
794
|
}
|
|
709
795
|
|
|
710
|
-
|
|
796
|
+
// Public cache haritası veya private/fragment: HTML'i burada sıkıştır —
|
|
797
|
+
// middleware'in ikinci kez buffer'lamasını ve private yolda q=5 maliyetini kes.
|
|
798
|
+
let buffer = encoded?.get(encoding);
|
|
711
799
|
if (!buffer) {
|
|
712
800
|
buffer = await encodeText(body, encoding);
|
|
713
|
-
|
|
714
|
-
// gelince eski buffer düşer, bayt bütçesi iki gövdeyi birden saymaz.
|
|
715
|
-
rememberHtmlEncoding(encoded, encoding, buffer);
|
|
801
|
+
if (encoded) rememberHtmlEncoding(encoded, encoding, buffer);
|
|
716
802
|
}
|
|
717
803
|
|
|
718
804
|
res.setHeader("Content-Encoding", encoding);
|
|
719
805
|
res.setHeader("Vary", "Accept-Encoding");
|
|
720
806
|
res.setHeader("Content-Length", String(buffer.length));
|
|
721
|
-
res.end(buffer);
|
|
807
|
+
res.end(req.method === "HEAD" ? undefined : buffer);
|
|
722
808
|
}
|
|
723
809
|
|
|
724
810
|
/**
|
|
@@ -770,12 +856,47 @@ async function produce(controller, ctx) {
|
|
|
770
856
|
if (!first.transient.length) return { html: await renderNotFound(), status: 404 };
|
|
771
857
|
|
|
772
858
|
// Buraya gelindiyse `notFound()` veri gelmediği için çağrılmış. **Var olan
|
|
773
|
-
// bir sayfayı** 404 olarak servis etmek en kötü sonu
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
const { attempts, delayMs } = transientRetry();
|
|
859
|
+
// bir sayfayı** 404 olarak servis etmek en kötü sonuç. Varsayılan: hemen
|
|
860
|
+
// 503 dön, retry'yi arka planda çalıştır (data cache ısınır). `background:
|
|
861
|
+
// false` eski senkron retry davranışını geri getirir.
|
|
862
|
+
const { attempts, delayMs, background } = transientRetry();
|
|
777
863
|
let failures = first.transient;
|
|
778
864
|
|
|
865
|
+
if (background && attempts > 0) {
|
|
866
|
+
const retryDetail =
|
|
867
|
+
`returned notFound() while upstream is failing ` +
|
|
868
|
+
`(${summarizeFailures(failures)}), retrying in background (${attempts}×)`;
|
|
869
|
+
if (!suppressForPrewarm(retryDetail)) {
|
|
870
|
+
console.warn(`[render] ${ctx.pathname} ${retryDetail}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
void (async () => {
|
|
874
|
+
for (let round = 1; round <= attempts; round += 1) {
|
|
875
|
+
await sleep(delayMs * round);
|
|
876
|
+
try {
|
|
877
|
+
const retried = await withUpstreamTracking(() =>
|
|
878
|
+
withRequestCache(() => attempt(controller, ctx)),
|
|
879
|
+
);
|
|
880
|
+
if ("page" in retried || !retried.transient.length) return;
|
|
881
|
+
failures = retried.transient;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
console.warn(
|
|
884
|
+
`[render] ${ctx.pathname} background retry failed`,
|
|
885
|
+
error instanceof Error ? error.message : error,
|
|
886
|
+
);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
})();
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
html: await renderStatusPage(503),
|
|
894
|
+
status: 503,
|
|
895
|
+
degraded: true,
|
|
896
|
+
retryAfter: RETRY_AFTER_SECONDS,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
779
900
|
for (let round = 1; round <= attempts; round += 1) {
|
|
780
901
|
const retryDetail =
|
|
781
902
|
`returned notFound() while upstream is failing ` +
|
|
@@ -784,29 +905,20 @@ async function produce(controller, ctx) {
|
|
|
784
905
|
console.warn(`[render] ${ctx.pathname} ${retryDetail}`);
|
|
785
906
|
}
|
|
786
907
|
|
|
787
|
-
// Beklemeden tekrar denemek rate limit'e girmiş bir API'de aynı 429'u
|
|
788
|
-
// getirir; kısa bekleme hem pencerenin dönmesine şans verir hem de
|
|
789
|
-
// fırtınayı büyütmez.
|
|
790
908
|
await sleep(delayMs * round);
|
|
791
909
|
|
|
792
|
-
// Her deneme kendi upstream ve istek içi cache bağlamında çalışır: ilk
|
|
793
|
-
// turun hataları ikinci turun kararını kirletmesin ve memoize edilmiş
|
|
794
|
-
// boş cevaplar tekrar kullanılmasın.
|
|
795
910
|
const retried = await withUpstreamTracking(() =>
|
|
796
911
|
withRequestCache(() => attempt(controller, ctx)),
|
|
797
912
|
);
|
|
798
913
|
|
|
799
914
|
if ("page" in retried) return retried.page;
|
|
800
915
|
if (!retried.transient.length) {
|
|
801
|
-
// Bu kez upstream sağlam cevap verdi ve "yok" dedi: gerçek 404.
|
|
802
916
|
return { html: await renderNotFound(), status: 404 };
|
|
803
917
|
}
|
|
804
918
|
|
|
805
919
|
failures = retried.transient;
|
|
806
920
|
}
|
|
807
921
|
|
|
808
|
-
// Denemeler tükendi. 404 yerine 503: önbelleğe girmez, `Retry-After` taşır
|
|
809
|
-
// ve bir sonraki istek gerçek içeriği üretebilir.
|
|
810
922
|
const outageDetail =
|
|
811
923
|
`could not be produced, upstream is still failing ` +
|
|
812
924
|
`(${summarizeFailures(failures)}), serving an uncached 503 instead of a 404`;
|
|
@@ -830,11 +942,10 @@ async function produce(controller, ctx) {
|
|
|
830
942
|
const RETRY_AFTER_SECONDS = 30;
|
|
831
943
|
|
|
832
944
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
* iki katına çıkabilir, ama alternatifi var olan sayfaları 404'e düşürmek.
|
|
945
|
+
* Varsayılan: arka plan retry + hemen 503 (TTFB'yi sleep ile şişirmez).
|
|
946
|
+
* `background: false` → eski senkron döngü.
|
|
836
947
|
*
|
|
837
|
-
* @returns {{ attempts: number, delayMs: number }}
|
|
948
|
+
* @returns {{ attempts: number, delayMs: number, background: boolean }}
|
|
838
949
|
*/
|
|
839
950
|
function transientRetry() {
|
|
840
951
|
const raw = /** @type {any} */ (getConfig().transientRetry ?? {});
|
|
@@ -844,6 +955,7 @@ function transientRetry() {
|
|
|
844
955
|
return {
|
|
845
956
|
attempts: Number.isFinite(attempts) && attempts >= 0 ? Math.floor(attempts) : 1,
|
|
846
957
|
delayMs: Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 300,
|
|
958
|
+
background: raw.background !== false,
|
|
847
959
|
};
|
|
848
960
|
}
|
|
849
961
|
|
|
@@ -324,28 +324,57 @@ export function toKebab(name) {
|
|
|
324
324
|
* build yokken 404 link'i istememek için. `.jsk` ifade dilinde `asset()` /
|
|
325
325
|
* `hasAsset()` çağrılamadığı için layout bu etiketi kullanır.
|
|
326
326
|
*
|
|
327
|
-
*
|
|
327
|
+
* `blocking` varsayılan `true`: `<link rel="stylesheet">` boyamayı bekletir.
|
|
328
|
+
* Ölçümde bu, sheet geç gelince oluşan CLS'ten daha ucuz. `false` yalnızca
|
|
329
|
+
* açık boolean'dır (`:blocking="false"`); atlamak veya `true` aynı çıktıyı
|
|
330
|
+
* verir.
|
|
331
|
+
*
|
|
332
|
+
* @param {{ styles?: string[], blocking?: boolean }} [props]
|
|
328
333
|
* @returns {string}
|
|
329
334
|
*/
|
|
330
335
|
export function stylesheets(props = {}) {
|
|
331
336
|
const styles = Array.isArray(props.styles) ? props.styles : [];
|
|
337
|
+
const blocking = props.blocking !== false;
|
|
332
338
|
/** @type {string[]} */
|
|
333
339
|
const parts = [];
|
|
334
340
|
|
|
335
|
-
|
|
336
|
-
parts.push(
|
|
337
|
-
`<link rel="stylesheet" href="${esc(asset("app.css"))}" data-jskelet-css="app.css">`,
|
|
338
|
-
);
|
|
339
|
-
}
|
|
341
|
+
pushStylesheet(parts, "app.css", blocking);
|
|
340
342
|
|
|
341
343
|
for (const sheet of styles) {
|
|
342
|
-
if (!sheet
|
|
344
|
+
if (!sheet) continue;
|
|
345
|
+
pushStylesheet(parts, sheet, blocking);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return parts.join("\n ");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Tek bir manifest anahtarı için stylesheet linki.
|
|
353
|
+
*
|
|
354
|
+
* `blocking: false` iken `media="print"`: eşleşmeyen media tarayıcıda
|
|
355
|
+
* render-blocking sayılmaz, indirme boyamayı bekletmez. `onload` sheet
|
|
356
|
+
* inince `media`'yı `all` yapar. JS kapalıyken `onload` çalışmaz ve link
|
|
357
|
+
* print'te kalır; `<noscript>` içindeki normal link o yolu kapatır.
|
|
358
|
+
*
|
|
359
|
+
* @param {string[]} parts
|
|
360
|
+
* @param {string} name Manifest anahtarı (`app.css`, `home.css`, …)
|
|
361
|
+
* @param {boolean} blocking
|
|
362
|
+
*/
|
|
363
|
+
function pushStylesheet(parts, name, blocking) {
|
|
364
|
+
if (!hasAsset(name)) return;
|
|
365
|
+
|
|
366
|
+
const href = esc(asset(name));
|
|
367
|
+
const id = esc(name);
|
|
368
|
+
if (blocking) {
|
|
343
369
|
parts.push(
|
|
344
|
-
`<link rel="stylesheet" href="${
|
|
370
|
+
`<link rel="stylesheet" href="${href}" data-jskelet-css="${id}">`,
|
|
345
371
|
);
|
|
372
|
+
return;
|
|
346
373
|
}
|
|
347
374
|
|
|
348
|
-
|
|
375
|
+
parts.push(
|
|
376
|
+
`<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'" data-jskelet-css="${id}">\n <noscript><link rel="stylesheet" href="${href}" data-jskelet-css="${id}"></noscript>`,
|
|
377
|
+
);
|
|
349
378
|
}
|
|
350
379
|
|
|
351
380
|
/**
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* `data-island-idle` görünür olsa bile sayfa sakinleşene (load + boş zaman)
|
|
17
17
|
* kadar bekletir. İlk ekranda görünen ama kritik olmayan ağır modüller
|
|
18
18
|
* (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
|
|
19
|
+
* Gizli (zero-box) host'larda da aynı yol kullanılır — `hydrate()` onları
|
|
20
|
+
* `mountWhenReady` ile bağlar, hemen `mountIsland` çağırmaz.
|
|
19
21
|
*
|
|
20
22
|
* `data-island-props` JSON ise parse edilip props olarak geçer.
|
|
21
23
|
*/
|
|
@@ -175,6 +175,11 @@ export declare const DATA_CACHE_MAX_ENTRIES_CEILING = 20000;
|
|
|
175
175
|
export declare const DEFAULT_TRANSIENT_RETRY: {
|
|
176
176
|
attempts: number;
|
|
177
177
|
delayMs: number;
|
|
178
|
+
/**
|
|
179
|
+
* `true` (varsayılan): ilk transient `notFound`ta hemen 503; retry arka
|
|
180
|
+
* planda data cache'i ısıtır. `false`: yanıt öncesi senkron retry (eski).
|
|
181
|
+
*/
|
|
182
|
+
background: boolean;
|
|
178
183
|
};
|
|
179
184
|
/**
|
|
180
185
|
* Upstream veri önbelleği.
|
|
@@ -236,9 +241,8 @@ export declare const DEFAULT_UPSTREAM_LIMIT: {
|
|
|
236
241
|
* invalidation'ı bütün node'lara yaymak. Tek instance çalışan bir kurulumda
|
|
237
242
|
* kazanç neredeyse yok; bu yüzden `enabled` varsayılan olarak kapalı.
|
|
238
243
|
*
|
|
239
|
-
* `storeEncoded`
|
|
240
|
-
*
|
|
241
|
-
* indirmekten çoğu zaman daha ucuz.
|
|
244
|
+
* `storeEncoded` açık: soğuk replica L2 HIT'te yeniden brotli ödemesin.
|
|
245
|
+
* Boyut artar; tek encoding (br) tutulduğu için maliyet sınırlıdır.
|
|
242
246
|
*/
|
|
243
247
|
export declare const DEFAULT_REDIS: {
|
|
244
248
|
enabled: boolean;
|
|
@@ -251,7 +255,7 @@ export declare const DEFAULT_REDIS: {
|
|
|
251
255
|
html: boolean;
|
|
252
256
|
/** Veri önbelleği paylaşılsın mı. */
|
|
253
257
|
data: boolean;
|
|
254
|
-
/** Brotli
|
|
258
|
+
/** Brotli gövdeleri de paylaşılsın mı (yazma sonrası preencode). */
|
|
255
259
|
storeEncoded: boolean;
|
|
256
260
|
/** pub/sub üzerinden invalidation yayını. */
|
|
257
261
|
events: boolean;
|
package/types/config/index.d.ts
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* controller'lar bu yüzeyi kullanır. Alt yollardan (`jskelet/server`)
|
|
6
6
|
* ithal etmek de mümkün; buradaki liste "kararlı" sayılan yüzeydir.
|
|
7
7
|
*/
|
|
8
|
-
export { route, fragment, renderPage, renderView, renderNotFound, } from "./server/render.js";
|
|
8
|
+
export { route, fragment, renderPage, renderView, renderNotFound, warmRenderEngine, resetRenderEngine, } from "./server/render.js";
|
|
9
9
|
export { renderStatusPage, statusFromError } from "./server/status-page.js";
|
|
10
10
|
export { createApp, startServer } from "./server/create-app.js";
|
|
11
11
|
export { notFound, redirect, permanentRedirect, seeOther, isNotFoundError, isRedirectError, NotFoundError, RedirectError, } from "./http/control-flow.js";
|
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function encodeCacheValue(value: unknown): string | Buffer | Promise<string | Buffer> | null;
|
|
9
9
|
/**
|
|
10
|
+
* Senkron çözücü — küçük test / senkron yollar için. Hot path
|
|
11
|
+
* `decodeCacheValueAsync` kullanır (event loop'u brotli ile bloklamaz).
|
|
12
|
+
*
|
|
10
13
|
* @param {Buffer | string} raw
|
|
11
14
|
* @returns {unknown}
|
|
12
15
|
*/
|
|
13
16
|
export declare function decodeCacheValue(raw: Buffer | string): unknown;
|
|
17
|
+
/**
|
|
18
|
+
* @param {Buffer | string} raw
|
|
19
|
+
* @returns {Promise<unknown>}
|
|
20
|
+
*/
|
|
21
|
+
export declare function decodeCacheValueAsync(raw: Buffer | string): Promise<unknown>;
|
|
@@ -50,10 +50,8 @@ export type HtmlEntry = {
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function earlyRefreshLeadMs(produceMs: number, ttlMs: number): number;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* LRU'dan düşmüş bir haritaya yazınca bir daha inmeyen bir artık bırakır;
|
|
56
|
-
* store'dan yeniden okumak o artığı taşımaz.
|
|
53
|
+
* Bayt sayacını store'dan yeniden hesaplar. Testler elle `encoded` doldurunca
|
|
54
|
+
* çağırır; hot path `rememberHtmlEncoding` delta kullanır.
|
|
57
55
|
*
|
|
58
56
|
* @returns {void}
|
|
59
57
|
*/
|
|
@@ -202,7 +200,8 @@ export declare function isHtmlCacheFresh(pathname: string, req?: {
|
|
|
202
200
|
export declare function isHtmlCacheKeyFresh(key: string): boolean;
|
|
203
201
|
/**
|
|
204
202
|
* Girdide en fazla bir sıkıştırılmış gövde durur. Yeni kodlama eskisinin
|
|
205
|
-
* yerini alır; ham HTML kalır. Bayt sayacı
|
|
203
|
+
* yerini alır; ham HTML kalır. Bayt sayacı delta ile güncellenir — harita
|
|
204
|
+
* LRU'dan düşmüşse (store'da yok) sayaç oynatılmaz.
|
|
206
205
|
*
|
|
207
206
|
* @param {Map<string, Buffer>} encoded
|
|
208
207
|
* @param {string} encoding
|
package/types/server/render.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
export type RenderEngine = {
|
|
2
|
+
helpers: Record<string, unknown>;
|
|
3
|
+
options: Record<string, unknown>;
|
|
4
|
+
viewsDir: string;
|
|
5
|
+
viewRoots: string[];
|
|
6
|
+
layout: string;
|
|
7
|
+
compiled: Map<string, (data: object, helpers: object) => string>;
|
|
8
|
+
loading: Map<string, Promise<(data: object, helpers: object) => string | null>>;
|
|
9
|
+
manifest: Record<string, string>;
|
|
10
|
+
templatesDir: string;
|
|
11
|
+
bust: string;
|
|
12
|
+
layoutRender: ((data: object, helpers: object) => string) | null;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Listen öncesi: motor + asset manifest ısınır (sayfa prewarm değil).
|
|
16
|
+
*
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
export declare function warmRenderEngine(): Promise<void>;
|
|
1
20
|
/**
|
|
2
21
|
* Uygulama geliştirirken bileşen dosyaları değişince kayıt yenilenmeli.
|
|
3
22
|
* Dev sunucusu süreci yeniden başlattığı için normalde gerekmez; gömülü
|
|
@@ -102,11 +102,17 @@ export declare function toKebab(name: string): string;
|
|
|
102
102
|
* build yokken 404 link'i istememek için. `.jsk` ifade dilinde `asset()` /
|
|
103
103
|
* `hasAsset()` çağrılamadığı için layout bu etiketi kullanır.
|
|
104
104
|
*
|
|
105
|
-
*
|
|
105
|
+
* `blocking` varsayılan `true`: `<link rel="stylesheet">` boyamayı bekletir.
|
|
106
|
+
* Ölçümde bu, sheet geç gelince oluşan CLS'ten daha ucuz. `false` yalnızca
|
|
107
|
+
* açık boolean'dır (`:blocking="false"`); atlamak veya `true` aynı çıktıyı
|
|
108
|
+
* verir.
|
|
109
|
+
*
|
|
110
|
+
* @param {{ styles?: string[], blocking?: boolean }} [props]
|
|
106
111
|
* @returns {string}
|
|
107
112
|
*/
|
|
108
113
|
export declare function stylesheets(props?: {
|
|
109
114
|
styles?: string[];
|
|
115
|
+
blocking?: boolean;
|
|
110
116
|
}): string;
|
|
111
117
|
/**
|
|
112
118
|
* Layout gövde sonu script'leri: `main.js`, controller `entries`, isteğe
|