From be954758e7e66ad0c1c2a94772817c1771cb2193 Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Sun, 27 Sep 2020 20:55:05 -0700 Subject: [PATCH] [FEAT] Implements Helper Managers Implements the helper manager feature specified in RFC 625. Highlights: 1. Adds `getDebugName` to the interface for helper managers. This is an optional hook that is used for better logging purposes, and matches other internal APIs we've added recently. 2. `hasScheduledEffect` has not yet been implemented, and attempting to use it will cause an assertion to be thrown. Helper managers are not exposed with this PR, and the version passed to `helperCapabilities` is optimistic, but can be changed when we do expose them (along with an appropriate feature flag). Co-authored-by: Robert Jackson --- packages/@ember/-internals/glimmer/index.ts | 8 +- .../@ember/-internals/glimmer/lib/helper.ts | 110 ++++++- .../-internals/glimmer/lib/helpers/custom.ts | 86 +++++ .../@ember/-internals/glimmer/lib/resolver.ts | 62 ++-- .../-internals/glimmer/lib/utils/managers.ts | 26 +- .../glimmer/lib/utils/references.ts | 40 --- .../helpers/helper-manager-test.js | 294 ++++++++++++++++++ packages/@ember/canary-features/index.ts | 2 + packages/ember/index.js | 7 + packages/ember/tests/reexports_test.js | 12 +- .../lib/test-cases/rendering.js | 4 + 11 files changed, 561 insertions(+), 90 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/lib/helpers/custom.ts delete mode 100644 packages/@ember/-internals/glimmer/lib/utils/references.ts create mode 100644 packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index 0a65246c8a2..d9139b7b000 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -403,14 +403,10 @@ export { default as AbstractComponentManager } from './lib/component-managers/ab export { INVOKE } from './lib/helpers/action'; export { default as OutletView } from './lib/views/outlet'; export { OutletState } from './lib/utils/outlet'; +export { setComponentManager, setModifierManager, setHelperManager } from './lib/utils/managers'; export { capabilities } from './lib/component-managers/custom'; -export { - setComponentManager, - getComponentManager, - setModifierManager, - getModifierManager, -} from './lib/utils/managers'; export { capabilities as modifierCapabilities } from './lib/modifiers/custom'; +export { helperCapabilities, HelperManager } from './lib/helpers/custom'; export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers'; export { setComponentTemplate, getComponentTemplate } from './lib/utils/component-template'; export { CapturedRenderNode } from './lib/utils/debug-render-tree'; diff --git a/packages/@ember/-internals/glimmer/lib/helper.ts b/packages/@ember/-internals/glimmer/lib/helper.ts index 4403bb613f0..4362b2f16c1 100644 --- a/packages/@ember/-internals/glimmer/lib/helper.ts +++ b/packages/@ember/-internals/glimmer/lib/helper.ts @@ -4,10 +4,18 @@ import { Factory } from '@ember/-internals/owner'; import { FrameworkObject } from '@ember/-internals/runtime'; -import { symbol } from '@ember/-internals/utils'; +import { getDebugName, symbol } from '@ember/-internals/utils'; import { join } from '@ember/runloop'; -import { Dict } from '@glimmer/interfaces'; -import { createTag, dirtyTag } from '@glimmer/validator'; +import { DEBUG } from '@glimmer/env'; +import { Arguments, Dict } from '@glimmer/interfaces'; +import { + consumeTag, + createTag, + deprecateMutationsInTrackingTransaction, + dirtyTag, +} from '@glimmer/validator'; +import { helperCapabilities, HelperManager } from './helpers/custom'; +import { setHelperManager } from './utils/managers'; export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG'); @@ -30,18 +38,6 @@ export interface SimpleHelper { compute: HelperFunction; } -export function isHelperFactory( - helper: any | undefined | null -): helper is Factory> { - return ( - typeof helper === 'object' && helper !== null && helper.class && helper.class.isHelperFactory - ); -} - -export function isClassHelper(helper: SimpleHelper | HelperInstance): helper is HelperInstance { - return (helper as any).destroy !== undefined; -} - /** Ember Helpers are functions that can compute values, and are used in templates. For example, this code calls a helper named `format-currency`: @@ -138,6 +134,56 @@ let Helper = FrameworkObject.extend({ Helper.isHelperFactory = true; +interface ClassicHelperStateBucket { + instance: HelperInstance; + args: Arguments; +} + +class ClassicHelperManager implements HelperManager { + capabilities = helperCapabilities('3.23', { + hasValue: true, + hasDestroyable: true, + }); + + createHelper(definition: ClassHelperFactory, args: Arguments) { + return { + instance: definition.create(), + args, + }; + } + + getDestroyable({ instance }: ClassicHelperStateBucket) { + return instance; + } + + getValue({ instance, args }: ClassicHelperStateBucket) { + let ret; + let { positional, named } = args; + + if (DEBUG) { + deprecateMutationsInTrackingTransaction!(() => { + ret = instance.compute(positional, named); + }); + } else { + ret = instance.compute(positional, named); + } + + consumeTag(instance[RECOMPUTE_TAG]); + + return ret; + } + + getDebugName(definition: ClassHelperFactory) { + return getDebugName!(definition.class!['prototype']); + } +} + +export const CLASSIC_HELPER_MANAGER = new ClassicHelperManager(); + +setHelperManager(() => CLASSIC_HELPER_MANAGER, Helper); + +/////////// + class Wrapper implements HelperFactory { isHelperFactory: true = true; @@ -151,6 +197,40 @@ class Wrapper implements HelperFactory { } } +class SimpleClassicHelperManager implements HelperManager<() => unknown> { + capabilities = helperCapabilities('3.23', { + hasValue: true, + }); + + createHelper(definition: Wrapper, args: Arguments) { + if (DEBUG) { + return () => { + let ret; + + deprecateMutationsInTrackingTransaction!(() => { + ret = definition.compute.call(null, args.positional, args.named); + }); + + return ret; + }; + } + + return definition.compute.bind(null, args.positional, args.named); + } + + getValue(fn: () => unknown) { + return fn(); + } + + getDebugName(definition: Wrapper) { + return getDebugName!(definition.compute); + } +} + +export const SIMPLE_CLASSIC_HELPER_MANAGER = new SimpleClassicHelperManager(); + +setHelperManager(() => SIMPLE_CLASSIC_HELPER_MANAGER, Wrapper.prototype); + /** In many cases it is not necessary to use the full `Helper` class. The `helper` method create pure-function helpers without instances. diff --git a/packages/@ember/-internals/glimmer/lib/helpers/custom.ts b/packages/@ember/-internals/glimmer/lib/helpers/custom.ts new file mode 100644 index 00000000000..3f673627420 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/helpers/custom.ts @@ -0,0 +1,86 @@ +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import { Arguments, Helper as GlimmerHelper } from '@glimmer/interfaces'; +import { createComputeRef, UNDEFINED_REFERENCE } from '@glimmer/reference'; +import { argsProxyFor } from '../utils/args-proxy'; + +export type HelperDefinition = object; + +export interface HelperCapabilities { + hasValue: boolean; + hasDestroyable: boolean; + hasScheduledEffect: boolean; +} + +export function helperCapabilities( + managerAPI: string, + options: Partial = {} +): HelperCapabilities { + assert('Invalid helper manager compatibility specified', managerAPI === '3.23'); + + assert( + 'You must pass either the `hasValue` OR the `hasScheduledEffect` capability when defining a helper manager. Passing neither, or both, is not permitted.', + (options.hasValue || options.hasScheduledEffect) && + !(options.hasValue && options.hasScheduledEffect) + ); + + assert( + 'The `hasScheduledEffect` capability has not yet been implemented for helper managers. Please pass `hasValue` instead', + !options.hasScheduledEffect + ); + + return { + hasValue: Boolean(options.hasValue), + hasDestroyable: Boolean(options.hasDestroyable), + hasScheduledEffect: Boolean(options.hasScheduledEffect), + }; +} + +export interface HelperManager { + capabilities: HelperCapabilities; + + createHelper(definition: HelperDefinition, args: Arguments): HelperStateBucket; + + getDebugName?(definition: HelperDefinition): string; +} + +export interface HelperManagerWithValue + extends HelperManager { + getValue(bucket: HelperStateBucket): unknown; +} + +function hasValue(manager: HelperManager): manager is HelperManagerWithValue { + return manager.capabilities.hasValue; +} + +export interface HelperManagerWithDestroyable + extends HelperManager { + getDestroyable(bucket: HelperStateBucket): object; +} + +function hasDestroyable(manager: HelperManager): manager is HelperManagerWithDestroyable { + return manager.capabilities.hasDestroyable; +} + +export default function customHelper( + manager: HelperManager, + definition: HelperDefinition +): GlimmerHelper { + return (args, vm) => { + const bucket = manager.createHelper(definition, argsProxyFor(args.capture(), 'helper')); + + if (hasDestroyable(manager)) { + vm.associateDestroyable(manager.getDestroyable(bucket)); + } + + if (hasValue(manager)) { + return createComputeRef( + () => manager.getValue(bucket), + null, + DEBUG && manager.getDebugName && manager.getDebugName(definition) + ); + } else { + return UNDEFINED_REFERENCE; + } + }; +} diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index 7ae05061e22..7cdb26e7b5f 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -2,13 +2,15 @@ import { privatize as P } from '@ember/-internals/container'; import { ENV } from '@ember/-internals/environment'; import { Factory, FactoryClass, LookupOptions, Owner } from '@ember/-internals/owner'; import { OwnedTemplateMeta } from '@ember/-internals/views'; -import { EMBER_GLIMMER_SET_COMPONENT_TEMPLATE } from '@ember/canary-features'; +import { + EMBER_GLIMMER_HELPER_MANAGER, + EMBER_GLIMMER_SET_COMPONENT_TEMPLATE, +} from '@ember/canary-features'; import { isTemplateOnlyComponent } from '@ember/component/template-only'; import { assert, deprecate } from '@ember/debug'; import { PARTIALS } from '@ember/deprecated-features'; import EmberError from '@ember/error'; import { _instrumentStart } from '@ember/instrumentation'; -import { DEBUG } from '@glimmer/env'; import { ComponentDefinition, Helper, @@ -17,13 +19,19 @@ import { RuntimeResolver, } from '@glimmer/interfaces'; import { PartialDefinitionImpl } from '@glimmer/opcode-compiler'; -import { getDynamicVar, ModifierDefinition, registerDestructor } from '@glimmer/runtime'; +import { getDynamicVar, ModifierDefinition } from '@glimmer/runtime'; import { CurlyComponentDefinition } from './component-managers/curly'; import { CustomManagerDefinition } from './component-managers/custom'; import { InternalComponentDefinition, isInternalManager } from './component-managers/internal'; import { TemplateOnlyComponentDefinition } from './component-managers/template-only'; import InternalComponent from './components/internal'; -import { isClassHelper, isHelperFactory } from './helper'; +import { + CLASSIC_HELPER_MANAGER, + HelperFactory, + HelperInstance, + SIMPLE_CLASSIC_HELPER_MANAGER, + SimpleHelper, +} from './helper'; import { default as componentAssertionHelper } from './helpers/-assert-implicit-component-helper-argument'; import { default as inElementNullCheckHelper } from './helpers/-in-element-null-check'; import { default as normalizeClassHelper } from './helpers/-normalize-class'; @@ -31,6 +39,7 @@ import { default as trackArray } from './helpers/-track-array'; import { default as action } from './helpers/action'; import { default as array } from './helpers/array'; import { default as concat } from './helpers/concat'; +import customHelper from './helpers/custom'; import { default as eachIn } from './helpers/each-in'; import { default as fn } from './helpers/fn'; import { default as get } from './helpers/get'; @@ -48,8 +57,7 @@ import { mountHelper } from './syntax/mount'; import { outletHelper } from './syntax/outlet'; import { Factory as TemplateFactory, OwnedTemplate } from './template'; import { getComponentTemplate } from './utils/component-template'; -import { getComponentManager, getModifierManager } from './utils/managers'; -import { createHelperRef } from './utils/references'; +import { getComponentManager, getHelperManager, getModifierManager } from './utils/managers'; function instrumentationPayload(name: string) { return { object: `component:${name}` }; @@ -358,32 +366,32 @@ export default class RuntimeResolverImpl implements RuntimeResolver>( + `helper:${name}`, + options + ) || owner.factoryFor(`helper:${name}`); - if (!isHelperFactory(factory)) { + if (factory === undefined || factory.class === undefined) { return null; } - return (args, vm) => { - const helper = factory.create(); - - if (isClassHelper(helper)) { - let helperDestroyable = {}; - - // Do this so that `destroy` gets called correctly - registerDestructor(helperDestroyable, () => helper.destroy(), true); - vm.associateDestroyable(helperDestroyable); - } else if (DEBUG) { - // Bind to null in case someone accidentally passed an unbound function - // in, and attempts use `this` on it. - // - // TODO: Update buildUntouchableThis to be flexible enough to provide a - // nice error message here. - helper.compute = helper.compute.bind(null); - } + const manager = getHelperManager(owner, factory.class); - return createHelperRef(helper, args.capture()); - }; + if (manager === undefined) { + return null; + } + + assert( + 'helper managers have not been enabled yet, you must use classic helpers', + EMBER_GLIMMER_HELPER_MANAGER || + manager === CLASSIC_HELPER_MANAGER || + manager === SIMPLE_CLASSIC_HELPER_MANAGER + ); + + // For classic class based helpers, we need to pass the factoryFor result itself rather + // than the raw value (`factoryFor(...).class`). This is because injections are already + // bound in the factoryFor result, including type-based injections + return customHelper(manager, CLASSIC_HELPER_MANAGER === manager ? factory : factory.class); } private _lookupPartial(name: string, meta: OwnedTemplateMeta): PartialDefinition { diff --git a/packages/@ember/-internals/glimmer/lib/utils/managers.ts b/packages/@ember/-internals/glimmer/lib/utils/managers.ts index b89ea86ae07..8dfaf55a952 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/managers.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/managers.ts @@ -3,12 +3,14 @@ import { deprecate } from '@ember/debug'; import { COMPONENT_MANAGER_STRING_LOOKUP } from '@ember/deprecated-features'; import { ManagerDelegate as ComponentManagerDelegate } from '../component-managers/custom'; import InternalComponentManager from '../component-managers/internal'; +import { HelperManager } from '../helpers/custom'; import { ModifierManagerDelegate } from '../modifiers/custom'; type ManagerDelegate = | ComponentManagerDelegate | InternalComponentManager - | ModifierManagerDelegate; + | ModifierManagerDelegate + | HelperManager; const COMPONENT_MANAGERS = new WeakMap< object, @@ -17,6 +19,8 @@ const COMPONENT_MANAGERS = new WeakMap< const MODIFIER_MANAGERS = new WeakMap>>(); +const HELPER_MANAGERS = new WeakMap>>(); + const MANAGER_INSTANCES: WeakMap> = new WeakMap(); export type ManagerFactory = (owner: Owner) => D; @@ -96,6 +100,26 @@ export function getModifierManager( return undefined; } +export function setHelperManager( + factory: ManagerFactory>, + definition: object +) { + return setManager(HELPER_MANAGERS, factory, definition); +} + +export function getHelperManager( + owner: Owner, + definition: object +): HelperManager | undefined { + const factory = getManager(HELPER_MANAGERS, definition); + + if (factory !== undefined) { + return getManagerInstanceForOwner(owner, factory); + } + + return undefined; +} + export function setComponentManager( stringOrFunction: | string diff --git a/packages/@ember/-internals/glimmer/lib/utils/references.ts b/packages/@ember/-internals/glimmer/lib/utils/references.ts deleted file mode 100644 index 8a9a7400f96..00000000000 --- a/packages/@ember/-internals/glimmer/lib/utils/references.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getDebugName } from '@ember/-internals/utils'; -import { debugFreeze } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import { CapturedArguments } from '@glimmer/interfaces'; -import { createComputeRef, Reference } from '@glimmer/reference'; -import { reifyArgs } from '@glimmer/runtime'; -import { consumeTag, deprecateMutationsInTrackingTransaction } from '@glimmer/validator'; -import { HelperInstance, isClassHelper, RECOMPUTE_TAG, SimpleHelper } from '../helper'; - -export function createHelperRef( - helper: SimpleHelper | HelperInstance, - args: CapturedArguments -): Reference { - return createComputeRef( - () => { - let { positional, named } = reifyArgs(args); - - let ret: T; - - if (DEBUG) { - debugFreeze(positional); - debugFreeze(named); - - deprecateMutationsInTrackingTransaction!(() => { - ret = helper.compute(positional, named); - }); - } else { - ret = helper.compute(positional, named); - } - - if (helper[RECOMPUTE_TAG]) { - consumeTag(helper[RECOMPUTE_TAG]); - } - - return ret!; - }, - null, - DEBUG && (isClassHelper(helper) ? getDebugName!(helper) : getDebugName!(helper.compute)) - ); -} diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js new file mode 100644 index 00000000000..2611cd87fcb --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js @@ -0,0 +1,294 @@ +import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers'; +import { + setHelperManager, + setModifierManager, + helperCapabilities, +} from '@ember/-internals/glimmer'; +import { tracked, set } from '@ember/-internals/metal'; +import { setOwner } from '@ember/-internals/owner'; +import { EMBER_GLIMMER_HELPER_MANAGER } from '@ember/canary-features'; +import Service, { inject as service } from '@ember/service'; +import { backtrackingMessageFor } from '../../utils/backtracking-rerender'; +import { registerDestructor } from '@glimmer/runtime'; + +class TestHelperManager { + capabilities = helperCapabilities('3.23', { + hasValue: true, + hasDestroyable: true, + }); + + constructor(owner) { + this.owner = owner; + } + + createHelper(Helper, args) { + return new Helper(this.owner, args); + } + + getValue(instance) { + return instance.value(); + } + + getDestroyable(instance) { + return instance; + } + + getDebugName() { + return 'TEST_HELPER'; + } +} + +class TestHelper { + constructor(owner, args) { + setOwner(this, owner); + this.args = args; + + registerDestructor(this, () => this.willDestroy()); + } + + willDestroy() {} +} + +setHelperManager(owner => new TestHelperManager(owner), TestHelper); + +if (EMBER_GLIMMER_HELPER_MANAGER) { + moduleFor( + 'Helpers test: helper managers', + class extends RenderingTestCase { + ['@test it works']() { + this.registerCustomHelper( + 'hello', + class extends TestHelper { + value() { + return 'hello'; + } + } + ); + + this.render('{{hello}}'); + + this.assertText('hello'); + + runTask(() => this.rerender()); + + this.assertText('hello'); + } + + ['@test tracks changes to named arguments'](assert) { + let count = 0; + + this.registerCustomHelper( + 'hello', + class extends TestHelper { + value() { + count++; + return this.args.named.foo; + } + } + ); + + this.render('{{hello foo=foo}}', { + foo: 123, + }); + + assert.equal(count, 1, 'rendered once'); + this.assertText('123'); + + runTask(() => this.rerender()); + + assert.equal(count, 1, 'rendered once'); + this.assertText('123'); + + runTask(() => set(this.context, 'foo', 456)); + + assert.equal(count, 2, 'rendered twice'); + this.assertText('456'); + } + + ['@test tracks changes to positional arguments'](assert) { + let count = 0; + + this.registerCustomHelper( + 'hello', + class extends TestHelper { + value() { + count++; + return this.args.positional[0]; + } + } + ); + + this.render('{{hello foo}}', { + foo: 123, + }); + + assert.equal(count, 1, 'rendered once'); + this.assertText('123'); + + runTask(() => this.rerender()); + + assert.equal(count, 1, 'rendered once'); + this.assertText('123'); + + runTask(() => set(this.context, 'foo', 456)); + + assert.equal(count, 2, 'rendered twice'); + this.assertText('456'); + } + + ['@test tracks changes to tracked properties'](assert) { + let count = 0; + let instance; + + this.registerCustomHelper( + 'hello', + class extends TestHelper { + @tracked foo = 123; + + constructor(...args) { + super(...args); + instance = this; + } + + value() { + count++; + return this.foo; + } + } + ); + + this.render('{{hello}}'); + + assert.equal(count, 1, 'rendered once'); + this.assertText('123'); + + runTask(() => this.rerender()); + + assert.equal(count, 1, 'rendered once'); + this.assertText('123'); + + runTask(() => (instance.foo = 456)); + + assert.equal(count, 2, 'rendered twice'); + this.assertText('456'); + } + + ['@test services can be injected']() { + this.registerService( + 'hello', + Service.extend({ + value: 'hello', + }) + ); + + this.registerCustomHelper( + 'hello', + class extends TestHelper { + @service hello; + + value() { + return this.hello.value; + } + } + ); + + this.render('{{hello}}'); + + this.assertText('hello'); + } + + ['@test destroyable is associated correctly'](assert) { + this.registerCustomHelper( + 'hello', + class extends TestHelper { + value() { + return 'hello'; + } + + willDestroy() { + assert.ok(true, 'destructor called'); + } + } + ); + + this.render('{{hello}}'); + + this.assertText('hello'); + } + + ['@test debug name is used for backtracking message']() { + this.registerCustomHelper( + 'hello', + class extends TestHelper { + @tracked foo = 123; + + value() { + this.foo; + this.foo = 456; + } + } + ); + + let expectedMessage = backtrackingMessageFor('foo', '.*', { + renderTree: ['\\(result of a `TEST_HELPER` helper\\)'], + }); + + expectAssertion(() => { + this.render('{{hello}}'); + }, expectedMessage); + } + + ['@test asserts against using both `hasValue` and `hasScheduledEffect`']() { + expectAssertion(() => { + helperCapabilities('3.23', { + hasValue: true, + hasScheduledEffect: true, + }); + }, /You must pass either the `hasValue` OR the `hasScheduledEffect` capability when defining a helper manager. Passing neither, or both, is not permitted./); + } + + ['@test asserts requiring either `hasValue` or `hasScheduledEffect`']() { + expectAssertion(() => { + helperCapabilities('3.23', {}); + }, /You must pass either the `hasValue` OR the `hasScheduledEffect` capability when defining a helper manager. Passing neither, or both, is not permitted./); + } + + ['@test asserts against using `hasScheduledEffect`']() { + expectAssertion(() => { + helperCapabilities('3.23', { + hasScheduledEffect: true, + }); + }, /The `hasScheduledEffect` capability has not yet been implemented for helper managers. Please pass `hasValue` instead/); + } + + ['@test asserts against using incorrect version for capabilities']() { + expectAssertion(() => { + helperCapabilities('aoeu', { + hasScheduledEffect: true, + }); + }, /Invalid helper manager compatibility specified/); + } + + ['@test helper manager and modifier manager can be associated with the same value']() { + setModifierManager(() => ({}), TestHelper); + + this.registerCustomHelper( + 'hello', + class extends TestHelper { + value() { + return 'hello'; + } + } + ); + + this.render('{{hello}}'); + + this.assertText('hello'); + + runTask(() => this.rerender()); + + this.assertText('hello'); + } + } + ); +} diff --git a/packages/@ember/canary-features/index.ts b/packages/@ember/canary-features/index.ts index 88e0837c402..595761d2948 100644 --- a/packages/@ember/canary-features/index.ts +++ b/packages/@ember/canary-features/index.ts @@ -21,6 +21,7 @@ export const DEFAULT_FEATURES = { EMBER_GLIMMER_IN_ELEMENT: true, EMBER_CACHE_API: true, EMBER_DESTROYABLES: true, + EMBER_GLIMMER_HELPER_MANAGER: null, }; /** @@ -79,3 +80,4 @@ export const EMBER_ROUTING_MODEL_ARG = featureValue(FEATURES.EMBER_ROUTING_MODEL export const EMBER_GLIMMER_IN_ELEMENT = featureValue(FEATURES.EMBER_GLIMMER_IN_ELEMENT); export const EMBER_CACHE_API = featureValue(FEATURES.EMBER_CACHE_API); export const EMBER_DESTROYABLES = featureValue(FEATURES.EMBER_DESTROYABLES); +export const EMBER_GLIMMER_HELPER_MANAGER = featureValue(FEATURES.EMBER_GLIMMER_HELPER_MANAGER); diff --git a/packages/ember/index.js b/packages/ember/index.js index ab3397d6e51..a9b8f12898a 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -10,6 +10,7 @@ import * as metal from '@ember/-internals/metal'; import { FEATURES, isEnabled, + EMBER_GLIMMER_HELPER_MANAGER, EMBER_GLIMMER_SET_COMPONENT_TEMPLATE, EMBER_CACHE_API, EMBER_DESTROYABLES, @@ -103,11 +104,13 @@ import { Checkbox, Component, setComponentManager, + setHelperManager, capabilities, escapeExpression, getTemplates, Helper, helper, + helperCapabilities, htmlSafe, isHTMLSafe, LinkComponent, @@ -562,6 +565,10 @@ if (EMBER_GLIMMER_SET_COMPONENT_TEMPLATE) { Ember._setComponentTemplate = setComponentTemplate; Ember._templateOnlyComponent = templateOnlyComponent; } +if (EMBER_GLIMMER_HELPER_MANAGER) { + Ember._helperManagerCapabilities = helperCapabilities; + Ember._setHelperManager = setHelperManager; +} Ember._captureRenderTree = captureRenderTree; Ember.Handlebars = { template, diff --git a/packages/ember/tests/reexports_test.js b/packages/ember/tests/reexports_test.js index efd08df6407..12851c61600 100644 --- a/packages/ember/tests/reexports_test.js +++ b/packages/ember/tests/reexports_test.js @@ -1,5 +1,9 @@ import Ember from '../index'; -import { FEATURES, EMBER_GLIMMER_SET_COMPONENT_TEMPLATE } from '@ember/canary-features'; +import { + FEATURES, + EMBER_GLIMMER_HELPER_MANAGER, + EMBER_GLIMMER_SET_COMPONENT_TEMPLATE, +} from '@ember/canary-features'; import { confirmExport } from 'internal-test-helpers'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; import { jQueryDisabled, jQuery } from '@ember/-internals/views'; @@ -226,6 +230,12 @@ let allExports = [ EMBER_GLIMMER_SET_COMPONENT_TEMPLATE ? ['_templateOnlyComponent', '@ember/component/template-only', 'default'] : null, + EMBER_GLIMMER_HELPER_MANAGER + ? ['_setHelperManager', '@ember/-internals/glimmer', 'setHelperManager'] + : null, + EMBER_GLIMMER_HELPER_MANAGER + ? ['_helperManagerCapabilities', '@ember/-internals/glimmer', 'helperCapabilities'] + : null, ['_captureRenderTree', '@ember/debug', 'captureRenderTree'], // @ember/-internals/runtime diff --git a/packages/internal-test-helpers/lib/test-cases/rendering.js b/packages/internal-test-helpers/lib/test-cases/rendering.js index ab152b5e70c..2be75933b24 100644 --- a/packages/internal-test-helpers/lib/test-cases/rendering.js +++ b/packages/internal-test-helpers/lib/test-cases/rendering.js @@ -147,6 +147,10 @@ export default class RenderingTestCase extends AbstractTestCase { } } + registerCustomHelper(name, definition) { + this.owner.register(`helper:${name}`, definition); + } + registerPartial(name, template) { let owner = this.owner; if (typeof template === 'string') {