jskelet 0.5.4 → 0.6.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.
Files changed (149) hide show
  1. package/AGENTS.md +18 -13
  2. package/CHANGELOG.md +387 -385
  3. package/README.md +9 -7
  4. package/bin/jskelet.mjs +24 -10
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +4 -3
  7. package/docs/03-routing.md +11 -6
  8. package/docs/04-render-ve-sablonlar.md +35 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/07-yapilandirma.md +53 -25
  11. package/docs/08-build.md +40 -18
  12. package/docs/09-dev-araclari.md +5 -1
  13. package/docs/10-dagitim.md +6 -1
  14. package/docs/11-tasima.md +51 -17
  15. package/docs/12-panel-ve-oturum.md +10 -4
  16. package/docs/README.md +7 -5
  17. package/docs/en/01-getting-started.md +4 -3
  18. package/docs/en/02-architecture.md +5 -5
  19. package/docs/en/03-routing.md +12 -7
  20. package/docs/en/04-rendering.md +47 -59
  21. package/docs/en/05-islands.md +13 -8
  22. package/docs/en/07-configuration.md +55 -27
  23. package/docs/en/08-build.md +43 -21
  24. package/docs/en/09-dev-tools.md +6 -1
  25. package/docs/en/10-deployment.md +6 -1
  26. package/docs/en/11-migration.md +51 -16
  27. package/docs/en/12-dashboards-and-sessions.md +9 -4
  28. package/docs/en/README.md +7 -5
  29. package/package.json +49 -14
  30. package/src/build/tasks/client.mjs +91 -10
  31. package/src/build/tasks/icons.mjs +152 -18
  32. package/src/client/index.js +2 -2
  33. package/src/compile/codegen.js +4 -0
  34. package/src/compile/compile-all.js +12 -21
  35. package/src/compile/expr.js +5 -0
  36. package/src/compile/parse.js +64 -8
  37. package/src/compile/resolve.js +3 -0
  38. package/src/config/defaults.js +12 -2
  39. package/src/config/index.js +31 -3
  40. package/src/dev-server.mjs +26 -3
  41. package/src/http/cookies-entry.js +1 -0
  42. package/src/http/cookies.js +18 -0
  43. package/src/logo.png +0 -0
  44. package/src/migrate/apply.mjs +262 -0
  45. package/src/migrate/babel.mjs +79 -0
  46. package/src/migrate/classify.mjs +155 -0
  47. package/src/migrate/config.mjs +126 -0
  48. package/src/migrate/fs-walk.mjs +191 -0
  49. package/src/migrate/parse.mjs +26 -0
  50. package/src/migrate/scan.mjs +177 -0
  51. package/src/migrate/transform/expr-source.mjs +168 -0
  52. package/src/migrate/transform/island.mjs +67 -0
  53. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  54. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  55. package/src/migrate/transform/page-split.mjs +435 -0
  56. package/src/migrate/write.mjs +81 -0
  57. package/src/migrate.mjs +171 -0
  58. package/src/server/auth/handoff.js +94 -11
  59. package/src/server/create-app.js +28 -10
  60. package/src/server/ejs-adapter.js +59 -0
  61. package/src/server/image-optimizer.js +94 -26
  62. package/src/server/port-guard.js +255 -0
  63. package/src/server/render.js +27 -9
  64. package/src/server/status-page.js +105 -4
  65. package/src/start.mjs +18 -3
  66. package/src/templates/layout.ejs +8 -28
  67. package/src/templates/layout.jsk +30 -0
  68. package/src/templates/layout.render.js +41 -0
  69. package/src/views/helpers/tags.js +86 -3
  70. package/types/build/resolve-peer.d.mts +13 -0
  71. package/types/client/dom.d.ts +55 -0
  72. package/types/client/form.d.ts +19 -0
  73. package/types/client/index.d.ts +20 -0
  74. package/types/client/registry.d.ts +53 -0
  75. package/types/client/safe-image.d.ts +19 -0
  76. package/types/client/shared-cookie.d.ts +82 -0
  77. package/types/client/store.d.ts +18 -0
  78. package/types/client/swap.d.ts +46 -0
  79. package/types/compile/codegen.d.ts +32 -0
  80. package/types/compile/compile-all.d.ts +42 -0
  81. package/types/compile/errors.d.ts +30 -0
  82. package/types/compile/expr.d.ts +67 -0
  83. package/types/compile/index.d.ts +10 -0
  84. package/types/compile/parse.d.ts +82 -0
  85. package/types/compile/resolve.d.ts +46 -0
  86. package/types/compile/scan-exports.d.ts +9 -0
  87. package/types/config/defaults.d.ts +449 -0
  88. package/types/config/index.d.ts +299 -0
  89. package/types/config/pattern.d.ts +38 -0
  90. package/types/http/control-flow.d.ts +45 -0
  91. package/types/http/cookies-entry.d.ts +5 -0
  92. package/types/http/cookies.d.ts +113 -0
  93. package/types/http/request-cache.d.ts +13 -0
  94. package/types/http/request-context.d.ts +67 -0
  95. package/types/http/shared-cookie.d.ts +73 -0
  96. package/types/index.d.ts +30 -0
  97. package/types/log.d.mts +153 -0
  98. package/types/server/admin/actions.d.ts +16 -0
  99. package/types/server/admin/auth.d.ts +52 -0
  100. package/types/server/admin/event-log.d.ts +38 -0
  101. package/types/server/admin/gate.d.ts +43 -0
  102. package/types/server/admin/inventory.d.ts +40 -0
  103. package/types/server/admin/mount.d.ts +6 -0
  104. package/types/server/admin/router.d.ts +6 -0
  105. package/types/server/admin/snapshot.d.ts +6 -0
  106. package/types/server/assets.d.ts +47 -0
  107. package/types/server/auth/handoff.d.ts +12 -0
  108. package/types/server/cache-deps.d.ts +16 -0
  109. package/types/server/cache-vary.d.ts +30 -0
  110. package/types/server/cloudflare.d.ts +163 -0
  111. package/types/server/create-app.d.ts +25 -0
  112. package/types/server/data-cache.d.ts +116 -0
  113. package/types/server/dev/devtools.d.ts +44 -0
  114. package/types/server/dev/report.d.ts +229 -0
  115. package/types/server/dev/socket.d.ts +17 -0
  116. package/types/server/dev/version-check.d.mts +15 -0
  117. package/types/server/ejs-adapter.d.ts +11 -0
  118. package/types/server/head-hints.d.ts +40 -0
  119. package/types/server/html-cache.d.ts +173 -0
  120. package/types/server/image-optimizer.d.ts +68 -0
  121. package/types/server/logs/access-middleware.d.ts +7 -0
  122. package/types/server/logs/file-sink.d.ts +17 -0
  123. package/types/server/logs/pipeline.d.ts +37 -0
  124. package/types/server/logs/s3-put.d.ts +85 -0
  125. package/types/server/logs/s3-sink.d.ts +26 -0
  126. package/types/server/metadata.d.ts +38 -0
  127. package/types/server/middleware/compression.d.ts +17 -0
  128. package/types/server/middleware/csrf.d.ts +4 -0
  129. package/types/server/middleware/dev-gate.d.ts +2 -0
  130. package/types/server/middleware/headers.d.ts +2 -0
  131. package/types/server/middleware/redirects.d.ts +2 -0
  132. package/types/server/middleware/static-precompressed.d.ts +5 -0
  133. package/types/server/middleware/trailing-slash.d.ts +11 -0
  134. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  135. package/types/server/og-image.d.ts +149 -0
  136. package/types/server/port-guard.d.ts +50 -0
  137. package/types/server/prewarm.d.ts +128 -0
  138. package/types/server/redis.d.ts +163 -0
  139. package/types/server/render.d.ts +101 -0
  140. package/types/server/router.d.ts +5 -0
  141. package/types/server/status-page.d.ts +24 -0
  142. package/types/server/upstream-limiter.d.ts +123 -0
  143. package/types/server/upstream-tracking.d.ts +42 -0
  144. package/types/shared/cookie-domain.d.ts +29 -0
  145. package/types/templates/layout.render.d.ts +7 -0
  146. package/types/version.d.mts +10 -0
  147. package/types/views/components/loader.d.ts +5 -0
  148. package/types/views/helpers/html.d.ts +39 -0
  149. package/types/views/helpers/tags.d.ts +127 -0
