honox 0.0.0 → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 - present, Yusuke Wada and Hono contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,650 @@
1
+ # HonoX
2
+
3
+ **HonoX** is a simple and fast - _supersonic_ - meta framework for creating full-stack websites or Web APIs - (formerly _[Sonik](https://github.com/sonikjs/sonik)_). It stands on the shoulders of giants; built on [Hono](https://hono.dev/), [Vite](https://hono.dev/), and UI libraries.
4
+
5
+ **Note**: _HonoX is currently in a "beta stage". Breaking changes are introduced without following semantic versioning._
6
+
7
+ ## Features
8
+
9
+ - **File-based routing** - You can create a large application like Next.js.
10
+ - **Fast SSR** - Rendering is ultra-fast thanks to Hono.
11
+ - **BYOR** - You can bring your own renderer, not only one using hono/jsx.
12
+ - **Islands hydration** - If you want interactions, create an island. JavaScript is hydrated only for it.
13
+ - **Middleware** - It works as Hono, so you can use a lot of Hono's middleware.
14
+
15
+ ## Get Started - Basic
16
+
17
+ Let's create a basic HonoX application using hono/jsx as a renderer. This application has no client JavaScript and renders JSX on the server side.
18
+
19
+ ### Project Structure
20
+
21
+ Below is a typical project structure for a HonoX application.
22
+
23
+ ```txt
24
+ .
25
+ ├── app
26
+ │   ├── global.d.ts // global type definitions
27
+ │   ├── routes
28
+ │   │   ├── _404.tsx // not found page
29
+ │   │   ├── _error.tsx // error page
30
+ │   │   ├── _renderer.tsx // renderer definition
31
+ │   │   ├── about
32
+ │   │   │   └── [name].tsx // matches `/about/:name`
33
+ │   │   └── index.tsx // matches `/`
34
+ │   └── server.ts // server entry file
35
+ ├── package.json
36
+ ├── tsconfig.json
37
+ └── vite.config.ts
38
+ ```
39
+
40
+ ### `vite.config.ts`
41
+
42
+ The minimum Vite setup for development is as follows:
43
+
44
+ ```ts
45
+ import { defineConfig } from 'vite'
46
+ import honox from 'honox/vite'
47
+
48
+ export default defineConfig({
49
+ plugins: [honox()],
50
+ })
51
+ ```
52
+
53
+ ### Server Entry File
54
+
55
+ A server entry file is required. The file is should be placed at `app/server.ts`. This file is first called by the Vite during the development or build phase.
56
+
57
+ In the entry file, simply initialize your app using the `createApp()` function. `app` will be an instance of Hono, so you can use Hono's middleware and the `showRoutes()`in`hono/dev`.
58
+
59
+ ```ts
60
+ // app/server.ts
61
+ import { createApp } from 'honox/server'
62
+ import { showRoutes } from 'hono/dev'
63
+
64
+ const app = createApp()
65
+
66
+ showRoutes(app)
67
+
68
+ export default app
69
+ ```
70
+
71
+ ### Routes
72
+
73
+ There are three ways to define routes.
74
+
75
+ #### 1. `createRoute()`
76
+
77
+ Each route should return an array of `Handler | MiddlewareHandler`. `createRoute()` is a helper function to return it. You can write a route for a GET request with `default export`.
78
+
79
+ ```tsx
80
+ // `createRoute()` helps you create handlers
81
+ import { createRoute } from 'honox/factory'
82
+
83
+ export default createRoute((c) => {
84
+ return c.render(
85
+ <div>
86
+ <h1>Hello!</h1>
87
+ </div>
88
+ )
89
+ })
90
+ ```
91
+
92
+ You can also handle methods other than GET by `export` `POST`, `PUT`, and `DELETE`.
93
+
94
+ ```tsx
95
+ import { createRoute } from 'honox/factory'
96
+ import { getCookie, setCookie } from 'hono/cookie'
97
+
98
+ export const POST = createRoute(async (c) => {
99
+ const { name } = await c.req.parseBody<{ name: string }>()
100
+ setCookie(c, 'name', name)
101
+ return c.redirect('/')
102
+ })
103
+
104
+ export default createRoute((c) => {
105
+ const name = getCookie(c, 'name') ?? 'no name'
106
+ return c.render(
107
+ <div>
108
+ <h1>Hello, {name}!</h1>
109
+ <form method='POST'>
110
+ <input type='text' name='name' placeholder='name' />
111
+ <input type='submit' />
112
+ </form>
113
+ </div>
114
+ )
115
+ })
116
+ ```
117
+
118
+ #### 2. Using Hono instance
119
+
120
+ You can create API endpoints by exporting an instance of the Hono object.
121
+
122
+ ```ts
123
+ // app/routes/about/index.ts
124
+ import { Hono } from 'hono'
125
+
126
+ const app = new Hono()
127
+
128
+ // matches `/about/:name`
129
+ app.get('/:name', (c) => {
130
+ const name = c.req.param('name')
131
+ return c.json({
132
+ 'your name is': name,
133
+ })
134
+ })
135
+
136
+ export default app
137
+ ```
138
+
139
+ #### 3. Just return JSX
140
+
141
+ Or simply, you can just return JSX.
142
+
143
+ ```tsx
144
+ export default function Home() {
145
+ return <h1>Welcome!</h1>
146
+ }
147
+ ```
148
+
149
+ ### Renderer
150
+
151
+ Define your renderer - the middleware that does `c.setRender()` - by writing it in `_renderer.tsx`.
152
+
153
+ Before writing `_renderer.tsx`, write the Renderer type definition in `global.d.ts`.
154
+
155
+ ```ts
156
+ // app/global.d.ts
157
+ import type {} from 'hono'
158
+
159
+ type Head = {
160
+ title?: string
161
+ }
162
+
163
+ declare module 'hono' {
164
+ interface ContextRenderer {
165
+ (content: string | Promise<string>, head?: Head): Response | Promise<Response>
166
+ }
167
+ }
168
+ ```
169
+
170
+ The JSX Renderer middleware allows you to create a Renderer as follows:
171
+
172
+ ```tsx
173
+ // app/routes/_renderer.tsx
174
+ import { jsxRenderer } from 'hono/jsx-renderer'
175
+
176
+ export default jsxRenderer(({ children, title }) => {
177
+ return (
178
+ <html lang='en'>
179
+ <head>
180
+ <meta charset='UTF-8' />
181
+ <meta name='viewport' content='width=device-width, initial-scale=1.0' />
182
+ {title ? <title>{title}</title> : <></>}
183
+ </head>
184
+ <body>{children}</body>
185
+ </html>
186
+ )
187
+ })
188
+ ```
189
+
190
+ The `_renderer.tsx` is applied under each directory, and the `app/routes/posts/_renderer.tsx` is applied in `app/routes/posts/*`.
191
+
192
+ ### Not Found page
193
+
194
+ You can write a custom Not Found page in `_404.tx`.
195
+
196
+ ```tsx
197
+ // app/routes/_404.tsx
198
+ import { NotFoundHandler } from 'hono'
199
+
200
+ const handler: NotFoundHandler = (c) => {
201
+ return c.render(<h1>Sorry, Not Found...</h1>)
202
+ }
203
+
204
+ export default handler
205
+ ```
206
+
207
+ ### Error Page
208
+
209
+ You can write a custom Error page in `_error.tx`.
210
+
211
+ ```tsx
212
+ // app/routes/_error.ts
213
+ import { ErrorHandler } from 'hono'
214
+
215
+ const handler: ErrorHandler = (e, c) => {
216
+ return c.render(<h1>Error! {e.message}</h1>)
217
+ }
218
+
219
+ export default handler
220
+ ```
221
+
222
+ ## Get Started - with Client
223
+
224
+ Let's create an application that includes a client side. Here, we will use hono/jsx/dom.
225
+
226
+ ### Project Structure
227
+
228
+ The below is the project structure of a minimal application including a client side:
229
+
230
+ ```txt
231
+ .
232
+ ├── app
233
+ │   ├── client.ts // client entry file
234
+ │   ├── global.d.ts
235
+ │   ├── islands
236
+ │   │   └── counter.tsx // island component
237
+ │   ├── routes
238
+ │   │   ├── _renderer.tsx
239
+ │   │   └── index.tsx
240
+ │   └── server.ts
241
+ ├── package.json
242
+ ├── tsconfig.json
243
+ └── vite.config.ts
244
+ ```
245
+
246
+ ### Renderer
247
+
248
+ This is a `_renderer.tsx`, which will load the `/app/client.ts` entry file for the client. It will load the JavaScript file for the production according to the variable `import.meta.env.PROD`. And renders the inside of `HasIslands` if there are islands on that page.
249
+
250
+ ```tsx
251
+ // app/routes/_renderer.tsx
252
+ import { jsxRenderer } from 'hono/jsx-renderer'
253
+
254
+ export default jsxRenderer(({ children }) => {
255
+ return (
256
+ <html lang='en'>
257
+ <head>
258
+ <meta charset='UTF-8' />
259
+ <meta name='viewport' content='width=device-width, initial-scale=1.0' />
260
+ {import.meta.env.PROD ? (
261
+ <HasIslands>
262
+ <script type='module' src='/static/client.js'></script>
263
+ </HasIslands>
264
+ ) : (
265
+ <script type='module' src='/app/client.ts'></script>
266
+ )}
267
+ </head>
268
+ <body>{children}</body>
269
+ </html>
270
+ )
271
+ })
272
+ ```
273
+
274
+ ### Client Entry File
275
+
276
+ A client side entry file should be in `app/client.ts`. Simply, write `createClient()`.
277
+
278
+ ```ts
279
+ // app/client.ts
280
+ import { createClient } from 'honox/client'
281
+
282
+ createClient()
283
+ ```
284
+
285
+ ### Interactions
286
+
287
+ Function components placed in `app/islands/*` are also sent to the client side. For example, you can write interactive component such as the following counter:
288
+
289
+ ```tsx
290
+ // app/islands/counter.tsx
291
+ import { useState } from 'hono/jsx'
292
+
293
+ export default function Counter() {
294
+ const [count, setCount] = useState(0)
295
+ return (
296
+ <div>
297
+ <p>Count: {count}</p>
298
+ <button onClick={() => setCount(count + 1)}>Increment</button>
299
+ </div>
300
+ )
301
+ }
302
+ ```
303
+
304
+ When you load the component in a route file, it is rendered as Server-Side rendering and JavaScript is also send to the client-side.
305
+
306
+ ```tsx
307
+ // app/routes/index.tsx
308
+ import { createRoute } from 'honox/factory'
309
+ import Counter from '../islands/counter'
310
+
311
+ export default createRoute((c) => {
312
+ return c.render(
313
+ <div>
314
+ <h1>Hello</h1>
315
+ <Counter />
316
+ </div>
317
+ )
318
+ })
319
+ ```
320
+
321
+ ## BYOR - Bring Your Own Renderer
322
+
323
+ You can bring your own renderer using a UI library like React, Preact, Solid, or others.
324
+
325
+ **Note**: We may not provide supports for the renderer you bring.
326
+
327
+ ### React case
328
+
329
+ You can define a renderer using [`@hono/react-renderer`](https://github.com/honojs/middleware/tree/main/packages/react-renderer). Install the modules first.
330
+
331
+ ```txt
332
+ npm i @hono/react-renderer react react-dom hono
333
+ npm i -D @types/react @types/react-dom
334
+ ```
335
+
336
+ Define the Props that the renderer will receive in `global.d.ts`.
337
+
338
+ ```ts
339
+ // global.d.ts
340
+ import '@hono/react-renderer'
341
+
342
+ declare module '@hono/react-renderer' {
343
+ interface Props {
344
+ title?: string
345
+ }
346
+ }
347
+ ```
348
+
349
+ The following is an example of `app/routes/renderer.tsx`.
350
+
351
+ ```tsx
352
+ // app/routes/_renderer.tsx
353
+ import { reactRenderer } from '@hono/react-renderer'
354
+
355
+ export default reactRenderer(({ children, title }) => {
356
+ return (
357
+ <html lang='en'>
358
+ <head>
359
+ <meta charSet='UTF-8' />
360
+ <meta name='viewport' content='width=device-width, initial-scale=1.0' />
361
+ {import.meta.env.PROD ? (
362
+ <script type='module' src='/static/client.js'></script>
363
+ ) : (
364
+ <script type='module' src='/app/client.ts'></script>
365
+ )}
366
+ {title ? <title>{title}</title> : ''}
367
+ </head>
368
+ <body>{children}</body>
369
+ </html>
370
+ )
371
+ })
372
+ ```
373
+
374
+ The `app/client.ts` will be like this.
375
+
376
+ ```ts
377
+ // app/client.ts
378
+ import { createClient } from 'honox/client'
379
+
380
+ createClient({
381
+ hydrate: async (elem, root) => {
382
+ const { hydrateRoot } = await import('react-dom/client')
383
+ hydrateRoot(root, elem)
384
+ },
385
+ createElement: async (type: any, props: any, ...children: any[]) => {
386
+ const { createElement } = await import('react')
387
+ return createElement(type, props, ...children)
388
+ },
389
+ })
390
+ ```
391
+
392
+ ## Guides
393
+
394
+ ### Nested Layouts
395
+
396
+ If you are using the JSX Renderer middleware, you can nest layouts using ` <Layout />`.
397
+
398
+ ```tsx
399
+ // app/routes/posts/_renderer.tsx
400
+
401
+ import { jsxRenderer } from 'hono/jsx-renderer'
402
+
403
+ export default jsxRenderer(({ children, Layout }) => {
404
+ return (
405
+ <Layout>
406
+ <nav>Posts Menu</nav>
407
+ <div>{children}</div>
408
+ </Layout>
409
+ )
410
+ })
411
+ ```
412
+
413
+ ### Using Middleware
414
+
415
+ You can use Hono's Middleware in each root file with the same syntax as Hono. For example, to validate a value with the [Zod Validator](https://github.com/honojs/middleware/tree/main/packages/zod-validator), do the following:
416
+
417
+ ```tsx
418
+ import { z } from 'zod'
419
+ import { zValidator } from '@hono/zod-validator'
420
+
421
+ const schema = z.object({
422
+ name: z.string().max(10),
423
+ })
424
+
425
+ export const POST = createRoute(zValidator('form', schema), async (c) => {
426
+ const { name } = c.req.valid('form')
427
+ setCookie(c, 'name', name)
428
+ return c.redirect('/')
429
+ })
430
+ ```
431
+
432
+ ### Using Tailwind CSS
433
+
434
+ Given that HonoX is Vite-centric, if you wish to utilize [Tailwind CSS](https://tailwindcss.com/), simply adhere to the official instructions.
435
+
436
+ Prepare `tailwind.config.js` and `postcss.config.js`:
437
+
438
+ ```js
439
+ // tailwind.config.js
440
+ export default {
441
+ content: ['./app/**/*.tsx'],
442
+ theme: {
443
+ extend: {},
444
+ },
445
+ plugins: [],
446
+ }
447
+ ```
448
+
449
+ ```js
450
+ // postcss.config.js
451
+ export default {
452
+ plugins: {
453
+ tailwindcss: {},
454
+ autoprefixer: {},
455
+ },
456
+ }
457
+ ```
458
+
459
+ Write `app/style.css`:
460
+
461
+ ```css
462
+ @tailwind base;
463
+ @tailwind components;
464
+ @tailwind utilities;
465
+ ```
466
+
467
+ Finally, import it in a renderer file:
468
+
469
+ ```tsx
470
+ // app/routes/_renderer.tsx
471
+ import { jsxRenderer } from 'hono/jsx-renderer'
472
+
473
+ export default jsxRenderer(({ children }) => {
474
+ return (
475
+ <html lang='en'>
476
+ <head>
477
+ <meta charset='UTF-8' />
478
+ <meta name='viewport' content='width=device-width, initial-scale=1.0' />
479
+ {import.meta.env.PROD ? (
480
+ <link href='static/assets/style.css' rel='stylesheet' />
481
+ ) : (
482
+ <link href='/app/style.css' rel='stylesheet' />
483
+ )}
484
+ </head>
485
+ <body>{children}</body>
486
+ </html>
487
+ )
488
+ })
489
+ ```
490
+
491
+ ### MDX
492
+
493
+ MDX can also be used. Here is the `vite.config.ts`.
494
+
495
+ ```ts
496
+ import devServer from '@hono/vite-dev-server'
497
+ import mdx from '@mdx-js/rollup'
498
+ import honox from 'honox/vite'
499
+ import remarkFrontmatter from 'remark-frontmatter'
500
+ import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
501
+ import { defineConfig } from '../../node_modules/vite'
502
+
503
+ const entry = './app/server.ts'
504
+
505
+ export default defineConfig(() => {
506
+ return {
507
+ plugins: [
508
+ honox(),
509
+ devServer({ entry }),
510
+ mdx({
511
+ jsxImportSource: 'hono/jsx',
512
+ remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
513
+ }),
514
+ ],
515
+ }
516
+ })
517
+ ```
518
+
519
+ Blog site can be created.
520
+
521
+ ```tsx
522
+ // app/routes/index.tsx
523
+ import type { Meta } from '../types'
524
+
525
+ export default function Top() {
526
+ const posts = import.meta.glob<{ frontmatter: Meta }>('./posts/*.mdx', {
527
+ eager: true,
528
+ })
529
+ return (
530
+ <div>
531
+ <h2>Posts</h2>
532
+ <ul class='article-list'>
533
+ {Object.entries(posts).map(([id, module]) => {
534
+ if (module.frontmatter) {
535
+ return (
536
+ <li>
537
+ <a href={`${id.replace(/\.mdx$/, '')}`}>{module.frontmatter.title}</a>
538
+ </li>
539
+ )
540
+ }
541
+ })}
542
+ </ul>
543
+ </div>
544
+ )
545
+ }
546
+ ```
547
+
548
+ ## Deployment
549
+
550
+ Since a HonoX instance is essentially a Hono instance, it can be deployed on any platform that Hono supports.
551
+
552
+ ### Cloudflare Pages
553
+
554
+ Setup the `vite.config.ts`:
555
+
556
+ ```ts
557
+ import { defineConfig } from 'vite'
558
+ import honox from 'honox/vite'
559
+ import pages from '@hono/vite-cloudflare-pages'
560
+
561
+ export default defineConfig({
562
+ plugins: [honox(), pages()],
563
+ })
564
+ ```
565
+
566
+ If you want to include client side scripts and assets:
567
+
568
+ ```ts
569
+ // vite.config.ts
570
+ import { defineConfig } from 'vite'
571
+ import honox from 'honox/vite'
572
+ import pages from '@hono/vite-cloudflare-pages'
573
+
574
+ export default defineConfig(({ mode }) => {
575
+ if (mode === 'client') {
576
+ return {
577
+ build: {
578
+ rollupOptions: {
579
+ input: ['./app/client.ts'],
580
+ output: {
581
+ entryFileNames: 'static/client.js',
582
+ chunkFileNames: 'static/assets/[name]-[hash].js',
583
+ assetFileNames: 'static/assets/[name].[ext]',
584
+ },
585
+ },
586
+ emptyOutDir: false,
587
+ copyPublicDir: false,
588
+ },
589
+ }
590
+ } else {
591
+ return {
592
+ plugins: [honox(), pages()],
593
+ }
594
+ }
595
+ })
596
+ ```
597
+
598
+ Build command (including a client):
599
+
600
+ ```txt
601
+ vite build && vite build --mode client
602
+ ```
603
+
604
+ Deploy with the following commands after build. Ensure you have [Wrangler](https://developers.cloudflare.com/workers/wrangler/) installed:
605
+
606
+ ```txt
607
+ wrangler pages deploy ./dist
608
+ ```
609
+
610
+ ### SSG - Static Site Generation
611
+
612
+ Using Hono's SSG feature, you can generate static HTML for each route.
613
+
614
+ ```ts
615
+ import { defineConfig } from 'vite'
616
+ import honox from 'honox/vite'
617
+ import ssg from '@hono/vite-ssg'
618
+
619
+ const entry = './app/server.ts'
620
+
621
+ export default defineConfig(() => {
622
+ return {
623
+ plugins: [honox(), devServer({ entry }), ssg({ entry })],
624
+ }
625
+ })
626
+ ```
627
+
628
+ You can also deploy it to Cloudflare Pages.
629
+
630
+ ```txt
631
+ wrangler pages deploy ./dist
632
+ ```
633
+
634
+ ## Examples
635
+
636
+ - [/example](./examples/)
637
+ - https://github.com/yusukebe/honox-examples
638
+
639
+ ## Related projects
640
+
641
+ - [Hono](https://hono.dev/)
642
+ - [Vite](https://vitejs.dev/)
643
+
644
+ ## Authors
645
+
646
+ - Yusuke Wada <https://github.com/yusukebe>
647
+
648
+ ## License
649
+
650
+ MIT
@@ -0,0 +1,11 @@
1
+ import { Hydrate, CreateElement } from '../types.js';
2
+
3
+ type ClientOptions = {
4
+ hydrate?: Hydrate;
5
+ createElement?: CreateElement;
6
+ ISLAND_FILES?: Record<string, () => Promise<unknown>>;
7
+ island_root?: string;
8
+ };
9
+ declare const createClient: (options?: ClientOptions) => Promise<void>;
10
+
11
+ export { type ClientOptions, createClient };
@@ -0,0 +1,32 @@
1
+ import { jsx as jsxFn } from "hono/jsx";
2
+ import { render } from "hono/jsx/dom";
3
+ import { COMPONENT_NAME, DATA_SERIALIZED_PROPS } from "../constants.js";
4
+ const createClient = async (options) => {
5
+ const FILES = options?.ISLAND_FILES ?? import.meta.glob("/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)");
6
+ const root = options?.island_root ?? "/app/islands/";
7
+ const hydrateComponent = async () => {
8
+ const filePromises = Object.keys(FILES).map(async (filePath) => {
9
+ const componentName = filePath.replace(root, "");
10
+ const elements = document.querySelectorAll(`[${COMPONENT_NAME}="${componentName}"]`);
11
+ if (elements) {
12
+ const elementPromises = Array.from(elements).map(async (element) => {
13
+ const fileCallback = FILES[filePath];
14
+ const file = await fileCallback();
15
+ const Component = await file.default;
16
+ const serializedProps = element.attributes.getNamedItem(DATA_SERIALIZED_PROPS)?.value;
17
+ const props = JSON.parse(serializedProps ?? "{}");
18
+ const hydrate = options?.hydrate ?? render;
19
+ const createElement = options?.createElement ?? jsxFn;
20
+ const newElem = await createElement(Component, props);
21
+ await hydrate(newElem, element);
22
+ });
23
+ await Promise.all(elementPromises);
24
+ }
25
+ });
26
+ await Promise.all(filePromises);
27
+ };
28
+ await hydrateComponent();
29
+ };
30
+ export {
31
+ createClient
32
+ };
@@ -0,0 +1,2 @@
1
+ export { ClientOptions, createClient } from './client.js';
2
+ import '../types.js';