honox 0.1.10 → 0.1.12

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
@@ -241,7 +241,7 @@ Let's create an application that includes a client side. Here, we will use hono/
241
241
 
242
242
  ### Project Structure
243
243
 
244
- The below is the project structure of a minimal application including a client side:
244
+ Below is the project structure of a minimal application including a client side:
245
245
 
246
246
  ```txt
247
247
  .
@@ -261,7 +261,7 @@ 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 `<HasIslands />` if there are islands on that page.
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 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
@@ -324,7 +324,12 @@ createClient()
324
324
 
325
325
  ### Interactions
326
326
 
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:
327
+ If you want to add interactions to your page, create Island components. Islands components should be:
328
+
329
+ - Placed under `app/islands` directory or named with `_` prefix and `island.tsx` suffix like `_componentName.island.tsx`.
330
+ - Should `default export function`.
331
+
332
+ For example, you can write an interactive component such as the following counter:
328
333
 
329
334
  ```tsx
330
335
  // app/islands/counter.tsx
@@ -341,7 +346,7 @@ export default function Counter() {
341
346
  }
342
347
  ```
343
348
 
344
- 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.
349
+ When you load the component in a route file, it is rendered as Server-Side rendering and JavaScript is also sent to the client side.
345
350
 
346
351
  ```tsx
347
352
  // app/routes/index.tsx
@@ -358,7 +363,7 @@ export default createRoute((c) => {
358
363
  })
359
364
  ```
360
365
 
361
- **Note**: You cannot access a Context object in Island components. Therefore, you should pass the value from components outside of Island.
366
+ **Note**: You cannot access a Context object in Island components. Therefore, you should pass the value from components outside of the Island.
362
367
 
363
368
  ```ts
364
369
  import { useRequestContext } from 'hono/jsx-renderer'
@@ -779,7 +784,7 @@ Build command (including a client):
779
784
  vite build --mode client && vite build
780
785
  ```
781
786
 
782
- Deploy with the following commands after build. Ensure you have [Wrangler](https://developers.cloudflare.com/workers/wrangler/) installed:
787
+ Deploy with the following commands after the build. Ensure you have [Wrangler](https://developers.cloudflare.com/workers/wrangler/) installed:
783
788
 
784
789
  ```txt
785
790
  wrangler pages deploy ./dist
@@ -2,8 +2,11 @@ import { render } from "hono/jsx/dom";
2
2
  import { jsx as jsxFn } from "hono/jsx/dom/jsx-runtime";
3
3
  import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
