honox 0.1.15 → 0.1.17

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.
Files changed (34) hide show
  1. package/README.md +60 -10
  2. package/dist/client/client.d.ts +3 -0
  3. package/dist/client/client.js +21 -10
  4. package/dist/constants.d.ts +2 -1
  5. package/dist/constants.js +2 -0
  6. package/dist/server/components/has-islands.d.ts +3 -3
  7. package/dist/server/components/has-islands.js +6 -3
  8. package/dist/server/components/index.d.ts +0 -1
  9. package/dist/server/components/script.d.ts +2 -2
  10. package/dist/server/components/script.js +4 -3
  11. package/dist/server/context-storage.d.ts +6 -0
  12. package/dist/server/context-storage.js +5 -0
  13. package/dist/server/index.d.ts +0 -1
  14. package/dist/server/server.js +5 -1
  15. package/dist/server/utils/file.d.ts +7 -0
  16. package/dist/server/utils/file.js +76 -0
  17. package/dist/server/utils/file.test.d.ts +2 -0
  18. package/dist/server/utils/file.test.js +123 -0
  19. package/dist/vite/components/honox-island.d.ts +8 -0
  20. package/dist/vite/components/honox-island.js +56 -0
  21. package/dist/vite/components/index.d.ts +1 -0
  22. package/dist/vite/components/index.js +4 -0
  23. package/dist/vite/components.d.ts +7 -0
  24. package/dist/vite/components.js +36 -0
  25. package/dist/vite/inject-importing-islands.d.ts +5 -1
  26. package/dist/vite/inject-importing-islands.js +22 -8
  27. package/dist/vite/island-components.d.ts +6 -1
  28. package/dist/vite/island-components.js +90 -89
  29. package/dist/vite/island-components.test.js +175 -15
  30. package/dist/vite/utils/path.d.ts +22 -0
  31. package/dist/vite/utils/path.js +13 -0
  32. package/dist/vite/utils/path.test.d.ts +2 -0
  33. package/dist/vite/utils/path.test.js +48 -0
  34. package/package.json +11 -3
package/README.md CHANGED
@@ -131,7 +131,7 @@ export default createRoute((c) => {
131
131
  })
132
132
  ```
133
133
 
134
- #### 2. Using Hono instance
134
+ #### 2. Using a Hono instance
135
135
 
136
136
  You can create API endpoints by exporting an instance of the Hono object.
137
137
 
@@ -311,9 +311,48 @@ export default jsxRenderer(({ children }) => {
311
311
 
312
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
313
 
314
+ #### nonce Attribute
315
+
316
+ If you want to add a `nonce` attribute to `<Script />` or `<script />` element, you can use [Security Headers Middleware](https://hono.dev/middleware/builtin/secure-headers).
317
+
318
+ Define the middleware:
319
+
320
+ ```ts
321
+ // app/routes/_middleware.ts
322
+ import { createRoute } from 'honox/factory'
323
+ import { secureHeaders, NONCE } from 'hono/secure-headers'
324
+
325
+ secureHeaders({
326
+ contentSecurityPolicy: import.meta.env.PROD
327
+ ? {
328
+ scriptSrc: [NONCE],
329
+ }
330
+ : undefined,
331
+ })
332
+ ```
333
+
334
+ You can get the `nonce` value with `c.get('secureHeadersNonce')`:
335
+
336
+ ```tsx
337
+ // app/routes/_renderer.tsx
338
+ import { jsxRenderer } from 'hono/jsx-renderer'
339
+ import { Script } from 'honox/server'
340
+
341
+ export default jsxRenderer(({ children }, c) => {
342
+ return (
343
+ <html lang='en'>
344
+ <head>
345
+ <Script src='/app/client.ts' async nonce={c.get('secureHeadersNonce')} />
346
+ </head>
347
+ <body>{children}</body>
348
+ </html>
349
+ )
350
+ })
351
+ ```
352
+
314
353
  ### Client Entry File
315
354
 
316
- A client side entry file should be in `app/client.ts`. Simply, write `createClient()`.
355
+ A client-side entry file should be in `app/client.ts`. Simply, write `createClient()`.
317
356
 
318
357
  ```ts
319
358
  // app/client.ts
