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
@@ -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 |
@@ -48,7 +50,7 @@ change one, change the other.
48
50
 
49
51
  ## Runnable examples
50
52
 
51
- All four are in working order; most of the examples in the docs were taken from
53
+ All three are in working order; most of the examples in the docs were taken from
52
54
  them.
53
55
 
54
56
  **`examples/minimal/`** — two routes, one component, one island, minimal config.
@@ -70,34 +72,7 @@ npm --prefix examples/blog install
70
72
  npm --prefix examples/blog run dev
71
73
  ```
72
74
 
73
- **`examples/marketing/`** — the framework's own marketing site: hero, comparison
74
- table, live latency measurement, FAQ, docs index, release notes and a download
75
- page. The byte counts on the page are read in `lib/payload.js` from the site's
76
- **own** build output, and the release info in `lib/release.js` from the
77
- installed package's `package.json`; the latency numbers are measured in the
78
- browser by the `latency` island. With a long TTL (one hour) and a prewarm that
79
- warms every page, it shows the profile in which the cache works most
80
- efficiently.
81
-
82
- It also serves **these documents**: `/docs/<chapter>` reads the markdown files
83
- in `node_modules/jskelet/docs/` and renders them with a sidebar, an "on this
84
- page" list and sequential navigation. The renderer is a small module in
85
- `lib/markdown.js` — no dependency — and the source of truth stays the package,
86
- so the site never drifts from the installed version.
87
-
88
- The site is also **bilingual**: English by default at the root, Turkish under
89
- `/tr`, with the same route names in both languages. There is no i18n in the
90
- framework; language resolution lives in `lib/i18n.js` as the application's own
91
- contract and is wired to a dictionary via `hooks.layoutContext`. This is the
92
- place to look if you want to see how to build a multilingual site with this
93
- surface.
94
-
95
- ```bash
96
- npm --prefix examples/marketing install
97
- npm --prefix examples/marketing run dev
98
- ```
99
-
100
- **`examples/dashboard/`** — the opposite axis from the other three: per-visitor
75
+ **`examples/dashboard/`** — the opposite axis from the other two: per-visitor
101
76
  pages. Sign-in with a signed cookie session, a `private: true` protected panel,
102
77
  a paginated table fragment, a CSRF-protected mutation form and an island that
103
78
  returns a cleanup function. It also has a public landing page, so a cached
@@ -108,5 +83,5 @@ npm --prefix examples/dashboard install
108
83
  npm --prefix examples/dashboard run dev
109
84
  ```
110
85
 
111
- In all four examples, `node smoke.mjs` verifies that the endpoints respond as
86
+ In all three examples, `node smoke.mjs` verifies that the endpoints respond as
112
87
  expected while the server is up.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.5.5",
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.",
3
+ "version": "0.6.1",
4
+ "description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), 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",
@@ -17,7 +17,7 @@
17
17
  "ssr",
18
18
  "islands",
19
19
  "express",
20
- "ejs",
20
+ "jsk",
21
21
  "esbuild",
22
22
  "tailwind",
23
23
  "seo",
@@ -28,19 +28,42 @@
28
28
  },
29
29
  "main": "./src/index.js",
