diff --git a/.travis.yml b/.travis.yml index c8cec19..3c2e1e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,8 +49,13 @@ jobs: # we recommend new addons test the current and previous LTS # as well as latest stable release (bonus points to beta/canary) - stage: "Additional Tests" - env: EMBER_TRY_SCENARIO=ember-lts-2.18 + env: EMBER_TRY_SCENARIO=ember-lts-2.12 + - env: EMBER_TRY_SCENARIO=ember-lts-2.16 + - env: EMBER_TRY_SCENARIO=ember-lts-2.18 - env: EMBER_TRY_SCENARIO=ember-lts-3.4 + - env: EMBER_TRY_SCENARIO=ember-3.5 + - env: EMBER_TRY_SCENARIO=ember-3.6 + - env: EMBER_TRY_SCENARIO=ember-3.7 - env: EMBER_TRY_SCENARIO=ember-release - env: EMBER_TRY_SCENARIO=ember-beta - env: EMBER_TRY_SCENARIO=ember-canary diff --git a/addon/.gitkeep b/addon/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/.gitkeep b/app/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/config/ember-try.js b/config/ember-try.js index ec08a5b..62a0538 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -11,6 +11,30 @@ module.exports = function() { return { useYarn: true, scenarios: [ + { + name: 'ember-lts-2.12', + env: { + EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), + }, + npm: { + devDependencies: { + '@ember/jquery': '^0.5.1', + 'ember-source': '~2.12.0', + }, + }, + }, + { + name: 'ember-lts-2.16', + env: { + EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), + }, + npm: { + devDependencies: { + '@ember/jquery': '^0.5.1', + 'ember-source': '~2.16.0', + }, + }, + }, { name: 'ember-lts-2.18', env: { @@ -31,6 +55,30 @@ module.exports = function() { }, }, }, + { + name: 'ember-3.5', + npm: { + devDependencies: { + 'ember-source': '~3.5.0', + }, + }, + }, + { + name: 'ember-3.6', + npm: { + devDependencies: { + 'ember-source': '~3.6.0', + }, + }, + }, + { + name: 'ember-3.7', + npm: { + devDependencies: { + 'ember-source': '~3.7.0', + }, + }, + }, { name: 'ember-release', npm: { diff --git a/index.js b/index.js index 0ca063d..77a51e2 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,44 @@ 'use strict'; +const VersionChecker = require('ember-cli-version-checker'); + module.exports = { 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.8.0-alpha.1'); + }, + + included() { + this._super.included.apply(this, arguments); + + if (!this.shouldPolyfill) { + return; + } + + this.import('vendor/ember-modifier-manager-polyfill.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 f0b1c7a..399c4c9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "test:all": "ember try:each" }, "dependencies": { - "ember-cli-babel": "^7.4.0" + "ember-cli-babel": "^7.4.0", + "ember-cli-version-checker": "^2.1.2", + "ember-compatibility-helpers": "^1.2.0-beta.1" }, "devDependencies": { "@ember/optional-features": "^0.7.0", diff --git a/tests/integration/components/modifier-manager-test.js b/tests/integration/components/modifier-manager-test.js new file mode 100644 index 0000000..f84fc85 --- /dev/null +++ b/tests/integration/components/modifier-manager-test.js @@ -0,0 +1,202 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import Ember from 'ember'; +import { assign } from '@ember/polyfills'; + +module('Integration | Component | modifier-manager', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function(assert) { + assert.namedEquals = function(actual, expected, message) { + // this is needed because older versions of Ember pass an `EmptyObject` + // based object and QUnit fails due to the prototypes not matching + let sanitizedActual = assign({}, actual); + + assert.deepEqual(sanitizedActual, expected, message); + }; + }); + + module('installModifier', function(hooks) { + hooks.beforeEach(function() { + class DidInsertModifier {} + + Ember._setModifierManager( + () => ({ + createModifier(_factory, args) { + return args.positional[0]; + }, + + installModifier(_state, element, args) { + let [fn, ...positional] = args.positional; + + fn(element, positional, args.named); + }, + + updateModifier() {}, + destroyModifier() {}, + }), + DidInsertModifier + ); + this.owner.register('modifier:did-insert', DidInsertModifier); + }); + + test('it basically works', async function(assert) { + assert.expect(2); + + this.someMethod = element => { + assert.equal(element.tagName, 'DIV', 'correct element tagName'); + assert.dom(element).hasAttribute('data-foo', 'some-thing'); + }; + await render(hbs`
`); + }); + + test('it can accept arguments', async function(assert) { + assert.expect(4); + + this.someMethod = (element, positional, named) => { + assert.equal(element.tagName, 'DIV', 'correct element tagName'); + assert.dom(element).hasAttribute('data-foo', 'some-thing'); + + assert.namedEquals(named, { some: 'hash-value' }, 'named args match'); + assert.deepEqual(positional, ['some-positional-value'], 'positional args match'); + }; + + await render( + hbs`
` + ); + }); + + test('it is not invoked again when arguments change', async function(assert) { + assert.expect(4); + + this.someMethod = (element, positional, named) => { + assert.equal(element.tagName, 'DIV', 'correct element tagName'); + assert.dom(element).hasAttribute('data-foo', 'some-thing'); + + assert.namedEquals(named, {}, 'named args match'); + assert.deepEqual(positional, ['initial'], 'positional args match'); + }; + + this.set('firstArg', 'initial'); + await render( + hbs`
` + ); + this.set('firstArg', 'updated'); + }); + }); + + module('updateModifier', function(hooks) { + hooks.beforeEach(function() { + class DidUpdateModifier {} + + Ember._setModifierManager( + () => ({ + createModifier() { + return {}; + }, + installModifier(state, element) { + state.element = element; + }, + + updateModifier({ element }, args) { + let [fn, ...positional] = args.positional; + + fn(element, positional, args.named); + }, + + destroyModifier() {}, + }), + DidUpdateModifier + ); + this.owner.register('modifier:did-update', DidUpdateModifier); + }); + + test('it basically works', async function(assert) { + assert.expect(4); + + this.someMethod = (element, positional, named) => { + assert.equal(element.tagName, 'DIV', 'correct element tagName'); + assert.dom(element).hasAttribute('data-foo', 'some-thing'); + + assert.namedEquals(named, {}, 'named args match'); + assert.deepEqual(positional, ['update'], 'positional args match'); + }; + + this.set('boundValue', 'initial'); + await render( + hbs`
` + ); + + this.set('boundValue', 'update'); + }); + }); + + module('destroyModifier', function(hooks) { + hooks.beforeEach(function() { + class WillDestroyModifier {} + + Ember._setModifierManager( + () => ({ + createModifier() { + return {}; + }, + + installModifier(state, element) { + state.element = element; + }, + + updateModifier() {}, + + destroyModifier({ element }, args) { + let [fn, ...positional] = args.positional; + + fn(element, positional, args.named); + }, + }), + WillDestroyModifier + ); + + this.owner.register('modifier:will-destroy', WillDestroyModifier); + }); + + test('it basically works', async function(assert) { + assert.expect(2); + + this.someMethod = element => { + assert.equal(element.tagName, 'DIV', 'correct element tagName'); + assert.dom(element).hasAttribute('data-foo', 'some-thing'); + }; + this.set('show', true); + + await render( + hbs`{{#if show}}
{{/if}}` + ); + + // trigger destroy + this.set('show', false); + }); + + test('it can accept arguments', async function(assert) { + assert.expect(4); + + this.someMethod = (element, positional, named) => { + assert.equal(element.tagName, 'DIV', 'correct element tagName'); + assert.dom(element).hasAttribute('data-foo', 'some-thing'); + + assert.namedEquals(named, { some: 'hash-value' }, 'named args match'); + assert.deepEqual(positional, ['some-positional-value'], 'positional args match'); + }; + + this.set('show', true); + + await render( + hbs`{{#if show}}
{{/if}}` + ); + + // trigger destroy + this.set('show', false); + }); + }); +}); diff --git a/vendor/.gitkeep b/vendor/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/vendor/ember-modifier-manager-polyfill.js b/vendor/ember-modifier-manager-polyfill.js new file mode 100644 index 0000000..d058a32 --- /dev/null +++ b/vendor/ember-modifier-manager-polyfill.js @@ -0,0 +1,228 @@ +/* globals Ember */ +/* eslint-disable ember/new-module-imports */ + +import { lte, gte } from 'ember-compatibility-helpers'; + +(() => { + 'use strict'; + + const getPrototypeOf = Object.getPrototypeOf; + const { Application } = Ember; + let MODIFIER_MANAGERS = new WeakMap(); + Ember._setModifierManager = function Polyfilled_setModifierManager(managerFactory, modifier) { + MODIFIER_MANAGERS.set(modifier, managerFactory); + }; + + let getModifierManager = obj => { + let pointer = obj; + while (pointer !== undefined && pointer !== null) { + if (MODIFIER_MANAGERS.has(pointer)) { + return MODIFIER_MANAGERS.get(pointer); + } + + pointer = getPrototypeOf(pointer); + } + + return; + }; + + let valueForCapturedArgs = function valueForCapturedArgs(args) { + return { + named: args.named.value(), + positional: args.positional.value(), + }; + }; + + Application.reopenClass({ + buildRegistry() { + let registry = this._super(...arguments); + + class CustomModifierState { + constructor(element, delegate, modifier, args) { + this.element = element; + this.delegate = delegate; + this.modifier = modifier; + this.args = args; + } + + destroy() { + const { delegate, modifier, args } = this; + let modifierArgs = valueForCapturedArgs(args); + delegate.destroyModifier(modifier, modifierArgs); + } + } + + class Polyfilled_CustomModifierManager { + //create(element: Simple.Element, state: ModifierDefinitionState, args: IArguments, dynamicScope: DynamicScope, dom: DOMChanges): ModifierInstanceState; + create(element, definition, args) { + let capturedArgs = gte('2.15.0-alpha.1') ? args.capture() : args; + let modifierArgs = valueForCapturedArgs(capturedArgs); + + let instance = definition.delegate.createModifier(definition.ModifierClass, modifierArgs); + + return new CustomModifierState(element, definition.delegate, instance, capturedArgs); + } + + //getTag(modifier: ModifierInstanceState): Tag; + getTag({ args }) { + return args.tag; + } + + //install(modifier: ModifierInstanceState): void; + install(state) { + let { element, args, delegate, modifier } = state; + let modifierArgs = valueForCapturedArgs(args); + delegate.installModifier(modifier, element, modifierArgs); + } + + //update(modifier: ModifierInstanceState): void; + update(state) { + let { args, delegate, modifier } = state; + let modifierArgs = valueForCapturedArgs(args); + delegate.updateModifier(modifier, modifierArgs); + } + + //getDestructor(modifier: ModifierInstanceState): Option; + getDestructor(state) { + return state; + } + } + + let Polyfilled_CustomModifierManagerLt36; + if (lte('3.6.0-alpha.1')) { + Polyfilled_CustomModifierManagerLt36 = class Polyfilled_CustomModifierManagerLt36 extends Polyfilled_CustomModifierManager { + constructor(name, ModifierClass, manager) { + super(); + + this.state = { + ModifierClass, + delegate: manager, + }; + } + + // create(element: Simple.Element, args: Arguments, _dynamicScope: DynamicScope, dom: any) { + create(element, args) { + return super.create(element, this.state, args); + } + }; + } + + class Polyfilled_CustomModifierDefinition { + constructor(name, ModifierClass, delegate) { + this.name = name; + this.state = { + ModifierClass, + name, + delegate, + }; + this.manager = new Polyfilled_CustomModifierManager(); + } + } + + if (gte('3.1.0-beta.1')) { + let containerModule = gte('3.6.0-alpha.1') ? '@ember/-internals/container' : 'container'; + const P = Ember.__loader.require(containerModule).privatize; + + let compilerName = gte('3.2.0-alpha.1') + ? P`template-compiler:main` + : P`template-options:main`; + let TemplateCompiler = registry.resolve(compilerName); + + let ORIGINAL_TEMPLATE_COMPILER_CREATE = TemplateCompiler.create; + if (ORIGINAL_TEMPLATE_COMPILER_CREATE.__MODIFIER_MANAGER_PATCHED === true) { + return registry; + } + + TemplateCompiler.create = function() { + let compiler = ORIGINAL_TEMPLATE_COMPILER_CREATE(...arguments); + let compileTimeLookup = compiler.resolver; + let runtimeResolver = compileTimeLookup.resolver; + + // meta was not passed to `_lookupModifier` until 3.7 + if (lte('3.7.0-alpha.1')) { + runtimeResolver.lookupModifier = function(name, meta) { + return this.handle(this._lookupModifier(name, meta)); + }; + } + + runtimeResolver._lookupModifier = function(name, meta) { + let builtin = this.builtInModifiers[name]; + + if (builtin === undefined) { + let { owner } = meta; + let modifier = owner.factoryFor(`modifier:${name}`); + if (modifier !== undefined) { + let managerFactory = getModifierManager(modifier.class); + let manager = managerFactory(owner); + + if (gte('3.6.0-alpha.1')) { + return new Polyfilled_CustomModifierDefinition(name, modifier.class, manager); + } else { + return new Polyfilled_CustomModifierManagerLt36(name, modifier.class, manager); + } + } + } + + return builtin; + }; + + return compiler; + }; + TemplateCompiler.create.__MODIFIER_MANAGER_PATCHED = true; + } else if (gte('2.12.0-beta.1')) { + let Environment = registry.resolve('service:-glimmer-environment'); + let ORIGINAL_ENVIRONMENT_CREATE = Environment.create; + if (ORIGINAL_ENVIRONMENT_CREATE.__MODIFIER_MANAGER_PATCHED === true) { + return registry; + } + + let factoryForMethodName = 'factoryFor'; + if (lte('2.13.99999999')) { + factoryForMethodName = Ember.__loader.require('container').FACTORY_FOR; + } + + Environment.create = function Polyfilled_EnvironmentCreate() { + let environment = ORIGINAL_ENVIRONMENT_CREATE.apply(this, arguments); + + environment.hasModifier = function(name, metaOrSymbolTable) { + let owner = gte('2.15.0-alpha.1') + ? metaOrSymbolTable.owner + : metaOrSymbolTable.getMeta().owner; + + return !!this.builtInModifiers[name] || !!owner.hasRegistration(`modifier:${name}`); + }; + + environment.lookupModifier = function(name, metaOrSymbolTable) { + let modifier = this.builtInModifiers[name]; + + if (!modifier) { + let owner = gte('2.15.0-alpha.1') + ? metaOrSymbolTable.owner + : metaOrSymbolTable.getMeta().owner; + let modifier = owner[factoryForMethodName](`modifier:${name}`); + if (modifier !== undefined) { + let managerFactory = getModifierManager(modifier.class); + let manager = managerFactory(owner); + + return new Polyfilled_CustomModifierManagerLt36(name, modifier.class, manager); + } + } + + if (!modifier) { + throw new Error(`${name} is not a modifier`); + } + + return modifier; + }; + + return environment; + }; + Environment.create.__MODIFIER_MANAGER_PATCHED = true; + + return registry; + } + + return registry; + }, + }); +})(); diff --git a/yarn.lock b/yarn.lock index e2ecd12..2b97377 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1153,7 +1153,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== @@ -2953,7 +2953,7 @@ ember-cli-test-loader@^2.2.0: dependencies: ember-cli-babel "^6.8.1" -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.1.2" resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.1.2.tgz#305ce102390c66e4e0f1432dea9dc5c7c19fed98" integrity sha512-sjkHGr4IGXnO3EUcY21380Xo9Qf6bC8HWH4D62bVnrQop/8uha5XgMQRoAflMCeH6suMrezQL287JUoYc2smEw== @@ -3057,6 +3057,15 @@ ember-cli@~3.7.1: watch-detector "^0.1.0" yam "^1.0.0" +ember-compatibility-helpers@^1.2.0-beta.1: + version "1.2.0-beta.1" + resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.0-beta.1.tgz#cd5898734f398f977707cbf8d448689377449180" + integrity sha512-ewKxBP0DIcbuMyGR8P+ned3H6vgCI5+2t+5MOwcaIFxRYUx6Bm0Dqk6Y8UWzwVgkxlZspC34k8QE/QLlSI7oiA== + 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"