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 +22 -6
- package/bin/jskelet.mjs +20 -7
- package/docs/02-mimari.md +3 -0
- package/docs/03-routing.md +19 -3
- package/docs/04-render-ve-sablonlar.md +60 -6
- package/docs/07-yapilandirma.md +17 -13
- package/docs/08-build.md +5 -0
- package/docs/en/02-architecture.md +3 -0
- package/docs/en/03-routing.md +19 -4
- package/docs/en/04-rendering.md +61 -9
- package/docs/en/07-configuration.md +14 -10
- package/docs/en/08-build.md +5 -0
- package/package.json +2 -2
- package/src/build/build.mjs +6 -0
- package/src/build/ensure-build.mjs +5 -1
- package/src/build/tasks/icons.mjs +2 -2
- package/src/build/tasks/templates.mjs +20 -0
- package/src/compile/codegen.js +332 -0
- package/src/compile/compile-all.js +158 -0
- package/src/compile/errors.js +66 -0
- package/src/compile/expr.js +404 -0
- package/src/compile/index.js +15 -0
- package/src/compile/parse.js +485 -0
- package/src/compile/resolve.js +159 -0
- package/src/config/defaults.js +7 -3
- package/src/config/index.js +71 -13
- package/src/dev-server.mjs +4 -2
- package/src/generate.mjs +163 -0
- package/src/init.mjs +8 -6
- package/src/server/logs/pipeline.js +6 -22
- package/src/server/render.js +153 -40
- package/src/server/router.js +26 -3
- package/src/views/components/loader.js +26 -13
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`
|
|
34
|
-
be a plain bucket or a
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`
|
|
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
|
|
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
|
|
98
|
-
" build
|
|
99
|
-
" start
|
|
100
|
-
" init
|
|
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
|
|
package/docs/03-routing.md
CHANGED
|
@@ -63,9 +63,25 @@ export default {
|
|
|
63
63
|
};
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
**2. Liste yoksa `routes/` dizini alfabetik taranır
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
-
##
|
|
30
|
+
## `.jsk` — build-time derlenmiş şablonlar
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
56
|
-
3.
|
|
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
|
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -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"` |
|
|
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
|
|
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` | `
|
|
772
|
-
| `s3.endpoint` | `string \| null` | `null` |
|
|
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` | — |
|
|
979
|
-
| `
|
|
980
|
-
| `
|
|
981
|
-
| `
|
|
982
|
-
| `
|
|
983
|
-
| `
|
|
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
|
|
package/docs/en/03-routing.md
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
package/docs/en/04-rendering.md
CHANGED
|
@@ -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
|
|
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
|
-
##
|
|
31
|
+
## `.jsk` — build-time compiled templates
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
58
|
-
3.
|
|
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
|
-
|
|
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"` |
|
|
159
|
-
| `
|
|
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` | `
|
|
788
|
-
| `s3.endpoint` | `string \| null` | `null` |
|
|
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` | — |
|
|
999
|
-
| `
|
|
1000
|
-
| `
|
|
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` |
|
|
1003
|
-
| `JSKELET_S3_API_URL` | `logs.s3` | — |
|
|
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 |
|
package/docs/en/08-build.md
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "A framework that feels like no framework: Express 5 + EJS
|
|
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",
|
package/src/build/build.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|