mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
1c69ce3fef
* fix(pages-router): load custom _app/_document via resolved file paths in dev The dev server imported pages/_app and pages/_document by extensionless id and relied on the module runner applying custom resolve.extensions (e.g. ".page.tsx" from pageExtensions). The runner no longer resolves those, so apps using pageExtensions silently lost their custom App/Document (the load failure is swallowed). Import the file path resolved by findFileWithExts instead, at all three call sites. * test(e2e): add pages-router-complex example app and behaviour suite A deliberately convoluted Pages Router app that serves as a compatibility target: the kinds of patterns that only surface in large, long-lived enterprise pages-router codebases, each pinned by a Playwright spec whose behaviour is verified against real Next.js (73/73 under next dev --webpack). Highlights: custom pageExtensions everywhere (middleware.page.ts, instrumentation.page.ts), app-shell getInitialProps with a TTL-memoised chrome fetch and embedded-shell/draft bypasses, class _document with its own GIP, a zone route dimension driven by middleware rewrites and a real i18next/react-i18next runtime, catch-all routes with static-sibling precedence and cacheable-404/redirect hygiene, record-driven template branching on a dynamic/static/dynamic route, an urql-style server-snapshot data layer, shallow routing + router.events + next/compat/router + next/navigation hooks in pages, next/image with a custom loader, draft-mode gateway APIs, a function-form next.config, and a Cloudflare setup matching the vinext init scaffold. The playwright project (pages-router-complex, port 4199) is intentionally not in the CI e2e matrix: vinext currently passes 59/73, and the failures are the compat backlog documented in the example README. * fix(pages-router): cover compound page extensions in CI
pages-router-complex
A deliberately convoluted Pages Router app. It exists as a compatibility
target for vinext: the patterns here are the kind that only surface in big,
old, enterprise pages-router codebases. It is set up for Cloudflare the way
vinext init scaffolds it (vinext + cloudflare Vite plugins, KV data cache,
Workers CDN cache, Images optimizer, wrangler.jsonc with the
vinext/server/fetch-handler worker entry).
It is expected to run correctly under real Next.js (pnpm dev:next).
Running it under vinext (pnpm dev) is the goal, not (yet) the guarantee —
the e2e suite in tests/e2e/pages-router-complex/ documents the target
behaviour and may fail under vinext today.
The patterns being exercised
pageExtensions: ["page.tsx", "page.ts"]— every routable file uses the suffix, non-page helpers live alongside pages (pages/shell-initial-props.ts), andmiddleware.page.ts/instrumentation.page.tscarry the suffix too.- App-shell
getInitialProps(disables automatic static optimisation) that fetches masthead/baseboard chrome through an in-memory TTL memoiser, skips the fetch entirely for embedded-shell (cookie-flagged) requests, bypasses the memo in draft mode, reads env-based settings, and emits a beacon when it re-runs client-side on navigations. - Extra named exports from special files:
_appre-exports a conditional top-level-awaitoutboundStubpromise;middleware.page.tsexports an unrelated async loader alongsidemiddleware+config. - Class-based
_documentwith its owngetInitialProps(palette derived fromctx.req.url,<html lang>from the zone),beforeInteractivescripts (external +dangerouslySetInnerHTMLbootstrap), raw inline<script>tags, and data attributes on<body>. - Middleware pipeline (regex
matcherform): CDN-prefix rewrite (/atlas/cdn/_next/*→/_next/*), a 403 for/_next/image, a JSONhardNavToresponse for raw/_next/datarequests (skipMiddlewareUrlNormalize), editor-draft redirect on a request header, draft-cookie scrubbing for API routes viaNextResponse.next({ request }), then a single-segment zone rewrite/redirect with anx-zoneresponse header. - A
[zone]route dimension hidden from public URLs for the home zone, feeding a real i18next/react-i18next runtime (one instance per tree, synchronous init for SSR, per-zone bundles with fallback) and a zone-awarenext/linkwrapper for all chrome links. - Catch-all + siblings:
gallery/[...facets]with static siblings that must win precedence (gallery/curated/first,gallery/directory/a-z), facet-dedupe redirects, character-scrub redirects, permanent deep-trail collapses, cacheable 404s, and per-wall surrogate TTL overrides. - A dynamic/static/dynamic route sandwich (
[collection]/item/[assetId]) whose page-data function branches into three templates off what the catalogue record says the asset is (withdrawn records and malformed ids 404 first). getServerSidePropswrapped in a metering HOF on every page, custom CDN cache headers (Surrogate-Control/Surrogate-Key), a conditional CSP response-header side effect, and one page with no data-fetching function at all (detail-tools/client-flags).- Server-snapshot data layer: gSSP runs ops through a server handle that
records a snapshot, the snapshot rides page props, and the browser handle
is seeded with it so hydration reads from memory (
useGraphOp). - Client-side routing machinery: shallow
router.pushwith the internal dynamic-route pattern aspathnameand the public URL asas;router.eventsdriving a transition overlay with a failsafe timeout; a router-agnostic reset hook onnext/compat/router+usePathname. - App Router hooks in Pages Router pages:
useSearchParamsseeding initial state andusePathname, combined with rawwindow.history.replaceStatequery updates that bypass the router. next/imagewith a custom loader infillmode + css-module class on the fault screens — the framework optimizer endpoint is never used, which is what makes the middleware's/_next/image403 safe.- API routes: draft-mode gateway with landing-path resolution, a
bearer-gated memo purge endpoint, a cookie-reflecting upstream relay proxy
in promise-chain style, a legacy path rewritten in
afterFiles, and a 204-with-cache-headers type-ahead shim. next.config.js(CJS) exporting the function form — an async function of(phase, context)built by a decorator composer — with a throwinggenerateBuildId, prod-onlyassetPrefix, env-conditionalfallbackrewrites, and a webpack hook (workspace alias + test-module replacement viaNormalModuleReplacementPlugin).instrumentation.page.tsgated onNEXT_RUNTIME === "nodejs", with its effect observable through/api/status.
Running
pnpm dev # vinext dev server (needs RELEASE_TAG for now, see below)
pnpm dev:next # real Next.js dev server (ground truth; --webpack, see below)
pnpm build # vinext build (RELEASE_TAG is required and set inline)
pnpm build:next # next build
# The behaviour suite (server starts under vinext automatically):
PLAYWRIGHT_PROJECT=pages-router-complex pnpm run test:e2e
Known findings
- Next.js 16.2.7 + Turbopack fails to compile: the global-CSS-in-_app
validation false-positives when
pageExtensionsrenames_appto_app.page.tsx. Thedev:next/build:nextscripts pass--webpack. - TypeScript 7 (native preview): Next's own tsconfig-paths support
degrades under it, so the
@atlas/*alias is wired into both bundlers explicitly (webpack hook + Viteresolve.alias); tsconfigpaths(without the removedbaseUrl) stays authoritative for the type checker. - vinext dev (Cloudflare plugin): 59/73 specs pass; the 14 known gaps are
marked
test.fixmeso the passing surface runs in CI. Known gaps:generateBuildIdis invoked at dev startup (Next.js only calls it at build time — the e2e server exportsRELEASE_TAGto compensate), shallow routingrouter.events(including a hydration knock-on that breaks page interactivity), thenext/imagecustom-loader/fillpath, raw/_next/datainterception and the/_next/image403 middleware branches, thehistory.replaceState-beside-the-router hybrid, the gallery scrub redirect, cacheable-404 surrogate headers, theafterFilestype-ahead rewrite, and the purge endpoint pair (possibly env vars not reaching the workerd runtime).
- The pinned workerd binary caps
compatibility_dateat 2026-04-08, sowrangler.jsoncpins 2026-04-01 rather than the scaffold's "today".
Layout
lib/— the app's internal platform libraries, consumed through tsconfig path aliases (@atlas/*) the way a monorepo app consumes shared packages: zones (zone routing + i18next runtime), edge-policy (CDN headers), memo (TTL result cache), beacon (metrics/RUM), graph-handle (data layer), chrome (masthead/baseboard), wiring + blocks (the provider pyramid and frame), trials, draft, and so on.helpers/,surfaces/— app-level page-data helpers and page templates.pages/— the route tree described above.