test: cover Pages Router _app availability in Turbopack dev (#99789) (#99910)
### What?
Regression coverage for #99789, stacked on #98823.
This is PR 2 of a six-PR stack, based on `canary`:
1. #98823 — Pages Router dev client graph chaining
2. **This PR** — regression test for Pages Router `_app` availability
(#99789)
3. #98734 — use graph-derived merged groups
4. #99750 — chunking takes a `ChunkGroupKey`
5. #98685 — chunk-group availability bitmaps
6. #99050 — keyed chunk-group index lookups
- **New e2e suite** `test/e2e/pages-app-availability-boundary`: a Pages
Router app with the import shape from #99789:
- `_app` imports `c-shared` and dynamically imports a module that also
imports it.
- `pages/index` imports `a-page-only`, `b-boundary`, `c-shared`,
`d-boundary`, `e-page`, and dynamically imports a module that imports
`b`/`d`, which makes `b` and `d` batching boundaries.
The test asserts that the page hydrates (a marker set in `useEffect`),
that every imported value renders, and that no "module factory is not
available" error is logged.
- **`app-document-import-order`**: the "loads modules shared by _app and
the page only once" assertion added in #97664 was skipped in all of dev.
It now runs everywhere and is gated `// @gate !(turbopack && dev)`
(expected failure).
### Why?
In Turbopack dev, #97664 seeds each page's client chunk group with
`/_app`'s availability. But each page had its own module graph, so
availability items (`ModuleOrBatch` identities) were compared across
graphs. In the page graph, batching can produce a
`ModuleOrBatch::Module(c-shared)` node whose edges point at earlier
siblings (`a`, `b`). Because `/_app` already has `Module(c-shared)`, the
traversal excluded that subtree, and `a`/`b` were never chunked.
Hydration then fails with `Module [project]/lib/a-page-only.js … was
instantiated … but the module factory is not available`. No test covered
this, because the #97664 test was skipped in dev.
#98823 chains `/_app` and the page into one dev client module graph,
which fixes the missing modules.
It does **not** yet dedupe the `app-document-import-order` fixture in
Turbopack dev. The shared module is still emitted in both the `_app` and
the page chunk, because the page's batches in the combined graph carry
different chunk-group bitmaps than `_app`'s own graph, so they don't
match `_app`'s availability items. Hence the gate. It stays valid
through #98734 and #99750; #98685 (higher in this stack) dedupes the
fixture in Turbopack dev, turning it into a "stale gate", and removes
it.
### How was it verified?
Local JS build of this branch plus the preview native binaries of
#98823's head (`8dd759a`) and its base (`26fd598`) in
`packages/next-swc/native`. The two commits differ only in Rust.
| Suite | Mode | #98823 base | #98823 head |
|---|---|---|---|
| `pages-app-availability-boundary` | Turbopack dev | ❌ module factory
not available | ✅ |
| `pages-app-availability-boundary` | Turbopack start, webpack dev/start
| | ✅ |
| `app-document-import-order` | Turbopack dev | | ✅ (gated: failed as
expected) |
| `app-document-import-order` | webpack dev, Turbopack/webpack start | |
✅ |
After restacking onto canary, with native builds of each layer, in
Turbopack dev:
- This PR's tree: `pages-app-availability-boundary` ✅,
`app-document-import-order` ✅ (gated: failed as expected).
- #98734 and #99750: gated assertion still fails as expected.
- #98685 and #99050: gate stale, so #98685 removes it; ungated, both
suites ✅.
Fixes #99789 (together with #98823)
<!-- NEXT_JS_LLM -->
<!-- fleet a7614af3-e186-4297-beaf-7e55eaddc0da -->
<!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e -->
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>