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.
Files changed (46) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/docs/04-render-ve-sablonlar.md +20 -9
  3. package/docs/05-islands.md +8 -4
  4. package/docs/06-cache.md +18 -16
  5. package/docs/07-yapilandirma.md +8 -7
  6. package/docs/en/04-rendering.md +21 -9
  7. package/docs/en/05-islands.md +8 -4
  8. package/docs/en/06-caching.md +18 -16
  9. package/docs/en/07-configuration.md +8 -6
  10. package/package.json +1 -1
  11. package/src/build/build.mjs +23 -8
  12. package/src/build/tasks/precompress.mjs +51 -11
  13. package/src/client/registry.js +13 -3
  14. package/src/compile/codegen.js +70 -4
  15. package/src/compile/compile-all.js +18 -3
  16. package/src/compile/index.js +6 -1
  17. package/src/config/defaults.js +9 -5
  18. package/src/config/index.js +3 -3
  19. package/src/http/request-cache.js +19 -1
  20. package/src/index.js +2 -0
  21. package/src/server/assets.js +51 -22
  22. package/src/server/cache-blob.js +33 -0
  23. package/src/server/create-app.js +5 -0
  24. package/src/server/data-cache.js +77 -11
  25. package/src/server/disk-cache.js +2 -2
  26. package/src/server/head-hints.js +21 -9
  27. package/src/server/html-cache.js +169 -39
  28. package/src/server/image-optimizer.js +72 -28
  29. package/src/server/redis.js +2 -2
  30. package/src/server/render.js +183 -60
  31. package/src/templates/layout.jsk +4 -4
  32. package/src/templates/layout.render.js +2 -21
  33. package/src/views/components/loader.js +11 -6
  34. package/src/views/helpers/html.js +13 -5
  35. package/src/views/helpers/tags.js +21 -4
  36. package/types/client/registry.d.ts +2 -0
  37. package/types/compile/codegen.d.ts +9 -4
  38. package/types/compile/compile-all.d.ts +3 -1
  39. package/types/compile/index.d.ts +1 -1
  40. package/types/config/defaults.d.ts +8 -4
  41. package/types/config/index.d.ts +1 -0
  42. package/types/index.d.ts +1 -1
  43. package/types/server/cache-blob.d.ts +8 -0
  44. package/types/server/data-cache.d.ts +1 -0
  45. package/types/server/html-cache.d.ts +4 -5
  46. package/types/server/render.d.ts +19 -0
@@ -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
- * @type {{ helpers: Record<string, unknown>, options: Record<string, unknown>,
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
- * } | null}
69
+ * }} RenderEngine
66
70
  */
71
+
72
+ /** @type {RenderEngine | null} */
67
73
  let engine = null;
68
74
 
69
75
  /**
70
- * @returns {Promise<NonNullable<typeof engine>>}
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 { compiled, layoutRender: appLayoutRender } = await loadCompiledTemplates(
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 = appLayoutRender;
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
- * Derlenmiş `.jsk` modüllerini yükler. İstek anında parse/compile yok.
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 { compiled, layoutRender: null };
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
- for (const [viewId, rel] of Object.entries(manifest)) {
165
- const file = path.join(templatesDir, rel);
166
- if (!fs.existsSync(file)) continue;
167
- const mod = await import(pathToFileURL(file).href + bust);
168
- if (typeof mod.render !== "function") continue;
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 { helpers, options, compiled, viewRoots, viewsDir } = await getEngine();
226
- const renderFn = compiled.get(view);
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
- const metadata = {
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
- ...(page.metadata ?? {}),
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
- // Layout bağlamı ve gövde paralel üretilir: navigasyon çoğu projede
255
- // upstream'den geliyor ve gövde render'ıyla sırayla beklemek her sayfaya
256
- // gereksiz gecikme ekliyor.
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
- const locals = {
263
- ...helpers,
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(locals, helpers);
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, locals, options);
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 || !encoded) {
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
- let buffer = encoded.get(encoding);
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
- // Ham HTML kalır; sıkıştırılmış kopya tek kodlamadır. Diğer istemci
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ç: arama motoru geçici
763
- // bir rate limit'i kalıcı bir kayıp sanar. Bu yüzden sayfa yeniden denenir —
764
- // ısıtma günlükleri gösteriyor ki aynı yol saniyeler sonra 200 dönüyor.
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
- * Tekrar denemenin maliyeti upstream'e binen ikinci bir istek turu; bu yüzden
823
- * varsayılan tek deneme ve kısa bekleme. Rate limit fırtınasında toplam yük
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
 
@@ -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 local'ler: metadata, headMeta, extraHead, structuredData,
9
- body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
10
- asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
11
- `hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
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 += "\r\n ";
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 += "\r\n ";
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
- * Tek bir `components/` dizinini yükler.
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
- for (const file of files) {
63
- const isBarrel = path.basename(file) === BARREL;
64
- // Kimlik mutlak yol — çoklu components kökünde göreli ad çakışmasın.
65
- const fileId = isBarrel ? BARREL : file.split(path.sep).join("/");
66
- const module = await import(pathToFileURL(file).href);
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
  "'": "&#39;",
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
- return String(value).replace(/[&<>"']/g, (char) => ESCAPE_MAP[char]);
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
- const parts = [];
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
- parts.push(key);
56
+ out += ` ${key}`;
49
57
  continue;
50
58
  }
51
- parts.push(`${key}="${esc(value)}"`);
59
+ out += ` ${key}="${esc(value)}"`;
52
60
  }
53
- return parts.length ? ` ${parts.join(" ")}` : "";
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: fill
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
- const spritePath = asset("sprite.svg", { origin: "document" });
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
@@ -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";