jskelet 0.5.5 → 0.6.1

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 (156) hide show
  1. package/AGENTS.md +19 -15
  2. package/CHANGELOG.md +165 -15
  3. package/README.md +16 -21
  4. package/bin/jskelet.mjs +23 -9
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +10 -4
  7. package/docs/03-routing.md +14 -7
  8. package/docs/04-render-ve-sablonlar.md +60 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/06-cache.md +18 -7
  11. package/docs/07-yapilandirma.md +69 -27
  12. package/docs/08-build.md +15 -9
  13. package/docs/09-dev-araclari.md +22 -8
  14. package/docs/10-dagitim.md +14 -13
  15. package/docs/11-tasima.md +51 -17
  16. package/docs/12-panel-ve-oturum.md +10 -4
  17. package/docs/README.md +10 -33
  18. package/docs/en/01-getting-started.md +4 -3
  19. package/docs/en/02-architecture.md +12 -6
  20. package/docs/en/03-routing.md +15 -8
  21. package/docs/en/04-rendering.md +71 -59
  22. package/docs/en/05-islands.md +13 -8
  23. package/docs/en/06-caching.md +21 -7
  24. package/docs/en/07-configuration.md +69 -29
  25. package/docs/en/08-build.md +16 -10
  26. package/docs/en/09-dev-tools.md +24 -8
  27. package/docs/en/10-deployment.md +14 -14
  28. package/docs/en/11-migration.md +51 -16
  29. package/docs/en/12-dashboards-and-sessions.md +9 -4
  30. package/docs/en/README.md +10 -35
  31. package/package.json +48 -13
  32. package/src/build/tasks/client.mjs +91 -10
  33. package/src/build/tasks/icons.mjs +11 -1
  34. package/src/client/index.js +2 -2
  35. package/src/compile/codegen.js +4 -0
  36. package/src/compile/compile-all.js +12 -21
  37. package/src/compile/expr.js +5 -0
  38. package/src/compile/parse.js +64 -8
  39. package/src/compile/resolve.js +3 -0
  40. package/src/config/defaults.js +48 -5
  41. package/src/config/index.js +138 -27
  42. package/src/dev-server.mjs +26 -3
  43. package/src/http/cookies-entry.js +1 -0
  44. package/src/http/cookies.js +18 -0
  45. package/src/logo.png +0 -0
  46. package/src/migrate/apply.mjs +262 -0
  47. package/src/migrate/babel.mjs +79 -0
  48. package/src/migrate/classify.mjs +155 -0
  49. package/src/migrate/config.mjs +126 -0
  50. package/src/migrate/fs-walk.mjs +191 -0
  51. package/src/migrate/parse.mjs +26 -0
  52. package/src/migrate/scan.mjs +177 -0
  53. package/src/migrate/transform/expr-source.mjs +168 -0
  54. package/src/migrate/transform/island.mjs +67 -0
  55. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  56. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  57. package/src/migrate/transform/page-split.mjs +435 -0
  58. package/src/migrate/write.mjs +81 -0
  59. package/src/migrate.mjs +171 -0
  60. package/src/server/auth/handoff.js +94 -11
  61. package/src/server/create-app.js +37 -10
  62. package/src/server/ejs-adapter.js +59 -0
  63. package/src/server/html-cache.js +178 -32
  64. package/src/server/image-optimizer.js +94 -26
  65. package/src/server/middleware/dev-gate.js +21 -8
  66. package/src/server/middleware/robots-txt.js +341 -0
  67. package/src/server/port-guard.js +255 -0
  68. package/src/server/prewarm.js +137 -51
  69. package/src/server/render.js +30 -10
  70. package/src/server/status-page.js +105 -4
  71. package/src/start.mjs +18 -3
  72. package/src/templates/layout.ejs +8 -28
  73. package/src/templates/layout.jsk +30 -0
  74. package/src/templates/layout.render.js +41 -0
  75. package/src/views/helpers/tags.js +86 -3
  76. package/types/build/resolve-peer.d.mts +13 -0
  77. package/types/client/dom.d.ts +55 -0
  78. package/types/client/form.d.ts +19 -0
  79. package/types/client/index.d.ts +20 -0
  80. package/types/client/registry.d.ts +53 -0
  81. package/types/client/safe-image.d.ts +19 -0
  82. package/types/client/shared-cookie.d.ts +82 -0
  83. package/types/client/store.d.ts +18 -0
  84. package/types/client/swap.d.ts +46 -0
  85. package/types/compile/codegen.d.ts +32 -0
  86. package/types/compile/compile-all.d.ts +42 -0
  87. package/types/compile/errors.d.ts +30 -0
  88. package/types/compile/expr.d.ts +67 -0
  89. package/types/compile/index.d.ts +10 -0
  90. package/types/compile/parse.d.ts +82 -0
  91. package/types/compile/resolve.d.ts +46 -0
  92. package/types/compile/scan-exports.d.ts +9 -0
  93. package/types/config/defaults.d.ts +477 -0
  94. package/types/config/index.d.ts +304 -0
  95. package/types/config/pattern.d.ts +38 -0
  96. package/types/http/control-flow.d.ts +45 -0
  97. package/types/http/cookies-entry.d.ts +5 -0
  98. package/types/http/cookies.d.ts +113 -0
  99. package/types/http/request-cache.d.ts +13 -0
  100. package/types/http/request-context.d.ts +67 -0
  101. package/types/http/shared-cookie.d.ts +73 -0
  102. package/types/index.d.ts +30 -0
  103. package/types/log.d.mts +153 -0
  104. package/types/server/admin/actions.d.ts +16 -0
  105. package/types/server/admin/auth.d.ts +52 -0
  106. package/types/server/admin/event-log.d.ts +38 -0
  107. package/types/server/admin/gate.d.ts +43 -0
  108. package/types/server/admin/inventory.d.ts +40 -0
  109. package/types/server/admin/mount.d.ts +6 -0
  110. package/types/server/admin/router.d.ts +6 -0
  111. package/types/server/admin/snapshot.d.ts +6 -0
  112. package/types/server/assets.d.ts +47 -0
  113. package/types/server/auth/handoff.d.ts +12 -0
  114. package/types/server/cache-deps.d.ts +16 -0
  115. package/types/server/cache-vary.d.ts +30 -0
  116. package/types/server/cloudflare.d.ts +163 -0
  117. package/types/server/create-app.d.ts +25 -0
  118. package/types/server/data-cache.d.ts +116 -0
  119. package/types/server/dev/devtools.d.ts +44 -0
  120. package/types/server/dev/report.d.ts +229 -0
  121. package/types/server/dev/socket.d.ts +17 -0
  122. package/types/server/dev/version-check.d.mts +15 -0
  123. package/types/server/ejs-adapter.d.ts +11 -0
  124. package/types/server/head-hints.d.ts +40 -0
  125. package/types/server/html-cache.d.ts +207 -0
  126. package/types/server/image-optimizer.d.ts +68 -0
  127. package/types/server/logs/access-middleware.d.ts +7 -0
  128. package/types/server/logs/file-sink.d.ts +17 -0
  129. package/types/server/logs/pipeline.d.ts +37 -0
  130. package/types/server/logs/s3-put.d.ts +85 -0
  131. package/types/server/logs/s3-sink.d.ts +26 -0
  132. package/types/server/metadata.d.ts +38 -0
  133. package/types/server/middleware/compression.d.ts +17 -0
  134. package/types/server/middleware/csrf.d.ts +4 -0
  135. package/types/server/middleware/dev-gate.d.ts +2 -0
  136. package/types/server/middleware/headers.d.ts +2 -0
  137. package/types/server/middleware/redirects.d.ts +2 -0
  138. package/types/server/middleware/robots-txt.d.ts +33 -0
  139. package/types/server/middleware/static-precompressed.d.ts +5 -0
  140. package/types/server/middleware/trailing-slash.d.ts +11 -0
  141. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  142. package/types/server/og-image.d.ts +149 -0
  143. package/types/server/port-guard.d.ts +50 -0
  144. package/types/server/prewarm.d.ts +131 -0
  145. package/types/server/redis.d.ts +163 -0
  146. package/types/server/render.d.ts +101 -0
  147. package/types/server/router.d.ts +5 -0
  148. package/types/server/status-page.d.ts +24 -0
  149. package/types/server/upstream-limiter.d.ts +123 -0
  150. package/types/server/upstream-tracking.d.ts +42 -0
  151. package/types/shared/cookie-domain.d.ts +29 -0
  152. package/types/templates/layout.render.d.ts +7 -0
  153. package/types/version.d.mts +10 -0
  154. package/types/views/components/loader.d.ts +5 -0
  155. package/types/views/helpers/html.d.ts +39 -0
  156. package/types/views/helpers/tags.d.ts +127 -0
