Skip to content

Commit

Permalink
feat: support nested layouts
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukebe committed Feb 4, 2024
1 parent 1e1dc1d commit 81427ed
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 112 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test:unit": "vitest --run test/unit",
"test:integration": "bun test:integration:api && bun test:integration:hono-jsx",
"test:integration:hono-jsx": "vitest run -c ./test/hono-jsx/vitest.config.ts ./test/hono-jsx/integration.test.ts",
"test:integration:hono-jsx:watch": "vitest -c ./test/hono-jsx/vitest.config.ts ./test/hono-jsx/integration.test.ts",
"test:integration:api": "vitest run -c ./test/api/vitest.config.ts ./test/api/integration.test.ts",
"test:e2e": "playwright test -c ./test/hono-jsx/playwright.config.ts ./test/hono-jsx/e2e.test.ts",
"typecheck": "tsc --noEmit",
Expand Down Expand Up @@ -117,4 +118,4 @@
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.9.6"
}
}
}
45 changes: 22 additions & 23 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,13 @@ export const createApp = <E extends Env>(options?: ServerOptions<E>): Hono<E> =>
eager: true,
})
const rendererList = listByDirectory(RENDERER_FILE)
const applyRenderer = (rendererFile: string) => {

const applyRenderer = (app: Hono, rendererFile: string) => {
const renderer = RENDERER_FILE[rendererFile]
const path = pathToDirectoryPath(rendererFile).replace(rootRegExp, '')
app.all(`${filePathToPath(path)}*`, renderer.default)
const rendererDefault = renderer['default']
if (rendererDefault) {
app.all('*', rendererDefault)
}
}

// Routes
Expand All @@ -94,30 +97,26 @@ export const createApp = <E extends Env>(options?: ServerOptions<E>): Hono<E> =>
const subApp = new Hono()

// Renderer
let rendererFiles = rendererList[dir]

if (rendererFiles) {
applyRenderer(rendererFiles[0])
}

if (!rendererFiles) {
const dirPaths = dir.split('/')
const getRendererPaths = (paths: string[]) => {
rendererFiles = rendererList[paths.join('/')]
if (!rendererFiles) {
paths.pop()
if (paths.length) {
getRendererPaths(paths)
}
let rendererPaths = rendererList[dir] ?? []

const getRendererPaths = (paths: string[]) => {
rendererPaths = rendererList[paths.join('/')]
if (!rendererPaths) {
paths.pop()
if (paths.length) {
getRendererPaths(paths)
}
return rendererFiles
}
rendererFiles = getRendererPaths(dirPaths)
if (rendererFiles) {
applyRenderer(rendererFiles[0])
}
return rendererPaths ?? []
}

const dirPaths = dir.split('/')
rendererPaths = getRendererPaths(dirPaths)
rendererPaths.sort((a, b) => a.split('/').length - b.split('/').length)
rendererPaths.map((path) => {
applyRenderer(subApp, path)
})

// Root path
let rootPath = dir.replace(rootRegExp, '')
rootPath = filePathToPath(rootPath)
Expand Down
14 changes: 14 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/_renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { jsxRenderer } from 'hono/jsx-renderer'

export default jsxRenderer(
({ children }) => {
return (
<main>
<>{children}</>
</main>
)
},
{
docType: false,
}
)
17 changes: 17 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/foo/_renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { jsxRenderer } from 'hono/jsx-renderer'

export default jsxRenderer(
({ children, Layout }) => {
return (
<Layout>
<>
<nav>foo menu</nav>
{children}
</>
</Layout>
)
},
{
docType: false,
}
)
17 changes: 17 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/foo/bar/_renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { jsxRenderer } from 'hono/jsx-renderer'

export default jsxRenderer(
({ children, Layout }) => {
return (
<Layout>
<>
<nav>bar menu</nav>
{children}
</>
</Layout>
)
},
{
docType: false,
}
)
3 changes: 3 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/foo/bar/baz/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Baz() {
return <h1>Baz</h1>
}
3 changes: 3 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/foo/bar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Bar() {
return <h1>Bar</h1>
}
3 changes: 3 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/foo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Foo() {
return <h1>Foo</h1>
}
3 changes: 3 additions & 0 deletions test/hono-jsx/app-nested/routes/nested/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Nested() {
return <h1>Nested</h1>
}
216 changes: 128 additions & 88 deletions test/hono-jsx/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,110 +132,150 @@ describe('Basic', () => {
expect(res.status).toBe(500)
expect(await res.text()).toBe('Internal Server Error')
})
})

describe('With preserved', () => {
const ROUTES = import.meta.glob('./app/routes/**/[a-z[-][a-z-_[]*.(tsx|ts)', {
eager: true,
})
describe('With preserved', () => {
const ROUTES = import.meta.glob('./app/routes/**/[a-z[-][a-z-_[]*.(tsx|ts)', {
eager: true,
})

const RENDERER = import.meta.glob('./app/routes/**/_renderer.tsx', {
eager: true,
})
const RENDERER = import.meta.glob('./app/routes/**/_renderer.tsx', {
eager: true,
})

const NOT_FOUND = import.meta.glob('./app/routes/_404.tsx', {
eager: true,
})
const NOT_FOUND = import.meta.glob('./app/routes/_404.tsx', {
eager: true,
})

const ERROR = import.meta.glob('./app/routes/_error.tsx', {
eager: true,
})
const ERROR = import.meta.glob('./app/routes/_error.tsx', {
eager: true,
})

const app = createApp({
root: './app/routes',
ROUTES: ROUTES as any,
RENDERER: RENDERER as any,
NOT_FOUND: NOT_FOUND as any,
ERROR: ERROR as any,
})
const app = createApp({
root: './app/routes',
ROUTES: ROUTES as any,
RENDERER: RENDERER as any,
NOT_FOUND: NOT_FOUND as any,
ERROR: ERROR as any,
})

it('Should return 200 response - /', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>This is a title</title></head><body><h1>Hello</h1></body></html>'
)
})
it('Should return 200 response - /', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>This is a title</title></head><body><h1>Hello</h1></body></html>'
)
})

it('Should return 404 response - /foo', async () => {
const res = await app.request('/foo')
expect(res.status).toBe(404)
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>Not Found</title></head><body><h1>Not Found</h1></body></html>'
)
})
it('Should return 404 response - /foo', async () => {
const res = await app.request('/foo')
expect(res.status).toBe(404)
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>Not Found</title></head><body><h1>Not Found</h1></body></html>'
)
})