30
30
  "exports": {
31
- ".": "./src/index.js",
32
- "./server": "./src/index.js",
33
- "./client": "./src/client/index.js",
34
- "./html": "./src/views/helpers/html.js",
35
- "./tags": "./src/views/helpers/tags.js",
36
- "./cookies": "./src/http/cookies-entry.js",
37
- "./log": "./src/log.mjs",
31
+ ".": {
32
+ "types": "./types/index.d.ts",
33
+ "default": "./src/index.js"
34
+ },
35
+ "./server": {
36
+ "types": "./types/index.d.ts",
37
+ "default": "./src/index.js"
38
+ },
39
+ "./client": {
40
+ "types": "./types/client/index.d.ts",
41
+ "default": "./src/client/index.js"
42
+ },
43
+ "./html": {
44
+ "types": "./types/views/helpers/html.d.ts",
45
+ "default": "./src/views/helpers/html.js"
46
+ },
47
+ "./tags": {
48
+ "types": "./types/views/helpers/tags.d.ts",
49
+ "default": "./src/views/helpers/tags.js"
50
+ },
51
+ "./cookies": {
52
+ "types": "./types/http/cookies-entry.d.ts",
53
+ "default": "./src/http/cookies-entry.js"
54
+ },
55
+ "./log": {
56
+ "types": "./types/log.d.mts",
57
+ "default": "./src/log.mjs"
58
+ },
38
59
  "./register": "./src/runtime/register.mjs",
39
- "./layout": "./src/templates/layout.ejs"
60
+ "./layout": "./src/templates/layout.jsk",
61
+ "./layout/ejs": "./src/templates/layout.ejs"
40
62
  },
41
63
  "files": [
42
64
  "bin",
43
65
  "src",
66
+ "types",
44
67
  "docs",
45
68
  "README.md",
46
69
  "AGENTS.md",
@@ -53,12 +76,16 @@
53
76
  "scripts": {
54
77
  "lint": "eslint",
55
78
  "test": "node --test \"test/**/*.test.mjs\"",
79
+ "types": "tsc -p tsconfig.types.json",
80
+ "prepublishOnly": "npm run types",
81
+ "test:framework-layout": "node scripts/compile-framework-layout.mjs --check",
56
82
  "example:minimal": "npm --prefix examples/minimal run dev",
57
83
  "example:blog": "npm --prefix examples/blog run dev",
58
84
  "example:dashboard": "npm --prefix examples/dashboard run dev"
59
85
  },
60
86
  "dependencies": {
61
- "ejs": "^6.0.1",
87
+ "@babel/parser": "^8.0.6",
88
+ "@babel/types": "^8.0.6",
62
89
  "esbuild": "^0.28.2",
63
90
  "express": "^5.2.1",
64
91
  "tailwind-merge": "^3.5.0"
@@ -66,6 +93,7 @@
66
93
  "peerDependencies": {
67
94
  "@phosphor-icons/core": "^2.1.1",
68
95
  "@tailwindcss/postcss": "^4.3.3",
96
+ "ejs": "^6.0.1",
69
97
  "ioredis": "^5.4.0 || ^6.0.0",
70
98
  "lightningcss": "^1.32.0",
71
99
  "postcss": "^8.5.26",
@@ -79,6 +107,9 @@
79
107
  "@tailwindcss/postcss": {
80
108
  "optional": true
81
109
  },
110
+ "ejs": {
111
+ "optional": true
112
+ },
82
113
  "ioredis": {
83
114
  "optional": true
84
115
  },
@@ -97,7 +128,11 @@
97
128
  },
98
129
  "devDependencies": {
99
130
  "@eslint/js": "^9",
131
+ "@types/express": "^5.0.6",
132
+ "@types/node": "^26.6.1",
133
+ "ejs": "^6.0.1",
100
134
  "eslint": "^9",
101
- "globals": "^16"
135
+ "globals": "^16",
136
+ "typescript": "^7.0.2"
102
137
  }
103
138
  }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Island bundle'ı: esbuild, ESM, code splitting.
3
3
  *
4
- * `client/entries/*.js` içindeki her dosya bir entry'dir. `main.js` her sayfada
5
- * yüklenen ortak island bootstrap'ıdır; ek entry'ler yalnızca onları isteyen
6
- * sayfalarda (`controller` → `entries: ["chart.js"]`) yüklenir.
4
+ * `client/entries/*.{js,ts,mts}` içindeki her dosya bir entry'dir. `main.js`
5
+ * (veya `main.ts`) her sayfada yüklenen ortak island bootstrap'ıdır; ek
6
+ * entry'ler yalnızca onları isteyen sayfalarda
7
+ * (`controller` → `entries: ["chart.js"]`) yüklenir. Manifest anahtarı her
8
+ * zaman `*.js` kalır.
7
9
  *
8
10
  * Hedef tarayıcılar `package.json` → `browserslist` yerine burada sabit: ESM +
9
11
  * dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı,
@@ -16,11 +18,17 @@ import * as esbuild from "esbuild";
16
18
  import { paths, patchManifest, pruneAssets } from "../paths.mjs";
17
19
  import * as log from "../../log.mjs";
18
20
 
21
+ /** Client entry ve `@/` alias için kabul edilen kaynak uzantıları. `.tsx` yok. */
22
+ const CLIENT_SOURCE_EXTS = [".js", ".ts", ".mts"];
23
+
19
24
  /**
20
25
  * `@/` alias'ını proje köküne çözer — Node tarafındaki `alias-hooks.mjs` ile
21
26
  * aynı davranış, böylece `lib/` altındaki modüller hem sunucuda hem
22
27
  * tarayıcıda aynı import stilini kullanabilir.
23
28
  *
29
+ * Not: sunucu runtime `.ts` çözmez; paylaşılan `@/lib` dosyaları `.js`
30
+ * kalmalıdır. `.ts` yalnızca esbuild client hattında anlamlıdır.
31
+ *
24
32
  * @param {string} root
25
33
  * @returns {esbuild.Plugin}
26
34
  */
@@ -35,6 +43,23 @@ function aliasPlugin(root) {
35
43
  };
36
44
  }
