honox 0.0.1 → 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/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # HonoX
2
2
 
3
- **HonoX** is a simple and fast meta framework for creating websites and Web APIs with Server-Side Rendering - (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.
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
4
 
5
- **Note**: _HonoX is currently in a "beta stage". There will be breaking changes without any announcement. Don't use it in production. However, feel free to try it in your hobby project and give us your feedback!_
5
+ **Note**: _HonoX is currently in a "beta stage". Breaking changes are introduced without following semantic versioning._
6
6
 
7
7
  ## Features
8
8
 
9
- - **File-based routing** - You can create a large app by separating concerns.
9
+ - **File-based routing** - You can create a large application like Next.js.
10
10
  - **Fast SSR** - Rendering is ultra-fast thanks to Hono.
11
11
  - **BYOR** - You can bring your own renderer, not only one using hono/jsx.
12
- - **Island hydration** - If you want interactions, create an island. JavaScript is hydrated only for it.
12
+ - **Islands hydration** - If you want interactions, create an island. JavaScript is hydrated only for it.
13
13
  - **Middleware** - It works as Hono, so you can use a lot of Hono's middleware.
14
14
 
15
15
  ## Get Started - Basic
@@ -154,7 +154,7 @@ Before writing `_renderer.tsx`, write the Renderer type definition in `global.d.
154
154
 
155
155
  ```ts
156
156
  // app/global.d.ts
157
- import 'hono'
157
+ import type {} from 'hono'
158
158
 
159
159
  type Head = {
160
160
  title?: string
@@ -179,7 +179,7 @@ export default jsxRenderer(({ children, title }) => {
179
179
  <head>
180
180
  <meta charset='UTF-8' />
181
181
  <meta name='viewport' content='width=device-width, initial-scale=1.0' />
182
- {title ? <title>{title}</title> : ''}
182
+ {title ? <title>{title}</title> : <></>}
183
183
  </head>
184
184
  <body>{children}</body>
185
185
  </html>
@@ -245,7 +245,7 @@ The below is the project structure of a minimal application including a client s
245
245
 
246
246
  ### Renderer
247
247
 
248
- This is a `_renderer.tsx` which will load the `/app/client.ts` entry file for the client. It can also load the JavaScript file for the production according to the variable `import.meta.env.PROD`.
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
249
 
250
250
  ```tsx
251
251
  // app/routes/_renderer.tsx
@@ -258,7 +258,9 @@ export default jsxRenderer(({ children }) => {
258
258
  <meta charset='UTF-8' />
259
259
  <meta name='viewport' content='width=device-width, initial-scale=1.0' />
260
260
  {import.meta.env.PROD ? (
261
- <script type='module' src='/static/client.js'></script>
261
+ <HasIslands>
262
+ <script type='module' src='/static/client.js'></script>
263
+ </HasIslands>
262
264
  ) : (
263
265
  <script type='module' src='/app/client.ts'></script>
264
266
  )}
@@ -320,7 +322,7 @@ export default createRoute((c) => {
320
322
 
321
323
  You can bring your own renderer using a UI library like React, Preact, Solid, or others.
322
324
 
323
- **Note**: We cannot provide technical support for the renderer you bring.
325
+ **Note**: We may not provide supports for the renderer you bring.
324
326
 
325
327
  ### React case
326
328
 
@@ -389,6 +391,25 @@ createClient({
389
391
 
390
392
  ## Guides
391
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
+
392
413
  ### Using Middleware
393
414
 
394
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:
@@ -1,4 +1,5 @@
1
1
  declare const COMPONENT_NAME = "component-name";
2
2
  declare const DATA_SERIALIZED_PROPS = "data-serialized-props";
3
+ declare const IMPORTING_ISLANDS_ID: "__importing_islands";
3
4
 
4
- export { COMPONENT_NAME, DATA_SERIALIZED_PROPS };
5
+ export { COMPONENT_NAME, DATA_SERIALIZED_PROPS, IMPORTING_ISLANDS_ID };
package/dist/constants.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const COMPONENT_NAME = "component-name";
2
2
  const DATA_SERIALIZED_PROPS = "data-serialized-props";
3
+ const IMPORTING_ISLANDS_ID = "__importing_islands";
3
4
  export {
4
5
  COMPONENT_NAME,
5
- DATA_SERIALIZED_PROPS
6
+ DATA_SERIALIZED_PROPS,
7
+ IMPORTING_ISLANDS_ID
6
8
  };
@@ -0,0 +1,5 @@
1
+ import { FC } from 'hono/jsx';
2
+
3
+ declare const HasIslands: FC;
4
+
5
+ export { HasIslands };
@@ -0,0 +1,10 @@
1
+ import { Fragment, jsx } from "hono/jsx/jsx-runtime";
2
+ import { useRequestContext } from "hono/jsx-renderer";
3
+ import { IMPORTING_ISLANDS_ID } from "../constants.js";
4
+ const HasIslands = ({ children }) => {
5
+ const c = useRequestContext();
6
+ return /* @__PURE__ */ jsx(Fragment, { children: c.get(IMPORTING_ISLANDS_ID) ? children : /* @__PURE__ */ jsx(Fragment, {}) });
7
+ };
8
+ export {
9
+ HasIslands
10
+ };
@@ -1,3 +1,6 @@
1
1
  export { ServerOptions, createApp } from './server.js';
2
+ export { HasIslands } from './components.js';
2
3
  import 'hono/types';
3
4
  import 'hono';
5
+ import '../constants.js';
6
+ import 'hono/jsx';
@@ -1,4 +1,6 @@
1
1
  import { createApp } from "./server.js";
2
+ import { HasIslands } from "./components.js";
2
3
  export {
4
+ HasIslands,
3
5
  createApp
4
6
  };
@@ -1,13 +1,17 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { H } from 'hono/types';
3
3
  import { Env, Hono, MiddlewareHandler, NotFoundHandler, ErrorHandler } from 'hono';
4
+ import { IMPORTING_ISLANDS_ID } from '../constants.js';
4
5
 
5
6
  declare const METHODS: readonly ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
7
+ type InnerMeta = {
8
+ [key in typeof IMPORTING_ISLANDS_ID]?: boolean;
9
+ };
6
10
  type RouteFile = {
7
11
  default?: Function;
8
12
  } & {
9
13
  [M in (typeof METHODS)[number]]?: H[];
10
- };
14
+ } & InnerMeta;
11
15
  type RendererFile = {
12
16
  default: MiddlewareHandler;
13
17
  };
@@ -1,9 +1,10 @@
1
1
  import { Hono } from "hono";
2
+ import { createMiddleware } from "hono/factory";
3
+ import { IMPORTING_ISLANDS_ID } from "../constants.js";
2
4
  import {
3
5
  filePathToPath,
4
6
  groupByDirectory,
5
7
  listByDirectory,
6
- pathToDirectoryPath,
7
8
  sortDirectoriesByDepth
8
9
  } from "../utils/file.js";
9
10
  const NOTFOUND_FILENAME = "_404.tsx";
@@ -28,10 +29,12 @@ const createApp = (options) => {
28
29
  eager: true
29
30
  });
30
31
  const rendererList = listByDirectory(RENDERER_FILE);
31
- const applyRenderer = (rendererFile) => {
32
+ const applyRenderer = (app2, rendererFile) => {
32
33
  const renderer = RENDERER_FILE[rendererFile];
33
- const path = pathToDirectoryPath(rendererFile).replace(rootRegExp, "");
34
- app.all(`${filePathToPath(path)}*`, renderer.default);
34
+ const rendererDefault = renderer["default"];
35
+ if (rendererDefault) {
36
+ app2.all("*", rendererDefault);
37
+ }
35
38
  };
36
39
  const ROUTES_FILE = options?.ROUTES ?? import.meta.glob("/app/routes/**/[!_]*.(ts|tsx|mdx)", {
37
40
  eager: true
@@ -40,30 +43,31 @@ const createApp = (options) => {
40
43
  for (const map of routesMap) {
41
44
  for (const [dir, content] of Object.entries(map)) {
42
45
  const subApp = new Hono();
43
- let rendererFiles = rendererList[dir];
44
- if (rendererFiles) {
45
- applyRenderer(rendererFiles[0]);
46
- }
47
- if (!rendererFiles) {
48
- const dirPaths = dir.split("/");
49
- const getRendererPaths = (paths) => {
50
- rendererFiles = rendererList[paths.join("/")];
51
- if (!rendererFiles) {
52
- paths.pop();
53
- if (paths.length) {
54
- getRendererPaths(paths);
55
- }
46
+ let rendererPaths = rendererList[dir] ?? [];
47
+ const getRendererPaths = (paths) => {
48
+ rendererPaths = rendererList[paths.join("/")];
49
+ if (!rendererPaths) {
50
+ paths.pop();
51
+ if (paths.length) {
52
+ getRendererPaths(paths);
56
53
  }
57
- return rendererFiles;
58
- };
59
- rendererFiles = getRendererPaths(dirPaths);
60
- if (rendererFiles) {
61
- applyRenderer(rendererFiles[0]);
62
54
  }
63
- }
55
+ return rendererPaths ?? [];
56
+ };
57
+ const dirPaths = dir.split("/");
58
+ rendererPaths = getRendererPaths(dirPaths);
59
+ rendererPaths.sort((a, b) => a.split("/").length - b.split("/").length);
60
+ rendererPaths.map((path) => {
61
+ applyRenderer(subApp, path);
62
+ });
64
63
  let rootPath = dir.replace(rootRegExp, "");
65
64
  rootPath = filePathToPath(rootPath);
66
65
  for (const [filename, route] of Object.entries(content)) {
66
+ const importingIslands = route[IMPORTING_ISLANDS_ID];
67
+ const setInnerMeta = createMiddleware(async function innerMeta(c, next) {
68
+ c.set(IMPORTING_ISLANDS_ID, importingIslands);
69
+ await next();
70
+ });
67
71
  const routeDefault = route.default;
68
72
  const path = filePathToPath(filename);
69
73
  if (routeDefault && "fetch" in routeDefault) {
@@ -72,13 +76,16 @@ const createApp = (options) => {
72
76
  for (const m of METHODS) {
73
77
  const handlers = route[m];
74
78
  if (handlers) {
79
+ subApp.on(m, path, setInnerMeta);
75
80
  subApp.on(m, path, ...handlers);
76
81
  }
77
82
  }
78
83
  if (routeDefault && Array.isArray(routeDefault)) {
84
+ subApp.get(path, setInnerMeta);
79
85
  subApp.get(path, ...routeDefault);
80
86
  }
81
87
  if (typeof routeDefault === "function") {
88
+ subApp.get(path, setInnerMeta);
82
89
  subApp.get(path, (c) => {
83
90
  return c.render(routeDefault(), route);
84
91
  });
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import devServer, { defaultOptions as devServerDefaultOptions } from "@hono/vite-dev-server";
3
+ import { injectImportingIslands } from "./inject-importing-islands.js";
3
4
  import { islandComponents } from "./island-components.js";
4
5
  const defaultOptions = {
5
6
  islands: true,
@@ -23,6 +24,7 @@ function honox(options) {
23
24
  if (options?.islands !== false) {
24
25
  plugins.push(islandComponents());
25
26
  }
27
+ plugins.push(injectImportingIslands());
26
28
  return [
27
29
  {
28
30
  name: "honox-vite-config"
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ declare function injectHasIslands(): Plugin;
4
+
5
+ export { injectHasIslands };
@@ -0,0 +1,52 @@
1
+ import _generate from "@babel/generator";
2
+ import { parse } from "@babel/parser";
3
+ import _traverse from "@babel/traverse";
4
+ import { HAS_ISLANDS_ID } from "../constants.js";
5
+ const traverse = _traverse.default ?? _traverse;
6
+ const generate = _generate.default ?? _generate;
7
+ function injectHasIslands() {
8
+ return {
9
+ name: "inject-has-islands",
10
+ transform(code, id) {
11
+ if (id.endsWith(".tsx") || id.endsWith(".jsx")) {
12
+ let hasIslandsImport = false;
13
+ const ast = parse(code, {
14
+ sourceType: "module",
15
+ plugins: ["jsx"]
16
+ });
17
+ traverse(ast, {
18
+ ImportDeclaration(path) {
19
+ if (path.node.source.value.includes("islands/")) {
20
+ hasIslandsImport = true;
21
+ }
22
+ }
23
+ });
24
+ if (hasIslandsImport) {
25
+ const hasIslandsNode = {
26
+ type: "ExportNamedDeclaration",
27
+ declaration: {
28
+ type: "VariableDeclaration",
29
+ declarations: [
30
+ {
31
+ type: "VariableDeclarator",
32
+ id: { type: "Identifier", name: HAS_ISLANDS_ID },
33
+ init: { type: "BooleanLiteral", value: true }
34
+ }
35
+ ],
36
+ kind: "const"
37
+ }
38
+ };
39
+ ast.program.body.push(hasIslandsNode);
40
+ }
41
+ const output = generate(ast, {}, code);
42
+ return {
43
+ code: output.code,
44
+ map: output.map
45
+ };
46
+ }
47
+ }
48
+ };
49
+ }
50
+ export {
51
+ injectHasIslands
52
+ };
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ declare function injectImportingIslands(): Plugin;
4
+
5
+ export { injectImportingIslands };
@@ -0,0 +1,52 @@
1
+ import _generate from "@babel/generator";
2
+ import { parse } from "@babel/parser";
3
+ import _traverse from "@babel/traverse";
4
+ import { IMPORTING_ISLANDS_ID } from "../constants.js";
5
+ const traverse = _traverse.default ?? _traverse;
6
+ const generate = _generate.default ?? _generate;
7
+ function injectImportingIslands() {
8
+ return {
9
+ name: "inject-importing-islands",
10
+ transform(code, id) {
11
+ if (id.endsWith(".tsx") || id.endsWith(".jsx")) {
12
+ let hasIslandsImport = false;
13
+ const ast = parse(code, {
14
+ sourceType: "module",
15
+ plugins: ["jsx"]
16
+ });
17
+ traverse(ast, {
18
+ ImportDeclaration(path) {
19
+ if (path.node.source.value.includes("islands/")) {
20
+ hasIslandsImport = true;
21
+ }
22
+ }
23
+ });
24
+ if (hasIslandsImport) {
25
+ const hasIslandsNode = {
26
+ type: "ExportNamedDeclaration",
27
+ declaration: {
28
+ type: "VariableDeclaration",
29
+ declarations: [
30
+ {
31
+ type: "VariableDeclarator",
32
+ id: { type: "Identifier", name: IMPORTING_ISLANDS_ID },
33
+ init: { type: "BooleanLiteral", value: true }
34
+ }
35
+ ],
36
+ kind: "const"
37
+ }
38
+ };
39
+ ast.program.body.push(hasIslandsNode);
40
+ }
41
+ const output = generate(ast, {}, code);
42
+ return {
43
+ code: output.code,
44
+ map: output.map
45
+ };
46
+ }
47
+ }
48
+ };
49
+ }
50
+ export {
51
+ injectImportingIslands
52
+ };
@@ -75,8 +75,9 @@ const transformJsxTags = (contents, componentName) => {
75
75
  ExportDefaultDeclaration(path) {
76
76
  if (path.node.declaration.type === "FunctionDeclaration") {
77
77
  const functionId = path.node.declaration.id;
78
- if (!functionId)
78
+ if (!functionId) {
79
79
  return;
80
+ }
80
81
  const isAsync = path.node.declaration.async;
81
82
  const originalFunctionId = identifier(functionId.name + "Original");
82
83
  const originalFunction = functionExpression(
@@ -0,0 +1,8 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ type RemoveElementsOptions = {
4
+ attributes: string[];
5
+ };
6
+ declare function removeElementsPlugin(options: RemoveElementsOptions): Plugin;
7
+
8
+ export { removeElementsPlugin };
@@ -0,0 +1,52 @@
1
+ import _generate from "@babel/generator";
2
+ import { parse } from "@babel/parser";
3
+ import _traverse from "@babel/traverse";
4
+ import { HAS_ISLANDS_ID } from "../constants";
5
+ const traverse = _traverse.default ?? _traverse;
6
+ const generate = _generate.default ?? _generate;
7
+ function injectHasIslands() {
8
+ return {
9
+ name: "inject-has-islands",
10
+ transform(code, id) {
11
+ if (id.endsWith(".tsx") || id.endsWith(".jsx")) {
12
+ let hasIslandsImport = false;
13
+ const ast = parse(code, {
14
+ sourceType: "module",
15
+ plugins: ["jsx"]
16
+ });
17
+ traverse(ast, {
18
+ ImportDeclaration(path) {
19
+ if (path.node.source.value.includes("islands/")) {
20
+ hasIslandsImport = true;
21
+ }
22
+ }
23
+ });
24
+ if (hasIslandsImport) {
25
+ const hasIslandsNode = {
26
+ type: "ExportNamedDeclaration",
27
+ declaration: {
28
+ type: "VariableDeclaration",
29
+ declarations: [
30
+ {
31
+ type: "VariableDeclarator",
32
+ id: { type: "Identifier", name: HAS_ISLANDS_ID },
33
+ init: { type: "BooleanLiteral", value: true }
34
+ }
35
+ ],
36
+ kind: "const"
37
+ }
38
+ };
39
+ ast.program.body.push(hasIslandsNode);
40
+ }
41
+ const output = generate(ast, {}, code);
42
+ return {
43
+ code: output.code,
44
+ map: output.map
45
+ };
46
+ }
47
+ }
48
+ };
49
+ }
50
+ export {
51
+ injectHasIslands
52
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honox",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,15 +8,18 @@
8
8
  "test:unit": "vitest --run test/unit",
9
9
  "test:integration": "bun test:integration:api && bun test:integration:hono-jsx",
10
10
  "test:integration:hono-jsx": "vitest run -c ./test/hono-jsx/vitest.config.ts ./test/hono-jsx/integration.test.ts",
11
+ "test:integration:hono-jsx:watch": "vitest -c ./test/hono-jsx/vitest.config.ts ./test/hono-jsx/integration.test.ts",
11
12
  "test:integration:api": "vitest run -c ./test/api/vitest.config.ts ./test/api/integration.test.ts",
12
13
  "test:e2e": "playwright test -c ./test/hono-jsx/playwright.config.ts ./test/hono-jsx/e2e.test.ts",
13
14
  "typecheck": "tsc --noEmit",
14
15
  "build": "tsup && publint",
15
16
  "watch": "tsup --watch",
16
- "lint": "eslint src/**.ts",
17
- "lint:fix": "eslint src/**.ts --fix",
18
- "prerelease": "yarn test && yarn build",
19
- "release": "np --no-yarn"
17
+ "lint": "eslint --ext js,ts src test",
18
+ "lint:fix": "eslint --ext js,ts src test --fix",
19
+ "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
20
+ "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
21
+ "prerelease": "bun run test && bun run build",
22
+ "release": "np"
20
23
  },
21
24
  "files": [
22
25
  "dist"
@@ -89,10 +92,10 @@
89
92
  "@babel/parser": "^7.23.6",
90
93
  "@babel/traverse": "^7.23.6",
91
94
  "@babel/types": "^7.23.6",
92
- "@hono/vite-dev-server": "^0.4.1"
95
+ "@hono/vite-dev-server": "^0.5.0"
93
96
  },
94
97
  "devDependencies": {
95
- "@hono/eslint-config": "^0.0.3",
98
+ "@hono/eslint-config": "^0.0.4",
96
99
  "@mdx-js/rollup": "^3.0.0",
97
100
  "@playwright/test": "^1.41.0",
98
101
  "@types/babel__generator": "^7",
@@ -100,8 +103,8 @@
100
103
  "@types/node": "^20.10.5",
101
104
  "eslint": "^8.56.0",
102
105
  "glob": "^10.3.10",
103
- "hono": "4.0.0-rc.2",
104
- "np": "^9.2.0",
106
+ "hono": "4.0.0-rc.4",
107
+ "np": "7.7.0",
105
108
  "prettier": "^3.1.1",
106
109
  "publint": "^0.2.7",
107
110
  "tsup": "^8.0.1",
@@ -109,8 +112,10 @@
109
112
  "vite": "^5.0.12",
110
113
  "vitest": "^1.2.1"
111
114
  },
112
- "packageManager": "yarn@4.0.2",
113
115
  "engines": {
114
116
  "node": ">=18.14.1"
117
+ },
118
+ "optionalDependencies": {
119
+ "@rollup/rollup-linux-x64-gnu": "^4.9.6"
115
120
  }
116
121
  }