jskelet 0.3.4 → 0.4.0

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 CHANGED
@@ -27,15 +27,26 @@ one is listed under a **Breaking** heading.
27
27
 
28
28
  ### Added
29
29
 
30
+ - Build-time `.jsk` templates: declarative HTML-like syntax compiled to ESM
31
+ render modules under `.jskelet/templates/` (no request-time parse, `eval`, or
32
+ `new Function`). Coexists with EJS; compiled `.jsk` wins when both exist.
33
+ Syntax: `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}` / `{#include}`, PascalCase
34
+ components (`:prop` bindings), built-ins `Link` / `Image` / `Icon` /
35
+ `CsrfField` / `PreloadImage`.
36
+ - Feature-first conventions: `paths.features` / `paths.shared`, multi-root
37
+ views and components, `features/<name>/index.js` route registration after
38
+ `routes/`. CLI: `jskelet generate feature|page|island`. `jskelet init`
39
+ scaffolds `.jsk` pages.
40
+ - Template compile step in `jskelet build`; icon scan and Tailwind docs cover
41
+ `.jsk` / `features` / `shared`. Bench: `node scripts/bench-templates.mjs`.
30
42
  - Top-level `logs` config for persistent sinks: daily NDJSON files
31
43
  (`logs.file`) and batched S3 PutObject (`logs.s3`) with embedded SigV4 — no
32
44
  `@aws-sdk` dependency. `kinds` selects `http` / `event` / `error`; `console`
33
- toggles runtime stdout lines. `JSKELET_LOG_BUCKET` (and `logs.s3.bucket`) may
34
- be a plain bucket or a `bucket/prefix/…` path. `JSKELET_S3_API_URL` sets the
35
- R2/MinIO endpoint (region defaults to `auto`). Missing credentials warn and
36
- disable the S3 sink without taking the site down. Env: `JSKELET_LOG_BUCKET`,
37
- `JSKELET_S3_ACCESS_KEY_ID`, `JSKELET_S3_SECRET_ACCESS_KEY`,
38
- `JSKELET_S3_SESSION_TOKEN`, `JSKELET_S3_REGION`, `JSKELET_S3_API_URL`.
45
+ toggles runtime stdout lines. `JSKELET_LOG_BUCKET` / `JSKELET_S3_BUCKET`
46
+ (+ optional `JSKELET_S3_KEY_PREFIX`) may be a plain bucket or a
47
+ `bucket/prefix/…` path; with credentials present the sink turns on without
48
+ `enabled: true`. `JSKELET_S3_API_URL` sets the S3-compatible endpoint
49
+ (region defaults to `auto`). Env: `JSKELET_LOG_BUCKET`, `JSKELET_S3_*`.
39
50
  - Admin panel pages under `/_jskelet/admin`: Overview, Cache, Routes, Views,
40
51
  Logs and System. Configurable `allowIps` (exact or CIDR), `blockBots` (default
41
52
  on — crawler UAs get 404 before login), and `logSize`. Live Logs use an
@@ -114,6 +125,11 @@ one is listed under a **Breaking** heading.
114
125
  variant of a path, which is the right default for a webhook but wrong when you
115
126
  want `/list?page=2` gone and `/list?page=3` left hot.
116
127
 
128
+ ### Changed
129
+
130
+ - `examples/minimal` pages moved to `.jsk`; adds `features/demo` as a
131
+ co-located route + view sample.
132
+
117
133
  - An adaptive per-host rate limit for upstream calls, `cache().upstream`. It sits
118
134
  in the `fetch` wrapper rather than in the prewarm pass, because what spends the
119
135
  quota is the API call, not the page: one render may make one call or twenty, so
package/bin/jskelet.mjs CHANGED
@@ -1,11 +1,12 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * JSkelet CLI.
4
4
  *
5
5
  * jskelet dev build watch + sunucu, canlı yenileme, dev overlay
6
6
  * jskelet build tek seferlik prod build (fontlar, sprite, CSS, JS, görseller)
7
7
  * jskelet start prod sunucu (build eksikse önce üretir)
8
- * jskelet init bulunduğun dizine minimal iskelet kurar
8
+ * jskelet init bulunduğun dizine minimal iskelet kurar
9
+ * jskelet generate feature / page / island iskeleti
9
10
  *
10
11
  * Alt komutlar ayrı süreçlerde çalışır. Sebep: `dev` iki uzun ömürlü süreci
