Repository: honojs/honox
Pull request #151 (2024-04-15 16:09:35)

fix: nested islands ssr
fix #150 

With this change, nested islands will behave as one island.

New HTML:
<img width="517" alt="image" src="https://github.com/honojs/honox/assets/104847600/a58a176a-eac1-40c1-9df3-cd07f01a3a90">


yusukebe: Hi @kosei28 !

Awesome! This may work great. I've left a comment and updated the test by myself.

yusukebe: Hi @kosei28 

Looks good! I'll merge it now. Thank you for your contribution!

Patch:
diff --git a/mocks/app/islands/Badge.tsx b/mocks/app/islands/Badge.tsx
new file mode 100644
index 0000000..1c837b4
--- /dev/null
+++ b/mocks/app/islands/Badge.tsx
@@ -0,0 +1,3 @@
+export default function Badge({ name }: { name: string }) {
+  return <p>{name}</p>
+}
diff --git a/mocks/app/islands/Counter.tsx b/mocks/app/islands/Counter.tsx
index a716175..e2c3739 100644
--- a/mocks/app/islands/Counter.tsx
+++ b/mocks/app/islands/Counter.tsx
@@ -1,5 +1,6 @@
 import type { PropsWithChildren } from 'hono/jsx'
 import { useState } from 'hono/jsx'
+import Badge from './Badge'
 
 export default function Counter({
   children,
@@ -13,6 +14,7 @@ export default function Counter({
   const increment = () => setCount(count + 1)
   return (
     <div id={id}>
+      <Badge name='Counter' />
       <p>Count: {count}</p>
       <button onClick={increment}>Increment</button>
       {children}
diff --git a/src/vite/island-components.ts b/src/vite/island-components.ts
index f3d2d05..382c66a 100644
--- a/src/vite/island-components.ts
+++ b/src/vite/island-components.ts
@@ -197,25 +197,47 @@ export type IslandComponentsOptions = {
   isIsland: IsIsland
 }
 
+function getIslandComponentName(
+  root: string,
+  id: string,
+  options?: IslandComponentsOptions
+): string | null {
+  const defaultIsIsland: IsIsland = (id) => {
+    const islandDirectoryPath = path.join(root, 'app')
+    return path.resolve(id).startsWith(islandDirectoryPath)
+  }
+  const matchIslandPath = options?.isIsland ?? defaultIsIsland
+  if (!matchIslandPath(id)) {
+    return null
+  }
+  const match = id.match(/(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)/)
+  if (!match) {
+    return null
+  }
+  return match[0]
+}
+
 export function islandComponents(options?: IslandComponentsOptions): Plugin {
+  const insideIslandSuffix = '?inside-island'
   let root = ''
   return {
     name: 'transform-island-components',
+    enforce: 'pre',
     configResolved: (config) => {
       root = config.root
     },
-    async load(id) {
-      const defaultIsIsland: IsIsland = (id) => {
-        const islandDirectoryPath = path.join(root, 'app')
-        return path.resolve(id).startsWith(islandDirectoryPath)
+    async resolveId(source, importer) {
+      const resolution = await this.resolve(source, importer)
+      if (resolution && importer && getIslandComponentName(root, importer, options)) {
+        return resolution.id + insideIslandSuffix
       }
-      const matchIslandPath = options?.isIsland ?? defaultIsIsland
-      if (!matchIslandPath(id)) {
+    },
+    async load(id) {
+      if (id.endsWith(insideIslandSuffix)) {
         return
       }
-      const match = id.match(/(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)/)
-      if (match) {
-        const componentName = match[0]
+      const componentName = getIslandComponentName(root, id, options)
+      if (componentName) {
         const contents = await fs.readFile(id, 'utf-8')
         const code = transformJsxTags(contents, componentName)
         if (code) {


Test patch:
diff --git a/test-integration/apps.test.ts b/test-integration/apps.test.ts
index 3d1fa50..3045c46 100644
--- a/test-integration/apps.test.ts
+++ b/test-integration/apps.test.ts
@@ -300,7 +300,7 @@ describe('With preserved', () => {
     expect(res.status).toBe(200)
     // hono/jsx escape a single quote to &#39;
     expect(await res.text()).toBe(
-      '<!DOCTYPE html><html><head><title></title></head><body><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:5}"><div id=""><p>Count: 5</p><button>Increment</button></div></honox-island><script type="module" async="" src="/app/client.ts"></script></body></html>'
+      '<!DOCTYPE html><html><head><title></title></head><body><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:5}"><div id=""><p>Counter</p><p>Count: 5</p><button>Increment</button></div></honox-island><script type="module" async="" src="/app/client.ts"></script></body></html>'
     )
   })
 
@@ -320,6 +320,7 @@ describe('With preserved', () => {
                <h1>Nested Island Test</h1>
                <honox-island component-name="/islands/Counter.tsx" data-serialized-props="{}">
                   <div id="">
+                     <p>Counter</p>
                      <p>Count: 0</p>
                      <button>Increment</button>
                   </div>