From 1b279691a3149bf79a7ff1a6198542108b7046d4 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 8 May 2024 22:44:56 +0900 Subject: [PATCH 1/2] feat: `$` prefix named file as island component --- mocks/app/routes/directory/$counter.tsx | 21 ++++++++++++++++ .../app/routes/directory/_Counter.island.tsx | 4 ++-- mocks/app/routes/directory/index.tsx | 10 ++++++-- src/vite/island-components.test.ts | 24 ++++++++++++++++++- src/vite/island-components.ts | 14 ++++++++++- test-e2e/e2e.test.ts | 10 ++++++-- test-integration/apps.test.ts | 2 +- test-integration/vitest.config.ts | 2 +- 8 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 mocks/app/routes/directory/$counter.tsx 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..cf6f5f5 100644 --- a/src/vite/island-components.test.ts +++ b/src/vite/island-components.test.ts @@ -1,5 +1,27 @@ import path from 'path' -import { transformJsxTags, islandComponents } from './island-components' +import { matchIslandComponentId, transformJsxTags, islandComponents } from './island-components' + +describe('matchIslandComponentId', () => { + it('Should match /islands/counter.tsx', () => { + const match = matchIslandComponentId('/islands/counter.tsx') + expect(match).not.toBeNull() + expect(match![0]).toBe('/islands/counter.tsx') + }) + it('Should match /routes/directory/$counter.tsx', () => { + const match = matchIslandComponentId('/routes/directory/$counter.tsx') + expect(match).not.toBeNull() + expect(match![0]).toBe('/routes/directory/$counter.tsx') + }) + it('Should match /routes/directory/_counter.island.tsx', () => { + const match = matchIslandComponentId('/routes/directory/_counter.island.tsx') + expect(match).not.toBeNull() + expect(match![0]).toBe('/routes/directory/_counter.island.tsx') + }) + it('Should not match /routes/directory/component.tsx', () => { + const match = matchIslandComponentId('/routes/directory/component.tsx') + 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..3f32c73 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$)|(\/routes.*\/\$[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)) }, From 7ff4899a4ce3afcbfdb8a39e5200e0c97d623a49 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 9 May 2024 23:15:57 +0900 Subject: [PATCH 2/2] fixed regexp and added tests Co-authored-by: Taku Amano --- src/vite/island-components.test.ts | 49 +++++++++++++++++++----------- src/vite/island-components.ts | 2 +- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/vite/island-components.test.ts b/src/vite/island-components.test.ts index cf6f5f5..98130cd 100644 --- a/src/vite/island-components.test.ts +++ b/src/vite/island-components.test.ts @@ -2,24 +2,39 @@ import path from 'path' import { matchIslandComponentId, transformJsxTags, islandComponents } from './island-components' describe('matchIslandComponentId', () => { - it('Should match /islands/counter.tsx', () => { - const match = matchIslandComponentId('/islands/counter.tsx') - expect(match).not.toBeNull() - expect(match![0]).toBe('/islands/counter.tsx') - }) - it('Should match /routes/directory/$counter.tsx', () => { - const match = matchIslandComponentId('/routes/directory/$counter.tsx') - expect(match).not.toBeNull() - expect(match![0]).toBe('/routes/directory/$counter.tsx') - }) - it('Should match /routes/directory/_counter.island.tsx', () => { - const match = matchIslandComponentId('/routes/directory/_counter.island.tsx') - expect(match).not.toBeNull() - expect(match![0]).toBe('/routes/directory/_counter.island.tsx') + 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) + }) + }) }) - it('Should not match /routes/directory/component.tsx', () => { - const match = matchIslandComponentId('/routes/directory/component.tsx') - expect(match).toBeNull() + + 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() + }) + }) }) }) diff --git a/src/vite/island-components.ts b/src/vite/island-components.ts index 3f32c73..986b9a2 100644 --- a/src/vite/island-components.ts +++ b/src/vite/island-components.ts @@ -60,7 +60,7 @@ function isComponentName(name: string) { */ export function matchIslandComponentId(id: string) { return id.match( - /(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)|(\/routes.*\/\$[a-zA-Z0-9[-]+\.tsx$)/ + /\/islands\/.+?\.tsx$|\/routes\/(?:.*\/)?(?:\_[a-zA-Z0-9-]+\.island\.tsx$|\$[a-zA-Z0-9-]+\.tsx$)/ ) }