Skip to content

Commit

Permalink
Work on implementing decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Dec 11, 2018
1 parent b27e344 commit de44861
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 51 deletions.
1 change: 1 addition & 0 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const rename = require('./broccoli/rename');
const {
routerES,
jquery,
emberDecorators,
internalLoader,
qunit,
handlebarsES,
Expand Down
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@
"babel-plugin-debug-macros": "^0.2.0",
"babel-plugin-filter-imports": "^2.0.4",
"babel-plugin-module-resolver": "^3.1.1",
"amd-name-resolver": "^1.2.0",
"babel-plugin-check-es2015-constants": "^6.22.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
"babel-plugin-transform-es2015-block-scoping": "^6.26.0",
"babel-plugin-transform-es2015-classes": "^6.24.1",
"babel-plugin-transform-es2015-computed-properties": "^6.24.1",
"babel-plugin-transform-es2015-destructuring": "^6.23.0",
"babel-plugin-transform-es2015-literals": "^6.22.0",
"babel-plugin-transform-es2015-modules-amd": "^6.24.1",
"babel-plugin-transform-es2015-parameters": "^6.24.1",
"babel-plugin-transform-es2015-shorthand-properties": "^6.24.1",
"babel-plugin-transform-es2015-spread": "^6.22.0",
"babel-plugin-transform-es2015-template-literals": "^6.22.0",
"babel-plugin-transform-object-assign": "^6.22.0",
"babel-template": "^6.26.0",
"backburner.js": "^2.4.2",
"broccoli-babel-transpiler": "^7.1.1",
Expand Down
157 changes: 155 additions & 2 deletions packages/@ember/-internals/metal/lib/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { inspect } from '@ember/-internals/utils';
import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';
import { assert, warn } from '@ember/debug';
import EmberError from '@ember/error';

import { computedDecoratorWithParams } from './decorator-utils/index';

import {
getCachedValueFor,
getCacheFor,
Expand All @@ -21,6 +24,9 @@ import { notifyPropertyChange } from './property_events';
import { set } from './property_set';
import { tagForProperty, update } from './tags';
import { getCurrentTracker, setCurrentTracker } from './tracked';
import { decoratorWithParams } from './decorator-utils/decorator';
import { getModifierMeta } from './decorator-utils/computed';
import { isComputedDescriptor, computedDescriptorFor } from './decorator-utils/-private/descriptor';

export type ComputedPropertyGetter = (keyName: string) => any;
export type ComputedPropertySetter = (leyName: string, value: any) => any;
Expand Down Expand Up @@ -609,7 +615,7 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {
@return {ComputedProperty} property descriptor instance
@public
*/
export default function computed(...args: (string | ComputedPropertyConfig)[]): ComputedProperty {
export function computed_original(...args: (string | ComputedPropertyConfig)[]): ComputedProperty {
let func = args.pop();

let cp = new ComputedProperty(func as ComputedPropertyConfig);
Expand All @@ -620,7 +626,154 @@ export default function computed(...args: (string | ComputedPropertyConfig)[]):

return cp;
}

/**
Decorator that turns a native getter/setter into a computed property. Note
that though they use getters and setters, you must still use the Ember `get`/
`set` functions to get and set their values.
```js
import Component from '@ember/component';
import { computed } from '@ember-decorators/object';
export default class UserProfileComponent extends Component {
first = 'John';
last = 'Smith';
@computed('first', 'last')
get name() {
const first = this.get('first');
const last = this.get('last');
return `${first} ${last}`; // => 'John Smith'
}
set name(value) {
if (typeof value !== 'string' || !value.test(/^[a-z]+ [a-z]+$/i)) {
throw new TypeError('Invalid name');
}
const [first, last] = value.split(' ');
this.setProperties({ first, last });
return value;
}
}
```
@function
@param {...string} propertyNames - List of property keys this computed is dependent on
@return {ComputedProperty}
*/

// https://tc39.github.io/proposal-decorators/#sec-elementdescriptor-specification-type
interface ElementDescriptor {
descriptor: PropertyDescriptor;
initializer?: () => any; // unknown
key: string;
kind: 'method' | 'field' | 'initializer';
placement: 'own' | 'prototype' | 'static';
}

function computedDecoratorInner(fn: any) {
return (desc: ElementDescriptor, params = []) => {
// All computeds are methods
desc.kind = 'method';
desc.placement = 'prototype';

desc.finisher = function initializeComputedProperty(target) {
let { prototype } = target;
let { key } = desc;

assert(
`ES6 property getters/setters only need to be decorated once, '${key}' was decorated on both the getter and the setter`,
!computedDescriptorFor(prototype, key)
);

let computedDesc = fn(desc, params);

assert(
`computed decorators must return an instance of an Ember ComputedProperty descriptor, received ${computedDesc}`,
isComputedDescriptor(computedDesc)
);

let modifierMeta = getModifierMeta(prototype);

if (modifierMeta !== undefined && modifierMeta[key] !== undefined) {
computedDesc[modifierMeta[key]]();
}

defineProperty(prototype, key, computedDesc);

return target;
};
};
}

// computed(function() { ... })
// computed('dependentKey', { get: function() { ... }})
// computed('dependentKey', {})
// computed('dependentKey', function() { ...})
function isLegacyComputed(...args) {
const lastArg = args[args.length - 1];

// only non-decorator computeds have a last arg as a function
if (typeof lastArg === 'function') {
return true;
}

if (lastArg.hasOwnProperty('descriptor')) {
return false;
}

// TODO: configuration
// lastArg could have readOnly: true
if (typeof lastArg === 'object') {
const keys = Object.keys(lastArg);

if (keys.length === 2 && keys.includes('get') && keys.includes('set')) {
return true;
}

if (keys.length === 1 && keys.includes('get')) {
return true;
}

// else: configuration object or descriptor?
return !lastArg.hasOwnProperty('configurable');
}

// dependent key array
return false;
}

/**
decorator? or original, non-decorator-computed
non-decorator usage:
property = computed(function() { ... })
property = computed('dependentKey', function() { ... })
property = computed({ get: ..., set?: ... })
decorator usage:
@computed get property() { ... }
@computed('dependentKey') get property() { ... }
@computed('dependentKey', { readOnly: true }) get property() { ... }
**/
const computed = function(...args: any[]) {
if (isLegacyComputed(...args)) {
return computed_original(...args);
}

// any decorator usage should be handled here,
// as all of the non-decorator usages are handled above
return decoratorWithParams(computedDecoratorInner(args));
};

export default computed;

// used for the Ember.computed global only
export const _globalsComputed = computed.bind(null);
export const _globalsComputed = computed_original.bind(null);

export { ComputedProperty, computed };
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DEBUG } from '@glimmer/env';

import { assert } from '@ember/debug';

const DESCRIPTOR = '__DESCRIPTOR__';

function isCPGetter(getter: any) {
// Hack for descriptor traps, we want to be able to tell if the function
// is a descriptor trap before we call it at all
return getter !== null && typeof getter === 'function' && getter.toString().indexOf('CPGETTER_FUNCTION') !== -1;
}

function isDescriptorTrap(possibleDesc: any) {
throw new Error('Cannot call `isDescriptorTrap` in production');
}

export function isComputedDescriptor(possibleDesc: any) {
return possibleDesc !== null && typeof possibleDesc === 'object' && possibleDesc.isDescriptor;
}

export function computedDescriptorFor(obj: any, keyName: any) {
assert('Cannot call `descriptorFor` on null', obj !== null);
assert('Cannot call `descriptorFor` on undefined', obj !== undefined);
assert(`Cannot call \`descriptorFor\` on ${typeof obj}`, typeof obj === 'object' || typeof obj === 'function');

let { value: possibleDesc, get: possibleCPGetter } = Object.getOwnPropertyDescriptor(obj, keyName);

return isComputedDescriptor(possibleDesc) ? possibleDesc : undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const MODIFIER_META_MAP = new WeakMap();

export function getModifierMeta(target: any) {
return MODIFIER_META_MAP.get(target);
}

export function getOrCreateModifierMeta(target: any) {
if (!MODIFIER_META_MAP.has(target)) {
MODIFIER_META_MAP.set(target, {});
}

return MODIFIER_META_MAP.get(target);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function collapseProto(target: any) {
// We must collapse the superclass prototype to make sure that the `actions`
// object will exist. Since collapsing doesn't generally happen until a class is
// instantiated, we have to do it manually.
if (typeof target.constructor.proto === 'function') {
target.constructor.proto();
}
}
80 changes: 80 additions & 0 deletions packages/@ember/-internals/metal/lib/decorator-utils/computed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// import { defineProperty } from '@ember/object';
import { decoratorWithParams, decoratorWithRequiredParams, decorator } from './decorator';
// import { HAS_NATIVE_COMPUTED_GETTERS } from 'ember-compatibility-helpers';
// import { NEEDS_STAGE_1_DECORATORS } from 'ember-decorators-flags';

import { computedDescriptorFor, isComputedDescriptor } from './-private/descriptor';
import { getModifierMeta, getOrCreateModifierMeta } from './-private/modifier-meta';

import { assert } from '@ember/debug';

export { computedDescriptorFor, getModifierMeta, getOrCreateModifierMeta };

/**
* A macro that receives a decorator function which returns a ComputedProperty,
* and defines that property using `Ember.defineProperty`. Conceptually, CPs
* are custom property descriptors that require Ember's intervention to apply
* correctly. In the future, we will use finishers to define the CPs rather than
* directly defining them in the decorator function.
*
* @param {Function} fn - decorator function
*/
function computedDecoratorInner(fn) {
return (desc, params = []) => {
// All computeds are methods
desc.kind = 'method';
desc.placement = 'prototype';

desc.finisher = function initializeComputedProperty(target) {
let { prototype } = target;
let { key } = desc;

assert(`ES6 property getters/setters only need to be decorated once, '${key}' was decorated on both the getter and the setter`, !computedDescriptorFor(prototype, key));

let computedDesc = fn(desc, params);

assert(`computed decorators must return an instance of an Ember ComputedProperty descriptor, received ${computedDesc}`, isComputedDescriptor(computedDesc));

let modifierMeta = getModifierMeta(prototype);

if (modifierMeta !== undefined && modifierMeta[key] !== undefined) {
computedDesc[modifierMeta[key]]();
}

// if (!HAS_NATIVE_COMPUTED_GETTERS) {
// // Until recent versions of Ember, computed properties would be defined
// // by just setting them. We need to blow away any predefined properties
// // (getters/setters, etc.) to allow Ember.defineProperty to work correctly.
// Object.defineProperty(prototype, key, {
// configurable: true,
// writable: true,
// enumerable: true,
// value: undefined
// });
// }

// defineProperty(prototype, key, computedDesc);
Object.defineProperty(prototype, key, computedDesc);

// if (NEEDS_STAGE_1_DECORATORS) {
// // There's currently no way to disable redefining the property when decorators
// // are run, so return the property descriptor we just assigned
// desc.descriptor = Object.getOwnPropertyDescriptor(prototype, key);
// }

return target;
}
}
}

export function computedDecorator(fn) {
return decorator(computedDecoratorInner(fn));
}

export function computedDecoratorWithParams(fn) {
return decoratorWithParams(computedDecoratorInner(fn));
}

export function computedDecoratorWithRequiredParams(fn, name) {
return decoratorWithRequiredParams(computedDecoratorInner(fn), name);
}
Loading

0 comments on commit de44861

Please sign in to comment.