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

Enforce usage of capabilities generation. #19173

Merged
merged 1 commit into from
Sep 30, 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
27 changes: 11 additions & 16 deletions packages/@ember/-internals/glimmer/lib/component-managers/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { EmberVMEnvironment } from '../environment';
import RuntimeResolver from '../resolver';
import { OwnedTemplate } from '../template';
import { argsProxyFor } from '../utils/args-proxy';
import { buildCapabilities, InternalCapabilities } from '../utils/managers';
import AbstractComponentManager from './abstract';

const CAPABILITIES = {
Expand Down Expand Up @@ -49,6 +50,12 @@ export interface OptionalCapabilities {
};
}

export interface Capabilities extends InternalCapabilities {
asyncLifeCycleCallbacks: boolean;
destructor: boolean;
updateHook: boolean;
}

export function capabilities<Version extends keyof OptionalCapabilities>(
managerAPI: Version,
options: OptionalCapabilities[Version] = {}
Expand All @@ -64,11 +71,11 @@ export function capabilities<Version extends keyof OptionalCapabilities>(
updateHook = Boolean((options as OptionalCapabilities['3.13']).updateHook);
}

return {
return buildCapabilities({
asyncLifeCycleCallbacks: Boolean(options.asyncLifecycleCallbacks),
destructor: Boolean(options.destructor),
updateHook,
};
}) as Capabilities;
}

