diff --git a/.gitignore b/.gitignore index 9bf32df156f..5c750072c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ npm-debug.log # couple of the files. Once it is, we can switch this over to just ignoring # `types/stable` entirely. types/stable +_notes diff --git a/package.json b/package.json index 64891b338bb..b6b1f87e957 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,9 @@ "@ember/-internals/metal/index.js": "ember-source/@ember/-internals/metal/index.js", "@ember/-internals/owner/index.js": "ember-source/@ember/-internals/owner/index.js", "@ember/-internals/routing/index.js": "ember-source/@ember/-internals/routing/index.js", + "@ember/-internals/routing/route-managers/classic-route-manager.js": "ember-source/@ember/-internals/routing/route-managers/classic-route-manager.js", + "@ember/-internals/routing/route-managers/route-manager.js": "ember-source/@ember/-internals/routing/route-managers/route-manager.js", + "@ember/-internals/routing/route-managers/utils.js": "ember-source/@ember/-internals/routing/route-managers/utils.js", "@ember/-internals/runtime/index.js": "ember-source/@ember/-internals/runtime/index.js", "@ember/-internals/runtime/lib/ext/rsvp.js": "ember-source/@ember/-internals/runtime/lib/ext/rsvp.js", "@ember/-internals/runtime/lib/mixins/-proxy.js": "ember-source/@ember/-internals/runtime/lib/mixins/-proxy.js", diff --git a/packages/@ember/-internals/container/lib/container.ts b/packages/@ember/-internals/container/lib/container.ts index 540fd98bb35..4997f821103 100644 --- a/packages/@ember/-internals/container/lib/container.ts +++ b/packages/@ember/-internals/container/lib/container.ts @@ -6,12 +6,13 @@ import type { FactoryManager, FullName, } from '@ember/-internals/owner'; -import { setOwner } from '@ember/-internals/owner'; -import dictionary from '@ember/-internals/utils/lib/dictionary'; +import { getOwner, setOwner } from '@ember/-internals/owner'; +import { dictionary } from '@ember/-internals/utils'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import type { DebugRegistry } from './registry'; import type Registry from './registry'; +import type EmberRouter from '@ember/routing/router'; interface LeakTracking { hasContainers(): boolean; @@ -282,6 +283,25 @@ function lookup( ): InternalFactory | object | undefined { let normalizedName = fullName; + if (normalizedName.startsWith('route:')) { + assert('Expected container to have an owner when looking up a route', container.owner); + + let router = container.owner.lookup('router:main') as EmberRouter; + let routeName = normalizedName.split('route:')[1]; + if (routeName !== undefined) { + // For the main app container, pass the name directly — EmberRouter.getRoute + // resolves engine vs app routes internally via _engineInfoByRoute. + // For engine containers (owner !== router's owner), pass the engine owner + // explicitly so EmberRouter.getRoute uses it directly without a name lookup. + const isEngineContainer = getOwner(router) !== container.owner; + const engineOwner = isEngineContainer ? container.owner : undefined; + return router.getRoute(routeName, engineOwner) as + | InternalFactory + | object + | undefined; + } + } + if ( options.singleton === true || (options.singleton === undefined && isSingleton(container, fullName)) diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index 66afa93812e..6e8586bc859 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -499,3 +499,4 @@ export { export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers'; export { default as element } from './lib/helpers/element'; export { uniqueId } from './lib/helpers/unique-id'; +export { makeRouteTemplate } from './lib/component-managers/route-template'; diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts index 823e1239f51..5a6a9626fc9 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts @@ -24,6 +24,7 @@ import { unwrapTemplate } from './unwrap-template'; import type { DynamicScope } from '../renderer'; import type { OutletState } from '../utils/outlet'; import type OutletView from '../views/outlet'; +import type { RouteStateBucket } from '../../../routing'; function instrumentationPayload(def: OutletDefinitionState) { // "main" used to be the outlet name, keeping it around for compatibility @@ -41,8 +42,16 @@ interface OutletInstanceState { export interface OutletDefinitionState { ref: Reference; name: string; + // The thing the outlet renders. For wrapper-driven routes this is the + // invokable returned from `manager.getInvokable` (used as `@Component` on + // the wrapper). For the legacy OutletState path (raw template from + // `setOutletState`) it is the makeRouteTemplate-wrapped component. template: object; - controller: unknown; + // Set on the wrapper-driven path. The outlet helper curries `@Component` + // (the invokable) and `@routeInfo` onto `wrapper` at render time. + wrapper?: object; + invokable?: object; + bucket?: RouteStateBucket; } const CAPABILITIES: InternalComponentCapabilities = { @@ -170,10 +179,7 @@ class OutletComponentManager const OUTLET_MANAGER = new OutletComponentManager(); -const OUTLET_COMPONENT_TEMPLATE = precompileTemplate( - '<@Component @controller={{@controller}} @model={{@model}} />', - { strictMode: true } -); +const OUTLET_COMPONENT_TEMPLATE = precompileTemplate('<@Component />', { strictMode: true }); export class OutletComponent implements ComponentDefinition< OutletDefinitionState, diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index bf90ff27c9e..d4a614ed523 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -857,7 +857,7 @@ export class Renderer extends BaseRenderer { // we can refactor this to do something more direct/less convoluted // and with less setup, but get it working first let outlet = createRootOutlet(view); - let { name, /* controller, */ template } = view.state; + let { name, template } = view.state; let named = dict(); @@ -866,18 +866,6 @@ export class Renderer extends BaseRenderer { '@Component' ); - // TODO: is this guaranteed to be undefined? It seems to be the - // case in the `OutletView` class. Investigate how much that class - // exists as an internal implementation detail only, or if it was - // used outside of core. As far as I can tell, test-helpers uses - // it but only for `setOutletState`. - // named['controller'] = createConstRef(controller, '@controller'); - // Update: at least according to the debug render tree tests, we - // appear to always expect this to be undefined. Not a definitive - // source by any means, but is useful evidence - named['controller'] = UNDEFINED_REFERENCE; - named['model'] = UNDEFINED_REFERENCE; - let args = createCapturedArgs(named, EMPTY_POSITIONAL); this._appendDefinition( diff --git a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts index d0027f2339e..fde7421580b 100644 --- a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts @@ -12,7 +12,6 @@ import { childRefFromParts, createComputeRef, createConstRef, - createDebugAliasRef, valueForRef, } from '@glimmer/reference/lib/reference'; import type { CurriedValue } from '@glimmer/runtime/lib/curried-value'; @@ -89,85 +88,91 @@ export const outletHelper = internalHelper( let named = dict(); - // Here we either have a raw template that needs to be normalized, - // or a component that we can render as-is. `RouteTemplate` upgrades - // the template into a component so we can have a unified code path. - // We still store the original `template` value, because we rely on - // its identity for the stability check, and the `RouteTemplate` - // wrapper doesn't dedup for us. - let template = state.template; let component: object; - if (hasInternalComponentManager(template)) { - component = template; + // Wrapper-driven path: manager-provided wrapper + uncurried invokable. + // Curry the wrapper with @Component (the user's invokable), @routeInfo, + // @model, and @controller. The wrapper template uses these to render + // the route. + if (state.wrapper !== undefined && state.invokable !== undefined) { + let wrapperArgs = dict(); + wrapperArgs['Component'] = createConstRef(state.invokable, '@Component'); + + // @controller must be a const ref because RouteTemplateManager + // uses it as the route template's `self`, which is then passed + // as `caller` to inner internal components (LinkTo, Input, etc.) + // and those assert isConstRef(caller). Resolve the controller + // eagerly here from the route on the routeInfo. The route has an + // idempotent _initController that creates or returns the cached + // controller instance. + if (state.bucket?.controller !== undefined) { + wrapperArgs['controller'] = createConstRef(state.bucket.controller, '@controller'); + } + + // @model is a compute ref over outletRef.render.context. + // The path-based ref consumes outletStateTag; when setOutletState + // dirties the tag (each transition / model update), the ref + // invalidates and re-reads the new context. + let modelRef = childRefFromParts(outletRef, ['render', 'context']); + let model = valueForRef(modelRef); + let frozenState = state; + wrapperArgs['model'] = createComputeRef(() => { + if (lastState === frozenState) { + model = valueForRef(modelRef); + } + return model; + }); + + // isResolved=false because the wrapper is a "definition state" that + // the VM must resolve to a ComponentDefinition via its registered + // setComponentTemplate / setInternalComponentManager metadata. + component = curry( + 0 as CurriedComponent, + state.wrapper, + outletOwner, + createCapturedArgs(wrapperArgs, EMPTY_POSITIONAL), + false + ); } else { - if (DEBUG) { - // We don't appear to have a standard way or a brand to check, but for the - // purpose of avoiding obvious user errors, this probably gets you close - // enough. - let isTemplate = (template: unknown): template is Template => { - if (template === null || typeof template !== 'object') { - return false; - } else { - let t = template as Partial