37
45
 
46
+ /**
47
+ * Secret benzeri isimler public bundle'a gömülmemeli. `PUBLIC` / `PUBLISHABLE`
48
+ * içerenler (örn. Stripe publishable key) muaf; diğer `SECRET`, `PASSWORD`,
49
+ * `TOKEN`, `API_KEY`, `PRIVATE` vb. reddedilir.
50
+ */
51
+ const SECRETISH_CLIENT_ENV =
52
+ /(?:^|_)(SECRET|PASSWORD|PASSWD|TOKEN|PRIVATE|CREDENTIAL|API[_-]?KEY)(?:_|$)|(?:^|_)(SECRET|PASSWORD|TOKEN|PRIVATE|KEY)$/i;
53
+
54
+ /**
55
+ * @param {string} key
56
+ * @returns {boolean}
57
+ */
58
+ export function isSecretLikeClientEnvKey(key) {
59
+ if (/PUBLIC|PUBLISHABLE/i.test(key)) return false;
60
+ return SECRETISH_CLIENT_ENV.test(key);
61
+ }
62
+
38
63
  /**
39
64
  * Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de
40
65
  * `process.env` okur. `clientEnv` ile bildirilen anahtarlar build zamanında
@@ -48,6 +73,14 @@ function aliasPlugin(root) {
48
73
  * @returns {Record<string, string>}
49
74
  */