11
12
  * (build watch + sunucu) yönetiyor ve sunucunun ESM resolve hook'larına
@@ -90,14 +91,26 @@ switch (command) {
90
91
  break;
91
92
  }
92
93
 
94
+ case "generate": {
95
+ const { generate } = await import("../src/generate.mjs");
96
+ try {
97
+ await generate(process.cwd(), rest);
98
+ } catch (error) {
99
+ process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
100
+ process.exit(1);
101
+ }
102
+ break;
103
+ }
104
+
93
105
  default: {
94
106
  const known = command ? `unknown command: ${command}\n\n` : "";
95
107
  process.stderr.write(
96
- `${known}usage: jskelet <dev|build|start|init>\n\n` +
97
- " dev build watch + server (live reload, dev overlay)\n" +
98
- " build production build\n" +
99
- " start production server\n" +
100
- " init scaffold a minimal skeleton in the current directory\n",
108
+ `${known}usage: jskelet <dev|build|start|init|generate>\n\n` +
109
+ " dev build watch + server (live reload, dev overlay)\n" +
110
+ " build production build\n" +
111
+ " start production server\n" +
112
+ " init scaffold a minimal skeleton in the current directory\n" +
113
+ " generate scaffold feature | page | island\n",
101
114
  );
102
115
  process.exit(command ? 1 : 0);
103
116
  }
package/docs/02-mimari.md CHANGED
@@ -25,6 +25,9 @@ JSkelet bu gözlemi mimarinin merkezine alır:
25
25
  olarak, kendi modülüyle, kendi zamanında bağlanır.
26
26
  3. **Sayfa üretimi önbelleklenir.** Aynı HTML'i her istekte yeniden üretmenin
27
27
  anlamı yok; TTL'li bir bellek önbelleği ISR'nin yerini tutar.
28
+ 4. **Şablonlar build-time derlenir (`.jsk`).** İstek anında parse yok; EJS
29
+ legacy olarak yan yana kalır. Feature'lar `features/<name>/{server,views,client}`
30
+ altında toplanabilir — URL kaydı yine açıktır.
28
31
 
29
32
  ## Bir isteğin yolu
30
33
 
@@ -63,9 +63,25 @@ export default {
63
63
  };
64
64
  ```
65
65
 
66
- **2. Liste yoksa `routes/` dizini alfabetik taranır.** Tarama özyinelemelidir
67
- (alt dizinler de dâhil), yalnızca `.js` ve `.mjs` dosyaları alınır ve adı `_`
68
- ile başlayan dosyalar atlanır (`_helpers.js` gibi paylaşılan modüller için).
66
+ **2. Liste yoksa `routes/` dizini alfabetik taranır**, ardından her
67
+ `features/<name>/index.js` (veya `.mjs`) alfabetik eklenir. Tarama
68
+ özyinelemelidir (alt dizinler de dâhil), yalnızca `.js` ve `.mjs` dosyaları
69
+ alınır ve adı `_` ile başlayan dosyalar atlanır (`_helpers.js` gibi paylaşılan
70
+ modüller için).
71
+
72
+ Feature-first düzen zorunlu değildir; bir feature örneği:
73
+
74
+ ```
75
+ features/piyasalar/
76
+ index.js # register(app, api) — URL'ler açıkça yazılır
77
+ server/
78
+ views/pages/… # .jsk veya .ejs
79
+ views/components/
80
+ client/ # island'lar; client/entries'ten registerAll
81
+ ```
82
+
83
+ `jskelet generate feature|page|island` iskelet üretir. Filesystem URL routing
84
+ yoktur.
69
85
 
70
86
  Bu durumda dosya adlarına sayısal önek verin:
71
87
 
@@ -20,17 +20,70 @@ route(controller)
20
20
  │ renderView(page.view, { …data, metadata }), → body
21
21
  │ hooks.layoutContext({ pathname, metadata }), → context
22
22
  │ ])
23
- └─ layout.ejs render → tam HTML
23
+ └─ layout (.jsk derlenmiş veya .ejs) → tam HTML
24
24
  ```
25
25
 
26
26
  Layout bağlamı ve gövde **paralel** üretilir. Sebebi ölçümden geliyor:
27
27
  navigasyon çoğu projede upstream'den geliyor ve gövde render'ıyla sırayla
28
28
  beklemek her sayfaya gereksiz gecikme ekliyor.
29
29
 
30
- ## EJS motoru
30
+ ## `.jsk` — build-time derlenmiş şablonlar
31
31
 
