hono-svelte 0.4.2 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,195 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ - **fix(shell): nested layouts nest outermost first.** With a root
6
+ `layout.svelte` plus `<dir>/layout.svelte`, the server wrapped the page in
7
+ the root layout first, so the directory layout ended up outside.
8
+ - **fix(vite): client pages mount the whole layout chain.** The client entry
9
+ imported every layout but only mounted `Layout0`; inner layouts vanished.
10
+ Client entries are never server-rendered, so the chain is now mounted
11
+ (no hydration of an empty root).
12
+ - **fix(vite): components imported by static pages and layouts keep their
13
+ `<style>`.** Only the page/layout files were compiled with
14
+ `css: "injected"` for the server; a layout's nav or a component-library
15
+ component lost its CSS on zero-JS pages.
16
+ - **fix(handlers): `errorHandler` keeps the headers of `HTTPException`
17
+ responses** — `basicAuth`/`bearerAuth` 401s now carry `WWW-Authenticate`.
18
+ - **fix(shell): prefetch.** `prefetch: "all"` no longer preloads static pages
19
+ (no JS chunk → 404); `"hover"` dedupes its links (it looked for
20
+ `rel="prefetch"` but created `modulepreload`) and maps `/` → `index` and
21
+ `/dir` → `dir/index`.
22
+ - **fix(shell):** the global stylesheet fallback follows `assetsBase`; a
23
+ throwing `nonce()` is logged instead of silently dropping the CSP nonce.
24
+ - **fix(vite):** `pagesDir` outside the Vite root (including another drive
25
+ on Windows) fails with a clear English error.
26
+ - **test:** `test/regressions.test.ts` (one test per fix) and a Playwright
27
+ contract suite in `e2e/` that runs the same browser tests against
28
+ hono-svelte on Node, on Bun, and against SvelteKit as the reference. CI
29
+ adds the e2e job and a peer-dependency matrix (declared minimums and
30
+ latest within the majors).
31
+
32
+ ## 0.6.0
33
+
34
+ - **feat(shell): `htmlAttributes` option.** Only `lang` could be set on
35
+ `<html>`, so themes driven by a root attribute (DaisyUI `data-theme`,
36
+ `class="dark"`) needed CSS workarounds. `htmlAttributes` takes a record
37
+ or `(c) => record` (per-request, e.g. from a cookie); values are escaped,
38
+ invalid names (and `lang`, which has its own option) throw.
39
+
40
+ - **feat(handlers): error pages get `{ status, message }`; JSON for API
41
+ paths.** `notFoundHandler`/`errorHandler` rendered the page without any
42
+ data, always with 404/500, and had no way to keep API routes on JSON.
43
+ Now: pages receive `{ status, message }` (merged under `opts.data`);
44
+ `errorHandler` uses the status carried by the error (`HTTPException`
45
+ and friends), exposes the error message only for 4xx (`expose` to
46
+ customize, `message` for the generic text) and logs only 5xx; new
47
+ `json: (c) => boolean` answers `{ message }` JSON with the same status.
48
+ `notFoundHandler` takes a `message`. `ErrorPageData` type exported.
49
+
50
+ - **feat: `getPage()` from `hono-svelte/page`.** Layouts had no way to
51
+ know which route rendered them (active-link logic needed the route
52
+ threaded through every page's `data`). `getPage()` returns `{ path,
53
+ route }` (request pathname + Hono route pattern, e.g. `/users/:id`) in
54
+ server-rendered components and client pages alike: the shell provides it
55
+ as Svelte context on SSR and writes `data-hs-path`/`data-hs-route` on the
56
+ root element, which the generated client entry passes as `context` to
57
+ `mount`/`hydrate`. New `./page` export; `PageInfo` type exported from the
58
+ main entry.
59
+
60
+ - **feat(shell): global stylesheet resolved from the manifest
61
+ (`stylesEntry`).** With `assets: manifest`, the global CSS kept pointing at
62
+ the unhashed `/static/styles.css`, so hashed client builds needed a
63
+ hand-written `stylesHref: "/" + manifest["src/styles.css"].file`. The
64
+ shell now resolves it from the manifest. New `stylesEntry` option
65
+ (default `"src/styles.css"`) names the source file for both modes: dev
66
+ links `/<stylesEntry>`, prod the hashed file (fallback
67
+ `/static/styles.css`). `stylesHref` still wins when set.
68
+
69
+ - **feat(types): `Entry` type exported from `hono-svelte`.** The generated
70
+ `.d.ts` now also augments the package (`interface Register { entries }`),
71
+ so helpers that forward an entry to `c.render` can be typed with
72
+ `import type { Entry } from "hono-svelte"` instead of importing the
73
+ generated file by relative path. Without the `.d.ts`, `Entry` is
74
+ `string`. A new `test/types.test.ts` runs `tsc` against the generated
75
+ declarations.
76
+
77
+ - **fix(shell): the page's `<svelte:head>` title and description win.**
78
+ The shell always emitted its default `<title>` first and browsers use the
79
+ first one, so a static page's `<svelte:head><title>` was ignored (and
80
+ `shell({ head })` descriptions were duplicated). The document now keeps a
81
+ single title — `c.render(entry, { title })` > page/layout `<svelte:head>`
82
+ > `shell({ title })` — and a page description (`<svelte:head>` or
83
+ `head.description`) removes the one from `shell({ head })`.
84
+
85
+ - **fix(assets): CSS of shared chunks is linked in production.** Vite
86
+ attaches a module's CSS to the chunk that owns it, so a layout or
87
+ component shared by several pages gets its own chunk + CSS file.
88
+ `manifestCssFor`/`manifestImportsFor` (and therefore `shell({ assets:
89
+ manifest })`) only read the entry chunk: pages rendered without their
90
+ layout's styles. Both now walk the static `imports` graph (shared chunks
91
+ first, cycles handled, `dynamicImports` skipped). `ViteManifestChunk`
92
+ gains the optional `src`, `name` and `dynamicImports` fields.
93
+ - **fix(vite): static pages and layouts keep their `<style>` after `vite
94
+ build`.** Server-rendered components have no client bundle, but with
95
+ vite-plugin-svelte's default `emitCss: true` the compiler emits their CSS
96
+ as an import that SSR drops — zero-JS pages shipped unstyled. `pages()`
97
+ now loads static pages and layouts for the server with
98
+ `css: "injected"` (merged into an existing `<svelte:options>`, explicit
99
+ `css` respected), so `render()` returns the styles in `head`. New
100
+ `test/build.test.ts` exercises the real `vite build --ssr` pipeline.
101
+
102
+ - **fix(scaffold): `isSafeAppPath` normalizes absolute targets.** An
103
+ absolute path such as `/app/../evil.ts` passed the in-root prefix check
104
+ on POSIX (only relative targets went through `resolve()`). Every target is
105
+ now resolved before the comparison.
106
+ - **CI green on main**: the Windows-only `isSafeAppPath` assertions run only
107
+ on win32 (drive letters are relative segments on POSIX); the nested-layout
108
+ test uses its temp dir as the Vite root (it only passed on Windows because
109
+ tmpdir sits on another drive); the `example` job installs Bun, which its
110
+ build script calls.
111
+
112
+ ## 0.5.1
113
+
114
+ - **Scaffold hardening** (`hono-svelte init`):
115
+ - the generated `package.json` pins the security floors
116
+ (`hono ^4.13.8`, `svelte ^5.57.0`, `vite ^8.3.0`) — the previous
117
+ `^4`/`^5`/`^8` ranges resolved versions with known advisories; a
118
+ regression test keeps the floors in sync with the package peers.
119
+ - `init` writes an initial `src/hono-svelte-entries.d.ts` stub
120
+ (plain-string renderer): `tsc` right after `init` used to fail on
121
+ `c.render(entry, props)` (2 args vs hono's 1-arg default renderer)
122
+ because the typed dts only exists after the first vite run;
123
+ `pages({ dts: true })` replaces the stub with the typed union on the
124
+ first `dev`/`build`.
125
+ - **`c.render()` coexistence contract**: 6 tests pin the shell against
126
+ hono's renderer semantics — `c.render(string)` (default renderer) is
127
+ reinterpreted as an entry name under the shell (strict 500); renderer
128
+ middlewares on the same path pattern are last-registered-wins; and
129
+ **path-scoped coexistence works**: default-render routes and
130
+ jsxRenderer-style middlewares keep working next to a `shell()` mounted on
131
+ its own path (`app.use("/pages/*", shell())`). Documented in the README
132
+ ("Coexistence with other renderers").
133
+ - **CI**: new `scaffold` job smoke-tests `init --full` end to end (install
134
+ with the local package via `file:` → typecheck → build), guarding the
135
+ inline templates against drift from the (unpublished) playground example.
136
+
137
+ ## 0.5.0
138
+
139
+ - **Per-page data typing (opt-in, `pages({ dts: true })`)**: declare
140
+ `export type Data` (or `interface Data`) in a page's `<script module>` and
141
+ the generated `.d.ts` adds a typed overload per page —
142
+ `c.render("dashboard", { data })` is checked by `tsc` (a missing/mistyped
143
+ field fails the typecheck instead of shipping `undefined`). The declaration
144
+ is inlined verbatim, so it must be self-contained; types that reference
145
+ imports are skipped with a one-time warning and keep
146
+ `Record<string, unknown>` data. `RenderProps` is now generic
147
+ (`RenderProps<TData = Record<string, unknown>>`) and `pages().dataTypes()`
148
+ exposes the extractions. The dts also regenerates when a page's `Data`
149
+ type changes (dev watcher + `buildStart`).
150
+ - **Neutral `<body>`**: the shell no longer hardcodes DaisyUI classes
151
+ (`bg-base-200 min-h-screen text-base-content`) — it emits a plain,
152
+ CSS-framework-agnostic document. Style `body`/`html` from your global
153
+ stylesheet instead (`body { background: var(--color-base-200); ... }`, or
154
+ `@apply` with Tailwind — the playground does exactly that in
155
+ `styles.css`). **Breaking** for apps relying on the hardcoded classes.
156
+ - **`shell({ ssrFallback })`**: when SSR of a page throws at request time,
157
+ log the error and fall back to client rendering (entry script) instead of
158
+ a 500. Default `false` keeps the previous behavior.
159
+ - `prefetch: "all"` / `"hover"` now emit `rel="modulepreload"` (prefetch on a
160
+ module URL could double-fetch in some browsers; modulepreload also warms
161
+ the entry's import graph).
162
+ - `dataLimit` warning fires once per **(page, limit)** instead of once per
163
+ process — a second oversized page is reported too, with the entry name in
164
+ the message.
165
+ - **Removed dead stubs**: `versionsMatch()` (always returned `true`), the
166
+ unused `hasClient` export from the ssr-manifest stub/virtual manifest, and
167
+ the no-op `checkPagesDir`. **Breaking: `versionsMatch` is no longer
168
+ exported.**
169
+ - Fully typed handlers/middleware: `notFoundHandler()` / `errorHandler()`
170
+ match Hono's `NotFoundHandler`/`ErrorHandler` signatures and `shell()` no
171
+ longer uses `any` internally.
172
+ - **CLI: `hono-svelte add page <name>`** — scaffolds
173
+ `src/pages/<name>.svelte` (static by default, skip-if-exists, traversal
174
+ refused). `isValidPageName()` / `newPageFile()` exported for tests.
175
+ - Metadata: `engines` now declares `bun >= 1.4` and `node >= 26`, the
176
+ `./package.json` export subpath was added, and TypeScript 7 builds the
177
+ package.
178
+ - **Security floors in `peerDependencies`**: `hono ^4.13.8`, `svelte ^5.57.0`,
179
+ `vite ^8.3.0` — every published advisory sits outside these floors
180
+ (hono < 4.12.12 and 4.12.0–4.12.33, patched in 4.12.34; svelte ≤ 5.55.6,
181
+ patched in 5.55.7; vite ≤ 8.0.15, patched in 8.0.16; transitive `devalue`
182
+ resolves to the patched 5.9.2, which also closes CVE-2026-81176). The
183
+ versions installed in the repo and in the example are the latest published
184
+ and none is affected by a known advisory — the ranges just avoid
185
+ advertising vulnerable versions to consumers and scanners.
186
+ - CI: ubuntu + windows runners (Windows-only regressions like the old
187
+ `isSafeAppPath` bug are caught now), coverage via `@vitest/coverage-v8`
188
+ with thresholds (`npm test` runs it), all on the officially supported
189
+ runtimes (Node 26, TS 7, Vite 8, Bun 1.4+).
190
+ - `hono-svelte init` now scaffolds TypeScript 7 + `@types/node` 26 in the
191
+ generated app.
192
+
3
193
  ## 0.4.2
4
194
 
5
195
  - `hono-svelte init` now scaffolds a complete runnable app (works with
package/README.md CHANGED
@@ -15,10 +15,10 @@ Real sites and dashboards mix simple pages (landing, login, terms) with interact
15
15
 
16
16
  ## Installation
17
17
 
18
- Requires Node 22+, Hono 4, Svelte 5, and Vite 6/7/8.
18
+ Requires Bun 1.4+, Node 26+, Hono 4, Svelte 5, Vite 8, and TypeScript 7.
19
19
 
20
20
  ```sh