@@ -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
 
@@ -92,7 +92,7 @@ export default {
92
92
  watch: ["data"],
93
93
 
94
94
  fonts: [{ family: "Inter", weights: [400, 600, 700] }],
95
- icons: { scan: ["views", "client", "routes", "lib"] },
95
+ icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
96
96
  images: { widths: [400, 800, 1200], quality: 78, skip: ["downloads"] },
97
97
  clientEnv: ["PUBLIC_WS_URL"],
98
98
 
@@ -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
 
@@ -335,7 +340,7 @@ field reference.
335
340
  | `trustProxy` | `boolean` | `true` | Express's `trust proxy` setting. Needed behind a reverse proxy for the correct protocol and client IP. |
336
341
  | `cookieSecret` | `string \| null` | `null` | The signed cookie secret. When absent, `JSKELET_SECRET` is read. |
337
342
  | `csrf.enabled` | `boolean` | `true` | The origin / `Sec-Fetch-Site` check. |
338
- | `csrf.token` | `boolean` | `false` | The double-submit token layer. |
343
+ | `csrf.token` | `boolean` | `false` | The double-submit token layer. **Turn on** for cookie-session forms. |
339
344
  | `csrf.allowedOrigins` | `string[]` | `[]` | Origins accepted alongside our own host. |
340
345
  | `csrf.exclude` | `string[]` | `[]` | Paths exempt from the check; `source` pattern syntax. |
341
346
  | `csrf.cookieName` | `string` | `"csrf_token"` | Name of the token cookie. |
@@ -343,14 +348,17 @@ field reference.
343
348
  | `csrf.headerName` | `string` | `"x-csrf-token"` | Header the token is also accepted in. |
344
349
 
345
350
  `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.
351
+ internet: while it is on, a client can forge `X-Forwarded-For` /
352
+ `X-Forwarded-Proto` / Host, and rate limits, admin IP allowlists, Secure
353
+ cookies, and cache `vary.host` see the wrong address. Behind a reverse proxy
354
+ (nginx, Caddy, Cloudflare), `true` is the right default.
348
355
 
349
356
  The CSRF check only rejects requests that are **known** to be cross-site — when
350
357
  `Origin` does not match or `Sec-Fetch-Site: cross-site` arrives. If neither is
351
358
  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.
359
+ cross-origin POST while webhooks never do. For cookie-session dashboards,
360
+ enable `csrf.token: true` and `csrfField()` as a second layer; put webhook
361
+ paths in `csrf.exclude`.
354
362
 
355
363
  ## `navigation`
356
364
 
@@ -500,21 +508,29 @@ fonts: [
500
508
 
501
509
  ## `icons`
502
510
 
503
- **Type:** `{ scan?: string[] } | false` — **Default:** `{}`
511
+ **Type:** `{ scan?: string[], dir?: string } | false` — **Default:** `{ dir: "icons" }`
504
512
 
505
- Phosphor SVG sprite generation.
513
+ SVG icon sprite generation. The source is chosen **XOR**: if the `icons.dir`
514
+ directory exists, only the flat SVGs there are used; otherwise
515
+ `@phosphor-icons/core` (when installed).
506
516
 
507
517
  | Value | Result |
508
518
  | --- | --- |
509
- | `{}` (default) | The sprite is generated; scanned directories are `["views", "client", "routes", "lib"]` |
519
+ | `{}` (default) | `dir: "icons"`; scanned directories are `["views", "client", "routes", "lib", "features", "shared"]` |
520
+ | `{ dir: "assets/icons" }` | Changes the local SVG root |
510
521
  | `{ scan: [...] }` | Changes the scanned directories |
511
522
  | `false` | The sprite step is skipped entirely |
512
523
 
513
- If `@phosphor-icons/core` is not in the application's `node_modules`, the step is
514
- silently skipped. Details: [08-build.md](./08-build.md).
524
+ A local directory (when present) uses flat file names: `house.svg` →
525
+ `house:regular`, `house-bold.svg` → `house:bold`. An empty `icons/` directory
526
+ does not fall back to Phosphor — delete the directory to open the fallback.
527
+ Details: [08-build.md](./08-build.md).
515
528
 
516
529
  ```js
517
- icons: { scan: ["views", "client", "routes", "lib", "content"] }
530
+ icons: {
531
+ dir: "icons",
532
+ scan: ["views", "client", "routes", "lib", "content"],
533
+ }
518
534
  ```
519
535
 
520
536
  ## `images`
@@ -547,8 +563,9 @@ When `remote.allowHosts` is set, also proxies remote images at runtime
547
563
 
548
564
  If `false` is given, neither surface runs. The build step requires `sharp` and
549
565
  never runs on a watch pass. With remote enabled, `sharp` is also needed at
550
- **runtime**; without it the optimizer 302-redirects to the source URL. Details:
551
- [08-build.md](./08-build.md).
566
+ **runtime**; without it the optimizer 302-redirects to the source URL. Fetch
567
+ does not auto-follow redirects: every hop is re-checked against `allowHosts`
568
+ and private addresses. Details: [08-build.md](./08-build.md).
552
569
 
553
570
  ```js
554
571
  images: {
@@ -580,7 +597,10 @@ that is not in the list returns `undefined` instead of crashing.
580
597
  clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
581
598
  ```
582
599
 
583
- **Do not put secrets here** — the values sit in the bundle in plain text.
600
+ **Do not put secrets here** — the values sit in the bundle in plain text. Keys
601
+ whose names look secret-like (`SECRET`, `PASSWORD`, `TOKEN`, `API_KEY`,
602
+ `PRIVATE`, …) are **rejected at build time** (`PUBLIC` / `PUBLISHABLE` names
603
+ are exempt).
584
604
 
585
605
  ## `headers()`
586
606
 
@@ -590,6 +610,7 @@ clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
590
610
  Response headers by path pattern. The framework only writes long-lived cache
591
611
  headers for static files; every other header (CSP, COOP, HSTS,
592
612
  X-Frame-Options…) comes from here and takes precedence over the defaults.
613
+ Production sites should at least define the security headers below.
593
614
 
594
615
  **All** matching rules are applied (unlike redirects, it does not stop at the
595
616
  first match), in order; if two rules write the same header, the later one wins.
@@ -604,11 +625,18 @@ async headers() {
604
625
  source: "/:path*",
605
626
  headers: [
606
627
  { key: "X-Frame-Options", value: "SAMEORIGIN" },
628
+ { key: "X-Content-Type-Options", value: "nosniff" },
607
629
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
630
+ {
631
+ key: "Permissions-Policy",
632
+ value: "camera=(), microphone=(), geolocation=()",
633
+ },
608
634
  {
609
635
  key: "Content-Security-Policy",
610
- value: "default-src 'self'; img-src 'self' https://cdn.example.com data:",
636
+ value: "default-src 'self'; img-src 'self' https://cdn.example.com data:; script-src 'self'",
611
637
  },
638
+ // Only when you terminate HTTPS yourself:
639
+ // { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
612
640
  ],
613
641
  },
614
642
  {
@@ -1098,7 +1126,7 @@ and no warning is printed.
1098
1126
  | Variable | Who reads it | Default | Meaning |
1099
1127
  | --- | --- | --- | --- |
1100
1128
  | `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. |
1101
- | `PORT` | `startServer` | `3000` | Port to listen on |
1129
+ | `PORT` | `startServer` | `3000` | Port to listen on. If busy, the process refuses to start; `jskelet start|dev --murder` kills the listener |
1102
1130
  | `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 |
1103
1131
  | `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) |
1104
1132
  | `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) |
@@ -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
 
@@ -259,17 +263,33 @@ Because the `.woff2` extension and the `/fonts/` prefix are in the default
259
263
 
260
264
  ## Icon sprite
261
265
 
262
- From the individual SVGs inside `@phosphor-icons/core`, it produces a `<symbol>`
263
- set for **only the icons actually used in the source**. Shipping the whole set
264
- means 1500+ icons, i.e. several megabytes; usage scanning typically keeps the
265
- sprite at 10-30 symbols.
266
+ Produces a `<symbol>` set for **only the icons actually used in the source**.
267
+ Shipping the whole set means 1500+ icons, i.e. several megabytes; usage scanning
268
+ typically keeps the sprite at 10-30 symbols. The hashed `sprite.svg` is written
269
+ under `public/assets/` and is covered by precompress.
270
+
271
+ The source is chosen **XOR** — the two are never merged:
272
+
273
+ 1. If `icons.dir` (default `icons/`) **exists as a directory**, only the flat
274
+ SVGs there. An empty directory does not fall back to Phosphor; delete the
275
+ directory to open the fallback.
276
+ 2. Otherwise `@phosphor-icons/core` (from the application's `node_modules`). If
277
+ it is not installed, the step is silently skipped.
278
+
279
+ Local file names:
280
+
281
+ | File | Sprite key |
282
+ | --- | --- |
283
+ | `icons/house.svg` | `house:regular` |
284
+ | `icons/house-regular.svg` | `house:regular` |
285
+ | `icons/arrow-right-bold.svg` | `arrow-right:bold` |
266
286
 
267
287
  - Symbol id: `<kebab-name>-<weight>`, e.g. `arrow-right-bold`.
268
- - The package is resolved from the **application's** `node_modules` (the icon set
269
- is the application's devDependency); if it is not installed, the step is
270
- silently skipped.
271
- - The scanned directories default to `views`, `client`, `routes`, `lib`; they can
272
- be changed with `icons.scan`. Scanned extensions: `.ejs`, `.js`, `.mjs`.
288
+ - `viewBox` is copied from the source SVG onto the `<symbol>`; if missing,
289
+ `0 0 256 256` (recommended for Phosphor / `icon()` compatibility).
290
+ - The scanned directories default to `views`, `client`, `routes`, `lib`,
291
+ `features`, `shared`; they can be changed with `icons.scan`. Scanned
292
+ extensions: `.ejs`, `.jsk`, `.js`, `.mjs`, `.ts`, `.mts`.
273
293
  - Weights: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. An
274
294
  unrecognised weight counts as `regular`.
275
295
 
@@ -296,8 +316,8 @@ If you see this warning, either write the name as a constant, or add the relevan
296
316
  directory to the `icons.scan` list, or keep the name in a configuration field in
297
317
  the form `icon: "XLogo"`.
298
318
 
299
- Names that cannot be found in Phosphor are warned about as a summary at the end
300
- of the build: `N icons missing → …`
319
+ Names that cannot be found in the chosen source are warned about as a summary at
320
+ the end of the build: `N icons missing → …`
301
321
 
302
322
  ## Image optimisation
303
323
 
@@ -339,7 +359,9 @@ and `image()` falls back to the original file. It never runs on a watch pass.
339
359
  When `images.remote.allowHosts` is set, `createApp` mounts `/_jskelet/image`.
340
360
  CMS / CDN covers never enter the build, so `image()` rewrites those host URLs to
341
361
  `?url=&w=`; the endpoint encodes webp with sharp and stores files under
342
- `.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).
343
365
 
