honertia 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,18 +1,21 @@
1
1
  # Honertia
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/honertia.svg)](https://www.npmjs.com/package/honertia)
4
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/honertia)](https://bundlephobia.com/package/honertia)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
7
 
6
- An Inertia.js-style adapter for Hono with Effect.js integration. Build full-stack applications with type-safe server actions, Laravel-inspired validation, and seamless React rendering.
8
+ [![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=fff)](https://hono.dev/)
9
+ [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=fff)](https://workers.cloudflare.com/)
10
+ [![Effect](https://img.shields.io/badge/Effect-TS-black)](https://effect.website/)
7
11
 
8
- ## Requirements
12
+ ## Overview
9
13
 
10
- - **Runtime**: Node.js 18+ or Bun 1.0+
11
- - **Peer Dependencies**:
12
- - `hono` >= 4.0.0
13
- - `better-auth` >= 1.0.0
14
- - **Dependencies**:
15
- - `effect` >= 3.12.0
14
+ An Inertia.js-style adapter for Hono with Effect.js integration. Inertia keeps a server-driven app but behaves like an SPA: link clicks and form posts are intercepted, a fetch/XHR request returns a JSON page object (component + props), and the client swaps the page without a full reload. Honertia layers Laravel-style route patterns and Effect actions on top of that so handlers stay clean, readable, and composable.
15
+
16
+ ## Raison d'être
17
+
18
+ I've found myself wanting to use Cloudflare Workers for everything, but having come from a Laravel background nothing quite matched the DX and simplicity of Laravel x Inertia.js. When building Laravel projects I would always use Loris Leiva's laravel-actions package among other opinionated architecture decisions such as Vite, Tailwind, Bun, React etc., all of which have or will be incorporated into this project. With Cloudflare Workers the obvious choice is Hono and so we've adapted the Inertia.js protocol to run on workers+hono to mimic a Laravel-style app. Ever since learning of Effect.ts I've known that I wanted to use it for something bigger, and so we've utilised it here. Ultimately this is a workers+hono+vite+bun+laravel+inertia+effect+betterauth+planetscale mashup.
16
19
 
17
20
  ## Installation
18
21
 
@@ -25,157 +28,400 @@ bun add honertia
25
28
  ```typescript
26
29
  // src/index.ts
27
30
  import { Hono } from 'hono'
28
- import {
29
- honertia,
30
- createTemplate,
31
- loadUser,
32
- shareAuthMiddleware,
33
- effectBridge,
34
- effectRoutes,
35
- effectAuthRoutes,
36
- RequireAuthLayer,
37
- } from 'honertia'
38
-
39
- const app = new Hono()
40
-
41
- // Core middleware
42
- app.use('*', honertia({
43
- version: '1.0.0',
44
- render: createTemplate({
45
- title: 'My App',
46
- scripts: ['http://localhost:5173/src/main.tsx'],
47
- }),
48
- }))
49
-
50
- // Database & Auth setup (your own middleware)
31
+ import { logger } from 'hono/logger'
32
+ import { setupHonertia, createTemplate, createVersion, registerErrorHandlers, vite } from 'honertia'
33
+ import { Context, Layer } from 'effect'
34
+ import manifest from '../dist/manifest.json'
35
+
36
+ import { createDb } from './db'
37
+ import type { Env } from './types'
38
+ import { createAuth } from './lib/auth'
39
+ import { registerRoutes } from './routes'
40
+
41
+ const app = new Hono<Env>()
42
+ const assetVersion = createVersion(manifest)
43
+ const entry = manifest['src/main.tsx']
44
+ const assetPath = (path: string) => `/${path}`
45
+
46
+ class BindingsService extends Context.Tag('app/Bindings')<
47
+ BindingsService,
48
+ { KV: KVNamespace }
49
+ >() {}
50
+
51
+ // Request-scoped setup: put db/auth on c.var so Honertia/Effect can read them.
51
52
  app.use('*', async (c, next) => {
52
53
  c.set('db', createDb(c.env.DATABASE_URL))
53
- c.set('auth', createAuth({ /* config */ }))
54
+ c.set('auth', createAuth({
55
+ db: c.var.db,
56
+ secret: c.env.BETTER_AUTH_SECRET,
57
+ baseURL: new URL(c.req.url).origin,
58
+ }))
54
59
  await next()
55
60
  })
56
61
 
57
- // Auth middleware
58
- app.use('*', loadUser())
59
- app.use('*', shareAuthMiddleware())
62
+ // Honertia bundles the core middleware + auth loading + Effect runtime setup.
63
+ app.use('*', setupHonertia<Env, BindingsService>({
64
+ honertia: {
65
+ // Use your asset manifest hash so Inertia reloads on deploy.
66
+ version: assetVersion,
67
+ render: createTemplate((ctx) => {
68
+ const isProd = ctx.env.ENVIRONMENT === 'production'
69
+ return {
70
+ title: 'My Web App',
71
+ scripts: isProd ? [assetPath(entry.file)] : [vite.script()],
72
+ styles: isProd ? (entry.css ?? []).map(assetPath) : [],
73
+ head: isProd ? '' : vite.hmrHead(),
74
+ }
75
+ }),
76
+ },
77
+ effect: {
78
+ // Expose Cloudflare bindings to Effect handlers via a service layer.
79
+ services: (c) => Layer.succeed(BindingsService, {
80
+ KV: c.env.MY_KV,
81
+ }),
82
+ },
83
+ // Optional: extra Hono middleware in the same chain.
84
+ middleware: [
85
+ logger(),
86
+ // register additional middleware here...
87
+ ],
88
+ }))
60
89
 
61
- // Effect bridge (sets up Effect runtime per request)
62
- app.use('*', effectBridge())
90
+ registerRoutes(app)
91
+ registerErrorHandlers(app)
63
92
 
64
- // Register routes
65
- effectAuthRoutes(app, {
66
- loginComponent: 'Auth/Login',
67
- registerComponent: 'Auth/Register',
68
- })
93
+ export default app
94
+ ```
69
95
 
70
- effectRoutes(app)
71
- .provide(RequireAuthLayer)
72
- .group((route) => {
73
- route.get('/', showDashboard)
74
- route.get('/projects', listProjects)
75
- route.post('/projects', createProject)
96
+ ```typescript
97
+ // src/routes.ts
98
+ import type { Hono } from 'hono'
99
+ import type { Env } from './types'
100
+ import { effectRoutes } from 'honertia/effect'
101
+ import { effectAuthRoutes, RequireAuthLayer } from 'honertia/auth'
102
+ import { showDashboard, listProjects, createProject, showProject, deleteProject } from './actions'
103
+
104
+ export function registerRoutes(app: Hono<Env>) {
105
+ // Auth routes (login, register, logout, API handler) wired to better-auth.
106
+ // CORS for /api/auth/* can be enabled via the `cors` option (see below).
107
+ effectAuthRoutes(app, {
108
+ loginComponent: 'Auth/Login',
109
+ registerComponent: 'Auth/Register',
76
110
  })
77
111
 
78
- export default app
112
+ // Effect routes give you typed, DI-friendly handlers (no direct Hono ctx).
113
+ effectRoutes(app)
114
+ .provide(RequireAuthLayer)
115
+ .group((route) => {
116
+ // Grouped routes share layers and path prefixes.
117
+ route.get('/', showDashboard) // GET example.com
118
+
119
+ route.prefix('/projects').group((route) => {
120
+ route.get('/', listProjects) // GET example.com/projects
121
+ route.post('/', createProject) // POST example.com/projects
122
+ route.get('/:id', showProject) // GET example.com/projects/2
123
+ route.delete('/:id', deleteProject) // DELETE example.com/projects/2
124
+ })
125
+ })
126
+ }
79
127
  ```
80
128
 
81
- ### Using `setupHonertia` (Recommended)
129
+ ### Environment Variables
82
130
 
83
- For a cleaner setup, use `setupHonertia` which bundles all core middleware into a single call:
131
+ Honertia reads these from `c.env` (Cloudflare Workers bindings):
84
132
 
85
- ```typescript
86
- import { Hono } from 'hono'
87
- import { setupHonertia, createTemplate, effectRoutes } from 'honertia'
133
+ ```toml
134
+ # wrangler.toml
135
+ ENVIRONMENT = "production"
136
+ ```
88
137
 
89
- const app = new Hono()
138
+ If you prefer `wrangler.jsonc`, the same binding looks like:
90
139
 
91
- app.use('*', setupHonertia({
92
- honertia: {
93
- version: '1.0.0',
94
- render: createTemplate({
95
- title: 'My App',
96
- scripts: ['http://localhost:5173/src/main.tsx'],
97
- }),
140
+ ```jsonc
141
+ {
142
+ "vars": {
143
+ "ENVIRONMENT": "production"
144
+ }
145
+ }
146
+ ```
147
+
148
+ Set secrets like `DATABASE_URL` and `BETTER_AUTH_SECRET` via Wrangler (not in source control):
149
+
150
+ ```bash
151
+ wrangler secret put DATABASE_URL
152
+ wrangler secret put BETTER_AUTH_SECRET
153
+ ```
154
+
155
+ ### Client Setup (React + Inertia)
156
+
157
+ Honertia uses the standard Inertia React client. You'll need a client entry
158
+ point and a Vite build that emits a manifest (for `createVersion`).
159
+
160
+ Install client dependencies:
161
+
162
+ ```bash
163
+ bun add react react-dom @inertiajs/react
164
+ bun add -d @vitejs/plugin-react tailwindcss @tailwindcss/vite
165
+ ```
166
+
167
+ Create a Vite config that enables Tailwind v4, sets up an alias used in the
168
+ examples, and emits `dist/manifest.json`:
169
+
170
+ ```typescript
171
+ // vite.config.ts
172
+ import { defineConfig } from 'vite'
173
+ import react from '@vitejs/plugin-react'
174
+ import tailwindcss from '@tailwindcss/vite'
175
+ import path from 'path'
176
+
177
+ export default defineConfig({
178
+ plugins: [tailwindcss(), react()],
179
+ build: {
180
+ outDir: 'dist',
181
+ // Use an explicit filename so imports match build output.
182
+ manifest: 'manifest.json',
183
+ emptyOutDir: true,
98
184
  },
99
- auth: {
100
- userKey: 'user',
101
- sessionCookie: 'session',
185
+ resolve: {
186
+ alias: {
187
+ '~': path.resolve(__dirname, 'src'),
188
+ },
102
189
  },
103
- effect: {
104
- // Effect bridge configuration
190
+ })
191
+ ```
192
+
193
+ Create a Tailwind CSS entry file:
194
+
195
+ ```css
196
+ /* src/styles.css */
197
+ @import "tailwindcss";
198
+
199
+ @layer base {
200
+ body {
201
+ margin: 0;
202
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
203
+ background-color: #f8fafc;
204
+ color: #0f172a;
205
+ }
206
+ }
207
+ ```
208
+
209
+ Set up the Inertia client entry point (default path matches `vite.script()`):
210
+
211
+ ```tsx
212
+ // src/main.tsx
213
+ import './styles.css'
214
+
215
+ import { createInertiaApp } from '@inertiajs/react'
216
+ import { createRoot } from 'react-dom/client'
217
+
218
+ const pages = import.meta.glob('./pages/**/*.tsx')
219
+
220
+ createInertiaApp({
221
+ resolve: (name) => {
222
+ const page = pages[`./pages/${name}.tsx`]
223
+ if (!page) {
224
+ throw new Error(`Page not found: ${name}`)
225
+ }
226
+ return page()
105
227
  },
106
- }))
228
+ setup({ el, App, props }) {
229
+ createRoot(el).render(<App {...props} />)
230
+ },
231
+ })
232
+ ```
107
233
 
108
- export default app
234
+ The `resolve` function maps `render('Projects/Index')` to
235
+ `src/pages/Projects/Index.tsx`.
236
+
237
+ Optional: add a `tailwind.config.ts` only if you need theme extensions or
238
+ custom content globs.
239
+
240
+ ### Build & Deploy Notes
241
+
242
+ The server imports `dist/manifest.json`, so it must exist at build time. In
243
+ production, read scripts and styles from the manifest (Tailwind's CSS is listed
244
+ under your entry's `css` array). When deploying with Wrangler, build the client
245
+ assets first:
246
+
247
+ ```bash
248
+ # build client assets before deploying the worker
249
+ bun run build:client
250
+ wrangler deploy
109
251
  ```
110
252
 
111
- This is equivalent to manually registering:
112
- - `honertia()` - Core Honertia middleware
113
- - `loadUser()` - Loads authenticated user into context
114
- - `shareAuthMiddleware()` - Shares auth state with pages
115
- - `effectBridge()` - Sets up Effect runtime for each request
253
+ Optional dev convenience: if you want to run the worker without building the
254
+ client, you can keep a stub `dist/manifest.json` (ignored by git) and replace it
255
+ once you run `vite build`.
116
256
 
117
- #### Adding Custom Middleware
257
+ ### Recommended File Structure
118
258
 
119
- You can inject additional middleware that runs after the core setup:
259
+ Here's a minimal project layout that matches the Quick Start:
260
+
261
+ ```
262
+ .
263
+ ├── src/
264
+ │ ├── index.ts # Hono app setup (setupHonertia)
265
+ │ ├── routes.ts # effectRoutes / effectAuthRoutes
266
+ │ ├── main.tsx # Inertia + React client entry
267
+ │ ├── styles.css # Tailwind CSS entry
268
+ │ ├── actions/
269
+ │ │ └── projects/
270
+ │ │ └── list.ts # listProjects action
271
+ │ ├── pages/
272
+ │ │ └── Projects/
273
+ │ │ └── Index.tsx # render('Projects/Index')
274
+ │ ├── db.ts
275
+ │ ├── lib/
276
+ │ │ └── auth.ts
277
+ │ └── types.ts
278
+ ├── dist/
279
+ │ └── manifest.json # generated by Vite build
280
+ ├── vite.config.ts
281
+ ├── wrangler.toml # or wrangler.jsonc
282
+ ├── package.json
283
+ └── tsconfig.json
284
+ ```
285
+
286
+ ### Example Action
287
+
288
+ Here's the `listProjects` action referenced above:
120
289
 
121
290
  ```typescript
122
- import { cors } from 'hono/cors'
123
- import { logger } from 'hono/logger'
291
+ // src/actions/projects/list.ts
292
+ import { Effect } from 'effect'
293
+ import { eq } from 'drizzle-orm'
294
+ import { DatabaseService, AuthUserService, render, type AuthUser } from 'honertia/effect'
295
+ import { schema, type Database, type Project } from '../../db'
124
296
 
125
- app.use('*', setupHonertia({
126
- honertia: {
127
- version: '1.0.0',
128
- render: createTemplate({ title: 'My App', scripts: [...] }),
129
- },
130
- middleware: [
131
- cors(),
132
- logger(),
133
- myCustomMiddleware(),
134
- ],
135
- }))
297
+ interface ProjectsIndexProps {
298
+ projects: Project[]
299
+ }
300
+
301
+ const fetchProjects = (
302
+ db: Database,
303
+ user: AuthUser
304
+ ): Effect.Effect<ProjectsIndexProps, Error, never> =>
305
+ Effect.tryPromise({
306
+ try: async () => {
307
+ const projects = await db.query.projects.findMany({
308
+ where: eq(schema.projects.userId, user.user.id),
309
+ orderBy: (projects, { desc }) => [desc(projects.createdAt)],
310
+ })
311
+ return { projects }
312
+ },
313
+ catch: (error) => error instanceof Error ? error : new Error(String(error)),
314
+ })
315
+
316
+ export const listProjects = Effect.gen(function* () {
317
+ const db = yield* DatabaseService
318
+ const user = yield* AuthUserService
319
+ const props = yield* fetchProjects(db as Database, user)
320
+ return yield* render('Projects/Index', props)
321
+ })
322
+ ```
323
+
324
+ The component name `Projects/Index` maps to a file on disk. A common
325
+ Vite + React layout is:
326
+
327
+ ```
328
+ src/pages/Projects/Index.tsx
136
329
  ```
137
330
 
138
- The custom middleware runs in order after `effectBridge`, giving you access to all Honertia context variables.
331
+ That means the folders mirror the component path, and `Index.tsx` is the file
332
+ that exports the page component. In the example below, `Link` comes from
333
+ `@inertiajs/react` because it performs Inertia client-side visits (preserving
334
+ page state and avoiding full reloads), whereas a plain `<a>` would do a full
335
+ navigation.
336
+
337
+ ```tsx
338
+ // src/pages/Projects/Index.tsx
339
+ /**
340
+ * Projects Index Page
341
+ */
342
+
343
+ import { Link } from '@inertiajs/react'
344
+ import Layout from '~/components/Layout'
345
+ import type { PageProps, Project } from '~/types'
346
+
347
+ interface Props {
348
+ projects: Project[]
349
+ }
350
+
351
+ export default function ProjectsIndex({ projects }: PageProps<Props>) {
352
+ return (
353
+ <Layout>
354
+ <div className="flex justify-between items-center mb-6">
355
+ <h1 className="text-2xl font-bold text-gray-900">Projects</h1>
356
+ <Link
357
+ href="/projects/create"
358
+ className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700"
359
+ >
360
+ New Project
361
+ </Link>
362
+ </div>
363
+
364
+ <div className="bg-white rounded-lg shadow">
365
+ {projects.length === 0 ? (
366
+ <div className="p-6 text-center text-gray-500">
367
+ No projects yet.{' '}
368
+ <Link href="/projects/create" className="text-indigo-600 hover:underline">
369
+ Create your first project
370
+ </Link>
371
+ </div>
372
+ ) : (
373
+ <ul className="divide-y divide-gray-200">
374
+ {projects.map((project) => (
375
+ <li key={project.id}>
376
+ <Link
377
+ href={`/projects/${project.id}`}
378
+ className="block px-6 py-4 hover:bg-gray-50"
379
+ >
380
+ <div className="flex justify-between items-start">
381
+ <div>
382
+ <h3 className="text-sm font-medium text-gray-900">
383
+ {project.name}
384
+ </h3>
385
+ {project.description && (
386
+ <p className="text-sm text-gray-500 mt-1">
387
+ {project.description}
388
+ </p>
389
+ )}
390
+ </div>
391
+ <span className="text-sm text-gray-400">
392
+ {new Date(project.createdAt).toLocaleDateString()}
393
+ </span>
394
+ </div>
395
+ </Link>
396
+ </li>
397
+ ))}
398
+ </ul>
399
+ )}
400
+ </div>
401
+ </Layout>
402
+ )
403
+ }
404
+ ```
139
405
 
140
- #### Environment-Aware Templates
406
+ ### Vite Helpers
141
407
 
142
- `createTemplate` can accept a function that receives the Hono context, enabling environment-specific configuration:
408
+ The `vite` helper provides dev/prod asset management:
143
409
 
144
410
  ```typescript
145
- const viteHmrHead = `
146
- <script type="module">
147
- import RefreshRuntime from 'http://localhost:5173/@react-refresh'
148
- RefreshRuntime.injectIntoGlobalHook(window)
149
- window.$RefreshReg$ = () => {}
150
- window.$RefreshSig$ = () => (type) => type
151
- window.__vite_plugin_react_preamble_installed__ = true
152
- </script>
153
- <script type="module" src="http://localhost:5173/@vite/client"></script>
154
- `
155
-
156
- app.use('*', setupHonertia({
157
- honertia: {
158
- version: '1.0.0',
159
- render: createTemplate((ctx) => {
160
- const isProd = ctx.env.ENVIRONMENT === 'production'
161
- return {
162
- title: 'My App',
163
- scripts: isProd
164
- ? ['/assets/main.js']
165
- : ['http://localhost:5173/src/main.tsx'],
166
- head: isProd
167
- ? '<link rel="icon" href="/favicon.svg" />'
168
- : `${viteHmrHead}<link rel="icon" href="/favicon.svg" />`,
169
- }
170
- }),
171
- },
172
- }))
411
+ import { vite } from 'honertia'
412
+
413
+ vite.script() // 'http://localhost:5173/src/main.tsx'
414
+ vite.hmrHead() // HMR preamble script tags for React Fast Refresh
173
415
  ```
174
416
 
175
- This pattern allows you to:
176
- - Use Vite HMR in development, built assets in production
177
- - Access environment variables from `ctx.env`
178
- - Dynamically configure any template option based on request context
417
+ ## Requirements
418
+
419
+ - **Runtime**: Node.js 18+ or Bun 1.0+
420
+ - **Peer Dependencies**:
421
+ - `hono` >= 4.0.0
422
+ - `better-auth` >= 1.0.0
423
+ - **Dependencies**:
424
+ - `effect` >= 3.12.0
179
425
 
180
426
  ## Core Concepts
181
427
 
@@ -237,6 +483,91 @@ Honertia provides these services via Effect's dependency injection:
237
483
  | `RequestService` | Request context (params, query, body) |
238
484
  | `ResponseFactoryService` | Response builders |
239
485
 
486
+ #### Custom Services
487
+
488
+ You can inject Cloudflare Worker bindings (KV, D1, Queues, Analytics Engine) as services using the `services` option in `setupHonertia`, `effectBridge`, or `effectRoutes`.
489
+
490
+ Choose the option that matches your setup:
491
+
492
+ - `setupHonertia`: recommended for most apps; keeps config in one place and applies services to every Effect handler.
493
+ - `effectBridge`: use when wiring middleware manually or when you need precise middleware ordering; applies services to all Effect handlers.
494
+ - `effectRoutes`: use when you want services scoped to a route group or different services per group.
495
+
496
+ ```typescript
497
+ import { Effect, Layer, Context } from 'effect'
498
+ import { setupHonertia, effectBridge, effectRoutes } from 'honertia'
499
+
500
+ // Define your custom service
501
+ export class BindingsService extends Context.Tag('app/Bindings')<
502
+ BindingsService,
503
+ {
504
+ KV: KVNamespace
505
+ ANALYTICS: AnalyticsEngineDataset
506
+ DB: D1Database
507
+ }
508
+ >() {}
509
+
510
+ // Option 1: setupHonertia (global services via the one-liner setup)
511
+ app.use('*', setupHonertia<Env, BindingsService>({
512
+ honertia: {
513
+ version: '1.0.0',
514
+ render: (page) => JSON.stringify(page),
515
+ },
516
+ effect: {
517
+ services: (c) => Layer.succeed(BindingsService, {
518
+ KV: c.env.MY_KV,
519
+ ANALYTICS: c.env.ANALYTICS,
520
+ DB: c.env.DB,
521
+ }),
522
+ },
523
+ }))
524
+
525
+ // Option 2: effectBridge (manual middleware wiring)
526
+ app.use('*', effectBridge<Env, BindingsService>({
527
+ database: (c) => createDb(c.env.DATABASE_URL),
528
+ services: (c) => Layer.succeed(BindingsService, {
529
+ KV: c.env.MY_KV,
530
+ ANALYTICS: c.env.ANALYTICS,
531
+ DB: c.env.DB,
532
+ }),
533
+ }))
534
+
535
+ // Option 3: effectRoutes (scoped to a route group)
536
+ effectRoutes<Env, BindingsService>(app, {
537
+ services: (c) => Layer.succeed(BindingsService, {
538
+ KV: c.env.MY_KV,
539
+ ANALYTICS: c.env.ANALYTICS,
540
+ DB: c.env.DB,
541
+ }),
542
+ }).group((route) => {
543
+ route.get('/data', getDataFromKV)
544
+ })
545
+
546
+ // Use the custom service in your actions
547
+ const getDataFromKV = Effect.gen(function* () {
548
+ const bindings = yield* BindingsService
549
+ const value = yield* Effect.tryPromise(() =>
550
+ bindings.KV.get('my-key')
551
+ )
552
+ return yield* json({ value })
553
+ })
554
+ ```
555
+
556
+ You can provide multiple bindings in any option using `Layer.mergeAll` (for example, a `QueueService` tag for a queue binding):
557
+
558
+ ```typescript
559
+ app.use('*', effectBridge<Env, BindingsService | QueueService>({
560
+ services: (c) => Layer.mergeAll(
561
+ Layer.succeed(BindingsService, {
562
+ KV: c.env.MY_KV,
563
+ ANALYTICS: c.env.ANALYTICS,
564
+ DB: c.env.DB,
565
+ }),
566
+ Layer.succeed(QueueService, c.env.MY_QUEUE),
567
+ ),
568
+ }))
569
+ ```
570
+
240
571
  ### Routing
241
572
 
242
573
  Use `effectRoutes` for Laravel-style route definitions:
@@ -487,6 +818,23 @@ effectAuthRoutes(app, {
487
818
  })
488
819
  ```
489
820
 
821
+ To enable CORS for the auth API handler (`/api/auth/*`), pass a `cors` config.
822
+ By default, no CORS headers are added (recommended when your UI and API share the same origin).
823
+ Use this when your frontend is on a different origin (local dev, separate domain, mobile app, etc.).
824
+
825
+ ```typescript
826
+ effectAuthRoutes(app, {
827
+ apiPath: '/api/auth',
828
+ cors: {
829
+ origin: ['http://localhost:5173', 'http://localhost:3000'],
830
+ credentials: true,
831
+ },
832
+ })
833
+ ```
834
+
835
+ This sets the appropriate `Access-Control-*` headers and handles `OPTIONS` preflight for the auth API routes.
836
+ Always keep the `origin` list tight; avoid `'*'` for auth endpoints, especially with `credentials: true`.
837
+
490
838
  ## Action Factories
491
839
 
492
840
  For common patterns, use action factories:
@@ -8,9 +8,33 @@ import type { Context as HonoContext, MiddlewareHandler, Env } from 'hono';
8
8
  import { DatabaseService, AuthService, AuthUserService, HonertiaService, RequestService, ResponseFactoryService } from './services.js';
9
9
  /**
10
10
  * Configuration for the Effect bridge.
11
+ *
12
+ * @typeParam E - Hono environment type
13
+ * @typeParam CustomServices - Custom services provided via the `services` option
14
+ *
15
+ * @example
16
+ * // Provide Cloudflare Worker bindings as a service
17
+ * effectBridge<Env, BindingsService>({
18
+ * database: (c) => createDb(c.env.DATABASE_URL),
19
+ * services: (c) => Layer.succeed(BindingsService, c.env),
20
+ * })
21
+ *
22
+ * @example
23
+ * // Provide multiple custom services
24
+ * effectBridge<Env, BindingsService | LoggerService>({
25
+ * services: (c) => Layer.mergeAll(
26
+ * Layer.succeed(BindingsService, c.env),
27
+ * Layer.succeed(LoggerService, createLogger(c)),
28
+ * ),
29
+ * })
11
30
  */
12
- export interface EffectBridgeConfig<E extends Env> {
31
+ export interface EffectBridgeConfig<E extends Env, CustomServices = never> {
13
32
  database?: (c: HonoContext<E>) => unknown;
33
+ /**
34
+ * Custom services to provide to all Effect handlers.
35
+ * Return a Layer that provides your custom services.
36
+ */
37
+ services?: (c: HonoContext<E>) => Layer.Layer<CustomServices, never, never>;
14
38
  }
15
39
  /**
16
40
  * Symbol for storing Effect runtime in Hono context.
@@ -27,7 +51,7 @@ declare module 'hono' {
27
51
  /**
28
52
  * Build the Effect layer from Hono context.
29
53
  */
30
- export declare function buildContextLayer<E extends Env>(c: HonoContext<E>, config?: EffectBridgeConfig<E>): Layer.Layer<RequestService | ResponseFactoryService | HonertiaService | DatabaseService | AuthService | AuthUserService, never, never>;
54
+ export declare function buildContextLayer<E extends Env, CustomServices = never>(c: HonoContext<E>, config?: EffectBridgeConfig<E, CustomServices>): Layer.Layer<RequestService | ResponseFactoryService | HonertiaService | DatabaseService | AuthService | AuthUserService | CustomServices, never, never>;
31
55
  /**
32
56
  * Get the Effect runtime from Hono context.
33
57
  */
@@ -35,6 +59,6 @@ export declare function getEffectRuntime<E extends Env>(c: HonoContext<E>): Mana
35
59
  /**
36
60
  * Middleware that sets up the Effect runtime for each request.
37
61
  */
38
- export declare function effectBridge<E extends Env>(config?: EffectBridgeConfig<E>): MiddlewareHandler<E>;
62
+ export declare function effectBridge<E extends Env, CustomServices = never>(config?: EffectBridgeConfig<E, CustomServices>): MiddlewareHandler<E>;
39
63
  export {};
40
64
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/effect/bridge.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAU,KAAK,EAAE,cAAc,EAAW,MAAM,QAAQ,CAAA;AAC/D,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAC1E,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EAKvB,MAAM,eAAe,CAAA;AAEtB;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,GAAG;IAC/C,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAA;CAC1C;AAED;;GAEG;AACH,QAAA,MAAM,cAAc,eAA0B,CAAA;AAE9C;;GAEG;AACH,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAkB;QAC1B,CAAC,cAAc,CAAC,CAAC,EAAE,cAAc,CAAC,cAAc,CAC5C,eAAe,GACf,WAAW,GACX,eAAe,GACf,eAAe,GACf,cAAc,GACd,sBAAsB,EACxB,KAAK,CACN,CAAA;KACF;CACF;AAqDD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,GAAG,EAC7C,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EACjB,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC7B,KAAK,CAAC,KAAK,CACV,cAAc,GACd,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,WAAW,GACX,eAAe,EACjB,KAAK,EACL,KAAK,CACN,CAmCA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAC5C,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAChB,cAAc,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,SAAS,CAEvD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,GAAG,EACxC,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC7B,iBAAiB,CAAC,CAAC,CAAC,CAetB"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/effect/bridge.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAU,KAAK,EAAE,cAAc,EAAW,MAAM,QAAQ,CAAA;AAC/D,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAC1E,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EAKvB,MAAM,eAAe,CAAA;AAEtB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK;IACvE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;CAC5E;AAED;;GAEG;AACH,QAAA,MAAM,cAAc,eAA0B,CAAA;AAE9C;;GAEG;AACH,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAkB;QAC1B,CAAC,cAAc,CAAC,CAAC,EAAE,cAAc,CAAC,cAAc,CAC5C,eAAe,GACf,WAAW,GACX,eAAe,GACf,eAAe,GACf,cAAc,GACd,sBAAsB,EACxB,KAAK,CACN,CAAA;KACF;CACF;AAqDD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK,EACrE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EACjB,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,GAC7C,KAAK,CAAC,KAAK,CACV,cAAc,GACd,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,WAAW,GACX,eAAe,GACf,cAAc,EAChB,KAAK,EACL,KAAK,CACN,CA0CA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAC5C,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAChB,cAAc,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,SAAS,CAEvD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK,EAChE,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,GAC7C,iBAAiB,CAAC,CAAC,CAAC,CAetB"}
@@ -75,6 +75,11 @@ export function buildContextLayer(c, config) {
75
75
  if (c.var?.authUser) {
76
76
  baseLayer = Layer.merge(baseLayer, Layer.succeed(AuthUserService, c.var.authUser));
77
77
  }
78
+ // Merge custom services if provided
79
+ if (config?.services) {
80
+ const customServicesLayer = config.services(c);
81
+ baseLayer = Layer.merge(baseLayer, customServicesLayer);
82
+ }
78
83
  return baseLayer;
79
84
  }
80
85
  /**
@@ -21,25 +21,25 @@ export type BaseServices = RequestService | ResponseFactoryService | HonertiaSer
21
21
  /**
22
22
  * Effect Route Builder with layer composition.
23
23
  */
24
- export declare class EffectRouteBuilder<E extends Env, ProvidedServices = never> {
24
+ export declare class EffectRouteBuilder<E extends Env, ProvidedServices = never, CustomServices = never> {
25
25
  private readonly app;
26
26
  private readonly layers;
27
27
  private readonly pathPrefix;
28
28
  private readonly bridgeConfig?;
29
- constructor(app: Hono<E>, layers?: Layer.Layer<any, never, never>[], pathPrefix?: string, bridgeConfig?: EffectBridgeConfig<E> | undefined);
29
+ constructor(app: Hono<E>, layers?: Layer.Layer<any, never, never>[], pathPrefix?: string, bridgeConfig?: EffectBridgeConfig<E, CustomServices> | undefined);
30
30
  /**
31
31
  * Add a layer to provide services to all routes in this builder.
32
32
  * The layer's error type must be handled by the effect bridge (AppError or subtype).
33
33
  */
34
- provide<S, LayerErr extends AppError>(layer: Layer.Layer<S, LayerErr, never>): EffectRouteBuilder<E, ProvidedServices | S>;
34
+ provide<S, LayerErr extends AppError>(layer: Layer.Layer<S, LayerErr, never>): EffectRouteBuilder<E, ProvidedServices | S, CustomServices>;
35
35
  /**
36
36
  * Set path prefix for all routes in this builder.
37
37
  */
38
- prefix(path: string): EffectRouteBuilder<E, ProvidedServices>;
38
+ prefix(path: string): EffectRouteBuilder<E, ProvidedServices, CustomServices>;
39
39
  /**
40
40
  * Create a nested group with the same configuration.
41
41
  */
42
- group(callback: (route: EffectRouteBuilder<E, ProvidedServices>) => void): void;
42
+ group(callback: (route: EffectRouteBuilder<E, ProvidedServices, CustomServices>) => void): void;
43
43
  /**
44
44
  * Resolve the full path.
45
45
  */
@@ -51,27 +51,27 @@ export declare class EffectRouteBuilder<E extends Env, ProvidedServices = never>
51
51
  /**
52
52
  * Register a GET route.
53
53
  */
54
- get<R extends BaseServices | ProvidedServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
54
+ get<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
55
55
  /**
56
56
  * Register a POST route.
57
57
  */
58
- post<R extends BaseServices | ProvidedServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
58
+ post<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
59
59
  /**
60
60
  * Register a PUT route.
61
61
  */
62
- put<R extends BaseServices | ProvidedServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
62
+ put<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
63
63
  /**
64
64
  * Register a PATCH route.
65
65
  */
66
- patch<R extends BaseServices | ProvidedServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
66
+ patch<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
67
67
  /**
68
68
  * Register a DELETE route.
69
69
  */
70
- delete<R extends BaseServices | ProvidedServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
70
+ delete<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
71
71
  /**
72
72
  * Register a route for all HTTP methods.
73
73
  */
74
- all<R extends BaseServices | ProvidedServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
74
+ all<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
75
75
  }
76
76
  /**
77
77
  * Create an Effect route builder for an app.
@@ -86,5 +86,5 @@ export declare class EffectRouteBuilder<E extends Env, ProvidedServices = never>
86
86
  * route.post('/projects', createProject)
87
87
  * })
88
88
  */
89
- export declare function effectRoutes<E extends Env>(app: Hono<E>, config?: EffectBridgeConfig<E>): EffectRouteBuilder<E, never>;
89
+ export declare function effectRoutes<E extends Env, CustomServices = never>(app: Hono<E>, config?: EffectBridgeConfig<E, CustomServices>): EffectRouteBuilder<E, never, CustomServices>;
90
90
  //# sourceMappingURL=routing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/effect/routing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/effect/routing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACtC,OAAO,KAAK,EAAE,IAAI,EAAqB,GAAG,EAAE,MAAM,MAAM,CAAA;AAExD,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EACL,eAAe,EACf,WAAW,EAEX,eAAe,EACf,cAAc,EACd,sBAAsB,EACvB,MAAM,eAAe,CAAA;AAEtB;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,SAAS,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,CACjG,QAAQ,GAAG,QAAQ,EACnB,CAAC,EACD,CAAC,CACF,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,WAAW,CAAA;AAEf;;GAEG;AACH,qBAAa,kBAAkB,CAC7B,CAAC,SAAS,GAAG,EACb,gBAAgB,GAAG,KAAK,EACxB,cAAc,GAAG,KAAK;IAGpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;gBAHb,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,MAAM,GAAE,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,EAAO,EAC7C,UAAU,GAAE,MAAW,EACvB,YAAY,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,YAAA;IAGvE;;;OAGG;IACH,OAAO,CAAC,CAAC,EAAE,QAAQ,SAAS,QAAQ,EAClC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,GACrC,kBAAkB,CAAC,CAAC,EAAE,gBAAgB,GAAG,CAAC,EAAE,cAAc,CAAC;IAS9D;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAC,EAAE,gBAAgB,EAAE,cAAc,CAAC;IAU7E;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC,EAAE,gBAAgB,EAAE,cAAc,CAAC,KAAK,IAAI,GAAG,IAAI;IAI/F;;OAEG;IACH,OAAO,CAAC,WAAW;IAOnB;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,YAAY,GAAG,gBAAgB,GAAG,cAAc,EAAM,IAAI,EAAE,MAAM,EAC9E,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GACzC,IAAI;IAIP;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,YAAY,GAAG,gBAAgB,GAAG,cAAc,EAC7D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GACzC,IAAI;IAIP;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,YAAY,GAAG,gBAAgB,GAAG,cAAc,EAC5D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GACzC,IAAI;IAIP;;OAEG;IACH,KAAK,CAAC,CAAC,SAAS,YAAY,GAAG,gBAAgB,GAAG,cAAc,EAC9D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GACzC,IAAI;IAIP;;OAEG;IACH,MAAM,CAAC,CAAC,SAAS,YAAY,GAAG,gBAAgB,GAAG,cAAc,EAC/D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GACzC,IAAI;IAIP;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,YAAY,GAAG,gBAAgB,GAAG,cAAc,EAC5D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,GACzC,IAAI;CAGR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK,EAChE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,GAC7C,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAE9C"}
package/dist/helpers.d.ts CHANGED
@@ -6,6 +6,12 @@ import type { PageObject } from './types.js';
6
6
  export interface PageProps {
7
7
  errors?: Record<string, string>;
8
8
  }
9
+ export interface AssetManifestEntry {
10
+ file?: string;
11
+ css?: string[];
12
+ assets?: string[];
13
+ }
14
+ export type AssetManifest = Record<string, string | AssetManifestEntry>;
9
15
  export interface TemplateOptions {
10
16
  title?: string;
11
17
  scripts?: string[];
@@ -26,16 +32,23 @@ export interface TemplateOptions {
26
32
  *
27
33
  * @example Dynamic config based on environment
28
34
  * ```ts
35
+ * const entry = manifest['src/main.tsx']
36
+ * const assetPath = (path: string) => `/${path}`
37
+ *
29
38
  * createTemplate((ctx) => ({
30
39
  * title: 'App',
31
40
  * scripts: ctx.env.ENVIRONMENT === 'production'
32
- * ? ['/assets/main.js']
33
- * : ['http://localhost:5173/src/main.tsx'],
41
+ * ? [assetPath(entry.file)]
42
+ * : [vite.script()],
43
+ * styles: ctx.env.ENVIRONMENT === 'production'
44
+ * ? (entry.css ?? []).map(assetPath)
45
+ * : [],
46
+ * head: ctx.env.ENVIRONMENT === 'production' ? '' : vite.hmrHead(),
34
47
  * }))
35
48
  * ```
36
49
  */
37
50
  export declare function createTemplate(options: TemplateOptions | ((ctx: Context) => TemplateOptions)): (page: PageObject, ctx?: Context) => string;
38
- export declare function createVersion(manifest: Record<string, string>): string;
51
+ export declare function createVersion(manifest: AssetManifest): string;
39
52
  /**
40
53
  * Vite development configuration helpers.
41
54
  */
@@ -57,7 +70,7 @@ export declare const vite: {
57
70
  * @param port - Vite dev server port (default: 5173)
58
71
  * @example
59
72
  * ```ts
60
- * scripts: isProd ? ['/assets/main.js'] : [vite.script()]
73
+ * scripts: isProd ? [manifest['src/main.tsx'].file] : [vite.script()]
61
74
  * ```
62
75
  */
63
76
  script(entry?: string, port?: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,eAAe,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,eAAe,CAAC,GAC7D,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,MAAM,CA6C7C;AAWD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;IACf;;;;;;;;OAQG;4BACmB,MAAM;IAa5B;;;;;;;;;OASG;2CAC2C,MAAM;CAGrD,CAAA"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,EAAE,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAA;AAEvE,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,eAAe,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,eAAe,CAAC,GAC7D,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,MAAM,CA6C7C;AAWD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CA8B7D;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;IACf;;;;;;;;OAQG;4BACmB,MAAM;IAa5B;;;;;;;;;OASG;2CAC2C,MAAM;CAGrD,CAAA"}
package/dist/helpers.js CHANGED
@@ -14,11 +14,18 @@
14
14
  *
15
15
  * @example Dynamic config based on environment
16
16
  * ```ts
17
+ * const entry = manifest['src/main.tsx']
18
+ * const assetPath = (path: string) => `/${path}`
19
+ *
17
20
  * createTemplate((ctx) => ({
18
21
  * title: 'App',
19
22
  * scripts: ctx.env.ENVIRONMENT === 'production'
20
- * ? ['/assets/main.js']
21
- * : ['http://localhost:5173/src/main.tsx'],
23
+ * ? [assetPath(entry.file)]
24
+ * : [vite.script()],
25
+ * styles: ctx.env.ENVIRONMENT === 'production'
26
+ * ? (entry.css ?? []).map(assetPath)
27
+ * : [],
28
+ * head: ctx.env.ENVIRONMENT === 'production' ? '' : vite.hmrHead(),
22
29
  * }))
23
30
  * ```
24
31
  */
@@ -66,7 +73,26 @@ function escapeHtml(str) {
66
73
  .replace(/'/g, '&#39;');
67
74
  }
68
75
  export function createVersion(manifest) {
69
- const combined = Object.values(manifest).sort().join('');
76
+ const assetFiles = [];
77
+ for (const value of Object.values(manifest)) {
78
+ if (typeof value === 'string') {
79
+ assetFiles.push(value);
80
+ continue;
81
+ }
82
+ if (!value || typeof value !== 'object') {
83
+ continue;
84
+ }
85
+ if (typeof value.file === 'string') {
86
+ assetFiles.push(value.file);
87
+ }
88
+ if (Array.isArray(value.css)) {
89
+ assetFiles.push(...value.css);
90
+ }
91
+ if (Array.isArray(value.assets)) {
92
+ assetFiles.push(...value.assets);
93
+ }
94
+ }
95
+ const combined = assetFiles.sort().join('');
70
96
  let hash = 0;
71
97
  for (let i = 0; i < combined.length; i++) {
72
98
  const char = combined.charCodeAt(i);
@@ -107,7 +133,7 @@ export const vite = {
107
133
  * @param port - Vite dev server port (default: 5173)
108
134
  * @example
109
135
  * ```ts
110
- * scripts: isProd ? ['/assets/main.js'] : [vite.script()]
136
+ * scripts: isProd ? [manifest['src/main.tsx'].file] : [vite.script()]
111
137
  * ```
112
138
  */
113
139
  script(entry = '/src/main.tsx', port = 5173) {
package/dist/setup.d.ts CHANGED
@@ -10,7 +10,7 @@ import { type EffectBridgeConfig } from './effect/bridge.js';
10
10
  /**
11
11
  * Configuration for Honertia setup.
12
12
  */
13
- export interface HonertiaSetupConfig<E extends Env = Env> {
13
+ export interface HonertiaSetupConfig<E extends Env = Env, CustomServices = never> {
14
14
  /**
15
15
  * Honertia core configuration.
16
16
  */
@@ -18,7 +18,7 @@ export interface HonertiaSetupConfig<E extends Env = Env> {
18
18
  /**
19
19
  * Effect bridge configuration (optional).
20
20
  */
21
- effect?: EffectBridgeConfig<E>;
21
+ effect?: EffectBridgeConfig<E, CustomServices>;
22
22
  /**
23
23
  * Auth configuration (optional).
24
24
  */
@@ -53,7 +53,7 @@ export interface HonertiaSetupConfig<E extends Env = Env> {
53
53
  * }))
54
54
  * ```
55
55
  */
56
- export declare function setupHonertia<E extends Env>(config: HonertiaSetupConfig<E>): MiddlewareHandler<E>;
56
+ export declare function setupHonertia<E extends Env, CustomServices = never>(config: HonertiaSetupConfig<E, CustomServices>): MiddlewareHandler<E>;
57
57
  /**
58
58
  * Error handler configuration.
59
59
  */
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE1E;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACtD;;OAEG;IACH,QAAQ,EAAE,cAAc,CAAA;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAA;IAE9B;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,CAAA;IAED;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAA;CACpC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,GAAG,EACzC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC7B,iBAAiB,CAAC,CAAC,CAAC,CAmBtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,GAAE,kBAAuB;kBAQ3D,OAAO,CAAC,CAAC,CAAC;mBAOT,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC;EAU3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,GAAG,EACjD,GAAG,EAAE;IAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAA;CAAE,EAC1E,MAAM,GAAE,kBAAuB,GAC9B,IAAI,CAIN"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE1E;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG,EAAE,cAAc,GAAG,KAAK;IAC9E;;OAEG;IACH,QAAQ,EAAE,cAAc,CAAA;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,CAAA;IAE9C;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,CAAA;IAED;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAA;CACpC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK,EACjE,MAAM,EAAE,mBAAmB,CAAC,CAAC,EAAE,cAAc,CAAC,GAC7C,iBAAiB,CAAC,CAAC,CAAC,CAmBtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,GAAE,kBAAuB;kBAQ3D,OAAO,CAAC,CAAC,CAAC;mBAOT,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC;EAU3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,GAAG,EACjD,GAAG,EAAE;IAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAA;CAAE,EAC1E,MAAM,GAAE,kBAAuB,GAC9B,IAAI,CAIN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honertia",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Inertia.js-style server-driven SPA adapter for Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",