21
- npm install hono-svelte
21
+ bun add hono-svelte
22
22
  ```
23
23
 
24
24
  ## Quick start
@@ -115,6 +115,8 @@ app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
115
115
 
116
116
  The rule is simple: **if the `.svelte` file has no `<script>`, the page ships as plain HTML. If it has one, it hydrates on the client** with initial data available via `$props()`.
117
117
 
118
+ Server-rendered components (static pages and the layouts wrapping them) have no client bundle, so `pages()` compiles them for the server with `css: "injected"`: their `<style>` blocks arrive inline in the `<head>`. An explicit `<svelte:options css="...">` in the component wins.
119
+
118
120
  Files starting with `_` and `layout.svelte` are ignored (convention for partials and layouts).
119
121
 
120
122
  ## Passing data to the page
@@ -126,7 +128,9 @@ Works for static (zero-JS) pages too: the shell passes `data` as SSR props **and
126
128
  app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
127
129
  ```
128
130
 
129
- Large payloads trigger a one-time `console.warn` (default limit 100KB, `shell({ dataLimit })`).
131
+ Declare `export type Data` in the page's `<script module>` to type it per page — see [per-page data typing](#per-page-data-typing).
132
+
133
+ Large payloads trigger a `console.warn` — once per page (default limit 100KB, `shell({ dataLimit })`).
130
134
  Anything sensitive stays in the API (`hc<AppType>`, HttpOnly cookie). Never put secrets in `data`.