@@ -123,25 +123,30 @@ will still mount.
123
123
  ```
124
124
  client/
125
125
  ├── entries/
126
- │ ├── main.js the shared bootstrap loaded on every page
126
+ │ ├── main.js shared bootstrap on every page (or main.ts)
127
127
  │ └── chart.js only on the pages that ask for it
128
128
  └── islands/
129
- ├── counter.js
129
+ ├── counter.ts .js or .ts
130
130
  └── chart.js
131
131
  ```
132
132
 
133
- **Every file** under `client/entries/*.js` **is an esbuild entry**. `main.js`
134
- is loaded by the layout on every page (if it is in the manifest). Extra entries
135
- are loaded only on the pages that ask for them:
133
+ **Every file** under `client/entries/*.{js,ts,mts}` **is an esbuild entry**.
134
+ `main.js` (or `main.ts`) is loaded by the layout on every page (if it is in the
135
+ manifest). Extra entries are loaded only on the pages that ask for them. Two
136
+ extensions for the same stem (`main.js` + `main.ts`) fail the build.
136
137
 
137
138
  ```js
138
- // controller
139
+ // controller — the manifest key is always *.js
139
140
  return { view: "pages/markets", entries: ["chart.js"] };
140
141
  ```
141
142
 
142
143
  The layout resolves every name in the `entries` array with `asset(entry)` and