it('Should return 200 response /about/me', async () => {
const res = await app.request('/about/me')
expect(res.status).toBe(200)
// hono/jsx escape a single quote to &#39;
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>me</title></head><body><p>It&#39;s me</p><b>My name is me</b></body></html>'
)
})
it('Should return 200 response /about/me', async () => {
const res = await app.request('/about/me')
expect(res.status).toBe(200)
// hono/jsx escape a single quote to &#39;
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>me</title></head><body><p>It&#39;s me</p><b>My name is me</b></body></html>'
)
})

it('Should return 200 response /about/me/address', async () => {
const res = await app.request('/about/me/address')
expect(res.status).toBe(200)
// hono/jsx escape a single quote to &#39;
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>me&#39;s address</title></head><body><h1>About</h1><address><b>me&#39;s address</b></address></body></html>'
)
})
it('Should return 200 response /about/me/address', async () => {
const res = await app.request('/about/me/address')
expect(res.status).toBe(200)
// hono/jsx escape a single quote to &#39;
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>me&#39;s address</title></head><body><h1>About</h1><address><b>me&#39;s address</b></address></body></html>'
)
})

it('Should return 200 response /interaction', async () => {
const res = await app.request('/interaction')
expect(res.status).toBe(200)
// hono/jsx escape a single quote to &#39;
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title></title><script type="module" src="/app/client.ts"></script></head><body><honox-island component-name="Counter.tsx" data-serialized-props="{&quot;initial&quot;:5}"><div><p>Count: 5</p><button onClick="() =&gt; setCount(count + 1)">Increment</button></div></honox-island></body></html>'
)
})
it('Should return 200 response /interaction', async () => {
const res = await app.request('/interaction')
expect(res.status).toBe(200)
// hono/jsx escape a single quote to &#39;
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title></title><script type="module" src="/app/client.ts"></script></head><body><honox-island component-name="Counter.tsx" data-serialized-props="{&quot;initial&quot;:5}"><div><p>Count: 5</p><button onClick="() =&gt; setCount(count + 1)">Increment</button></div></honox-island></body></html>'
)
})