131
135
 
132
136
  ## Status, headers and per-page head
@@ -146,7 +150,7 @@ app.get("/post", (c) =>
146
150
  );
147
151
  ```
148
152
 
149
- `<svelte:head>` inside pages also works (shell forwards SSR `head`). Page `head` never emits executable inline scripts (CSP-safe; `extra` is for trusted raw HTML like fonts or JSON-LD).
153
+ `<svelte:head>` inside pages also works (shell forwards SSR `head`). The document keeps a single `<title>` and `<meta name="description">`: `c.render(entry, { title })` > the page/layout `<svelte:head>` > `shell({ title })`, and a page description (`<svelte:head>` or `head.description`) replaces one set in `shell({ head })`. Page `head` never emits executable inline scripts (CSP-safe; `extra` is for trusted raw HTML like fonts or JSON-LD).
150
154
 
151
155
  ## Nested layouts
152
156
 
@@ -180,6 +184,28 @@ c.render("dashboard/page1", { data: { title: "Page 1", description: "..." } });
180
184
 
181
185
  Layouts never become entries (`pages().layouts()` / `layoutChain(entry)` inspect them; `layouts: false` restores legacy ignore).
182
186
 
187
+ > **Hydration wrapper:** each layout level wraps the page's SSR output in one `<div>` (a Svelte 5 raw-snippet requirement — an empty snippet render breaks hydration). The wrapper is hydration-safe, but it is part of the DOM: account for it in CSS selectors (e.g. `:global(> div)`).
188
+
189
+
190
+ ## Current page (`getPage`)
191
+
192
+ Pages and layouts can read the request path and the Hono route pattern that rendered them — e.g. to highlight the active link in a layout nav, ignoring params:
193
+
194
+ ```svelte
195
+ <!-- src/pages/dashboard/layout.svelte -->
196
+ <script lang="ts">
197
+ import type { Snippet } from "svelte";
198
+ import { getPage } from "hono-svelte/page";
199
+
200
+ let { children }: { children: Snippet } = $props();
201
+ const page = getPage(); // { path: "/dashboard/users/7", route: "/dashboard/users/:id" }
202
+ </script>
203
+
204
+ <a href="/dashboard/users" aria-current={page.route.startsWith("/dashboard/users") ? "page" : undefined}>Users</a>
205
+ {@render children()}
206
+ ```
207
+
208
+ It works in server-rendered components (static pages + layouts) and in client pages: the shell passes it as Svelte context on the server and writes it on the root element for the client entry. Call it during component initialisation, like `getContext`.
183
209
 
184
210
  ## Error pages
185
211
 
@@ -190,27 +216,52 @@ app.notFound(notFoundHandler()); // renders `404.svelte` with status 404
190
216
  app.onError(errorHandler()); // renders `500.svelte` with status 500
191
217
  ```