143
- emits a `<script type="module">`. The name is the manifest key, that is, the
144
- file name itself (`chart.js`), not its hashed form.
144
+ emits a `<script type="module">`. The name is the manifest key (`chart.js`);
145
+ even when the source is `chart.ts`, the unhashed key stays `.js`.
146
+
147
+ Shared `@/lib` modules imported on the server must stay **`.js`** — the Node
148
+ runtime does not resolve `.ts`; TypeScript is compiled only on the esbuild
149
+ client path.
145
150
 
146
151
  Code splitting (`splitting: true`) is on: modules shared by two entries end up
147
152
  in a common chunk and are not downloaded twice.
@@ -231,7 +231,12 @@ price is acceptable, because live fields such as prices are updated on the
231
231
  client over WebSocket.
232
232
 
233
233
  The store is an LRU: an accessed entry is moved to the end, and once the limit
234
- (`cache().maxEntries`, 500 by default) is exceeded the oldest is evicted.
234
+ (`cache().maxEntries`, 500 by default) is exceeded the oldest is evicted. Config
235
+ may ask for more than 500; it **cannot exceed 800** — a higher value is clamped
236
+ to 800 with a warning. Separately, in-process HTML strings plus compressed
237
+ bodies **cannot exceed 256 MB**. A fat page or a `vary.host` copy that is still
238
+ under the count limit is evicted by this budget too. A single page larger than
239
+ 256 MB is not stored; that response is still sent.
235
240
 
236
241
  ## What gets written to the cache
237
242
 
@@ -1145,9 +1150,9 @@ export default {
1145
1150
  html: { "/": 60, "/news/:slug": 300 },
1146
1151
  prewarm: {
1147
1152
  onVisit: {
1148
- perPage: 20, // at most this many links per page
1149
- concurrency: 2, // optional
1150
- rps: 4, // optional; 0 = unlimited
1153
+ perPage: 20, // at most this many links per page; ceiling 20
1154
+ concurrency: 2, // ceiling 2
1155
+ rps: 2, // ceiling 2; 0 is clamped to 2 as well
1151
1156
  },
1152
1157
  },
1153
1158
  };
@@ -1163,7 +1168,15 @@ Rules:
1163
1168
  degraded or `no-store` responses do not extract links.
1164
1169
  - The warmer's own UA (`brand.prewarmUserAgent`) does not trigger — no crawl
1165
1170
  loop.
1166
- - Paths that are already fresh are not enqueued.
1171
+ - Paths that are already fresh are not enqueued. Real keys look like
1172
+ `h=host|/path?`; the check sees the vary prefix and the trailing `?`.
1173
+ With `vary.host`, only this request's host counts as fresh.
1174
+ - The pending queue holds at most 64 paths; links beyond that are left for a
1175
+ later response.
1176
+ - `perPage` 20, `rps` 2 and `concurrency` 2 are ceilings. A higher value (and
1177
+ `rps: 0`) is clamped with a warning. Warm requests stay on loopback; when
1178
+ `vary.host` is on, the public host is sent as `x-forwarded-host`, so a second
1179
+ `h=127.0.0.1` entry is not created.
1167
1180
  - `nofollow`, `target="_blank"`, `data-no-prefetch`, `prewarmSkip` and
1168
1181
  `navigation.exclude` share the same exemptions as Speculation Rules.
1169
1182
  - Query strings are not warmed (default cache policy treats query as dynamic).
@@ -1308,8 +1321,9 @@ The requests go out with the headers `user-agent: jskelet-prewarm`
1308
1321
  (`brand.prewarmUserAgent`) and `accept-encoding: br, gzip`; the second one so
1309
1322
  that the compressed body enters the cache too.
1310
1323
 
1311
- If `DEV_TOKEN` is set, the warm-up carries the token as a cookie; otherwise the
1312
- dev gate returns 404 for all pages and the cache never fills.
1324
+ If the dev gate is on, the warm-up carries the token as a cookie; otherwise the
1325
+ gate returns 404 for all pages and the cache never fills. `DEV_TOKEN` alone
1326
+ does not turn the gate on.
1313
1327
 
1314
1328
  The request list in the dev panel and the terminal filter out requests carrying
1315
1329
  `prewarmUserAgent`: so that hundreds of warm-up requests do not flood the view.
@@ -55,7 +55,7 @@ export default {
55
55
  lang: "tr",
56
56
  },
57
57
 
58
- layout: "views/layout.ejs",
58
+ layout: "views/layout.jsk",
59
59
  routes: ["./routes/10-pages.mjs", "./routes/99-catch-all.mjs"],
60
60
  trailingSlash: false,
61
61
 
@@ -213,27 +213,32 @@ cross-subdomain handoff bridge for a short session id.
213
213
 
214
214
  | Field | Type | Default | Meaning |
215
215
  | --- | --- | --- | --- |
