honox 0.1.4 → 0.1.6

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
@@ -1,6 +1,6 @@
1
1
  # HonoX
2
2
 
3
- **HonoX** is a simple and fast - _supersonic_ - meta framework for creating full-stack websites or Web APIs - (formerly _[Sonik](https://github.com/sonikjs/sonik)_). It stands on the shoulders of giants; built on [Hono](https://hono.dev/), [Vite](https://hono.dev/), and UI libraries.
3
+ **HonoX** is a simple and fast - _supersonic_ - meta framework for creating full-stack websites or Web APIs - (formerly _[Sonik](https://github.com/sonikjs/sonik)_). It stands on the shoulders of giants; built on [Hono](https://hono.dev/), [Vite](https://vitejs.dev/), and UI libraries.
4
4
 
5
5
  **Note**: _HonoX is currently in a "alpha stage". Breaking changes are introduced without following semantic versioning._
6
6
 
@@ -261,11 +261,12 @@ The below is the project structure of a minimal application including a client s
261
261
 
262
262
  ### Renderer
263
263
 
264
- This is a `_renderer.tsx`, which will load the `/app/client.ts` entry file for the client. It will load the JavaScript file for the production according to the variable `import.meta.env.PROD`. And renders the inside of `HasIslands` if there are islands on that page.
264
+ This is a `_renderer.tsx`, which will load the `/app/client.ts` entry file for the client. It will load the JavaScript file for the production according to the variable `import.meta.env.PROD`. And renders the inside of `<HasIslands />` if there are islands on that page.
265
265
 
266
266
  ```tsx
267
267
  // app/routes/_renderer.tsx
268
268
  import { jsxRenderer } from 'hono/jsx-renderer'
269
+ import { HasIslands } from 'honox/server'
269
270
 
270
271
  export default jsxRenderer(({ children }) => {
271
272
  return (
@@ -308,6 +309,8 @@ export default jsxRenderer(({ children }) => {
308
309
  })
309
310
  ```
310
311
 
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
+
311
314
  ### Client Entry File
312
315
 
313
316
  A client side entry file should be in `app/client.ts`. Simply, write `createClient()`.
@@ -321,7 +324,7 @@ createClient()
321
324
 
322
325
  ### Interactions
323
326
 
324
- Function components placed in `app/islands/*` are also sent to the client side. For example, you can write interactive component such as the following counter:
327
+ Function components placed in `app/islands/*` - Island components - are also sent to the client side. For example, you can write interactive component such as the following counter:
325
328
 
326
329
  ```tsx
327
330
  // app/islands/counter.tsx
@@ -355,6 +358,18 @@ export default createRoute((c) => {
355
358
  })
356
359
  ```
357
360
 
361
+ **Note**: You cannot access a Context object in Island components. Therefore, you should pass the value from components outside of Island.
362
+
363
+ ```ts
364
+ import { useRequestContext } from 'hono/jsx-renderer'
365
+ import Counter from '../islands/counter.tsx'
366
+
367
+ export default function Component() {
368
+ const c = useRequestContext()
369
+ return <Counter init={parseInt(c.req.query('count') ?? '0', 10)} />
370
+ }
371
+ ```
372
+
358
373
  ## BYOR - Bring Your Own Renderer
359
374
 
360
375
  You can bring your own renderer using a UI library like React, Preact, Solid, or others.
@@ -383,7 +398,7 @@ declare module '@hono/react-renderer' {
383
398
  }
384
399
  ```
385
400
 
386
- The following is an example of `app/routes/renderer.tsx`.
401
+ The following is an example of `app/routes/_renderer.tsx`.
387
402
 
388
403
  ```tsx
389
404
  // app/routes/_renderer.tsx
@@ -447,6 +462,51 @@ export default jsxRenderer(({ children, Layout }) => {
447
462
  })
448
463
  ```
449
464
 
465
+ #### Passing Additional Props in Nested Layouts
466
+
467
+ Props passed to nested renderers do not automatically propagate to the parent renderers. To ensure that the parent layouts receive the necessary props, you should explicitly pass them from the nested <Layout /> component. Here's how you can achieve that:
468
+
469
+ Let's start with our route handler:
470
+
471
+ ```tsx
472
+ // app/routes/nested/index.tsx
473
+ export default createRoute((c) => {
474
+ return c.render(<div>Content</div>, { title: 'Dashboard' })
475
+ })
476
+ ```
477
+
478
+ Now, let's take a look at our nested renderer:
479
+
480
+ ```tsx
481
+ // app/routes/nested/_renderer.tsx
482
+ export default jsxRenderer(({ children, Layout, title }) => {
483
+ return (
484
+ <Layout title={title}>
485
+ {/* Pass the title prop to the parent renderer */}
486
+ <main>{children}</main>
487
+ </Layout>
488
+ )
489
+ })
490
+ ```
491
+
492
+ In this setup, all the props sent to the nested renderer's <Layout /> are consumed by the parent renderer:
493
+
494
+ ```tsx
495
+ // app/routes/_renderer.tsx
496
+ export default jsxRenderer(({ children, title }) => {
497
+ return (
498
+ <html lang='en'>
499
+ <head>
500
+ <title>{title}</title> {/* Use the title prop here */}
501
+ </head>
502
+ <body>
503
+ {children} {/* Insert the Layout's children here */}
504
+ </body>
505
+ </html>
506
+ )
507
+ })
508
+ ```
509
+
450
510
  ### Using Middleware
451
511
 
452
512
  You can use Hono's Middleware in each root file with the same syntax as Hono. For example, to validate a value with the [Zod Validator](https://github.com/honojs/middleware/tree/main/packages/zod-validator), do the following:
@@ -1,4 +1,4 @@
1
- import { Hydrate, CreateElement, CreateChildren } from '../types.js';
1
+ import { Hydrate, CreateElement, CreateChildren, TriggerHydration } from '../types.js';
2
2
 
3
3
  type ClientOptions = {
4
4
  hydrate?: Hydrate;
@@ -7,6 +7,10 @@ type ClientOptions = {
7
7
  * Create "children" attribute of a component from a list of child nodes
8
8
  */
9
9
  createChildren?: CreateChildren;
10
+ /**
11
+ * Trigger hydration on your own
12
+ */
13
+ triggerHydration?: TriggerHydration;
10
14
  ISLAND_FILES?: Record<string, () => Promise<unknown>>;
11
15
  island_root?: string;
12
16
  };
@@ -4,12 +4,15 @@ import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../co
4
4
  const createClient = async (options) => {
5
5
  const FILES = options?.ISLAND_FILES ?? import.meta.glob("/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)");
6
6
  const root = options?.island_root ?? "/app/islands/";
7
- const hydrateComponent = async () => {
7
+ const hydrateComponent = async (document2) => {
8
8
  const filePromises = Object.keys(FILES).map(async (filePath) => {
9
9
  const componentName = filePath.replace(root, "");
10
- const elements = document.querySelectorAll(`[${COMPONENT_NAME}="${componentName}"]`);
10
+ const elements = document2.querySelectorAll(
11
+ `[${COMPONENT_NAME}="${componentName}"]:not([data-hono-hydrated])`
12
+ );
11
13
  if (elements) {
12
14
  const elementPromises = Array.from(elements).map(async (element) => {
15
+ element.setAttribute("data-hono-hydrated", "true");
13
16
  const fileCallback = FILES[filePath];
14
17
  const file = await fileCallback();
15
18
  const Component = await file.default;
@@ -22,7 +25,10 @@ const createClient = async (options) => {
22
25
  let createChildren = options?.createChildren;
23
26
  if (!createChildren) {
24
27
  const { buildCreateChildrenFn } = await import("./runtime");
25
- createChildren = buildCreateChildrenFn(createElement);
28
+ createChildren = buildCreateChildrenFn(
29
+ createElement,
30
+ async (name) => (await FILES[`${root}${name}`]()).default
31
+ );
26
32
  }
27
33
  props.children = await createChildren(
28
34
  maybeTemplate.content.childNodes
@@ -36,7 +42,14 @@ const createClient = async (options) => {
36
42
  });
37
43
  await Promise.all(filePromises);
38
44
  };
39
- await hydrateComponent();
45
+ const triggerHydration = options?.triggerHydration ?? (async (hydrateComponent2) => {
46
+ if (document.querySelector('template[id^="H:"], template[id^="E:"]')) {
47
+ const { hydrateComponentHonoSuspense } = await import("./runtime");
48
+ await hydrateComponentHonoSuspense(hydrateComponent2);
49
+ }
50
+ await hydrateComponent2(document);
51
+ });
52
+ await triggerHydration?.(hydrateComponent);
40
53
  };
41
54
  export {
42
55
  createClient
@@ -1,5 +1,7 @@
1
- import { CreateElement, CreateChildren } from '../types.js';
1
+ import { CreateElement, CreateChildren, HydrateComponent } from '../types.js';
2
2
 
3
- declare const buildCreateChildrenFn: (createElement: CreateElement) => CreateChildren;
3
+ type ImportComponent = (name: string) => Promise<Function | undefined>;
4
+ declare const buildCreateChildrenFn: (createElement: CreateElement, importComponent: ImportComponent) => CreateChildren;
5
+ declare const hydrateComponentHonoSuspense: (hydrateComponent: HydrateComponent) => Promise<void>;
4
6
 
5
- export { buildCreateChildrenFn };
7
+ export { buildCreateChildrenFn, hydrateComponentHonoSuspense };
@@ -1,5 +1,14 @@
1
1
  import { Suspense, use } from "hono/jsx/dom";
2
- const buildCreateChildrenFn = (createElement) => {
2
+ import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
3
+ const buildCreateChildrenFn = (createElement, importComponent) => {
4
+ const setChildrenFromTemplate = async (props, element) => {
5
+ const maybeTemplate = element.childNodes[element.childNodes.length - 1];
6
+ if (maybeTemplate?.nodeName === "TEMPLATE" && maybeTemplate?.getAttribute(DATA_HONO_TEMPLATE) !== null) {
7
+ props.children = await createChildren(
8
+ maybeTemplate.content.childNodes
9
+ );
10
+ }
11
+ };
3
12
  const createElementFromHTMLElement = async (element) => {
4
13
  const props = {
5
14
  children: await createChildren(element.childNodes)
@@ -71,13 +80,58 @@ const buildCreateChildrenFn = (createElement) => {
71
80
  })
72
81
  );
73
82
  } else {
74
- children.push(await createElementFromHTMLElement(child));
83
+ let component = void 0;
84
+ const componentName = child.getAttribute(COMPONENT_NAME);
85
+ if (componentName) {
86
+ component = await importComponent(componentName);
87
+ }
88
+ if (component) {
89
+ const props = JSON.parse(child.getAttribute(DATA_SERIALIZED_PROPS) || "{}");
90
+ await setChildrenFromTemplate(props, child);
91
+ children.push(await createElement(component, props));
92
+ } else {
93
+ children.push(await createElementFromHTMLElement(child));
94
+ }
75
95
  }
76
96
  }
77
97
  return children;
78
98
  };
79
99
  return createChildren;
80
100
  };
101
+ const hydrateComponentHonoSuspense = async (hydrateComponent) => {
102
+ const templates = /* @__PURE__ */ new Set();
103
+ const observerTargets = /* @__PURE__ */ new Set();
104
+ document.querySelectorAll('template[id^="H:"], template[id^="E:"]').forEach((template) => {
105
+ if (template.parentElement) {
106
+ templates.add(template);
107
+ observerTargets.add(template.parentElement);
108
+ }
109
+ });
110
+ if (observerTargets.size === 0) {
111
+ return;
112
+ }
113
+ const observer = new MutationObserver((mutations) => {
114
+ const targets = /* @__PURE__ */ new Set();
115
+ mutations.forEach((mutation) => {
116
+ if (mutation.target instanceof Element) {
117
+ targets.add(mutation.target);
118
+ mutation.removedNodes.forEach((node) => {
119
+ templates.delete(node);
120
+ });
121
+ }
122
+ });
123
+ targets.forEach((target) => {
124
+ hydrateComponent(target);
125
+ });
126
+ if (templates.size === 0) {
127
+ observer.disconnect();
128
+ }
129
+ });
130
+ observerTargets.forEach((target) => {
131
+ observer.observe(target, { childList: true });
132
+ });
133
+ };
81
134
  export {
82
- buildCreateChildrenFn
135
+ buildCreateChildrenFn,
136
+ hydrateComponentHonoSuspense
83
137
  };
@@ -0,0 +1,4 @@
1
+ export { createApp } from './server.js';
2
+ import 'hono/types';
3
+ import 'hono';
4
+ import '../constants.js';
@@ -0,0 +1,4 @@
1
+ import { createApp } from "./server.js";
2
+ export {
3
+ createApp
4
+ };
@@ -0,0 +1,11 @@
1
+ import { FC } from 'hono/jsx';
2
+ import { Manifest } from 'vite';
3
+
4
+ type Options = {
5
+ src: string;
6
+ prod?: boolean;
7
+ manifest?: Manifest;
8
+ };
9
+ declare const Css: FC<Options>;
10
+
11
+ export { Css };
@@ -0,0 +1,38 @@
1
+ import { Fragment, jsx } from "hono/jsx/jsx-runtime";
2
+ const Css = async (options) => {
3
+ const src = options.src;
4
+ if (options.prod ?? import.meta.env.PROD) {
5
+ let manifest = options.manifest;
6
+ if (!manifest) {
7
+ const MANIFEST = import.meta.glob("/dist/.vite/manifest.json", {
8
+ eager: true
9
+ });
10
+ for (const [, manifestFile] of Object.entries(MANIFEST)) {
11
+ if (manifestFile["default"]) {
12
+ manifest = manifestFile["default"];
13
+ break;
14
+ }
15
+ }
16
+ }
17
+ if (manifest) {
18
+ const scriptInManifest = manifest[src.replace(/^\//, "")];
19
+ if (scriptInManifest) {
20
+ const elements = [];
21
+ if (scriptInManifest.css) {
22
+ for (const css of scriptInManifest.css) {
23
+ elements.push(/* @__PURE__ */ jsx("link", { href: css, rel: "stylesheet" }));
24
+ }
25
+ }
26
+ return /* @__PURE__ */ jsx(Fragment, { children: elements.map((element) => {
27
+ return /* @__PURE__ */ jsx(Fragment, { children: element });
28
+ }) });
29
+ }
30
+ }
31
+ return /* @__PURE__ */ jsx(Fragment, {});
32
+ } else {
33
+ return /* @__PURE__ */ jsx("link", { href: src, rel: "stylesheet" });
34
+ }
35
+ };
36
+ export {
37
+ Css
38
+ };
@@ -1,8 +1,9 @@
1
- export { ServerOptions, createApp } from './server.js';
1
+ export { createApp } from './with-defaults.js';
2
+ export { ServerOptions } from './server.js';
2
3
  export { HasIslands } from './components/has-islands.js';
3
4
  export { Script } from './components/script.js';
4
- import 'hono/types';
5
5
  import 'hono';
6
+ import 'hono/types';
6
7
  import '../constants.js';
7
8
  import 'hono/jsx';
8
9
  import 'vite';
@@ -1,4 +1,4 @@
1
- import { createApp } from "./server.js";
1
+ import { createApp } from "./with-defaults.js";
2
2
  export * from "./components/index.js";
3
3
  export {
4
4
  createApp
@@ -4,6 +4,9 @@ import { Env, Hono, MiddlewareHandler, NotFoundHandler, ErrorHandler } from 'hon
4
4
  import { IMPORTING_ISLANDS_ID } from '../constants.js';
5
5
 
6
6
  declare const METHODS: readonly ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
7
+ type AppFile = {
8
+ default: Hono;
9
+ };
7
10
  type InnerMeta = {
8
11
  [key in typeof IMPORTING_ISLANDS_ID]?: boolean;
9
12
  };
@@ -25,16 +28,17 @@ type MiddlewareFile = {
25
28
  default: MiddlewareHandler[];
26
29
  };
27
30
  type InitFunction<E extends Env = Env> = (app: Hono<E>) => void;
28
- type ServerOptions<E extends Env = Env> = {
29
- ROUTES?: Record<string, RouteFile>;
30
- RENDERER?: Record<string, RendererFile>;
31
- NOT_FOUND?: Record<string, NotFoundFile>;
32
- ERROR?: Record<string, ErrorFile>;
33
- MIDDLEWARE?: Record<string, MiddlewareFile>;
34
- root?: string;
31
+ type BaseServerOptions<E extends Env = Env> = {
32
+ ROUTES: Record<string, RouteFile | AppFile>;
33
+ RENDERER: Record<string, RendererFile>;
34
+ NOT_FOUND: Record<string, NotFoundFile>;
35
+ ERROR: Record<string, ErrorFile>;
36
+ MIDDLEWARE: Record<string, MiddlewareFile>;
37
+ root: string;
35
38
  app?: Hono<E>;
36
39
  init?: InitFunction<E>;
37
40
  };
38
- declare const createApp: <E extends Env>(options?: ServerOptions<E> | undefined) => Hono<E, hono_types.BlankSchema, "/">;
41
+ type ServerOptions<E extends Env = Env> = Partial<BaseServerOptions<E>>;
42
+ declare const createApp: <E extends Env>(options: BaseServerOptions<E>) => Hono<E, hono_types.BlankSchema, "/">;
39
43
 
40
44
  export { type ServerOptions, createApp };
@@ -11,31 +11,21 @@ const NOTFOUND_FILENAME = "_404.tsx";
11
11
  const ERROR_FILENAME = "_error.tsx";
12
12
  const METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
13
13
  const createApp = (options) => {
14
- const root = options?.root ?? "/app/routes";
14
+ const root = options.root;
15
15
  const rootRegExp = new RegExp(`^${root}`);
16
- const app = options?.app ?? new Hono();
17
- if (options?.init) {
16
+ const app = options.app ?? new Hono();
17
+ if (options.init) {
18
18
  options.init(app);
19
19
  }
20
- const NOT_FOUND_FILE = options?.NOT_FOUND ?? import.meta.glob("/app/routes/**/_404.(ts|tsx)", {
21
- eager: true
22
- });
20
+ const NOT_FOUND_FILE = options.NOT_FOUND;
23
21
  const notFoundMap = groupByDirectory(NOT_FOUND_FILE);
24
- const ERROR_FILE = options?.ERROR ?? import.meta.glob("/app/routes/**/_error.(ts|tsx)", {
25
- eager: true
26
- });
22
+ const ERROR_FILE = options.ERROR;
27
23
  const errorMap = groupByDirectory(ERROR_FILE);
28
- const RENDERER_FILE = options?.RENDERER ?? import.meta.glob("/app/routes/**/_renderer.tsx", {
29
- eager: true
30
- });
24
+ const RENDERER_FILE = options.RENDERER;
31
25
  const rendererList = listByDirectory(RENDERER_FILE);
32
- const MIDDLEWARE_FILE = options?.MIDDLEWARE ?? import.meta.glob("/app/routes/**/_middleware.(ts|tsx)", {
33
- eager: true
34
- });
26
+ const MIDDLEWARE_FILE = options.MIDDLEWARE;
35
27
  const middlewareList = listByDirectory(MIDDLEWARE_FILE);
36
- const ROUTES_FILE = options?.ROUTES ?? import.meta.glob("/app/routes/**/[!_]*.(ts|tsx|mdx)", {
37
- eager: true
38
- });
28
+ const ROUTES_FILE = options.ROUTES;
39
29
  const routesMap = sortDirectoriesByDepth(groupByDirectory(ROUTES_FILE));
40
30
  const getPaths = (currentDirectory, fileList) => {
41
31
  let paths = fileList[currentDirectory] ?? [];
@@ -0,0 +1,37 @@
1
+ import * as hono from 'hono';
2
+ import { Env } from 'hono';
3
+ import * as hono_types from 'hono/types';
4
+
5
+ declare const createApp: <E extends Env>(options?: Partial<{
6
+ ROUTES: Record<string, ({
7
+ default?: Function | undefined;
8
+ } & {
9
+ GET?: hono_types.H[] | undefined;
10
+ POST?: hono_types.H[] | undefined;
11
+ PUT?: hono_types.H[] | undefined;
12
+ DELETE?: hono_types.H[] | undefined;
13
+ OPTIONS?: hono_types.H[] | undefined;
14
+ PATCH?: hono_types.H[] | undefined;
15
+ } & {
16
+ __importing_islands?: boolean | undefined;
17
+ }) | {
18
+ default: hono.Hono<Env, hono_types.BlankSchema, "/">;
19
+ }>;
20
+ RENDERER: Record<string, {
21
+ default: hono.MiddlewareHandler;
22
+ }>;
23
+ NOT_FOUND: Record<string, {
24
+ default: hono.NotFoundHandler;
25
+ }>;
26
+ ERROR: Record<string, {
27
+ default: hono.ErrorHandler;
28
+ }>;
29
+ MIDDLEWARE: Record<string, {
30
+ default: hono.MiddlewareHandler[];
31
+ }>;
32
+ root: string;
33
+ app?: hono.Hono<E, hono_types.BlankSchema, "/"> | undefined;
34
+ init?: ((app: hono.Hono<E, hono_types.BlankSchema, "/">) => void) | undefined;
35
+ }> | undefined) => hono.Hono<E, hono_types.BlankSchema, "/">;
36
+
37
+ export { createApp };
@@ -0,0 +1,27 @@
1
+ import { createApp as baseCreateApp } from "./server.js";
2
+ const createApp = (options) => {
3
+ const newOptions = {
4
+ root: options?.root ?? "/app/routes",
5
+ app: options?.app,
6
+ init: options?.init,
7
+ NOT_FOUND: options?.NOT_FOUND ?? import.meta.glob("/app/routes/**/_404.(ts|tsx)", {
8
+ eager: true
9
+ }),
10
+ ERROR: options?.ERROR ?? import.meta.glob("/app/routes/**/_error.(ts|tsx)", {
11
+ eager: true
12
+ }),
13
+ RENDERER: options?.RENDERER ?? import.meta.glob("/app/routes/**/_renderer.tsx", {
14
+ eager: true
15
+ }),
16
+ MIDDLEWARE: options?.MIDDLEWARE ?? import.meta.glob("/app/routes/**/_middleware.(ts|tsx)", {
17
+ eager: true
18
+ }),
19
+ ROUTES: options?.ROUTES ?? import.meta.glob("/app/routes/**/[!_]*.(ts|tsx|mdx)", {
20
+ eager: true
21
+ })
22
+ };
23
+ return baseCreateApp(newOptions);
24
+ };
25
+ export {
26
+ createApp
27
+ };
package/dist/types.d.ts CHANGED
@@ -2,5 +2,9 @@
2
2
  type CreateElement = (type: any, props: any) => Node | Promise<Node>;
3
3
  type Hydrate = (children: Node, parent: Element) => void | Promise<void>;
4
4
  type CreateChildren = (childNodes: NodeListOf<ChildNode>) => Node[] | Promise<Node[]>;
5
+ type HydrateComponent = (doc: {
6
+ querySelectorAll: typeof document.querySelectorAll;
7
+ }) => Promise<void>;
8
+ type TriggerHydration = (trigger: HydrateComponent) => void;
5
9
 
6
- export type { CreateChildren, CreateElement, Hydrate };
10
+ export type { CreateChildren, CreateElement, Hydrate, HydrateComponent, TriggerHydration };
@@ -1,12 +1,14 @@
1
1
  import { DevServerOptions } from '@hono/vite-dev-server';
2
2
  export { defaultOptions as devServerDefaultOptions } from '@hono/vite-dev-server';
3
3
  import { PluginOption } from 'vite';
4
+ import { IslandComponentsOptions } from './island-components.js';
4
5
  export { islandComponents } from './island-components.js';
5
6
 
6
7
  type Options = {
7
8
  islands?: boolean;
8
9
  entry?: string;
9
10
  devServer?: DevServerOptions;
11
+ islandComponents?: IslandComponentsOptions;
10
12
  external?: string[];
11
13
  };
12
14
  declare const defaultOptions: Options;
@@ -23,7 +23,7 @@ function honox(options) {
23
23
  })
24
24
  );
25
25
  if (options?.islands !== false) {
26
- plugins.push(islandComponents());
26
+ plugins.push(islandComponents(options?.islandComponents));
27
27
  }
28
28
  plugins.push(injectImportingIslands());
29
29
  plugins.push(restartOnAddUnlink());
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- declare function injectImportingIslands(): Plugin;
3
+ declare function injectImportingIslands(): Promise<Plugin>;
4
4
 
5
5
  export { injectImportingIslands };
@@ -1,49 +1,68 @@
1
+ import { readFile } from "fs/promises";
2
+ import path from "path";
1
3
  import _generate from "@babel/generator";
2
4
  import { parse } from "@babel/parser";
3
- import _traverse from "@babel/traverse";
5
+ import precinct from "precinct";
6
+ import { normalizePath } from "vite";
4
7
  import { IMPORTING_ISLANDS_ID } from "../constants.js";
5
- const traverse = _traverse.default ?? _traverse;
6
8
  const generate = _generate.default ?? _generate;
7
- function injectImportingIslands() {
9
+ async function injectImportingIslands() {
10
+ const isIslandRegex = new RegExp(/\/islands\//);
11
+ const routesRegex = new RegExp(/routes\/.*\.[t|j]sx$/);
12
+ const cache = {};
13
+ const walkDependencyTree = async (baseFile, dependencyFile) => {
14
+ const depPath = dependencyFile ? path.join(path.dirname(baseFile), dependencyFile) + ".tsx" : baseFile;
15
+ const deps = [depPath];
16
+ try {
17
+ if (!cache[depPath]) {
18
+ cache[depPath] = (await readFile(depPath, { flag: "" })).toString();
19
+ }
20
+ const currentFileDeps = precinct(cache[depPath], {
21
+ type: "tsx"
22
+ });
23
+ const childDeps = await Promise.all(
24
+ currentFileDeps.map(async (x) => await walkDependencyTree(depPath, x))
25
+ );
26
+ deps.push(...childDeps.flat());
27
+ return deps;
28
+ } catch (err) {
29
+ return deps;
30
+ }
31
+ };
8
32
  return {
9
33
  name: "inject-importing-islands",
10
- transform(code, id) {
11
- if (id.endsWith(".tsx") || id.endsWith(".jsx")) {
12
- let hasIslandsImport = false;
13
- const ast = parse(code, {
14
- sourceType: "module",
15
- plugins: ["jsx"]
16
- });
17
- traverse(ast, {
18
- ImportDeclaration(path) {
19
- if (path.node.source.value.includes("islands/")) {
20
- hasIslandsImport = true;
21
- }
22
- }
23
- });
24
- if (hasIslandsImport) {
25
- const hasIslandsNode = {
26
- type: "ExportNamedDeclaration",
27
- declaration: {
28
- type: "VariableDeclaration",
29
- declarations: [
30
- {
31
- type: "VariableDeclarator",
32
- id: { type: "Identifier", name: IMPORTING_ISLANDS_ID },
33
- init: { type: "BooleanLiteral", value: true }
34
- }
35
- ],
36
- kind: "const"
34
+ async transform(sourceCode, id) {
35
+ if (!routesRegex.test(id)) {
36
+ return;
37
+ }
38
+ const hasIslandsImport = (await walkDependencyTree(id)).flat().some((x) => isIslandRegex.test(normalizePath(x)));
39
+ if (!hasIslandsImport) {
40
+ return;
41
+ }
42
+ const ast = parse(sourceCode, {
43
+ sourceType: "module",
44
+ plugins: ["jsx", "typescript"]
45
+ });
46
+ const hasIslandsNode = {
47
+ type: "ExportNamedDeclaration",
48
+ declaration: {
49
+ type: "VariableDeclaration",
50
+ declarations: [
51
+ {
52
+ type: "VariableDeclarator",
53
+ id: { type: "Identifier", name: IMPORTING_ISLANDS_ID },
54
+ init: { type: "BooleanLiteral", value: true }
37
55
  }
38
- };
39
- ast.program.body.push(hasIslandsNode);
56
+ ],
57
+ kind: "const"
40
58
  }
41
- const output = generate(ast, {}, code);
42
- return {
43
- code: output.code,
44
- map: output.map
45
- };
46
- }
59
+ };
60
+ ast.program.body.push(hasIslandsNode);
61
+ const output = generate(ast, {}, sourceCode);
62
+ return {
63
+ code: output.code,
64
+ map: output.map
65
+ };
47
66
  }
48
67
  };
49
68
  }
@@ -1,6 +1,10 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  declare const transformJsxTags: (contents: string, componentName: string) => string | undefined;
4
- declare function islandComponents(): Plugin;
4
+ type IsIsland = (id: string) => boolean;
5
+ type IslandComponentsOptions = {
6
+ isIsland: IsIsland;
7
+ };
8
+ declare function islandComponents(options?: IslandComponentsOptions): Plugin;
5
9
 
6
- export { islandComponents, transformJsxTags };
10
+ export { type IslandComponentsOptions, islandComponents, transformJsxTags };
@@ -1,4 +1,5 @@
1
1
  import fs from "fs/promises";
2
+ import path from "path";
2
3
  import _generate from "@babel/generator";
3
4
  const generate = _generate.default ?? _generate;
4
5
  import { parse } from "@babel/parser";
@@ -25,7 +26,7 @@ import {
25
26
  memberExpression
26
27
  } from "@babel/types";
27
28
  import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
28
- function addSSRCheck(funcName, componentName, isAsync = false) {
29
+ function addSSRCheck(funcName, componentName) {
29
30
  const isSSR = memberExpression(
30
31
  memberExpression(identifier("import"), identifier("meta")),
31
32
  identifier("env.SSR")
@@ -86,11 +87,7 @@ function addSSRCheck(funcName, componentName, isAsync = false) {
86
87
  []
87
88
  );
88
89
  const returnStmt = returnStatement(conditionalExpression(isSSR, ssrElement, clientElement));
89
- const functionExpr = functionExpression(null, [identifier("props")], blockStatement([returnStmt]));
90
- if (isAsync) {
91
- functionExpr.async = true;
92
- }
93
- return functionExpr;
90
+ return functionExpression(null, [identifier("props")], blockStatement([returnStmt]));
94
91
  }
95
92
  const transformJsxTags = (contents, componentName) => {
96
93
  const ast = parse(contents, {
@@ -98,43 +95,78 @@ const transformJsxTags = (contents, componentName) => {
98
95
  plugins: ["typescript", "jsx"]
99
96
  });
100
97
  if (ast) {
98
+ let wrappedFunctionId;
101
99
  traverse(ast, {
102
- ExportDefaultDeclaration(path) {
103
- if (path.node.declaration.type === "FunctionDeclaration") {
104
- const functionId = path.node.declaration.id;
105
- if (!functionId) {
106
- return;
100
+ ExportNamedDeclaration(path2) {
101
+ for (const specifier of path2.node.specifiers) {
102
+ if (specifier.type !== "ExportSpecifier") {
103
+ continue;
107
104
  }
108
- const isAsync = path.node.declaration.async;
109
- const originalFunctionId = identifier(functionId.name + "Original");
110
- const originalFunction = functionExpression(
111
- null,
112
- path.node.declaration.params,
113
- path.node.declaration.body
114
- );
115
- if (isAsync) {
116
- originalFunction.async = true;
105
+ const exportAs = specifier.exported.type === "StringLiteral" ? specifier.exported.value : specifier.exported.name;
106
+ if (exportAs !== "default") {
107
+ continue;
117
108
  }
118
- path.insertBefore(
119
- variableDeclaration("const", [variableDeclarator(originalFunctionId, originalFunction)])
109
+ const wrappedFunction = addSSRCheck(specifier.local.name, componentName);
110
+ const wrappedFunctionId2 = identifier("Wrapped" + specifier.local.name);
111
+ path2.insertBefore(
112
+ variableDeclaration("const", [variableDeclarator(wrappedFunctionId2, wrappedFunction)])
120
113
  );
121
- const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName, isAsync);
122
- const wrappedFunctionId = identifier("Wrapped" + functionId.name);
123
- path.replaceWith(
114
+ specifier.local.name = wrappedFunctionId2.name;
115
+ }
116
+ },
117
+ ExportDefaultDeclaration(path2) {
118
+ const declarationType = path2.node.declaration.type;
119
+ if (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression" || declarationType === "ArrowFunctionExpression" || declarationType === "Identifier") {
120
+ const functionName = (declarationType === "Identifier" ? path2.node.declaration.name : (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression") && path2.node.declaration.id?.name) || "__HonoIsladComponent__";
121
+ let originalFunctionId;
122
+ if (declarationType === "Identifier") {
123
+ originalFunctionId = path2.node.declaration;
124
+ } else {
125
+ originalFunctionId = identifier(functionName + "Original");
126
+ const originalFunction = path2.node.declaration.type === "FunctionExpression" || path2.node.declaration.type === "ArrowFunctionExpression" ? path2.node.declaration : functionExpression(
127
+ null,
128
+ path2.node.declaration.params,
129
+ path2.node.declaration.body,
130
+ void 0,
131
+ path2.node.declaration.async
132
+ );
133
+ path2.insertBefore(
134
+ variableDeclaration("const", [
135
+ variableDeclarator(originalFunctionId, originalFunction)
136
+ ])
137
+ );
138
+ }
139
+ const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName);
140
+ wrappedFunctionId = identifier("Wrapped" + functionName);
141
+ path2.replaceWith(
124
142
  variableDeclaration("const", [variableDeclarator(wrappedFunctionId, wrappedFunction)])
125
143
  );
126
- path.insertAfter(exportDefaultDeclaration(wrappedFunctionId));
127
144
  }
128
145
  }
129
146
  });
147
+ if (wrappedFunctionId) {
148
+ ast.program.body.push(exportDefaultDeclaration(wrappedFunctionId));
149
+ }
130
150
  const { code } = generate(ast);
131
151
  return code;
132
152
  }
133
153
  };
134
- function islandComponents() {
154
+ function islandComponents(options) {
155
+ let root = "";
135
156
  return {
136
157
  name: "transform-island-components",
158
+ configResolved: (config) => {
159
+ root = config.root;
160
+ },
137
161
  async load(id) {
162
+ const defaultIsIsland = (id2) => {
163
+ const islandDirectoryPath = path.join(root, "app/islands");
164
+ return id2.startsWith(islandDirectoryPath);
165
+ };
166
+ const matchIslandPath = options?.isIsland ?? defaultIsIsland;
167
+ if (!matchIslandPath(id)) {
168
+ return;
169
+ }
138
170
  const match = id.match(/\/islands\/(.+?\.tsx)$/);
139
171
  if (match) {
140
172
  const componentName = match[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honox",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -41,6 +41,10 @@
41
41
  "types": "./dist/server/index.d.ts",
42
42
  "import": "./dist/server/index.js"
43
43
  },
44
+ "./server/base": {
45
+ "types": "./dist/server/base.d.ts",
46
+ "import": "./dist/server/base.js"
47
+ },
44
48
  "./client": {
45
49
  "types": "./dist/client/index.d.ts",
46
50
  "import": "./dist/client/index.js"
@@ -69,6 +73,9 @@
69
73
  "server": [
70
74
  "./dist/server"
71
75
  ],
76
+ "server/base": [
77
+ "./dist/server/base"
78
+ ],
72
79
  "client": [
73
80
  "./dist/client"
74
81
  ],
@@ -99,7 +106,8 @@
99
106
  "@babel/parser": "^7.23.6",
100
107
  "@babel/traverse": "^7.23.6",
101
108
  "@babel/types": "^7.23.6",
102
- "@hono/vite-dev-server": "^0.7.0"
109
+ "@hono/vite-dev-server": "^0.8.1",
110
+ "precinct": "^11.0.5"
103
111
  },
104
112
  "peerDependencies": {
105
113
  "hono": ">=4.*"
@@ -107,7 +115,7 @@
107
115
  "devDependencies": {
108
116
  "@hono/eslint-config": "^0.0.4",
109
117
  "@mdx-js/rollup": "^3.0.0",
110
- "@playwright/test": "^1.41.0",
118
+ "@playwright/test": "^1.42.0",
111
119
  "@types/babel__generator": "^7",
112
120
  "@types/babel__traverse": "^7",
113
121
  "@types/node": "^20.10.5",