192
218
 
219
+ Both pages receive `{ status, message }` as props (merged under `data`). `errorHandler` takes the status from errors that carry one (hono's `HTTPException(410, { message })` → 410) and only exposes the error's own message for 4xx — 5xx get `message` (default `"Internal Server Error"`) and are the only ones logged. `json` answers `{ message }` JSON instead of the page, e.g. for API routes:
220
+
221
+ ```ts
222
+ const isApi = (c: Context) => c.req.path.startsWith("/api/");
223
+
224
+ app.notFound(notFoundHandler({ json: isApi, message: "Page not found" }));
225
+ app.onError(errorHandler({ json: isApi, expose: (err, status) => status < 500 }));
226
+ ```
227
+
228
+ ```svelte
229
+ <!-- src/pages/500.svelte -->
230
+ <script lang="ts">
231
+ let { status, message }: { status: number; message: string } = $props();
232
+ </script>
233
+
234
+ <h1>{status}</h1>
235
+ <p>{message}</p>
236
+ ```
237
+
193
238
  ## CLI
194
239
 
195
240
  ```sh
196
- # Start a new app (works with npx, bunx or pnpm dlx — no install needed):
241
+ # Start a new app (no install needed):
197
242
  bunx hono-svelte init # minimal: 2 pages + routes + configs
198
243
  bunx hono-svelte init --full # + auth, dashboard w/ layout, typed RPC
199
244
 
200
- cd my-app && bun install && bun run dev # or: npm install && npm run dev
245
+ cd my-app && bun install && bun run dev
201
246
  ```
202
247
 
203
248
  `init` scaffolds a complete runnable app: `package.json`, `tsconfig.json`,
204
249
  `vite.config.ts` (with `pages({ dts: true })`), `src/pages/` and
205
250
  `src/routes/`. The `--full` flavor adds cookie-session auth, a dashboard with
206
251
  nested layout, and a typed API (`hc<AppType>`) — open `/auth`, sign in, explore
207
- `/dashboard`. Generated scripts run under npm and bun.
252
+ `/dashboard`. (npm/pnpm work too: `npx hono-svelte init`, `npm install`.)
208
253
 
209
254
  ```sh
210
- npx hono-svelte doctor # check dist, link and vite config
211
- npx hono-svelte doctor --app=./my-app # check another app directory
255
+ bunx hono-svelte add page blog/post-1 # create src/pages/blog/post-1.svelte
256
+ bunx hono-svelte doctor # check dist, link and vite config
257
+ bunx hono-svelte doctor --app=./my-app # check another app directory
212
258
  ```
213
259
 
260
+ `add page <name>` writes a static page (no `<script>` → zero JS; add one to
261
+ hydrate on the client). Existing files are skipped, traversal names are
262
+ refused, and the default pagesDir is `src/pages` (a custom one set via
263
+ `pages({ pagesDir })` is not visible to the CLI — adjust the path manually).
264
+
214
265
  ## API
215
266
 
216
267
  ### `shell(options?)`
@@ -219,25 +270,49 @@ Hono middleware that provides `c.render(entry, { title?, data?, status?, headers
219
270
 
220
271
  | Option | Default | Description |
221
272
  |---|---|---|
222
- | `title` | `"App"` | Title used when the route doesn't provide one |
273
+ | `title` | `"App"` | Title used when neither `c.render(entry, { title })` nor the page's `<svelte:head>` provides one |
223
274
  | `lang` | `"en"` | `<html>` `lang` attribute |
275
+ | `htmlAttributes` | — | Extra `<html>` attributes (`{ "data-theme": "dark" }`) or `(c) => attrs` per request; values escaped |
224
276
  | `assetsBase` | `"/static"` | Prefix for JS files in production (legacy; prefer `assets`) |
225
277
  | `assets` | `"/static"` | String prefix, Vite manifest object, or resolver `(entry) => url` (hashed files) |
226
278
  | `isProd` | auto | Force prod/dev asset behavior |
227
- | `stylesHref` | `"/static/styles.css"` in prod, `"/src/styles.css"` in dev | Global stylesheet (or an `(isProd) => string` function) |
279
+ | `stylesHref` | from `stylesEntry` | Global stylesheet href (or an `(isProd) => string` function); overrides `stylesEntry` |
280
+ | `stylesEntry` | `"src/styles.css"` | Source of the global stylesheet: dev links `/<stylesEntry>`, prod resolves the hashed file from the manifest in `assets` (fallback `/static/styles.css`) |
228
281
  | `styles` | `[]` | Extra CSS hrefs emitted per response |
229
282
  | `head` | `""` | Extra HTML in `<head>` (fonts, meta tags) |
230
283
  | `nonce` | — | CSP nonce string or `(c) => string` (applied to shell `<script>`/`<link>`) |
231
284
  | `preload` | `true` in prod | `<link rel="modulepreload">` for the entry (+ manifest imports) |
232
- | `prefetch` | `false` | `"all"` prefetches other entries; `"hover"` injects a hover-prefetch script |
285
+ | `prefetch` | `false` | `"all"` emits `modulepreload` for other entries; `"hover"` injects a script that preloads them on link hover |
233
286
  | `dataLimit` | `102400` | Warn once above this many `data` bytes (`false` disables) |
234
287
  | `status` | `200` | Default response status |
235
288
  | `headers` | `{}` | Default response headers (per-render `headers` merge over them) |
236
289
  | `strict` | `true` | Unknown entries throw a dev-friendly error; `false` disables |
290
+ | `ssrFallback` | `false` | SSR failures log and fall back to client rendering instead of a 500 |
237
291
  | `knownEntries` | auto from manifest | Override the page list used by strict mode and prefetch |
238
292
 
239
293
  `shell()` also exposes `availableEntries()` for diagnostics.
240
294
 
295
+ #### Coexistence with other renderers
296
+
297
+ `c.render(entry)` rides on hono's renderer mechanism — one renderer per
298
+ request, **last-registered wins** (the same contract as hono's own
299
+ `hono/jsx` `jsx-renderer`). In practice:
300
+
301
+ - **Path-scoped coexistence works**: mount the shell only where pages live
302
+ (`app.use("/pages/*", shell())`, registered after broader renderer
303
+ middlewares) — existing routes using hono's default `c.render(string)` or
304
+ `jsxRenderer()` keep working on their own paths, each renderer serves its
305
+ routes (pinned by `test/render-conflicts.test.ts`).
306
+ - Two renderer middlewares on the **same pattern** (e.g. two shells on `/*`,
307
+ or shell + `jsxRenderer` on `/*`): the last one registered silently
308
+ replaces the earlier one — keep one renderer per path pattern.
309
+ - With more than one `shell()` on nested paths, register the **more
310
+ specific one last** — it then overrides the catch-all for its paths.
311
+ - `c.render("some string")` (hono's default string renderer) is reinterpreted
312
+ by the shell as an entry name — under `strict` it fails loudly with the
313
+ available pages list. Use `c.html(...)` for raw string responses, or scope
314
+ the shell away from those routes.
315
+
241
316
  ### `pages(options?)`
242
317
 
243
318
  Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entries.
@@ -250,7 +325,7 @@ Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entri
250
325
  | `dts` | `false` | Typed `c.render` entries: `true` writes `src/hono-svelte-entries.d.ts`, a string sets a custom path |
251
326
  | `alwaysClient` | `[]` | Pages that always get JS, even without `<script>` |
252
327
 
253
- Handy methods: `input()` (client build entries), `entries()` (all pages), `staticEntries()` (static pages only), `hasClient(entry)`, `layouts()`, `layoutChain(entry)`, `types()` (entry union), `typeDeclarations()` (hono module snippet), `dtsPath()` / `writeDts()` (generated `.d.ts`), `validateConfig()` (fail fast on bad Vite setup).
328
+ Handy methods: `input()` (client build entries), `entries()` (all pages), `staticEntries()` (static pages only), `hasClient(entry)`, `layouts()`, `layoutChain(entry)`, `dataTypes()` (extracted `Data` declarations), `types()` (entry union), `typeDeclarations()` (hono module snippet), `dtsPath()` / `writeDts()` (generated `.d.ts`), `validateConfig()` (fail fast on bad Vite setup).
254
329
 
255
330
  ### Typing `c.render`
256
331
 
@@ -272,6 +347,12 @@ const appPages = pages({ dts: true }); // writes src/hono-svelte-entries.d.ts
272
347
  // generated src/hono-svelte-entries.d.ts (commit it — CI needs no Vite run)
273
348
  export type HonoSvelteEntries = "auth" | "dashboard/index" | "index";
274
349
 
350
+ declare module "hono-svelte" {
351
+ interface Register {
352
+ entries: HonoSvelteEntries;
353
+ }
354
+ }
355
+
275
356
  declare module "hono" {
276
357
  interface ContextRenderer {
277
358
  (entryName: HonoSvelteEntries, props?: import("hono-svelte").RenderProps): Response | Promise<Response>;
@@ -279,6 +360,14 @@ declare module "hono" {
279
360
  }
280
361
  ```
281
362
 
363
+ Helpers that forward an entry to `c.render` use the `Entry` type exported by the package — the generated union once the `.d.ts` is in the program, `string` without it. No import of the generated file needed:
364
+
365
+ ```ts
366
+ import type { Entry } from "hono-svelte";
367
+
368
+ export const page = (entry: Entry, title: string) => (c: Context) => c.render(entry, { title });
369
+ ```
370
+
282
371
  `dts: "path/to/entries.d.ts"` sets a custom path (relative to the Vite root). The file regenerates on `buildStart` and via the dev watcher when entries change. Without `dts` (default `false`), declare the renderer manually with a plain `string`:
283
372
 
284
373
  ```ts
@@ -291,13 +380,57 @@ declare module "hono" {
291
380
  }
292
381
  ```
293
382
 
294
- `data` stays `Record<string, unknown>` — per-page payload types are a future feature. Programmatic access: `pages().types()` → `"admin" | "home"`, `pages().typeDeclarations()` for the snippet, `dtsPath()` / `writeDts()` for the file.
383
+ `data` stays `Record<string, unknown>` by default — declare a `Data` type in the page for per-page typing (below). Programmatic access: `pages().types()` → `"admin" | "home"`, `pages().typeDeclarations()` for the snippet, `dtsPath()` / `writeDts()` for the file.
384
+
385
+ ### Per-page data typing
386
+
387
+ Declare the page payload once — the `Data` type in a `<script module>` — and reuse it as the page props:
388
+
389
+ ```svelte
390
+ <!-- src/pages/dashboard.svelte -->
391
+ <script module lang="ts">
392
+ export type Data = { plan: string; seats: number };
393
+ </script>
394
+
395
+ <script lang="ts">
396
+ let { plan, seats }: Data = $props();
397
+ </script>
398
+
399
+ <h1>{plan} — {seats} seats</h1>
400
+ ```
401
+
402
+ With `pages({ dts: true })`, the generated `.d.ts` adds a typed overload per
403
+ page, so `c.render("dashboard", { data: { plan: "pro" } })` is checked by
404
+ `tsc` — a missing `plan` fails the typecheck instead of shipping `undefined`
405
+ to the client:
406
+
407
+ ```ts
408
+ // generated (excerpt)
409
+ type HonoSvelteData_dashboard = { plan: string; seats: number };
410
+
411
+ declare module "hono" {
412
+ interface ContextRenderer {
413
+ (entryName: "dashboard", props?: import("hono-svelte").RenderProps<HonoSvelteData_dashboard>): Response | Promise<Response>;
414
+ (entryName: HonoSvelteEntries, props?: import("hono-svelte").RenderProps): Response | Promise<Response>;
415
+ }
416
+ }
417
+ ```
418
+
419
+ The `Data` declaration is inlined verbatim into the dts, so it must be
420
+ **self-contained** — inline shapes or built-ins only. References to imported
421
+ types can't be resolved from the dts location; such pages keep
422
+ `Record<string, unknown>` data with a one-time warning. Both
423
+ `export type Data = ...` and `export interface Data { ... }` work (including
424
+ legacy `<script context="module">`). `pages().dataTypes()` exposes the
425
+ extracted declarations.
295
426
 
296
427
  ## Production tips
297
428
 
298
- - Pass the Vite manifest to `shell({ assets: manifest })` for hashed files; manifest CSS and shared chunks are emitted automatically (`modulepreload`).
429
+ - Pass the Vite manifest to `shell({ assets: manifest })` for hashed files — including the global stylesheet: with `styles: resolve("src/styles.css")` in the client input (or `stylesEntry` pointing at another file), its hashed name comes from the manifest, no hand-written `stylesHref`. The shell walks the entry's static import graph: the CSS of shared chunks (layouts, components used by several pages) is linked together with the entry's own CSS, and every reachable chunk gets a `modulepreload`.
299
430
  - Serve hashed files with long cache and `immutable` (`immutableHeaders()` / `CACHE_IMMUTABLE`); the HTML shell itself suits `CACHE_NO_STORE`.
300
431
  - CSP: pass `shell({ nonce })` (string or from `secureHeaders()` context) — shell tags carry the nonce. Without a nonce, static pages keep working with `script-src 'self'` (no executable inline script, except the opt-in `prefetch: "hover"` helper).
432
+ - Theme attributes on the root element: `shell({ htmlAttributes: { "data-theme": "dark" } })` (DaisyUI), or a function reading a cookie for per-user themes.
433
+ - The shell emits a neutral `<body>` (no framework classes) — style `body`/`html` from your global stylesheet (`stylesHref`), e.g. `body { @apply bg-base-200 min-h-screen text-base-content; }` with Tailwind/DaisyUI.
301
434
  - Run both client and server builds before serving; the example in `examples/playground/` shows the full setup.
302
435
 
303
436
  ## Example
@@ -305,9 +438,15 @@ declare module "hono" {
305
438
  `examples/playground/` is a real Hono app using the package: static landing, login, and a dashboard with typed RPC and cookie session. To run it:
306
439
 
307
440
  ```sh
308
- cd examples/playground && npm install && npm run build
441
+ cd examples/playground && bun install && bun run build
309
442
  ```
310
443
 
444
+ ## Testing
445
+
446
+ - `npm test` — unit + integration suite (vitest, real `vite build --ssr` included), with coverage thresholds.
447
+ - `npm run test:e2e` — Playwright contract suite in [`e2e/`](./e2e/README.md): the same browser tests run against hono-svelte on Node, on Bun, and against an equivalent SvelteKit app used as the reference implementation (layout nesting, `<title>` precedence, scoped CSS, interactivity, console errors, broken assets).
448
+ - CI also runs the suite against the minimum and the latest peer versions (`hono`, `svelte`, `vite`).
449
+
311
450
  ## License
312
451
 
313
452
  MIT — see [LICENSE](./LICENSE). Changelog in [CHANGELOG.md](./CHANGELOG.md).
package/dist/assets.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export type ViteManifestChunk = {
2
2
  file: string;
3
+ src?: string;
4
+ name?: string;
3
5
  css?: string[];
4
6
  imports?: string[];
7
+ dynamicImports?: string[];
5
8
  isEntry?: boolean;
6
9
  };
7
10
  export type ViteManifest = Record<string, ViteManifestChunk>;
@@ -15,9 +18,16 @@ export type ManifestAssetsOptions = {
15
18
  /** Manifest keys tried for an entry (virtual ids first, bare entry last). */
16
19
  export declare function manifestKeysFor(entryName: string): string[];
17
20
  export declare function createManifestAssets(manifest: ViteManifest, opts?: ManifestAssetsOptions): AssetsResolver;
18
- /** Hashed CSS files for an entry, via the Vite manifest. */
21
+ /**
22
+ * Hashed CSS files for an entry, via the Vite manifest — including the CSS
23
+ * of every statically imported chunk (shared layouts, components), shared
24
+ * chunks first so the entry's own rules come last.
25
+ */
19
26
  export declare function manifestCssFor(manifest: ViteManifest, entryName: string, prefix?: string): string[];
20
- /** Shared-chunk URLs (for `modulepreload`), via the Vite manifest. */
27
+ /**
28
+ * Shared-chunk URLs (for `modulepreload`), via the Vite manifest: every chunk
29
+ * the entry reaches through static imports, not only the direct ones.
30
+ */
21
31
  export declare function manifestImportsFor(manifest: ViteManifest, entryName: string, prefix?: string): string[];
22
32
  /** Long-term caching for hashed `/static` files. */
23
33
  export declare const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
package/dist/assets.js CHANGED
@@ -33,23 +33,54 @@ export function createManifestAssets(manifest, opts = {}) {
33
33
  return `${fallbackBase}/${entryName}.js`;
34
34
  };
35
35
  }
