hono-svelte 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ - **Typed `c.render` entries (opt-in)**: `pages({ dts })` writes a `.d.ts` file
6
+ with the entry union (`HonoSvelteEntries`) + `declare module "hono"`
7
+ augmentation. `true` writes `src/hono-svelte-entries.d.ts` (relative to the
8
+ Vite root); a string sets a custom path. Regenerated on `buildStart` and by
9
+ the dev watcher when entries change. `dtsPath()` / `writeDts()` exposed on
10
+ the plugin. The playground wires it (`pages({ dts: true })` +
11
+ `/// <reference path="./hono-svelte-entries.d.ts" />` in `env.d.ts`), so a
12
+ typo in `c.render("...")` fails `tsc` instead of 500ing at runtime.
13
+ `data` stays `Record<string, unknown>` (per-page payload types are a future
14
+ feature).
15
+ - **Layouts, single source of truth**: `<dir>/layout.svelte` wraps pages
16
+ automatically (SSR inside-out + client hydrate, `layouts()` /
17
+ `layoutChain()` to inspect, `layouts: false` = legacy ignore). Pages must
18
+ NOT import their layout manually — the plugin fails fast at build time
19
+ (`manually imports ... but the layout is applied automatically`) instead of
20
+ rendering the layout twice. Layout props (`title`, `description`) come from
21
+ the route via `c.render(entry, { data })`, with `title = "Dashboard"`
22
+ fallback in the layout. Rule shared by plugin and shell: static means
23
+ "no `<script>`", layouts don't matter; `__clientEntries` travels in the
24
+ virtual manifest so the shell picks the SSR branch for static+layouts pages.
25
+ - **Runtime fixes**: static `import.meta.env.PROD/DEV` access (Vite module
26
+ runner forbids dynamic access — dev 500 fixed); static
27
+ `createRawSnippet` import in layout entries (no duplicated Svelte runtime —
28
+ `Illegal invocation` fixed); layout children snippet renders `<div>` wrapper
29
+ (empty render broke hydration — `Cannot set properties of null` fixed);
30
+ bundled server defaults to prod so plain `node ./dist/index.js` works on
31
+ Windows with no `NODE_ENV` prefix.
32
+ - `c.render(entry, { status, headers, head })`: per-render status/headers
33
+ (merged over shell defaults) and CSP-safe per-page head tags
34
+ (`description`, `canonical`, `og`, `twitter`, `meta`, `extra`).
35
+ - SSR static pages now receive `data` as component props (plus the embedded JSON).
36
+ - Strict entries: unknown entryNames throw a dev-friendly error (`Available pages: ...`, generic in prod); `shell({ strict: false })` disables; `availableEntries()` exposes the list.
37
+ - `pages().types()` / `typeDeclarations()` generate the entry union + hono module snippet.
38
+ - Nested layouts: `<dir>/layout.svelte` wraps pages (SSR inside-out, `layouts()`/`layoutChain()`, `layouts: false` = legacy).
39
+ - `notFoundHandler()` / `errorHandler()` for conventional `404.svelte` / `500.svelte` pages.
40
+ - `hono-svelte init|doctor` CLI (`bin`) with `--app` flag.
41
+ - `pages().validateConfig()` fails fast on missing svelte plugin / pagesDir.
42
+ - `shell({ assets })` accepts a Vite manifest (hashed URLs, CSS, shared-chunk preload) with fallback; `preload`/`prefetch` controls; `styles` extra CSS.
43
+ - `shell({ nonce })` CSP support on shell tags; `svelte/server` CSP passthrough for SSR.
44
+ - Cache helpers: `immutableHeaders()`, `CACHE_IMMUTABLE`, `CACHE_NO_STORE`.
45
+ - `data` size one-time warning (`shell({ dataLimit })`, default 100KB).
46
+
47
+ ## 0.3.1
48
+
49
+ - Package metadata: repository/homepage/author, publishConfig, LICENSE + CHANGELOG in files.
50
+ - No code changes since 0.3.0.
51
+
52
+ ## 0.3.0
53
+
54
+ - Fully in-memory package: entries and manifest are virtual modules (no files written to disk).
55
+ - Automatic zero-JS: `.svelte` pages without `<script>` render on the server (`svelte/server`)
56
+ and download no JS on the client.
57
+ - Deterministic opaque IDs derived from the entryName (no salt, no env, no disk).
58
+ - Zero config: the shell resolves the SSR map on its own (relative import redirected by the plugin).
59
+ - Test suite (vitest) and CI (typecheck + tests + build + publint).
60
+
61
+ ## 0.2.0
62
+
63
+ - Opaque IDs (`r-` + hash) replace configurable `rootId`/`dataId`.
64
+
65
+ ## 0.1.0
66
+
67
+ - First version: `shell()` middleware + `pages()` plugin with disk-generated entries.
package/README.md CHANGED
@@ -1,71 +1,302 @@
1
1
  # hono-svelte