344
366
  ## Precompress
345
367
 
@@ -373,7 +395,7 @@ copy, the request is handed over to `express.static`
373
395
  | `tailwindcss` | CSS (peer) | Tailwind directives cannot be resolved |
374
396
  | `lightningcss` | CSS minification | Tailwind's output is used, a few kB bigger |
375
397
  | `sharp` | Image optimisation | The step is skipped; `image()` uses the original |
376
- | `@phosphor-icons/core` | Icon sprite | The step is skipped; `icon()` produces an empty `<use>` |
398
+ | `@phosphor-icons/core` | Icon sprite (when no local `icons/` dir) | The step is skipped; `icon()` produces an empty `<use>` |
377
399
 
378
400
  If you are not going to use CSS, simply never create the `paths.styles` file: the
379
401
  step is skipped with a warning and postcss is not needed.
@@ -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
@@ -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
  ```
@@ -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
 
@@ -8,6 +8,29 @@ modeled on the subset of Next that people actually use — concepts like the
8
8
  will feel familiar. The *reasons* behind the differences are in
9
9
  [02-architecture.md](./02-architecture.md).
10
10
 
11
+ ## `jskelet migrate` (codemod)
12
+
13
+ Run the codemod against an App Router tree. Babel (`@babel/parser`,
14
+ `@babel/types`) ships with JSkelet — no extra install.
15
+
16
+ ```bash
17
+ npx jskelet migrate scan ../my-next-app
18
+ npx jskelet migrate apply ../my-next-app --out . --write
19
+ npx jskelet migrate config ../my-next-app --write
20
+ ```
21
+
22
+ | Command | What it does |
23
+ | --- | --- |
24
+ | `migrate` / `migrate scan` | Inventory pages, layouts, `"use client"` modules, blockers (nested layouts, Server Actions, Suspense). |
25
+ | `migrate apply` | **Automatic convert:** `page.*` → feature controller + `.jsk`; presentational components → `views/components/*.js`; clients → island `mount()` stubs. Default is dry-run; pass `--write`. Never overwrites (conflicts get a `.migrate` suffix). |
26
+ | `migrate config` | Draft `jskelet.config.mjs` from `next.config` (`headers` / `redirects` / `rewrites`, `images.widths`, `NEXT_PUBLIC_*` → `clientEnv`). |
27
+
28
+ Flags: `--out <dir>`, `--only pages,components,islands`, `--json`, `--strict` (exit 1 on partial/skipped).
29
+
30
+ **Converted automatically:** `className`, `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}`, `next/image` → `<Image />`, `next/link` → `<Link />`, `dangerouslySetInnerHTML`, `revalidate`, simple controller prelude (`await` data + `notFound()`).
31
+
32
+ **Not converted (reported):** React hooks, Server Actions, nested layout flattening, Streaming/Suspense, client-side routing. Confidence per file is `ok` / `partial` / `skipped`.
33
+
11
34
  ## Equivalence table
12
35
 
13
36
  ### Configuration
@@ -31,8 +54,8 @@ will feel familiar. The *reasons* behind the differences are in
31
54
  | `app/page.js` (file-based routing) | `app.get(...)` inside `routes/*.mjs` | The order is written explicitly ([03](./03-routing.md)) |
32
55
  | `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express pattern syntax |
33
56
  | `params`, `searchParams` | `ctx.params`, `ctx.query` | The controller's single argument |
34
- | `layout.js` | `views/layout.ejs` + `hooks.layoutContext()` | A single layout; no nested layouts |
35
- | Server component (RSC) | Controller + EJS template + `views/components/**` | A function returns an HTML string |
57
+ | `layout.js` | `views/layout.jsk` + `hooks.layoutContext()` | A single layout; no nested layouts |
58
+ | Server component (RSC) | Controller + `.jsk` template + `views/components/**` | A function returns an HTML string |
36
59
  | Client component (`"use client"`) | Island (`data-island` + `mount`) | The whole page is not hydrated ([05](./05-islands.md)) |
37
60
  | `notFound()` | `notFound()` | Same name, same control flow |
38
61
  | `redirect()` | `redirect()` (307) | For permanent, `permanentRedirect()` (308) |
@@ -88,8 +111,11 @@ Account for these from the start in your migration plan:
88
111
 
89
112
  - **React itself.** Components turn into functions that return HTML strings. No
90
113
  JSX, no hooks, no virtual DOM.
91
- - **TypeScript.** The project is plain JS + JSDoc. With `checkJs: true` in
92
- `jsconfig.json` you get type checking from the editor.
114
+ - **TypeScript.** Framework source is plain JS + JSDoc and publishes `.d.ts` for
115
+ consumers. Client entries and islands may be `.ts` / `.mts` (esbuild strips
116
+ types; the manifest key stays `*.js`). Server routes, hooks and
117
+ `jskelet.config.mjs` remain Node ESM JavaScript — use `checkJs: true` in
118
+ `jsconfig.json` for editor checking there.
93
119
  - **Nested layouts.** There is a single layout; you share common sections with
94
120
  EJS `include` or component functions.
95
121
  - **Streaming / Suspense / partial prerendering.** The response is produced as a
@@ -172,12 +198,12 @@ export default function register(app, { route, notFound }) {
172
198
  }
173
199
  ```
174
200
 
175
- ```ejs
176
- <%# views/pages/article.ejs %>
201
+ ```jsk
202
+ {# views/pages/article.jsk #}
177
203
  <article class="wrapper">
178
- <h1 class="text-3xl font-bold"><%= article.title %></h1>
179
- <%- image({ src: article.cover, alt: article.title, priority: true, width: 1200, height: 630 }) %>
180
- <div><%- article.body %></div>
204
+ <h1 class="text-3xl font-bold">{{ article.title }}</h1>
205
+ <Image :src="article.cover" :alt="article.title" priority :width="1200" :height="630" />
206
+ <div>{{{ article.body }}}</div>
181
207
  </article>
182
208
  ```
183
209
 
@@ -190,7 +216,8 @@ single upstream request is made ([06-caching.md](./06-caching.md)).
190
216
  ### 1. Set up the skeleton (half a day)
191
217
 
192
218
  Run `npx jskelet init` in a new directory and watch `jskelet dev` come up. Leave
193
- the existing Next project as it is; let the migration run in parallel.
219
+ the existing Next project as it is; let the migration run in parallel. Optionally
220
+ run `jskelet migrate scan <next-root>` first to list pages and blockers.
194
221
 
195
222
  Carry over the `paths` aliases from your `jsconfig.json` — prefixes like `@/`
196
223
  work the same way both on the server and in the bundle
@@ -198,6 +225,8 @@ work the same way both on the server and in the bundle
198
225
 
199
226
  ### 2. Translate `next.config.mjs` (1-2 hours)
200
227
 
228
+ `jskelet migrate config <next-root> --write` drafts most of this. Then review:
229
+
201
230
  The `headers()`, `redirects()` and `rewrites()` sections are copied almost
202
231
  verbatim. Check the pattern syntax: JSkelet supports the `:slug`, `:path*`,
203
232
  `/a-:b` and `/:path*.svg` forms; more complex `path-to-regexp` expressions are
@@ -218,9 +247,9 @@ they are copied as-is. Make two changes:
218
247
 
219
248
  ### 4. Set up the layout (half a day)
220
249
 
221
- Translate `app/layout.jsx` into `views/layout.ejs`. Copying the framework's
222
- default layout (`node_modules/jskelet/src/templates/layout.ejs`) and editing it
223
- is the fastest path.
250
+ Translate `app/layout.jsx` into `views/layout.jsk` (or let `migrate apply` draft
251
+ it). Copying the framework's default layout (`jskelet/layout` → `.jsk`) and
252
+ editing it is the fastest path.
224
253
 
225
254
  If you fetch data inside `layout.jsx` (navigation, site settings), move it into
226
255
  `hooks.layoutContext()`: it runs in parallel with the body render, and every
@@ -231,6 +260,10 @@ into `hooks.metadata()`.
231
260
 
232
261
  ### 5. Translate the components (the longest step)
233
262
 
263
+ `jskelet migrate apply --only components --write` converts presentational
264
+ components that are props + JSX with no hooks. Everything else you finish by
265
+ hand:
266
+
234
267
  Every React component turns into a function:
235
268
 
236
269
  ```jsx
@@ -264,8 +297,9 @@ Keep components small and pure; leave data fetching in the controller.
264
297
 
265
298
  ### 6. Migrate the pages (hours per page)
266
299
 
267
- Every `page.jsx` splits into a controller plus an EJS template. File them with
268
- the order in mind:
300
+ `jskelet migrate apply --only pages --write` splits each `page.*` into a
301
+ feature controller plus a `.jsk` template. Review `partial` / `skipped` rows,
302
+ then finish the TODO markers. File them with the order in mind:
269
303
 
270
304
  ```
271
305
  routes/
@@ -342,7 +376,8 @@ especially for verifying that the redirect rules are correct.
342
376
  ## Common mistakes during migration
343
377
 
344
378
  - **Forgetting `esc()`.** Writing `${value}` out of JSX habit means XSS. In
345
- templates, mind the distinction between `<%= %>` (escaped) and `<%- %>` (raw).
379
+ `.jsk` templates use `{{ }}` (escaped) vs `{{{ }}}` (raw); in components call
380
+ `esc()` yourself.
346
381
  - **Opening a new directory without adding `@source`.** The classes are silently
347
382
  dropped.
348
383
  - **Putting the catch-all route in the wrong order.** `/:slug` always goes last.
@@ -145,7 +145,9 @@ export default {
145
145
  sharedCookieRoots: [".investvio.com", ".localhost"],
146
146
  },
147
147
  auth: {
148
- crossSubdomainHandoff: true, // POST /_jskelet/auth/handoff
148
+ crossSubdomainHandoff: {
149
+ allowedCookieNames: ["sid"], // required allowlist
150
+ },
149
151
  },
150
152
  };
151
153
  ```
@@ -210,17 +212,20 @@ a **read-back** runs; if the browser rejected the Domain, `handoff: true`.
210
212
 
211
213
  With `auth.crossSubdomainHandoff` on:
212
214
 
213
- 1. `POST /_jskelet/auth/handoff` `{ name, value, next }` → `{ url }` (with `?handoff=`)
215
+ 1. `POST /_jskelet/auth/handoff` `{ name, value, next }` → `{ url }` (with
216
+ `?handoff=`). Mint is mounted **after** the CSRF middleware; `name` must be
217
+ in `allowedCookieNames` and an RFC 6265 token.
214
218
  2. On the target host a GET middleware redeems the one-time ticket, sets the
215
219
  cookie (shared Domain first, else host-only), and 303-redirects without
216
220
  `handoff`
217
221
 
218
222
  `next` must be under the same `sharedCookieRoots`. Tickets live ~60s in process
219
- memory. Do not put a JWT in the URL.
223
+ memory, with pending-ticket and per-IP mint limits. Do not put a JWT in the URL.
220
224
 
221
225
  The `window.name` bridge is the cookie-less fallback:
222
226
  `handoffViaWindowName` on the source page, `consumeWindowNameHandoff` on the
223
- target.
227
+ target. Prefer the server handoff when possible — `window.name` remains readable
228
+ across origins in the same tab.
224
229
 
225
230
  ## CSRF
226
231
 
package/docs/en/README.md CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  JSkelet is a framework that "feels frameworkless", built for SEO- and
4
4
  speed-focused sites: it produces complete HTML on the server with Express 5 +
5
- EJS, adds interactivity with vanilla JS islands, compiles CSS into a single
6
- stylesheet with Tailwind v4, and instead of ISR uses an HTML TTL cache that
7
- lives in process memory with stale-while-revalidate. No React, no TypeScript;
8
- plain JavaScript and JSDoc.
5
+ build-time `.jsk` (EJS is an optional legacy peer), adds interactivity with
6
+ vanilla JS islands, compiles CSS into a single stylesheet with Tailwind v4, and
7
+ instead of ISR uses an HTML TTL cache that lives in process memory with
8
+ stale-while-revalidate. No React; the framework source is plain JavaScript with
9
+ JSDoc. Apps may write client islands and entries in TypeScript, and the
10
+ published package ships declaration files.
9
11
 
10
12
  This directory is the full reference for the framework. To read it in order,
11
13
  start from the beginning; if you are looking for a specific topic, go straight
@@ -22,7 +24,7 @@ change one, change the other.
22
24
  | [01-getting-started.md](./01-getting-started.md) | Installation, `jskelet init`, first route, first island, directory structure, CLI commands |
23
25
  | [02-architecture.md](./02-architecture.md) | Architectural decisions and their rationale: the island model, complete server HTML, cache strategy, middleware order |
24
26
  | [03-routing.md](./03-routing.md) | The route module contract, load order, the controller contract, `ctx`, `notFound`/`redirect`, config redirects/rewrites |
25
- | [04-rendering.md](./04-rendering.md) | EJS layout, pages, automatic component registration, `html`/`tags` helpers, metadata → `<head>`, hooks |
27
+ | [04-rendering.md](./04-rendering.md) | `.jsk` layout/pages, automatic component registration, `html`/`tags`, metadata → `<head>`, hooks; EJS legacy |
26
28
  | [05-islands.md](./05-islands.md) | The `data-island` contract, hydration strategies, `client/entries/*`, `createStore`, DOM helpers, `startSafeImages` |
27
29
  | [06-caching.md](./06-caching.md) | `withHtmlCache`, `revalidate`, stale-while-revalidate, the cache key, `X-JSkelet-Cache`, in-request cache, degraded render, prewarm |
28
30
  | [07-configuration.md](./07-configuration.md) | Full `jskelet.config.mjs` reference, the `source` pattern syntax, environment variable table |