32
- Motor ilk render'da bir kez kurulur; bileşen taraması dosya sistemine
33
- dokunduğu için her istekte yapılamaz ve config yüklenmeden hesaplanamaz.
32
+ Yeni uygulamalarda varsayılan şablon biçimi `.jsk`'dir. Build sırasında
33
+ (`.jskelet/templates/*.mjs`) normal ESM modüllerine çevrilir; **istek anında
34
+ parse / `eval` / `new Function` yoktur**. Production yolu:
35
+
36
+ ```
37
+ controller data → import edilmiş render(data, helpers) → HTML
38
+ ```
39
+
40
+ ### Sözdizimi özeti
41
+
42
+ ```html
43
+ <section class="wrapper">
44
+ <h1>{{ title }}</h1>
45
+ <div>{{{ trustedHtml }}}</div>
46
+
47
+ {#if items.length}
48
+ <List :items="items" />
49
+ {#else}
50
+ <p>Boş</p>
51
+ {/if}
52
+
53
+ {#each items as item, i}
54
+ <li data-i="{{ i }}">{{ item }}</li>
55
+ {/each}
56
+
57
+ <Link href="/" text="Home" />
58
+ <div data-island="counter" data-island-props='{"start":0}'></div>
59
+ </section>
60
+ ```
61
+
62
+ | Özellik | Yazım |
63
+ | --- | --- |
64
+ | Kaçışlı metin | `{{ expr }}` |
65
+ | Ham HTML | `{{{ expr }}}` |
66
+ | Koşul | `{#if expr}` … `{#else}` … `{/if}` |
67
+ | Döngü | `{#each list as item}` veya `as item, i` |
68
+ | Include | `{#include "partials/header"}` (derlenmiş `.jsk`) |
69
+ | Bileşen | PascalCase etiket; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
70
+ | Yerleşikler | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
71
+
72
+ İfade dili kasıtlı olarak dardır (erişim, karşılaştırma, ternary, `.length`).
73
+ Atama ve rastgele fonksiyon çağrısı yok — mantık controller veya JS bileşende
74
+ kalır.
75
+
76
+ ### EJS ile birlikte yaşam
77
+
78
+ Aynı `view` id için derlenmiş `.jsk` varsa o kullanılır; yoksa `.ejs` dosyası
79
+ EJS ile render edilir. Mevcut uygulamalar değişmeden çalışır. `jskelet init`
80
+ yeni iskeleti `.jsk` ile kurar.
81
+
82
+ ## EJS motoru (legacy)
83
+
84
+ EJS hâlâ desteklenir. Motor ilk render'da bir kez kurulur; bileşen taraması
85
+ dosya sistemine dokunduğu için her istekte yapılamaz ve config yüklenmeden
86
+ hesaplanamaz.
34
87
 
35
88
  Ayarlar:
36
89
 
@@ -52,8 +105,9 @@ normal akışta gerekmez.
52
105
  1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
53
106
  dizininin üst dizinine** göre çözülür: `views` varsayılansa
54
107
  `layout: "views/ozel.ejs"` → `<root>/views/ozel.ejs`.
55
- 2. Verilmemişse `views/layout.ejs` varsa o kullanılır.
56
- 3. O da yoksa framework'ün kendi minimal layout'u kullanılır
108
+ 2. Verilmemişse `views/layout.jsk` (derlenmiş) varsa o kullanılır.
109
+ 3. Yoksa `views/layout.ejs` varsa o kullanılır.
110
+ 4. O da yoksa framework'ün kendi minimal layout'u kullanılır
57
111
  (`node_modules/jskelet/src/templates/layout.ejs`, ayrıca
58
112
  `jskelet/layout` belirteciyle de erişilebilir).
59
113
 
@@ -150,12 +150,14 @@ göre çözülür ve içeride mutlak yola çevrilir.
150
150
 
151
151
  | Anahtar | Varsayılan | İçeriği |
152
152
  | --- | --- | --- |
153
- | `views` | `"views"` | EJS layout, sayfalar, bileşenler |
153
+ | `views` | `"views"` | Layout, sayfalar, bileşenler (klasik kök; `.jsk` / `.ejs`) |
154
+ | `features` | `"features"` | Feature-first dilimler (`<name>/{server,views,client}`) |
155
+ | `shared` | `"shared"` | Özellikler arası paylaşılan server/views/client |
154
156
  | `public` | `"public"` | Statik dosyalar; build çıktısı da buraya yazılır |