36
- /** Hashed CSS files for an entry, via the Vite manifest. */
36
+ /**
37
+ * The entry chunk plus every chunk it reaches through static `imports`,
38
+ * dependencies first (the order the browser evaluates them). Vite attaches
39
+ * a module's CSS to the chunk that owns the module — a layout or component
40
+ * shared by several pages lives in its own chunk, so reading only the entry
41
+ * chunk misses its styles. `dynamicImports` are not followed: they load
42
+ * later, with their own CSS handling.
43
+ */
44
+ function staticChunkGraph(manifest, entryName) {
45
+ const entry = findChunk(manifest, entryName);
46
+ if (!entry)
47
+ return [];
48
+ const ordered = [];
49
+ const seen = new Set();
50
+ const visit = (chunk) => {
51
+ if (seen.has(chunk))
52
+ return;
53
+ seen.add(chunk);
54
+ for (const key of chunk.imports ?? []) {
55
+ const dep = manifest[key];
56
+ if (dep)
57
+ visit(dep);
58
+ }
59
+ ordered.push(chunk);
60
+ };
61
+ visit(entry);
62
+ return ordered;
63
+ }
64
+ function unique(values) {
65
+ return [...new Set(values)];
66
+ }
67
+ /**
68
+ * Hashed CSS files for an entry, via the Vite manifest — including the CSS
69
+ * of every statically imported chunk (shared layouts, components), shared
70
+ * chunks first so the entry's own rules come last.
71
+ */
37
72
  export function manifestCssFor(manifest, entryName, prefix = "/") {
38
- const chunk = findChunk(manifest, entryName);
39
- return (chunk?.css ?? []).map((f) => joinPrefix(prefix, f));
73
+ return unique(staticChunkGraph(manifest, entryName).flatMap((chunk) => (chunk.css ?? []).map((f) => joinPrefix(prefix, f))));
40
74
  }
41
- /** Shared-chunk URLs (for `modulepreload`), via the Vite manifest. */
75
+ /**
76
+ * Shared-chunk URLs (for `modulepreload`), via the Vite manifest: every chunk
77
+ * the entry reaches through static imports, not only the direct ones.
78
+ */
42
79
  export function manifestImportsFor(manifest, entryName, prefix = "/") {
43
- const chunk = findChunk(manifest, entryName);
44
- if (!chunk)
45
- return [];
46
- const out = [];
47
- for (const imp of chunk.imports ?? []) {
48
- const dep = manifest[imp];
49
- if (dep?.file)
50
- out.push(joinPrefix(prefix, dep.file));
51
- }
52
- return out;
80
+ const entry = findChunk(manifest, entryName);
81
+ return unique(staticChunkGraph(manifest, entryName)
82
+ .filter((chunk) => chunk !== entry && chunk.file)
83
+ .map((chunk) => joinPrefix(prefix, chunk.file)));
53
84
  }
54
85
  /** Long-term caching for hashed `/static` files. */
55
86
  export const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";