Route Handlers: Fix devRequestTimingInternalsEnd and Turbopack server HMR (#92271)
### What?
Fixes issues with `AppRouteRouteModule` that interact with
`devRequestTiming` and Turbopack server HMR.
1. **Incorrect `devRequestTimingInternalsEnd` attribution**: The
app-route template previously statically imported userland (`import * as
userland from 'VAR_USERLAND'`), meaning module load time was included in
the framework's startup overhead. The `devRequestTimingInternalsEnd`
timestamp is now set immediately before `routeModule.handle()` is
called, and userland is loaded lazily on the first request so the timing
correctly captures the framework→application boundary.
2. **Stale route handlers after Turbopack server HMR**: After a
Turbopack server HMR update, `devModuleCache` is updated with a fresh
route module, but the entry chunk remains in Node.js `require.cache`.
The lazy `_userlandFactory` only runs once and would cache the old
module reference — so route handler changes were not reflected after
HMR. This restores a synchronous per-request `getUserland` getter that
calls `require('VAR_USERLAND')` on every request. Since `require()` is a
synchronous `devModuleCache` lookup, it has negligible overhead and
doesn't add async time that would be incorrectly attributed to
`application-code`.
3. **Route handlers with top-level await**: `require()` returns a
Promise for modules that use top-level await. The module now accepts a
lazy factory for `userland` and handles this case via
`ensureUserland()`, which is called before the first request is handled.
4. **`output: export` route validation**: With the lazy factory,
validation errors (e.g. `dynamic = "force-dynamic"` on an exported
route) were deferred to request time, returning a 500 instead of
surfacing as a Redbox in dev. The constructor now eagerly calls the
factory for `output: export` routes so `_initFromUserland()` throws at
module-load time — preserving the Redbox error.
5. **`node:stream` in edge bundles** (CI fix): `render-result.ts` was
missing a `NEXT_RUNTIME === 'edge'` compile-time guard around
`require('node:stream')` / `__non_webpack_require__('node:stream')`,
which prevented webpack from DCE-ing those branches in edge builds.
Vercel's deploy adapter rejects edge functions that reference
`node:stream`. The guard is now restored to match the canary pattern.
### Why?
#91466 enabled server HMR for app route handlers and introduced
`getUserland: () => import('VAR_USERLAND')` to pick up HMR updates on
each request. However, the async `import()` adds latency incorrectly
attributed to application-code time in `devRequestTiming`. This PR
replaces `import()` with synchronous `require()` and also lazily loads
userland on the first request for accurate
`devRequestTimingInternalsEnd` placement.
### How?
- **Template** (`app-route.ts`): Replaces the static `import * as
userland` with a lazy `() => require('VAR_USERLAND')` factory. In
Turbopack dev mode, also provides `getUserland: () =>
require('VAR_USERLAND')` — a synchronous getter called on every request
to pick up server HMR updates.
- **Module** (`module.ts`):
- Accepts `userland` as either a direct module or a lazy factory.
- For `output: export` routes, calls the factory eagerly in the
constructor so `_initFromUserland()` validation errors throw at
module-load time (Redbox) rather than at request time (500).
- For async modules (top-level await), stores the pending Promise in
`_pendingUserland` so `ensureUserland()` can await it before the first
request.
- Accepts optional synchronous `getUserland` for per-request live
userland access (Turbopack dev HMR).
- `handle()` calls `_getUserland?.()` at request start; if set, uses the
result for handler resolution, `fetchCache`, `hasNonStaticMethods`, and
`dynamic` — ensuring HMR changes to all exported values are immediately
reflected.
- Extracts `_initFromUserland()` helper to deduplicate initialization
between lazy and eager paths.
- **Route module base** (`route-module.ts`): Makes `isDev` public for
template access.
- **Test** (`app-custom-routes.test.ts` + fixture): Adds an integration
test for route handlers that use top-level await, ensuring the handler
is correctly resolved even when `require()` returns a Promise.
- **`render-result.ts`**: Restores the `if (process.env.NEXT_RUNTIME ===
'edge') { throw }` guard matching canary, so `node:stream` is DCE'd from
edge builds.
Fixes #91318
---------
Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>