Skip to content

Commit

Permalink
Enforce usage of capabilities generation.
Browse files Browse the repository at this point in the history
Prior to this change it was possible to avoid using a given type's
`capabilities` builder function (intentionally or on accident) by doing
something like:

```js
class MyManager {
  capabilities = {
    // magical properties from Ember's internals
  }
}
```

The API's that we _intended_ folks to be using is something like:

```js
import { capabilties as modifierCapabilities } from '@ember/modifier';

class MyManager {
  capabilities = modifierCapabilities('3.22');
}
```

This commit ensures that Ember's own internal structures can not be
"spoofed" (avoiding our constraints, or creating a frankenstein
manager).
  • Loading branch information
rwjblue committed Sep 30, 2020
1 parent 15217a3 commit c76e288
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 33 deletions.
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
55 changes: 50 additions & 5 deletions packages/@ember/-internals/glimmer/lib/utils/managers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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 { 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 +18,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 +74,7 @@ function getManagerInstanceForOwner<D extends ManagerDelegate>(

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

managers.set(factory, instance!);
}

Expand All @@ -94,7 +98,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 +126,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 +182,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

0 comments on commit c76e288

Please sign in to comment.