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( - '

Count: 5

' + '

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)) },