@@ -326,8 +365,8 @@ createClient()
326
365
 
327
366
  If you want to add interactions to your page, create Island components. Islands components should be:
328
367
 
329
- - Placed under `app/islands` directory or named with `_` prefix and `island.tsx` suffix like `_componentName.island.tsx`.
330
- - Should `export default function`.
368
+ - Placed under `app/islands` directory or named with `$` prefix like `$componentName.tsx`.
369
+ - It should be exported as a `default` or a proper component name that uses camel case but does not contain `_` and is not all uppercase.
331
370
 
332
371
  For example, you can write an interactive component such as the following counter:
333
372
 
@@ -379,7 +418,7 @@ export default function Component() {
379
418
 
380
419
  You can bring your own renderer using a UI library like React, Preact, Solid, or others.
381
420
 
382
- **Note**: We may not provide supports for the renderer you bring.
421
+ **Note**: We may not provide support for the renderer you bring.
383
422
 
384
423
  ### React case
385
424
 
@@ -531,7 +570,7 @@ export const POST = createRoute(zValidator('form', schema), async (c) => {
531
570
  })
532
571
  ```
533
572
 
534
- Alternatively, you can use a `_middleware.(ts|tsx)` file in a directory to have that middleware applied to the current route, as well as all child routes. Middleware is ran in the order that it is listed within the array.
573
+ Alternatively, you can use a `_middleware.(ts|tsx)` file in a directory to have that middleware applied to the current route, as well as all child routes. Middleware is run in the order that it is listed within the array.
535
574
 
536
575
  ```ts
537
576
  // /app/routes/_middleware.ts
@@ -708,7 +747,8 @@ If you want to use Cloudflare's Bindings in your development environment, create
708
747
 
709
748
  ```toml
710
749
  name = "my-project-name"
711
- compatibility_date = "2023-12-01"
750
+ compatibility_date = "2024-04-01"
751
+ compatibility_flags = [ "nodejs_compat" ]
712
752
  pages_build_output_dir = "./dist"
713
753
 
714
754
  # [vars]
@@ -743,6 +783,16 @@ Since a HonoX instance is essentially a Hono instance, it can be deployed on any
743
783
 
744
784
  ### Cloudflare Pages
745
785
 
786
+ Add the `wrangler.toml`:
787
+
788
+ ```toml
789
+ # wrangler.toml
790
+ name = "my-project-name"
791
+ compatibility_date = "2024-04-01"
792
+ compatibility_flags = [ "nodejs_compat" ]
793
+ pages_build_output_dir = "./dist"
794
+ ```
795
+
746
796
  Setup the `vite.config.ts`:
747
797
 
748
798
  ```ts
@@ -756,7 +806,7 @@ export default defineConfig({
756
806
  })
757
807
  ```
758
808
 
759
- If you want to include client side scripts and assets:
809
+ If you want to include client-side scripts and assets:
760
810
 
761
811
  ```ts
762
812
  // vite.config.ts
@@ -787,7 +837,7 @@ vite build --mode client && vite build
787
837
  Deploy with the following commands after the build. Ensure you have [Wrangler](https://developers.cloudflare.com/workers/wrangler/) installed:
788
838
 
789
839
  ```txt
790
- wrangler pages deploy ./dist
840
+ wrangler pages deploy
791
841
  ```
792
842
 
793
843
  ### SSG - Static Site Generation
@@ -808,7 +858,7 @@ export default defineConfig(() => {
808
858
  })
809
859
  ```
810
860
 
811
- If you want to include client side scripts and assets:
861
+ If you want to include client-side scripts and assets:
812
862
 
813
863
  ```ts
814
864
  // vite.config.ts
@@ -12,6 +12,9 @@ type ClientOptions = {
12
12
  */
13
13
  triggerHydration?: TriggerHydration;
14
14
  ISLAND_FILES?: Record<string, () => Promise<unknown>>;
15
+ /**
16
+ * @deprecated
17
+ */
15
18
  island_root?: string;
16
19
  };
17
20
  declare const createClient: (options?: ClientOptions) => Promise<void>;
@@ -1,41 +1,52 @@
1
1
  import { render } from "hono/jsx/dom";
2
2
  import { jsx as jsxFn } from "hono/jsx/dom/jsx-runtime";
3
- import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
3
+ import {
4
+ COMPONENT_NAME,
5
+ COMPONENT_EXPORT,
6
+ DATA_HONO_TEMPLATE,
7
+ DATA_SERIALIZED_PROPS
8
+ } from "../constants.js";
4
9
  const createClient = async (options) => {
5
10
  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)")
11
+ ...import.meta.glob("/app/islands/**/[a-zA-Z0-9-]+.tsx"),
12
+ ...import.meta.glob("/app/**/_[a-zA-Z0-9-]+.island.tsx"),
13
+ ...import.meta.glob("/app/**/$[a-zA-Z0-9-]+.tsx")
8
14
  };
9
- const root = options?.island_root ?? "/app";
10
15
  const hydrateComponent = async (document2) => {
11
16
  const filePromises = Object.keys(FILES).map(async (filePath) => {
12
- const componentName = filePath.replace(root, "");
17
+ const componentName = filePath;
13
18
  const elements = document2.querySelectorAll(
14
19
  `[${COMPONENT_NAME}="${componentName}"]:not([data-hono-hydrated])`
15
20
  );
16
21
  if (elements) {
17
22
  const elementPromises = Array.from(elements).map(async (element) => {
18
23
  element.setAttribute("data-hono-hydrated", "true");
24
+ const exportName = element.getAttribute(COMPONENT_EXPORT) || "default";
19
25
  const fileCallback = FILES[filePath];
20
26
  const file = await fileCallback();
21
- const Component = await file.default;
27
+ const Component = await file[exportName];
22
28
  const serializedProps = element.attributes.getNamedItem(DATA_SERIALIZED_PROPS)?.value;
23
29
  const props = JSON.parse(serializedProps ?? "{}");
24
30
  const hydrate = options?.hydrate ?? render;
25
31
  const createElement = options?.createElement ?? jsxFn;
26
- const maybeTemplate = element.childNodes[element.childNodes.length - 1];
27
- if (maybeTemplate?.nodeName === "TEMPLATE" && maybeTemplate?.attributes.getNamedItem(DATA_HONO_TEMPLATE) !== null) {
32
+ let maybeTemplate = element.childNodes[element.childNodes.length - 1];
33
+ while (maybeTemplate?.nodeName === "TEMPLATE") {
34
+ const propKey = maybeTemplate.getAttribute(DATA_HONO_TEMPLATE);
35
+ if (propKey == null) {
36
+ break;
37
+ }
28
38
  let createChildren = options?.createChildren;
29
39
  if (!createChildren) {
30
40
  const { buildCreateChildrenFn } = await import("./runtime");
31
41
  createChildren = buildCreateChildrenFn(
32
42
  createElement,
33
- async (name) => (await FILES[`${root}${name}`]()).default
43
+ async (name) => (await FILES[`${name}`]()).default
34
44
  );
35
45
  }
36
- props.children = await createChildren(
46
+ props[propKey] = await createChildren(
37
47
  maybeTemplate.content.childNodes
38
48
  );
49
+ maybeTemplate = maybeTemplate.previousSibling;
39
50
  }
40
51
  const newElem = await createElement(Component, props);
41
52
  await hydrate(newElem, element);
@@ -1,6 +1,7 @@
1
1
  declare const COMPONENT_NAME = "component-name";
2
+ declare const COMPONENT_EXPORT = "component-export";
2
3
  declare const DATA_SERIALIZED_PROPS = "data-serialized-props";
3
4
  declare const DATA_HONO_TEMPLATE = "data-hono-template";
4
5
  declare const IMPORTING_ISLANDS_ID: "__importing_islands";
5
6
 
6
- export { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS, IMPORTING_ISLANDS_ID };
7
+ export { COMPONENT_EXPORT, COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS, IMPORTING_ISLANDS_ID };
package/dist/constants.js CHANGED
@@ -1,8 +1,10 @@
1
1
  const COMPONENT_NAME = "component-name";
2
+ const COMPONENT_EXPORT = "component-export";
2
3
  const DATA_SERIALIZED_PROPS = "data-serialized-props";
3
4
  const DATA_HONO_TEMPLATE = "data-hono-template";
4
5
  const IMPORTING_ISLANDS_ID = "__importing_islands";
5
6
  export {
7
+ COMPONENT_EXPORT,
6
8
  COMPONENT_NAME,
7
9
  DATA_HONO_TEMPLATE,
8
10
  DATA_SERIALIZED_PROPS,
@@ -1,5 +1,5 @@
1
- import { FC } from 'hono/jsx';
2
-
3
- declare const HasIslands: FC;
1
+ declare const HasIslands: ({ children }: {
2
+ children: any;
3
+ }) => any;
4
4
 
5
5
  export { HasIslands };
@@ -1,9 +1,12 @@
1
1
  import { Fragment, jsx } from "hono/jsx/jsx-runtime";
2
- import { useRequestContext } from "hono/jsx-renderer";
3
2
  import { IMPORTING_ISLANDS_ID } from "../../constants.js";
3
+ import { contextStorage } from "../context-storage.js";
4
4
  const HasIslands = ({ children }) => {
5
- const c = useRequestContext();
6
- return /* @__PURE__ */ jsx(Fragment, { children: c.get(IMPORTING_ISLANDS_ID) ? children : /* @__PURE__ */ jsx(Fragment, {}) });
5
+ const c = contextStorage.getStore();
6
+ if (!c) {
7
+ throw new Error("No context found");
8
+ }
9
+ return /* @__PURE__ */ jsx(Fragment, { children: c.get(IMPORTING_ISLANDS_ID) && children });
7
10
  };
8
11
  export {
9
12
  HasIslands
@@ -1,4 +1,3 @@
1
1
  export { HasIslands } from './has-islands.js';
2
2
  export { Script } from './script.js';
3
- import 'hono/jsx';
4
3
  import 'vite';
@@ -1,4 +1,3 @@
1
- import { FC } from 'hono/jsx';
2
1
  import { Manifest } from 'vite';
3
2
 
4
3
  type Options = {
@@ -6,7 +5,8 @@ type Options = {
6
5
  async?: boolean;
7
6
  prod?: boolean;
8
7
  manifest?: Manifest;
8
+ nonce?: string;
9
9
  };
10
- declare const Script: FC<Options>;
10
+ declare const Script: (options: Options) => any;
11
11
 
12
12
  export { Script };
@@ -1,6 +1,6 @@
1
1
  import { Fragment, jsx } from "hono/jsx/jsx-runtime";
2
2
  import { HasIslands } from "./has-islands.js";
3
- const Script = async (options) => {
3
+ const Script = (options) => {
4
4
  const src = options.src;
5
5
  if (options.prod ?? import.meta.env.PROD) {
6
6
  let manifest = options.manifest;
@@ -23,14 +23,15 @@ const Script = async (options) => {
23
23
  {
24
24
  type: "module",
25
25
  async: !!options.async,
26
- src: `/${scriptInManifest.file}`
26
+ src: `/${scriptInManifest.file}`,
27
+ nonce: options.nonce
27
28
  }
28
29
  ) });
29
30
  }
30
31
  }
31
32
  return /* @__PURE__ */ jsx(Fragment, {});
32
33
  } else {
33
- return /* @__PURE__ */ jsx("script", { type: "module", async: !!options.async, src });
34
+ return /* @__PURE__ */ jsx("script", { type: "module", async: !!options.async, src, nonce: options.nonce });
34
35
  }
35
36
  };
36
37
  export {
@@ -0,0 +1,6 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { Context } from 'hono';
3
+
4
+ declare const contextStorage: AsyncLocalStorage<Context<any, any, {}>>;
5
+
6
+ export { contextStorage };
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const contextStorage = new AsyncLocalStorage();
3
+ export {
4
+ contextStorage
5
+ };
@@ -5,5 +5,4 @@ export { Script } from './components/script.js';
5
5
  import 'hono';
6
6
  import 'hono/types';
7
7
  import '../constants.js';
8
- import 'hono/jsx';
9
8
  import 'vite';
@@ -1,12 +1,13 @@
1
1
  import { Hono } from "hono";
2
2
  import { createMiddleware } from "hono/factory";
3
3
  import { IMPORTING_ISLANDS_ID } from "../constants.js";
4
+ import { contextStorage } from "./context-storage.js";
4
5
  import {
5
6
  filePathToPath,
6
7
  groupByDirectory,
7
8
  listByDirectory,
8
9
  sortDirectoriesByDepth
9
- } from "../utils/file.js";
10
+ } from "./utils/file.js";
10
11
  const NOTFOUND_FILENAME = "_404.tsx";
11
12
  const ERROR_FILENAME = "_error.tsx";
12
13
  const METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
@@ -16,6 +17,9 @@ const createApp = (options) => {
16
17
  const getRootPath = (dir) => filePathToPath(dir.replace(rootRegExp, ""));
17
18
  const app = options.app ?? new Hono();
18
19
  const trailingSlash = options.trailingSlash ?? false;
20
+ app.use(async function ShareContext(c, next) {
21
+ await contextStorage.run(c, () => next());
22
+ });
19
23
  if (options.init) {
20
24
  options.init(app);
21
25
  }
@@ -0,0 +1,7 @@
1
+ declare const filePathToPath: (filePath: string) => string;
2
+ declare const groupByDirectory: <T = unknown>(files: Record<string, T>) => Record<string, Record<string, T>>;
3
+ declare const sortDirectoriesByDepth: <T>(directories: Record<string, T>) => Record<string, T>[];
4
+ declare const listByDirectory: <T = unknown>(files: Record<string, T>) => Record<string, string[]>;
5
+ declare const pathToDirectoryPath: (path: string) => string;
6
+
7
+ export { filePathToPath, groupByDirectory, listByDirectory, pathToDirectoryPath, sortDirectoriesByDepth };
@@ -0,0 +1,76 @@
1
+ const filePathToPath = (filePath) => {
2
+ filePath = filePath.replace(/\.tsx?$/g, "").replace(/\.mdx$/g, "").replace(/^\/?index$/, "/").replace(/\/index$/, "").replace(/\[\.{3}.+\]/, "*").replace(/\[(.+?)\]/g, ":$1");
3
+ return /^\//.test(filePath) ? filePath : "/" + filePath;
4
+ };
5
+ const groupByDirectory = (files) => {
6
+ const organizedFiles = {};
7
+ for (const [path, content] of Object.entries(files)) {
8
+ const pathParts = path.split("/");
9
+ const fileName = pathParts.pop();
10
+ const directory = pathParts.join("/");
11
+ if (!organizedFiles[directory]) {
12
+ organizedFiles[directory] = {};
13
+ }
14
+ if (fileName) {
15
+ organizedFiles[directory][fileName] = content;
16
+ }
17
+ }
18
+ for (const [directory, files2] of Object.entries(organizedFiles)) {
19
+ const sortedEntries = Object.entries(files2).sort(([keyA], [keyB]) => {
20
+ if (keyA[0] === "[" && keyB[0] !== "[") {
21
+ return 1;
22
+ }
23
+ if (keyA[0] !== "[" && keyB[0] === "[") {
24
+ return -1;
25
+ }
26
+ return keyA.localeCompare(keyB);
27
+ });
28
+ organizedFiles[directory] = Object.fromEntries(sortedEntries);
29
+ }
30
+ return organizedFiles;
31
+ };
32
+ const sortDirectoriesByDepth = (directories) => {
33
+ const sortedKeys = Object.keys(directories).sort((a, b) => {
34
+ const depthA = a.split("/").length;
35
+ const depthB = b.split("/").length;
36
+ return depthA - depthB || b.localeCompare(a);
37
+ });
38
+ return sortedKeys.map((key) => ({
39
+ [key]: directories[key]
40
+ }));
41
+ };
42
+ const listByDirectory = (files) => {
43
+ const organizedFiles = {};
44
+ for (const path of Object.keys(files)) {
45
+ const pathParts = path.split("/");
46
+ pathParts.pop();
47
+ const directory = pathParts.join("/");
48
+ if (!organizedFiles[directory]) {
49
+ organizedFiles[directory] = [];
50
+ }
51
+ if (!organizedFiles[directory].includes(path)) {
52
+ organizedFiles[directory].push(path);
53
+ }
54
+ }
55
+ const directories = Object.keys(organizedFiles).sort((a, b) => b.length - a.length);
56
+ for (const dir of directories) {
57
+ for (const subDir of directories) {
58
+ if (subDir.startsWith(dir) && subDir !== dir) {
59
+ const uniqueFiles = /* @__PURE__ */ new Set([...organizedFiles[subDir], ...organizedFiles[dir]]);
60
+ organizedFiles[subDir] = [...uniqueFiles];
61
+ }
62
+ }
63
+ }
64
+ return organizedFiles;
65
+ };
66
+ const pathToDirectoryPath = (path) => {
67
+ const dirPath = path.replace(/[^\/]+$/, "");
68
+ return dirPath;
69
+ };
70
+ export {
71
+ filePathToPath,
72
+ groupByDirectory,
73
+ listByDirectory,
74
+ pathToDirectoryPath,
75
+ sortDirectoriesByDepth
76
+ };
@@ -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
+ });
@@ -0,0 +1,8 @@
1
+ declare const HonoXIsland: ({ componentName, componentExport, Component, props, }: {
2
+ componentName: string;
3
+ componentExport: string;
4
+ Component: Function;
5
+ props: any;
6
+ }) => JSX.Element;
7
+
8
+ export { HonoXIsland };
@@ -0,0 +1,56 @@
1
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
2
+ import { createContext, useContext, isValidElement } from "hono/jsx";
3
+ import {
4
+ COMPONENT_NAME,
5
+ COMPONENT_EXPORT,
6
+ DATA_SERIALIZED_PROPS,
7
+ DATA_HONO_TEMPLATE
8
+ } from "../../constants";
9
+ const inIsland = Symbol();
10
+ const inChildren = Symbol();
11
+ const IslandContext = createContext({
12
+ [inIsland]: false,
13
+ [inChildren]: false
14
+ });
15
+ const isElementPropValue = (value) => Array.isArray(value) ? value.some(isElementPropValue) : typeof value === "object" && isValidElement(value);
16
+ const HonoXIsland = ({
17
+ componentName,
18
+ componentExport,
19
+ Component,
20
+ props
21
+ }) => {
22
+ const elementProps = {};
23
+ const restProps = {};
24
+ for (const key in props) {
25
+ const value = props[key];
26
+ if (isElementPropValue(value)) {
27
+ elementProps[key] = value;
28
+ } else {
29
+ restProps[key] = value;
30
+ }
31
+ }
32
+ const islandState = useContext(IslandContext);
33
+ return islandState[inChildren] || !islandState[inIsland] ? (
34
+ // top-level or slot content
35
+ /* @__PURE__ */ jsxs(
36
+ "honox-island",
37
+ {
38
+ ...{
39
+ [COMPONENT_NAME]: componentName,
40
+ [COMPONENT_EXPORT]: componentExport || void 0,
41
+ [DATA_SERIALIZED_PROPS]: JSON.stringify(restProps)
42
+ },
43
+ children: [
44
+ /* @__PURE__ */ jsx(IslandContext.Provider, { value: { ...islandState, [inIsland]: true }, children: /* @__PURE__ */ jsx(Component, { ...props }) }),
45
+ Object.entries(elementProps).map(([key, children]) => /* @__PURE__ */ jsx("template", { ...{ [DATA_HONO_TEMPLATE]: key }, children: /* @__PURE__ */ jsx(IslandContext.Provider, { value: { ...islandState, [inChildren]: true }, children }) }))
46
+ ]
47
+ }
48
+ )
49
+ ) : (
50
+ // nested component
51
+ /* @__PURE__ */ jsx(Component, { ...props })
52
+ );
53
+ };
54
+ export {
55
+ HonoXIsland
56
+ };
@@ -0,0 +1 @@
1
+ export { HonoXIsland } from './honox-island.js';
@@ -0,0 +1,4 @@
1
+ import { HonoXIsland } from "./honox-island.js";
2
+ export {
3
+ HonoXIsland
4
+ };
@@ -0,0 +1,7 @@
1
+ declare const HonoXIsland: ({ componentName, Component, props, }: {
2
+ componentName: string;
3
+ Component: Function;
4
+ props: any;
5
+ }) => JSX.Element;
6
+
7
+ export { HonoXIsland };