Repository navigation
Conversation
commit: |
Reproduces the storefront dead-end end to end: a layout that registers a loader with no loading() on the LAYOUT pins every shell capture, and a loading() on the ppr child route does NOT unpin it — the tree-build await lives at the entry that registers the loaders. Previously only the route-level variant (/shell-cache/no-hole) was pinned. New fixtures (test-app shell-cache): - /shell-cache/layout-loader — ShellTrapChromeLayout registers ShellChromeLoader (unconsumed; registration alone pins), child route has its own loader + loading() + ppr:true. Test: 6 spaced GETs stay x-rango-shell: MISS while axis 1 stays healthy. - /shell-cache/slot-hole — the escape from skills/ppr's layout-with-loaders playbook: the same chrome data owned by a @badge parallel slot with its own loader + loading(); neither the layout nor the route carries loading(). Test: warms to HIT; prelude carries chrome, static page content, and the frozen badge fallback (never a badge value); the badge value streams in the resumed tail and its seq advances across HITs (stays live). Both tests run in the dev AND (production) describes via the existing runShellCacheSpec wiring. Full shell-cache suite: 38 passed in both modes. router.named-routes.gen.ts regenerated for the two new named routes.
…rs, handles, and loaders
Design for closing the last doctrine asymmetry: a loader on a no-loading()
entry executes during shell capture, its settled container bakes, and
nested promises hole at the consumer's Suspense — the return shape is the
declaration ({static: 'baked', dynamic: promise}), no new API. loading()
stays the guaranteed live lane, byte-for-byte unchanged.
Grounded in the existing mechanisms: axis 1 already implements
container-settles/nested-streams for loading()-less loaders (e2e-pinned);
capture unmasking + the identity guard losing its loader exemption during
capture; HIT parity via a third capture-data-snapshot family with a
fresh-run + recorded-container overlay. Ships default-on: the only shape
whose behavior changes is today's eternal-MISS structural refusal.
Includes rollout note for the shell-capture.ts:244 warning fix (route-level
framing misled a real migration) and the red->green test plan built on the
layout-loader trap e2e added in the previous commit.
… under a loader layout /shell-cache/layout-loader-bare: a ppr route with no loader, no loading(), no use list at all, under the loader-registering trap layout. The route is fully shell-eligible on its own; the layout's boundary-less loader alone keeps every capture refusing (MISS across 6 spaced GETs, axis 1 healthy), in dev and production. Completes the trap coverage: with-child-loading() and bare variants both pinned.
ivogt
force-pushed
the
test/ppr-layout-loader-e2e
branch
from
July 4, 2026 12:53
714ee8d to
644191e
Compare
Merged
6 tasks done
…scape Same three pins as the test-app suite, on the real KV-backed CFCacheStore under workerd (dev worker + built preview worker): layout-registered loader with no loading() on the layout stays MISS with a loading()-bearing child route; the bare storefront-homepage shape (ppr route with no use list under the loader layout) stays MISS; the @badge parallel-slot escape flips to HIT with the fallback frozen in the prelude and the badge value live (seq advances) across HITs. Full cloudflare-basic ppr-shell suite: 34 passed in dev and production. router.named-routes.gen.ts regenerated for the three new named routes.
ivogt
force-pushed
the
test/ppr-layout-loader-e2e
branch
from
July 4, 2026 13:12
644191e to
8be1a39
Compare
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The most common production shape — an app-wide layout registering session/basket-style loaders, with
ppron a page beneath it — dead-ends today: every shell capture refuses and the route staysx-rango-shell: MISSforever. A real storefront migration hit this and was misled twice: the refusal warning's route-level framing ("add loading() to the loader route") suggested a fix that does not work when the loaders live on a layout, and nothing pinned the actual rules. Only the route-level variant (/shell-cache/no-hole) had e2e coverage.What this PR does
Pins the trap in its exact consumer shapes, pins the working escape, and adds the design doc for the real fix. No framework behavior changes — the router behaves identically before and after this PR.
New e2e (test-app suite AND cloudflare-basic twin — dev + production in both)
/shell-cache/layout-loader+ CF twinloading()loading()does not unpin a parent layout's loaders; the tree-build await lives at the entry that registers them/shell-cache/layout-loader-bare+ CF twin/shell-cache/slot-hole+ CF twin@badgeparallel slot with its ownloader()+loading(); noloading()on layout or routeThe cloudflare-basic twin runs the same three pins on the real KV-backed
CFCacheStoreunder workerd (dev worker + built preview worker).Design doc:
docs/design/loader-container-bake.mdOne promise doctrine for all three lanes: a loader on a no-
loading()entry executes during capture, its settled container bakes, nested promises hole at the consumer's<Suspense>—{ static: 'baked', dynamic: promise }, the return shape as the entire API. Key grounding: axis 1 already implements container-settles/nested-streams forloading()-less loaders (now e2e-pinned by this PR), so the design makes capture match axis 1 rather than inventing semantics. Covers capture unmasking, the identity guard dropping its loader exemption during capture (fail-closed), HIT parity via a third capture-data-snapshot family with a fresh-run + recorded-container overlay, rollout (default-on: the only affected shape is today's eternal-MISS refusal), and open questions. The trap tests in this PR are the red→green vehicle for the implementation.Also included: the promise-holes docs expansion already on main gets its e2e backing here (the
/pprlayout-with-loaders playbook's slot pattern was previously source-verified only).Consumer story
Nothing to adopt in this PR. For a consumer hitting the trap today, the pinned escape is the playbook shape:
Chrome bakes into the shell; the badge is a badge-sized hole; no page-level skeleton exists. After the design doc ships, the same app drops the slot machinery and declares liveness by return shape alone.
Semantics table (all pinned, none changed)
loading()on layout, ppr child (with or without ownloading())loading()loading()(/shell-cache/no-hole)Code changes
packages/rangojs-router/e2e/test-app:shell-cache.defs.ts(+2 loaders),shell-cache.tsx(+2 layouts, 3 routes),components/ShellBadge.tsx(new),router.named-routes.gen.tspackages/rangojs-router/e2e/shell-cache.test.ts: +3 tests in the shared spec (runs in both dev and(production)describes)tests/cloudflare-basic: mirrored fixtures (loaders/ppr-shell.ts,pages/ppr-shell.tsx,components/PprShellBadge.tsx,urls.tsx), +3 tests ine2e/ppr-shell.test.ts(describePprShelldev + production),router.named-routes.gen.tspackages/rangojs-router/docs/design/loader-container-bake.md(new)Test plan
shell-cachesuite: 38 passed × dev + production (test-app)ppr-shellsuite: 34 passed × dev + production (cloudflare-basic, workerd)pnpm run typecheck,pnpm run test:unit:all,pnpm run lint,pnpm run format,pnpm check:e2e-bucketing*.gen.tsregenerated and committed for both apps that gained routesNotes
cloudflare-basic-noncehas no ppr tests); deliberately left out of this PR since nonce work is in flight elsewhere.