honox 0.1.16 → 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 +22 -12
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +5 -5
- 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 +1 -2
- package/dist/server/components/script.js +1 -1
- 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/inject-importing-islands.d.ts +5 -1
- package/dist/vite/inject-importing-islands.js +22 -8
- package/dist/vite/island-components.d.ts +4 -0
- package/dist/vite/island-components.js +5 -13
- package/dist/vite/island-components.test.js +41 -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 +3 -3
package/README.md
CHANGED
|
@@ -313,7 +313,7 @@ export default jsxRenderer(({ children }) => {
|
|
|
313
313
|
|
|
314
314
|
#### nonce Attribute
|
|
315
315
|
|
|
316
|
-
If you want to add a `nonce` attribute to `<Script
|
|
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
317
|
|
|
318
318
|
Define the middleware:
|
|
319
319
|
|
|
@@ -322,14 +322,13 @@ Define the middleware:
|
|
|
322
322
|
import { createRoute } from 'honox/factory'
|
|
323
323
|
import { secureHeaders, NONCE } from 'hono/secure-headers'
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
325
|
+
secureHeaders({
|
|
326
|
+
contentSecurityPolicy: import.meta.env.PROD
|
|
327
|
+
? {
|
|
328
|
+
scriptSrc: [NONCE],
|
|
329
|
+
}
|
|
330
|
+
: undefined,
|
|
331
|
+
})
|
|
333
332
|
```
|
|
334
333
|
|
|
335
334
|
You can get the `nonce` value with `c.get('secureHeadersNonce')`:
|
|
@@ -366,7 +365,7 @@ createClient()
|
|
|
366
365
|
|
|
367
366
|
If you want to add interactions to your page, create Island components. Islands components should be:
|
|
368
367
|
|
|
369
|
-
- Placed under `app/islands` directory or named with
|
|
368
|
+
- Placed under `app/islands` directory or named with `$` prefix like `$componentName.tsx`.
|
|
370
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.
|
|
371
370
|
|
|
372
371
|
For example, you can write an interactive component such as the following counter:
|
|
@@ -748,7 +747,8 @@ If you want to use Cloudflare's Bindings in your development environment, create
|
|
|
748
747
|
|
|
749
748
|
```toml
|
|
750
749
|
name = "my-project-name"
|
|
751
|
-
compatibility_date = "
|
|
750
|
+
compatibility_date = "2024-04-01"
|
|
751
|
+
compatibility_flags = [ "nodejs_compat" ]
|
|
752
752
|
pages_build_output_dir = "./dist"
|
|
753
753
|
|
|
754
754
|
# [vars]
|
|
@@ -783,6 +783,16 @@ Since a HonoX instance is essentially a Hono instance, it can be deployed on any
|
|
|
783
783
|
|
|
784
784
|
### Cloudflare Pages
|
|
785
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
|
+
|
|
786
796
|
Setup the `vite.config.ts`:
|
|
787
797
|
|
|
788
798
|
```ts
|
|
@@ -827,7 +837,7 @@ vite build --mode client && vite build
|
|
|
827
837
|
Deploy with the following commands after the build. Ensure you have [Wrangler](https://developers.cloudflare.com/workers/wrangler/) installed:
|
|
828
838
|
|
|
829
839
|
```txt
|
|
830
|
-
wrangler pages deploy
|
|
840
|
+
wrangler pages deploy
|
|
831
841
|
```
|
|
832
842
|
|
|
833
843
|
### SSG - Static Site Generation
|
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
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
} from "../constants.js";
|
|
9
9
|
const createClient = async (options) => {
|
|
10
10
|
const FILES = options?.ISLAND_FILES ?? {
|
|
11
|
-
...import.meta.glob("/app/islands/**/[a-zA-Z0-9
|
|
12
|
-
...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")
|
|
13
14
|
};
|
|
14
|
-
const root = options?.island_root ?? "/app";
|
|
15
15
|
const hydrateComponent = async (document2) => {
|
|
16
16
|
const filePromises = Object.keys(FILES).map(async (filePath) => {
|
|
17
|
-
const componentName = filePath
|
|
17
|
+
const componentName = filePath;
|
|
18
18
|
const elements = document2.querySelectorAll(
|
|
19
19
|
`[${COMPONENT_NAME}="${componentName}"]:not([data-hono-hydrated])`
|
|
20
20
|
);
|
|
@@ -40,7 +40,7 @@ const createClient = async (options) => {
|
|
|
40
40
|
const { buildCreateChildrenFn } = await import("./runtime");
|
|
41
41
|
createChildren = buildCreateChildrenFn(
|
|
42
42
|
createElement,
|
|
43
|
-
async (name) => (await FILES[`${
|
|
43
|
+
async (name) => (await FILES[`${name}`]()).default
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
props[propKey] = await createChildren(
|
|
@@ -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 = {
|
|
@@ -8,6 +7,6 @@ type Options = {
|
|
|
8
7
|
manifest?: Manifest;
|
|
9
8
|
nonce?: string;
|
|
10
9
|
};
|
|
11
|
-
declare const Script:
|
|
10
|
+
declare const Script: (options: Options) => any;
|
|
12
11
|
|
|
13
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;
|
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
|
+
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type InjectImportingIslandsOptions = {
|
|
4
|
+
appDir?: string;
|
|
5
|
+
islandDir?: string;
|
|
6
|
+
};
|
|
7
|
+
declare function injectImportingIslands(options?: InjectImportingIslandsOptions): Promise<Plugin>;
|
|
4
8
|
|
|
5
9
|
export { injectImportingIslands };
|
|
@@ -5,13 +5,15 @@ import { parse } from "@babel/parser";
|
|
|
5
5
|
import precinct from "precinct";
|
|
6
6
|
import { normalizePath } from "vite";
|
|
7
7
|
import { IMPORTING_ISLANDS_ID } from "../constants.js";
|
|
8
|
+
import { matchIslandComponentId } from "./utils/path.js";
|
|
8
9
|
const generate = _generate.default ?? _generate;
|
|
9
|
-
async function injectImportingIslands() {
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
async function injectImportingIslands(options) {
|
|
11
|
+
let appPath = "";
|
|
12
|
+
const islandDir = options?.islandDir ?? "/app/islands";
|
|
13
|
+
let root = "";
|
|
12
14
|
const cache = {};
|
|
13
|
-
const walkDependencyTree = async (baseFile, dependencyFile) => {
|
|
14
|
-
const depPath = dependencyFile ? path.join(path.dirname(baseFile), dependencyFile) + ".tsx" : baseFile;
|
|
15
|
+
const walkDependencyTree = async (baseFile, resolve, dependencyFile) => {
|
|
16
|
+
const depPath = dependencyFile ? typeof dependencyFile === "string" ? path.join(path.dirname(baseFile), dependencyFile) + ".tsx" : dependencyFile["id"] : baseFile;
|
|
15
17
|
const deps = [depPath];
|
|
16
18
|
try {
|
|
17
19
|
if (!cache[depPath]) {
|
|
@@ -21,7 +23,10 @@ async function injectImportingIslands() {
|
|
|
21
23
|
type: "tsx"
|
|
22
24
|
});
|
|
23
25
|
const childDeps = await Promise.all(
|
|
24
|
-
currentFileDeps.map(async (
|
|
26
|
+
currentFileDeps.map(async (file) => {
|
|
27
|
+
const resolvedId = await resolve(file, baseFile);
|
|
28
|
+
return await walkDependencyTree(depPath, resolve, resolvedId ?? file);
|
|
29
|
+
})
|
|
25
30
|
);
|
|
26
31
|
deps.push(...childDeps.flat());
|
|
27
32
|
return deps;
|
|
@@ -31,11 +36,20 @@ async function injectImportingIslands() {
|
|
|
31
36
|
};
|
|
32
37
|
return {
|
|
33
38
|
name: "inject-importing-islands",
|
|
39
|
+
configResolved: async (config) => {
|
|
40
|
+
appPath = path.join(config.root, options?.appDir ?? "/app");
|
|
41
|
+
root = config.root;
|
|
42
|
+
},
|
|
34
43
|
async transform(sourceCode, id) {
|
|
35
|
-
if (!
|
|
44
|
+
if (!path.resolve(id).startsWith(appPath)) {
|
|
36
45
|
return;
|
|
37
46
|
}
|
|
38
|
-
const hasIslandsImport = (await
|
|
47
|
+
const hasIslandsImport = (await Promise.all(
|
|
48
|
+
(await walkDependencyTree(id, async (id2) => await this.resolve(id2))).flat().map(async (x) => {
|
|
49
|
+
const rootPath = "/" + path.relative(root, normalizePath(x)).replace(/\\/g, "/");
|
|
50
|
+
return matchIslandComponentId(rootPath, islandDir);
|
|
51
|
+
})
|
|
52
|
+
)).some((matched) => matched);
|
|
39
53
|
if (!hasIslandsImport) {
|
|
40
54
|
return;
|
|
41
55
|
}
|
|
@@ -3,7 +3,11 @@ import { Plugin } from 'vite';
|
|
|
3
3
|
declare const transformJsxTags: (contents: string, componentName: string) => string | undefined;
|
|
4
4
|
type IsIsland = (id: string) => boolean;
|
|
5
5
|
type IslandComponentsOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated
|
|
8
|
+
*/
|
|
6
9
|
isIsland?: IsIsland;
|
|
10
|
+
islandDir?: string;
|
|
7
11
|
reactApiImportSource?: string;
|
|
8
12
|
};
|
|
9
13
|
declare function islandComponents(options?: IslandComponentsOptions): Plugin;
|
|
@@ -29,9 +29,7 @@ import {
|
|
|
29
29
|
exportSpecifier
|
|
30
30
|
} from "@babel/types";
|
|
31
31
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
32
|
-
|
|
33
|
-
return /^[A-Z][A-Z0-9]*[a-z][A-Za-z0-9]*$/.test(name);
|
|
34
|
-
}
|
|
32
|
+
import { matchIslandComponentId, isComponentName } from "./utils/path.js";
|
|
35
33
|
function addSSRCheck(funcName, componentName, componentExport) {
|
|
36
34
|
const isSSR = memberExpression(
|
|
37
35
|
memberExpression(identifier("import"), identifier("meta")),
|
|
@@ -158,6 +156,7 @@ const transformJsxTags = (contents, componentName) => {
|
|
|
158
156
|
function islandComponents(options) {
|
|
159
157
|
let root = "";
|
|
160
158
|
let reactApiImportSource = options?.reactApiImportSource;
|
|
159
|
+
const islandDir = options?.islandDir ?? "/app/islands";
|
|
161
160
|
return {
|
|
162
161
|
name: "transform-island-components",
|
|
163
162
|
configResolved: async (config) => {
|
|
@@ -177,7 +176,7 @@ function islandComponents(options) {
|
|
|
177
176
|
}
|
|
178
177
|
},
|
|
179
178
|
async load(id) {
|
|
180
|
-
if (/\/honox\/.*?\/vite\/components\//.test(id)) {
|
|
179
|
+
if (/\/honox\/.*?\/(?:server|vite)\/components\//.test(id)) {
|
|
181
180
|
if (!reactApiImportSource) {
|
|
182
181
|
return;
|
|
183
182
|
}
|
|
@@ -187,15 +186,8 @@ function islandComponents(options) {
|
|
|
187
186
|
map: null
|
|
188
187
|
};
|
|
189
188
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
return path.resolve(id2).startsWith(islandDirectoryPath);
|
|
193
|
-
};
|
|
194
|
-
const matchIslandPath = options?.isIsland ?? defaultIsIsland;
|
|
195
|
-
if (!matchIslandPath(id)) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const match = id.match(/(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)/);
|
|
189
|
+
const rootPath = "/" + path.relative(root, id).replace(/\\/g, "/");
|
|
190
|
+
const match = matchIslandComponentId(rootPath, islandDir);
|
|
199
191
|
if (match) {
|
|
200
192
|
const componentName = match[0];
|
|
201
193
|
const contents = await fs.readFile(id, "utf-8");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
1
3
|
import path from "path";
|
|
2
|
-
import { transformJsxTags, islandComponents } from "./island-components";
|
|
4
|
+
import { transformJsxTags, islandComponents } from "./island-components.js";
|
|
3
5
|
describe("transformJsxTags", () => {
|
|
4
6
|
it("Should add component-wrapper and component-name attribute", () => {
|
|
5
7
|
const code = `export default function Badge() {
|
|
@@ -215,22 +217,46 @@ export { utilityFn, WrappedExportViaVariable as default };`
|
|
|
215
217
|
});
|
|
216
218
|
describe("options", () => {
|
|
217
219
|
describe("reactApiImportSource", () => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
describe("vite/components", () => {
|
|
221
|
+
const component = path.resolve(__dirname, "../vite/components/honox-island.tsx").replace(/\\/g, "/");
|
|
222
|
+
it("use 'hono/jsx' by default", async () => {
|
|
223
|
+
const plugin = islandComponents();
|
|
224
|
+
await plugin.configResolved({ root: "root" });
|
|
225
|
+
const res = await plugin.load(component);
|
|
226
|
+
expect(res.code).toMatch(/'hono\/jsx'/);
|
|
227
|
+
expect(res.code).not.toMatch(/'react'/);
|
|
228
|
+
});
|
|
229
|
+
it("enable to specify 'react'", async () => {
|
|
230
|
+
const plugin = islandComponents({
|
|
231
|
+
reactApiImportSource: "react"
|
|
232
|
+
});
|
|
233
|
+
await plugin.configResolved({ root: "root" });
|
|
234
|
+
const res = await plugin.load(component);
|
|
235
|
+
expect(res.code).not.toMatch(/'hono\/jsx'/);
|
|
236
|
+
expect(res.code).toMatch(/'react'/);
|
|
237
|
+
});
|
|
225
238
|
});
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
239
|
+
describe("server/components", () => {
|
|
240
|
+
const tmpdir = os.tmpdir();
|
|
241
|
+
const component = path.resolve(tmpdir, "honox/dist/server/components/has-islands.js").replace(/\\/g, "/");
|
|
242
|
+
fs.mkdirSync(path.dirname(component), { recursive: true });
|
|
243
|
+
fs.writeFileSync(component, "import { jsx } from 'hono/jsx/jsx-runtime'");
|
|
244
|
+
it("use 'hono/jsx' by default", async () => {
|
|
245
|
+
const plugin = islandComponents();
|
|
246
|
+
await plugin.configResolved({ root: "root" });
|
|
247
|
+
const res = await plugin.load(component);
|
|
248
|
+
expect(res.code).toMatch(/'hono\/jsx\/jsx-runtime'/);
|
|
249
|
+
expect(res.code).not.toMatch(/'react\/jsx-runtime'/);
|
|
250
|
+
});
|
|
251
|
+
it("enable to specify 'react'", async () => {
|
|
252
|
+
const plugin = islandComponents({
|
|
253
|
+
reactApiImportSource: "react"
|
|
254
|
+
});
|
|
255
|
+
await plugin.configResolved({ root: "root" });
|
|
256
|
+
const res = await plugin.load(component);
|
|
257
|
+
expect(res.code).not.toMatch(/'hono\/jsx\/jsx-runtime'/);
|
|
258
|
+
expect(res.code).toMatch(/'react\/jsx-runtime'/);
|
|
229
259
|
});
|
|
230
|
-
await plugin.configResolved({ root: "root" });
|
|
231
|
-
const res = await plugin.load(component);
|
|
232
|
-
expect(res.code).not.toMatch(/'hono\/jsx'/);
|
|
233
|
-
expect(res.code).toMatch(/'react'/);
|
|
234
260
|
});
|
|
235
261
|
});
|
|
236
262
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if the name is a valid component name
|
|
3
|
+
*
|
|
4
|
+
* @param name - The name to check
|
|
5
|
+
* @returns true if the name is a valid component name
|
|
6
|
+
* @example
|
|
7
|
+
* isComponentName('Badge') // true
|
|
8
|
+
* isComponentName('BadgeComponent') // true
|
|
9
|
+
* isComponentName('badge') // false
|
|
10
|
+
* isComponentName('MIN') // false
|
|
11
|
+
* isComponentName('Badge_Component') // false
|
|
12
|
+
*/
|
|
13
|
+
declare function isComponentName(name: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Matches when id is the filename of Island component
|
|
16
|
+
*
|
|
17
|
+
* @param id - The id to match
|
|
18
|
+
* @returns The result object if id is matched or null
|
|
19
|
+
*/
|
|
20
|
+
declare function matchIslandComponentId(id: string, islandDir?: string): RegExpMatchArray | null;
|
|
21
|
+
|
|
22
|
+
export { isComponentName, matchIslandComponentId };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isComponentName(name) {
|
|
2
|
+
return /^[A-Z][A-Z0-9]*[a-z][A-Za-z0-9]*$/.test(name);
|
|
3
|
+
}
|
|
4
|
+
function matchIslandComponentId(id, islandDir = "/islands") {
|
|
5
|
+
const regExp = new RegExp(
|
|
6
|
+
`^${islandDir}/.+?.tsx$|.*/(?:_[a-zA-Z0-9-]+.island.tsx$|\\$[a-zA-Z0-9-]+.tsx$)`
|
|
7
|
+
);
|
|
8
|
+
return id.match(regExp);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
isComponentName,
|
|
12
|
+
matchIslandComponentId
|
|
13
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { matchIslandComponentId } from "./path";
|
|
2
|
+
describe("matchIslandComponentId", () => {
|
|
3
|
+
describe("match", () => {
|
|
4
|
+
const paths = [
|
|
5
|
+
"/islands/counter.tsx",
|
|
6
|
+
"/islands/directory/counter.tsx",
|
|
7
|
+
"/routes/$counter.tsx",
|
|
8
|
+
"/routes/directory/$counter.tsx",
|
|
9
|
+
"/routes/_counter.island.tsx",
|
|
10
|
+
"/routes/directory/_counter.island.tsx",
|
|
11
|
+
"/$counter.tsx",
|
|
12
|
+
"/directory/$counter.tsx",
|
|
13
|
+
"/_counter.island.tsx",
|
|
14
|
+
"/directory/_counter.island.tsx"
|
|
15
|
+
];
|
|
16
|
+
paths.forEach((path) => {
|
|
17
|
+
it(`Should match ${path}`, () => {
|
|
18
|
+
const match = matchIslandComponentId(path);
|
|
19
|
+
expect(match).not.toBeNull();
|
|
20
|
+
expect(match[0]).toBe(path);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("not match", () => {
|
|
25
|
+
const paths = [
|
|
26
|
+
"/routes/directory/component.tsx",
|
|
27
|
+
"/routes/directory/foo$component.tsx",
|
|
28
|
+
"/routes/directory/foo_component.island.tsx",
|
|
29
|
+
"/routes/directory/component.island.tsx",
|
|
30
|
+
"/directory/islands/component.tsx"
|
|
31
|
+
];
|
|
32
|
+
paths.forEach((path) => {
|
|
33
|
+
it(`Should not match ${path}`, () => {
|
|
34
|
+
const match = matchIslandComponentId(path);
|
|
35
|
+
expect(match).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("not match - with `islandDir`", () => {
|
|
40
|
+
const paths = ["/islands/component.tsx"];
|
|
41
|
+
paths.forEach((path) => {
|
|
42
|
+
it(`Should not match ${path}`, () => {
|
|
43
|
+
const match = matchIslandComponentId(path, "/directory/islands");
|
|
44
|
+
expect(match).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"@babel/parser": "^7.23.6",
|
|
113
113
|
"@babel/traverse": "^7.23.6",
|
|
114
114
|
"@babel/types": "^7.23.6",
|
|
115
|
-
"@hono/vite-dev-server": "^0.12.
|
|
115
|
+
"@hono/vite-dev-server": "^0.12.1",
|
|
116
116
|
"jsonc-parser": "^3.2.1",
|
|
117
117
|
"precinct": "^12.0.2"
|
|
118
118
|
},
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"@types/node": "^20.10.5",
|
|
129
129
|
"eslint": "^8.56.0",
|
|
130
130
|
"glob": "^10.3.10",
|
|
131
|
-
"hono": "^4.3.
|
|
131
|
+
"hono": "^4.3.4",
|
|
132
132
|
"np": "7.7.0",
|
|
133
133
|
"prettier": "^3.1.1",
|
|
134
134
|
"publint": "^0.2.7",
|