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 +190 -0
- package/README.md +155 -16
- package/dist/assets.d.ts +12 -2
- package/dist/assets.js +45 -14
- package/dist/cli.js +41 -7
- package/dist/handlers.d.ts +24 -2
- package/dist/handlers.js +61 -18
- package/dist/index.d.ts +51 -9
- package/dist/index.js +128 -43
- package/dist/page-data.d.ts +7 -0
- package/dist/page-data.js +119 -0
- package/dist/page.d.ts +23 -0
- package/dist/page.js +24 -0
- package/dist/scaffold.d.ts +8 -1
- package/dist/scaffold.js +79 -18
- package/dist/ssr-manifest.d.ts +0 -1
- package/dist/ssr-manifest.js +0 -4
- package/dist/vite.d.ts +11 -5
- package/dist/vite.js +165 -64
- package/package.json +45 -37
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
|
|
18
|
+
Requires Bun 1.4+, Node 26+, Hono 4, Svelte 5, Vite 8, and TypeScript 7.
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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`.
|
|
252
|
+
`/dashboard`. (npm/pnpm work too: `npx hono-svelte init`, `npm install`.)
|
|
208
253
|
|
|
209
254
|
```sh
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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` |
|
|
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"`
|
|
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>` —
|
|
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
|
|
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 &&
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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";
|