155
157
  | `client` | `"client"` | Island runtime kaynakları ve entry'ler |
156
158
  | `routes` | `"routes"` | Route modülleri |
157
159
  | `styles` | `"styles/globals.css"` | Tailwind/PostCSS giriş **dosyası** |
158
- | `generated` | `".jskelet"` | `manifest.json`, `metafile.json`, `images.json` |
160
+ | `generated` | `".jskelet"` | `manifest.json`, `templates/`, `metafile.json`, `images.json` |
159
161
 
160
162
  `styles` bir dosya yolu olduğu hâlde aynı çözümlemeden geçer; ayrı bir alan
161
163
  tutmaya değmiyor.
@@ -200,8 +202,8 @@ Layout `.ejs` dosyasının yolu. Verilen değer **views dizininin üst dizinine*
200
202
  göre çözülür, yani varsayılan `views` ile `"views/ozel.ejs"` →
201
203
  `<root>/views/ozel.ejs`.
202
204
 
203
- Verilmezse sırayla: `views/layout.ejs` varsa o, yoksa framework'ün minimal
204
- layout'u. Ayrıntı: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
205
+ Verilmezse sırayla: `views/layout.jsk`, `views/layout.ejs`, yoksa framework'ün
206
+ minimal layout'u. Ayrıntı: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
205
207
 
206
208
  ## `routes`
207
209
 
@@ -467,7 +469,7 @@ Phosphor SVG sprite üretimi.
467
469
 
468
470
  | Değer | Sonuç |
469
471
  | --- | --- |
470
- | `{}` (varsayılan) | Sprite üretilir; taranan dizinler `["views", "client", "routes", "lib"]` |
472
+ | `{}` (varsayılan) | Sprite üretilir; taranan dizinler `["views", "client", "routes", "lib", "features", "shared"]` |
471
473
  | `{ scan: [...] }` | Taranan dizinler değiştirilir |
472
474
  | `false` | Sprite adımı tamamen atlanır |
473
475
 
@@ -768,8 +770,8 @@ mevcut davranışını korur. Açıldığında HTTP access log ile framework ola
768
770
  | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink'i |
769
771
  | `s3.bucket` | `string \| null` | `null` | Bucket ya da `bucket/prefix/…` yolu; `JSKELET_LOG_BUCKET` ezer |
770
772
  | `s3.prefix` | `string` | `"jskelet/logs/"` | Nesne anahtarı öneki (yolda verilmediyse) |
771
- | `s3.region` | `string \| null` | `null` | Bölge; verilmezse `JSKELET_S3_REGION`, endpoint varken `auto` |
772
- | `s3.endpoint` | `string \| null` | `null` | R2 / MinIO API adresi; `JSKELET_S3_API_URL` ezer |
773
+ | `s3.region` | `string \| null` | `"auto"` | Bölge; verilmezse `JSKELET_S3_REGION`, yoksa `auto` |
774
+ | `s3.endpoint` | `string \| null` | `null` | S3-uyumlu API adresi; `JSKELET_S3_API_URL` ezer |
773
775
  | `s3.flushIntervalMs` | `number` | `5000` | Batch flush aralığı |
774
776
  | `s3.maxBatch` | `number` | `100` | Bu kadar satırda erken flush |
775
777
 
