jskelet 0.7.0 → 0.7.2
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 +56 -0
- package/docs/04-render-ve-sablonlar.md +20 -9
- package/docs/05-islands.md +8 -4
- package/docs/06-cache.md +18 -16
- package/docs/07-yapilandirma.md +8 -7
- package/docs/en/04-rendering.md +21 -9
- 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/package.json +1 -1
- package/src/build/build.mjs +23 -8
- package/src/build/tasks/precompress.mjs +51 -11
- package/src/client/registry.js +13 -3
- package/src/compile/codegen.js +70 -4
- package/src/compile/compile-all.js +18 -3
- package/src/compile/index.js +6 -1
- 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/assets.js +51 -22
- 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 +183 -60
- package/src/templates/layout.jsk +4 -4
- package/src/templates/layout.render.js +2 -21
- package/src/views/components/loader.js +11 -6
- package/src/views/helpers/html.js +13 -5
- package/src/views/helpers/tags.js +21 -4
- package/types/client/registry.d.ts +2 -0
- package/types/compile/codegen.d.ts +9 -4
- package/types/compile/compile-all.d.ts +3 -1
- package/types/compile/index.d.ts +1 -1
- 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/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,21 +198,40 @@ 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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
214
|
+
const eagerIds = Object.keys(manifest).filter(
|
|
215
|
+
(id) => id === "layout" || id.startsWith("components/"),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const entries = await Promise.all(
|
|
219
|
+
eagerIds.map(async (viewId) => {
|
|
220
|
+
const rel = manifest[viewId];
|
|
221
|
+
const file = path.join(templatesDir, rel);
|
|
222
|
+
try {
|
|
223
|
+
const mod = await import(pathToFileURL(file).href + bust);
|
|
224
|
+
return /** @type {const} */ ([viewId, mod]);
|
|
225
|
+
} catch {
|
|
226
|
+
return /** @type {const} */ ([viewId, null]);
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
for (const [viewId, mod] of entries) {
|
|
232
|
+
if (!mod || typeof mod.render !== "function") continue;
|
|
169
233
|
compiled.set(viewId, mod.render);
|
|
170
234
|
|
|
171
|
-
// `components/*.jsk` → helpers.PascalName
|
|
172
235
|
if (viewId.startsWith("components/")) {
|
|
173
236
|
const base = viewId.slice("components/".length).split("/").pop() ?? "";
|
|
174
237
|
const pascal = base
|
|
@@ -183,7 +246,24 @@ async function loadCompiledTemplates(config, helpers) {
|
|
|
183
246
|
}
|
|
184
247
|
|
|
185
248
|
const layoutRender = compiled.get("layout") ?? null;
|
|
186
|
-
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
|
+
}
|
|
187
267
|
}
|
|
188
268
|
|
|
189
269
|
/**
|
|
@@ -222,8 +302,13 @@ export function resetRenderEngine() {
|
|
|
222
302
|
* @returns {Promise<string>}
|
|
223
303
|
*/
|
|
224
304
|
export async function renderView(view, data = {}) {
|
|
225
|
-
const
|
|
226
|
-
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
|
+
}
|
|
227
312
|
if (renderFn) {
|
|
228
313
|
return renderFn({ ...data }, helpers);
|
|
229
314
|
}
|
|
@@ -246,21 +331,36 @@ export async function renderPage(page) {
|
|
|
246
331
|
const { helpers, options, layout, layoutRender } = await getEngine();
|
|
247
332
|
const config = getConfig();
|
|
248
333
|
|
|
249
|
-
|
|
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 () => ({
|
|
250
338
|
...(await hook("metadata", {}, page)),
|
|
251
|
-
...
|
|
252
|
-
};
|
|
339
|
+
...pageMeta,
|
|
340
|
+
}))();
|
|
341
|
+
|
|
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
|
+
});
|
|
253
355
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const [body, context] = await Promise.all([
|
|
258
|
-
renderView(page.view, { ...(page.data ?? {}), metadata }),
|
|
259
|
-
hook("layoutContext", {}, { pathname: page.pathname ?? "", metadata }),
|
|
356
|
+
const [body, { metadata, context }] = await Promise.all([
|
|
357
|
+
bodyPromise,
|
|
358
|
+
layoutPromise,
|
|
260
359
|
]);
|
|
261
360
|
|
|
262
|
-
|
|
263
|
-
|
|
361
|
+
// Layout local'leri. `.jsk` `(data, helpers)` aldığı için helpers'ı data'ya
|
|
362
|
+
// kopyalamak gereksiz allocation; EJS layout hâlâ bare helper adları bekler.
|
|
363
|
+
const pageLocals = {
|
|
264
364
|
...context,
|
|
265
365
|
metadata,
|
|
266
366
|
// Boş varsayılan bilinçli: "/" yazmak her sayfayı ana sayfa sanıp
|
|
@@ -286,7 +386,7 @@ export async function renderPage(page) {
|
|
|
286
386
|
};
|
|
287
387
|
|
|
288
388
|
if (layoutRender) {
|
|
289
|
-
return layoutRender(
|
|
389
|
+
return layoutRender(pageLocals, helpers);
|
|
290
390
|
}
|
|
291
391
|
|
|
292
392
|
if (layout.endsWith(".jsk")) {
|
|
@@ -296,7 +396,7 @@ export async function renderPage(page) {
|
|
|
296
396
|
);
|
|
297
397
|
}
|
|
298
398
|
|
|
299
|
-
return renderEjsFile(layout,
|
|
399
|
+
return renderEjsFile(layout, { ...helpers, ...pageLocals }, options);
|
|
300
400
|
}
|
|
301
401
|
|
|
302
402
|
/**
|
|
@@ -683,31 +783,28 @@ async function sendHtml(req, res, body, encoded, options = {}) {
|
|
|
683
783
|
const encoding =
|
|
684
784
|
req.method === "HEAD" ? null : negotiateEncoding(req.headers["accept-encoding"]);
|
|
685
785
|
|
|
686
|
-
if (!encoding
|
|
786
|
+
if (!encoding) {
|
|
687
787
|
if (options.etag === false) {
|
|
688
|
-
// Sıkıştırma middleware'i devreye girerse Content-Length'i kendisi
|
|
689
|
-
// kaldırır; girmezse doğru uzunlukla gider.
|
|
690
788
|
res.setHeader("Content-Length", String(Buffer.byteLength(body)));
|
|
691
789
|
res.end(req.method === "HEAD" ? undefined : body);
|
|
692
790
|
return;
|
|
693
791
|
}
|
|
694
|
-
|
|
695
792
|
res.send(body);
|
|
696
793
|
return;
|
|
697
794
|
}
|
|
698
795
|
|
|
699
|
-
|
|
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);
|
|
700
799
|
if (!buffer) {
|
|
701
800
|
buffer = await encodeText(body, encoding);
|
|
702
|
-
|
|
703
|
-
// gelince eski buffer düşer, bayt bütçesi iki gövdeyi birden saymaz.
|
|
704
|
-
rememberHtmlEncoding(encoded, encoding, buffer);
|
|
801
|
+
if (encoded) rememberHtmlEncoding(encoded, encoding, buffer);
|
|
705
802
|
}
|
|
706
803
|
|
|
707
804
|
res.setHeader("Content-Encoding", encoding);
|
|
708
805
|
res.setHeader("Vary", "Accept-Encoding");
|
|
709
806
|
res.setHeader("Content-Length", String(buffer.length));
|
|
710
|
-
res.end(buffer);
|
|
807
|
+
res.end(req.method === "HEAD" ? undefined : buffer);
|
|
711
808
|
}
|
|
712
809
|
|
|
713
810
|
/**
|
|
@@ -759,12 +856,47 @@ async function produce(controller, ctx) {
|
|
|
759
856
|
if (!first.transient.length) return { html: await renderNotFound(), status: 404 };
|
|
760
857
|
|
|
761
858
|
// Buraya gelindiyse `notFound()` veri gelmediği için çağrılmış. **Var olan
|
|
762
|
-
// bir sayfayı** 404 olarak servis etmek en kötü sonu
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
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();
|
|
766
863
|
let failures = first.transient;
|
|
767
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
|
+
|
|
768
900
|
for (let round = 1; round <= attempts; round += 1) {
|
|
769
901
|
const retryDetail =
|
|
770
902
|
`returned notFound() while upstream is failing ` +
|
|
@@ -773,29 +905,20 @@ async function produce(controller, ctx) {
|
|
|
773
905
|
console.warn(`[render] ${ctx.pathname} ${retryDetail}`);
|
|
774
906
|
}
|
|
775
907
|
|
|
776
|
-
// Beklemeden tekrar denemek rate limit'e girmiş bir API'de aynı 429'u
|
|
777
|
-
// getirir; kısa bekleme hem pencerenin dönmesine şans verir hem de
|
|
778
|
-
// fırtınayı büyütmez.
|
|
779
908
|
await sleep(delayMs * round);
|
|
780
909
|
|
|
781
|
-
// Her deneme kendi upstream ve istek içi cache bağlamında çalışır: ilk
|
|
782
|
-
// turun hataları ikinci turun kararını kirletmesin ve memoize edilmiş
|
|
783
|
-
// boş cevaplar tekrar kullanılmasın.
|
|
784
910
|
const retried = await withUpstreamTracking(() =>
|
|
785
911
|
withRequestCache(() => attempt(controller, ctx)),
|
|
786
912
|
);
|
|
787
913
|
|
|
788
914
|
if ("page" in retried) return retried.page;
|
|
789
915
|
if (!retried.transient.length) {
|
|
790
|
-
// Bu kez upstream sağlam cevap verdi ve "yok" dedi: gerçek 404.
|
|
791
916
|
return { html: await renderNotFound(), status: 404 };
|
|
792
917
|
}
|
|
793
918
|
|
|
794
919
|
failures = retried.transient;
|
|
795
920
|
}
|
|
796
921
|
|
|
797
|
-
// Denemeler tükendi. 404 yerine 503: önbelleğe girmez, `Retry-After` taşır
|
|
798
|
-
// ve bir sonraki istek gerçek içeriği üretebilir.
|
|
799
922
|
const outageDetail =
|
|
800
923
|
`could not be produced, upstream is still failing ` +
|
|
801
924
|
`(${summarizeFailures(failures)}), serving an uncached 503 instead of a 404`;
|
|
@@ -819,11 +942,10 @@ async function produce(controller, ctx) {
|
|
|
819
942
|
const RETRY_AFTER_SECONDS = 30;
|
|
820
943
|
|
|
821
944
|
/**
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
* 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ü.
|
|
825
947
|
*
|
|
826
|
-
* @returns {{ attempts: number, delayMs: number }}
|
|
948
|
+
* @returns {{ attempts: number, delayMs: number, background: boolean }}
|
|
827
949
|
*/
|
|
828
950
|
function transientRetry() {
|
|
829
951
|
const raw = /** @type {any} */ (getConfig().transientRetry ?? {});
|
|
@@ -833,6 +955,7 @@ function transientRetry() {
|
|
|
833
955
|
return {
|
|
834
956
|
attempts: Number.isFinite(attempts) && attempts >= 0 ? Math.floor(attempts) : 1,
|
|
835
957
|
delayMs: Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 300,
|
|
958
|
+
background: raw.background !== false,
|
|
836
959
|
};
|
|
837
960
|
}
|
|
838
961
|
|
package/src/templates/layout.jsk
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
anda bu dosya devre dışı kalır; amacı yeni bir projenin tek route ile
|
|
6
6
|
çalışabilmesi. Kopyalayıp başlangıç noktası olarak kullanın.
|
|
7
7
|
|
|
8
|
-
Kullanılabilir
|
|
9
|
-
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath
|
|
10
|
-
|
|
11
|
-
`hooks.layoutContext()`
|
|
8
|
+
Kullanılabilir data alanları: metadata, headMeta, extraHead, structuredData,
|
|
9
|
+
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath.
|
|
10
|
+
Yardımcılar ikinci `helpers` argümanında (PascalCase etiketler /
|
|
11
|
+
helpers.esc). `hooks.layoutContext()` alanları data'ya eklenir.
|
|
12
12
|
|
|
13
13
|
Stylesheets / JsonLd / BodyScripts: `.jsk` ifade dilinde fonksiyon çağrısı
|
|
14
14
|
olmadığı için asset döngüleri bu yerleşik etiketlere taşındı.
|
|
@@ -6,36 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export function render(data, helpers) {
|
|
8
8
|
let s = "";
|
|
9
|
-
s += "\r\n";
|
|
10
9
|
s += "<!DOCTYPE html>";
|
|
11
|
-
s += "\r\n";
|
|
12
10
|
s += "<html" + helpers.attrs({"lang": data.lang}) + ">";
|
|
13
|
-
s += "\
|
|
14
|
-
s += "<head>";
|
|
15
|
-
s += "\r\n ";
|
|
16
|
-
s += "<meta charset=\"utf-8\">";
|
|
17
|
-
s += "\r\n ";
|
|
18
|
-
s += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
|
|
19
|
-
s += "\r\n ";
|
|
11
|
+
s += "<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
|
|
20
12
|
s += (data.extraHead ?? "");
|
|
21
|
-
s += "\r\n ";
|
|
22
13
|
s += (helpers.Stylesheets({ "styles": data.styles }) ?? "");
|
|
23
|
-
s += "\r\n ";
|
|
24
14
|
s += (data.headMeta ?? "");
|
|
25
|
-
s += "\r\n ";
|
|
26
15
|
s += (helpers.JsonLd({ "items": data.structuredData }) ?? "");
|
|
27
|
-
s += "\r\n ";
|
|
28
16
|
s += "</head>";
|
|
29
|
-
s += "\r\n ";
|
|
30
17
|
s += "<body" + helpers.attrs({"class": data.bodyClass}) + ">";
|
|
31
|
-
s += "\r\n ";
|
|
32
18
|
s += (data.body ?? "");
|
|
33
|
-
s += "\r\n ";
|
|
34
19
|
s += (helpers.BodyScripts({ "entries": data.entries, "devtools": data.devtools, "devBasePath": data.devBasePath }) ?? "");
|
|
35
|
-
s += "
|
|
36
|
-
s += "</body>";
|
|
37
|
-
s += "\r\n";
|
|
38
|
-
s += "</html>";
|
|
39
|
-
s += "\r\n";
|
|
20
|
+
s += "</body></html>";
|
|
40
21
|
return s;
|
|
41
22
|
}
|
|
@@ -43,7 +43,8 @@ function collect(dir, out = []) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Modülleri paralel import eder; export uygulama sırası dosya listesiyle
|
|
47
|
+
* aynı kalır (barrel önce, sonra sıralı bileşenler).
|
|
47
48
|
*
|
|
48
49
|
* @param {string} dir
|
|
49
50
|
* @param {Record<string, unknown>} components
|
|
@@ -59,12 +60,16 @@ async function loadDir(dir, components, origin) {
|
|
|
59
60
|
...collect(dir).sort(),
|
|
60
61
|
];
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const loaded = await Promise.all(
|
|
64
|
+
files.map(async (file) => {
|
|
65
|
+
const isBarrel = path.basename(file) === BARREL;
|
|
66
|
+
const fileId = isBarrel ? BARREL : file.split(path.sep).join("/");
|
|
67
|
+
const module = await import(pathToFileURL(file).href);
|
|
68
|
+
return { fileId, module };
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
67
71
|
|
|
72
|
+
for (const { fileId, module } of loaded) {
|
|
68
73
|
for (const [name, value] of Object.entries(module)) {
|
|
69
74
|
if (name === "default") continue;
|
|
70
75
|
|
|
@@ -9,6 +9,11 @@ const ESCAPE_MAP = {
|
|
|
9
9
|
"'": "'",
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/** Kaçış gerektiren karakter var mı — yoksa `replace` callback'ine girilmez. */
|
|
13
|
+
const NEEDS_ESCAPE = /[&<>"']/;
|
|
14
|
+
/** Tüm eşleşmeleri değiştirmek için ayrı global kopya (`test` lastIndex'i kirletmesin). */
|
|
15
|
+
const ESCAPE_ALL = /[&<>"']/g;
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Metin içeriği ve attribute değerleri için kaçış.
|
|
14
19
|
* @param {unknown} value
|
|
@@ -16,7 +21,9 @@ const ESCAPE_MAP = {
|
|
|
16
21
|
*/
|
|
17
22
|
export function esc(value) {
|
|
18
23
|
if (value == null || value === false) return "";
|
|
19
|
-
|
|
24
|
+
const str = typeof value === "string" ? value : String(value);
|
|
25
|
+
if (!NEEDS_ESCAPE.test(str)) return str;
|
|
26
|
+
return str.replace(ESCAPE_ALL, (char) => ESCAPE_MAP[char]);
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
@@ -41,16 +48,17 @@ export function jsonScript(value) {
|
|
|
41
48
|
* @returns {string}
|
|
42
49
|
*/
|
|
43
50
|
export function attrs(attrs) {
|
|
44
|
-
|
|
51
|
+
// Dizi + join yerine doğrudan birikim: tipik etiket 2–6 attribute.
|
|
52
|
+
let out = "";
|
|
45
53
|
for (const [key, value] of Object.entries(attrs)) {
|
|
46
54
|
if (value == null || value === false) continue;
|
|
47
55
|
if (value === true) {
|
|
48
|
-
|
|
56
|
+
out += ` ${key}`;
|
|
49
57
|
continue;
|
|
50
58
|
}
|
|
51
|
-
|
|
59
|
+
out += ` ${key}="${esc(value)}"`;
|
|
52
60
|
}
|
|
53
|
-
return
|
|
61
|
+
return out;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
@@ -24,6 +24,12 @@ const isDev = process.env.NODE_ENV === "development";
|
|
|
24
24
|
/** Aynı eksik ikon için tek uyarı. */
|
|
25
25
|
const warnedIcons = new Set();
|
|
26
26
|
|
|
27
|
+
/** `fill` Image varsayılan sınıfları — `cn`/`twMerge` olmadan sabit. */
|
|
28
|
+
const IMAGE_FILL_CLASS = "absolute inset-0 h-full w-full object-cover";
|
|
29
|
+
|
|
30
|
+
/** Sprite URL process ömründe bir kez çözülür (hash değişince process yenilenir). */
|
|
31
|
+
let cachedSpritePath = /** @type {string | null} */ (null);
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* `SeoLink` karşılığı: title otomatik doldurulur.
|
|
29
35
|
* @param {{ href: string, text?: string, html?: string, class?: string,
|
|
@@ -101,14 +107,19 @@ export function image(props) {
|
|
|
101
107
|
(optimized && optimized.variants.length > 1) ||
|
|
102
108
|
(remote && remote.srcset);
|
|
103
109
|
|
|
110
|
+
// `fill` + ek sınıf yoksa twMerge'e girme; ek sınıf varsa çakışma çözümü gerekir.
|
|
111
|
+
const fillClass = fill
|
|
112
|
+
? className
|
|
113
|
+
? cn(IMAGE_FILL_CLASS, className)
|
|
114
|
+
: IMAGE_FILL_CLASS
|
|
115
|
+
: className;
|
|
116
|
+
|
|
104
117
|
const attributes = attrs({
|
|
105
118
|
src: largest?.url ?? remote?.src ?? src,
|
|
106
119
|
alt: alt ?? "",
|
|
107
120
|
width: fill ? undefined : (width ?? optimized?.width),
|
|
108
121
|
height: fill ? undefined : (height ?? optimized?.height),
|
|
109
|
-
class:
|
|
110
|
-
? cn("absolute inset-0 h-full w-full object-cover", className)
|
|
111
|
-
: className,
|
|
122
|
+
class: fillClass,
|
|
112
123
|
sizes: responsive
|
|
113
124
|
? (sizes ?? (optimized ? defaultSizes(optimized) : remote?.sizes))
|
|
114
125
|
: sizes,
|
|
@@ -213,7 +224,13 @@ export function icon(props) {
|
|
|
213
224
|
attrs: extra = {},
|
|
214
225
|
} = props;
|
|
215
226
|
|
|
216
|
-
|
|
227
|
+
// Prod'da hash deploy ile birlikte değişir (process yenilenir); dev'de
|
|
228
|
+
// watch rebuild sprite yolunu güncelleyebildiği için her seferinde çözülür.
|
|
229
|
+
let spritePath = cachedSpritePath;
|
|
230
|
+
if (spritePath == null || isDev) {
|
|
231
|
+
spritePath = asset("sprite.svg", { origin: "document" });
|
|
232
|
+
if (!isDev) cachedSpritePath = spritePath;
|
|
233
|
+
}
|
|
217
234
|
const id = `${toKebab(name)}-${weight}`;
|
|
218
235
|
|
|
219
236
|
if (isDev && !warnedIcons.has(id)) {
|
|
@@ -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
|
*/
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export type AstNode = import('./parse.js').AstNode;
|
|
2
2
|
export type AttrNode = import('./parse.js').AttrNode;
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {import('./parse.js').AstNode} AstNode
|
|
5
|
-
* @typedef {import('./parse.js').AttrNode} AttrNode
|
|
6
|
-
*/
|
|
7
3
|
/**
|
|
8
4
|
* @param {AstNode[]} ast
|
|
9
5
|
* @param {{ viewId: string, file?: string, knownComponents?: Set<string> }} options
|
|
@@ -30,3 +26,12 @@ export declare function normalizeIncludeId(includePath: string): string;
|
|
|
30
26
|
* @returns {string}
|
|
31
27
|
*/
|
|
32
28
|
export declare function toPascalCase(name: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Şablon girintisini düşür: satır sonu içeren salt-whitespace → boş;
|
|
31
|
+
* yalnızca boşluk/tab → tek boşluk (inline aralık). Koruma alanı dışında.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} value
|
|
34
|
+
* @param {boolean} preserve
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export declare function collapseTemplateWhitespace(value: string, preserve: boolean): string;
|
|
@@ -12,7 +12,9 @@ export declare function compileAll(config: {
|
|
|
12
12
|
outDir: string;
|
|
13
13
|
}>;
|
|
14
14
|
/**
|
|
15
|
-
* Manifest yoksa veya herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
15
|
+
* Manifest yoksa veya (dev'de) herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
16
|
+
* Production'da manifest varsa mtime taraması atlanır — `jskelet build` CI
|
|
17
|
+
* güvencesi; ilk istekte yüzlerce `statSync` cold start'ı şişirmesin.
|
|
16
18
|
* Dev sunucu restart'ında build watch kaçırmış olsa bile şablonlar güncel kalır.
|
|
17
19
|
*
|
|
18
20
|
* @param {{ root: string, dirs: Record<string, string> }} config
|
package/types/compile/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export { CompileError, indexToLocation } from "./errors.js";
|
|
5
5
|
export { parseExpr, codegenExpr } from "./expr.js";
|
|
6
6
|
export { parseTemplate, isComponentTag } from "./parse.js";
|
|
7
|
-
export { codegen, toPascalCase, normalizeIncludeId } from "./codegen.js";
|
|
7
|
+
export { codegen, toPascalCase, normalizeIncludeId, collapseTemplateWhitespace, } from "./codegen.js";
|
|
8
8
|
export { getViewRoots, getComponentDirs, discoverJskFiles, componentNameFromViewId, collectKnownComponents, toComponentTag, } from "./resolve.js";
|
|
9
9
|
export { scanNamedExports } from "./scan-exports.js";
|
|
10
10
|
export { compileAll, compileSource, ensureTemplatesCompiled } from "./compile-all.js";
|