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 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 />`, `<script />`, or `<style />` element, you can use [Security Headers Middleware](https://hono.dev/middleware/builtin/secure-headers).
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
- export default createRoute(
326
- secureHeaders({
327
- contentSecurityPolicy: {
328
- scriptSrc: [NONCE],
329
- styleSrc: [NONCE],
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 `_` prefix and `island.tsx` suffix like `_componentName.island.tsx`.
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 = "2023-12-01"
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 ./dist
840
+ wrangler pages deploy
831
841
  ```
832
842
 
833
843
  ### SSG - Static Site Generation
@@ -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>;
@@ -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[-]+.(tsx|ts)"),
12
- ...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")
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.replace(root, "");
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[`${root}${name}`]()).default
43
+ async (name) => (await FILES[`${name}`]()).default
44
44
  );
45
45
  }
46
46
  props[propKey] = await createChildren(
@@ -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 = {
@@ -8,6 +7,6 @@ type Options = {
8
7
  manifest?: Manifest;
9
8
  nonce?: string;
10
9
  };
11
- declare const Script: FC<Options>;
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 = 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;
@@ -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
+ });
@@ -1,5 +1,9 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- declare function injectImportingIslands(): Promise<Plugin>;
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
- const isIslandRegex = new RegExp(/(\/islands\/|\_[a-zA-Z0-9[-]+\.island\.[tj]sx$)/);
11
- const fileRegex = new RegExp(/(routes|_renderer|_error|_404)\/.*\.[tj]sx$/);
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 (x) => await walkDependencyTree(depPath, x))
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 (!fileRegex.test(id)) {
44
+ if (!path.resolve(id).startsWith(appPath)) {
36
45
  return;
37
46
  }
38
- const hasIslandsImport = (await walkDependencyTree(id)).flat().some((x) => isIslandRegex.test(normalizePath(x)));
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
- function isComponentName(name) {
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 defaultIsIsland = (id2) => {
191
- const islandDirectoryPath = path.join(root, "app");
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
- const component = path.resolve(__dirname, "../vite/components/honox-island.tsx").replace(/\\/g, "/");
219
- it("use 'hono/jsx' by default", async () => {
220
- const plugin = islandComponents();
221
- await plugin.configResolved({ root: "root" });
222
- const res = await plugin.load(component);
223
- expect(res.code).toMatch(/'hono\/jsx'/);
224
- expect(res.code).not.toMatch(/'react'/);
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
- it("enable to specify 'react'", async () => {
227
- const plugin = islandComponents({
228
- reactApiImportSource: "react"
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,2 @@
1
+
2
+ export { }
@@ -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.16",
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.0",
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.2",
131
+ "hono": "^4.3.4",
132
132
  "np": "7.7.0",
133
133
  "prettier": "^3.1.1",
134
134
  "publint": "^0.2.7",