Implement the Route Manager API (RFC #1169)#21460
Open
evoactivity wants to merge 42 commits into
Open
Conversation
Introduce a Route Manager layer between the router and route base classes so the router drives routes through a well-defined manager interface instead of calling classic Route methods directly. This decouples the router from the classic Route and is the stepping stone toward alternative route base classes and a future router. Add the manager interface, capabilities, and registration, implement a ClassicRouteManager that encapsulates today's classic Route behaviour behind it, and make router_js dispatch lifecycle, rendering, model resolution, and the classic-interop surface through the manager.
37bd3e6 to
7582a9f
Compare
johanrd
added a commit
to johanrd/ember.js
that referenced
this pull request
Jun 15, 2026
…s on emberjs#21460 Reverts the outlet.ts compute-ref guard to the current ember-7.0.0 behavior, demonstrating that the @model-during-willDestroy instability (emberjs#18987) still reproduces on top of the Route Manager RFC implementation (emberjs#21460). The smoke-test job's '@model stability during route transitions' tests are expected to fail here. Not for merge.
8 tasks
refactor: add manager.getRoute(bucket)
…d through `setOutletState`
…fill the resolvedModels and context in becomeResolved
- wire up the getDestroyable
…a weakmap keyed by the route associates the manager and bucket, so the routeInfo can look them up instead of requiring managers to put the bucket and manager on the route class itself. Also aligns the lifecycle hook states with the RFC (NavigationState from/to, cancel on willExit) and gates the classic interop args behind the classicInterop capability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ugh the manager contract
Deletes the uncalled Route.enter/exit (private, no compat promise — their bodies live in the classic manager). ClassicRouteBucket.controller is now a getter over route.controller, fixing loading/error substate templates rendering with an undefined {{this}} (substates skip willEnter, so the eager copy was never populated); regression test included.
Loading and error substates now follow one symmetric flow: the manager fires the classic event (loading from willEnter's timer, error from an unhandled rejection), it bubbles through the app's actions handlers, and the router's default action handler forwards unhandled events back through the new enterLoadingSubstate/enterErrorSubstate interop methods. This restores the public actions.loading and willResolveModel APIs the manager containment had dropped, and removes EmberRouter's imports from the classic manager package. The loading-action test was rewritten to genuinely exercise a slow transition with mandatory assertions — its key assertion was previously unreachable and optional.
Also: invokable cached once in buildClassicInvokable, one shared copyDefaultValue, and _setOutlets is the single shouldRender gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The {{outlet}} helper now curries once: @component (the manager's wrapper) and the live @context ref go onto OutletComponent, whose template forwards them — the anonymous inner curried layer is gone. The root outlet uses an arg-less template variant so an always-undefined @context doesn't leak into the debug render tree's named args.
OutletDefinitionState loses its template/controller dummy fields: the root OutletView state carries its upgraded invokable (built in the constructor — all template→invokable upgrading now lives in views/outlet.ts, and renderer.appendOutletView just reads it), and per-outlet states carry the controller/wrapper/invokable the stability check keys on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… regression tests afterModel can once again swap the resolved model by writing into transition.resolvedModels: the classic manager's enter() re-reads the stash after afterModel instead of resolving with the captured value. router_js had a test for this contract, but it exercised the test-helper route manager (which re-read faithfully) rather than the production classic manager (which didn't) — a new app-level test now covers the production path. Resolving a route info no longer writes the entered context onto the unresolved info: the enter-promise subscription captures it into a local passed to becomeResolved. shouldSupersede treats an own `context` as meaningful when route infos are reused across transitions, so fabricating one changed superseding semantics vs main; a unit test guards the parity. Both fixes were verified red/green (tests written first, failing on the divergent behavior). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EmberRouter.getRoute: drop the never-passed engineOwner param, cache {manager, bucket} per (owner, route name) so repeat lookups skip factory resolution, the auto-generation check, the engine-serialize check, and the registry walk, and document why the 'undefined' name guard exists (without it, auto-generation would register a junk route:undefined).
getAncestorContext now only matches true ancestors — a route asking about itself or a descendant gets undefined instead of a pending enter promise (deadlock bait for a parallel manager); unit test covers both directions.
Also types ClassicRouteBucket.loadingSubstateTimer properly, removing a cast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d coalesce incremental outlet renders The outlet retakes responsibility for currying @component and @bucket onto the module-stable wrapper and keys stability on the invokable, so managers need no glimmer internals. A manager can omit the wrapper entirely (one component boundary per outlet level, ~8-10µs/level benchmarked) — measured wrapper-less, route-manager machinery is at parity with pre-manager Ember. Mid-transition outlet renders are batched onto the next timer tick (12 passes down to 2 on a 9-deep transition) without changing settle/substate timing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The outlet has a single arg-less layout again: wrapped renders curry @Component/@bucket/@context onto the module-stable wrapper, wrapper-less renders curry @context onto the invokable itself (curried refs stay live). The outlet curries exactly what the target can't obtain for itself — a module-stable wrapper can't know its route; a per-bucket invokable already does, except for the live context ref only the outlet can build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* upstream/main: Post-release version bump Add v7.2.0-beta.1 to CHANGELOG for `ember-source` sanitize url attributes case-insensitively Improve some remaining side-effects and update tests for telling us which modules have side-effects [BUGFIX] Include named argument hint in debugger message for class-backed components [BUGFIX] Improve debugger message for template-only components # Conflicts: # packages/@ember/-internals/glimmer/lib/renderer.ts
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.
This is a big PR for a big feature.
Introduces a Route Manager layer between the router and route base classes so the router drives routes through a well-defined manager interface instead of calling classic Route methods directly. This decouples the router from the classic Route and is the stepping stone toward alternative route base classes and a future router. The classic Route behaves exactly as before, there are no changes to app authoring.
Three layers, top to bottom:
@ember/routingpublic re-exports of the authoring API@ember/-internals/routing/route-managersThe classic route manager and route manager infrastructurerouter_jsDrives the routes lifecycle through the manager, and owns the route manager contractWhy does the outlet have a legacy path?
A handful of tests directly set the outlet state, one in particular is testing something for liquid-fire. If there are addons or user code that expects to be able to create their own render state, they will not include the route wrapper and invokable we expect from the route manager, the legacy path path lets them continue working.
Query Params
The current implementation of query params is driven by the router, I have gated the parts that directly reach into routes behind the classic interop capability of route managers. I have left the "machinery" in place in the router. When we come to replace the router, a decision will need to be made if we bridge the classic router manager to use whatever new QP implementation we come up with, or if we fully migrate the router.js QP implementation to be fully encapsulated by the classic route manager.
RFC #1169