216
- | `crossSubdomainHandoff` | `boolean \| object` | `false` | `true` or `{ ttlSeconds?, path?, maxValueBytes? }` → `POST /_jskelet/auth/handoff` + `?handoff=` redeem |
216
+ | `crossSubdomainHandoff` | `boolean \| object` | `false` | When on: `POST /_jskelet/auth/handoff` + `?handoff=` redeem. Object: `allowedCookieNames` (required), `ttlSeconds?`, `path?`, `maxValueBytes?`, `maxPendingTickets?`, `maxMintsPerIpPerMinute?` |
217
217
 
218
218
  ```js
219
219
  auth: {
220
- crossSubdomainHandoff: { ttlSeconds: 60 },
220
+ crossSubdomainHandoff: {
221
+ allowedCookieNames: ["sid"],
222
+ ttlSeconds: 60,
223
+ },
221
224
  },
222
225
  ```
223
226
 
224
- Details and the `window.name` fallback:
225
- [12-dashboards-and-sessions.md](./12-dashboards-and-sessions.md).
227
+ The mint endpoint is mounted **after** the CSRF middleware (origin checks).
228
+ Cookie names outside the allowlist or that are not RFC 6265 tokens get 400.
229
+ Details: [12-dashboards-and-sessions.md](./12-dashboards-and-sessions.md).
226
230
 
227
231
  ## `layout`
228
232
 
229
233
  **Type:** `string` — **Default:** none (automatic resolution)
230
234
 
231
- Path of the layout `.ejs` file. The value given is resolved relative to the
232
- **parent directory of the views directory**, so with the default `views`,
233
- `"views/custom.ejs"` → `<root>/views/custom.ejs`.
235
+ Path of the layout file (`.jsk` or legacy `.ejs`). The value given is resolved
236
+ relative to the **parent directory of the views directory**, so with the default
237
+ `views`, `"views/custom.jsk"` → `<root>/views/custom.jsk`.
234
238
 
235
- If not given, in order: `views/layout.ejs` if it exists, otherwise the
236
- framework's minimal layout. Details: [04-rendering.md](./04-rendering.md).
239
+ If not given, in order: `views/layout.jsk`, `views/layout.ejs` (legacy),
240
+ otherwise the framework's `src/templates/layout.jsk` default. Details:
241
+ [04-rendering.md](./04-rendering.md).
237
242
 
238
243
  ## `routes`
239
244
 
@@ -293,6 +298,21 @@ static: {
293
298
  }
294
299
  ```
295
300
 
301
+ ## `devGate`
302
+
303
+ **Type:** `boolean` — **Default:** `false`
304
+
305
+ Hides an environment that is not public yet. **`DEV_TOKEN` alone does not lock
306
+ the site.** A shared task definition can carry the same variable into
307
+ production; visitors are not required to present a token, and the site stays
308
+ open.
309
+
310
+ Turn the gate on with `devGate: true` or `DEV_GATE=1`. Then a request without
311
+ the token gets a 404. `DEV_GATE=0` also turns off a gate the config enabled.
312
+ If the token is empty, requests still pass even when the gate is on.
313
+
314
+ Details: [09-dev-tools.md](./09-dev-tools.md).
315
+
296
316
  ## `devGateBypass`
297
317
 
298
318
  **Type:** `string[]` — **Default:**
@@ -300,8 +320,7 @@ static: {
300
320
 
301
321
  **Exact** paths the dev gate never closes off under any circumstances (not a
302
322
  prefix, an exact match). This is so that the health check and the robots files
303
- stay reachable in an environment where `DEV_TOKEN` is set. If provided, it
304
- replaces the default.
323
+ stay reachable while the gate is on. If provided, it replaces the default.
305
324
 
306
325
  Details: [09-dev-tools.md](./09-dev-tools.md).
307
326
 
@@ -335,7 +354,7 @@ field reference.
335
354
  | `trustProxy` | `boolean` | `true` | Express's `trust proxy` setting. Needed behind a reverse proxy for the correct protocol and client IP. |
336
355
  | `cookieSecret` | `string \| null` | `null` | The signed cookie secret. When absent, `JSKELET_SECRET` is read. |
337
356
  | `csrf.enabled` | `boolean` | `true` | The origin / `Sec-Fetch-Site` check. |
338
- | `csrf.token` | `boolean` | `false` | The double-submit token layer. |
357
+ | `csrf.token` | `boolean` | `false` | The double-submit token layer. **Turn on** for cookie-session forms. |
339
358
  | `csrf.allowedOrigins` | `string[]` | `[]` | Origins accepted alongside our own host. |
340
359
  | `csrf.exclude` | `string[]` | `[]` | Paths exempt from the check; `source` pattern syntax. |
341
360
  | `csrf.cookieName` | `string` | `"csrf_token"` | Name of the token cookie. |
@@ -343,14 +362,17 @@ field reference.
343
362
  | `csrf.headerName` | `string` | `"x-csrf-token"` | Header the token is also accepted in. |
344
363
 
345
364
  `trustProxy` should be **turned off** on a server exposed directly to the
346
- internet: while it is on, a client can forge its own `X-Forwarded-For` and rate
347
- limiting or audit logs see the wrong address.
365
+ internet: while it is on, a client can forge `X-Forwarded-For` /
366
+ `X-Forwarded-Proto` / Host, and rate limits, admin IP allowlists, Secure
367
+ cookies, and cache `vary.host` see the wrong address. Behind a reverse proxy
368
+ (nginx, Caddy, Cloudflare), `true` is the right default.
348
369
 
349
370
  The CSRF check only rejects requests that are **known** to be cross-site — when
350
371
  `Origin` does not match or `Sec-Fetch-Site: cross-site` arrives. If neither is
351
372
  present the request passes, because browsers always send `Origin` on a
352
- cross-origin POST while webhooks never do. Even so, listing non-browser
353
- endpoints in `csrf.exclude` makes the intent readable.
373
+ cross-origin POST while webhooks never do. For cookie-session dashboards,
374
+ enable `csrf.token: true` and `csrfField()` as a second layer; put webhook
375
+ paths in `csrf.exclude`.
354
376
 
355
377
  ## `navigation`
356
378
 
@@ -433,7 +455,8 @@ body > footer { view-transition-name: site-footer; }
433
455
  ::view-transition-new(root) { animation-duration: 180ms; }
434
456
  ```
