Repository: honojs/honox (ts)

Issue:
Middleware not running
### What version of HonoX are you using?

0.1.40

### What steps can reproduce the bug?

Create a directory called `admin`. In that directory create a file called `_middleware.ts`, create a new directory in the admin directory called `dashboard`, place an `index.tsx` file in that directory.

if you now go to the `/admin/dashboard` route the middleware will not be ran. If you place a file in the `admin` directory the middleware will run.

will NOT RUN the middleware
```
admin/
├── _middleware.ts
├── dashboard/
│   └── index.tsx
```

will RUN the middleware
```
admin/
├── _middleware.ts
├── anotherFile.tsx
├── dashboard/
│   └── index.tsx
```

the other file can be any .tsx/.ts file

### What is the expected behavior?

that the middleware should run, not depending if there are other files in the directory or not.

### What do you see instead?

the middleware is not being run on that route/child-routes when empty in an folder

### Additional information

_No response_

Pull request description:
fix: middleware not applied when directory has no other files
 Fixes #290

Patch:
diff --git a/mocks/app-nested-middleware/routes/admin/_middleware.ts b/mocks/app-nested-middleware/routes/admin/_middleware.ts
new file mode 100644
index 0000000..1ab6cbc
--- /dev/null
+++ b/mocks/app-nested-middleware/routes/admin/_middleware.ts
@@ -0,0 +1,8 @@
+import { createMiddleware } from 'hono/factory'
+
+export default [
+  createMiddleware(async (c, next) => {
+    await next()
+    c.header('X-Admin-Middleware', 'true', { append: true })
+  }),
+]
diff --git a/mocks/app-nested-middleware/routes/admin/dashboard/index.tsx b/mocks/app-nested-middleware/routes/admin/dashboard/index.tsx
new file mode 100644
index 0000000..86edd7c
--- /dev/null
+++ b/mocks/app-nested-middleware/routes/admin/dashboard/index.tsx
@@ -0,0 +1,8 @@
+export default function AdminDashboard() {
+  return (
+    <div>
+      <h1>Admin Dashboard</h1>
+      <p>This route should have admin middleware applied</p>
+    </div>
+  )
+}
\ No newline at end of file
diff --git a/mocks/app-nested-middleware/routes/admin/dashboard/settings/index.tsx b/mocks/app-nested-middleware/routes/admin/dashboard/settings/index.tsx
new file mode 100644
index 0000000..37e0ef8
--- /dev/null
+++ b/mocks/app-nested-middleware/routes/admin/dashboard/settings/index.tsx
@@ -0,0 +1,8 @@
+export default function AdminSettings() {
+  return (
+    <div>
+      <h1>Admin Settings</h1>
+      <p>This route should inherit admin middleware from grandparent directory</p>
+    </div>
+  )
+}
\ No newline at end of file
diff --git a/src/server/server.ts b/src/server/server.ts
index bb9e91c..4238ad4 100644
--- a/src/server/server.ts
+++ b/src/server/server.ts
@@ -63,6 +63,9 @@ export const createApp = <E extends Env>(options: BaseServerOptions<E>): Hono<E>
   const app = options.app ?? new Hono()
   const trailingSlash = options.trailingSlash ?? false
 
+  // Track applied middleware to prevent duplication
+  const appliedMiddleware = new Set<string>()
+
   // Share context by AsyncLocalStorage
   app.use(async function ShareContext(c, next) {
     await contextStorage.run(c, () => next())
@@ -149,19 +152,47 @@ export const createApp = <E extends Env>(options: BaseServerOptions<E>): Hono<E>
         }
       })
 
-      const middlewareFile = Object.keys(MIDDLEWARE_FILE).find((x) => {
-        const replacedDir = dir
+      // Helper functions
+      const escapeDir = (directory: string) =>
+        directory
           .replaceAll('[', '\\[')
           .replaceAll(']', '\\]')
           .replaceAll('(', '\\(')
           .replaceAll(')', '\\)')
 
-        return new RegExp(replacedDir + '/_middleware.tsx?').test(x)
-      })
+      const findMiddleware = (directory: string) =>
+        Object.keys(MIDDLEWARE_FILE).find((x) =>
+          new RegExp(escapeDir(directory) + '/_middleware.tsx?').test(x)
+        )
+
+      // Find middleware for current directory first
+      let middlewareFile = findMiddleware(dir)
+
+      // Issue 290 fix: Search parent directories if no middleware found
+      if (!middlewareFile) {
+        const dirParts = dir.split('/')
+        for (let i = dirParts.length - 1; i >= 0; i--) {
+          const parentDir = dirParts.slice(0, i).join('/')
+          middlewareFile = findMiddleware(parentDir)
+          if (middlewareFile) break
+        }
+      }
 
       if (middlewareFile) {
         const middleware = MIDDLEWARE_FILE[middlewareFile]
-        if (middleware.default) {
+        const middlewareDir = middlewareFile
+          .replace('/_middleware.tsx', '')
+          .replace('/_middleware.ts', '')
+
+        // Only apply if this directory should have this middleware
+        const shouldApply =
+          middlewareDir === dir ||
+          (!appliedMiddleware.has(middlewareFile) && dir.startsWith(middlewareDir + '/'))
+
+        if (middleware.default && shouldApply) {
+          if (!appliedMiddleware.has(middlewareFile)) {
+            appliedMiddleware.add(middlewareFile)
+          }
           subApp.use(...middleware.default)
         }
       }


Test patch:
diff --git a/test-integration/apps.test.ts b/test-integration/apps.test.ts
index e58e103..c318dd8 100644
--- a/test-integration/apps.test.ts
+++ b/test-integration/apps.test.ts
@@ -466,6 +466,18 @@ describe('Nested Middleware', () => {
     expect(res.headers.get('foo')).toEqual('foo')
     expect(res.headers.get('bar')).toEqual('bar')
   })
+
+  it('Should apply admin middleware to child route - /admin/dashboard', async () => {
+    const res = await app.request('/admin/dashboard')
+    expect(res.status).toBe(200)
+    expect(res.headers.get('X-Admin-Middleware')).toEqual('true')
+  })
+
+  it('Should apply admin middleware to grandchild route - /admin/dashboard/settings', async () => {
+    const res = await app.request('/admin/dashboard/settings')
+    expect(res.status).toBe(200)
+    expect(res.headers.get('X-Admin-Middleware')).toEqual('true')
+  })
 })
 
 describe('<Script /> component', () => {