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.
- package/AGENTS.md +19 -15
- package/CHANGELOG.md +165 -15
- package/README.md +16 -21
- package/bin/jskelet.mjs +23 -9
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +10 -4
- package/docs/03-routing.md +14 -7
- package/docs/04-render-ve-sablonlar.md +60 -43
- package/docs/05-islands.md +12 -8
- package/docs/06-cache.md +18 -7
- package/docs/07-yapilandirma.md +69 -27
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +22 -8
- package/docs/10-dagitim.md +14 -13
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +10 -33
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +12 -6
- package/docs/en/03-routing.md +15 -8
- package/docs/en/04-rendering.md +71 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/06-caching.md +21 -7
- package/docs/en/07-configuration.md +69 -29
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +24 -8
- package/docs/en/10-deployment.md +14 -14
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +10 -35
- package/package.json +48 -13
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +11 -1
- package/src/client/index.js +2 -2
- package/src/compile/codegen.js +4 -0
- package/src/compile/compile-all.js +12 -21
- package/src/compile/expr.js +5 -0
- package/src/compile/parse.js +64 -8
- package/src/compile/resolve.js +3 -0
- package/src/config/defaults.js +48 -5
- package/src/config/index.js +138 -27
- package/src/dev-server.mjs +26 -3
- package/src/http/cookies-entry.js +1 -0
- package/src/http/cookies.js +18 -0
- package/src/logo.png +0 -0
- package/src/migrate/apply.mjs +262 -0
- package/src/migrate/babel.mjs +79 -0
- package/src/migrate/classify.mjs +155 -0
- package/src/migrate/config.mjs +126 -0
- package/src/migrate/fs-walk.mjs +191 -0
- package/src/migrate/parse.mjs +26 -0
- package/src/migrate/scan.mjs +177 -0
- package/src/migrate/transform/expr-source.mjs +168 -0
- package/src/migrate/transform/island.mjs +67 -0
- package/src/migrate/transform/jsx-to-component.mjs +302 -0
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
- package/src/migrate/transform/page-split.mjs +435 -0
- package/src/migrate/write.mjs +81 -0
- package/src/migrate.mjs +171 -0
- package/src/server/auth/handoff.js +94 -11
- package/src/server/create-app.js +37 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/html-cache.js +178 -32
- package/src/server/image-optimizer.js +94 -26
- package/src/server/middleware/dev-gate.js +21 -8
- package/src/server/middleware/robots-txt.js +341 -0
- package/src/server/port-guard.js +255 -0
- package/src/server/prewarm.js +137 -51
- package/src/server/render.js +30 -10
- package/src/server/status-page.js +105 -4
- package/src/start.mjs +18 -3
- package/src/templates/layout.ejs +8 -28
- package/src/templates/layout.jsk +30 -0
- package/src/templates/layout.render.js +41 -0
- package/src/views/helpers/tags.js +86 -3
- package/types/build/resolve-peer.d.mts +13 -0
- package/types/client/dom.d.ts +55 -0
- package/types/client/form.d.ts +19 -0
- package/types/client/index.d.ts +20 -0
- package/types/client/registry.d.ts +53 -0
- package/types/client/safe-image.d.ts +19 -0
- package/types/client/shared-cookie.d.ts +82 -0
- package/types/client/store.d.ts +18 -0
- package/types/client/swap.d.ts +46 -0
- package/types/compile/codegen.d.ts +32 -0
- package/types/compile/compile-all.d.ts +42 -0
- package/types/compile/errors.d.ts +30 -0
- package/types/compile/expr.d.ts +67 -0
- package/types/compile/index.d.ts +10 -0
- package/types/compile/parse.d.ts +82 -0
- package/types/compile/resolve.d.ts +46 -0
- package/types/compile/scan-exports.d.ts +9 -0
- package/types/config/defaults.d.ts +477 -0
- package/types/config/index.d.ts +304 -0
- package/types/config/pattern.d.ts +38 -0
- package/types/http/control-flow.d.ts +45 -0
- package/types/http/cookies-entry.d.ts +5 -0
- package/types/http/cookies.d.ts +113 -0
- package/types/http/request-cache.d.ts +13 -0
- package/types/http/request-context.d.ts +67 -0
- package/types/http/shared-cookie.d.ts +73 -0
- package/types/index.d.ts +30 -0
- package/types/log.d.mts +153 -0
- package/types/server/admin/actions.d.ts +16 -0
- package/types/server/admin/auth.d.ts +52 -0
- package/types/server/admin/event-log.d.ts +38 -0
- package/types/server/admin/gate.d.ts +43 -0
- package/types/server/admin/inventory.d.ts +40 -0
- package/types/server/admin/mount.d.ts +6 -0
- package/types/server/admin/router.d.ts +6 -0
- package/types/server/admin/snapshot.d.ts +6 -0
- package/types/server/assets.d.ts +47 -0
- package/types/server/auth/handoff.d.ts +12 -0
- package/types/server/cache-deps.d.ts +16 -0
- package/types/server/cache-vary.d.ts +30 -0
- package/types/server/cloudflare.d.ts +163 -0
- package/types/server/create-app.d.ts +25 -0
- package/types/server/data-cache.d.ts +116 -0
- package/types/server/dev/devtools.d.ts +44 -0
- package/types/server/dev/report.d.ts +229 -0
- package/types/server/dev/socket.d.ts +17 -0
- package/types/server/dev/version-check.d.mts +15 -0
- package/types/server/ejs-adapter.d.ts +11 -0
- package/types/server/head-hints.d.ts +40 -0
- package/types/server/html-cache.d.ts +207 -0
- package/types/server/image-optimizer.d.ts +68 -0
- package/types/server/logs/access-middleware.d.ts +7 -0
- package/types/server/logs/file-sink.d.ts +17 -0
- package/types/server/logs/pipeline.d.ts +37 -0
- package/types/server/logs/s3-put.d.ts +85 -0
- package/types/server/logs/s3-sink.d.ts +26 -0
- package/types/server/metadata.d.ts +38 -0
- package/types/server/middleware/compression.d.ts +17 -0
- package/types/server/middleware/csrf.d.ts +4 -0
- package/types/server/middleware/dev-gate.d.ts +2 -0
- package/types/server/middleware/headers.d.ts +2 -0
- package/types/server/middleware/redirects.d.ts +2 -0
- package/types/server/middleware/robots-txt.d.ts +33 -0
- package/types/server/middleware/static-precompressed.d.ts +5 -0
- package/types/server/middleware/trailing-slash.d.ts +11 -0
- package/types/server/middleware/upstream-proxy.d.ts +21 -0
- package/types/server/og-image.d.ts +149 -0
- package/types/server/port-guard.d.ts +50 -0
- package/types/server/prewarm.d.ts +131 -0
- package/types/server/redis.d.ts +163 -0
- package/types/server/render.d.ts +101 -0
- package/types/server/router.d.ts +5 -0
- package/types/server/status-page.d.ts +24 -0
- package/types/server/upstream-limiter.d.ts +123 -0
- package/types/server/upstream-tracking.d.ts +42 -0
- package/types/shared/cookie-domain.d.ts +29 -0
- package/types/templates/layout.render.d.ts +7 -0
- package/types/version.d.mts +10 -0
- package/types/views/components/loader.d.ts +5 -0
- package/types/views/helpers/html.d.ts +39 -0
- package/types/views/helpers/tags.d.ts +127 -0
package/docs/en/11-migration.md
CHANGED
|
@@ -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.
|
|
35
|
-
| Server component (RSC) | Controller +
|
|
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.**
|
|
92
|
-
|
|
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
|
-
```
|
|
176
|
-
|
|
201
|
+
```jsk
|
|
202
|
+
{# views/pages/article.jsk #}
|
|
177
203
|
<article class="wrapper">
|
|
178
|
-
<h1 class="text-3xl font-bold"
|
|
179
|
-
|
|
180
|
-
<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.
|
|
222
|
-
default layout (`
|
|
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
|
-
|
|
268
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
6
|
-
|
|
7
|
-
lives in process memory with
|
|
8
|
-
plain JavaScript
|
|
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) |
|
|
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
|
|
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/
|
|
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
|
|
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.
|
|
4
|
-
"description": "A framework that feels like no framework: Express 5 + build-time .jsk (
|
|
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
|
-
"
|
|
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
|
-
".":
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"./
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
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
|
-
"
|
|
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`
|
|
5
|
-
* yüklenen ortak island bootstrap'ıdır; ek
|
|
6
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|
package/src/client/index.js
CHANGED
|
@@ -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.
|
|
9
|
+
* counter: () => import("../islands/counter.ts"),
|
|
10
10
|
* });
|
|
11
11
|
*
|
|
12
12
|
* start();
|
package/src/compile/codegen.js
CHANGED
|
@@ -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
|
}
|