honox icon indicating copy to clipboard operation
honox copied to clipboard

fix(jsx): fix nested islands output by using honox/vite/components

Open usualoma opened this issue 1 year ago • 4 comments

What is this?

  • When island components are nested, the outermost components are wrapped in honox-island and the inner components are not.
  • However, in template elements for restoring children, all nested components are wrapped in honox-island.

Implementation

Context API is provided by the runtime, so we need to replace it depending on the runtime the app is using. However, we can use jsxImportSource from tsconfig.json in the following places (or honoxReactImportSource if the app wants a special override) and replace it.

https://github.com/honojs/honox/compare/main...usualoma:honox:fix-nested-islands-output?expand=1#diff-dbf4bfe9bdc2ab09701d7f99d62aa69e45740919b350c8166aebe64daa3fdd54R181-R206

In addition, before this PR, the very complicated expression assembly used to be done in "src/vite/island-components.ts", but by making it independent as "src/vite/components.tsx", it can be cleaned up and future enhancements can be made easier.

tasks

  • [x] implement
  • [x] add tests
  • [x] add more tests

usualoma avatar Apr 29 '24 12:04 usualoma

@usualoma

It looks awesome at first glance. Making HonoXIsland is good! I'll look at the details later.

My additional thoughts is if we use useContext depending on the JSX runtime we are using at the time, we can use HasIslands on any JSX runtime, not only Hono's JSX.

https://github.com/honojs/honox/blob/main/src/server/components/has-islands.tsx

yusukebe avatar Apr 29 '24 22:04 yusukebe

@yusukebe Thank you!

My additional thoughts is if we use useContext depending on the JSX runtime we are using at the time, we can use HasIslands on any JSX runtime, not only Hono's JSX.

Sure! It seems that runtime-independent HasIslands could also be implemented.

usualoma avatar Apr 30 '24 01:04 usualoma

Hi @usualoma

Looks good!

One thing. This uses tsConfig.compilerOptions?.honoxReactImportSource if the user want to override it. But I think it is not good to write our custom option to tsconfig.json. So, as you mentioned, how about using useContextModule in vite.config.ts?

export default defineConfig({
  plugins: [
    honox({
      useContextModule: 'react',
    }),
  ],
})

My rough implementation:

diff --git a/src/vite/index.ts b/src/vite/index.ts
index 415ce59..bec7546 100644
--- a/src/vite/index.ts
+++ b/src/vite/index.ts
@@ -13,6 +13,7 @@ type Options = {
   devServer?: DevServerOptions
   islandComponents?: IslandComponentsOptions
   external?: string[]
+  useContextModule?: string
 }

 export const defaultOptions: Options = {
diff --git a/src/vite/island-components.ts b/src/vite/island-components.ts
index 5496971..5203051 100644
--- a/src/vite/island-components.ts
+++ b/src/vite/island-components.ts
@@ -168,39 +168,41 @@ export const transformJsxTags = (contents: string, componentName: string) => {
 type IsIsland = (id: string) => boolean
 export type IslandComponentsOptions = {
   isIsland: IsIsland
+  useContextModule?: string
 }

 export function islandComponents(options?: IslandComponentsOptions): Plugin {
   let root = ''
-  let jsxImportSource: string | undefined
+  let useContextModule = options?.useContextModule
   return {
     name: 'transform-island-components',
     configResolved: async (config) => {
       root = config.root

-      const tsConfigPath = path.resolve(process.cwd(), 'tsconfig.json')
-      try {
-        const tsConfigRaw = await fs.readFile(tsConfigPath, 'utf8')
-        const tsConfig = parseJsonc(tsConfigRaw)
+      if (useContextModule) {
+        const tsConfigPath = path.resolve(process.cwd(), 'tsconfig.json')
+        try {
+          const tsConfigRaw = await fs.readFile(tsConfigPath, 'utf8')
+          const tsConfig = parseJsonc(tsConfigRaw)

-        jsxImportSource =
-          tsConfig.compilerOptions?.honoxReactImportSource ??
-          tsConfig.compilerOptions?.jsxImportSource
-        if (jsxImportSource === 'hono/jsx/dom') {
-          jsxImportSource = 'hono/jsx' // we should use hono/jsx instead of hono/jsx/dom
+          useContextModule = tsConfig.compilerOptions?.jsxImportSource
+          if (useContextModule === 'hono/jsx/dom') {
+            useContextModule = 'hono/jsx' // we should use hono/jsx instead of hono/jsx/dom
+          }
+        } catch (error) {
+          console.warn('Error reading tsconfig.json:', error)
         }
-      } catch (error) {
-        console.warn('Error reading tsconfig.json:', error)
       }
     },
+
     async load(id) {
       if (/\/honox\/.*?\/vite\/components\./.test(id)) {
-        if (!jsxImportSource) {
+        if (!useContextModule) {
           return
         }
         const contents = await fs.readFile(id, 'utf-8')
         return {
-          code: contents.replaceAll('hono/jsx', jsxImportSource),
+          code: contents.replaceAll('hono/jsx', useContextModule),
           map: null,
         }
       }

yusukebe avatar Apr 30 '24 13:04 yusukebe

@yusukebe

You're right, we shouldn't add our own keys to "tsconfig.json", we should be able to specify them in honox().

useContextModule?

I have previously named it useContextModule, but in the future I would like to use isValidElement as well. https://github.com/honojs/honox/pull/162/files#diff-301678e36b084a28211205e06d8a73be0af507fd3ff8efef222069ef574ba3b4R1

So I'm thinking of making it reactApiImportSource, meaning "source for importing React API".

Priority

Suppose I consider it as reactApiImportSource. I would like to prioritise the following.

  1. Use reactApiImportSource in honox() if specified.
  2. If reactApiImportSource is not specified, try to get "compilerOptions?.jsxImportSource" from tsconfig.json <- this is the default behaviour
  3. If "compilerOptions?.jsxImportSource" cannot be obtained from tsconfig.json, the default value 'hono/jsx' is used

What do you think?

usualoma avatar Apr 30 '24 23:04 usualoma

@yusukebe

Hypothetically, I pushed 8fb2913, which was implemented with the above content.

In most cases, automatic configuration from tsconfig.json should be fine, but if you want to specify explicitly, you can write the following.

export default defineConfig({
  plugins: [
    honox({
      islandComponents: {
        reactApiImportSource: 'react',
      },
    }),
  ],
})

Feel free to comment if you have any requests for changes.

usualoma avatar May 01 '24 21:05 usualoma

@usualoma

I totally agree with naming reactApiImportSource and the priority!

One thing: how about making the src/site/components directory and putting honox-island.tsx and index.tsx instead of src/site/components.tsx?

yusukebe avatar May 02 '24 15:05 yusukebe

@yusukebe Thank you!

Indeed, I was also wondering where to put the files. I have changed it in 9638879, please check.

usualoma avatar May 02 '24 21:05 usualoma

@usualoma

Ahhhhh, sorry, sorry. I've typed it. /site/components should be /vite/components! Because It is used for the Vite plugin. Please can you rename it?

yusukebe avatar May 03 '24 00:05 yusukebe

OK, I did think that site and vite were prone to typo.

usualoma avatar May 03 '24 01:05 usualoma

fixed in a1342e4

usualoma avatar May 03 '24 01:05 usualoma

@usualoma

Thaaank! I'll merge this now.

yusukebe avatar May 03 '24 01:05 yusukebe