Problem
In a Stacks app (hq.training), resources/views/password/reset/[token].stx did not override the framework's defaults/resources/views/password/reset/[token].stx. The dev server rendered the framework default, even though the generated routes.ts listed the app file for /password/reset/:token. Static overrides such as forgot-password.stx worked.
getRoute sorts dynamic candidates with routeSpecificity(filePath) on the whole path. Every directory of the page root counts as a static segment (+100), so a page under a deeper root outranks the same route under a shallower one. Stacks passes ['resources/views', '<framework>/defaults/resources/views'], and the default wins by being further down the filesystem.
Page middleware had the same precedence bug. detectPageMiddleware only indexed pages that declared middleware, and it ran under Promise.all, so the last read to finish won. A default page's definePageMeta({ middleware }) could land on the app page that replaced it, or clobber the app's own.
Fix
pageRootOf(file, patterns)/orderDynamicRouteFiles(files, patterns), both exported and pure: rank on the path relative to its root, and give ties to the earlier root, then to discovery order.- Strategy 6 of
getRouteiterates in that order. indexPageMiddlewarerecords every page, including ones with no middleware. The first root owns each static URL, and dynamic patterns are indexed in the ordergetRoutetries them.
Test
test/serve-page-root-precedence.test.ts covers the pure ordering and a real serve() with an app root ahead of a deeper defaults root:
- the app page renders at a dynamic path both roots define
- a default nobody overrode is still served
- the overriding page's middleware runs, not the default's (dynamic and static), and the app's own guard still holds
Against the previous serve.ts, the first case renders DEFAULT-RESET and the middleware case answers 403. The full packages/bun-plugin suite passes (256 tests).
Stacks needs a release with this and a bun-plugin-stx bump. The companion Stacks PR has the details.
🤖 Generated with Claude Code