2
2
 
3
- Shell server-side + paginas Svelte multi-entry para apps Hono, 100% in-memory
4
- (nenhum arquivo gerado em disco) e com zero-JS automatico para paginas estaticas.
5
- Zero config: o app nao importa nada alem de `shell()` e `pages()`.
6
-
7
- - `hono-svelte` — middleware `shell()`: registra `c.render(entry, { title?, data? })`.
8
- - `data` (pequeno, publico, serializavel) vai embutido no HTML
9
- (`<script type="application/json">` com id opaco) e chega na pagina via `$props()`.
10
- - Dados privados, grandes ou mutaveis: buscar via RPC (`hc<AppType>`) apos o mount.
11
- - Zero config: o shell descobre sozinho quais paginas sao estaticas e
12
- renderiza no servidor via `svelte/server`, sem import nenhum no app.
13
- - `hono-svelte/vite` — plugin `pages()`: cada `*.svelte` em `pagesDir`
14
- (exceto parciais) vira um entry virtual com `mount()` automatico.
15
- Nada e escrito em disco.
16
-
17
- ## Uso
3
+ Render Svelte 5 pages in Hono apps with a single `c.render("page")` — no SPA, no build boilerplate.
4
+
5
+ Each `.svelte` file becomes an independent page. Static pages reach the browser as ready-made HTML with **zero JavaScript**. Interactive pages receive only their own JS. Works in dev with HMR and in production with per-page bundles.
6
+
7
+ ## Why use it
8
+
9
+ Real sites and dashboards mix simple pages (landing, login, terms) with interactive screens (panels, forms). In a traditional SPA, a landing visitor downloads the entire dashboard's JS. With hono-svelte:
10
+
11
+ - **Static pages cost zero JS** — HTML arrives ready from the server;
12
+ - **Interactive pages cost only their own JS** — independent bundles, no loading the rest of the app;
13
+ - **Public initial data ships in the HTML** — the page mounts with content, no extra fetch;
14
+ - **Sensitive data stays in the API** — typed end to end with the Hono client.
15
+
16
+ ## Installation
17
+
18
+ Requires Node 22+, Hono 4, Svelte 5, and Vite 6/7/8.
19
+
20
+ ```sh
21
+ npm install hono-svelte
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ **1. Configure Vite** — add the pages plugin in every mode and use the entry list in the client build:
18
27
 
19
28
  ```ts
20
- // vite.config.ts
29
+ import build from "@hono/vite-build/node";
30
+ import devServer from "@hono/vite-dev-server";
31
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
21
32
  import { pages } from "hono-svelte/vite";
22
- const appPages = pages(); // defaults: src/pages
23
- // input: { ...appPages.input(), styles: "src/styles.css" }
24
- // plugins: [tailwindcss(), svelte(), appPages, ...] em TODOS os modos
25
- // (client, serve e server build) - o modo server precisa de appPages para
26
- // o shell enxergar o manifesto in-memory.
33
+ import { resolve } from "node:path";
34
+ import { defineConfig } from "vite";
35
+
36
+ const appPages = pages();
37
+
38
+ export default defineConfig(({ command, mode }) => {
39
+ if (mode === "client") {
40
+ return {
41
+ plugins: [svelte(), appPages],
42
+ build: {
43
+ rollupOptions: {
44
+ input: { ...appPages.input(), styles: resolve("src/styles.css") },
45
+ output: {
46
+ entryFileNames: "static/[name].js",
47
+ chunkFileNames: "static/chunks/[name]-[hash].js",
48
+ assetFileNames: "static/[name][extname]",
49
+ },
50
+ },
51
+ },
52
+ };
53
+ }
54
+
55
+ if (command === "serve") {
56
+ return {
57
+ plugins: [svelte(), appPages, devServer({ entry: "src/routes/index.ts" })],
58
+ };
59
+ }
60
+
61
+ return {
62
+ plugins: [svelte(), appPages, build({ entry: "src/routes/index.ts", staticRoot: "./dist" })],
63
+ };
64
+ });
27
65
  ```
28
66
 
67
+ **2. Register the shell on the server:**
68
+
29
69
  ```ts