@@ -975,12 +977,14 @@ basılmaz.
975
977
  | `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
976
978
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | Ayarlıysa token taşımayan her isteğe 404 döner. Isıtma token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
977
979
  | `JSKELET_ADMIN` | `createApp` | — | Ayarlıysa yönetim panelini açar; `0` config'te açık olan paneli kapatır. Env config'i ezer, çünkü panel genelde bir arıza sırasında tek seferlik açılır. [06](./06-cache.md) |
978
- | `JSKELET_LOG_BUCKET` | `logs.s3` | — | S3 bucket ya da `ayberkenis/jskelet/logs` gibi `bucket/prefix` yolu; `logs.s3.bucket`'ı ezer (`JSKELET_S3_BUCKET` yedek ad) |
979
- | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | S3 PutObject imzası. Yoksa ve `s3.enabled` ise sink kapanır |
980
- | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | S3 imza sırrı |
981
- | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Geçici credential'lar için isteğe bağlı |
982
- | `JSKELET_S3_REGION` | `logs.s3` | — | `logs.s3.region` verilmezse; endpoint varken varsayılan `auto` |
983
- | `JSKELET_S3_API_URL` | `logs.s3` | — | R2 / MinIO endpoint; `logs.s3.endpoint`'i ezer |
980
+ | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log hedefi: bucket ya da `bucket/prefix` yolu. Credential ile birlikte varsa sink otomatik açılır |
981
+ | `JSKELET_S3_BUCKET` | `logs.s3` | — | `JSKELET_LOG_BUCKET` yoksa bucket; `JSKELET_S3_KEY_PREFIX` ile birleşir |
982
+ | `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | `JSKELET_S3_BUCKET` ile kullanılır (`bucket/prefix`) |
983
+ | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | PutObject imzası |
984
+ | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | İmza sırrı (`JSKELET_S3_ACCESS_SECRET` yedek ad) |
985
+ | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Geçici credential için isteğe bağlı |
986
+ | `JSKELET_S3_REGION` | `logs.s3` | `auto` | Verilmezse `auto` |
987
+ | `JSKELET_S3_API_URL` | `logs.s3` | — | S3-uyumlu endpoint; `logs.s3.endpoint`'i ezer |
984
988
  | `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache yüzeyi | — | API token. Verilene kadar CDN purge'ü ve edge analitiği kapalıdır; config'teki `apiToken`'ı ezer. Token hiçbir cevapta dönmez. [06](./06-cache.md) |
985
989
  | `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
986
990
  | `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
package/docs/08-build.md CHANGED
@@ -12,6 +12,7 @@ davranışı burada. Çıktının çalışma anında nasıl servis edildiği
12
12
  ## Hat ve sırası
13
13
 
14
14
  ```
