Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Implements Helper Managers #19160

Merged
merged 1 commit into from
Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions packages/@ember/-internals/glimmer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
110 changes: 95 additions & 15 deletions packages/@ember/-internals/glimmer/lib/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -30,18 +38,6 @@ export interface SimpleHelper<T = unknown> {
compute: HelperFunction<T>;
}

export function isHelperFactory(
helper: any | undefined | null
): helper is Factory<SimpleHelper | HelperInstance, HelperFactory<SimpleHelper | HelperInstance>> {
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`:
Expand Down Expand Up @@ -138,6 +134,56 @@ let Helper = FrameworkObject.extend({

Helper.isHelperFactory = true;

interface ClassicHelperStateBucket {
instance: HelperInstance;
args: Arguments;
}

class ClassicHelperManager implements HelperManager<ClassicHelperStateBucket> {
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<SimpleHelper> {
isHelperFactory: true = true;

Expand All @@ -151,6 +197,40 @@ class Wrapper implements HelperFactory<SimpleHelper> {
}
}

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.
Expand Down
86 changes: 86 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/custom.ts
Original file line number Diff line number Diff line change
@@ -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> = {}
): 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<HelperStateBucket = unknown> {
capabilities: HelperCapabilities;

createHelper(definition: HelperDefinition, args: Arguments): HelperStateBucket;

getDebugName?(definition: HelperDefinition): string;
}

export interface HelperManagerWithValue<HelperStateBucket = unknown>
extends HelperManager<HelperStateBucket> {
getValue(bucket: HelperStateBucket): unknown;
}

function hasValue(manager: HelperManager): manager is HelperManagerWithValue {
return manager.capabilities.hasValue;
}

export interface HelperManagerWithDestroyable<HelperStateBucket = unknown>
extends HelperManager<HelperStateBucket> {
getDestroyable(bucket: HelperStateBucket): object;
}

function hasDestroyable(manager: HelperManager): manager is HelperManagerWithDestroyable {
return manager.capabilities.hasDestroyable;
}

export default function customHelper(
manager: HelperManager<unknown>,
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;
}
};
}
62 changes: 35 additions & 27 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,20 +19,27 @@ 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';
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';
Expand All @@ -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}` };
Expand Down Expand Up @@ -358,32 +366,32 @@ export default class RuntimeResolverImpl implements RuntimeResolver<OwnedTemplat
const options: LookupOptions = makeOptions(moduleName, namespace);

const factory =
owner.factoryFor(`helper:${name}`, options) || owner.factoryFor(`helper:${name}`);
owner.factoryFor<SimpleHelper | HelperInstance, HelperFactory<SimpleHelper | HelperInstance>>(
`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 {
Expand Down
Loading