honox 0.1.4 → 0.1.6
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 +64 -4
- package/dist/client/client.d.ts +5 -1
- package/dist/client/client.js +17 -4
- package/dist/client/runtime.d.ts +5 -3
- package/dist/client/runtime.js +57 -3
- package/dist/server/base.d.ts +4 -0
- package/dist/server/base.js +4 -0
- package/dist/server/components/css.d.ts +11 -0
- package/dist/server/components/css.js +38 -0
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +1 -1
- package/dist/server/server.d.ts +12 -8
- package/dist/server/server.js +8 -18
- package/dist/server/with-defaults.d.ts +37 -0
- package/dist/server/with-defaults.js +27 -0
- package/dist/types.d.ts +5 -1
- package/dist/vite/index.d.ts +2 -0
- package/dist/vite/index.js +1 -1
- package/dist/vite/inject-importing-islands.d.ts +1 -1
- package/dist/vite/inject-importing-islands.js +57 -38
- package/dist/vite/island-components.d.ts +6 -2
- package/dist/vite/island-components.js +59 -27
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HonoX
|
|
2
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://
|
|
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://vitejs.dev/), and UI libraries.
|
|
4
4
|
|
|
5
5
|
**Note**: _HonoX is currently in a "alpha stage". Breaking changes are introduced without following semantic versioning._
|
|
6
6
|
|
|
@@ -261,11 +261,12 @@ The below is the project structure of a minimal application including a client s
|
|
|
261
261
|
|
|
262
262
|
### Renderer
|
|
263
263
|
|
|
264
|
-
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
|
|
264
|
+
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.
|
|
265
265
|
|
|
266
266
|
```tsx
|
|
267
267
|
// app/routes/_renderer.tsx
|
|
268
268
|
import { jsxRenderer } from 'hono/jsx-renderer'
|
|
269
|
+
import { HasIslands } from 'honox/server'
|
|
269
270
|
|
|
270
271
|
export default jsxRenderer(({ children }) => {
|
|
271
272
|
return (
|
|
@@ -308,6 +309,8 @@ export default jsxRenderer(({ children }) => {
|
|
|
308
309
|
})
|
|
309
310
|
```
|
|
310
311
|
|
|
312
|
+
**Note**: Since `<HasIslands />` can slightly affect build performance when used, it is recommended that you do not use it in the development environment, but only at build time. `<Script />` does not cause performance degradation during development, so it's better to use it.
|
|
313
|
+
|
|
311
314
|
### Client Entry File
|
|
312
315
|
|
|
313
316
|
A client side entry file should be in `app/client.ts`. Simply, write `createClient()`.
|
|
@@ -321,7 +324,7 @@ createClient()
|
|
|
321
324
|
|
|
322
325
|
### Interactions
|
|
323
326
|
|
|
324
|
-
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:
|
|
327
|
+
Function components placed in `app/islands/*` - Island components - are also sent to the client side. For example, you can write interactive component such as the following counter:
|
|
325
328
|
|
|
326
329
|
```tsx
|
|
327
330
|
// app/islands/counter.tsx
|
|
@@ -355,6 +358,18 @@ export default createRoute((c) => {
|
|
|
355
358
|
})
|
|
356
359
|
```
|
|
357
360
|
|
|
361
|
+
**Note**: You cannot access a Context object in Island components. Therefore, you should pass the value from components outside of Island.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { useRequestContext } from 'hono/jsx-renderer'
|
|
365
|
+
import Counter from '../islands/counter.tsx'
|
|
366
|
+
|
|
367
|
+
export default function Component() {
|
|
368
|
+
const c = useRequestContext()
|
|
369
|
+
return <Counter init={parseInt(c.req.query('count') ?? '0', 10)} />
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
358
373
|
## BYOR - Bring Your Own Renderer
|
|
359
374
|
|
|
360
375
|
You can bring your own renderer using a UI library like React, Preact, Solid, or others.
|
|
@@ -383,7 +398,7 @@ declare module '@hono/react-renderer' {
|
|
|
383
398
|
}
|
|
384
399
|
```
|
|
385
400
|
|
|
386
|
-
The following is an example of `app/routes/
|
|
401
|
+
The following is an example of `app/routes/_renderer.tsx`.
|
|
387
402
|
|
|
388
403
|
```tsx
|
|
389
404
|
// app/routes/_renderer.tsx
|
|
@@ -447,6 +462,51 @@ export default jsxRenderer(({ children, Layout }) => {
|
|
|
447
462
|
})
|
|
448
463
|
```
|
|
449
464
|
|
|
465
|
+
#### Passing Additional Props in Nested Layouts
|
|
466
|
+
|
|
467
|
+
Props passed to nested renderers do not automatically propagate to the parent renderers. To ensure that the parent layouts receive the necessary props, you should explicitly pass them from the nested <Layout /> component. Here's how you can achieve that:
|
|
468
|
+
|
|
469
|
+
Let's start with our route handler:
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
// app/routes/nested/index.tsx
|
|
473
|
+
export default createRoute((c) => {
|
|
474
|
+
return c.render(<div>Content</div>, { title: 'Dashboard' })
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Now, let's take a look at our nested renderer:
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
// app/routes/nested/_renderer.tsx
|
|
482
|
+
export default jsxRenderer(({ children, Layout, title }) => {
|
|
483
|
+
return (
|
|
484
|
+
<Layout title={title}>
|
|
485
|
+
{/* Pass the title prop to the parent renderer */}
|
|
486
|
+
<main>{children}</main>
|
|
487
|
+
</Layout>
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
In this setup, all the props sent to the nested renderer's <Layout /> are consumed by the parent renderer:
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
// app/routes/_renderer.tsx
|
|
496
|
+
export default jsxRenderer(({ children, title }) => {
|
|
497
|
+
return (
|
|
498
|
+
<html lang='en'>
|
|
499
|
+
<head>
|
|
500
|
+
<title>{title}</title> {/* Use the title prop here */}
|
|
501
|
+
</head>
|
|
502
|
+
<body>
|
|
503
|
+
{children} {/* Insert the Layout's children here */}
|
|
504
|
+
</body>
|
|
505
|
+
</html>
|
|
506
|
+
)
|
|
507
|
+
})
|
|
508
|
+
```
|
|
509
|
+
|
|
450
510
|
### Using Middleware
|
|
451
511
|
|
|
452
512
|
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:
|
package/dist/client/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hydrate, CreateElement, CreateChildren } from '../types.js';
|
|
1
|
+
import { Hydrate, CreateElement, CreateChildren, TriggerHydration } from '../types.js';
|
|
2
2
|
|
|
3
3
|
type ClientOptions = {
|
|
4
4
|
hydrate?: Hydrate;
|
|
@@ -7,6 +7,10 @@ type ClientOptions = {
|
|
|
7
7
|
* Create "children" attribute of a component from a list of child nodes
|
|
8
8
|
*/
|
|
9
9
|
createChildren?: CreateChildren;
|
|
10
|
+
/**
|
|
11
|
+
* Trigger hydration on your own
|
|
12
|
+
*/
|
|
13
|
+
triggerHydration?: TriggerHydration;
|
|
10
14
|
ISLAND_FILES?: Record<string, () => Promise<unknown>>;
|
|
11
15
|
island_root?: string;
|
|
12
16
|
};
|
package/dist/client/client.js
CHANGED
|
@@ -4,12 +4,15 @@ import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../co
|
|
|
4
4
|
const createClient = async (options) => {
|
|
5
5
|
const FILES = options?.ISLAND_FILES ?? import.meta.glob("/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)");
|
|
6
6
|
const root = options?.island_root ?? "/app/islands/";
|
|
7
|
-
const hydrateComponent = async () => {
|
|
7
|
+
const hydrateComponent = async (document2) => {
|
|
8
8
|
const filePromises = Object.keys(FILES).map(async (filePath) => {
|
|
9
9
|
const componentName = filePath.replace(root, "");
|
|
10
|
-
const elements =
|
|
10
|
+
const elements = document2.querySelectorAll(
|
|
11
|
+
`[${COMPONENT_NAME}="${componentName}"]:not([data-hono-hydrated])`
|
|
12
|
+
);
|
|
11
13
|
if (elements) {
|
|
12
14
|
const elementPromises = Array.from(elements).map(async (element) => {
|
|
15
|
+
element.setAttribute("data-hono-hydrated", "true");
|
|
13
16
|
const fileCallback = FILES[filePath];
|
|
14
17
|
const file = await fileCallback();
|
|
15
18
|
const Component = await file.default;
|
|
@@ -22,7 +25,10 @@ const createClient = async (options) => {
|
|
|
22
25
|
let createChildren = options?.createChildren;
|
|
23
26
|
if (!createChildren) {
|
|
24
27
|
const { buildCreateChildrenFn } = await import("./runtime");
|
|
25
|
-
createChildren = buildCreateChildrenFn(
|
|
28
|
+
createChildren = buildCreateChildrenFn(
|
|
29
|
+
createElement,
|
|
30
|
+
async (name) => (await FILES[`${root}${name}`]()).default
|
|
31
|
+
);
|
|
26
32
|
}
|
|
27
33
|
props.children = await createChildren(
|
|
28
34
|
maybeTemplate.content.childNodes
|
|
@@ -36,7 +42,14 @@ const createClient = async (options) => {
|
|
|
36
42
|
});
|
|
37
43
|
await Promise.all(filePromises);
|
|
38
44
|
};
|
|
39
|
-
|
|
45
|
+
const triggerHydration = options?.triggerHydration ?? (async (hydrateComponent2) => {
|
|
46
|
+
if (document.querySelector('template[id^="H:"], template[id^="E:"]')) {
|
|
47
|
+
const { hydrateComponentHonoSuspense } = await import("./runtime");
|
|
48
|
+
await hydrateComponentHonoSuspense(hydrateComponent2);
|
|
49
|
+
}
|
|
50
|
+
await hydrateComponent2(document);
|
|
51
|
+
});
|
|
52
|
+
await triggerHydration?.(hydrateComponent);
|
|
40
53
|
};
|
|
41
54
|
export {
|
|
42
55
|
createClient
|
package/dist/client/runtime.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { CreateElement, CreateChildren } from '../types.js';
|
|
1
|
+
import { CreateElement, CreateChildren, HydrateComponent } from '../types.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type ImportComponent = (name: string) => Promise<Function | undefined>;
|
|
4
|
+
declare const buildCreateChildrenFn: (createElement: CreateElement, importComponent: ImportComponent) => CreateChildren;
|
|
5
|
+
declare const hydrateComponentHonoSuspense: (hydrateComponent: HydrateComponent) => Promise<void>;
|
|
4
6
|
|
|
5
|
-
export { buildCreateChildrenFn };
|
|
7
|
+
export { buildCreateChildrenFn, hydrateComponentHonoSuspense };
|
package/dist/client/runtime.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Suspense, use } from "hono/jsx/dom";
|
|
2
|
-
|
|
2
|
+
import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
|
|
3
|
+
const buildCreateChildrenFn = (createElement, importComponent) => {
|
|
4
|
+
const setChildrenFromTemplate = async (props, element) => {
|
|
5
|
+
const maybeTemplate = element.childNodes[element.childNodes.length - 1];
|
|
6
|
+
if (maybeTemplate?.nodeName === "TEMPLATE" && maybeTemplate?.getAttribute(DATA_HONO_TEMPLATE) !== null) {
|
|
7
|
+
props.children = await createChildren(
|
|
8
|
+
maybeTemplate.content.childNodes
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
3
12
|
const createElementFromHTMLElement = async (element) => {
|
|
4
13
|
const props = {
|
|
5
14
|
children: await createChildren(element.childNodes)
|
|
@@ -71,13 +80,58 @@ const buildCreateChildrenFn = (createElement) => {
|
|
|
71
80
|
})
|
|
72
81
|
);
|
|
73
82
|
} else {
|
|
74
|
-
|
|
83
|
+
let component = void 0;
|
|
84
|
+
const componentName = child.getAttribute(COMPONENT_NAME);
|
|
85
|
+
if (componentName) {
|
|
86
|
+
component = await importComponent(componentName);
|
|
87
|
+
}
|
|
88
|
+
if (component) {
|
|
89
|
+
const props = JSON.parse(child.getAttribute(DATA_SERIALIZED_PROPS) || "{}");
|
|
90
|
+
await setChildrenFromTemplate(props, child);
|
|
91
|
+
children.push(await createElement(component, props));
|
|
92
|
+
} else {
|
|
93
|
+
children.push(await createElementFromHTMLElement(child));
|
|
94
|
+
}
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
return children;
|
|
78
98
|
};
|
|
79
99
|
return createChildren;
|
|
80
100
|
};
|
|
101
|
+
const hydrateComponentHonoSuspense = async (hydrateComponent) => {
|
|
102
|
+
const templates = /* @__PURE__ */ new Set();
|
|
103
|
+
const observerTargets = /* @__PURE__ */ new Set();
|
|
104
|
+
document.querySelectorAll('template[id^="H:"], template[id^="E:"]').forEach((template) => {
|
|
105
|
+
if (template.parentElement) {
|
|
106
|
+
templates.add(template);
|
|
107
|
+
observerTargets.add(template.parentElement);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (observerTargets.size === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const observer = new MutationObserver((mutations) => {
|
|
114
|
+
const targets = /* @__PURE__ */ new Set();
|
|
115
|
+
mutations.forEach((mutation) => {
|
|
116
|
+
if (mutation.target instanceof Element) {
|
|
117
|
+
targets.add(mutation.target);
|
|
118
|
+
mutation.removedNodes.forEach((node) => {
|
|
119
|
+
templates.delete(node);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
targets.forEach((target) => {
|
|
124
|
+
hydrateComponent(target);
|
|
125
|
+
});
|
|
126
|
+
if (templates.size === 0) {
|
|
127
|
+
observer.disconnect();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
observerTargets.forEach((target) => {
|
|
131
|
+
observer.observe(target, { childList: true });
|
|
132
|
+
});
|
|
133
|
+
};
|
|
81
134
|
export {
|
|
82
|
-
buildCreateChildrenFn
|
|
135
|
+
buildCreateChildrenFn,
|
|
136
|
+
hydrateComponentHonoSuspense
|
|
83
137
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fragment, jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
const Css = async (options) => {
|
|
3
|
+
const src = options.src;
|
|
4
|
+
if (options.prod ?? import.meta.env.PROD) {
|
|
5
|
+
let manifest = options.manifest;
|
|
6
|
+
if (!manifest) {
|
|
7
|
+
const MANIFEST = import.meta.glob("/dist/.vite/manifest.json", {
|
|
8
|
+
eager: true
|
|
9
|
+
});
|
|
10
|
+
for (const [, manifestFile] of Object.entries(MANIFEST)) {
|
|
11
|
+
if (manifestFile["default"]) {
|
|
12
|
+
manifest = manifestFile["default"];
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (manifest) {
|
|
18
|
+
const scriptInManifest = manifest[src.replace(/^\//, "")];
|
|
19
|
+
if (scriptInManifest) {
|
|
20
|
+
const elements = [];
|
|
21
|
+
if (scriptInManifest.css) {
|
|
22
|
+
for (const css of scriptInManifest.css) {
|
|
23
|
+
elements.push(/* @__PURE__ */ jsx("link", { href: css, rel: "stylesheet" }));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return /* @__PURE__ */ jsx(Fragment, { children: elements.map((element) => {
|
|
27
|
+
return /* @__PURE__ */ jsx(Fragment, { children: element });
|
|
28
|
+
}) });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return /* @__PURE__ */ jsx(Fragment, {});
|
|
32
|
+
} else {
|
|
33
|
+
return /* @__PURE__ */ jsx("link", { href: src, rel: "stylesheet" });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
Css
|
|
38
|
+
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createApp } from './with-defaults.js';
|
|
2
|
+
export { ServerOptions } from './server.js';
|
|
2
3
|
export { HasIslands } from './components/has-islands.js';
|
|
3
4
|
export { Script } from './components/script.js';
|
|
4
|
-
import 'hono/types';
|
|
5
5
|
import 'hono';
|
|
6
|
+
import 'hono/types';
|
|
6
7
|
import '../constants.js';
|
|
7
8
|
import 'hono/jsx';
|
|
8
9
|
import 'vite';
|
package/dist/server/index.js
CHANGED
package/dist/server/server.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { Env, Hono, MiddlewareHandler, NotFoundHandler, ErrorHandler } from 'hon
|
|
|
4
4
|
import { IMPORTING_ISLANDS_ID } from '../constants.js';
|
|
5
5
|
|
|
6
6
|
declare const METHODS: readonly ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
|
|
7
|
+
type AppFile = {
|
|
8
|
+
default: Hono;
|
|
9
|
+
};
|
|
7
10
|
type InnerMeta = {
|
|
8
11
|
[key in typeof IMPORTING_ISLANDS_ID]?: boolean;
|
|
9
12
|
};
|
|
@@ -25,16 +28,17 @@ type MiddlewareFile = {
|
|
|
25
28
|
default: MiddlewareHandler[];
|
|
26
29
|
};
|
|
27
30
|
type InitFunction<E extends Env = Env> = (app: Hono<E>) => void;
|
|
28
|
-
type
|
|
29
|
-
ROUTES
|
|
30
|
-
RENDERER
|
|
31
|
-
NOT_FOUND
|
|
32
|
-
ERROR
|
|
33
|
-
MIDDLEWARE
|
|
34
|
-
root
|
|
31
|
+
type BaseServerOptions<E extends Env = Env> = {
|
|
32
|
+
ROUTES: Record<string, RouteFile | AppFile>;
|
|
33
|
+
RENDERER: Record<string, RendererFile>;
|
|
34
|
+
NOT_FOUND: Record<string, NotFoundFile>;
|
|
35
|
+
ERROR: Record<string, ErrorFile>;
|
|
36
|
+
MIDDLEWARE: Record<string, MiddlewareFile>;
|
|
37
|
+
root: string;
|
|
35
38
|
app?: Hono<E>;
|
|
36
39
|
init?: InitFunction<E>;
|
|
37
40
|
};
|
|
38
|
-
|
|
41
|
+
type ServerOptions<E extends Env = Env> = Partial<BaseServerOptions<E>>;
|
|
42
|
+
declare const createApp: <E extends Env>(options: BaseServerOptions<E>) => Hono<E, hono_types.BlankSchema, "/">;
|
|
39
43
|
|
|
40
44
|
export { type ServerOptions, createApp };
|
package/dist/server/server.js
CHANGED
|
@@ -11,31 +11,21 @@ const NOTFOUND_FILENAME = "_404.tsx";
|
|
|
11
11
|
const ERROR_FILENAME = "_error.tsx";
|
|
12
12
|
const METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
|
|
13
13
|
const createApp = (options) => {
|
|
14
|
-
const root = options
|
|
14
|
+
const root = options.root;
|
|
15
15
|
const rootRegExp = new RegExp(`^${root}`);
|
|
16
|
-
const app = options
|
|
17
|
-
if (options
|
|
16
|
+
const app = options.app ?? new Hono();
|
|
17
|
+
if (options.init) {
|
|
18
18
|
options.init(app);
|
|
19
19
|
}
|
|
20
|
-
const NOT_FOUND_FILE = options
|
|
21
|
-
eager: true
|
|
22
|
-
});
|
|
20
|
+
const NOT_FOUND_FILE = options.NOT_FOUND;
|
|
23
21
|
const notFoundMap = groupByDirectory(NOT_FOUND_FILE);
|
|
24
|
-
const ERROR_FILE = options
|
|
25
|
-
eager: true
|
|
26
|
-
});
|
|
22
|
+
const ERROR_FILE = options.ERROR;
|
|
27
23
|
const errorMap = groupByDirectory(ERROR_FILE);
|
|
28
|
-
const RENDERER_FILE = options
|
|
29
|
-
eager: true
|
|
30
|
-
});
|
|
24
|
+
const RENDERER_FILE = options.RENDERER;
|
|
31
25
|
const rendererList = listByDirectory(RENDERER_FILE);
|
|
32
|
-
const MIDDLEWARE_FILE = options
|
|
33
|
-
eager: true
|
|
34
|
-
});
|
|
26
|
+
const MIDDLEWARE_FILE = options.MIDDLEWARE;
|
|
35
27
|
const middlewareList = listByDirectory(MIDDLEWARE_FILE);
|
|
36
|
-
const ROUTES_FILE = options
|
|
37
|
-
eager: true
|
|
38
|
-
});
|
|
28
|
+
const ROUTES_FILE = options.ROUTES;
|
|
39
29
|
const routesMap = sortDirectoriesByDepth(groupByDirectory(ROUTES_FILE));
|
|
40
30
|
const getPaths = (currentDirectory, fileList) => {
|
|
41
31
|
let paths = fileList[currentDirectory] ?? [];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as hono from 'hono';
|
|
2
|
+
import { Env } from 'hono';
|
|
3
|
+
import * as hono_types from 'hono/types';
|
|
4
|
+
|
|
5
|
+
declare const createApp: <E extends Env>(options?: Partial<{
|
|
6
|
+
ROUTES: Record<string, ({
|
|
7
|
+
default?: Function | undefined;
|
|
8
|
+
} & {
|
|
9
|
+
GET?: hono_types.H[] | undefined;
|
|
10
|
+
POST?: hono_types.H[] | undefined;
|
|
11
|
+
PUT?: hono_types.H[] | undefined;
|
|
12
|
+
DELETE?: hono_types.H[] | undefined;
|
|
13
|
+
OPTIONS?: hono_types.H[] | undefined;
|
|
14
|
+
PATCH?: hono_types.H[] | undefined;
|
|
15
|
+
} & {
|
|
16
|
+
__importing_islands?: boolean | undefined;
|
|
17
|
+
}) | {
|
|
18
|
+
default: hono.Hono<Env, hono_types.BlankSchema, "/">;
|
|
19
|
+
}>;
|
|
20
|
+
RENDERER: Record<string, {
|
|
21
|
+
default: hono.MiddlewareHandler;
|
|
22
|
+
}>;
|
|
23
|
+
NOT_FOUND: Record<string, {
|
|
24
|
+
default: hono.NotFoundHandler;
|
|
25
|
+
}>;
|
|
26
|
+
ERROR: Record<string, {
|
|
27
|
+
default: hono.ErrorHandler;
|
|
28
|
+
}>;
|
|
29
|
+
MIDDLEWARE: Record<string, {
|
|
30
|
+
default: hono.MiddlewareHandler[];
|
|
31
|
+
}>;
|
|
32
|
+
root: string;
|
|
33
|
+
app?: hono.Hono<E, hono_types.BlankSchema, "/"> | undefined;
|
|
34
|
+
init?: ((app: hono.Hono<E, hono_types.BlankSchema, "/">) => void) | undefined;
|
|
35
|
+
}> | undefined) => hono.Hono<E, hono_types.BlankSchema, "/">;
|
|
36
|
+
|
|
37
|
+
export { createApp };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createApp as baseCreateApp } from "./server.js";
|
|
2
|
+
const createApp = (options) => {
|
|
3
|
+
const newOptions = {
|
|
4
|
+
root: options?.root ?? "/app/routes",
|
|
5
|
+
app: options?.app,
|
|
6
|
+
init: options?.init,
|
|
7
|
+
NOT_FOUND: options?.NOT_FOUND ?? import.meta.glob("/app/routes/**/_404.(ts|tsx)", {
|
|
8
|
+
eager: true
|
|
9
|
+
}),
|
|
10
|
+
ERROR: options?.ERROR ?? import.meta.glob("/app/routes/**/_error.(ts|tsx)", {
|
|
11
|
+
eager: true
|
|
12
|
+
}),
|
|
13
|
+
RENDERER: options?.RENDERER ?? import.meta.glob("/app/routes/**/_renderer.tsx", {
|
|
14
|
+
eager: true
|
|
15
|
+
}),
|
|
16
|
+
MIDDLEWARE: options?.MIDDLEWARE ?? import.meta.glob("/app/routes/**/_middleware.(ts|tsx)", {
|
|
17
|
+
eager: true
|
|
18
|
+
}),
|
|
19
|
+
ROUTES: options?.ROUTES ?? import.meta.glob("/app/routes/**/[!_]*.(ts|tsx|mdx)", {
|
|
20
|
+
eager: true
|
|
21
|
+
})
|
|
22
|
+
};
|
|
23
|
+
return baseCreateApp(newOptions);
|
|
24
|
+
};
|
|
25
|
+
export {
|
|
26
|
+
createApp
|
|
27
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -2,5 +2,9 @@
|
|
|
2
2
|
type CreateElement = (type: any, props: any) => Node | Promise<Node>;
|
|
3
3
|
type Hydrate = (children: Node, parent: Element) => void | Promise<void>;
|
|
4
4
|
type CreateChildren = (childNodes: NodeListOf<ChildNode>) => Node[] | Promise<Node[]>;
|
|
5
|
+
type HydrateComponent = (doc: {
|
|
6
|
+
querySelectorAll: typeof document.querySelectorAll;
|
|
7
|
+
}) => Promise<void>;
|
|
8
|
+
type TriggerHydration = (trigger: HydrateComponent) => void;
|
|
5
9
|
|
|
6
|
-
export type { CreateChildren, CreateElement, Hydrate };
|
|
10
|
+
export type { CreateChildren, CreateElement, Hydrate, HydrateComponent, TriggerHydration };
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { DevServerOptions } from '@hono/vite-dev-server';
|
|
2
2
|
export { defaultOptions as devServerDefaultOptions } from '@hono/vite-dev-server';
|
|
3
3
|
import { PluginOption } from 'vite';
|
|
4
|
+
import { IslandComponentsOptions } from './island-components.js';
|
|
4
5
|
export { islandComponents } from './island-components.js';
|
|
5
6
|
|
|
6
7
|
type Options = {
|
|
7
8
|
islands?: boolean;
|
|
8
9
|
entry?: string;
|
|
9
10
|
devServer?: DevServerOptions;
|
|
11
|
+
islandComponents?: IslandComponentsOptions;
|
|
10
12
|
external?: string[];
|
|
11
13
|
};
|
|
12
14
|
declare const defaultOptions: Options;
|
package/dist/vite/index.js
CHANGED
|
@@ -23,7 +23,7 @@ function honox(options) {
|
|
|
23
23
|
})
|
|
24
24
|
);
|
|
25
25
|
if (options?.islands !== false) {
|
|
26
|
-
plugins.push(islandComponents());
|
|
26
|
+
plugins.push(islandComponents(options?.islandComponents));
|
|
27
27
|
}
|
|
28
28
|
plugins.push(injectImportingIslands());
|
|
29
29
|
plugins.push(restartOnAddUnlink());
|
|
@@ -1,49 +1,68 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
1
3
|
import _generate from "@babel/generator";
|
|
2
4
|
import { parse } from "@babel/parser";
|
|
3
|
-
import
|
|
5
|
+
import precinct from "precinct";
|
|
6
|
+
import { normalizePath } from "vite";
|
|
4
7
|
import { IMPORTING_ISLANDS_ID } from "../constants.js";
|
|
5
|
-
const traverse = _traverse.default ?? _traverse;
|
|
6
8
|
const generate = _generate.default ?? _generate;
|
|
7
|
-
function injectImportingIslands() {
|
|
9
|
+
async function injectImportingIslands() {
|
|
10
|
+
const isIslandRegex = new RegExp(/\/islands\//);
|
|
11
|
+
const routesRegex = new RegExp(/routes\/.*\.[t|j]sx$/);
|
|
12
|
+
const cache = {};
|
|
13
|
+
const walkDependencyTree = async (baseFile, dependencyFile) => {
|
|
14
|
+
const depPath = dependencyFile ? path.join(path.dirname(baseFile), dependencyFile) + ".tsx" : baseFile;
|
|
15
|
+
const deps = [depPath];
|
|
16
|
+
try {
|
|
17
|
+
if (!cache[depPath]) {
|
|
18
|
+
cache[depPath] = (await readFile(depPath, { flag: "" })).toString();
|
|
19
|
+
}
|
|
20
|
+
const currentFileDeps = precinct(cache[depPath], {
|
|
21
|
+
type: "tsx"
|
|
22
|
+
});
|
|
23
|
+
const childDeps = await Promise.all(
|
|
24
|
+
currentFileDeps.map(async (x) => await walkDependencyTree(depPath, x))
|
|
25
|
+
);
|
|
26
|
+
deps.push(...childDeps.flat());
|
|
27
|
+
return deps;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return deps;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
8
32
|
return {
|
|
9
33
|
name: "inject-importing-islands",
|
|
10
|
-
transform(
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
type: "
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type: "VariableDeclarator",
|
|
32
|
-
id: { type: "Identifier", name: IMPORTING_ISLANDS_ID },
|
|
33
|
-
init: { type: "BooleanLiteral", value: true }
|
|
34
|
-
}
|
|
35
|
-
],
|
|
36
|
-
kind: "const"
|
|
34
|
+
async transform(sourceCode, id) {
|
|
35
|
+
if (!routesRegex.test(id)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const hasIslandsImport = (await walkDependencyTree(id)).flat().some((x) => isIslandRegex.test(normalizePath(x)));
|
|
39
|
+
if (!hasIslandsImport) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const ast = parse(sourceCode, {
|
|
43
|
+
sourceType: "module",
|
|
44
|
+
plugins: ["jsx", "typescript"]
|
|
45
|
+
});
|
|
46
|
+
const hasIslandsNode = {
|
|
47
|
+
type: "ExportNamedDeclaration",
|
|
48
|
+
declaration: {
|
|
49
|
+
type: "VariableDeclaration",
|
|
50
|
+
declarations: [
|
|
51
|
+
{
|
|
52
|
+
type: "VariableDeclarator",
|
|
53
|
+
id: { type: "Identifier", name: IMPORTING_ISLANDS_ID },
|
|
54
|
+
init: { type: "BooleanLiteral", value: true }
|
|
37
55
|
}
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
],
|
|
57
|
+
kind: "const"
|
|
40
58
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
};
|
|
60
|
+
ast.program.body.push(hasIslandsNode);
|
|
61
|
+
const output = generate(ast, {}, sourceCode);
|
|
62
|
+
return {
|
|
63
|
+
code: output.code,
|
|
64
|
+
map: output.map
|
|
65
|
+
};
|
|
47
66
|
}
|
|
48
67
|
};
|
|
49
68
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
3
|
declare const transformJsxTags: (contents: string, componentName: string) => string | undefined;
|
|
4
|
-
|
|
4
|
+
type IsIsland = (id: string) => boolean;
|
|
5
|
+
type IslandComponentsOptions = {
|
|
6
|
+
isIsland: IsIsland;
|
|
7
|
+
};
|
|
8
|
+
declare function islandComponents(options?: IslandComponentsOptions): Plugin;
|
|
5
9
|
|
|
6
|
-
export { islandComponents, transformJsxTags };
|
|
10
|
+
export { type IslandComponentsOptions, islandComponents, transformJsxTags };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import _generate from "@babel/generator";
|
|
3
4
|
const generate = _generate.default ?? _generate;
|
|
4
5
|
import { parse } from "@babel/parser";
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
memberExpression
|
|
26
27
|
} from "@babel/types";
|
|
27
28
|
import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
|
|
28
|
-
function addSSRCheck(funcName, componentName
|
|
29
|
+
function addSSRCheck(funcName, componentName) {
|
|
29
30
|
const isSSR = memberExpression(
|
|
30
31
|
memberExpression(identifier("import"), identifier("meta")),
|
|
31
32
|
identifier("env.SSR")
|
|
@@ -86,11 +87,7 @@ function addSSRCheck(funcName, componentName, isAsync = false) {
|
|
|
86
87
|
[]
|
|
87
88
|
);
|
|
88
89
|
const returnStmt = returnStatement(conditionalExpression(isSSR, ssrElement, clientElement));
|
|
89
|
-
|
|
90
|
-
if (isAsync) {
|
|
91
|
-
functionExpr.async = true;
|
|
92
|
-
}
|
|
93
|
-
return functionExpr;
|
|
90
|
+
return functionExpression(null, [identifier("props")], blockStatement([returnStmt]));
|
|
94
91
|
}
|
|
95
92
|
const transformJsxTags = (contents, componentName) => {
|
|
96
93
|
const ast = parse(contents, {
|
|
@@ -98,43 +95,78 @@ const transformJsxTags = (contents, componentName) => {
|
|
|
98
95
|
plugins: ["typescript", "jsx"]
|
|
99
96
|
});
|
|
100
97
|
if (ast) {
|
|
98
|
+
let wrappedFunctionId;
|
|
101
99
|
traverse(ast, {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return;
|
|
100
|
+
ExportNamedDeclaration(path2) {
|
|
101
|
+
for (const specifier of path2.node.specifiers) {
|
|
102
|
+
if (specifier.type !== "ExportSpecifier") {
|
|
103
|
+
continue;
|
|
107
104
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
null,
|
|
112
|
-
path.node.declaration.params,
|
|
113
|
-
path.node.declaration.body
|
|
114
|
-
);
|
|
115
|
-
if (isAsync) {
|
|
116
|
-
originalFunction.async = true;
|
|
105
|
+
const exportAs = specifier.exported.type === "StringLiteral" ? specifier.exported.value : specifier.exported.name;
|
|
106
|
+
if (exportAs !== "default") {
|
|
107
|
+
continue;
|
|
117
108
|
}
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
const wrappedFunction = addSSRCheck(specifier.local.name, componentName);
|
|
110
|
+
const wrappedFunctionId2 = identifier("Wrapped" + specifier.local.name);
|
|
111
|
+
path2.insertBefore(
|
|
112
|
+
variableDeclaration("const", [variableDeclarator(wrappedFunctionId2, wrappedFunction)])
|
|
120
113
|
);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
specifier.local.name = wrappedFunctionId2.name;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
ExportDefaultDeclaration(path2) {
|
|
118
|
+
const declarationType = path2.node.declaration.type;
|
|
119
|
+
if (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression" || declarationType === "ArrowFunctionExpression" || declarationType === "Identifier") {
|
|
120
|
+
const functionName = (declarationType === "Identifier" ? path2.node.declaration.name : (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression") && path2.node.declaration.id?.name) || "__HonoIsladComponent__";
|
|
121
|
+
let originalFunctionId;
|
|
122
|
+
if (declarationType === "Identifier") {
|
|
123
|
+
originalFunctionId = path2.node.declaration;
|
|
124
|
+
} else {
|
|
125
|
+
originalFunctionId = identifier(functionName + "Original");
|
|
126
|
+
const originalFunction = path2.node.declaration.type === "FunctionExpression" || path2.node.declaration.type === "ArrowFunctionExpression" ? path2.node.declaration : functionExpression(
|
|
127
|
+
null,
|
|
128
|
+
path2.node.declaration.params,
|
|
129
|
+
path2.node.declaration.body,
|
|
130
|
+
void 0,
|
|
131
|
+
path2.node.declaration.async
|
|
132
|
+
);
|
|
133
|
+
path2.insertBefore(
|
|
134
|
+
variableDeclaration("const", [
|
|
135
|
+
variableDeclarator(originalFunctionId, originalFunction)
|
|
136
|
+
])
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName);
|
|
140
|
+
wrappedFunctionId = identifier("Wrapped" + functionName);
|
|
141
|
+
path2.replaceWith(
|
|
124
142
|
variableDeclaration("const", [variableDeclarator(wrappedFunctionId, wrappedFunction)])
|
|
125
143
|
);
|
|
126
|
-
path.insertAfter(exportDefaultDeclaration(wrappedFunctionId));
|
|
127
144
|
}
|
|
128
145
|
}
|
|
129
146
|
});
|
|
147
|
+
if (wrappedFunctionId) {
|
|
148
|
+
ast.program.body.push(exportDefaultDeclaration(wrappedFunctionId));
|
|
149
|
+
}
|
|
130
150
|
const { code } = generate(ast);
|
|
131
151
|
return code;
|
|
132
152
|
}
|
|
133
153
|
};
|
|
134
|
-
function islandComponents() {
|
|
154
|
+
function islandComponents(options) {
|
|
155
|
+
let root = "";
|
|
135
156
|
return {
|
|
136
157
|
name: "transform-island-components",
|
|
158
|
+
configResolved: (config) => {
|
|
159
|
+
root = config.root;
|
|
160
|
+
},
|
|
137
161
|
async load(id) {
|
|
162
|
+
const defaultIsIsland = (id2) => {
|
|
163
|
+
const islandDirectoryPath = path.join(root, "app/islands");
|
|
164
|
+
return id2.startsWith(islandDirectoryPath);
|
|
165
|
+
};
|
|
166
|
+
const matchIslandPath = options?.isIsland ?? defaultIsIsland;
|
|
167
|
+
if (!matchIslandPath(id)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
138
170
|
const match = id.match(/\/islands\/(.+?\.tsx)$/);
|
|
139
171
|
if (match) {
|
|
140
172
|
const componentName = match[1];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/server/index.d.ts",
|
|
42
42
|
"import": "./dist/server/index.js"
|
|
43
43
|
},
|
|
44
|
+
"./server/base": {
|
|
45
|
+
"types": "./dist/server/base.d.ts",
|
|
46
|
+
"import": "./dist/server/base.js"
|
|
47
|
+
},
|
|
44
48
|
"./client": {
|
|
45
49
|
"types": "./dist/client/index.d.ts",
|
|
46
50
|
"import": "./dist/client/index.js"
|
|
@@ -69,6 +73,9 @@
|
|
|
69
73
|
"server": [
|
|
70
74
|
"./dist/server"
|
|
71
75
|
],
|
|
76
|
+
"server/base": [
|
|
77
|
+
"./dist/server/base"
|
|
78
|
+
],
|
|
72
79
|
"client": [
|
|
73
80
|
"./dist/client"
|
|
74
81
|
],
|
|
@@ -99,7 +106,8 @@
|
|
|
99
106
|
"@babel/parser": "^7.23.6",
|
|
100
107
|
"@babel/traverse": "^7.23.6",
|
|
101
108
|
"@babel/types": "^7.23.6",
|
|
102
|
-
"@hono/vite-dev-server": "^0.
|
|
109
|
+
"@hono/vite-dev-server": "^0.8.1",
|
|
110
|
+
"precinct": "^11.0.5"
|
|
103
111
|
},
|
|
104
112
|
"peerDependencies": {
|
|
105
113
|
"hono": ">=4.*"
|
|
@@ -107,7 +115,7 @@
|
|
|
107
115
|
"devDependencies": {
|
|
108
116
|
"@hono/eslint-config": "^0.0.4",
|
|
109
117
|
"@mdx-js/rollup": "^3.0.0",
|
|
110
|
-
"@playwright/test": "^1.
|
|
118
|
+
"@playwright/test": "^1.42.0",
|
|
111
119
|
"@types/babel__generator": "^7",
|
|
112
120
|
"@types/babel__traverse": "^7",
|
|
113
121
|
"@types/node": "^20.10.5",
|