hono-svelte 0.3.0 → 0.3.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 ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1
4
+
5
+ - Package metadata: repository/homepage/author, publishConfig, LICENSE + CHANGELOG in files.
6
+ - No code changes since 0.3.0.
7
+
8
+ ## 0.3.0
9
+
10
+ - Fully in-memory package: entries and manifest are virtual modules (no files written to disk).
11
+ - Automatic zero-JS: `.svelte` pages without `<script>` render on the server (`svelte/server`)
12
+ and download no JS on the client.
13
+ - Deterministic opaque IDs derived from the entryName (no salt, no env, no disk).
14
+ - Zero config: the shell resolves the SSR map on its own (relative import redirected by the plugin).
15
+ - Test suite (vitest) and CI (typecheck + tests + build + publint).
16
+
17
+ ## 0.2.0
18
+
19
+ - Opaque IDs (`r-` + hash) replace configurable `rootId`/`dataId`.
20
+
21
+ ## 0.1.0
22
+
23
+ - First version: `shell()` middleware + `pages()` plugin with disk-generated entries.
package/README.md CHANGED
@@ -1,71 +1,187 @@
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" }));
34
- app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
73
+ const app = new Hono().use("/*", shell({ title: "My App", lang: "en" }));
35
74
  ```
36
75
 
76
+ **3. Create pages** in `src/pages` — one page per file:
77
+
37
78
  ```svelte