15
+ 0. Templates .jsk → .jskelet/templates/*.mjs (her zaman; dosya yoksa no-op)
15
16
  1. Fonts config.fonts varsa
16
17
  2. Icon sprite config.icons !== false ise
17
18
  3. CSS styles giriş dosyası varsa
@@ -21,6 +22,10 @@ davranışı burada. Çıktının çalışma anında nasıl servis edildiği
21
22
  7. Precompress watch değilse
22
23
  ```
23
24
 
25
+ Şablon derlemesi asset taramasından **önce** biter; istek yolunda parse yoktur.
26
+ Tailwind `@source` ve ikon taraması kaynak `.jsk` dosyalarını okur (üretilmiş
27
+ `.mjs` değil).
28
+
24
29
  Sıra rastgele değil:
25
30
 
26
31
  - **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
@@ -25,6 +25,9 @@ JSkelet puts this observation at the centre of the architecture:
25
25
  independent "island", with its own module, at its own time.
26
26
  3. **Page production is cached.** There is no point in producing the same HTML
27
27
  again on every request; a memory cache with a TTL takes the place of ISR.
28
+ 4. **Templates are compiled at build time (`.jsk`).** No request-time parsing;
29
+ EJS remains as a legacy path. Features may co-locate under
30
+ `features/<name>/{server,views,client}` — route URLs stay explicit.
28
31
 
29
32
  ## The path of a request
30
33
 
@@ -65,10 +65,25 @@ export default {
65
65
  };
66
66
  ```
67
67
 
68
- **2. If there is no list, the `routes/` directory is scanned alphabetically.**
69
- The scan is recursive (subdirectories included), only `.js` and `.mjs` files are
70
- picked up, and files whose name begins with `_` are skipped (for shared modules
71
- like `_helpers.js`).
68
+ **2. If there is no list, the `routes/` directory is scanned alphabetically**,
69
+ then each `features/<name>/index.js` (or `.mjs`) is appended alphabetically.
70
+ The scan under `routes/` is recursive (subdirectories included), only `.js` and
71
+ `.mjs` files are picked up, and files whose name begins with `_` are skipped
72
+ (for shared modules like `_helpers.js`).
73
+
74
+ Feature-first layout is optional:
75
+
76
+ ```
77
+ features/markets/
78
+ index.js # register(app, api) — URLs are still explicit
79
+ server/
80
+ views/pages/… # .jsk or .ejs
81
+ views/components/
82
+ client/ # islands; register from client/entries
83
+ ```
84
+
85
+ `jskelet generate feature|page|island` scaffolds this. There is no filesystem
86
+ URL routing.
72
87
 
73
88
  In that case, give the file names a numeric prefix:
74
89
 
@@ -21,18 +21,69 @@ route(controller)
21
21
  │ renderView(page.view, { …data, metadata }), → body
22
22
  │ hooks.layoutContext({ pathname, metadata }), → context
23
23
  │ ])
24
- └─ layout.ejs render → full HTML
24
+ └─ layout (.jsk compiled or .ejs) → full HTML
25
25
  ```
26
26
 
27
27
  The layout context and the body are produced **in parallel**. The reason comes
28
28
  from measurement: in most projects navigation comes from upstream, and waiting
29
29
  for it in sequence with the body render adds needless latency to every page.
30
30
 
31
- ## The EJS engine
31
+ ## `.jsk` — build-time compiled templates
32
32
 
33
- The engine is set up once on the first render; the component scan touches the
34
- file system, so it cannot be done on every request and cannot be computed
35
- before the config is loaded.
33
+ New apps default to `.jsk`. At build time they become normal ESM modules under
34
+ `.jskelet/templates/*.mjs`. There is **no request-time parsing, `eval`, or
35
+ `new Function`**. Production path:
36
+
37
+ ```
38
+ controller data → imported render(data, helpers) → HTML
39
+ ```
40
+
41
+ ### Syntax summary
42
+
43
+ ```html
44
+ <section class="wrapper">
45
+ <h1>{{ title }}</h1>
46
+ <div>{{{ trustedHtml }}}</div>
47
+
48
+ {#if items.length}
49
+ <List :items="items" />
50
+ {#else}
51
+ <p>Empty</p>
52
+ {/if}
53
+
54
+ {#each items as item, i}
55
+ <li data-i="{{ i }}">{{ item }}</li>
56
+ {/each}
57
+
58
+ <Link href="/" text="Home" />
59
+ <div data-island="counter" data-island-props='{"start":0}'></div>
60
+ </section>
61
+ ```
62
+
63
+ | Feature | Form |
64
+ | --- | --- |
65
+ | Escaped text | `{{ expr }}` |
66
+ | Raw HTML | `{{{ expr }}}` |
67
+ | Conditional | `{#if expr}` … `{#else}` … `{/if}` |
68
+ | Loop | `{#each list as item}` or `as item, i` |
69
+ | Include | `{#include "partials/header"}` (compiled `.jsk`) |
70
+ | Component | PascalCase tag; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
71
+ | Built-ins | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
72
+
73
+ The expression language is intentionally small (access, compare, ternary,
74
+ `.length`). No assignments or arbitrary calls — keep logic in controllers or JS
75
+ components.
76
+
77
+ ### Coexistence with EJS
78
+
79
+ If a compiled `.jsk` exists for a view id it wins; otherwise `.ejs` is rendered
80
+ with EJS. Existing apps keep working unchanged. `jskelet init` scaffolds `.jsk`.
81
+
82
+ ## The EJS engine (legacy)
83
+
84
+ EJS remains supported. The engine is set up once on the first render; the
85
+ component scan touches the file system, so it cannot be done on every request
86
+ and cannot be computed before the config is loaded.
36
87
 
37
88
  Settings:
38
89
 
@@ -54,14 +105,15 @@ normal flow because the dev server restarts the process.
54
105
  1. `jskelet.config.mjs` → if `layout` is given, it is used. The path is
55
106
  resolved relative to the **parent directory of the views directory**: if
56
107
  `views` is the default, `layout: "views/custom.ejs"` → `<root>/views/custom.ejs`.
57
- 2. If it is not given and `views/layout.ejs` exists, that is used.
58
- 3. If that does not exist either, the framework's own minimal layout is used
108
+ 2. If it is not given and `views/layout.jsk` exists (compiled), that is used.
109
+ 3. Else if `views/layout.ejs` exists, that is used.
110
+ 4. If that does not exist either, the framework's own minimal layout is used
59
111
  (`node_modules/jskelet/src/templates/layout.ejs`, also reachable through the
60
112
  `jskelet/layout` specifier).
61
113
 
62
- The third option exists so that a new project can work with a single route. The
114
+ These fallbacks exist so that a new project can work with a single route. The
63
115
  most practical way to move to your own layout is to copy that file to
64
- `views/layout.ejs`.
116
+ `views/layout.ejs` or author `views/layout.jsk`.
65
117
 
66
118
  ### The framework's default layout
67
119
 
@@ -155,12 +155,14 @@ internally.
155
155
 
156
156
  | Key | Default | Contents |
157
157
  | --- | --- | --- |
158
- | `views` | `"views"` | EJS layout, pages, components |
159
- | `public` | `"public"` | Static files; the build output is written here too |
158
+ | `views` | `"views"` | Layout, pages, components (classic root; `.jsk` / `.ejs`) |
159
+ | `features` | `"features"` | Feature-first slices (`<name>/{server,views,client}`) |
160
+ | `shared` | `"shared"` | Cross-feature server/views/client |
161
+ | `public` | `"public"` | Static files; build output lands here too |
160
162
  | `client` | `"client"` | Island runtime sources and entries |
161
163
  | `routes` | `"routes"` | Route modules |
162
164
  | `styles` | `"styles/globals.css"` | Tailwind/PostCSS entry **file** |
163
- | `generated` | `".jskelet"` | `manifest.json`, `metafile.json`, `images.json` |
165
+ | `generated` | `".jskelet"` | `manifest.json`, `templates/`, `metafile.json`, `images.json` |
164
166
 
165
167
  Even though `styles` is a file path it goes through the same resolution; keeping
166
168
  a separate field for it is not worth it.
@@ -784,8 +786,8 @@ events (`event` / `error`) are written as NDJSON lines to a file and/or S3.
784
786
  | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink |
785
787
  | `s3.bucket` | `string \| null` | `null` | Bucket or a `bucket/prefix/…` path; `JSKELET_LOG_BUCKET` overrides |
786
788
  | `s3.prefix` | `string` | `"jskelet/logs/"` | Object key prefix (when not given in the path) |
787
- | `s3.region` | `string \| null` | `null` | Region; falls back to `JSKELET_S3_REGION`, or `auto` when an endpoint is set |
788
- | `s3.endpoint` | `string \| null` | `null` | R2 / MinIO API URL; `JSKELET_S3_API_URL` overrides |
789
+ | `s3.region` | `string \| null` | `"auto"` | Region; falls back to `JSKELET_S3_REGION`, otherwise `auto` |
790
+ | `s3.endpoint` | `string \| null` | `null` | S3-compatible API URL; `JSKELET_S3_API_URL` overrides |
789
791
  | `s3.flushIntervalMs` | `number` | `5000` | Batch flush interval |
790
792
  | `s3.maxBatch` | `number` | `100` | Flush early after this many lines |
791
793
 
@@ -995,12 +997,14 @@ and no warning is printed.
995
997
  | `JSKELET_SECRET` | `jskelet/cookies` | — | The signed cookie secret. Read when `security.cookieSecret` is not set; if neither exists, the signed cookie API throws. [12](./12-dashboards-and-sessions.md) |
996
998
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | If set, every request without a token gets a 404. Prewarming carries the token as a cookie. [09](./09-dev-tools.md) |
997
999
  | `JSKELET_ADMIN` | `createApp` | — | When set, turns the admin panel on; `0` turns off a panel enabled in the config. The env wins because the panel is usually opened once during an incident. [06](./06-caching.md) |
998
- | `JSKELET_LOG_BUCKET` | `logs.s3` | — | S3 bucket or a `bucket/prefix` path like `ayberkenis/jskelet/logs`; overrides `logs.s3.bucket` (`JSKELET_S3_BUCKET` is an alias) |
999
- | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | Signs S3 PutObject. Missing when `s3.enabled` disables the sink |
1000
- | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | S3 signing secret |
1000
+ | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log target: bucket or `bucket/prefix` path. With credentials, the sink turns on automatically |
1001
+ | `JSKELET_S3_BUCKET` | `logs.s3` | — | Bucket when `JSKELET_LOG_BUCKET` is unset; joins with `JSKELET_S3_KEY_PREFIX` |
1002
+ | `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | Used with `JSKELET_S3_BUCKET` (`bucket/prefix`) |
1003
+ | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | Signs PutObject |
1004
+ | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | Signing secret (`JSKELET_S3_ACCESS_SECRET` is an alias) |
1001
1005
  | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Optional, for temporary credentials |
1002
- | `JSKELET_S3_REGION` | `logs.s3` | — | Used when `logs.s3.region` is unset; defaults to `auto` when an endpoint is set |
1003
- | `JSKELET_S3_API_URL` | `logs.s3` | — | R2 / MinIO endpoint; overrides `logs.s3.endpoint` |
1006
+ | `JSKELET_S3_REGION` | `logs.s3` | `auto` | Defaults to `auto` when unset |
1007
+ | `JSKELET_S3_API_URL` | `logs.s3` | — | S3-compatible endpoint; overrides `logs.s3.endpoint` |
1004
1008
  | `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache surface | — | API token. Until it is set, CDN purging and edge analytics stay off; it overrides `apiToken` in the config. The token is never returned in a response. [06](./06-caching.md) |
1005
1009
  | `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache surface | — | Zone identifier. No Cloudflare endpoint is called unless it is set alongside the token |
1006
1010
  | `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache surface | — | The root for purge URLs. Required when the panel is opened over an internal address |
@@ -12,6 +12,7 @@ build is in [09-dev-tools.md](./09-dev-tools.md).
12
12
  ## The pipeline and its order
13
13
 
14
14
  ```
15
+ 0. Templates .jsk → .jskelet/templates/*.mjs (always; no-op if none)
15
16
  1. Fonts if config.fonts is set
16
17
  2. Icon sprite if config.icons !== false
17
18
  3. CSS if the styles entry file exists
@@ -21,6 +22,10 @@ build is in [09-dev-tools.md](./09-dev-tools.md).
21
22
  7. Precompress if not watch
22
23
  ```
23
24
 
25
+ Template compilation finishes **before** asset scanning; there is no parse on
26
+ the request path. Tailwind `@source` and the icon scan read source `.jsk` files
27
+ (not the generated `.mjs`).
28
+
24
29
  The order is not arbitrary:
25
30
 
26
31
  - **CSS comes after the icon sprite.** The sprite is an asset and produces no
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.3.4",
4
- "description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
3
+ "version": "0.4.0",
4
+ "description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Ayberk Enis",
@@ -40,6 +40,12 @@ if (!child) {
40
40
 
41
41
  log.section("build");
42
42
 
43
+ // Şablonlar asset taramasından önce derlenir; istek anında parse yok.
44
+ await task("Templates", async () => {
45
+ const { buildTemplates } = await import("./tasks/templates.mjs");
46
+ await buildTemplates(config);
47
+ });
48
+
43
49
  /** @type {Record<string, string>} */
