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.
- package/README.md +60 -10
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +21 -10
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +2 -0
- package/dist/server/components/has-islands.d.ts +3 -3
- package/dist/server/components/has-islands.js +6 -3
- package/dist/server/components/index.d.ts +0 -1
- package/dist/server/components/script.d.ts +2 -2
- package/dist/server/components/script.js +4 -3
- package/dist/server/context-storage.d.ts +6 -0
- package/dist/server/context-storage.js +5 -0
- package/dist/server/index.d.ts +0 -1
- package/dist/server/server.js +5 -1
- package/dist/server/utils/file.d.ts +7 -0
- package/dist/server/utils/file.js +76 -0
- package/dist/server/utils/file.test.d.ts +2 -0
- package/dist/server/utils/file.test.js +123 -0
- package/dist/vite/components/honox-island.d.ts +8 -0
- package/dist/vite/components/honox-island.js +56 -0
- package/dist/vite/components/index.d.ts +1 -0
- package/dist/vite/components/index.js +4 -0
- package/dist/vite/components.d.ts +7 -0
- package/dist/vite/components.js +36 -0
- package/dist/vite/inject-importing-islands.d.ts +5 -1
- package/dist/vite/inject-importing-islands.js +22 -8
- package/dist/vite/island-components.d.ts +6 -1
- package/dist/vite/island-components.js +90 -89
- package/dist/vite/island-components.test.js +175 -15
- package/dist/vite/utils/path.d.ts +22 -0
- package/dist/vite/utils/path.js +13 -0
- package/dist/vite/utils/path.test.d.ts +2 -0
- package/dist/vite/utils/path.test.js +48 -0
- 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
|
|
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
|
|
330
|
-
-
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
861
|
+
If you want to include client-side scripts and assets:
|
|
812
862
|
|
|
813
863
|
```ts
|
|
814
864
|
// vite.config.ts
|
package/dist/client/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/client/client.js
CHANGED
|
@@ -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 {
|
|
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
|
|
7
|
-
...import.meta.glob("/app
|
|
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
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
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[`${
|
|
43
|
+
async (name) => (await FILES[`${name}`]()).default
|
|
34
44
|
);
|
|
35
45
|
}
|
|
36
|
-
props
|
|
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);
|
package/dist/constants.d.ts
CHANGED
|
@@ -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,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 =
|
|
6
|
-
|
|
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
|
-
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:
|
|
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 =
|
|
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 {
|
package/dist/server/index.d.ts
CHANGED
package/dist/server/server.js
CHANGED
|
@@ -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 "
|
|
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,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,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';
|