hono-svelte 0.4.1 → 0.4.2

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ - `hono-svelte init` now scaffolds a complete runnable app (works with
6
+ `npx`, `bunx` and `pnpm dlx` — writes text files only, no install/network).
7
+ Minimal flavor (default): `package.json`, `tsconfig.json`, `.gitignore`,
8
+ `vite.config.ts` (with `pages({ dts: true })`), two pages (`index` zero-JS,
9
+ `hello` with `data`), `src/routes/index.ts`, `src/styles.css`.
10
+ `init --full` adds auth (cookie session), dashboard with nested layout,
11
+ and typed RPC (`hc<AppType>`) — `/auth` → login → `/dashboard`.
12
+ Generated scripts (`dev`/`build`/`typecheck`/`start`) run under npm and bun.
13
+ `InitFlavor` type exported.
14
+
3
15
  ## 0.4.1
4
16
 
5
17
  - Fix `hono-svelte init` refusing every file on Windows: `isSafeAppPath()`
package/README.md CHANGED
@@ -193,7 +193,20 @@ app.onError(errorHandler()); // renders `500.svelte` with status 500
193
193
  ## CLI
194
194
 
195
195
  ```sh
196
- npx hono-svelte init # scaffold src/pages, vite.config.ts, env.d.ts
196
+ # Start a new app (works with npx, bunx or pnpm dlx — no install needed):
197
+ bunx hono-svelte init # minimal: 2 pages + routes + configs
198
+ bunx hono-svelte init --full # + auth, dashboard w/ layout, typed RPC
199
+
200
+ cd my-app && bun install && bun run dev # or: npm install && npm run dev
201
+ ```
202
+
203
+ `init` scaffolds a complete runnable app: `package.json`, `tsconfig.json`,
204
+ `vite.config.ts` (with `pages({ dts: true })`), `src/pages/` and
205
+ `src/routes/`. The `--full` flavor adds cookie-session auth, a dashboard with
206
+ nested layout, and a typed API (`hc<AppType>`) — open `/auth`, sign in, explore
207
+ `/dashboard`. Generated scripts run under npm and bun.
208
+
209
+ ```sh
197
210
  npx hono-svelte doctor # check dist, link and vite config
198
211
  npx hono-svelte doctor --app=./my-app # check another app directory
199
212
  ```
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { doctorChecks, checkAppLink, initFiles, isSafeAppPath } from "./scaffold.js";
7
+ import { doctorChecks, checkAppLink, initFiles, isSafeAppPath, } from "./scaffold.js";
8
8
  const args = process.argv.slice(2);
9
9
  const command = args[0];