30
- // servidor
70
+ import { Hono } from "hono";
31
71
  import { shell } from "hono-svelte";
32
72
 
33
- app.use("/*", shell({ title: "Meu App", lang: "pt-BR" }));
73
+ const app = new Hono().use("/*", shell({ title: "My App", lang: "en" }));
74
+ ```
75
+
76
+ **3. Create pages** in `src/pages` — one page per file:
77
+
78
+ ```svelte
79
+ <!-- src/pages/home.svelte — no <script>: becomes plain HTML, zero JS -->
80
+ <main>
81
+ <h1>Welcome</h1>
82
+ <a href="/dashboard">Sign in</a>
83
+ </main>
84
+ ```
85
+
86
+ ```svelte
87
+ <!-- src/pages/dashboard.svelte — has <script>: gets its own JS -->
88
+ <script lang="ts">
89
+ import { hc } from "hono/client";
90
+ import type { AppType } from "../routes/api";
91
+
92
+ let { plan } = $props<{ plan: string }>();
93
+ const client = hc<AppType>("/");
94
+
95
+ let time = $state("--:--");
96
+ async function refresh() {
97
+ const res = await client.api.time.$get();
98
+ time = (await res.json()).time;
99
+ }
100
+ </script>
101
+
102
+ <main>
103
+ <h1>Dashboard — {plan} plan</h1>
104
+ <p>Server time: {time}</p>
105
+ <button onclick={refresh}>Refresh</button>
106
+ </main>
107
+ ```
108
+
109
+ **4. Render in routes:**
110
+
111
+ ```ts
112
+ app.get("/", (c) => c.render("home"));
34
113
  app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
35
114
  ```
36
115
 
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
+
118
+ Files starting with `_` and `layout.svelte` are ignored (convention for partials and layouts).
119
+
120
+ ## Passing data to the page
121
+
122
+ Small, public initial data (plan name, title, preferences) goes in `data` and arrives via `$props()` — no extra request.
123
+ Works for static (zero-JS) pages too: the shell passes `data` as SSR props **and** embeds the JSON for hydration:
124
+
125
+ ```ts
126
+ app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
127
+ ```
128
+
129
+ Large payloads trigger a one-time `console.warn` (default limit 100KB, `shell({ dataLimit })`).
130
+ Anything sensitive stays in the API (`hc<AppType>`, HttpOnly cookie). Never put secrets in `data`.
131
+
132
+ ## Status, headers and per-page head
133
+
134
+ ```ts
135
+ app.get("/old", (c) => c.render("gone", { status: 410, headers: { "X-Gone": "1" } }));
136
+
137
+ app.get("/post", (c) =>
138
+ c.render("post", {
139
+ head: {
140
+ description: "A post about islands",
141
+ canonical: "https://example.com/post",
142
+ og: { title: "Islands", image: "https://example.com/og.png" },
143
+ twitter: { card: "summary_large_image" },
144
+ },
145
+ }),
146
+ );
147
+ ```
148
+
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).
150
+
151
+ ## Nested layouts
152
+
153
+ `<dir>/layout.svelte` wraps every page under `<dir>/` (SSR inside-out, outermost first). A root `layout.svelte` wraps everything. Layouts receive `children` plus all page `data` as props — e.g. `title`/`description` passed via `c.render(entry, { data })`:
154
+
155
+ ```svelte
156
+ <!-- src/pages/dashboard/layout.svelte -->
157
+ <script lang="ts">
158
+ import type { Snippet } from "svelte";
159
+ type Props = { title?: string; description?: string; children: Snippet };
160
+ let { title = "Dashboard", description = "", children }: Props = $props();
161
+ </script>
162
+
163
+ <div class="layout">
164
+ <h1>{title}</h1>
165
+ {@render children()}
166
+ </div>
167
+ ```
168
+
169
+ ```ts
170
+ // routes/dashboard/page1.ts — layout data comes from the route, not the page
171
+ c.render("dashboard/page1", { data: { title: "Page 1", description: "..." } });
172
+ ```
173
+
37
174
  ```svelte