50
75
  function publicEnv(keys) {
76
+ const secretish = keys.filter((key) => isSecretLikeClientEnvKey(key));
77
+ if (secretish.length) {
78
+ throw new Error(
79
+ `[build] clientEnv must not include secret-like keys: ${secretish.join(", ")}. ` +
80
+ "Only values safe to publish in the browser bundle belong here.",
81
+ );
82
+ }
83
+
51
84
  /** @type {Record<string, string>} */
52
85
  const env = { NODE_ENV: process.env.NODE_ENV ?? "production" };
53
86
  for (const key of keys) {
@@ -61,13 +94,16 @@ function publicEnv(keys) {
61
94
  * @param {string} base
62
95
  * @returns {string}
63
96
  */
64
- function resolveWithExtension(base) {
97
+ export function resolveWithExtension(base) {
65
98
  const candidates = [
66
99
  base,
67
100
  `${base}.js`,
68
101
  `${base}.mjs`,
102
+ `${base}.ts`,
103
+ `${base}.mts`,
69
104
  `${base}.json`,
70
105
  path.join(base, "index.js"),
106
+ path.join(base, "index.ts"),
71
107
  ];
72
108
  for (const candidate of candidates) {
73
109
  if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
@@ -75,6 +111,53 @@ function resolveWithExtension(base) {
75
111
  return base;
76
112
  }
77
113
 
114
+ /**
115
+ * Entry dosya adını manifest anahtarına çevirir (`main.ts` → `main.js`).
116
+ * Layout / `entries: ["chart.js"]` sözleşmesi hash'siz `.js` anahtarları bekler.
117
+ *
118
+ * @param {string} entryPath
119
+ * @returns {string}
120
+ */
121
+ export function entryManifestName(entryPath) {
122
+ const base = path.basename(entryPath);
123
+ const ext = path.extname(base);
124
+ const stem = CLIENT_SOURCE_EXTS.includes(ext)
125
+ ? base.slice(0, -ext.length)
126
+ : path.basename(base, ".js");
127
+ return `${stem}.js`;
128
+ }
129
+
130
+ /**
131
+ * `client/entries/` altındaki kaynakları listeler. Aynı stem için birden fazla
132
+ * uzantı (`main.js` + `main.ts`) sessiz tercih yerine hata verir.
133
+ *
134
+ * @param {string} entryDir
135
+ * @returns {string[]}
136
+ */
137
+ export function listClientEntries(entryDir) {
138
+ /** @type {Map<string, string>} */
139
+ const byStem = new Map();
140
+
141
+ for (const file of fs.readdirSync(entryDir)) {
142
+ const ext = path.extname(file);
143
+ if (!CLIENT_SOURCE_EXTS.includes(ext)) continue;
144
+
145
+ const stem = file.slice(0, -ext.length);
146
+ const previous = byStem.get(stem);
147
+ if (previous) {
148
+ throw new Error(
149
+ `[build] conflicting client entries for "${stem}": ${previous} and ${file}. ` +
150
+ "Keep a single extension per entry name.",
151
+ );
152
+ }
153
+ byStem.set(stem, file);
154
+ }
155
+
156
+ return [...byStem.values()]
157
+ .sort()
158
+ .map((file) => path.join(entryDir, file));
159
+ }
160
+
78
161
  /**
79
162
  * @param {import('../../config/index.js').ResolvedConfig} config
80
163
  * @param {{ watch?: boolean }} [options]
@@ -89,10 +172,7 @@ export async function buildClient(config, { watch = false } = {}) {
89
172
  return {};
90
173
  }
91
174
 
92
- const entryPoints = fs
93
- .readdirSync(entryDir)
94
- .filter((file) => file.endsWith(".js"))
95
- .map((file) => path.join(entryDir, file));
175
+ const entryPoints = listClientEntries(entryDir);
96
176
 
97
177
  if (!entryPoints.length) {
98
178
  log.detail("no entries, skipped");
@@ -117,7 +197,8 @@ export async function buildClient(config, { watch = false } = {}) {
117
197
  format: "esm",
118
198
  target: ["chrome111", "edge111", "firefox111", "safari16.4"],
119
199
  minify: true,
120
- sourcemap: true,
200
+ // Prod'da map dosyaları `public/assets` altında herkese açık kalırdı.
201
+ sourcemap: process.env.NODE_ENV === "development",
121
202
  metafile: true,
122
203
  entryNames: "[name].[hash]",
123
204
  chunkNames: "chunks/[name].[hash]",
@@ -256,7 +337,7 @@ function toManifest(metafile, config, entryRoot) {
256
337
  for (const [outputPath, output] of Object.entries(metafile.outputs)) {
257
338
  if (!output.entryPoint?.startsWith(entryRoot)) continue;
258
339
 
259
- const name = `${path.basename(output.entryPoint, ".js")}.js`;
340
+ const name = entryManifestName(output.entryPoint);
260
341
  const absolute = path.resolve(config.root, outputPath);
261
342
  manifest[name] = `/${path
262
343
  .relative(config.dirs.public, absolute)
@@ -24,12 +24,15 @@ import { createRequire } from "node:module";
24
24
  import { pruneAssets, writeAsset } from "../paths.mjs";
25
25
  import * as log from "../../log.mjs";
26
26
 
27
- const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs"]);
27
+ const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs", ".ts", ".mts"]);
28
28
 
29
29
  /** `icon({ … })` çağrısının tamamı; `name:` ifadesi ayrıca çözümlenir. */
30
30
  const ICON_CALL = /icon\(\s*\{([^}]*)\}/g;
31
31
  const ICON_NAME_EXPR = /name:\s*([^,}]+)/;
32
32
  const ICON_WEIGHT = /weight:\s*["']([^"']+)["']/;
33
+ /** `.jsk` / JSX tarzı `<Icon name="Moon" />` ve isteğe bağlı `weight="bold"`. */
34
+ const ICON_TAG =
35
+ /<Icon\b[^>]*\bname=["']([A-Z][A-Za-z0-9]*)["'][^>]*(?:\bweight=["']([a-z]+)["'])?/gi;
33
36
  /** `data-icon="flag:fill"` ve JS nesnesindeki `"data-icon": "flag:fill"`. */
34
37
  const ICON_ATTR = /data-icon"?\s*[:=]\s*["']([a-z0-9-]+)(?::([a-z]+))?["']/g;
35
38
 
@@ -191,6 +194,13 @@ function scanUsedIcons(scanDirs) {
191
194
  for (const name of names) used.add(`${toKebab(name)}:${weight}`);
192
195
  }
193
196
 
197
+ for (const match of source.matchAll(ICON_TAG)) {
198
+ const weight = match[2] ?? "regular";
199
+ used.add(
200
+ `${toKebab(match[1])}:${WEIGHTS.has(weight) ? weight : "regular"}`,
201
+ );
202
+ }
203
+
194
204
  for (const match of source.matchAll(ICON_NAME_PROP)) {
195
205
  indirectNames.add(toKebab(match[1]));
196
206
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * JSkelet island runtime'ının tarayıcı tarafı API'si.
3
3
  *
4
- * `client/entries/main.js` içinde:
4
+ * `client/entries/main.js` (veya `main.ts`) içinde:
5
5
  *
6
6
  * import { registerAll, start } from "jskelet/client";
7
7
  *
8
8
  * registerAll({
9
- * counter: () => import("../islands/counter.js"),
9
+ * counter: () => import("../islands/counter.ts"),
10
10
  * });
11
11
  *
12
12
  * start();
@@ -21,6 +21,7 @@ export function codegen(ast, options) {
21
21
  return {
22
22
  code: ctx.finish(),
23
23
  includes: [...ctx.includes],
24
+ includeEntries: ctx.includeEntries,
24
25
  components: [...ctx.components],
25
26
  };
26
27
  }
@@ -58,6 +59,8 @@ class CodegenContext {
58
59
  this.knownComponents = options.knownComponents ?? null;
59
60
  /** @type {Set<string>} */
60
61
  this.includes = new Set();
62
+ /** @type {{ id: string, index: number }[]} */
63
+ this.includeEntries = [];
61
64
  /** @type {Set<string>} */
62
65
  this.components = new Set();
63
66
  /** @type {string[]} */
@@ -249,6 +252,7 @@ class CodegenContext {
249
252
  emitInclude(node) {
250
253
  const id = normalizeIncludeId(node.path);
251
254
  this.includes.add(id);
255
+ this.includeEntries.push({ id, index: node.index });
252
256
  const alias = includeAlias(id);
253
257
  this.append(`(${alias}(data, helpers) ?? "")`);
254
258
  }