44
50
  const manifest = {};
45
51
 
@@ -10,6 +10,10 @@ import { loadConfig } from "../config/index.js";
10
10
 
11
11
  const config = await loadConfig();
12
12
 
13
- if (!fs.existsSync(path.join(config.dirs.generated, "manifest.json"))) {
13
+ const generated = config.dirs.generated;
14
+ const needsAssets = !fs.existsSync(path.join(generated, "manifest.json"));
15
+ const needsTemplates = !fs.existsSync(path.join(generated, "templates", "manifest.json"));
16
+
17
+ if (needsAssets || needsTemplates) {
14
18
  await import("./build.mjs");
15
19
  }
@@ -19,7 +19,7 @@ import { createRequire } from "node:module";
19
19
  import { pruneAssets, writeAsset } from "../paths.mjs";
20
20
  import * as log from "../../log.mjs";
21
21
 
22
- const SCAN_EXTENSIONS = new Set([".ejs", ".js", ".mjs"]);
22
+ const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs"]);
23
23
 
24
24
  /** `icon({ … })` çağrısının tamamı; `name:` ifadesi ayrıca çözümlenir. */
25
25
  const ICON_CALL = /icon\(\s*\{([^}]*)\}/g;
@@ -189,7 +189,7 @@ export async function buildIconSprite(config) {
189
189
  }
190
190
 
191
191
  const scanDirs = (
192
- config.icons?.scan ?? ["views", "client", "routes", "lib"]
192
+ config.icons?.scan ?? ["views", "client", "routes", "lib", "features", "shared"]
193
193
  ).map((dir) => path.resolve(config.root, dir));
194
194
 
195
195
  pruneAssets(["sprite."]);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `.jsk` şablonlarını `.jskelet/templates/` altına derler.
3
+ * Asset pipeline'dan önce çalışır; Tailwind/ikon taraması kaynak `.jsk`'yi okur.
4
+ */
5
+ import { compileAll } from "../../compile/compile-all.js";
6
+ import * as log from "../../log.mjs";
7
+
8
+ /**
9
+ * @param {import('../../config/index.js').ResolvedConfig} config
10
+ * @returns {Promise<number>} Derlenen dosya sayısı.
11
+ */
12
+ export async function buildTemplates(config) {
13
+ const result = await compileAll(config);
14
+ if (result.count === 0) {
15
+ log.line("no .jsk templates");
16
+ } else {
17
+ log.line(`${result.count} template${result.count === 1 ? "" : "s"} → .jskelet/templates`);
18
+ }
19
+ return result.count;
20
+ }