diff --git a/.gitignore b/.gitignore index 9bf32df156f..86bbbbb3da9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ test/version_tmp test_*.html /tests/ember-tests.js /smoke-tests/scenarios/output/ +/smoke-tests/scenarios/output-*/ tmp tmp*.gem tmp.bpm diff --git a/package.json b/package.json index 86c1d3d37db..a005a274a8f 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,13 @@ "@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/api.js": "ember-source/@ember/-internals/routing/route-managers/api.js", + "@ember/-internals/routing/route-managers/classic/bucket.js": "ember-source/@ember/-internals/routing/route-managers/classic/bucket.js", + "@ember/-internals/routing/route-managers/classic/manager.js": "ember-source/@ember/-internals/routing/route-managers/classic/manager.js", + "@ember/-internals/routing/route-managers/classic/query-params.js": "ember-source/@ember/-internals/routing/route-managers/classic/query-params.js", + "@ember/-internals/routing/route-managers/classic/substates.js": "ember-source/@ember/-internals/routing/route-managers/classic/substates.js", + "@ember/-internals/routing/route-managers/classic/wrapper.js": "ember-source/@ember/-internals/routing/route-managers/classic/wrapper.js", + "@ember/-internals/routing/route-managers/registry.js": "ember-source/@ember/-internals/routing/route-managers/registry.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/glimmer/lib/component-managers/outlet.ts b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts index aba8e2e979d..f3825d3f60c 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts @@ -41,8 +41,17 @@ interface OutletInstanceState { export interface OutletDefinitionState { ref: Reference; name: string; - template: object; - controller: unknown; + + /** + * What this outlet renders. The root `OutletView` provides the upgraded + * root template as `invokable`; per-outlet states built by the `{{outlet}}` + * helper carry the manager's `wrapper` (when present) plus `invokable` and + * `controller`, which the helper's stability check keys on. + */ + controller?: unknown; + wrapper?: object; + invokable?: object; + bucket?: object; } const CAPABILITIES: InternalComponentCapabilities = { @@ -171,10 +180,15 @@ class OutletComponentManager const OUTLET_MANAGER = /*@__PURE__*/ new OutletComponentManager(); -const OUTLET_COMPONENT_TEMPLATE = precompileTemplate( - '<@Component @controller={{@controller}} @model={{@model}} />', - { strictMode: true } -); +// The one outlet layout. `@Component` is always a value that already +// carries everything it needs — the outlet helper curries the args (the +// invokable/bucket/live context for wrapped renders, the live context for +// wrapper-less ones) onto the render target before handing it over, and the +// root outlet's invokable takes no args at all. Keeping the layout arg-less +// also keeps stray named args out of the debug render tree. +const OUTLET_COMPONENT_TEMPLATE = precompileTemplate('<@Component />', { + strictMode: true, +}); export class OutletComponent implements ComponentDefinition< OutletDefinitionState, diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts b/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts index c1cba0428ef..cc2acd2db1a 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts @@ -1,5 +1,4 @@ import type { InternalOwner } from '@ember/-internals/owner'; -import { _instrumentStart } from '@ember/instrumentation'; import type { CapturedArguments, CompilableProgram, @@ -14,20 +13,19 @@ import type { WithCustomDebugRenderTree, } from '@glimmer/interfaces'; import type { Nullable } from '@ember/-internals/utility-types'; -import { DEBUG } from '@glimmer/env'; import { capabilityFlagsFrom } from '@glimmer/manager/lib/util/capabilities'; import type { Reference } from '@glimmer/reference/lib/reference'; -import { createDebugAliasRef, valueForRef } from '@glimmer/reference/lib/reference'; +import { UNDEFINED_REFERENCE, valueForRef } from '@glimmer/reference/lib/reference'; import { curry, type CurriedValue } from '@glimmer/runtime/lib/curried-value'; import { unwrapTemplate } from './unwrap-template'; interface RouteTemplateInstanceState { self: Reference; - controller: unknown; } export interface RouteTemplateDefinitionState { name: string; + self: Reference; } const CAPABILITIES: InternalComponentCapabilities = { @@ -55,18 +53,10 @@ class RouteTemplateManager { create( _owner: InternalOwner, - _definition: RouteTemplateDefinitionState, - args: VMArguments + definition: RouteTemplateDefinitionState, + _args: VMArguments ): RouteTemplateInstanceState { - let self = args.named.get('controller'); - - if (DEBUG) { - self = createDebugAliasRef!('this', self); - } - - let controller = valueForRef(self); - - return { self, controller }; + return { self: definition.self }; } getSelf({ self }: RouteTemplateInstanceState): Reference { @@ -88,7 +78,7 @@ class RouteTemplateManager type: 'route-template', name, args, - instance: state.controller, + instance: valueForRef(state.self), }, ]; } @@ -111,7 +101,7 @@ class RouteTemplateManager const ROUTE_TEMPLATE_MANAGER = /*@__PURE__*/ new RouteTemplateManager(); /** - * This "upgrades" a route template into a invocable component. Conceptually + * This "upgrades" a route template into a invokable component. Conceptually * it can be 1:1 for each unique `Template`, but it's also cheap to construct, * so unless the stability is desirable for other reasons, it's probably not * worth caching this. @@ -129,13 +119,13 @@ export class RouteTemplate implements ComponentDefinition< public capabilities = CAPABILITIES_MASK; public compilable: CompilableProgram; - constructor(name: string, template: Template) { + constructor(name: string, template: Template, self: Reference) { let unwrapped = unwrapTemplate(template); // TODO This actually seems inaccurate – it ultimately came from the // outlet's name. Also, setting this overrides `getDebugName()` in that // message. Is that desirable? this.resolvedName = name; - this.state = { name }; + this.state = { name, self }; this.compilable = unwrapped.asLayout(); } } @@ -150,8 +140,9 @@ export class RouteTemplate implements ComponentDefinition< export function makeRouteTemplate( owner: InternalOwner, name: string, - template: Template + template: Template, + self: Reference = UNDEFINED_REFERENCE ): CurriedValue { - let routeTemplate = new RouteTemplate(name, template); + let routeTemplate = new RouteTemplate(name, template, self); return curry(0 as CurriedComponent, routeTemplate, owner, null, true); } diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index e533c4042ba..90334f6d5aa 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -41,7 +41,6 @@ import { RootComponentDefinition } from './component-managers/root'; import RouterResolver from './router-resolver'; import type { OutletState } from './utils/outlet'; import OutletView from './views/outlet'; -import { makeRouteTemplate } from './component-managers/route-template'; import type { IBuilder, RendererRoot } from './base-renderer'; import { BaseRenderer, errorLoopTransaction } from './base-renderer'; @@ -218,31 +217,25 @@ export class Renderer extends BaseRenderer { // renderer HOOKS appendOutletView(view: OutletView, target: SimpleElement): void { + // The append is scheduled into the `render` queue, so a teardown that + // starts in the same run loop (e.g. a deferred incremental outlet pass + // racing test cleanup) can destroy this renderer before the queue + // flushes. Rendering into a destroying renderer would assert deep in + // the destroyable graph; skip instead. + if (isDestroying(this) || isDestroyed(this)) { + return; + } + // TODO: This bypasses the {{outlet}} syntax so logically duplicates // some of the set up code. Since this is all internal (or is it?), // 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 { invokable } = view.state; + assert('[BUG] OutletView state is unexpectedly missing its root invokable', invokable); let named = dict(); - - named['Component'] = createConstRef( - makeRouteTemplate(view.owner, name, template as Template), - '@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; + named['Component'] = createConstRef(invokable, '@Component'); let args = createCapturedArgs(named, EMPTY_POSITIONAL); diff --git a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts index f39637e320b..ce2303b0c03 100644 --- a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts @@ -1,12 +1,7 @@ import type { InternalOwner } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import type { - CapturedArguments, - CurriedComponent, - DynamicScope, - Template, -} from '@glimmer/interfaces'; +import type { CapturedArguments, CurriedComponent, DynamicScope } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; import { childRefFromParts, @@ -19,9 +14,7 @@ import type { CurriedValue } from '@glimmer/runtime/lib/curried-value'; import { createCapturedArgs, EMPTY_POSITIONAL } from '@glimmer/runtime/lib/vm/arguments'; import { curry } from '@glimmer/runtime/lib/curried-value'; import { dict } from '@glimmer/util/lib/collections'; -import { hasInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import { OutletComponent, type OutletDefinitionState } from '../component-managers/outlet'; -import { makeRouteTemplate } from '../component-managers/route-template'; import { internalHelper } from '../helpers/internal-helper'; import type { OutletState } from '../utils/outlet'; @@ -33,15 +26,15 @@ import type { OutletState } from '../utils/outlet'; ```app/templates/application.gjs import MyHeader from '../components/my-header'; import MyFooter from '../components/my-footer'; - + ``` @@ -50,8 +43,8 @@ import type { OutletState } from '../utils/outlet'; information on how your `route` interacts with the `{{outlet}}` helper. Note: Your content __will not render__ if there isn't an `{{outlet}}` for it. - `outlet` is built-in and does not need to be imported. - + `outlet` is built-in and does not need to be imported. + @method outlet @for Ember.Templates.helpers @public @@ -86,110 +79,86 @@ export const outletHelper = /*@__PURE__*/ internalHelper( // same route, but return a different one when the route changes. On the // other hand, changing the model only intentionally do not teardown the // component and instead re-render in-place. - if (!isStable(state, lastState)) { - lastState = state; - - if (state !== null) { - // If we are crossing an engine mount point, this is how the owner - // gets switched. - let outletOwner = outletState?.render?.owner ?? owner; - - 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; - } 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