435
457
 
436
- A working version lives in `examples/marketing/styles/globals.css`.
458
+ Copy the Tailwind `@source` directives and view-transition CSS into your own
459
+ app's `styles/globals.css`; the blocks above are a starting point.
437
460
 
438
461
  **If you use CSP**, the rules are emitted as an inline
439
462
  `<script type="speculationrules">`; your `script-src` policy needs to allow it.
@@ -555,8 +578,9 @@ When `remote.allowHosts` is set, also proxies remote images at runtime
555
578
 
556
579
  If `false` is given, neither surface runs. The build step requires `sharp` and
557
580
  never runs on a watch pass. With remote enabled, `sharp` is also needed at
558
- **runtime**; without it the optimizer 302-redirects to the source URL. Details:
559
- [08-build.md](./08-build.md).
581
+ **runtime**; without it the optimizer 302-redirects to the source URL. Fetch
582
+ does not auto-follow redirects: every hop is re-checked against `allowHosts`
583
+ and private addresses. Details: [08-build.md](./08-build.md).
560
584
 
561
585
  ```js
562
586
  images: {
@@ -588,7 +612,10 @@ that is not in the list returns `undefined` instead of crashing.
588
612
  clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
589
613
  ```
590
614
 
591
- **Do not put secrets here** — the values sit in the bundle in plain text.
615
+ **Do not put secrets here** — the values sit in the bundle in plain text. Keys
616
+ whose names look secret-like (`SECRET`, `PASSWORD`, `TOKEN`, `API_KEY`,
617
+ `PRIVATE`, …) are **rejected at build time** (`PUBLIC` / `PUBLISHABLE` names
618
+ are exempt).
592
619
 
593
620
  ## `headers()`
594
621
 
@@ -598,6 +625,7 @@ clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
598
625
  Response headers by path pattern. The framework only writes long-lived cache
599
626
  headers for static files; every other header (CSP, COOP, HSTS,
600
627
  X-Frame-Options…) comes from here and takes precedence over the defaults.
628
+ Production sites should at least define the security headers below.
601
629
 
602
630
  **All** matching rules are applied (unlike redirects, it does not stop at the
603
631
  first match), in order; if two rules write the same header, the later one wins.
@@ -612,11 +640,18 @@ async headers() {
612
640
  source: "/:path*",
613
641
  headers: [
614
642
  { key: "X-Frame-Options", value: "SAMEORIGIN" },
643
+ { key: "X-Content-Type-Options", value: "nosniff" },
615
644
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
645
+ {
646
+ key: "Permissions-Policy",
647
+ value: "camera=(), microphone=(), geolocation=()",
648
+ },
616
649
  {
617
650
  key: "Content-Security-Policy",
618
- value: "default-src 'self'; img-src 'self' https://cdn.example.com data:",
651
+ value: "default-src 'self'; img-src 'self' https://cdn.example.com data:; script-src 'self'",
619
652
  },
653
+ // Only when you terminate HTTPS yourself:
654
+ // { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
620
655
  ],
621
656
  },