it('Should return 500 response /throw_error', async () => {
const res = await app.request('/throw_error')
expect(res.status).toBe(500)
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>Internal Server Error</title></head><body><h1>Custom Error Message: Foo</h1></body></html>'
)
})
it('Should return 500 response /throw_error', async () => {
const res = await app.request('/throw_error')
expect(res.status).toBe(500)
expect(await res.text()).toBe(
'<!DOCTYPE html><html><head><title>Internal Server Error</title></head><body><h1>Custom Error Message: Foo</h1></body></html>'
)
})
})

describe('API', () => {
const ROUES = import.meta.glob('./app/routes//**/[a-z[-][a-z-_[]*.(tsx|ts)', {
eager: true,
})
describe('API', () => {
const ROUES = import.meta.glob('./app/routes//**/[a-z[-][a-z-_[]*.(tsx|ts)', {
eager: true,
})

const app = createApp({
root: './app/routes',
ROUTES: ROUES as any,
})
const app = createApp({
root: './app/routes',
ROUTES: ROUES as any,
})

it('Should return 200 response - /api', async () => {
const res = await app.request('/api')
expect(res.status).toBe(200)
expect(res.headers.get('X-Custom')).toBe('Hello')
expect(await res.json()).toEqual({ foo: 'bar' })
})
it('Should return 200 response - /api', async () => {
const res = await app.request('/api')
expect(res.status).toBe(200)
expect(res.headers.get('X-Custom')).toBe('Hello')
expect(await res.json()).toEqual({ foo: 'bar' })
})

it('Should return 200 response - POST /api', async () => {
const res = await app.request('/api', {
method: 'POST',
})
expect(res.status).toBe(201)
expect(await res.json()).toEqual({
ok: true,
message: 'created',
})
it('Should return 200 response - POST /api', async () => {
const res = await app.request('/api', {
method: 'POST',
})
expect(res.status).toBe(201)
expect(await res.json()).toEqual({
ok: true,
message: 'created',
})
})
})

describe('Nested Layouts', () => {
const ROUTES = import.meta.glob('./app-nested/routes/**/[a-z[-][a-z-_[]*.(tsx|ts)', {
eager: true,
})

const RENDERER = import.meta.glob('./app-nested/routes/**/_renderer.tsx', {
eager: true,
})

const app = createApp({
root: './app-nested/routes',
ROUTES: ROUTES as any,
RENDERER: RENDERER as any,
})

it('Should return 200 response - /nested', async () => {
const res = await app.request('/nested')
expect(res.status).toBe(200)
expect(await res.text()).toBe('<main><h1>Nested</h1></main>')
})

it('Should return 200 response - /nested/foo', async () => {
const res = await app.request('/nested/foo')
expect(res.status).toBe(200)
expect(await res.text()).toBe('<main><nav>foo menu</nav><h1>Foo</h1></main>')
})

it('Should return 200 response - /nested/foo/bar', async () => {
const res = await app.request('/nested/foo/bar')
expect(res.status).toBe(200)
expect(await res.text()).toBe('<main><nav>foo menu</nav><nav>bar menu</nav><h1>Bar</h1></main>')
})

it('Should return 200 response - /nested/foo/bar/baz', async () => {
const res = await app.request('/nested/foo/bar/baz')
expect(res.status).toBe(200)
expect(await res.text()).toBe('<main><nav>foo menu</nav><nav>bar menu</nav><h1>Baz</h1></main>')
})
})

0 comments on commit 81427ed

Please sign in to comment.