4
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/";
5
+ const FILES = options?.ISLAND_FILES ?? {
6
+ ...import.meta.glob("/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)"),
7
+ ...import.meta.glob("/app/routes/**/_[a-zA-Z0-9[-]+.island.(tsx|ts)")
8
+ };
9
+ const root = options?.island_root ?? "/app";
7
10
  const hydrateComponent = async (document2) => {
8
11
  const filePromises = Object.keys(FILES).map(async (filePath) => {
9
12
  const componentName = filePath.replace(root, "");
@@ -13,6 +13,7 @@ const METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
13
13
  const createApp = (options) => {
14
14
  const root = options.root;
15
15
  const rootRegExp = new RegExp(`^${root}`);
16
+ const getRootPath = (dir) => filePathToPath(dir.replace(rootRegExp, ""));
16
17
  const app = options.app ?? new Hono();
17
18
  const trailingSlash = options.trailingSlash ?? false;
18
19
  if (options.init) {
@@ -69,8 +70,6 @@ const createApp = (options) => {
69
70
  subApp.use(...middleware.default);
70
71
  }
71
72
  }
72
- let rootPath = dir.replace(rootRegExp, "");
73
- rootPath = filePathToPath(rootPath);
74
73
  for (const [filename, route] of Object.entries(content)) {
75
74
  const importingIslands = route[IMPORTING_ISLANDS_ID];
76
75
  const setInnerMeta = createMiddleware(async function innerMeta(c, next) {
@@ -101,14 +100,21 @@ const createApp = (options) => {
101
100
  });
102
101
  }
103
102
  }
104
- applyNotFound(subApp, dir, notFoundMap);
105
103
  applyError(subApp, dir, errorMap);
104
+ let rootPath = getRootPath(dir);
106
105
  if (trailingSlash) {
107
106
  rootPath = /\/$/.test(rootPath) ? rootPath : rootPath + "/";
108
107
  }
109
108
  app.route(rootPath, subApp);
110
109
  }
111
110
  }
111
+ for (const map of routesMap.reverse()) {
112
+ const dir = Object.entries(map)[0][0];
113
+ const subApp = new Hono();
114
+ applyNotFound(subApp, dir, notFoundMap);
115
+ const rootPath = getRootPath(dir);
116
+ app.route(rootPath, subApp);
117
+ }
112
118
  return app;
113
119
  };
114
120
  function applyNotFound(app, dir, map) {
@@ -33,12 +33,8 @@ const sortDirectoriesByDepth = (directories) => {
33
33
  const sortedKeys = Object.keys(directories).sort((a, b) => {
34
34
  const depthA = a.split("/").length;
35
35
  const depthB = b.split("/").length;
36
- return depthA - depthB;
36
+ return depthA - depthB || b.localeCompare(a);
37
37
  });
38
- if (sortedKeys.find((x) => /.*\/routes$/.test(x))) {
39
- sortedKeys.push(sortedKeys[0]);
40
- sortedKeys.shift();
41
- }
42
38
  return sortedKeys.map((key) => ({
43
39
  [key]: directories[key]
44
40
  }));
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,123 @@
1
+ import {
2
+ filePathToPath,
3
+ groupByDirectory,
4
+ listByDirectory,
5
+ pathToDirectoryPath,
6
+ sortDirectoriesByDepth
7
+ } from "./file";
8
+ describe("filePathToPath", () => {
9
+ it("Should return a correct path", () => {
10
+ expect(filePathToPath("index.tsx")).toBe("/");
11
+ expect(filePathToPath("index.get.tsx")).toBe("/index.get");
12
+ expect(filePathToPath("about.tsx")).toBe("/about");
13
+ expect(filePathToPath("about/index.tsx")).toBe("/about");
14
+ expect(filePathToPath("about/me")).toBe("/about/me");
15
+ expect(filePathToPath("about/me/index.tsx")).toBe("/about/me");
16
+ expect(filePathToPath("about/me/address.tsx")).toBe("/about/me/address");
17
+ expect(filePathToPath("/index.tsx")).toBe("/");
18
+ expect(filePathToPath("/index.get.tsx")).toBe("/index.get");
19
+ expect(filePathToPath("/about.tsx")).toBe("/about");
20
+ expect(filePathToPath("/about/index.tsx")).toBe("/about");
21
+ expect(filePathToPath("/about/me")).toBe("/about/me");
22
+ expect(filePathToPath("/about/me/index.tsx")).toBe("/about/me");
23
+ expect(filePathToPath("/about/me/address.tsx")).toBe("/about/me/address");
24
+ expect(filePathToPath("/about/[name].tsx")).toBe("/about/:name");
25
+ expect(filePathToPath("/about/[...foo].tsx")).toBe("/about/*");
26
+ expect(filePathToPath("/about/[name]/address.tsx")).toBe("/about/:name/address");
27
+ expect(filePathToPath("/about/[arg1]/[arg2]")).toBe("/about/:arg1/:arg2");
28
+ });
29
+ });
30
+ describe("groupByDirectory", () => {
31
+ const files = {
32
+ "/app/routes/index.tsx": "file1",
33
+ "/app/routes/about.tsx": "file2",
34
+ "/app/routes/blog/index.tsx": "file3",
35
+ "/app/routes/blog/about.tsx": "file4",
36
+ "/app/routes/blog/posts/index.tsx": "file5",
37
+ "/app/routes/blog/posts/comments.tsx": "file6"
38
+ };
39
+ it("Should group by directories", () => {
40
+ expect(groupByDirectory(files)).toEqual({
41
+ "/app/routes": {
42
+ "index.tsx": "file1",
43
+ "about.tsx": "file2"
44
+ },
45
+ "/app/routes/blog": {
46
+ "index.tsx": "file3",
47
+ "about.tsx": "file4"
48
+ },
49
+ "/app/routes/blog/posts": {
50
+ "index.tsx": "file5",
51
+ "comments.tsx": "file6"
52
+ }
53
+ });
54
+ });
55
+ });
56
+ describe("sortDirectoriesByDepth", () => {
57
+ it("Should sort directories by the depth", () => {
58
+ expect(
59
+ sortDirectoriesByDepth({
60
+ "/dir": {
61
+ "index.tsx": "file1"
62
+ },
63
+ "/dir/blog/[id]": {
64
+ "index.tsx": "file2"
65
+ },
66
+ "/dir/blog/posts": {
67
+ "index.tsx": "file3"
68
+ },
69
+ "/dir/blog": {
70
+ "index.tsx": "file4"
71
+ }
72
+ })
73
+ ).toStrictEqual([
74
+ {
75
+ "/dir": {
76
+ "index.tsx": "file1"
77
+ }
78
+ },
79
+ {
80
+ "/dir/blog": {
81
+ "index.tsx": "file4"
82
+ }
83
+ },
84
+ {
85
+ "/dir/blog/posts": {
86
+ "index.tsx": "file3"
87
+ }
88
+ },
89
+ {
90
+ "/dir/blog/[id]": {
91
+ "index.tsx": "file2"
92
+ }
93
+ }
94
+ ]);
95
+ });
96
+ });
97
+ describe("listByDirectory", () => {
98
+ it("Should list files by their directory", () => {
99
+ const files = {
100
+ "/app/routes/blog/posts/_renderer.tsx": "foo3",
101
+ "/app/routes/_renderer.tsx": "foo",
102
+ "/app/routes/blog/_renderer.tsx": "foo2"
103
+ };
104
+ const result = listByDirectory(files);
105
+ expect(result).toEqual({
106
+ "/app/routes": ["/app/routes/_renderer.tsx"],
107
+ "/app/routes/blog": ["/app/routes/blog/_renderer.tsx", "/app/routes/_renderer.tsx"],
108
+ "/app/routes/blog/posts": [
109
+ "/app/routes/blog/posts/_renderer.tsx",
110
+ "/app/routes/blog/_renderer.tsx",
111
+ "/app/routes/_renderer.tsx"
112
+ ]
113
+ });
114
+ });
115
+ });
116
+ describe("pathToDirectoryPath", () => {
117
+ it("Should return the directory path", () => {
118
+ expect(pathToDirectoryPath("/")).toBe("/");
119
+ expect(pathToDirectoryPath("/about.tsx")).toBe("/");
120
+ expect(pathToDirectoryPath("/posts/index.tsx")).toBe("/posts/");
121
+ expect(pathToDirectoryPath("/posts/authors/index.tsx")).toBe("/posts/authors/");
122
+ });
123
+ });
@@ -7,7 +7,7 @@ import { normalizePath } from "vite";
7
7
  import { IMPORTING_ISLANDS_ID } from "../constants.js";
8
8
  const generate = _generate.default ?? _generate;
9
9
  async function injectImportingIslands() {
10
- const isIslandRegex = new RegExp(/\/islands\//);
10
+ const isIslandRegex = new RegExp(/(\/islands\/|\_[a-zA-Z0-9[-]+\.island\.[tj]sx$)/);
11
11
  const fileRegex = new RegExp(/(routes|_renderer|_error|_404)\/.*\.[tj]sx$/);
12
12
  const cache = {};
13
13
  const walkDependencyTree = async (baseFile, dependencyFile) => {
@@ -160,16 +160,16 @@ function islandComponents(options) {
160
160
  },
161
161
  async load(id) {
162
162
  const defaultIsIsland = (id2) => {
163
- const islandDirectoryPath = path.join(root, "app/islands");
163
+ const islandDirectoryPath = path.join(root, "app");
164
164
  return path.resolve(id2).startsWith(islandDirectoryPath);
165
165
  };
166
166
  const matchIslandPath = options?.isIsland ?? defaultIsIsland;
167
167
  if (!matchIslandPath(id)) {
168
168
  return;
169
169
  }
170
- const match = id.match(/\/islands\/(.+?\.tsx)$/);
170
+ const match = id.match(/(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)/);
171
171
  if (match) {
172
- const componentName = match[1];
172
+ const componentName = match[0];
173
173
  const contents = await fs.readFile(id, "utf-8");
174
174
  const code = transformJsxTags(contents, componentName);
175
175
  if (code) {
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,102 @@
1
+ import { transformJsxTags } from "./island-components";
2
+ describe("transformJsxTags", () => {
3
+ it("Should add component-wrapper and component-name attribute", () => {
4
+ const code = `export default function Badge() {
5
+ return <h1>Hello</h1>
6
+ }`;
7
+ const result = transformJsxTags(code, "Badge.tsx");
8
+ expect(result).toBe(
9
+ `const BadgeOriginal = function () {
10
+ return <h1>Hello</h1>;
11
+ };
12
+ const WrappedBadge = function (props) {
13
+ return import.meta.env.SSR ? <honox-island component-name="Badge.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><BadgeOriginal {...props}></BadgeOriginal>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <BadgeOriginal {...props}></BadgeOriginal>;
14
+ };
15
+ export default WrappedBadge;`
16
+ );
17
+ });
18
+ it("Should not transform if it is blank", () => {
19
+ const code = transformJsxTags("", "Badge.tsx");
20
+ expect(code).toBe("");
21
+ });
22
+ it("async", () => {
23
+ const code = `export default async function AsyncComponent() {
24
+ return <h1>Hello</h1>
25
+ }`;
26
+ const result = transformJsxTags(code, "AsyncComponent.tsx");
27
+ expect(result).toBe(
28
+ `const AsyncComponentOriginal = async function () {
29
+ return <h1>Hello</h1>;
30
+ };
31
+ const WrappedAsyncComponent = function (props) {
32
+ return import.meta.env.SSR ? <honox-island component-name="AsyncComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><AsyncComponentOriginal {...props}></AsyncComponentOriginal>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <AsyncComponentOriginal {...props}></AsyncComponentOriginal>;
33
+ };
34
+ export default WrappedAsyncComponent;`
35
+ );
36
+ });
37
+ it("unnamed", () => {
38
+ const code = `export default async function() {
39
+ return <h1>Hello</h1>
40
+ }`;
41
+ const result = transformJsxTags(code, "UnnamedComponent.tsx");
42
+ expect(result).toBe(
43
+ `const __HonoIsladComponent__Original = async function () {
44
+ return <h1>Hello</h1>;
45
+ };
46
+ const Wrapped__HonoIsladComponent__ = function (props) {
47
+ return import.meta.env.SSR ? <honox-island component-name="UnnamedComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>;
48
+ };
49
+ export default Wrapped__HonoIsladComponent__;`
50
+ );
51
+ });
52
+ it("arrow - block", () => {
53
+ const code = `export default () => {
54
+ return <h1>Hello</h1>
55
+ }`;
56
+ const result = transformJsxTags(code, "UnnamedComponent.tsx");
57
+ expect(result).toBe(
58
+ `const __HonoIsladComponent__Original = () => {
59
+ return <h1>Hello</h1>;
60
+ };
61
+ const Wrapped__HonoIsladComponent__ = function (props) {
62
+ return import.meta.env.SSR ? <honox-island component-name="UnnamedComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>;
63
+ };
64
+ export default Wrapped__HonoIsladComponent__;`
65
+ );
66
+ });
67
+ it("arrow - expression", () => {
68
+ const code = "export default () => <h1>Hello</h1>";
69
+ const result = transformJsxTags(code, "UnnamedComponent.tsx");
70
+ expect(result).toBe(
71
+ `const __HonoIsladComponent__Original = () => <h1>Hello</h1>;
72
+ const Wrapped__HonoIsladComponent__ = function (props) {
73
+ return import.meta.env.SSR ? <honox-island component-name="UnnamedComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>;
74
+ };
75
+ export default Wrapped__HonoIsladComponent__;`
76
+ );
77
+ });
78
+ it("export via variable", () => {
79
+ const code = "export default ExportViaVariable";
80
+ const result = transformJsxTags(code, "ExportViaVariable.tsx");
81
+ expect(result).toBe(
82
+ `const WrappedExportViaVariable = function (props) {
83
+ return import.meta.env.SSR ? <honox-island component-name="ExportViaVariable.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><ExportViaVariable {...props}></ExportViaVariable>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <ExportViaVariable {...props}></ExportViaVariable>;
84
+ };
85
+ export default WrappedExportViaVariable;`
86
+ );
87
+ });
88
+ it("export via specifier", () => {
89
+ const code = `const utilityFn = () => {}
90
+ const ExportViaVariable = () => <h1>Hello</h1>
91
+ export { utilityFn, ExportViaVariable as default }`;
92
+ const result = transformJsxTags(code, "ExportViaVariable.tsx");
93
+ expect(result).toBe(
94
+ `const utilityFn = () => {};
95
+ const ExportViaVariable = () => <h1>Hello</h1>;
96
+ const WrappedExportViaVariable = function (props) {
97
+ return import.meta.env.SSR ? <honox-island component-name="ExportViaVariable.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><ExportViaVariable {...props}></ExportViaVariable>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <ExportViaVariable {...props}></ExportViaVariable>;
98
+ };
99
+ export { utilityFn, WrappedExportViaVariable as default };`
100
+ );
101
+ });
102
+ });
package/package.json CHANGED
@@ -1,24 +1,22 @@
1
1
  {
2
2
  "name": "honox",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "test": "bun typecheck && bun test:unit && bun test:integration && bun test:e2e",
8
- "test:unit": "vitest --run test/unit",
9
- "test:integration": "bun test:integration:api && bun test:integration:hono-jsx",
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",
12
- "test:integration:api": "vitest run -c ./test/api/vitest.config.ts ./test/api/integration.test.ts",
13
- "test:e2e": "playwright test -c ./test/hono-jsx/playwright.config.ts ./test/hono-jsx/e2e.test.ts",
14
- "test:yarn": "yarn test:unit && yarn test:integration:api && yarn test:integration:hono-jsx && yarn test:e2e",
8
+ "test:unit": "vitest --run ./src",
9
+ "test:integration": "bun test:integration:api && bun test:integration:apps",
10
+ "test:integration:api": "vitest --run ./test-integration/api.test.ts",
11
+ "test:integration:apps": "vitest --run -c ./test-integration/vitest.config.ts ./test-integration/apps.test.ts",
12
+ "test:e2e": "playwright test -c ./test-e2e/playwright.config.ts ./test-e2e/e2e.test.ts",
15
13
  "typecheck": "tsc --noEmit",
16
14
  "build": "tsup && publint",
17
15
  "watch": "tsup --watch",
18
- "lint": "eslint --ext js,ts src test",
19
- "lint:fix": "eslint --ext js,ts src test --fix",
20
- "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
21
- "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
16
+ "lint": "eslint --ext js,ts src mocks test-integration test-e2e",
17
+ "lint:fix": "eslint --ext js,ts src mocks test-integration test-e2e --fix",
18
+ "format": "prettier --check \"src/**/*.{js,ts}\" \"mocks/**/*.{js,ts}\" \"test-*/**/*.{js,ts}\"",
19
+ "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"mocks/**/*.{js,ts}\" \"test-*/**/*.{js,ts}\"",
22
20
  "prerelease": "bun run test && bun run build",
23
21
  "release": "np"
24
22
  },
@@ -107,7 +105,7 @@
107
105
  "@babel/parser": "^7.23.6",
108
106
  "@babel/traverse": "^7.23.6",
109
107
  "@babel/types": "^7.23.6",
110
- "@hono/vite-dev-server": "^0.9.0",
108
+ "@hono/vite-dev-server": "^0.11.0",
111
109
  "precinct": "^11.0.5"
112
110
  },
113
111
  "peerDependencies": {
@@ -128,8 +126,8 @@
128
126
  "publint": "^0.2.7",
129
127
  "tsup": "^8.0.1",
130
128
  "typescript": "^5.3.3",
131
- "vite": "^5.0.12",
132
- "vitest": "^1.2.1"
129
+ "vite": "^5.2.8",
130
+ "vitest": "^1.4.0"
133
131
  },
134
132
  "engines": {
135
133
  "node": ">=18.14.1"