622
657
  {
@@ -751,6 +786,10 @@ raising this number burns through memory quickly; trying to solve a site with
751
786
  tens of thousands of paths from here is the wrong layer — the right place is
752
787
  `cache().data`.
753
788
 
789
+ **Ceiling 800.** A higher value is clamped to 800 with a warning at load.
790
+ In-process HTML plus compressed bodies also cannot exceed 256 MB; config
791
+ cannot raise that budget.
792
+
754
793
  ### `cache().data`
755
794
 
756
795
  The upstream data cache (`withDataCache`). Details:
@@ -758,7 +797,7 @@ The upstream data cache (`withDataCache`). Details:
758
797
 
759
798
  | Field | Type | Default | Meaning |
760
799
  | --- | --- | --- | --- |
761
- | `maxEntries` | `number` | `10000` | The LRU entry limit. The limit is high because JSON is tens of times smaller than HTML. |
800
+ | `maxEntries` | `number` | `10000` | The LRU entry limit. The limit is high because JSON is tens of times smaller than HTML. **Ceiling 20,000**; a higher value is clamped with a warning. |
762
801
  | `staleFactor` | `number` | `10` | For how many TTLs an entry stays usable after the TTL expired. `0` → no stale serving. |
763
802
 
764
803
  ### `cache().trackUpstream`
@@ -1003,13 +1042,13 @@ prewarm: {
1003
1042
  | Field | Type | Default | Meaning |
1004
1043
  | --- | --- | --- | --- |
1005
1044
  | `onVisit` | `true \| false \| object` | off | Visit-driven warming |
1006
- | `onVisit.perPage` | `number` | `20` | At most how many links per page (top to bottom) |
1007
- | `onVisit.concurrency` | `number` | same as classic | Parallel workers |
1008
- | `onVisit.rps` | `number` | same as classic | Requests per second cap; `0` unlimited |
1045
+ | `onVisit.perPage` | `number` | `20` | At most how many links per page (top to bottom). **Ceiling 20** |
1046
+ | `onVisit.concurrency` | `number` | `2` | Parallel workers. **Ceiling 2** |
1047
+ | `onVisit.rps` | `number` | `2` | Requests per second cap. **Ceiling 2**; `0` is clamped to 2 as well |
1009
1048
 
1010
1049
  ```js
1011
1050
  prewarm: {
1012
- onVisit: { perPage: 20, rps: 4 },
1051
+ onVisit: { perPage: 20, rps: 2 },
1013
1052
  }
1014
1053
  ```
1015
1054
 
@@ -1106,10 +1145,11 @@ and no warning is printed.
1106
1145
  | Variable | Who reads it | Default | Meaning |
1107
1146
  | --- | --- | --- | --- |
1108
1147
  | `NODE_ENV` | everywhere | `production` (start/build), `development` (dev) | Determines the dev overlay, EJS cache, manifest re-reading, route error behaviour and prewarm defaults. `jskelet dev` sets it itself — `cross-env` is not needed. |
1109
- | `PORT` | `startServer` | `3000` | Port to listen on |
1148
+ | `PORT` | `startServer` | `3000` | Port to listen on. If busy, the process refuses to start; `jskelet start|dev --murder` kills the listener |
1110
1149
  | `HOST` | `startServer` | `::` | Interface to bind to. The default listens dual-stack (IPv6 + IPv4); it falls back to `0.0.0.0` where IPv6 is unavailable |
1111
1150
  | `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) |
1112
- | `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) |
1151
+ | `DEV_GATE` | `devGate` | off | `1` turns the gate on, `0` turns it off even when config enabled it. `DEV_TOKEN` alone does not turn it on. [09](./09-dev-tools.md) |
1152
+ | `DEV_TOKEN` | `devGate`, `prewarm` | — | The secret expected while the gate is on. If it is missing, or the gate is off, the site stays public. Prewarming carries the token as a cookie only while the gate is on. [09](./09-dev-tools.md) |
1113
1153
  | `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) |
1114
1154
  | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log target: bucket or `bucket/prefix` path. With credentials, the sink turns on automatically |
1115
1155
  | `JSKELET_S3_BUCKET` | `logs.s3` | — | Bucket when `JSKELET_LOG_BUCKET` is unset; joins with `JSKELET_S3_KEY_PREFIX` |
@@ -174,8 +174,10 @@ a new utility is written. Changes are coalesced over 120 ms.
174
174
 
175
175
  ## Client JS — esbuild
176
176
 
177
- Every `.js` file inside `client/entries/*.js` is an entry. If the directory does
178
- not exist or is empty, the step is skipped.
177
+ Every source file under `client/entries/*.{js,ts,mts}` is an entry (no `.tsx`).
178
+ The manifest key is always `*.js` (`main.ts` → `main.js`). Multiple extensions
179
+ for the same stem fail the build. If the directory does not exist or is empty,
180
+ the step is skipped.
179
181
 
180
182
  esbuild settings:
181
183
 
@@ -185,7 +187,7 @@ esbuild settings:
185
187
  | `format` | `esm` | `type="module"` scripts |
186
188
  | `target` | `chrome111`, `edge111`, `firefox111`, `safari16.4` | The lower bound of the ESM + dynamic import + `IntersectionObserver` island model; transpiling to anything older grows the output without winning a single visitor |
187
189
  | `minify` | `true` | — |
188
- | `sourcemap` | `true` | Diagnostics in the browser |
190
+ | `sourcemap` | only when `NODE_ENV=development` | Production builds do not publish `.map` files under `public/assets` |
189
191
  | `entryNames` | `[name].[hash]` | `immutable` cache |
190
192
  | `chunkNames` | `chunks/[name].[hash]` | — |
191
193
  | `legalComments` | `none` | — |
@@ -196,17 +198,19 @@ The output lands under `public/assets/js/` and is cleaned first on every pass.
196
198
  ### The `@/` alias
197
199
 
198
200
  On the esbuild side, `@/` resolves to the project root and extension completion
199
- is performed (`.js`, `.mjs`, `.json`, `/index.js`). The same behaviour as
200
- `alias-hooks.mjs` on the Node side, so the modules under `lib/` can use the same
201
- import style both on the server and in the browser.
201
+ is performed (`.js`, `.mjs`, `.ts`, `.mts`, `.json`, `/index.js`, `/index.ts`).
202
+ Node `alias-hooks.mjs` resolves only `.js` / `.mjs` / `.json` on the server, so
203
+ shared `@/lib` modules must stay `.js`. Client-only `.ts` imports work on the
204
+ esbuild path.
202
205
 
203
206
  ### Inlining `clientEnv`
204
207
 
205
208
  There is no `process` in the browser; modules shared with the server still read
206
209
  `process.env`. The keys declared through `config.clientEnv` plus `NODE_ENV` are
207
210
  defined as a single object at build time, which means that reading a key not in
208
- the list returns `undefined` instead of crashing. Details:
209
- [07-configuration.md](./07-configuration.md).
211
+ the list returns `undefined` instead of crashing. Secret-like key names
212
+ (`SECRET`, `API_KEY`, …) fail the build; names containing `PUBLIC` /
213
+ `PUBLISHABLE` are exempt. Details: [07-configuration.md](./07-configuration.md).
210
214
 
211
215
  ### Manifest keys
212
216
 
@@ -285,7 +289,7 @@ Local file names:
285
289
  `0 0 256 256` (recommended for Phosphor / `icon()` compatibility).
286
290
  - The scanned directories default to `views`, `client`, `routes`, `lib`,
287
291
  `features`, `shared`; they can be changed with `icons.scan`. Scanned
288
- extensions: `.ejs`, `.jsk`, `.js`, `.mjs`.
292
+ extensions: `.ejs`, `.jsk`, `.js`, `.mjs`, `.ts`, `.mts`.
289
293
  - Weights: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. An
290
294
  unrecognised weight counts as `regular`.
291
295
 
@@ -355,7 +359,9 @@ and `image()` falls back to the original file. It never runs on a watch pass.
355
359
  When `images.remote.allowHosts` is set, `createApp` mounts `/_jskelet/image`.
356
360
  CMS / CDN covers never enter the build, so `image()` rewrites those host URLs to
357
361
  `?url=&w=`; the endpoint encodes webp with sharp and stores files under
358
- `.jskelet/image-cache/`. Details: [07-configuration.md](./07-configuration.md).
362
+ `.jskelet/image-cache/`. Upstream fetch follows redirects manually: every hop is
363
+ re-checked against the allowlist and private IP / DNS rules (open-redirect SSRF
364
+ is closed). Details: [07-configuration.md](./07-configuration.md).
359
365
 
360
366
  ## Precompress
361
367
 
@@ -23,6 +23,11 @@ jskelet dev
23
23
  (suppresses the build banner) and, if a TTY is present, `JSKELET_COLOR=1`
24
24
  (forces color on piped output).
25
25
 
26
+ If the listen port (`PORT`, default `3000`) is already taken, the server
27
+ **does not start**; the error line includes the PID and a `--murder` hint.
28
+ `jskelet dev --murder` kills the listener and binds the same port (for a
29
+ process left running in another terminal).
30
+
26
31
  Startup order: banner → build steps → server ready → `Ready` summary. The
27
32
  summary is printed once both the build and the server are ready; otherwise it
28
33
  got buried among the build lines arriving afterwards.
@@ -75,7 +80,7 @@ WATCH_DIRS = [
75
80
  The `jskelet.config.mjs` file itself is watched as well: when the config
76
81
  changes, both the server and the build must come up with the new settings.
77
82
 
78
- Watched extensions: `.js`, `.mjs`, `.json`, `.ejs`.
83
+ Watched extensions: `.js`, `.mjs`, `.json`, `.jsk`, `.ejs`.
79
84
 
80
85
  `views` is watched too, because most components live in
81
86
  `views/components/**.js` and, since those modules are imported into the server
@@ -298,11 +303,20 @@ production process.
298
303
 
299
304
  ## Dev gate — `DEV_TOKEN`
300
305
 
301
- To hide an environment that is not public yet: while `DEV_TOKEN` is set,
302
- **every** request without the token gets a 404.
306
+ To hide an environment that is not public yet. **The framework does not require
307
+ the token:** a `DEV_TOKEN` sitting in the environment does not lock the site.
308
+ You turn the gate on.
303
309
 
304
310
  ```bash
305
- DEV_TOKEN=a-long-random-string npm start
311
+ DEV_GATE=1 DEV_TOKEN=a-long-random-string npm start
312
+ ```
313
+
314
+ The same thing from config:
315
+
316
+ ```js
317
+ export default {
318
+ devGate: true,
319
+ };
306
320
  ```
307
321
 
308
322
  Access:
@@ -323,10 +337,12 @@ Behavior:
323
337
  `/site.webmanifest`, `/favicon.ico`. If your health check lives at a different
324
338
  path, remember to add it to this list, otherwise your orchestrator will see a
325
339
  404.
326
- - Without `DEV_TOKEN` the middleware is entirely disabled and costs nothing in
327
- production.
328
- - Since warming makes requests to its own server, it carries the token as a
329
- cookie; without it every page gets a 404 and the cache never fills up
340
+ - While `devGate` is off (the default) or `DEV_TOKEN` is empty, the middleware
341
+ passes the request through. A `DEV_TOKEN` that leaked into a production task
342
+ does not ask visitors for a token; startup prints a warning.
343
+ - `DEV_GATE=0` turns the gate off even when config says `devGate: true`.
344
+ - While the gate is on, warming carries the token as a cookie; without it every
345
+ page gets a 404 and the cache never fills up
330
346
  ([06-caching.md](./06-caching.md)).
331
347
 
332
348
  In the middleware chain the gate sits after `headers` and **before**
@@ -23,6 +23,10 @@ runs the build itself. In a Docker image the build has already happened, so this
23
23
  is a no-op; the point is that someone running `npm start` directly does not end
24
24
  up with an unstyled page.
25
25
 
26
+ If the listen port is already taken, the process **does not start** (PID + hint).
27
+ `jskelet start --murder` kills that listener and binds — useful for a leftover
28
+ dev process; production orchestrators usually do not need it.
29
+
26
30
  When the server is ready it prints a single line:
27
31
 
28
32
  ```
@@ -47,7 +51,7 @@ considering in production:
47
51
  | `HOST` | `0.0.0.0` | Only if you need to listen on IPv4 alone; the `::` default already listens dual-stack |
48
52
  | `PREWARM_MAX` | Depends on site size | Number of pages warmed at startup |
49
53
  | `PREWARM_INTERVAL_SECONDS` | `0` or a long value | If you want to keep never-visited pages warm |
50
- | `DEV_TOKEN` | Staging only | Hides an environment that is not public yet |
54
+ | `DEV_GATE` + `DEV_TOKEN` | Staging only | Hides an environment that is not public yet. The token alone does not lock the site |
51
55
  | `JSKELET_S3_*` | If you write access logs to S3 | Bucket + credentials; details in [07](./07-configuration.md) |
52
56
 
53
57
  When a file or S3 sink is enabled in production, the HTTP access log middleware
@@ -64,7 +68,8 @@ Using both sources together blurs which value actually applies; not shipping a
64
68
  `.env` in the prod image is the cleanest option.
65
69
 
66
70
  **Secret keys must not go into the `clientEnv` list:** those values are embedded
67
- into the client bundle as plain text ([08-build.md](./08-build.md)).
71
+ into the client bundle as plain text ([08-build.md](./08-build.md)). Secret-like
72
+ names (`SECRET`, `API_KEY`, …) now fail the build.
68
73
 
69
74
  ## Docker
70
75
 
@@ -152,16 +157,11 @@ The build stage produces these itself with `npx jskelet build`.
152
157
 
153
158
  The examples in this repo pull jskelet with `"jskelet": "file:../.."` rather
154
159
  than from npm. In tools like Coolify, Railway or Render, if you set the "base
155
- directory" to `examples/marketing`, the build context becomes only that
156
- directory, `../..` falls outside the context, and installation fails at
157
- `npm ci`. The correct setting: **base directory `/`**, Dockerfile location
158
- `/examples/marketing/Dockerfile`. The working example is in
159
- `examples/marketing/Dockerfile` and assumes the repo root as its context:
160
-
161
- ```bash
162
- docker build -f examples/marketing/Dockerfile -t jskelet-marketing .
163
- docker run --rm -p 3000:3000 -e SITE_URL=https://example.com jskelet-marketing
164
- ```
160
+ directory" to `examples/blog`, the build context becomes only that directory,
161
+ `../..` falls outside the context, and installation fails at `npm ci`. The
162
+ correct setting: **base directory `/`** (the repo root) and adapt the
163
+ multi-stage Dockerfile above to the app directory — or install jskelet as a
164
+ normal npm dependency and use the app directory as the context.
165
165
 
166
166
  In your own application jskelet will be an ordinary dependency, so this
167
167
  constraint does not apply; the multi-stage image above is enough.
@@ -171,7 +171,7 @@ constraint does not apply; the multi-stage image above is enough.
171
171
  The framework does **not** add a ready-made health check endpoint; you have to
172
172
  put it in your own route. Since the default `devGateBypass` list contains
173
173
  `/api/healthcheck`, using that name is the least surprising option: it stays
174
- reachable even in an environment with `DEV_TOKEN` set.
174
+ reachable even while the dev gate is on.
175
175
 
176
176
  ```js
177
177
  // routes/00-health.mjs
@@ -328,7 +328,7 @@ goes up, the work per request drops to almost zero.
328
328
  - [ ] `hooks.prewarmPaths()` puts the most important pages first
329
329
  - [ ] CSP and security headers are defined in `headers()`
330
330
  - [ ] A health check endpoint exists and is in the `devGateBypass` list
331
- - [ ] `DEV_TOKEN` is set on staging and **not set** in production
331
+ - [ ] Staging has `DEV_GATE=1` and `DEV_TOKEN`; production leaves the gate **off**
332
332
  - [ ] The reverse proxy forwards `Accept-Encoding` and does not do its own
333
333
  compression
334
334
  - [ ] There are no secret keys in the `clientEnv` list