export interface DefinitionState<ComponentInstance> {
Expand All @@ -77,21 +84,9 @@ export interface DefinitionState<ComponentInstance> {
template: OwnedTemplate;
}

export interface Capabilities {
asyncLifeCycleCallbacks: boolean;
destructor: boolean;
updateHook: boolean;
}

// TODO: export ICapturedArgumentsValue from glimmer and replace this
export interface Args {
named: Dict<unknown>;
positional: unknown[];
}

export interface ManagerDelegate<ComponentInstance> {
capabilities: Capabilities;
createComponent(factory: unknown, args: Args): ComponentInstance;
createComponent(factory: unknown, args: Arguments): ComponentInstance;
getContext(instance: ComponentInstance): unknown;
}

Expand All @@ -114,7 +109,7 @@ export function hasUpdateHook<ComponentInstance>(

export interface ManagerDelegateWithUpdateHook<ComponentInstance>
extends ManagerDelegate<ComponentInstance> {
updateComponent(instance: ComponentInstance, args: Args): void;
updateComponent(instance: ComponentInstance, args: Arguments): void;
}

export function hasAsyncUpdateHook<ComponentInstance>(
Expand Down
7 changes: 4 additions & 3 deletions packages/@ember/-internals/glimmer/lib/helpers/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ 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';
import { buildCapabilities, InternalCapabilities } from '../utils/managers';

export type HelperDefinition = object;

export interface HelperCapabilities {
export interface HelperCapabilities extends InternalCapabilities {
hasValue: boolean;
hasDestroyable: boolean;
hasScheduledEffect: boolean;
Expand All @@ -29,11 +30,11 @@ export function helperCapabilities(
!options.hasScheduledEffect
);

return {
return buildCapabilities({
hasValue: Boolean(options.hasValue),
hasDestroyable: Boolean(options.hasDestroyable),
hasScheduledEffect: Boolean(options.hasScheduledEffect),
};
});
}

export interface HelperManager<HelperStateBucket = unknown> {
Expand Down
12 changes: 4 additions & 8 deletions packages/@ember/-internals/glimmer/lib/modifiers/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { registerDestructor, reifyArgs } from '@glimmer/runtime';
import { createUpdatableTag, untrack, UpdatableTag } from '@glimmer/validator';
import { SimpleElement } from '@simple-dom/interface';
import { argsProxyFor } from '../utils/args-proxy';
import { buildCapabilities, InternalCapabilities } from '../utils/managers';

export interface CustomModifierDefinitionState<ModifierInstance> {
ModifierClass: Factory<ModifierInstance>;
Expand All @@ -25,7 +26,7 @@ export interface OptionalCapabilities {
};
}

export interface Capabilities {
export interface Capabilities extends InternalCapabilities {
disableAutoTracking: boolean;
useArgsProxy: boolean;
passFactoryToCreate: boolean;
Expand All @@ -40,11 +41,11 @@ export function capabilities<Version extends keyof OptionalCapabilities>(
managerAPI === '3.13' || managerAPI === '3.22'
);

return {
return buildCapabilities({
disableAutoTracking: Boolean(optionalFeatures.disableAutoTracking),
useArgsProxy: managerAPI === '3.13' ? false : true,
passFactoryToCreate: managerAPI === '3.13',
};
}) as Capabilities;
}

export class CustomModifierDefinition<ModifierInstance> {
Expand Down Expand Up @@ -124,11 +125,6 @@ class InteractiveCustomModifierManager<ModifierInstance>
let { delegate, ModifierClass } = definition;
let capturedArgs = vmArgs.capture();

assert(
'Custom modifier managers must define their capabilities using the capabilities() helper function',
typeof delegate.capabilities === 'object' && delegate.capabilities !== null
);

let { useArgsProxy, passFactoryToCreate } = delegate.capabilities;

let args = useArgsProxy ? argsProxyFor(capturedArgs, 'modifier') : reifyArgs(capturedArgs);
Expand Down
56 changes: 51 additions & 5 deletions packages/@ember/-internals/glimmer/lib/utils/managers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Owner } from '@ember/-internals/owner';
import { deprecate } from '@ember/debug';
import { assert, deprecate } from '@ember/debug';
import { COMPONENT_MANAGER_STRING_LOOKUP } from '@ember/deprecated-features';
import { DEBUG } from '@glimmer/env';
import { _WeakSet } from '@glimmer/util';
import { ManagerDelegate as ComponentManagerDelegate } from '../component-managers/custom';
import InternalComponentManager from '../component-managers/internal';
import InternalComponentManager, { isInternalManager } from '../component-managers/internal';
import { HelperManager } from '../helpers/custom';
import { ModifierManagerDelegate } from '../modifiers/custom';

Expand All @@ -17,6 +19,8 @@ const COMPONENT_MANAGERS = new WeakMap<
ManagerFactory<ComponentManagerDelegate<unknown> | InternalComponentManager>
>();

const FROM_CAPABILITIES = DEBUG ? new _WeakSet() : undefined;

const MODIFIER_MANAGERS = new WeakMap<object, ManagerFactory<ModifierManagerDelegate<unknown>>>();

const HELPER_MANAGERS = new WeakMap<object, ManagerFactory<HelperManager<unknown>>>();
Expand Down Expand Up @@ -71,6 +75,7 @@ function getManagerInstanceForOwner<D extends ManagerDelegate>(

if (instance === undefined) {
instance = factory(owner);

managers.set(factory, instance!);
}

Expand All @@ -94,7 +99,15 @@ export function getModifierManager(
const factory = getManager(MODIFIER_MANAGERS, definition);

if (factory !== undefined) {
return getManagerInstanceForOwner(owner, factory);
let manager = getManagerInstanceForOwner(owner, factory);
assert(
`Custom modifier managers must have a \`capabilities\` property that is the result of calling the \`capabilities('3.13' | '3.22')\` (imported via \`import { capabilities } from '@ember/modifier';\`). Received: \`${JSON.stringify(
manager.capabilities
)}\` for: \`${manager}\``,
FROM_CAPABILITIES!.has(manager.capabilities)
);

return manager;
}

return undefined;
Expand All @@ -114,7 +127,16 @@ export function getHelperManager(
const factory = getManager(HELPER_MANAGERS, definition);

if (factory !== undefined) {
return getManagerInstanceForOwner(owner, factory);
let manager = getManagerInstanceForOwner(owner, factory);

assert(
`Custom helper managers must have a \`capabilities\` property that is the result of calling the \`capabilities('3.23')\` (imported via \`import { capabilities } from '@ember/helper';\`). Received: \`${JSON.stringify(
manager.capabilities
)}\` for: \`${manager}\``,
FROM_CAPABILITIES!.has(manager.capabilities)
);

return manager;
}

return undefined;
Expand Down Expand Up @@ -161,8 +183,32 @@ export function getComponentManager(
);

if (factory !== undefined) {
return getManagerInstanceForOwner(owner, factory);
let manager = getManagerInstanceForOwner(owner, factory);

assert(
`Custom component managers must have a \`capabilities\` property that is the result of calling the \`capabilities('3.4' | '3.13')\` (imported via \`import { capabilities } from '@ember/component';\`). Received: \`${JSON.stringify(
(manager as ComponentManagerDelegate<unknown>).capabilities
)}\` for: \`${manager}\``,
isInternalManager(manager) || FROM_CAPABILITIES!.has(manager.capabilities)
);

return manager;
}

return undefined;
}

declare const INTERNAL_CAPABILITIES: unique symbol;

export interface InternalCapabilities {
[INTERNAL_CAPABILITIES]: true;
}

export function buildCapabilities<T extends object>(capabilities: T): T & InternalCapabilities {
if (DEBUG) {
FROM_CAPABILITIES!.add(capabilities);
Object.freeze(capabilities);
}

return capabilities as T & InternalCapabilities;
}
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,62 @@ moduleFor(
this.assertHTML(`<p>hello max</p>`);
assert.verifySteps(['updateComponent', 'didUpdateComponent']);
}

'@test capabilities helper function must be used to generate capabilities'(assert) {
let ComponentClass = setComponentManager(
() => {
return EmberObject.create({
capabilities: {
asyncLifecycleCallbacks: true,
destructor: true,
update: false,
},

createComponent(factory, args) {
assert.step('createComponent');
return factory.create({ args });
},

updateComponent(component, args) {
assert.step('updateComponent');
set(component, 'args', args);
},

destroyComponent(component) {
assert.step('destroyComponent');
component.destroy();
},

getContext(component) {
assert.step('getContext');
return component;
},

didCreateComponent() {
assert.step('didCreateComponent');
},

didUpdateComponent() {
assert.step('didUpdateComponent');
},
});
},
EmberObject.extend({
greeting: 'hello',
})
);

this.registerComponent('foo-bar', {
template: `<p>{{greeting}} {{@name}}</p>`,
ComponentClass,
});

expectAssertion(() => {
this.render('{{foo-bar name=name}}', { name: 'world' });
}, /Custom component managers must have a `capabilities` property that is the result of calling the `capabilities\('3.4' \| '3.13'\)` \(imported via `import \{ capabilities \} from '@ember\/component';`\). /);

assert.verifySteps([]);
}
}
);

Expand Down Expand Up @@ -774,5 +830,61 @@ moduleFor(
runTask(() => this.context.set('value', 'bar'));
assert.verifySteps([]);
}

'@test capabilities helper function must be used to generate capabilities'(assert) {
let ComponentClass = setComponentManager(
() => {
return EmberObject.create({
capabilities: {
asyncLifecycleCallbacks: true,
destructor: true,
update: false,
},

createComponent(factory, args) {
assert.step('createComponent');
return factory.create({ args });
},

updateComponent(component, args) {
assert.step('updateComponent');
set(component, 'args', args);
},

destroyComponent(component) {
assert.step('destroyComponent');
component.destroy();
},

getContext(component) {
assert.step('getContext');
return component;
},

didCreateComponent() {
assert.step('didCreateComponent');
},

didUpdateComponent() {
assert.step('didUpdateComponent');
},
});
},
EmberObject.extend({
greeting: 'hello',
})
);

this.registerComponent('foo-bar', {
template: `<p>{{greeting}} {{@name}}</p>`,
ComponentClass,
});

expectAssertion(() => {
this.render('<FooBar @name={{name}} />', { name: 'world' });
}, /Custom component managers must have a `capabilities` property that is the result of calling the `capabilities\('3.4' \| '3.13'\)` \(imported via `import \{ capabilities \} from '@ember\/component';`\). /);

assert.verifySteps([]);
}
}
);
Loading