38
- <!-- pagina estatica: sem <script> => zero-JS, HTML vem pronto do servidor -->
39
- <main>...</main>
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"));
113
+ app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
114
+ ```
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
+
124
+ ```ts
125
+ app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
40
126
  ```
41
127
 
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
- }
128
+
129
+ ## API
130
+
131
+ ### `shell(options?)`
132
+
133
+ Hono middleware that provides `c.render(entry, { title?, data? })` on every route.
134
+
135
+ | Option | Default | Description |
136
+ |---|---|---|
137
+ | `title` | `"App"` | Title used when the route doesn't provide one |
138
+ | `lang` | `"en"` | `<html>` `lang` attribute |
139
+ | `assetsBase` | `"/static"` | Prefix for JS files in production |
140
+ | `stylesHref` | `"/static/styles.css"` in prod, `"/src/styles.css"` in dev | Global stylesheet (or an `(isProd) => string` function) |
141
+ | `head` | `""` | Extra HTML in `<head>` (fonts, meta tags) |
142
+
143
+ ### `pages(options?)`
144
+
145
+ Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entries.
146
+
147
+ | Option | Default | Description |
148
+ |---|---|---|
149
+ | `pagesDir` | `"src/pages"` | Pages folder |
150
+ | `ignore` | `["**/layout.svelte", "**/_*.svelte"]` | Ignored patterns |
151
+ | `alwaysClient` | `[]` | Pages that always get JS, even without `<script>` |
152
+
153
+ Handy methods: `input()` (client build entries), `entries()` (all pages), `staticEntries()` (static pages only), `hasClient(entry)`.
154
+
155
+ ### Typing `c.render`
156
+
157
+ So TypeScript accepts `c.render` in routes, declare once in the app:
158
+
159
+ ```ts
160
+ import type { RenderProps } from "hono-svelte";
161
+
162
+ declare module "hono" {
163
+ interface ContextRenderer {
164
+ (entryName: string, props?: RenderProps): Response | Promise<Response>;
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Production tips
170
+
171
+ - Serve hashed files with long cache and `immutable`; the stylesheet with short cache or a versioned name.
172
+ - Works with `script-src 'self'` — there is no executable inline script on the page.
173
+ - Run both client and server builds before serving; the example in `examples/playground/` shows the full setup.
174
+
175
+ ## Example
176
+
177
+ `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:
178
+
179
+ ```sh
180
+ cd examples/playground && npm install && npm run build
71
181
  ```
182
+
183
+ ## License
184
+
185
+ MIT — see [LICENSE](./LICENSE). Changelog in [CHANGELOG.md](./CHANGELOG.md).
186
+
187
+ 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.
package/dist/ids.js CHANGED
@@ -1,6 +1,6 @@
1
- // IDs opacos derivados do entryName. Puro, sem dependencias de runtime.
2
- // (funciona em Node, Workers, Deno, Bun). Nao e segredo de estado:
3
- // apenas evita expor dados do servidor no HTML.
1
+ // Opaque IDs derived from the entryName. Pure, no runtime dependencies.
2
+ // (works on Node, Workers, Deno, Bun). Not a state secret:
3
+ // it just avoids exposing server internals in the HTML.
4
4
  function hashString(input) {
5
5
  let h1 = 0xdeadbeef;
6
6
  let h2 = 0x41c6ce57;
package/dist/index.d.ts CHANGED
@@ -14,8 +14,8 @@ export type ShellOptions = {
14
14
  assetsBase?: string;
15
15
  stylesHref?: string | ((isProd: boolean) => string);
16
16
  head?: string;
17
- /** Override manual do mapa entryName -> loader (opcional; sem isso o shell
18
- * resolve o mapa automaticamente via ssr-manifest). */
17
+ /** Manual override of the entryName -> loader map (optional; otherwise the shell
18
+ * resolves the map automatically via ssr-manifest). */
19
19
  ssrPages?: Record<string, SsrPageLoader>;
20
20
  };
21
21
  export type ShellContext = {
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ function serializePageData(data, dataId) {
23
23
  json = JSON.stringify(data);
24
24
  }
25
25
  catch {
26
- throw new Error("hono-svelte: props.data precisa ser serializavel em JSON");
26
+ throw new Error("hono-svelte: props.data must be JSON-serializable");
27
27
  }
28
28
  if (json === undefined)
29
29
  return "";
@@ -39,13 +39,13 @@ export function shell(options = {}) {
39
39
  const resolveStyles = typeof options.stylesHref === "function"
40
40
  ? options.stylesHref
41
41
  : () => options.stylesHref;
42
- // Zero config: com pages() ativo, este modulo e redirecionado para o
43
- // manifesto in-memory pelo resolveId do plugin (enforce: "pre").
42
+ // Zero config: with pages() active, this module is redirected to the
43
+ // in-memory manifest by the plugin's resolveId (enforce: "pre").
44
44
  const ssrPages = options.ssrPages ?? autoSsrPages;
45
45
  return async function shellMiddleware(c, next) {
46
46
  c.setRenderer(async (entryName, props) => {
47
47
  if (!isValidEntryName(entryName)) {
48
- throw new Error(`hono-svelte: entryName invalido: ${JSON.stringify(entryName)}`);
48
+ throw new Error(`hono-svelte: invalid entryName: ${JSON.stringify(entryName)}`);
49
49
  }
50
50
  const ids = getIds(entryName);
51
51
  const title = props?.title ?? titleDefault;
@@ -1,6 +1,6 @@
1
- // Stub estatico usado pelo shell quando o plugin hono-svelte/vite (pages())
2
- // nao esta ativo no bundler: nenhuma pagina estatica, todos os entries
3
- // sao client-side. Com pages() ativo, o resolveId do plugin (enforce: "pre")
4
- // redireciona o import relativo "./ssr-manifest.js" do shell para o
5
- // manifesto virtual gerado em memoria.
1
+ // Static stub used by the shell when the hono-svelte/vite plugin (pages())
2
+ // is not active in the bundler: no static pages, every entry
3
+ // is client-side. With pages() active, the plugin resolveId (enforce: 'pre')
4
+ // redirects the shell relative import './ssr-manifest.js' to the
5
+ // virtual manifest generated in memory.
6
6
  export const ssrPages = {};
package/dist/virtual.js CHANGED
@@ -1,6 +1,6 @@
1
- // IDs dos modulos virtuais compartilhados entre o plugin (hono-svelte/vite)
2
- // e o shell (hono-svelte). Nada e escrito em disco: entries e manifesto
3
- // sao servidos in-memory como virtual modules.
1
+ // IDs of the virtual modules shared between the plugin (hono-svelte/vite)
2
+ // and the shell (hono-svelte). Nothing is written to disk: entries and the manifest
3
+ // are served in-memory as virtual modules.
4
4
  const NULL = String.fromCharCode(0);
5
5
  export const ENTRY_PREFIX = "virtual:hono-svelte/entry/";
6
6
  export const ENTRY_RESOLVED_PREFIX = NULL + ENTRY_PREFIX;
@@ -12,8 +12,8 @@ export function entryVirtualId(entryName) {
12
12
  export function entryResolvedId(entryName) {
13
13
  return ENTRY_RESOLVED_PREFIX + entryName;
14
14
  }
15
- // URL de dev para o <script type="module"> gerado pelo shell.
16
- // \0 vira __x00__ na URL servida pelo dev server do Vite.
15
+ // Dev URL for the <script type='module'> generated by the shell.
16
+ // \0 becomes __x00__ in the URL served by the Vite dev server.
17
17
  export function devEntryUrl(entryName) {
18
18
  return "/@id/" + entryResolvedId(entryName).replace(NULL, "__x00__");
19
19
  }
package/dist/vite.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type Plugin } from "vite";
2
2
  export type PagesOptions = {
3
3
  pagesDir?: string;
4
4
  ignore?: string[];
5
- /** entryNames que sempre geram client JS, mesmo sem <script>. */
5
+ /** entryNames that always generate client JS, even without a `<script>`. */
6
6
  alwaysClient?: string[];
7
7
  generatedHeader?: string;
8
8
  };
package/dist/vite.js CHANGED
@@ -14,7 +14,7 @@ export function pages(options = {}) {
14
14
  const pagesDir = resolve(options.pagesDir ?? "src/pages");
15
15
  const ignore = options.ignore ?? DEFAULT_IGNORE;
16
16
  const alwaysClient = new Set(options.alwaysClient ?? []);
17
- const header = options.generatedHeader ?? "// @generated - hono-svelte, nao editar.";
17
+ const header = options.generatedHeader ?? "// @generated - hono-svelte, do not edit.";
18
18
  const filter = createFilter(["**/*.svelte"], ignore, { resolve: pagesDir });
19
19
  let cachedPages = null;
20
20
  let cachedInput = {};
@@ -29,12 +29,12 @@ export function pages(options = {}) {
29
29
  for (const file of files) {
30
30
  const entryName = entryNameFromFile(file);
31
31
  if (seen.has(entryName)) {
32
- throw new Error(`hono-svelte: entry duplicado: ${entryName}`);
32
+ throw new Error(`hono-svelte: duplicate entry: ${entryName}`);
33
33
  }
34
34
  seen.add(entryName);
35
35
  const absFile = resolve(pagesDir, file);
36
- // Zero-JS automatico: pagina sem <script> nao tem interatividade,
37
- // entao o shell renderiza no servidor e o cliente nao baixa JS.
36
+ // Automatic zero-JS: a page without <script> has no interactivity,
37
+ // so the shell renders it on the server and the client downloads no JS.
38
38
  const hasScript = /<script[\s>]/i.test(readFileSync(absFile, "utf8"));
39
39
  result.push({ entryName, file, absFile, isStatic: !hasScript && !alwaysClient.has(entryName) });
40
40
  }
@@ -52,7 +52,7 @@ export function pages(options = {}) {
52
52
  }
53
53
  if (log) {
54
54
  const staticCount = cachedPages.length - jsCount;
55
- console.log("[hono-svelte] " + jsCount + " pagina(s) com JS + " + staticCount + " estatica(s), tudo in-memory");
55
+ console.log("[hono-svelte] " + jsCount + " page(s) with JS + " + staticCount + " static(s), all in-memory");
56
56
  }
57
57
  }
58
58
  function listPages() {
@@ -97,7 +97,7 @@ export function pages(options = {}) {
97
97
  }
98
98
  function entrySource(page) {
99
99
  const ids = getIds(page.entryName);
100
- return (header + " Origem: " + page.file + "\n" +
100
+ return (header + " Source: " + page.file + "\n" +
101
101
  'import { mount } from "svelte";\n' +
102
102
  "import Page from " + JSON.stringify(pageImportPath(page)) + ";\n" +
103
103
  "const target = document.getElementById(" + JSON.stringify(ids.rootId) + ");\n" +
@@ -107,8 +107,8 @@ export function pages(options = {}) {
107
107
  }
108
108
  const plugin = {
109
109
  name: "hono-svelte-pages",
110
- // pre: precisa rodar ANTES do vite:resolve para interceptar o import
111
- // relativo do shell ("./ssr-manifest.js") e redirecionar ao manifesto.
110
+ // pre: must run BEFORE vite:resolve to intercept the shell's
111
+ // relative import ("./ssr-manifest.js") and redirect it to the manifest.
112
112
  enforce: "pre",
113
113
  configResolved(config) {
114
114
  viteRoot = config.root;
@@ -117,15 +117,15 @@ export function pages(options = {}) {
117
117
  refresh();
118
118
  },
119
119
  resolveId(source) {
120
- // Manifesto virtual: explicito (virtual:hono-svelte/manifest, retrocompat)
121
- // ou o import relativo do proprio shell. Sem pages() ativo, o import
122
- // relativo resolve o stub estatico dist/ssr-manifest.js.
120
+ // Virtual manifest: explicit (virtual:hono-svelte/manifest, backwards compat)
121
+ // or the shell's own relative import. Without pages() active, the
122
+ // relative import resolves the static dist/ssr-manifest.js stub.
123
123
  if (source === MANIFEST_ID || source === "./ssr-manifest.js")
124
124
  return MANIFEST_RESOLVED;
125
125
  if (source.startsWith(ENTRY_PREFIX)) {
126
126
  return ENTRY_RESOLVED_PREFIX + source.slice(ENTRY_PREFIX.length);
127
127
  }
128
- // ids ja resolvidos (\0...) passam direto para o load()
128
+ // already-resolved ids (\0...) go straight through to load()
129
129
  if (source === MANIFEST_RESOLVED || source.startsWith(ENTRY_RESOLVED_PREFIX)) {
130
130
  return source;
131
131
  }
@@ -138,7 +138,7 @@ export function pages(options = {}) {
138
138
  const entryName = id.slice(ENTRY_RESOLVED_PREFIX.length);
139
139
  const page = listPages().find((p) => p.entryName === entryName);
140
140
  if (!page) {
141
- throw new Error("hono-svelte: pagina nao encontrada para entry: " + entryName);
141
+ throw new Error("hono-svelte: no page found for entry: " + entryName);
142
142
  }
143
143
  return entrySource(page);
144
144
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-svelte",
3
- "version": "0.3.0",
4
- "description": "Shell server-side + multi-entry Svelte pages for Hono apps",
3
+ "version": "0.3.1",
4
+ "description": "Server-side shell + multi-entry Svelte pages for Hono apps",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -15,7 +15,9 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "README.md"
18
+ "README.md",
19
+ "CHANGELOG.md",
20
+ "LICENSE"
19
21
  ],
20
22
  "scripts": {
21
23
  "build": "tsc -p tsconfig.json",
@@ -49,5 +51,14 @@
49
51
  "ssr",
50
52
  "zero-js",
51
53
  "middleware"
52
- ]
54
+ ],
55
+ "author": "omarcos",
56
+ "homepage": "https://github.com/omarcosr/hono-svelte",
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/omarcosr/hono-svelte.git"
63
+ }
53
64
  }