Skip to content

Close Next.js 16 file-routing parity gaps (async params, forbidden/unauthorized, global-error/not-found, instrumentation) #848

Description

@vivek7405

Problem

webjs mirrors Next.js file-based routing so agents trained on Next carry over their muscle memory. Auditing against Next.js 16 (verified against a local Next canary clone, FILE_TYPES in next-app-loader + the file-conventions docs) surfaced routing conventions Next has that webjs lacks or names differently. Closing them tightens the parity that is webjs's whole onboarding thesis.

This issue covers the adopt set. Deliberately NOT in scope: middlewareproxy rename (webjs keeps middleware; its semantics are Remix/Koa-style chainable per-segment middleware, documented in agent-docs/nextjs-muscle-memory-gotchas.md §8); parallel/intercepting routes (deferred, large surface); and template.{js,ts} (evaluated and dropped: lowest value in webjs because a re-inserted web component already gets a fresh connectedCallback, and it is the only gap that touches the delicate client-router keyed reconciler, so the risk/reward does not justify it).

Design / approach

Adopt four conventions from Next 16:

  1. async params / searchParams. Next 15/16 made these awaitable (const { id } = await params). webjs passes plain sync objects. Make them thenable so both params.id and await params work (backward-compatible: awaiting a non-thenable already works, so this only ADDS the await form). Highest muscle-memory payoff.
  2. forbidden() / unauthorized() throwers + forbidden.{js,ts} / unauthorized.{js,ts} boundary files. Mirror Next's forbidden()/unauthorized() control-flow throwers and their nearest-wins boundary files, extending webjs's existing notFound()/redirect() sentinel model.
  3. global-error.{js,ts} / global-not-found.{js,ts}. Root-level boundaries. webjs's nested error/not-found cover most; add the explicit root equivalents for parity.
  4. instrumentation.{js,ts} / instrumentation-client.{js,ts}. A boot-time startup hook (server) and load-time hook (client). webjs covers the capability via readiness.js / env.js / the onError hook; add the standardized Next names (can delegate to existing infra).

Implementation notes (for the implementing agent)

  • params/searchParams: built in packages/server/src/ssr.js (~L69, the ctx = { params, searchParams: Object.fromEntries(...), ... }). Also the page-action / route.{js,ts} handler context. Make params/searchParams thenable objects (an object with a NON-enumerable then + own enumerable keys) so sync access AND await both work. Update PageProps/LayoutProps/RouteHandlerContext types. Landmine: the then must be non-enumerable so Object.fromEntries result stays a plain-keyed object for existing params.id reads / spread / JSON / for...in.
  • throwers: packages/core/src/nav.js defines notFound()/redirect() via Symbol.for('webjs.notFound') / webjs.redirect sentinels + isNotFound/isRedirect. Add forbidden()/unauthorized() the same way (new symbols, isForbidden/isUnauthorized), export from @webjsdev/core. Catch them in the SSR page-render pipeline (packages/server/src/ssr.js) and the page-action pipeline, mapping to 403/401 and rendering the nearest boundary file.
  • boundary/global files: route-tree parsing is packages/server/src/router.js (the stem === 'page' | 'layout' | 'error' | 'loading' | 'middleware' | 'not-found' | 'route' chain). Add forbidden, unauthorized, global-error, global-not-found stems and thread them through render/boundary resolution the way not-found/error already are (nearest-wins). Also fix the latent bug that nested not-found was parsed but only the root ever rendered.
  • instrumentation: boot path lives around packages/server/src/dev.js (server start) + env-schema.js/readiness. Wire an optional app-root instrumentation.{js,ts} register export called once at boot, and instrumentation-client.{js,ts} emitted first into the client boot.
  • Invariants: framework packages/ stays plain .js + JSDoc (no .ts). Respect AGENTS.md invariants (server-only boundary, erasable TS in examples). No backward-compat shims needed beyond the thenable params (project has no users yet).

Doc + scaffold surfaces to sync

  • AGENTS.md (App layout + file-conventions), agent-docs/nextjs-muscle-memory-gotchas.md (§4 params), the docs site (docs/app/docs/*), and scaffold rule files. Run the webjs-doc-sync + webjs-scaffold-sync skills.

Acceptance criteria

  • await params and await searchParams both work in pages/layouts/route handlers; sync params.id still works (counterfactual test that fails if thenable removed)
  • forbidden() / unauthorized() throw sentinels caught by SSR + page-action pipelines (403/401), rendering forbidden.{js,ts} / unauthorized.{js,ts} nearest-wins
  • global-error.{js,ts} / global-not-found.{js,ts} resolved at root
  • instrumentation.{js,ts} runs once at boot; instrumentation-client.{js,ts} runs first on client load
  • Tests at every layer touched (unit + Bun parity for the ssr/context change)
  • webjs check passes; docs + scaffold surfaces synced

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions