honox 0.1.5 → 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.
@@ -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:
@@ -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,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";
@@ -96,8 +97,8 @@ const transformJsxTags = (contents, componentName) => {
96
97
  if (ast) {
97
98
  let wrappedFunctionId;
98
99
  traverse(ast, {
99
- ExportNamedDeclaration(path) {
100
- for (const specifier of path.node.specifiers) {
100
+ ExportNamedDeclaration(path2) {
101
+ for (const specifier of path2.node.specifiers) {
101
102
  if (specifier.type !== "ExportSpecifier") {
102
103
  continue;
103
104
  }
@@ -107,29 +108,29 @@ const transformJsxTags = (contents, componentName) => {
107
108
  }
108
109
  const wrappedFunction = addSSRCheck(specifier.local.name, componentName);
109
110
  const wrappedFunctionId2 = identifier("Wrapped" + specifier.local.name);
110
- path.insertBefore(
111
+ path2.insertBefore(
111
112
  variableDeclaration("const", [variableDeclarator(wrappedFunctionId2, wrappedFunction)])
112
113
  );
113
114
  specifier.local.name = wrappedFunctionId2.name;
114
115
  }
115
116
  },
116
- ExportDefaultDeclaration(path) {
117
- const declarationType = path.node.declaration.type;
117
+ ExportDefaultDeclaration(path2) {
118
+ const declarationType = path2.node.declaration.type;
118
119
  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
+ const functionName = (declarationType === "Identifier" ? path2.node.declaration.name : (declarationType === "FunctionDeclaration" || declarationType === "FunctionExpression") && path2.node.declaration.id?.name) || "__HonoIsladComponent__";
120
121
  let originalFunctionId;
121
122
  if (declarationType === "Identifier") {
122
- originalFunctionId = path.node.declaration;
123
+ originalFunctionId = path2.node.declaration;
123
124
  } else {
124
125
  originalFunctionId = identifier(functionName + "Original");
125
- const originalFunction = path.node.declaration.type === "FunctionExpression" || path.node.declaration.type === "ArrowFunctionExpression" ? path.node.declaration : functionExpression(
126
+ const originalFunction = path2.node.declaration.type === "FunctionExpression" || path2.node.declaration.type === "ArrowFunctionExpression" ? path2.node.declaration : functionExpression(
126
127
  null,
127
- path.node.declaration.params,
128
- path.node.declaration.body,
128
+ path2.node.declaration.params,
129
+ path2.node.declaration.body,
129
130
  void 0,
130
- path.node.declaration.async
131
+ path2.node.declaration.async
131
132
  );
132
- path.insertBefore(
133
+ path2.insertBefore(
133
134
  variableDeclaration("const", [
134
135
  variableDeclarator(originalFunctionId, originalFunction)
135
136
  ])
@@ -137,7 +138,7 @@ const transformJsxTags = (contents, componentName) => {
137
138
  }
138
139
  const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName);
139
140
  wrappedFunctionId = identifier("Wrapped" + functionName);
140
- path.replaceWith(
141
+ path2.replaceWith(
141
142
  variableDeclaration("const", [variableDeclarator(wrappedFunctionId, wrappedFunction)])
142
143
  );
143
144
  }
@@ -150,10 +151,22 @@ const transformJsxTags = (contents, componentName) => {
150
151
  return code;
151
152
  }
152
153
  };
153
- function islandComponents() {
154
+ function islandComponents(options) {
155
+ let root = "";
154
156
  return {
155
157
  name: "transform-island-components",
158
+ configResolved: (config) => {
159
+ root = config.root;
160
+ },
156
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
+ }
157
170
  const match = id.match(/\/islands\/(.+?\.tsx)$/);
158
171
  if (match) {
159
172
  const componentName = match[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honox",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -106,7 +106,8 @@
106
106
  "@babel/parser": "^7.23.6",
107
107
  "@babel/traverse": "^7.23.6",
108
108
  "@babel/types": "^7.23.6",
109
- "@hono/vite-dev-server": "^0.7.1"
109
+ "@hono/vite-dev-server": "^0.8.1",
110
+ "precinct": "^11.0.5"
110
111
  },
111
112
  "peerDependencies": {
112
113
  "hono": ">=4.*"
@@ -114,7 +115,7 @@
114
115
  "devDependencies": {
115
116
  "@hono/eslint-config": "^0.0.4",
116
117
  "@mdx-js/rollup": "^3.0.0",
117
- "@playwright/test": "^1.41.0",
118
+ "@playwright/test": "^1.42.0",
118
119
  "@types/babel__generator": "^7",
119
120
  "@types/babel__traverse": "^7",
120
121
  "@types/node": "^20.10.5",