38
- <!-- pagina estatica: sem <script> => zero-JS, HTML vem pronto do servidor -->
39
- <main>...</main>
40
- ```
41
-
42
- ## Zero-JS automatico
43
-
44
- Pagina sem `<script>` no `.svelte` nao gera entry client: o `shell()` renderiza
45
- o HTML no servidor e nenhum `<script>` e enviado ao cliente. Para forcar client
46
- JS em uma pagina especifica: `pages({ alwaysClient: ["index"] })`.
47
- Para override manual do mapa de SSR: `shell({ ssrPages })` (opcional).
48
-
49
- ## IDs
50
-
51
- `root`/`data` usam ids opacos derivados do `entryName` via `getIds(entryName)`
52
- (`r-` + hash). Servidor e client derivam o mesmo par sem config, sem disco, sem salt.
53
-
54
- ## Repo
55
-
56
- - `/` — o pacote `hono-svelte` (src, test, dist)
57
- - `/examples/playground` — app Hono de demonstracao, consome o pacote via `file:../..`
58
-
59
- ## Tipagem do `c.render`
60
-
61
- Augmente o `ContextRenderer` no SEU app (padrao do Hono) — crie um `env.d.ts`:
62
-
63
- ```ts
64
- import type { RenderProps } from "hono-svelte";
65
-
66
- declare module "hono" {
67
- interface ContextRenderer {
68
- (entryName: string, props?: RenderProps): Response | Promise<Response>;
69
- }
70
- }
175
+ <!-- src/pages/dashboard/page1.svelte — content only, NO layout import -->
176
+ <section class="card">Page 1 content</section>
177
+ ```
178
+
179
+ > Pages must NOT import their layout manually — the shell applies it automatically. The plugin fails fast at build time (`manually imports ... but the layout is applied automatically`) instead of rendering `<main><div><main>` twice.
180
+
181
+ Layouts never become entries (`pages().layouts()` / `layoutChain(entry)` inspect them; `layouts: false` restores legacy ignore).
182
+
183
+
184
+ ## Error pages
185
+
186
+ ```ts
187
+ import { errorHandler, notFoundHandler } from "hono-svelte";
188
+
189
+ app.notFound(notFoundHandler()); // renders `404.svelte` with status 404
190
+ app.onError(errorHandler()); // renders `500.svelte` with status 500
191
+ ```
192
+
193
+ ## CLI
194
+
195
+ ```sh
196
+ npx hono-svelte init # scaffold src/pages, vite.config.ts, env.d.ts
197
+ npx hono-svelte doctor # check dist, link and vite config
198
+ npx hono-svelte doctor --app=./my-app # check another app directory
71
199
  ```
200
+
201
+ ## API
202
+
203
+ ### `shell(options?)`
204
+
205
+ Hono middleware that provides `c.render(entry, { title?, data?, status?, headers?, head? })` on every route.
206
+
207
+ | Option | Default | Description |
208
+ |---|---|---|
209
+ | `title` | `"App"` | Title used when the route doesn't provide one |
210
+ | `lang` | `"en"` | `<html>` `lang` attribute |
211
+ | `assetsBase` | `"/static"` | Prefix for JS files in production (legacy; prefer `assets`) |
212
+ | `assets` | `"/static"` | String prefix, Vite manifest object, or resolver `(entry) => url` (hashed files) |
213
+ | `isProd` | auto | Force prod/dev asset behavior |
214
+ | `stylesHref` | `"/static/styles.css"` in prod, `"/src/styles.css"` in dev | Global stylesheet (or an `(isProd) => string` function) |
215
+ | `styles` | `[]` | Extra CSS hrefs emitted per response |
216
+ | `head` | `""` | Extra HTML in `<head>` (fonts, meta tags) |
217
+ | `nonce` | — | CSP nonce string or `(c) => string` (applied to shell `<script>`/`<link>`) |
218
+ | `preload` | `true` in prod | `<link rel="modulepreload">` for the entry (+ manifest imports) |
219
+ | `prefetch` | `false` | `"all"` prefetches other entries; `"hover"` injects a hover-prefetch script |
220
+ | `dataLimit` | `102400` | Warn once above this many `data` bytes (`false` disables) |
221
+ | `status` | `200` | Default response status |
222
+ | `headers` | `{}` | Default response headers (per-render `headers` merge over them) |
223
+ | `strict` | `true` | Unknown entries throw a dev-friendly error; `false` disables |
224
+ | `knownEntries` | auto from manifest | Override the page list used by strict mode and prefetch |
225
+
226
+ `shell()` also exposes `availableEntries()` for diagnostics.
227
+
228
+ ### `pages(options?)`
229
+
230
+ Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entries.
231
+
232
+ | Option | Default | Description |
233
+ |---|---|---|
234
+ | `pagesDir` | `"src/pages"` | Pages folder |
235
+ | `ignore` | `["**/_*.svelte"]` | Ignored patterns (`layout.svelte` is a layout, not a page) |
236
+ | `layouts` | `true` | Nested `<dir>/layout.svelte` support (`false` = legacy ignore) |
237
+ | `dts` | `false` | Typed `c.render` entries: `true` writes `src/hono-svelte-entries.d.ts`, a string sets a custom path |
238
+ | `alwaysClient` | `[]` | Pages that always get JS, even without `<script>` |
239
+
240
+ 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).
241
+
242
+ ### Typing `c.render`
243
+
244
+ Opt-in typed entry names — a typo fails `tsc` instead of 500ing at runtime:
245
+
246
+ ```ts
247
+ // vite.config.ts
248
+ const appPages = pages({ dts: true }); // writes src/hono-svelte-entries.d.ts
249
+ ```
250
+
251
+ ```ts
252
+ // src/env.d.ts — the generated file carries the `declare module "hono"`
253
+ // augmentation, so this file is only a reference. Remove any manual
254
+ // `(entryName: string)` declaration or it will swallow the union.
255
+ /// <reference path="./hono-svelte-entries.d.ts" />
256
+ ```
257
+
258
+ ```ts
259
+ // generated src/hono-svelte-entries.d.ts (commit it — CI needs no Vite run)
260
+ export type HonoSvelteEntries = "auth" | "dashboard/index" | "index";
261
+
262
+ declare module "hono" {
263
+ interface ContextRenderer {
264
+ (entryName: HonoSvelteEntries, props?: import("hono-svelte").RenderProps): Response | Promise<Response>;
265
+ }
266
+ }
267
+ ```
268
+
269
+ `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`:
270
+
271
+ ```ts
272
+ import type { RenderProps } from "hono-svelte";
273
+
274
+ declare module "hono" {
275
+ interface ContextRenderer {
276
+ (entryName: string, props?: RenderProps): Response | Promise<Response>;
277
+ }
278
+ }
279
+ ```
280
+
281
+ `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.
282
+
283
+ ## Production tips
284
+
285
+ - Pass the Vite manifest to `shell({ assets: manifest })` for hashed files; manifest CSS and shared chunks are emitted automatically (`modulepreload`).
286
+ - Serve hashed files with long cache and `immutable` (`immutableHeaders()` / `CACHE_IMMUTABLE`); the HTML shell itself suits `CACHE_NO_STORE`.
287
+ - 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).
288
+ - Run both client and server builds before serving; the example in `examples/playground/` shows the full setup.
289
+
290
+ ## Example
291
+
292
+ `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:
293
+
294
+ ```sh
295
+ cd examples/playground && npm install && npm run build
296
+ ```
297
+
298
+ ## License
299
+
300
+ MIT — see [LICENSE](./LICENSE). Changelog in [CHANGELOG.md](./CHANGELOG.md).
301
+
302
+ Anything sensitive, large, or frequently changing stays in the API, fetched after mount with the typed Hono client (`hc<AppType>`) — with the session in an `HttpOnly` cookie as usual. Never put secrets in `data`: it is visible in the HTML.
@@ -0,0 +1,26 @@
1
+ export type ViteManifestChunk = {
2
+ file: string;
3
+ css?: string[];
4
+ imports?: string[];
5
+ isEntry?: boolean;
6
+ };
7
+ export type ViteManifest = Record<string, ViteManifestChunk>;
8
+ export type AssetsResolver = (entryName: string) => string;
9
+ export type ManifestAssetsOptions = {
10
+ /** Prefix for manifest file paths. @default "/" */
11
+ prefix?: string;
12
+ /** Fallback base when the entry is missing from the manifest. @default "/static" */
13
+ fallbackBase?: string;
14
+ };
15
+ /** Manifest keys tried for an entry (virtual ids first, bare entry last). */
16
+ export declare function manifestKeysFor(entryName: string): string[];
17
+ export declare function createManifestAssets(manifest: ViteManifest, opts?: ManifestAssetsOptions): AssetsResolver;
18
+ /** Hashed CSS files for an entry, via the Vite manifest. */
19
+ export declare function manifestCssFor(manifest: ViteManifest, entryName: string, prefix?: string): string[];
20
+ /** Shared-chunk URLs (for `modulepreload`), via the Vite manifest. */
21
+ export declare function manifestImportsFor(manifest: ViteManifest, entryName: string, prefix?: string): string[];
22
+ /** Long-term caching for hashed `/static` files. */
23
+ export declare const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
24
+ /** Safe default for HTML shell responses. */
25
+ export declare const CACHE_NO_STORE = "no-store";
26
+ export declare function immutableHeaders(extra?: Record<string, string>): Record<string, string>;
package/dist/assets.js ADDED
@@ -0,0 +1,60 @@
1
+ // Asset URL resolution (Vite manifest with hashed files) + cache helpers.
2
+ //
3
+ // Default behavior stays zero-config: `/static/<entry>.js` (unhashed names,
4
+ // as in the README example). Pass a Vite manifest object to `shell({ assets })`
5
+ // when the client build uses hashed file names.
6
+ const NULL = String.fromCharCode(0);
7
+ function joinPrefix(prefix, file) {
8
+ return prefix.replace(/\/$/, "") + "/" + file.replace(/^\//, "");
9
+ }
10
+ /** Manifest keys tried for an entry (virtual ids first, bare entry last). */
11
+ export function manifestKeysFor(entryName) {
12
+ return [
13
+ NULL + "virtual:hono-svelte/entry/" + entryName,
14
+ "virtual:hono-svelte/entry/" + entryName,
15
+ entryName,
16
+ ];
17
+ }
18
+ function findChunk(manifest, entryName) {
19
+ for (const key of manifestKeysFor(entryName)) {
20
+ const chunk = manifest[key];
21
+ if (chunk)
22
+ return chunk;
23
+ }
24
+ return undefined;
25
+ }
26
+ export function createManifestAssets(manifest, opts = {}) {
27
+ const prefix = opts.prefix ?? "/";
28
+ const fallbackBase = (opts.fallbackBase ?? "/static").replace(/\/$/, "");
29
+ return (entryName) => {
30
+ const chunk = findChunk(manifest, entryName);
31
+ if (chunk?.file)
32
+ return joinPrefix(prefix, chunk.file);
33
+ return `${fallbackBase}/${entryName}.js`;
34
+ };
35
+ }
36
+ /** Hashed CSS files for an entry, via the Vite manifest. */
37
+ export function manifestCssFor(manifest, entryName, prefix = "/") {
38
+ const chunk = findChunk(manifest, entryName);
39
+ return (chunk?.css ?? []).map((f) => joinPrefix(prefix, f));
40
+ }
41
+ /** Shared-chunk URLs (for `modulepreload`), via the Vite manifest. */
42
+ 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;
53
+ }
54
+ /** Long-term caching for hashed `/static` files. */
55
+ export const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
56
+ /** Safe default for HTML shell responses. */
57
+ export const CACHE_NO_STORE = "no-store";
58
+ export function immutableHeaders(extra = {}) {
59
+ return { "Cache-Control": CACHE_IMMUTABLE, ...extra };
60
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Thin CLI wrapper: `hono-svelte init` / `hono-svelte doctor`.
3
+ // Compiled to `dist/cli.js` and invoked via the `hono-svelte` bin.
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { doctorChecks, checkAppLink, initFiles, isSafeAppPath } from "./scaffold.js";
8
+ const args = process.argv.slice(2);
9
+ const command = args[0];
10
+ function flagValue(name) {
11
+ const pref = `--${name}=`;
12
+ const hit = args.find((a) => a.startsWith(pref));
13
+ return hit ? hit.slice(pref.length) : undefined;
14
+ }
15
+ const appRoot = flagValue("app") ?? process.cwd();
16
+ function init() {
17
+ for (const file of initFiles()) {
18
+ const dest = join(appRoot, file.path);
19
+ if (!isSafeAppPath(appRoot, dest)) {
20
+ console.error(`[hono-svelte] refusing to write outside ${appRoot}: ${file.path}`);
21
+ process.exitCode = 1;
22
+ continue;
23
+ }
24
+ if (file.skipIfExists && existsSync(dest)) {
25
+ console.log(`[hono-svelte] exists, skipping: ${file.path}`);
26
+ continue;
27
+ }
28
+ mkdirSync(dirname(dest), { recursive: true });
29
+ writeFileSync(dest, file.content);
30
+ console.log(`[hono-svelte] wrote ${file.path}`);
31
+ }
32
+ }
33
+ function packageRootForCli() {
34
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
35
+ }
36
+ function doctor() {
37
+ const packageRoot = packageRootForCli();
38
+ const issues = doctorChecks({ packageRoot, appRoot });
39
+ const linkIssue = checkAppLink(appRoot);
40
+ if (linkIssue)
41
+ issues.push(linkIssue);
42
+ if (issues.length === 0) {
43
+ console.log("[hono-svelte] doctor: all good!");
44
+ return;
45
+ }
46
+ for (const issue of issues) {
47
+ console.log(`[hono-svelte] ${issue.code}: ${issue.message}`);
48
+ console.log(` fix: ${issue.fix}`);
49
+ }
50
+ process.exitCode = 1;
51
+ }
52
+ if (command === "init")
53
+ init();
54
+ else if (command === "doctor")
55
+ doctor();
56
+ else {
57
+ console.log("Usage: hono-svelte <init|doctor> [--app=<dir>]");
58
+ process.exitCode = 1;
59
+ }
@@ -0,0 +1,16 @@
1
+ import type { HeadProps } from "./head.js";
2
+ export type ErrorPageOptions = {
3
+ /** Entry name. @default "404" (not-found) / "500" (error) */
4
+ entry?: string;
5
+ title?: string;
6
+ data?: Record<string, unknown>;
7
+ head?: string | HeadProps;
8
+ };
9
+ /** Hono `notFound` handler: `app.notFound(notFoundHandler())`. */
10
+ export declare function notFoundHandler(opts?: ErrorPageOptions): (c: any) => any;
11
+ export type ServerErrorHandlerOptions = ErrorPageOptions & {
12
+ /** Log the error. `true` = `console.error`. @default true */
13
+ log?: boolean | ((err: unknown) => void);
14
+ };
15
+ /** Hono `onError` handler: `app.onError(errorHandler())`. */
16
+ export declare function errorHandler(opts?: ServerErrorHandlerOptions): (err: unknown, c: any) => any;
@@ -0,0 +1,31 @@
1
+ // Conventional error-page handlers: `404.svelte` / `500.svelte` pages
2
+ // rendered with the matching HTTP status.
3
+ /** Hono `notFound` handler: `app.notFound(notFoundHandler())`. */
4
+ export function notFoundHandler(opts = {}) {
5
+ const entry = opts.entry ?? "404";
6
+ return (c) => {
7
+ return c.render(entry, {
8
+ title: opts.title,
9
+ data: opts.data,
10
+ head: opts.head,
11
+ status: 404,
12
+ });
13
+ };
14
+ }
15
+ /** Hono `onError` handler: `app.onError(errorHandler())`. */
16
+ export function errorHandler(opts = {}) {
17
+ const entry = opts.entry ?? "500";
18
+ const log = opts.log ?? true;
19
+ return (err, c) => {
20
+ if (log === true)
21
+ console.error(err);
22
+ else if (typeof log === "function")
23
+ log(err);
24
+ return c.render(entry, {
25
+ title: opts.title,
26
+ data: opts.data,
27
+ head: opts.head,
28
+ status: 500,
29
+ });
30
+ };
31
+ }
package/dist/head.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type HeadMetaItem = {
2
+ name?: string;
3
+ property?: string;
4
+ httpEquiv?: string;
5
+ content: string;
6
+ };
7
+ export type HeadProps = {
8
+ description?: string;
9
+ canonical?: string;
10
+ themeColor?: string;
11
+ meta?: HeadMetaItem[];
12
+ /** Open Graph tags: `{ title, description, image }` -> `property="og:*"`. */
13
+ og?: Record<string, string>;
14
+ /** Twitter tags: `{ card, title }` -> `name="twitter:*"`. */
15
+ twitter?: Record<string, string>;
16
+ /** Raw HTML appended as-is (fonts, ld+json, verification tags). */
17
+ extra?: string;
18
+ };
19
+ export declare function renderHead(head: string | HeadProps | undefined): string;