diff --git a/mocks/app/routes/directory/$counter.tsx b/mocks/app/routes/directory/$counter.tsx
new file mode 100644
index 0000000..0898773
--- /dev/null
+++ b/mocks/app/routes/directory/$counter.tsx
@@ -0,0 +1,21 @@
+import type { PropsWithChildren } from 'hono/jsx'
+import { useState } from 'hono/jsx'
+
+export default function Counter({
+ children,
+ initial = 0,
+ id = '',
+}: PropsWithChildren<{
+ initial?: number
+ id?: string
+}>) {
+ const [count, setCount] = useState(initial)
+ const increment = () => setCount(count + 1)
+ return (
+
+
DollarCount: {count}
+
+ {children}
+
+ )
+}
diff --git a/mocks/app/routes/directory/_Counter.island.tsx b/mocks/app/routes/directory/_Counter.island.tsx
index a716175..5fe2d06 100644
--- a/mocks/app/routes/directory/_Counter.island.tsx
+++ b/mocks/app/routes/directory/_Counter.island.tsx
@@ -13,8 +13,8 @@ export default function Counter({
const increment = () => setCount(count + 1)
return (
-
Count: {count}
-
+
UnderScoreCount: {count}
+
{children}
)
diff --git a/mocks/app/routes/directory/index.tsx b/mocks/app/routes/directory/index.tsx
index f4d6f90..ca50897 100644
--- a/mocks/app/routes/directory/index.tsx
+++ b/mocks/app/routes/directory/index.tsx
@@ -1,5 +1,11 @@
-import Counter from './_Counter.island'
+import DollarCounter from './$counter'
+import UnderScoreCounter from './_Counter.island'
export default function Interaction() {
- return
+ return (
+ <>
+
+
+ >
+ )
}
diff --git a/src/vite/island-components.test.ts b/src/vite/island-components.test.ts
index d9b8edc..98130cd 100644
--- a/src/vite/island-components.test.ts
+++ b/src/vite/island-components.test.ts
@@ -1,5 +1,42 @@
import path from 'path'
-import { transformJsxTags, islandComponents } from './island-components'
+import { matchIslandComponentId, transformJsxTags, islandComponents } from './island-components'
+
+describe('matchIslandComponentId', () => {
+ describe('match', () => {
+ const paths = [
+ '/islands/counter.tsx',
+ '/islands/directory/counter.tsx',
+ '/routes/$counter.tsx',
+ '/routes/directory/$counter.tsx',
+ '/routes/_counter.island.tsx',
+ '/routes/directory/_counter.island.tsx',
+ ]
+
+ paths.forEach((path) => {
+ it(`Should match ${path}`, () => {
+ const match = matchIslandComponentId(path)
+ expect(match).not.toBeNull()
+ expect(match![0]).toBe(path)
+ })
+ })
+ })
+
+ describe('not match', () => {
+ const paths = [
+ '/routes/directory/component.tsx',
+ '/routes/directory/foo$component.tsx',
+ '/routes/directory/foo_component.island.tsx',
+ '/routes/directory/component.island.tsx',
+ ]
+
+ paths.forEach((path) => {
+ it(`Should not match ${path}`, () => {
+ const match = matchIslandComponentId(path)
+ expect(match).toBeNull()
+ })
+ })
+ })
+})
describe('transformJsxTags', () => {
it('Should add component-wrapper and component-name attribute', () => {
diff --git a/src/vite/island-components.ts b/src/vite/island-components.ts
index c2aa466..986b9a2 100644
--- a/src/vite/island-components.ts
+++ b/src/vite/island-components.ts
@@ -52,6 +52,18 @@ function isComponentName(name: string) {
return /^[A-Z][A-Z0-9]*[a-z][A-Za-z0-9]*$/.test(name)
}
+/**
+ * Matches when id is the filename of Island component
+ *
+ * @param id - The id to match
+ * @returns The result object if id is matched or null
+ */
+export function matchIslandComponentId(id: string) {
+ return id.match(
+ /\/islands\/.+?\.tsx$|\/routes\/(?:.*\/)?(?:\_[a-zA-Z0-9-]+\.island\.tsx$|\$[a-zA-Z0-9-]+\.tsx$)/
+ )
+}
+
function addSSRCheck(funcName: string, componentName: string, componentExport?: string) {
const isSSR = memberExpression(
memberExpression(identifier('import'), identifier('meta')),
@@ -263,7 +275,7 @@ export function islandComponents(options?: IslandComponentsOptions): Plugin {
if (!matchIslandPath(id)) {
return
}
- const match = id.match(/(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)/)
+ const match = matchIslandComponentId(id)
if (match) {
const componentName = match[0]
const contents = await fs.readFile(id, 'utf-8')
diff --git a/test-e2e/e2e.test.ts b/test-e2e/e2e.test.ts
index 0e60111..a348b67 100644
--- a/test-e2e/e2e.test.ts
+++ b/test-e2e/e2e.test.ts
@@ -24,8 +24,14 @@ test('test counter - island in the same directory', async ({ page }) => {
await page.goto('/directory')
await page.waitForSelector('body[data-client-loaded]')
- await page.getByText('Count: 5').click()
- await page.getByRole('button', { name: 'Increment' }).click({
+ await page.getByText('UnderScoreCount: 5').click()
+ await page.getByRole('button', { name: 'UnderScore Increment' }).click({
+ clickCount: 1,
+ })
+ await page.getByText('Count: 6').click()
+
+ await page.getByText('DollarCount: 5').click()
+ await page.getByRole('button', { name: 'Dollar Increment' }).click({
clickCount: 1,
})
await page.getByText('Count: 6').click()
diff --git a/test-integration/apps.test.ts b/test-integration/apps.test.ts
index a47a023..16604bf 100644
--- a/test-integration/apps.test.ts
+++ b/test-integration/apps.test.ts
@@ -338,7 +338,7 @@ describe('With preserved', () => {
expect(res.status).toBe(200)
// hono/jsx escape a single quote to '
expect(await res.text()).toBe(
- ''
+ 'UnderScoreCount: 5
DollarCount: 5
'
)
})
diff --git a/test-integration/vitest.config.ts b/test-integration/vitest.config.ts
index 5f6068c..e4473a5 100644
--- a/test-integration/vitest.config.ts
+++ b/test-integration/vitest.config.ts
@@ -18,7 +18,7 @@ export default defineConfig({
isIsland: (id) => {
const resolvedPath = path.resolve(root).replace(/\\/g, '\\\\')
const regexp = new RegExp(
- `${resolvedPath}[\\\\/]app[^\\\\/]*[\\\\/]islands[\\\\/].+\.tsx?$|${resolvedPath}[\\\\/]app[^\\\\/]*[\\\\/]routes[\\\\/].+\.island\.tsx?$`
+ `${resolvedPath}[\\\\/]app[^\\\\/]*[\\\\/]islands[\\\\/].+\.tsx?$|${resolvedPath}[\\\\/]app[^\\\\/]*[\\\\/]routes[\\\\/].+\.island\.tsx?$|${resolvedPath}[\\\\/]app[^\\\\/]*[\\\\/]routes[\\\\/].*\\$.+\.tsx?$`
)
return regexp.test(path.resolve(id))
},