honox 0.1.5 → 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 +63 -3
- package/dist/server/components/css.d.ts +11 -0
- package/dist/server/components/css.js +38 -0
- 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 +27 -14
- package/package.json +4 -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.
|
|
@@ -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:
|
|
@@ -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/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";
|
|
@@ -96,8 +97,8 @@ const transformJsxTags = (contents, componentName) => {
|
|
|
96
97
|
if (ast) {
|
|
97
98
|
let wrappedFunctionId;
|
|
98
99
|
traverse(ast, {
|
|
99
|
-
ExportNamedDeclaration(
|
|
100
|
-
for (const specifier of
|
|
100
|
+
ExportNamedDeclaration(path2) {
|
|
101
|
+
for (const specifier of path2.node.specifiers) {
|
|
101
102
|
if (specifier.type !== "ExportSpecifier") {
|
|
102
103
|
continue;
|
|
103
104
|
}
|
|
@@ -107,29 +108,29 @@ const transformJsxTags = (contents, componentName) => {
|
|
|
107
108
|
}
|
|
108
109
|
const wrappedFunction = addSSRCheck(specifier.local.name, componentName);
|
|
109
110
|
const wrappedFunctionId2 = identifier("Wrapped" + specifier.local.name);
|
|
110
|
-
|
|
111
|
+
path2.insertBefore(
|
|
111
112
|
variableDeclaration("const", [variableDeclarator(wrappedFunctionId2, wrappedFunction)])
|
|
112
113
|
);
|
|
113
114
|
specifier.local.name = wrappedFunctionId2.name;
|
|
114
115
|
}
|
|
115
116
|
},
|
|
116
|
-
ExportDefaultDeclaration(
|
|
117
|
-
const declarationType =
|
|
117
|
+
ExportDefaultDeclaration(path2) {
|
|
118
|
+
const declarationType = path2.node.declaration.type;
|
|
118
119
|
if (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression" || declarationType === "ArrowFunctionExpression" || declarationType === "Identifier") {
|
|
119
|
-
const functionName = (declarationType === "Identifier" ?
|
|
120
|
+
const functionName = (declarationType === "Identifier" ? path2.node.declaration.name : (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression") && path2.node.declaration.id?.name) || "__HonoIsladComponent__";
|
|
120
121
|
let originalFunctionId;
|
|
121
122
|
if (declarationType === "Identifier") {
|
|
122
|
-
originalFunctionId =
|
|
123
|
+
originalFunctionId = path2.node.declaration;
|
|
123
124
|
} else {
|
|
124
125
|
originalFunctionId = identifier(functionName + "Original");
|
|
125
|
-
const originalFunction =
|
|
126
|
+
const originalFunction = path2.node.declaration.type === "FunctionExpression" || path2.node.declaration.type === "ArrowFunctionExpression" ? path2.node.declaration : functionExpression(
|
|
126
127
|
null,
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
path2.node.declaration.params,
|
|
129
|
+
path2.node.declaration.body,
|
|
129
130
|
void 0,
|
|
130
|
-
|
|
131
|
+
path2.node.declaration.async
|
|
131
132
|
);
|
|
132
|
-
|
|
133
|
+
path2.insertBefore(
|
|
133
134
|
variableDeclaration("const", [
|
|
134
135
|
variableDeclarator(originalFunctionId, originalFunction)
|
|
135
136
|
])
|
|
@@ -137,7 +138,7 @@ const transformJsxTags = (contents, componentName) => {
|
|
|
137
138
|
}
|
|
138
139
|
const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName);
|
|
139
140
|
wrappedFunctionId = identifier("Wrapped" + functionName);
|
|
140
|
-
|
|
141
|
+
path2.replaceWith(
|
|
141
142
|
variableDeclaration("const", [variableDeclarator(wrappedFunctionId, wrappedFunction)])
|
|
142
143
|
);
|
|
143
144
|
}
|
|
@@ -150,10 +151,22 @@ const transformJsxTags = (contents, componentName) => {
|
|
|
150
151
|
return code;
|
|
151
152
|
}
|
|
152
153
|
};
|
|
153
|
-
function islandComponents() {
|
|
154
|
+
function islandComponents(options) {
|
|
155
|
+
let root = "";
|
|
154
156
|
return {
|
|
155
157
|
name: "transform-island-components",
|
|
158
|
+
configResolved: (config) => {
|
|
159
|
+
root = config.root;
|
|
160
|
+
},
|
|
156
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
|
+
}
|
|
157
170
|
const match = id.match(/\/islands\/(.+?\.tsx)$/);
|
|
158
171
|
if (match) {
|
|
159
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": {
|
|
@@ -106,7 +106,8 @@
|
|
|
106
106
|
"@babel/parser": "^7.23.6",
|
|
107
107
|
"@babel/traverse": "^7.23.6",
|
|
108
108
|
"@babel/types": "^7.23.6",
|
|
109
|
-
"@hono/vite-dev-server": "^0.
|
|
109
|
+
"@hono/vite-dev-server": "^0.8.1",
|
|
110
|
+
"precinct": "^11.0.5"
|
|
110
111
|
},
|
|
111
112
|
"peerDependencies": {
|
|
112
113
|
"hono": ">=4.*"
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
"devDependencies": {
|
|
115
116
|
"@hono/eslint-config": "^0.0.4",
|
|
116
117
|
"@mdx-js/rollup": "^3.0.0",
|
|
117
|
-
"@playwright/test": "^1.
|
|
118
|
+
"@playwright/test": "^1.42.0",
|
|
118
119
|
"@types/babel__generator": "^7",
|
|
119
120
|
"@types/babel__traverse": "^7",
|
|
120
121
|
"@types/node": "^20.10.5",
|