10
10
  function flagValue(name) {
@@ -12,9 +12,14 @@ function flagValue(name) {
12
12
  const hit = args.find((a) => a.startsWith(pref));
13
13
  return hit ? hit.slice(pref.length) : undefined;
14
14
  }
15
+ function hasFlag(name) {
16
+ return args.includes(`--${name}`);
17
+ }
15
18
  const appRoot = flagValue("app") ?? process.cwd();
19
+ const flavor = hasFlag("full") ? "full" : "minimal";
16
20
  function init() {
17
- for (const file of initFiles()) {
21
+ const files = initFiles(flavor);
22
+ for (const file of files) {
18
23
  const dest = join(appRoot, file.path);
19
24
  if (!isSafeAppPath(appRoot, dest)) {
20
25
  console.error(`[hono-svelte] refusing to write outside ${appRoot}: ${file.path}`);
@@ -29,6 +34,16 @@ function init() {
29
34
  writeFileSync(dest, file.content);
30
35
  console.log(`[hono-svelte] wrote ${file.path}`);
31
36
  }
37
+ console.log(`[hono-svelte] done (${flavor}). Next:`);
38
+ if (flavor === "minimal") {
39
+ console.log(` 1. npm install (or: bun install)`);
40
+ console.log(` 2. npm run dev (or: bun run dev)`);
41
+ console.log(` Tip: --full scaffolds auth + dashboard + layout + typed RPC.`);
42
+ }
43
+ else {
44
+ console.log(` 1. npm install (or: bun install)`);
45
+ console.log(` 2. npm run dev (or: bun run dev) → /auth → /dashboard`);
46
+ }
32
47
  }
33
48
  function packageRootForCli() {
34
49
  return join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -54,6 +69,7 @@ if (command === "init")
54
69
  else if (command === "doctor")
55
70
  doctor();
56
71
  else {
57
- console.log("Usage: hono-svelte <init|doctor> [--app=<dir>]");
72
+ console.log("Usage: hono-svelte <init|doctor> [--app=<dir>] [--full]");
73
+ console.log(" init [--full] scaffold a runnable app (minimal by default)");
58
74
  process.exitCode = 1;
59
75
  }
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export type { AssetsResolver, ManifestAssetsOptions, ViteManifest, ViteManifestC
10
10
  export { notFoundHandler, errorHandler } from "./handlers.js";
11
11
  export type { ErrorPageOptions, ServerErrorHandlerOptions } from "./handlers.js";
12
12
  export { initFiles, doctorChecks, checkAppLink, versionsMatch, isSafeAppPath } from "./scaffold.js";
13
- export type { DoctorIssue, DoctorOptions, InitFile } from "./scaffold.js";
13
+ export type { DoctorIssue, DoctorOptions, InitFile, InitFlavor } from "./scaffold.js";
14
14
  export type SsrPageLoader = () => Promise<{
15
15
  default: unknown;
16
16
  }>;
@@ -3,7 +3,8 @@ export type InitFile = {
3
3
  content: string;
4
4
  skipIfExists: boolean;
5
5
  };
6
- export declare function initFiles(): InitFile[];
6
+ export type InitFlavor = "minimal" | "full";
7
+ export declare function initFiles(flavor?: InitFlavor): InitFile[];
7
8
  export type DoctorIssue = {
8
9
  /** Machine-readable code, e.g. "missing-dist". */
9
10
  code: string;
package/dist/scaffold.js CHANGED
@@ -2,16 +2,19 @@
2
2
  //
3
3
  // The package exposes pure helpers (`initFiles`, `doctorChecks`) so they can
4
4
  // be unit-tested; the thin CLI wrapper lives in `src/cli.ts` (compiled to
5
- // `dist/cli.js`, invoked via the `hono-svelte-doctor` bin).
5
+ // `dist/cli.js`, invoked via the `hono-svelte` bin).
6
+ //
7
+ // Runtime note: `init` writes plain text files only — no install, no network.
8
+ // It works identically under `npx` (npm), `bunx` (bun) and `pnpm dlx`.
9
+ // The generated app is runtime-agnostic too: `bun run dev`, `bun run build`
10
+ // and `bun run start` work because every script shells out to `vite` / `node`
11
+ // (both runnable from bun).
6
12
  import { existsSync } from "node:fs";
7
13
  import { isAbsolute, join, resolve } from "node:path";
8
- const ENV_DTS = `import type { RenderProps } from "hono-svelte";
9
-
10
- declare module "hono" {
11
- interface ContextRenderer {
12
- (entryName: string, props?: RenderProps): Response | Promise<Response>;
13
- }
14
- }
14
+ const ENV_DTS = `// Entry names are generated by \`pages({ dts: true })\` into
15
+ // \`src/hono-svelte-entries.d.ts\` (see vite.config.ts). \`c.render()\` only
16
+ // accepts those names — a typo fails the typecheck.
17
+ /// <reference path="./hono-svelte-entries.d.ts" />
15
18
  `;
16
19
  const VITE_CONFIG = `import build from "@hono/vite-build/node";
17
20
  import devServer from "@hono/vite-dev-server";
@@ -20,7 +23,7 @@ import { pages } from "hono-svelte/vite";
20
23
  import { resolve } from "node:path";
21
24
  import { defineConfig } from "vite";
22
25
 
23
- const appPages = pages();
26
+ const appPages = pages({ dts: true });
24
27
 
25
28
  export default defineConfig(({ command, mode }) => {
26
29
  if (mode === "client") {
@@ -50,16 +53,298 @@ export default defineConfig(({ command, mode }) => {
50
53
  };
51
54
  });
52
55
  `;
56
+ const PACKAGE_JSON = `{
57
+ "name": "my-hono-svelte-app",
58
+ "type": "module",
59
+ "private": true,
60
+ "scripts": {
61
+ "dev": "vite",
62
+ "build": "npm run build:client && npm run build:server",
63
+ "build:client": "vite build --mode client",
64
+ "build:server": "vite build --mode production",
65
+ "typecheck": "tsc --noEmit",
66
+ "start": "node ./dist/index.js"
67
+ },
68
+ "dependencies": {
69
+ "hono": "^4",
70
+ "hono-svelte": "latest",
71
+ "svelte": "^5"
72
+ },
73
+ "devDependencies": {
74
+ "@hono/vite-build": "^1",
75
+ "@hono/vite-dev-server": "^0",
76
+ "@sveltejs/vite-plugin-svelte": "^7",
77
+ "@types/node": "^22",
78
+ "typescript": "^5",
79
+ "vite": "^8"
80
+ }
81
+ }
82
+ `;
83
+ const TSCONFIG_JSON = `{
84
+ "compilerOptions": {
85
+ "target": "ESNext",
86
+ "module": "ESNext",
87
+ "moduleResolution": "Bundler",
88
+ "strict": true,
89
+ "skipLibCheck": true,
90
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
91
+ "types": ["vite/client", "node"]
92
+ },
93
+ "include": ["src/**/*", "vite.config.ts"],
94
+ "exclude": ["node_modules", "dist"]
95
+ }
96
+ `;
97
+ const GITIGNORE_TEXT = `node_modules
98
+ dist
99
+ `;
53
100
  const INDEX_SVELTE = `<main>
54
101
  <h1>Welcome</h1>
55
- <a href="/dashboard">Sign in</a>
102
+ <p><a href="/hello">Say hello</a></p>
56
103
  </main>
57
104
  `;
58
- export function initFiles() {
59
- return [
105
+ const HELLO_SVELTE = `<script lang="ts">
106
+ let { name }: { name?: string } = $props();
107
+ </script>
108
+
109
+ <main>
110
+ <h1>Hello, {name ?? "stranger"}!</h1>
111
+ <p><a href="/">Back home</a></p>
112
+ </main>
113
+ `;
114
+ const STYLES_CSS = `body {
115
+ font-family: system-ui, sans-serif;
116
+ margin: 0;
117
+ }
118
+
119
+ main {
120
+ max-width: 640px;
121
+ margin: 4rem auto;
122
+ padding: 0 1rem;
123
+ }
124
+ `;
125
+ const FULL_LIB_AUTH = `import type { Context } from "hono";
126
+ import { getCookie } from "hono/cookie";
127
+
128
+ export const SESSION_COOKIE = "hono_demo_session";
129
+ export const SESSION_VALUE = "authenticated";
130
+
131
+ export function hasSession(c: Context): boolean {
132
+ return getCookie(c, SESSION_COOKIE) === SESSION_VALUE;
133
+ }
134
+ `;
135
+ const FULL_MW_AUTH = `import { createMiddleware } from "hono/factory";
136
+ import { hasSession } from "../lib/auth";
137
+
138
+ export function requireSession() {
139
+ return createMiddleware(async (c, next) => {
140
+ if (!hasSession(c)) return c.redirect("/auth");
141
+ return next();
142
+ });
143
+ }
144
+ `;
145
+ const FULL_PAGE_AUTH = `<main>
146
+ <h1>Sign in</h1>
147
+ <form action="/auth/login" method="post">
148
+ <div>
149
+ <label for="email">Email</label>
150
+ <input id="email" name="email" type="email" required />
151
+ </div>
152
+ <div>
153
+ <label for="password">Password</label>
154
+ <input id="password" name="password" type="password" required />
155
+ </div>
156
+ <button type="submit">Sign in</button>
157
+ </form>
158
+ <p>Any email + password works.</p>
159
+ <p><a href="/">Back home</a></p>
160
+ </main>
161
+ `;
162
+ const FULL_PAGE_DASH_INDEX = `<script lang="ts">
163
+ import { hc } from "hono/client";
164
+ import type { AppType } from "../../routes";
165
+
166
+ const client = hc<AppType>("/");
167
+
168
+ let count = $state(0);
169
+ let time = $state<string | null>(null);
170
+
171
+ async function fetchTime() {
172
+ const res = await client.api.clock.$get();
173
+ if (res.ok) time = (await res.json()).now;
174
+ }
175
+ </script>
176
+
177
+ <div>
178
+ <p>Count: {count}</p>
179
+ <button type="button" onclick={() => (count += 1)}>Increment</button>
180
+ <p>
181
+ <button type="button" onclick={fetchTime}>Fetch time</button>
182
+ </p>
183
+ </div>
184
+ `;
185
+ const FULL_PAGE_DASH_PAGE1 = `<section>
186
+ <h2>Page 1 content</h2>
187
+ <p>Same layout, zero JS.</p>
188
+ <p><a href="/dashboard">Back to overview</a></p>
189
+ </section>
190
+ `;
191
+ const FULL_PAGE_DASH_LAYOUT = `<script lang="ts">
192
+ import type { Snippet } from "svelte";
193
+
194
+ type Props = { title?: string; description?: string; children: Snippet };
195
+ let { title = "Dashboard", description = "", children }: Props = $props();
196
+ </script>
197
+
198
+ <main>
199
+ <header>
200
+ <h1>{title}</h1>
201
+ </header>
202
+ {@render children()}
203
+ </main>
204
+ `;
205
+ const MIN_ROUTES_INDEX = `import { Hono } from "hono";
206
+ import { shell } from "hono-svelte";
207
+
208
+ const app = new Hono().use("/*", shell({ title: "My App" }));
209
+
210
+ app.get("/", (c) => c.render("index"));
211
+ app.get("/hello", (c) => c.render("hello", { data: { name: "Ada" } }));
212
+
213
+ export default app;
214
+ `;
215
+ export function initFiles(flavor = "minimal") {
216
+ const base = [
217
+ { path: "package.json", content: PACKAGE_JSON, skipIfExists: true },
218
+ { path: "tsconfig.json", content: TSCONFIG_JSON, skipIfExists: true },
219
+ { path: ".gitignore", content: GITIGNORE_TEXT, skipIfExists: true },
60
220
  { path: "src/env.d.ts", content: ENV_DTS, skipIfExists: true },
61
221
  { path: "vite.config.ts", content: VITE_CONFIG, skipIfExists: true },
222
+ { path: "src/styles.css", content: STYLES_CSS, skipIfExists: true },
62
223
  { path: "src/pages/index.svelte", content: INDEX_SVELTE, skipIfExists: true },
224
+ { path: "src/pages/hello.svelte", content: HELLO_SVELTE, skipIfExists: true },
225
+ { path: "src/routes/index.ts", content: MIN_ROUTES_INDEX, skipIfExists: true },
226
+ ];
227
+ if (flavor === "minimal")
228
+ return base;
229
+ return [...base, ...fullFlavorFiles()];
230
+ }
231
+ function fullFlavorFiles() {
232
+ const dashboardRoutes = `import { Hono } from "hono";
233
+ import { requireSession } from "../middleware/auth";
234
+
235
+ const app = new Hono()
236
+ .get("/", requireSession(), (c) =>
237
+ c.render("dashboard/index", {
238
+ data: {
239
+ title: "Your space is ready.",
240
+ description: "Counter + clock via typed RPC.",
241
+ },
242
+ }),
243
+ )
244
+ .get("/page1", requireSession(), (c) =>
245
+ c.render("dashboard/page1", {
246
+ data: { title: "Page 1", description: "A second page, same layout." },
247
+ }),
248
+ );
249
+
250
+ export default app;
251
+ `;
252
+ const siteRoutes = `import { Hono } from "hono";
253
+ import { hasSession } from "../lib/auth";
254
+
255
+ const app = new Hono().get("/", async (c) => {
256
+ if (hasSession(c)) return c.redirect("/dashboard");
257
+ return c.render("index");
258
+ });
259
+
260
+ export default app;
261
+ `;
262
+ const authRoutes = `import { Hono } from "hono";
263
+ import { setCookie } from "hono/cookie";
264
+ import { hasSession, SESSION_COOKIE, SESSION_VALUE } from "../lib/auth";
265
+
266
+ const app = new Hono();
267
+
268
+ app.get("/", (c) => {
269
+ if (hasSession(c)) return c.redirect("/dashboard");
270
+ return c.render("auth");
271
+ });
272
+
273
+ app.post("/login", async (c) => {
274
+ const body = await c.req.parseBody();
275
+ const email = String(body.email ?? "");
276
+ const password = String(body.password ?? "");
277
+ if (!email.includes("@") || !password) return c.redirect("/auth?error=invalid");
278
+ setCookie(c, SESSION_COOKIE, SESSION_VALUE, {
279
+ maxAge: 3600,
280
+ path: "/",
281
+ httpOnly: true,
282
+ sameSite: "Lax",
283
+ });
284
+ return c.redirect("/dashboard");
285
+ });
286
+
287
+ app.post("/logout", (c) => {
288
+ setCookie(c, SESSION_COOKIE, "", { maxAge: 0, path: "/" });
289
+ return c.redirect("/");
290
+ });
291
+
292
+ export default app;
293
+ `;
294
+ const apiRoutes = `import { Hono } from "hono";
295
+ import { hasSession } from "../lib/auth";
296
+
297
+ const app = new Hono().get("/clock", (c) => {
298
+ if (!hasSession(c)) return c.json({ error: "Not authenticated" }, 401);
299
+ return c.json({ now: new Date().toISOString() });
300
+ });
301
+
302
+ export default app;
303
+ `;
304
+ const appRoutes = `import { Hono } from "hono";
305
+ import { shell } from "hono-svelte";
306
+ import api from "./api";
307
+ import auth from "./auth";
308
+ import dashboard from "./dashboard";
309
+ import site from "./site";
310
+
311
+ const app = new Hono();
312
+
313
+ const routes = app
314
+ .use("/*", shell({ title: "My App", lang: "en" }))
315
+ .route("/", site)
316
+ .route("/auth", auth)
317
+ .route("/dashboard", dashboard)
318
+ .route("/api", api);
319
+
320
+ export type AppType = typeof routes;
321
+
322
+ export default app;
323
+ `;
324
+ return [
325
+ { path: "src/lib/auth.ts", content: FULL_LIB_AUTH, skipIfExists: true },
326
+ { path: "src/middleware/auth.ts", content: FULL_MW_AUTH, skipIfExists: true },
327
+ { path: "src/routes/index.ts", content: appRoutes, skipIfExists: false },
328
+ { path: "src/routes/site.ts", content: siteRoutes, skipIfExists: true },
329
+ { path: "src/routes/auth.ts", content: authRoutes, skipIfExists: true },
330
+ { path: "src/routes/dashboard.ts", content: dashboardRoutes, skipIfExists: true },
331
+ { path: "src/routes/api.ts", content: apiRoutes, skipIfExists: true },
332
+ { path: "src/pages/auth.svelte", content: FULL_PAGE_AUTH, skipIfExists: true },
333
+ {
334
+ path: "src/pages/dashboard/index.svelte",
335
+ content: FULL_PAGE_DASH_INDEX,
336
+ skipIfExists: true,
337
+ },
338
+ {
339
+ path: "src/pages/dashboard/page1.svelte",
340
+ content: FULL_PAGE_DASH_PAGE1,
341
+ skipIfExists: true,
342
+ },
343
+ {
344
+ path: "src/pages/dashboard/layout.svelte",
345
+ content: FULL_PAGE_DASH_LAYOUT,
346
+ skipIfExists: true,
347
+ },
63
348
  ];
64
349
  }
65
350
  export function versionsMatch(_linkedVersion, _appWants) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-svelte",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Server-side shell + multi-entry Svelte pages for Hono apps",
5
5
  "type": "module",
6
6
  "bin": {