honox 0.1.4 → 0.1.5

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
@@ -383,7 +383,7 @@ declare module '@hono/react-renderer' {
383
383
  }
384
384
  ```
385
385
 
386
- The following is an example of `app/routes/renderer.tsx`.
386
+ The following is an example of `app/routes/_renderer.tsx`.
387
387
 
388
388
  ```tsx
389
389
  // app/routes/_renderer.tsx
@@ -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
+ };
@@ -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 };
@@ -25,7 +25,7 @@ import {
25
25
  memberExpression
26
26
  } from "@babel/types";
27
27
  import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from "../constants.js";
28
- function addSSRCheck(funcName, componentName, isAsync = false) {
28
+ function addSSRCheck(funcName, componentName) {
29
29
  const isSSR = memberExpression(
30
30
  memberExpression(identifier("import"), identifier("meta")),
31
31
  identifier("env.SSR")
@@ -86,11 +86,7 @@ function addSSRCheck(funcName, componentName, isAsync = false) {
86
86
  []
87
87
  );
88
88
  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;
89
+ return functionExpression(null, [identifier("props")], blockStatement([returnStmt]));
94
90
  }
95
91
  const transformJsxTags = (contents, componentName) => {
96
92
  const ast = parse(contents, {
@@ -98,35 +94,58 @@ const transformJsxTags = (contents, componentName) => {
98
94
  plugins: ["typescript", "jsx"]
99
95
  });
100
96
  if (ast) {
97
+ let wrappedFunctionId;
101
98
  traverse(ast, {
102
- ExportDefaultDeclaration(path) {
103
- if (path.node.declaration.type === "FunctionDeclaration") {
104
- const functionId = path.node.declaration.id;
105
- if (!functionId) {
106
- return;
99
+ ExportNamedDeclaration(path) {
100
+ for (const specifier of path.node.specifiers) {
101
+ if (specifier.type !== "ExportSpecifier") {
102
+ continue;
107
103
  }
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;
104
+ const exportAs = specifier.exported.type === "StringLiteral" ? specifier.exported.value : specifier.exported.name;
105
+ if (exportAs !== "default") {
106
+ continue;
117
107
  }
108
+ const wrappedFunction = addSSRCheck(specifier.local.name, componentName);
109
+ const wrappedFunctionId2 = identifier("Wrapped" + specifier.local.name);
118
110
  path.insertBefore(
119
- variableDeclaration("const", [variableDeclarator(originalFunctionId, originalFunction)])
111
+ variableDeclaration("const", [variableDeclarator(wrappedFunctionId2, wrappedFunction)])
120
112
  );
121
- const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName, isAsync);
122
- const wrappedFunctionId = identifier("Wrapped" + functionId.name);
113
+ specifier.local.name = wrappedFunctionId2.name;
114
+ }
115
+ },
116
+ ExportDefaultDeclaration(path) {
117
+ const declarationType = path.node.declaration.type;
118
+ if (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression" || declarationType === "ArrowFunctionExpression" || declarationType === "Identifier") {
119
+ const functionName = (declarationType === "Identifier" ? path.node.declaration.name : (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression") && path.node.declaration.id?.name) || "__HonoIsladComponent__";
120
+ let originalFunctionId;
121
+ if (declarationType === "Identifier") {
122
+ originalFunctionId = path.node.declaration;
123
+ } else {
124
+ originalFunctionId = identifier(functionName + "Original");
125
+ const originalFunction = path.node.declaration.type === "FunctionExpression" || path.node.declaration.type === "ArrowFunctionExpression" ? path.node.declaration : functionExpression(
126
+ null,
127
+ path.node.declaration.params,
128
+ path.node.declaration.body,
129
+ void 0,
130
+ path.node.declaration.async
131
+ );
132
+ path.insertBefore(
133
+ variableDeclaration("const", [
134
+ variableDeclarator(originalFunctionId, originalFunction)
135
+ ])
136
+ );
137
+ }
138
+ const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName);
139
+ wrappedFunctionId = identifier("Wrapped" + functionName);
123
140
  path.replaceWith(
124
141
  variableDeclaration("const", [variableDeclarator(wrappedFunctionId, wrappedFunction)])
125
142
  );
126
- path.insertAfter(exportDefaultDeclaration(wrappedFunctionId));
127
143
  }
128
144
  }
129
145
  });
146
+ if (wrappedFunctionId) {
147
+ ast.program.body.push(exportDefaultDeclaration(wrappedFunctionId));
148
+ }
130
149
  const { code } = generate(ast);
131
150
  return code;
132
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honox",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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,7 @@
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.7.1"
103
110
  },
104
111
  "peerDependencies": {
105
112
  "hono": ">=4.*"