From 361a4369cdf11d1a17f8843e6bcb7ed81e27f2fc Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Thu, 4 Apr 2019 17:14:26 -0700 Subject: [PATCH] initial implementation --- .eslintrc.js | 1 + .travis.yml | 1 + README.md | 75 ++- addon/.gitkeep | 0 index.js | 42 +- package.json | 14 +- tests/dummy/config/targets.js | 4 +- tests/unit/.gitkeep | 0 tests/unit/action-test.js | 302 ++++++++++ tests/unit/computed-test.js | 446 ++++++++++++++ tests/unit/inject-test.js | 165 ++++++ tests/unit/macro-test.js | 690 ++++++++++++++++++++++ vendor/.gitkeep | 0 vendor/ember-decorators-polyfill/index.js | 504 ++++++++++++++++ yarn.lock | 39 +- 15 files changed, 2249 insertions(+), 34 deletions(-) delete mode 100644 addon/.gitkeep delete mode 100644 tests/unit/.gitkeep create mode 100644 tests/unit/action-test.js create mode 100644 tests/unit/computed-test.js create mode 100644 tests/unit/inject-test.js create mode 100644 tests/unit/macro-test.js delete mode 100644 vendor/.gitkeep create mode 100644 vendor/ember-decorators-polyfill/index.js diff --git a/.eslintrc.js b/.eslintrc.js index ec0142c..a9ea48d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { root: true, + parser: 'babel-eslint', parserOptions: { ecmaVersion: 2017, sourceType: 'module' diff --git a/.travis.yml b/.travis.yml index c8cec19..ef1bf7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ branches: jobs: fail_fast: true allow_failures: + - env: EMBER_TRY_SCENARIO=ember-beta - env: EMBER_TRY_SCENARIO=ember-canary include: diff --git a/README.md b/README.md index 8966603..3253937 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,70 @@ -ember-decorators-polyfill -============================================================================== +# ember-decorators-polyfill -[Short description of the addon.] +Polyfills Ember's built-in decorators +## Compatibility -Compatibility ------------------------------------------------------------------------------- +- Ember.js v2.18 or above +- Ember CLI v2.13 or above -* Ember.js v2.18 or above -* Ember CLI v2.13 or above +This addon is not needed in Ember 3.10+ - -Installation ------------------------------------------------------------------------------- +## Installation ``` ember install ember-decorators-polyfill ``` +## Usage + +Polyfills Ember's built-in decorators API: + +```js +import { action, computed } from '@ember/object'; + +import { inject as service } from '@ember/service'; +import { inject as controller } from '@ember/controller'; + +import { + alias, + and, + bool, + collect, + deprecatingAlias, + empty, + equal, + filter, + filterBy, + gt, + gte, + intersect, + lt, + lte, + map, + mapBy, + match, + max, + min, + none, + not, + notEmpty, + oneWay, + or, + reads, + readOnly, + setDiff, + sort, + sum, + union, + uniq, + uniqBy, +} from '@ember/object/computed'; +``` -Usage ------------------------------------------------------------------------------- - -[Longer description of how to use the addon in apps.] - - -Contributing ------------------------------------------------------------------------------- +## Contributing See the [Contributing](CONTRIBUTING.md) guide for details. - -License ------------------------------------------------------------------------------- +## License This project is licensed under the [MIT License](LICENSE.md). diff --git a/addon/.gitkeep b/addon/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/index.js b/index.js index 2e1d1d8..94bc3e6 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,45 @@ 'use strict'; +const VersionChecker = require('ember-cli-version-checker'); module.exports = { - name: require('./package').name + name: require('./package').name, + + init() { + this._super.init && this._super.init.apply(this, arguments); + + let checker = new VersionChecker(this.project); + let emberVersion = checker.forEmber(); + + this.shouldPolyfill = emberVersion.lt('3.10.0-alpha.1'); + }, + + included() { + this._super.included.apply(this, arguments); + + if (!this.shouldPolyfill) { + return; + } + + this.import('vendor/ember-decorators-polyfill/index.js'); + }, + + treeForVendor(rawVendorTree) { + if (!this.shouldPolyfill) { + return; + } + + let babelAddon = this.addons.find( + addon => addon.name === 'ember-cli-babel' + ); + + let transpiledVendorTree = babelAddon.transpileTree(rawVendorTree, { + babel: this.options.babel, + + 'ember-cli-babel': { + compileModules: false, + }, + }); + + return transpiledVendorTree; + }, }; diff --git a/package.json b/package.json index 5d2b29e..ce00e41 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "ember-decorators-polyfill", "version": "0.0.0", - "description": "The default blueprint for ember-cli addons.", + "description": "Polyfills Ember's built-in decorators", "keywords": [ "ember-addon" ], - "repository": "", + "repository": "https://github.com/pzuraq/ember-decorators-polyfill", "license": "MIT", - "author": "", + "author": "Chris Garrett", "directories": { "doc": "doc", "test": "tests" @@ -21,10 +21,13 @@ "test:all": "ember try:each" }, "dependencies": { - "ember-cli-babel": "^7.1.2" + "ember-cli-babel": "^7.1.2", + "ember-cli-version-checker": "^3.1.3", + "ember-compatibility-helpers": "^1.2.0" }, "devDependencies": { "@ember/optional-features": "^0.6.3", + "babel-eslint": "^10.0.1", "broccoli-asset-rev": "^2.7.0", "ember-cli": "~3.8.1", "ember-cli-dependency-checker": "^3.1.0", @@ -53,6 +56,7 @@ "node": "6.* || 8.* || >= 10.*" }, "ember-addon": { - "configPath": "tests/dummy/config" + "configPath": "tests/dummy/config", + "after": "ember-source" } } diff --git a/tests/dummy/config/targets.js b/tests/dummy/config/targets.js index 8ffae36..cc55392 100644 --- a/tests/dummy/config/targets.js +++ b/tests/dummy/config/targets.js @@ -3,7 +3,7 @@ const browsers = [ 'last 1 Chrome versions', 'last 1 Firefox versions', - 'last 1 Safari versions' + 'last 1 Safari versions', ]; const isCI = !!process.env.CI; @@ -14,5 +14,5 @@ if (isCI || isProduction) { } module.exports = { - browsers + browsers, }; diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/action-test.js b/tests/unit/action-test.js new file mode 100644 index 0000000..678cf08 --- /dev/null +++ b/tests/unit/action-test.js @@ -0,0 +1,302 @@ +import EmberObject, { action } from '@ember/object'; +import Component from '@ember/component'; + +import { module, test } from 'qunit'; +import { setupRenderingTest, skip } from 'ember-qunit'; +import { render, click, findAll } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +function registerComponent( + test, + name, + { ComponentClass = Component, template = null } +) { + let { owner } = test; + + if (ComponentClass) { + owner.register(`component:${name}`, ComponentClass); + } + + if (template) { + owner.register(`template:components/${name}`, template); + } +} + +module('action-decorator', function(hooks) { + setupRenderingTest(hooks); + + test('action decorator works with ES6 class', async function(assert) { + class FooComponent extends Component { + @action + foo() { + assert.ok(true, 'called!'); + } + } + + registerComponent(this, 'foo-bar', { + ComponentClass: FooComponent, + template: hbs``, + }); + + await render(hbs`{{foo-bar}}`); + + await click('button'); + }); + + test('action decorator does not add actions to superclass', async function(assert) { + class Foo extends EmberObject { + @action + foo() { + // Do nothing + } + } + + class Bar extends Foo { + @action + bar() { + assert.ok(false, 'called'); + } + } + + let foo = Foo.create(); + let bar = Bar.create(); + + assert.equal(typeof foo.actions.foo, 'function', 'foo has foo action'); + assert.equal( + typeof foo.actions.bar, + 'undefined', + 'foo does not have bar action' + ); + + assert.equal(typeof bar.actions.foo, 'function', 'bar has foo action'); + assert.equal(typeof bar.actions.bar, 'function', 'bar has bar action'); + }); + + test('actions are properly merged through traditional and ES6 prototype hierarchy', async function(assert) { + assert.expect(4); + + let FooComponent = Component.extend({ + actions: { + foo() { + assert.ok(true, 'foo called!'); + }, + }, + }); + + class BarComponent extends FooComponent { + @action + bar() { + assert.ok(true, 'bar called!'); + } + } + + let BazComponent = BarComponent.extend({ + actions: { + baz() { + assert.ok(true, 'baz called!'); + }, + }, + }); + + class QuxComponent extends BazComponent { + @action + qux() { + assert.ok(true, 'qux called!'); + } + } + + registerComponent(this, 'qux-component', { + ComponentClass: QuxComponent, + template: hbs` + + + + + `, + }); + + await render(hbs`{{qux-component}}`); + + let buttons = findAll('button'); + + for (let button of buttons) { + await click(button); + } + }); + + test('action decorator super works with native class methods', async function(assert) { + class FooComponent extends Component { + foo() { + assert.ok(true, 'called!'); + } + } + + class BarComponent extends FooComponent { + @action + foo() { + super.foo(); + } + } + + registerComponent(this, 'bar-bar', { + ComponentClass: BarComponent, + template: hbs``, + }); + + await render(hbs`{{bar-bar}}`); + + await click('button'); + }); + + test('action decorator super works with traditional class methods', async function(assert) { + let FooComponent = Component.extend({ + foo() { + assert.ok(true, 'called!'); + }, + }); + + class BarComponent extends FooComponent { + @action + foo() { + super.foo(); + } + } + + registerComponent(this, 'bar-bar', { + ComponentClass: BarComponent, + template: hbs``, + }); + + await render(hbs`{{bar-bar}}`); + + await click('button'); + }); + + // This test fails with _classes_ compiled in loose mode + skip('action decorator works with parent native class actions', async function(assert) { + class FooComponent extends Component { + @action + foo() { + assert.ok(true, 'called!'); + } + } + + class BarComponent extends FooComponent { + @action + foo() { + super.foo(); + } + } + + registerComponent(this, 'bar-bar', { + ComponentClass: BarComponent, + template: hbs``, + }); + + await render(hbs`{{bar-bar}}`); + + await click('button'); + }); + + test('action decorator binds functions', async function(assert) { + class FooComponent extends Component { + bar = 'some value'; + + @action + foo() { + assert.equal(this.bar, 'some value', 'context bound correctly'); + } + } + + registerComponent(this, 'foo-bar', { + ComponentClass: FooComponent, + template: hbs``, + }); + + await render(hbs`{{foo-bar}}`); + + await click('button'); + }); + + // This test fails with _classes_ compiled in loose mode + skip('action decorator super works correctly when bound', async function(assert) { + class FooComponent extends Component { + bar = 'some value'; + + @action + foo() { + assert.equal(this.bar, 'some value', 'context bound correctly'); + } + } + + class BarComponent extends FooComponent { + @action + foo() { + super.foo(); + } + } + + registerComponent(this, 'bar-bar', { + ComponentClass: BarComponent, + template: hbs``, + }); + + await render(hbs`{{bar-bar}}`); + + await click('button'); + }); + + test('action decorator throws an error if applied to non-methods', async function(assert) { + assert.throws(() => { + class TestObject extends EmberObject { + @action foo = 'bar'; + } + + new TestObject(); + }, /The @action decorator must be applied to methods/); + }); + + test('action decorator throws an error if passed a function in native classes', async function(assert) { + assert.throws(() => { + class TestObject extends EmberObject { + @action(function() {}) foo = 'bar'; + } + + new TestObject(); + }, /The @action decorator may only be passed a method when used in classic classes/); + }); + + test('action decorator can be used as a classic decorator with strings', async function(assert) { + let FooComponent = Component.extend({ + foo: action(function() { + assert.ok(true, 'called!'); + }), + }); + + registerComponent(this, 'foo-bar', { + ComponentClass: FooComponent, + template: hbs``, + }); + + await render(hbs`{{foo-bar}}`); + + await click('button'); + }); + + test('action decorator can be used as a classic decorator directly', async function(assert) { + let FooComponent = Component.extend({ + foo: action(function() { + assert.ok(true, 'called!'); + }), + }); + + registerComponent(this, 'foo-bar', { + ComponentClass: FooComponent, + template: hbs``, + }); + + await render(hbs`{{foo-bar}}`); + + await click('button'); + }); +}); diff --git a/tests/unit/computed-test.js b/tests/unit/computed-test.js new file mode 100644 index 0000000..bb333eb --- /dev/null +++ b/tests/unit/computed-test.js @@ -0,0 +1,446 @@ +import { DEBUG } from '@glimmer/env'; +import { module, test } from 'ember-qunit'; +import EmberObject, { computed, get, set, setProperties } from '@ember/object'; +import { addObserver } from '@ember/object/observers'; + +module('@computed', function() { + test('it works', function(assert) { + assert.expect(2); + + class Foo { + first = 'rob'; + last = 'jackson'; + + @computed('first', 'last') + get fullName() { + assert.equal(this.first, 'rob'); + assert.equal(this.last, 'jackson'); + } + } + + let obj = new Foo(); + get(obj, 'fullName'); + }); + + test('it works with functions', function(assert) { + assert.expect(2); + + class Foo { + first = 'rob'; + last = 'jackson'; + + @computed('first', 'last', function() { + assert.equal(this.first, 'rob'); + assert.equal(this.last, 'jackson'); + }) + fullName; + } + + let obj = new Foo(); + get(obj, 'fullName'); + }); + + test('it works with computed desc', function(assert) { + assert.expect(4); + + let expectedName = 'rob jackson'; + let expectedFirst = 'rob'; + let expectedLast = 'jackson'; + + class Foo { + first = 'rob'; + last = 'jackson'; + + @computed('first', 'last', { + get() { + assert.equal(this.first, expectedFirst, 'getter: first name matches'); + assert.equal(this.last, expectedLast, 'getter: last name matches'); + return `${this.first} ${this.last}`; + }, + + set(key, name) { + assert.equal(name, expectedName, 'setter: name matches'); + + const [first, last] = name.split(' '); + setProperties(this, { first, last }); + + return name; + }, + }) + fullName; + } + + let obj = new Foo(); + get(obj, 'fullName'); + + expectedName = 'yehuda katz'; + expectedFirst = 'yehuda'; + expectedLast = 'katz'; + set(obj, 'fullName', 'yehuda katz'); + + assert.strictEqual( + get(obj, 'fullName'), + expectedName, + 'return value of getter is new value of property' + ); + }); + + test('works with getter and setter', function(assert) { + assert.expect(6); + + let expectedName = 'rob jackson'; + let expectedFirst = 'rob'; + let expectedLast = 'jackson'; + + class Foo { + constructor() { + this.first = 'rob'; + this.last = 'jackson'; + } + + @computed('first', 'last') + get fullName() { + assert.equal(this.first, expectedFirst, 'getter: first name matches'); + assert.equal(this.last, expectedLast, 'getter: last name matches'); + return `${this.first} ${this.last}`; + } + + set fullName(name) { + assert.equal(name, expectedName, 'setter: name matches'); + + const [first, last] = name.split(' '); + setProperties(this, { first, last }); + } + } + + let obj = new Foo(); + get(obj, 'fullName'); + + expectedName = 'yehuda katz'; + expectedFirst = 'yehuda'; + expectedLast = 'katz'; + set(obj, 'fullName', 'yehuda katz'); + + assert.strictEqual( + get(obj, 'fullName'), + expectedName, + 'return value of getter is new value of property' + ); + }); + + test('it works with classic classes', function(assert) { + assert.expect(2); + + const Foo = EmberObject.extend({ + first: 'rob', + last: 'jackson', + + fullName: computed('first', 'last', function() { + assert.equal(this.first, 'rob'); + assert.equal(this.last, 'jackson'); + }), + }); + + let obj = Foo.create(); + get(obj, 'fullName'); + }); + + test('it works with classic classes with full desc', function(assert) { + assert.expect(4); + + let expectedName = 'rob jackson'; + let expectedFirst = 'rob'; + let expectedLast = 'jackson'; + + const Foo = EmberObject.extend({ + first: 'rob', + last: 'jackson', + + fullName: computed('first', 'last', { + get() { + assert.equal(this.first, expectedFirst, 'getter: first name matches'); + assert.equal(this.last, expectedLast, 'getter: last name matches'); + return `${this.first} ${this.last}`; + }, + + set(key, name) { + assert.equal(name, expectedName, 'setter: name matches'); + + const [first, last] = name.split(' '); + setProperties(this, { first, last }); + + return name; + }, + }), + }); + + let obj = Foo.create(); + get(obj, 'fullName'); + + expectedName = 'yehuda katz'; + expectedFirst = 'yehuda'; + expectedLast = 'katz'; + set(obj, 'fullName', 'yehuda katz'); + + assert.strictEqual( + get(obj, 'fullName'), + expectedName, + 'return value of getter is new value of property' + ); + }); + + test('dependent key changes invalidate the computed property', function(assert) { + class Foo { + first = 'rob'; + last = 'jackson'; + + @computed('first', 'last') + get name() { + return `${this.first} ${this.last}`; + } + } + + let obj = new Foo(); + + assert.equal(get(obj, 'name'), 'rob jackson'); + set(obj, 'first', 'al'); + assert.equal(get(obj, 'name'), 'al jackson'); + }); + + test('only calls getter when dependent keys change', function(assert) { + let callCount = 0; + class Foo { + first = 'rob'; + last = 'jackson'; + + @computed('first', 'last') + get name() { + callCount++; + } + } + + let obj = new Foo(); + + get(obj, 'name'); + assert.equal(callCount, 1); + + get(obj, 'name'); + assert.equal(callCount, 1); + + set(obj, 'first', 'al'); + get(obj, 'name'); + assert.equal(callCount, 2); + }); + + test('return value of ES6 setter is not required, but is not ignored', function(assert) { + class Foo { + constructor() { + this.first = 'rob'; + this.last = 'jackson'; + } + + @computed('first', 'last') + get fullNameNoReturn() { + return `${this.first} ${this.last}`; + } + + set fullNameNoReturn(name) { + const [first, last] = name.split(' '); + setProperties(this, { first, last }); + } + + @computed('first', 'last') + get fullNameWithReturn() { + return `${this.first} ${this.last}`; + } + + set fullNameWithReturn(name) { + const [first, last] = name.split(' '); + setProperties(this, { first, last }); + + return 'something else'; + } + } + + let obj = new Foo(); + + set(obj, 'fullNameNoReturn', 'yehuda katz'); + assert.strictEqual( + get(obj, 'fullNameNoReturn'), + 'yehuda katz', + 'return value of setter is not required, if there is a getter' + ); + + set(obj, 'fullNameWithReturn', 'tom dale'); + assert.strictEqual( + get(obj, 'fullNameWithReturn'), + 'something else', + 'if the setter returns a value, it is not ignored' + ); + }); + + test('can decorate the same property in multiple subclasses', function(assert) { + assert.expect(0); + + class Foo { + @computed('bar') + get fullName() {} + } + + class Bar extends Foo { + @computed('foo') + get fullName() {} + } + + class Baz extends Foo { + @computed('bar') + get fullName() {} + } + + // shouldn't cause any errors + new Foo(); + new Bar(); + new Baz(); + }); + + if (DEBUG) { + test('throws if used on non-getters', function(assert) { + assert.throws(() => { + class Foo { + constructor() { + this.first = 'rob'; + this.last = 'jackson'; + } + + @computed('first', 'last') + fullName() { + assert.equal(this.first, 'rob'); + assert.equal(this.last, 'jackson'); + } + } + + new Foo(); + }, /@computed can only be used on accessors or fields, attempted to use it with fullName but that was a method. Try converting it to a getter/); + }); + + test('throws if a ComputedDecorator is passed to `@computed`', function(assert) { + assert.throws(() => { + class Foo { + @computed( + computed('foo', 'bar', { + get() {}, + }) + ) + name; + } + + new Foo(); + }); + }); + } + + module('modifiers', function() { + test('volatile', function(assert) { + assert.expect(2); + class Foo extends EmberObject { + _count = 0; + + @(computed('first').volatile()) + get counter() { + return this._count++; + } + set counter(value) { + this._count = value; + } + } + + let obj = Foo.create(); + + addObserver(obj, 'counter', function() { + assert.ok(false, 'observer called'); + }); + + assert.equal(get(obj, 'counter'), 0, 'getter works'); + assert.equal(get(obj, 'counter'), 1, 'getter called each time'); + + set(obj, 'counter', 2); + }); + + test('readOnly', function(assert) { + class Foo { + first = 'rob'; + last = 'jackson'; + + @(computed('first', 'last').readOnly()) + get name() { + return `${this.first} ${this.last}`; + } + } + + let obj = new Foo(); + + assert.throws(() => { + set(obj, 'name', 'al'); + }, /Cannot set read-only property "name" on object:/); + }); + + test('property', function(assert) { + class Foo { + first = 'rob'; + last = 'jackson'; + + @(computed().property('first', 'last')) + get name() { + return `${this.first} ${this.last}`; + } + } + + let obj = new Foo(); + + set(obj, 'first', 'al'); + + assert.equal(get(obj, 'name'), 'al jackson'); + }); + + test('can be chained', assert => { + assert.throws(() => { + class Foo { + first = 'rob'; + last = 'jackson'; + + @(computed('first') + .volatile() + .readOnly() + .property('last')) + get name() { + return `${this.first} ${this.last}`; + } + } + + let obj = new Foo(); + + set(obj, 'name', 'al'); + }, /Cannot set read-only property "name" on object:/); + }); + + test('work on classic classes', assert => { + assert.throws(() => { + const Foo = EmberObject.extend({ + first: 'rob', + last: 'jackson', + + name: computed('first', function() { + return `${this.first} ${this.last}`; + }) + .volatile() + .readOnly() + .property('last'), + }); + + let obj = Foo.create(); + + set(obj, 'name', 'al'); + }, /Cannot set read-only property "name" on object:/); + }); + }); +}); diff --git a/tests/unit/inject-test.js b/tests/unit/inject-test.js new file mode 100644 index 0000000..f625e98 --- /dev/null +++ b/tests/unit/inject-test.js @@ -0,0 +1,165 @@ +import EmberObject from '@ember/object'; +import Controller from '@ember/controller'; +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import { inject as controller } from '@ember/controller'; +import { inject as service } from '@ember/service'; + +module('@controller', function(hooks) { + setupTest(hooks); + + test('it works', function(assert) { + const FooController = Controller.extend(); + + this.owner.register('controller:foo', FooController); + + class BarController extends Controller { + @controller foo; + } + + this.owner.register('controller:bar', BarController); + + const bar = this.owner.lookup('controller:bar'); + + assert.ok( + bar.get('foo') instanceof FooController, + 'controller injected correctly' + ); + }); + + test('controller decorator works with controller name', function(assert) { + const FooController = Controller.extend(); + + this.owner.register('controller:foo', FooController); + + class BarController extends Controller { + @controller('foo') baz; + } + + this.owner.register('controller:bar', BarController); + + const bar = this.owner.lookup('controller:bar'); + + assert.ok( + bar.get('baz') instanceof FooController, + 'controller injected correctly' + ); + }); + + test('can set controller field', function(assert) { + assert.expect(0); + + const FooController = Controller.extend(); + + this.owner.register('controller:foo', FooController); + + class BarController extends Controller { + @controller foo; + } + + this.owner.register('controller:bar', BarController); + + const bar = this.owner.lookup('controller:bar'); + + bar.set('foo', FooController.create()); + }); + + test('can use in classic classes', function(assert) { + const FooController = Controller.extend(); + + this.owner.register('controller:foo', FooController); + + const BarController = Controller.extend({ + foo: controller(), + }); + + this.owner.register('controller:bar', BarController); + + const bar = this.owner.lookup('controller:bar'); + + assert.ok( + bar.get('foo') instanceof FooController, + 'controller injected correctly' + ); + }); +}); + +module('@service', function(hooks) { + setupTest(hooks); + + test('it works', function(assert) { + const FooService = EmberObject.extend(); + + this.owner.register('service:foo', FooService); + + class Baz extends EmberObject { + @service foo; + } + + this.owner.register('class:baz', Baz); + + const baz = this.owner.lookup('class:baz'); + + assert.ok( + baz.get('foo') instanceof FooService, + 'service injected correctly' + ); + }); + + test('it works by passing name', function(assert) { + const FooService = EmberObject.extend(); + + this.owner.register('service:foo', FooService); + + class Baz extends EmberObject { + @service('foo') bar; + } + + this.owner.register('class:baz', Baz); + + const baz = this.owner.lookup('class:baz'); + + assert.ok( + baz.get('bar') instanceof FooService, + 'service injected correctly' + ); + }); + + test('can set service field', function(assert) { + assert.expect(0); + + const FooService = EmberObject.extend(); + + this.owner.register('service:foo', FooService); + + class Baz extends EmberObject { + @service foo; + } + + this.owner.register('class:baz', Baz); + + const baz = this.owner.lookup('class:baz'); + + baz.set('foo', FooService.create()); + }); + + test('can use in classic classes', function(assert) { + const FooService = EmberObject.extend(); + + this.owner.register('service:foo', FooService); + + const Baz = EmberObject.extend({ + foo: service(), + }); + + this.owner.register('class:baz', Baz); + + const baz = this.owner.lookup('class:baz'); + + assert.ok( + baz.get('foo') instanceof FooService, + 'service injected correctly' + ); + }); +}); diff --git a/tests/unit/macro-test.js b/tests/unit/macro-test.js new file mode 100644 index 0000000..68997d6 --- /dev/null +++ b/tests/unit/macro-test.js @@ -0,0 +1,690 @@ +import { DEBUG } from '@glimmer/env'; + +import EmberObject, { get, set } from '@ember/object'; +import { A as emberA } from '@ember/array'; + +import { + alias, + and, + bool, + collect, + deprecatingAlias, + empty, + equal, + filter, + filterBy, + gt, + gte as emberGte, + intersect, + lt, + lte, + map, + mapBy, + match, + max, + min, + none, + not, + notEmpty, + oneWay, + or, + reads, + readOnly, + setDiff, + sort, + sum, + union, + uniq, + uniqBy, +} from '@ember/object/computed'; + +import { module, test } from 'qunit'; +import { gte } from 'ember-compatibility-helpers'; + +module('macros', function() { + test('@alias', function(assert) { + class Foo { + constructor() { + this.friend = 'Guy'; + } + + @alias('friend') buddy; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'buddy'), 'Guy'); + + set(obj, 'buddy', 'Pal'); + assert.equal(get(obj, 'friend'), 'Pal'); + }); + + test('@and', function(assert) { + class Foo { + constructor() { + this.taco = true; + this.burrito = false; + this.fried = true; + } + + @and('taco', 'fried') isChalupa; + @and('burrito', 'fried') isChimichanga; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'isChalupa'), true); + assert.equal(get(obj, 'isChimichanga'), false); + }); + + test('@bool', function(assert) { + class Foo { + constructor() { + this.one = 1; + this.zero = 0; + this.string = 'string'; + this.undefined = undefined; + this.null = null; + } + @bool('one') isOneAwesome; + @bool('zero') isZeroSad; + @bool('string') isStringCool; + @bool('undefined') isUndefinedAlright; + @bool('null') isNullRad; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'isOneAwesome'), true); + assert.equal(get(obj, 'isZeroSad'), false); + assert.equal(get(obj, 'isStringCool'), true); + assert.equal(get(obj, 'isUndefinedAlright'), false); + assert.equal(get(obj, 'isNullRad'), false); + }); + + test('@collect', function(assert) { + class Foo { + constructor() { + this.topping = 'cheese'; + this.meat = 'beef'; + this.shell = 'tortilla'; + } + @collect('topping', 'meat', 'shell') taco; + @collect('topping', 'shell') quesadilla; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'taco').toArray(), [ + 'cheese', + 'beef', + 'tortilla', + ]); + assert.deepEqual(get(obj, 'quesadilla').toArray(), ['cheese', 'tortilla']); + }); + + test('@deprecatingAlias', function(assert) { + class Foo { + constructor() { + this.friend = 'Guy'; + } + @deprecatingAlias('friend', { + id: 'user-profile.firstName', + until: '3.0.0', + url: 'https://example.com/deprecations/user-profile.firstName', + }) + buddy; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'buddy'), 'Guy'); + set(obj, 'buddy', 'Tomster'); + assert.equal(get(obj, 'friend'), 'Tomster'); + }); + + test('@empty', function(assert) { + class Foo { + constructor() { + this.names = emberA(['one', 'two', 'three']); + } + @empty('names') hasNames; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'hasNames'), false); + set(obj, 'names', []); + assert.equal(get(obj, 'hasNames'), true); + set(obj, 'names', null); + assert.equal(get(obj, 'hasNames'), true); + set(obj, 'names', undefined); + assert.equal(get(obj, 'hasNames'), true); + }); + + test('@equal', function(assert) { + class Foo { + constructor() { + this.name = 'Tom'; + } + @equal('name', 'Tomster') isMascot; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'isMascot'), false); + set(obj, 'name', 'Tomster'); + assert.equal(get(obj, 'isMascot'), true); + }); + + test('@filter', function(assert) { + class Foo { + constructor() { + this.names = emberA([ + { name: 'b', valid: true }, + { name: 'z', valid: true }, + { name: 'a', valid: true }, + { name: 'foo', valid: false }, + ]); + } + @filter('names', function(item) { + return item.valid; + }) + validNames; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'validNames').mapBy('name'), ['b', 'z', 'a']); + }); + + test('@filterBy', function(assert) { + class Foo { + constructor() { + this.names = emberA([ + { name: 'b', valid: true }, + { name: 'z', valid: true }, + { name: 'a', valid: true }, + { name: 'foo', valid: false }, + ]); + } + @filterBy('names', 'valid') validNames; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'validNames').mapBy('name'), ['b', 'z', 'a']); + }); + + test('@gt', function(assert) { + class Foo { + constructor() { + this.total = 9; + } + @gt('total', 10) isGtTen; + } + + let obj = new Foo(); + assert.equal(get(obj, 'isGtTen'), false); + set(obj, 'total', 10); + assert.equal(get(obj, 'isGtTen'), false); + set(obj, 'total', 11); + assert.equal(get(obj, 'isGtTen'), true); + }); + + test('@gte', function(assert) { + class Foo { + constructor() { + this.total = 9; + } + @emberGte('total', 10) isGteTen; + } + + let obj = new Foo(); + assert.equal(get(obj, 'isGteTen'), false); + set(obj, 'total', 10); + assert.equal(get(obj, 'isGteTen'), true); + set(obj, 'total', 11); + assert.equal(get(obj, 'isGteTen'), true); + }); + + test('@intersect', function(assert) { + class Foo { + constructor() { + this.cool = emberA(['tacos', 'unicorns', 'pirates']); + this.rad = emberA(['tacos', 'zombies', 'ninjas']); + } + @intersect('cool', 'rad') coolRad; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'coolRad').toArray(), ['tacos']); + }); + + test('@lt', function(assert) { + class Foo { + constructor() { + this.total = 11; + } + @lt('total', 10) isLtTen; + } + + let obj = new Foo(); + assert.equal(get(obj, 'isLtTen'), false); + set(obj, 'total', 10); + assert.equal(get(obj, 'isLtTen'), false); + set(obj, 'total', 9); + assert.equal(get(obj, 'isLtTen'), true); + }); + + test('@lte', function(assert) { + class Foo { + constructor() { + this.total = 11; + } + @lte('total', 10) isLteTen; + } + + let obj = new Foo(); + assert.equal(get(obj, 'isLteTen'), false); + set(obj, 'total', 10); + assert.equal(get(obj, 'isLteTen'), true); + set(obj, 'total', 9); + assert.equal(get(obj, 'isLteTen'), true); + }); + + test('@map', function(assert) { + class Foo { + constructor() { + this.names = emberA(['one', 'two', 'three']); + } + @map('names', function(name) { + return name.toUpperCase() + '!'; + }) + loudNames; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'loudNames').toArray(), [ + 'ONE!', + 'TWO!', + 'THREE!', + ]); + }); + + test('@mapBy', function(assert) { + class Foo { + constructor() { + this.names = emberA([ + { name: 'b', valid: true }, + { name: 'z', valid: true }, + { name: 'a', valid: true }, + ]); + } + @mapBy('names', 'name') nameValues; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'nameValues').toArray(), ['b', 'z', 'a']); + }); + + test('@match', function(assert) { + class Foo { + constructor() {} + @match('email', /^.+@.+\..+$/) hasValidEmail; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'hasValidEmail'), false); + set(obj, 'email', ''); + assert.equal(get(obj, 'hasValidEmail'), false); + set(obj, 'email', 'ember_hamster@example.com'); + assert.equal(get(obj, 'hasValidEmail'), true); + }); + + test('@max', function(assert) { + class Foo { + constructor() { + this.values = emberA([1, 2, 3, 4, 5]); + } + @max('values') maxValue; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'maxValue'), 5); + get(obj, 'values').pushObject(10); + assert.equal(get(obj, 'maxValue'), 10); + }); + + test('@min', function(assert) { + class Foo { + constructor() { + this.values = emberA([5, 6, 7, 8, 9, 10]); + } + @min('values') minValue; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'minValue'), 5); + get(obj, 'values').pushObject(1); + assert.equal(get(obj, 'minValue'), 1); + }); + + test('@none', function(assert) { + class Foo { + constructor() { + this.thing = 'foo'; + } + @none('thing') isThing; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'isThing'), false); + set(obj, 'thing', null); + assert.equal(get(obj, 'isThing'), true); + }); + + test('@not', function(assert) { + class Foo { + constructor() { + this.aThing = 'chickenWing'; + } + @not('aThing') notAThing; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'notAThing'), false); + set(obj, 'aThing', null); + assert.equal(get(obj, 'notAThing'), true); + }); + + test('@notEmpty', function(assert) { + class Foo { + constructor() { + this.names = emberA(['one', 'one', 'two', 'three']); + } + @notEmpty('names') isNamesEmpty; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'isNamesEmpty'), true); + set(obj, 'names', []); + assert.equal(get(obj, 'isNamesEmpty'), false); + }); + + test('@oneWay', function(assert) { + class Foo { + constructor() { + this.names = 'Tom'; + this.nick = 'Tomster'; + } + @oneWay('nick') nickName; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'nickName'), 'Tomster'); + set(obj, 'nickName', 'Honeybadger'); + assert.equal(get(obj, 'nickName'), 'Honeybadger'); + assert.equal(get(obj, 'nick'), 'Tomster'); + }); + + test('@or', function(assert) { + class Foo { + constructor() { + this.cool = true; + this.rad = 'rad'; + } + @or('cool', 'rad') orValue; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'orValue'), true); + set(obj, 'cool', undefined); + assert.equal(get(obj, 'orValue'), 'rad'); + }); + + test('@reads', function(assert) { + class Foo { + constructor() { + this.names = 'Tom'; + this.nick = 'Tomster'; + } + @reads('nick') nickName; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'nickName'), 'Tomster'); + set(obj, 'nickName', 'Honeybadger'); + assert.equal(get(obj, 'nickName'), 'Honeybadger'); + assert.equal(get(obj, 'nick'), 'Tomster'); + }); + + test('@readOnly', function(assert) { + class Foo { + constructor() { + this.names = 'Tom'; + this.nick = 'Tomster'; + } + @readOnly('nick') nickName; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'nickName'), 'Tomster'); + + assert.throws(() => { + set(obj, 'nickName', 'Honeybadger'); + }, /Cannot set read-only property ['"]nickName['"] on object/); + }); + + test('@setDiff', function(assert) { + class Foo { + constructor() { + this.numbers = emberA(['one', 'two', 'three']); + this.oddNumbers = emberA(['one', 'three']); + } + + get(key) { + // setDiff uses this.get internally + return get(this, key); + } + + @setDiff('numbers', 'oddNumbers') evenNumbers; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'evenNumbers'), ['two']); + }); + + test('@sort', function(assert) { + class Foo { + constructor() { + this.names = emberA([ + { name: 'b' }, + { name: 'z' }, + { name: 'a' }, + { name: 'foo' }, + ]); + } + @sort('names', function(a, b) { + if (a.name > b.name) { + return 1; + } else if (a.name < b.name) { + return -1; + } + + return 0; + }) + sortedNames; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'sortedNames').mapBy('name'), [ + 'a', + 'b', + 'foo', + 'z', + ]); + }); + + test('@sort (no callback, use property value)', function(assert) { + class Foo { + constructor() { + this.names = emberA([ + { name: 'b' }, + { name: 'z' }, + { name: 'a' }, + { name: 'foo' }, + ]); + } + + sorts = ['name:asc']; + + @sort('names', 'sorts') sortedNames; + } + + let obj = new Foo(); + + var actual = get(obj, 'sortedNames').mapBy('name'); + assert.deepEqual(actual, ['a', 'b', 'foo', 'z']); + }); + + test('@sum', function(assert) { + class Foo { + constructor() { + this.things = emberA([1, 2, 3]); + } + @sum('things') countTotal; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'countTotal'), 6); + get(obj, 'things').pushObject(20); + assert.equal(get(obj, 'countTotal'), 26); + }); + + test('@union', function(assert) { + class Foo { + constructor() { + this.names = emberA(['one', 'one', 'two', 'three']); + this.numbers = emberA(['twenty', 'two']); + } + @union('names', 'numbers') unionNames; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'unionNames').toArray(), [ + 'one', + 'two', + 'three', + 'twenty', + ]); + }); + + test('@uniq', function(assert) { + class Foo { + constructor() { + this.names = emberA(['one', 'one', 'two', 'three']); + } + @uniq('names') uniqNames; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'uniqNames').toArray(), ['one', 'two', 'three']); + }); + + test('@uniqBy', function(assert) { + class Foo { + constructor() { + this.fruits = emberA([ + { name: 'banana', color: 'yellow' }, + { name: 'apple', color: 'red' }, + { name: 'kiwi', color: 'brown' }, + { name: 'cherry', color: 'red' }, + { name: 'lemon', color: 'yellow' }, + ]); + } + @uniqBy('fruits', 'color') oneOfEachColor; + } + + let obj = new Foo(); + + assert.deepEqual(get(obj, 'oneOfEachColor').toArray(), [ + { name: 'banana', color: 'yellow' }, + { name: 'apple', color: 'red' }, + { name: 'kiwi', color: 'brown' }, + ]); + }); + + test('readOnly modifier', function(assert) { + class Foo { + constructor() { + this.userName = 'Brohuda'; + } + @(alias('userName').readOnly()) finalName; + } + + let obj = new Foo(); + + assert.equal(get(obj, 'finalName'), 'Brohuda'); + assert.throws( + () => { + set(obj, 'finalName', 'Brotom'); + }, + /Cannot set read-only property ['"]finalName['"] on object/, + 'error message thrown when trying to set readOnly property' + ); + + assert.equal(get(obj, 'finalName'), 'Brohuda'); + }); + + test('macros can be used with classic classes', function(assert) { + const Foo = EmberObject.extend({ + init() { + this.friend = 'Guy'; + }, + + buddy: alias('friend'), + }); + + let obj = Foo.create(); + + assert.equal(get(obj, 'buddy'), 'Guy'); + + set(obj, 'buddy', 'Pal'); + assert.equal(get(obj, 'friend'), 'Pal'); + }); + + if (DEBUG && !gte('3.10.0')) { + test('macros cannot be used without parameters', function(assert) { + assert.throws( + () => { + class Foo { + @alias uniqNames; + } + + new Foo(); + }, + /@alias/, + 'error thrown correctly' + ); + }); + } +}); diff --git a/vendor/.gitkeep b/vendor/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/vendor/ember-decorators-polyfill/index.js b/vendor/ember-decorators-polyfill/index.js new file mode 100644 index 0000000..cc3d19e --- /dev/null +++ b/vendor/ember-decorators-polyfill/index.js @@ -0,0 +1,504 @@ +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; + +import { + HAS_NATIVE_COMPUTED_GETTERS, + HAS_DESCRIPTOR_TRAP, + gte, +} from 'ember-compatibility-helpers'; + +(function() { + const { + assign, + computed: emberComputed, + ComputedProperty, + defineProperty, + inject: { controller: injectController, service: injectService }, + } = Ember; + + // ***** UTILITIES ***** + + const DESCRIPTOR = '__DESCRIPTOR__'; + + function isCPGetter(getter) { + // 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) { + if (HAS_DESCRIPTOR_TRAP && DEBUG) { + return ( + possibleDesc !== null && + typeof possibleDesc === 'object' && + possibleDesc[DESCRIPTOR] !== undefined + ); + } else { + throw new Error('Cannot call `isDescriptorTrap` in production'); + } + } + + function isComputedDescriptor(possibleDesc) { + return ( + possibleDesc !== null && + typeof possibleDesc === 'object' && + possibleDesc.isDescriptor + ); + } + + function computedDescriptorFor(obj, keyName) { + 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' + ); + + if (HAS_NATIVE_COMPUTED_GETTERS) { + let meta = Ember.meta(obj); + + if (meta !== undefined && typeof meta._descriptors === 'object') { + // TODO: Just return the standard descriptor + if (gte('3.8.0')) { + return meta._descriptors.get(keyName); + } else { + return meta._descriptors[keyName]; + } + } + } else if (Object.hasOwnProperty.call(obj, keyName)) { + let { + value: possibleDesc, + get: possibleCPGetter, + } = Object.getOwnPropertyDescriptor(obj, keyName); + + if (DEBUG && HAS_DESCRIPTOR_TRAP && isCPGetter(possibleCPGetter)) { + possibleDesc = possibleCPGetter.call(obj); + + if (isDescriptorTrap(possibleDesc)) { + return possibleDesc[DESCRIPTOR]; + } + } + + return isComputedDescriptor(possibleDesc) ? possibleDesc : undefined; + } + } + + function isFieldDescriptor(possibleDesc) { + let [target, key, desc] = possibleDesc; + + return ( + possibleDesc.length === 3 && + typeof target === 'object' && + target !== null && + typeof key === 'string' && + ((typeof desc === 'object' && + desc !== null && + 'enumerable' in desc && + 'configurable' in desc) || + desc === undefined) // TS compatibility + ); + } + + const DECORATOR_COMPUTED_FN = new WeakMap(); + const DECORATOR_PARAMS = new WeakMap(); + const DECORATOR_MODIFIERS = new WeakMap(); + + // eslint-disable-next-line no-inner-declarations + function buildComputedDesc(dec, prototype, key, desc) { + let fn = DECORATOR_COMPUTED_FN.get(dec); + let params = DECORATOR_PARAMS.get(dec); + let modifiers = DECORATOR_MODIFIERS.get(dec); + + let computedDesc = fn(prototype, key, desc, params); + + assert( + `computed decorators must return an instance of an Ember ComputedProperty descriptor, received ${computedDesc}`, + isComputedDescriptor(computedDesc) + ); + + if (modifiers) { + modifiers.forEach(m => { + if (Array.isArray(m)) { + computedDesc[m[0]](...m[1]); + } else { + computedDesc[m](); + } + }); + } + + return computedDesc; + } + + class DecoratorDescriptor extends ComputedProperty { + setup(obj, key, meta) { + if (!this._computedDesc) { + this._computedDesc = buildComputedDesc(this, obj, key, {}); + } + + if (gte('3.6.0')) { + this._computedDesc.setup(obj, key, meta); + } else if (gte('3.1.0')) { + let meta = Ember.meta(obj); + + Object.defineProperty(obj, key, { + configurable: true, + enumerable: true, + get() { + return this._computedDesc.get(key); + }, + }); + + meta.writeDescriptors(key, this._computedDesc); + } else { + Object.defineProperty(obj, key, { + configurable: true, + writable: true, + enumerable: true, + value: this._computedDesc, + }); + } + } + + _addModifier(modifier) { + let modifiers = DECORATOR_MODIFIERS.get(this); + + if (modifiers === undefined) { + modifiers = []; + DECORATOR_MODIFIERS.set(this, modifiers); + } + + modifiers.push(modifier); + } + + get() { + return this._innerComputed.get.apply(this, arguments); + } + + set() { + return this._innerComputed.get.apply(this, arguments); + } + + readOnly() { + this._addModifier('readOnly'); + return this; + } + + volatile() { + this._addModifier('volatile'); + return this; + } + + property(...keys) { + this._addModifier(['property', keys]); + return this; + } + + meta(...args) { + this._addModifier(['meta', args]); + return this; + } + } + + function computedDecorator(fn, params) { + let dec = function(prototype, 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 = buildComputedDesc(dec, prototype, key, desc); + + 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); + + // There's currently no way to disable redefining the property when decorators + // are run, so return the property descriptor we just assigned + return Object.getOwnPropertyDescriptor(prototype, key); + }; + + Object.setPrototypeOf(dec, DecoratorDescriptor.prototype); + + DECORATOR_COMPUTED_FN.set(dec, fn); + DECORATOR_PARAMS.set(dec, params); + + return dec; + } + + function computedDecoratorWithParams(fn) { + return function(...params) { + if (isFieldDescriptor(params)) { + return Function.apply.call(computedDecorator(fn), undefined, params); + } else { + return computedDecorator(fn, params); + } + }; + } + + function computedDecoratorWithRequiredParams(fn, name) { + return function(...params) { + assert( + `The @${name || fn.name} decorator requires parameters`, + !isFieldDescriptor(params) && params.length > 0 + ); + + return computedDecorator(fn, params); + }; + } + + function legacyMacro(fn, fnName) { + let decorator = computedDecoratorWithRequiredParams( + (prototype, key, desc, params) => { + return fn(...params); + }, + fnName + ); + + if (DEBUG) { + Object.defineProperty(decorator, 'name', { + value: fnName, + }); + } + + return decorator; + } + + // ***** COMPUTED ***** + + Ember.ComputedProperty = DecoratorDescriptor; + Ember.computed = computedDecoratorWithParams( + (prototype, key, desc, params = []) => { + assert( + `@computed can only be used on accessors or fields, attempted to use it with ${key} but that was a method. Try converting it to a getter (e.g. \`get ${key}() {}\`)`, + !(desc && typeof desc.value === 'function') + ); + + assert( + `@computed can only be used on empty fields. ${key} has an initial value (e.g. \`${key} = someValue\`)`, + !(desc && desc.initializer) + ); + + let lastArg = params[params.length - 1]; + let get, set; + + assert( + `computed properties should not be passed to @computed directly`, + !( + (typeof lastArg === 'function' || typeof lastArg === 'object') && + lastArg instanceof ComputedProperty + ) + ); + + if (typeof lastArg === 'function') { + params.pop(); + get = lastArg; + } + + if (typeof lastArg === 'object' && lastArg !== null) { + params.pop(); + get = lastArg.get; + set = lastArg.set; + } + + assert( + `Attempted to apply a computed property that already has a getter/setter to a ${key}, but it is a method or an accessor. If you passed @computed a function or getter/setter (e.g. \`@computed({ get() { ... } })\`), then it must be applied to a field`, + !( + desc && + (typeof get === 'function' || typeof 'set' === 'function') && + (typeof desc.get === 'function' || typeof desc.get === 'function') + ) + ); + + let usedClassDescriptor = false; + + if (get === undefined && set === undefined) { + usedClassDescriptor = true; + get = desc.get; + set = desc.set; + } + + assert( + `Attempted to use @computed on ${key}, but it did not have a getter or a setter. You must either pass a get a function or getter/setter to @computed directly (e.g. \`@computed({ get() { ... } })\`) or apply @computed directly to a getter/setter`, + typeof get === 'function' || typeof 'set' === 'function' + ); + + if (desc !== undefined) { + // Unset the getter and setter so the descriptor just has a plain value + desc.get = undefined; + desc.set = undefined; + } + + let setter = set; + + if (usedClassDescriptor === true && typeof set === 'function') { + // Because the setter was defined using class syntax, it cannot have the + // same `set(key, value)` signature, and it may not return a value. We + // convert the call internally to pass the value as the first parameter, + // and check to see if the return value is undefined and if so call the + // getter again to get the value explicitly. + setter = function(key, value) { + let ret = set.call(this, value); + return typeof ret === 'undefined' ? get.call(this) : ret; + }; + } + + return emberComputed(...params, { get, set: setter }); + } + ); + + // ***** COMPUTED MACROS ***** + + Ember.computed.alias = legacyMacro(emberComputed.alias, 'alias'); + Ember.computed.and = legacyMacro(emberComputed.and, 'and'); + Ember.computed.bool = legacyMacro(emberComputed.bool, 'bool'); + Ember.computed.collect = legacyMacro(emberComputed.collect, 'collect'); + Ember.computed.deprecatingAlias = legacyMacro( + emberComputed.deprecatingAlias, + 'deprecatingAlias' + ); + Ember.computed.empty = legacyMacro(emberComputed.empty, 'empty'); + Ember.computed.equal = legacyMacro(emberComputed.equal, 'equal'); + Ember.computed.filter = legacyMacro(emberComputed.filter, 'filter'); + Ember.computed.filterBy = legacyMacro(emberComputed.filterBy, 'filterBy'); + Ember.computed.gt = legacyMacro(emberComputed.gt, 'gt'); + Ember.computed.gte = legacyMacro(emberComputed.gte, 'gte'); + Ember.computed.intersect = legacyMacro(emberComputed.intersect, 'intersect'); + Ember.computed.lt = legacyMacro(emberComputed.lt, 'lt'); + Ember.computed.lte = legacyMacro(emberComputed.lte, 'lte'); + Ember.computed.map = legacyMacro(emberComputed.map, 'map'); + Ember.computed.mapBy = legacyMacro(emberComputed.mapBy, 'mapBy'); + Ember.computed.match = legacyMacro(emberComputed.match, 'match'); + Ember.computed.max = legacyMacro(emberComputed.max, 'max'); + Ember.computed.min = legacyMacro(emberComputed.min, 'min'); + Ember.computed.none = legacyMacro(emberComputed.none, 'none'); + Ember.computed.not = legacyMacro(emberComputed.not, 'not'); + Ember.computed.notEmpty = legacyMacro(emberComputed.notEmpty, 'notEmpty'); + Ember.computed.oneWay = legacyMacro(emberComputed.oneWay, 'oneWay'); + Ember.computed.or = legacyMacro(emberComputed.or, 'or'); + Ember.computed.reads = legacyMacro(emberComputed.reads, 'reads'); + Ember.computed.readOnly = legacyMacro(emberComputed.readOnly, 'readOnly'); + Ember.computed.setDiff = legacyMacro(emberComputed.setDiff, 'setDiff'); + Ember.computed.sort = legacyMacro(emberComputed.sort, 'sort'); + Ember.computed.sum = legacyMacro(emberComputed.sum, 'sum'); + Ember.computed.union = legacyMacro(emberComputed.union, 'union'); + Ember.computed.uniq = legacyMacro(emberComputed.uniq, 'uniq'); + Ember.computed.uniqBy = legacyMacro(emberComputed.uniqBy, 'uniqBy'); + + // ***** INJECTIONS ***** + + Ember.inject.controller = computedDecoratorWithParams( + (prototype, key, desc, params) => { + return injectController.apply(this, params); + } + ); + + Ember.inject.service = computedDecoratorWithParams( + (prototype, key, desc, params) => { + return injectService.apply(this, params); + } + ); + + // ***** ACTION ***** + + let BINDINGS_MAP = new WeakMap(); + + function setupAction(target, key, actionFn) { + if ( + target.constructor !== undefined && + typeof target.constructor.proto === 'function' + ) { + target.constructor.proto(); + } + + if (!target.hasOwnProperty('actions')) { + let parentActions = target.actions; + // we need to assign because of the way mixins copy actions down when inheriting + target.actions = parentActions ? assign({}, parentActions) : {}; + } + + target.actions[key] = actionFn; + + return { + get() { + let bindings = BINDINGS_MAP.get(this); + + if (bindings === undefined) { + bindings = new Map(); + BINDINGS_MAP.set(this, bindings); + } + + let fn = bindings.get(actionFn); + + if (fn === undefined) { + fn = actionFn.bind(this); + bindings.set(actionFn, fn); + } + + return fn; + }, + }; + } + + class ActionDecoratorDescriptor extends ComputedProperty { + setup(obj, key, meta) { + assert( + 'The action() decorator must be passed a method when used in classic classes', + typeof this.__ACTION_FN__ === 'function' + ); + + Object.defineProperty( + obj, + key, + setupAction(obj, key, this.__ACTION_FN__) + ); + } + + get(obj, key) { + return obj[key]; + } + } + + Ember._action = function(target, key, desc) { + let actionFn; + + if (!isFieldDescriptor([target, key, desc])) { + actionFn = target; + + let decorator = function(target, key, desc) { + assert( + 'The @action decorator may only be passed a method when used in classic classes. You should decorate methods directly in native classes', + false + ); + }; + + decorator.__ACTION_FN__ = actionFn; + + Object.setPrototypeOf(decorator, ActionDecoratorDescriptor.prototype); + + return decorator; + } + + actionFn = desc.value; + + assert( + 'The @action decorator must be applied to methods when used in native classes', + typeof actionFn === 'function' + ); + + return setupAction(target, key, actionFn); + }; +})(); diff --git a/yarn.lock b/yarn.lock index 7b77612..143f203 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,7 +225,7 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.4.0", "@babel/parser@^7.4.3": +"@babel/parser@^7.0.0", "@babel/parser@^7.4.0", "@babel/parser@^7.4.3": version "7.4.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.3.tgz#eb3ac80f64aa101c907d4ce5406360fe75b7895b" integrity sha512-gxpEUhTS1sGA63EGQGuA+WESPR/6tz6ng7tSHFCmaTJK/cGK8y37cBTspX+U2xCAue2IQVvF6Z0oigmjwD8YGQ== @@ -655,7 +655,7 @@ "@babel/parser" "^7.4.0" "@babel/types" "^7.4.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.0", "@babel/traverse@^7.4.3": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.0", "@babel/traverse@^7.4.3": version "7.4.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.3.tgz#1a01f078fc575d589ff30c0f71bf3c3d9ccbad84" integrity sha512-HmA01qrtaCwwJWpSKpA948cBvU5BrmviAief/b3AVw936DtcdsTexlbyzNuDnthwhOQ37xshn7hvQaEQk7ISYQ== @@ -1103,6 +1103,18 @@ babel-core@^6.26.0: slash "^1.0.0" source-map "^0.5.7" +babel-eslint@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed" + integrity sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.0.0" + "@babel/traverse" "^7.0.0" + "@babel/types" "^7.0.0" + eslint-scope "3.7.1" + eslint-visitor-keys "^1.0.0" + babel-generator@^6.26.0: version "6.26.1" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" @@ -1251,7 +1263,7 @@ babel-plugin-debug-macros@^0.1.10: dependencies: semver "^5.3.0" -babel-plugin-debug-macros@^0.2.0-beta.6: +babel-plugin-debug-macros@^0.2.0, babel-plugin-debug-macros@^0.2.0-beta.6: version "0.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.2.0.tgz#0120ac20ce06ccc57bf493b667cf24b85c28da7a" integrity sha512-Wpmw4TbhR3Eq2t3W51eBAQSdKlr+uAyF0GI4GtPfMCD12Y4cIdpKC9l0RjNTH/P9isFypSqqewMPm7//fnZlNA== @@ -3184,7 +3196,7 @@ ember-cli-uglify@^2.1.0: broccoli-uglify-sourcemap "^2.1.1" lodash.defaultsdeep "^4.6.0" -ember-cli-version-checker@^2.0.0, ember-cli-version-checker@^2.1.0, ember-cli-version-checker@^2.1.2: +ember-cli-version-checker@^2.0.0, ember-cli-version-checker@^2.1.0, ember-cli-version-checker@^2.1.1, ember-cli-version-checker@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.2.0.tgz#47771b731fe0962705e27c8199a9e3825709f3b3" integrity sha512-G+KtYIVlSOWGcNaTFHk76xR4GdzDLzAS4uxZUKdASuFX0KJE43C6DaqL+y3VTpUFLI2FIkAS6HZ4I1YBi+S3hg== @@ -3192,7 +3204,7 @@ ember-cli-version-checker@^2.0.0, ember-cli-version-checker@^2.1.0, ember-cli-ve resolve "^1.3.3" semver "^5.3.0" -ember-cli-version-checker@^3.0.0: +ember-cli-version-checker@^3.0.0, ember-cli-version-checker@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-3.1.3.tgz#7c9b4f5ff30fdebcd480b1c06c4de43bb51c522c" integrity sha512-PZNSvpzwWgv68hcXxyjREpj3WWb81A7rtYNQq1lLEgrWIchF8ApKJjWP3NBpHjaatwILkZAV8klair5WFlXAKg== @@ -3296,6 +3308,15 @@ ember-cli@~3.8.1: watch-detector "^0.1.0" yam "^1.0.0" +ember-compatibility-helpers@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.0.tgz#feee16c5e9ef1b1f1e53903b241740ad4b01097e" + integrity sha512-pUW4MzJdcaQtwGsErYmitFRs0rlCYBAnunVzlFFUBr4xhjlCjgHJo0b53gFnhTgenNM3d3/NqLarzRhDTjXRTg== + dependencies: + babel-plugin-debug-macros "^0.2.0" + ember-cli-version-checker "^2.1.1" + semver "^5.4.1" + ember-disable-prototype-extensions@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/ember-disable-prototype-extensions/-/ember-disable-prototype-extensions-1.1.3.tgz#1969135217654b5e278f9fe2d9d4e49b5720329e" @@ -3567,6 +3588,14 @@ eslint-plugin-node@^7.0.1: resolve "^1.8.1" semver "^5.5.0" +eslint-scope@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug= + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-scope@^3.7.1: version "3.7.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535"