From e80fa137db6957d49e9f50586b5337502804516a Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 20 May 2020 14:55:41 +0100 Subject: [PATCH] Updated members routes/controllers with Octane idioms no issue - ran [`ember-native-class-codemod`](https://github.com/ember-codemods/ember-native-class-codemod) on members-related files - updated files to remove need for `@classic` decorator where possible - switched to tracked properties - removed usage of `this.get/set/toggleProperty` etc - swapped usage of `{{action 'foo'}}` for `{{this.foo}}` --- .eslintrc.js | 4 + app/components/gh-token-input.js | 2 + app/components/gh-token-input/trigger.js | 2 +- app/controllers/member.js | 164 ++++++++++--------- app/controllers/members.js | 191 ++++++++++++----------- app/controllers/members/import.js | 26 +-- app/routes/member.js | 51 +++--- app/routes/member/new.js | 8 +- app/routes/members.js | 31 ++-- app/routes/members/import.js | 3 +- app/templates/member.hbs | 18 +-- app/templates/members.hbs | 12 +- package.json | 1 + yarn.lock | 18 ++- 14 files changed, 289 insertions(+), 242 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 49216e921..1fdba8e0a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,10 @@ module.exports = { 'plugin:ghost/ember' ], rules: { + // octane 🏎 + 'ghost/ember/classic-decorator-hooks': 'error', + 'ghost/ember/classic-decorator-no-classic-methods': 'error', + // disable linting of `this.get` until there's a reliable autofix 'ghost/ember/use-ember-get-and-set': 'off', diff --git a/app/components/gh-token-input.js b/app/components/gh-token-input.js index f2b77d9ac..f0df70aa6 100644 --- a/app/components/gh-token-input.js +++ b/app/components/gh-token-input.js @@ -1,6 +1,7 @@ /* global key */ import Component from '@ember/component'; import Ember from 'ember'; +import classic from 'ember-classic-decorator'; import fallbackIfUndefined from '../utils/computed-fallback-if-undefined'; import {A, isArray} from '@ember/array'; import {action, computed, get} from '@ember/object'; @@ -19,6 +20,7 @@ const {Handlebars} = Ember; const BACKSPACE = 8; const TAB = 9; +@classic @tagName('') class GhTokenInput extends Component { // public attrs diff --git a/app/components/gh-token-input/trigger.js b/app/components/gh-token-input/trigger.js index 35e6995e8..d8552733e 100644 --- a/app/components/gh-token-input/trigger.js +++ b/app/components/gh-token-input/trigger.js @@ -50,7 +50,7 @@ export default class Trigger extends EmberPowerSelectMultipleTrigger { if (typeof lastSelection === 'string') { this.args.select.actions.search(lastSelection); } else { - let searchField = this.get('searchField'); + let searchField = this.searchField; assert('`{{power-select-multiple}}` requires a `searchField` when the options are not strings to remove options using backspace', searchField); this.args.select.actions.search(get(lastSelection, searchField)); } diff --git a/app/controllers/member.js b/app/controllers/member.js index 146a10167..04a232e0b 100644 --- a/app/controllers/member.js +++ b/app/controllers/member.js @@ -1,95 +1,111 @@ -import Controller from '@ember/controller'; -import EmberObject from '@ember/object'; +import Controller, {inject as controller} from '@ember/controller'; +import EmberObject, {action, defineProperty} from '@ember/object'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import moment from 'moment'; import {alias} from '@ember/object/computed'; -import {computed, defineProperty} from '@ember/object'; -import {inject as controller} from '@ember/controller'; import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; +import {task} from 'ember-concurrency-decorators'; +import {tracked} from '@glimmer/tracking'; const SCRATCH_PROPS = ['name', 'email', 'note']; -export default Controller.extend({ - members: controller(), - session: service(), - dropdown: service(), - notifications: service(), - router: service(), - store: service(), +export default class MemberController extends Controller { + @controller members; + @service session; + @service dropdown; + @service notifications; + @service router; + @service store; - showImpersonateMemberModal: false, + @tracked isLoading = false; + @tracked showDeleteMemberModal = false; + @tracked showImpersonateMemberModal = false; + @tracked showUnsavedChangesModal = false; - member: alias('model'), + leaveScreenTransition = null; - scratchMember: computed('member', function () { + // Computed properties ----------------------------------------------------- + + @alias('model') member; + + get scratchMember() { let scratchMember = EmberObject.create({member: this.member}); SCRATCH_PROPS.forEach(prop => defineProperty(scratchMember, prop, boundOneWay(`member.${prop}`))); return scratchMember; - }), + } - subscribedAt: computed('member.createdAtUTC', function () { + get subscribedAt() { let memberSince = moment(this.member.createdAtUTC).from(moment()); let createdDate = moment(this.member.createdAtUTC).format('MMM DD, YYYY'); return `${createdDate} (${memberSince})`; - }), + } - actions: { - setProperty(propKey, value) { - this._saveMemberProperty(propKey, value); - }, + // Actions ----------------------------------------------------------------- - toggleDeleteMemberModal() { - this.toggleProperty('showDeleteMemberModal'); - }, + @action + setProperty(propKey, value) { + this._saveMemberProperty(propKey, value); + } - toggleImpersonateMemberModal() { - this.toggleProperty('showImpersonateMemberModal'); - }, + @action + toggleDeleteMemberModal() { + this.showDeleteMemberModal = !this.showDeleteMemberModal; + } - save() { - return this.save.perform(); - }, + @action + toggleImpersonateMemberModal() { + this.showImpersonateMemberModal = !this.showImpersonateMemberModal; + } - deleteMember() { - return this.member.destroyRecord().then(() => { - return this.transitionToRoute('members'); - }, (error) => { - return this.notifications.showAPIError(error, {key: 'member.delete'}); - }); - }, + @action + save() { + return this.saveTask.perform(); + } - toggleUnsavedChangesModal(transition) { - let leaveTransition = this.leaveScreenTransition; + @action + deleteMember() { + return this.member.destroyRecord().then(() => { + return this.transitionToRoute('members'); + }, (error) => { + return this.notifications.showAPIError(error, {key: 'member.delete'}); + }); + } - if (!transition && this.showUnsavedChangesModal) { - this.set('leaveScreenTransition', null); - this.set('showUnsavedChangesModal', false); - return; - } + @action + toggleUnsavedChangesModal(transition) { + let leaveTransition = this.leaveScreenTransition; - if (!leaveTransition || transition.targetName === leaveTransition.targetName) { - this.set('leaveScreenTransition', transition); - - // if a save is running, wait for it to finish then transition - if (this.save.isRunning) { - return this.save.last.then(() => { - transition.retry(); - }); - } - - // we genuinely have unsaved data, show the modal - this.set('showUnsavedChangesModal', true); - } - }, - - leaveScreen() { - this.member.rollbackAttributes(); - return this.leaveScreenTransition.retry(); + if (!transition && this.showUnsavedChangesModal) { + this.leaveScreenTransition = null; + this.showUnsavedChangesModal = false; + return; } - }, - save: task(function* () { + if (!leaveTransition || transition.targetName === leaveTransition.targetName) { + this.leaveScreenTransition = transition; + + // if a save is running, wait for it to finish then transition + if (this.save.isRunning) { + return this.save.last.then(() => { + transition.retry(); + }); + } + + // we genuinely have unsaved data, show the modal + this.showUnsavedChangesModal = true; + } + } + + @action + leaveScreen() { + this.member.rollbackAttributes(); + return this.leaveScreenTransition.retry(); + } + + // Tasks ------------------------------------------------------------------- + + @task({drop: true}) + *saveTask() { let {member, scratchMember} = this; // if Cmd+S is pressed before the field loses focus make sure we're @@ -109,18 +125,20 @@ export default Controller.extend({ this.notifications.showAPIError(error, {key: 'member.save'}); } } - }).drop(), + } - fetchMember: task(function* (memberId) { - this.set('isLoading', true); + @task + *fetchMemberTask(memberId) { + this.isLoading = true; - let member = yield this.store.findRecord('member', memberId, { + this.member = yield this.store.findRecord('member', memberId, { reload: true }); - this.set('member', member); - this.set('isLoading', false); - }), + this.isLoading = false; + } + + // Private ----------------------------------------------------------------- _saveMemberProperty(propKey, newValue) { let currentValue = this.member.get(propKey); @@ -136,4 +154,4 @@ export default Controller.extend({ this.member.set(propKey, newValue); } -}); +} diff --git a/app/controllers/members.js b/app/controllers/members.js index ad0c52d03..ad34d83d4 100644 --- a/app/controllers/members.js +++ b/app/controllers/members.js @@ -1,43 +1,46 @@ import Controller from '@ember/controller'; import ghostPaths from 'ghost-admin/utils/ghost-paths'; import moment from 'moment'; -import {computed} from '@ember/object'; -import {get} from '@ember/object'; +import {A} from '@ember/array'; +import {action} from '@ember/object'; import {pluralize} from 'ember-inflector'; import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; +import {task} from 'ember-concurrency-decorators'; +import {tracked} from '@glimmer/tracking'; -/* eslint-disable ghost/ember/alias-model-in-controller */ -export default Controller.extend({ - store: service(), +export default class MembersController extends Controller { + @service store; - queryParams: ['label'], + queryParams = ['label']; - label: null, - members: null, - searchText: '', - modalLabel: null, - showLabelModal: false, + @tracked searchText = ''; + @tracked label = null; + @tracked members = null; + @tracked modalLabel = null; + @tracked showLabelModal = false; - _hasLoadedLabels: false, - _availableLabels: null, + @tracked _availableLabels = A([]); - init() { - this._super(...arguments); - this.set('members', this.store.peekAll('member')); + hasLoadedLabels = false; + + constructor() { + super(...arguments); + this.members = this.store.peekAll('member'); this._availableLabels = this.store.peekAll('label'); - }, + } - showLoader: computed('filteredMembers.length', 'fetchMembers.isRunning', function () { - return (!this.get('filteredMembers.length') && this.get('fetchMembers.isRunning')); - }), + // Computed properties ----------------------------------------------------- - listHeader: computed('selectedLabel', 'searchText', function () { + get showLoader() { + return (!this.filteredMembers.length && this.fetchMembersTask.isRunning); + } + + get listHeader() { let {searchText, selectedLabel, filteredMembers} = this; if (searchText) { return 'Search result'; } - if (this.fetchMembers.lastSuccessful) { + if (this.fetchMembersTask.lastSuccessful) { let count = pluralize(filteredMembers.length, 'member'); if (selectedLabel && selectedLabel.slug) { if (filteredMembers.length > 1) { @@ -49,44 +52,40 @@ export default Controller.extend({ return count; } return 'Loading...'; - }), + } - showingAll: computed('label', 'searchText', function () { - let {searchText, label} = this; + get showingAll() { + return !this.searchText && !this.label; + } - return !searchText && !label; - }), - - availableLabels: computed('_availableLabels.@each.isNew', function () { + get availableLabels() { let labels = this._availableLabels - .filter(label => !label.get('isNew')) - .filter(label => label.get('id') !== null) + .filter(label => !label.isNew) + .filter(label => label.id !== null) .sort((labelA, labelB) => labelA.name.localeCompare(labelB.name, undefined, {ignorePunctuation: true})); let options = labels.toArray(); options.unshiftObject({name: 'All labels', slug: null}); return options; - }), + } - selectedLabel: computed('label', 'availableLabels', function () { - let label = this.get('label'); - let labels = this.get('availableLabels'); + get selectedLabel() { + let {label, availableLabels} = this; + return availableLabels.findBy('slug', label); + } - return labels.findBy('slug', label); - }), - - labelModalData: computed('modalLabel', 'availableLabels', function () { - let label = this.get('modalLabel'); - let labels = this.get('availableLabels'); + get labelModalData() { + let label = this.modalLabel; + let labels = this.availableLabels; return { label, labels }; - }), + } - filteredMembers: computed('members.@each.{name,email}', 'searchText', 'label', function () { + get filteredMembers() { let {members, searchText, label} = this; searchText = searchText.toLowerCase(); @@ -106,59 +105,69 @@ export default Controller.extend({ return _label.slug === label; }); }).sort((a, b) => { - return b.get('createdAtUTC').valueOf() - a.get('createdAtUTC').valueOf(); + return b.createdAtUTC.valueOf() - a.createdAtUTC.valueOf(); }); return filtered; - }), + } - actions: { - exportData() { - let exportUrl = ghostPaths().url.api('members/csv'); - let downloadURL = `${exportUrl}?limit=all`; - let iframe = document.getElementById('iframeDownload'); + // Actions ----------------------------------------------------------------- - if (!iframe) { - iframe = document.createElement('iframe'); - iframe.id = 'iframeDownload'; - iframe.style.display = 'none'; - document.body.append(iframe); - } - iframe.setAttribute('src', downloadURL); - }, - changeLabel(label, e) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - this.set('label', get(label, 'slug')); - }, - addLabel(e) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - const newLabel = this.store.createRecord('label'); - this.set('modalLabel', newLabel); - this.toggleProperty('showLabelModal'); - }, - editLabel(label, e) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - let labels = this.get('availableLabels'); + @action + exportData() { + let exportUrl = ghostPaths().url.api('members/csv'); + let downloadURL = `${exportUrl}?limit=all`; + let iframe = document.getElementById('iframeDownload'); - let modalLabel = labels.findBy('slug', label); - this.set('modalLabel', modalLabel); - this.toggleProperty('showLabelModal'); - }, - toggleLabelModal() { - this.toggleProperty('showLabelModal'); + if (!iframe) { + iframe = document.createElement('iframe'); + iframe.id = 'iframeDownload'; + iframe.style.display = 'none'; + document.body.append(iframe); } - }, + iframe.setAttribute('src', downloadURL); + } - fetchMembers: task(function* () { + @action + changeLabel(label, e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + this.label = label.slug; + } + + @action + addLabel(e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + const newLabel = this.store.createRecord('label'); + this.modalLabel = newLabel; + this.showLabelModal = !this.showLabelModal; + } + + @action + editLabel(label, e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + let modalLabel = this.availableLabels.findBy('slug', label); + this.modalLabel = modalLabel; + this.showLabelModal = !this.showLabelModal; + } + + @action + toggleLabelModal() { + this.showLabelModal = !this.showLabelModal; + } + + // Tasks ------------------------------------------------------------------- + + @task + *fetchMembersTask() { let newFetchDate = new Date(); if (this._hasFetchedAll) { @@ -178,5 +187,5 @@ export default Controller.extend({ } this._lastFetchDate = newFetchDate; - }) -}); + } +} diff --git a/app/controllers/members/import.js b/app/controllers/members/import.js index cf070a79b..2a7e1378a 100644 --- a/app/controllers/members/import.js +++ b/app/controllers/members/import.js @@ -1,19 +1,19 @@ import Controller from '@ember/controller'; +import {action} from '@ember/object'; import {inject as controller} from '@ember/controller'; import {inject as service} from '@ember/service'; -/* eslint-disable ghost/ember/alias-model-in-controller */ -export default Controller.extend({ - members: controller(), - router: service(), +export default class ImportController extends Controller { + @controller members; + @service router; - actions: { - fetchNewMembers() { - this.members.fetchMembers.perform(); - }, - - close() { - this.router.transitionTo('members'); - } + @action + fetchNewMembers() { + this.members.fetchMembersTask.perform(); } -}); + + @action + close() { + this.router.transitionTo('members'); + } +} diff --git a/app/routes/member.js b/app/routes/member.js index f3f14e9df..606d4e27b 100644 --- a/app/routes/member.js +++ b/app/routes/member.js @@ -1,24 +1,26 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings'; +import classic from 'ember-classic-decorator'; +import {action} from '@ember/object'; import {inject as service} from '@ember/service'; -export default AuthenticatedRoute.extend(CurrentUserSettings, { - router: service(), +@classic +export default class MembersRoute extends AuthenticatedRoute.extend(CurrentUserSettings) { + @service router; - _requiresBackgroundRefresh: true, + _requiresBackgroundRefresh = true; init() { - this._super(...arguments); + super.init(...arguments); this.router.on('routeWillChange', (transition) => { this.showUnsavedChangesModal(transition); }); - }, + } beforeModel() { - this._super(...arguments); - return this.get('session.user') - .then(this.transitionAuthor()); - }, + super.beforeModel(...arguments); + return this.session.user.then(this.transitionAuthor()); + } model(params) { this._requiresBackgroundRefresh = false; @@ -28,33 +30,30 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, { } else { return this.store.createRecord('member'); } - }, + } setupController(controller, member) { - this._super(...arguments); + super.setupController(...arguments); if (this._requiresBackgroundRefresh) { - controller.fetchMember.perform(member.get('id')); + controller.fetchMemberTask.perform(member.get('id')); } - }, + } deactivate() { - this._super(...arguments); - + super.deactivate(...arguments); // clean up newly created records and revert unsaved changes to existing this.controller.member.rollbackAttributes(); - this._requiresBackgroundRefresh = true; - }, + } - actions: { - save() { - this.controller.send('save'); - } - }, + @action + save() { + this.controller.save(); + } titleToken() { - return this.controller.get('member.name'); - }, + return this.controller.member.name; + } showUnsavedChangesModal(transition) { if (transition.from && transition.from.name === this.routeName && transition.targetName) { @@ -65,9 +64,9 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, { if (!controller.member.isDeleted && isChanged) { transition.abort(); - controller.send('toggleUnsavedChangesModal', transition); + controller.toggleUnsavedChangesModal(transition); return; } } } -}); +} diff --git a/app/routes/member/new.js b/app/routes/member/new.js index a7bfc5325..30c27fc4a 100644 --- a/app/routes/member/new.js +++ b/app/routes/member/new.js @@ -1,6 +1,6 @@ import MemberRoute from '../member'; -export default MemberRoute.extend({ - controllerName: 'member', - templateName: 'member' -}); +export default class NewMemberRoute extends MemberRoute { + controllerName = 'member'; + templateName = 'member'; +} diff --git a/app/routes/members.js b/app/routes/members.js index cff6e6217..cf0e53a78 100644 --- a/app/routes/members.js +++ b/app/routes/members.js @@ -1,45 +1,44 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; import {inject as service} from '@ember/service'; -export default AuthenticatedRoute.extend({ - config: service(), +export default class MembersRoute extends AuthenticatedRoute { + @service config; - queryParams: { + queryParams = { label: {refreshModel: true} - }, + }; // redirect to posts screen if: // - TODO: members is disabled? // - logged in user isn't owner/admin beforeModel() { - this._super(...arguments); - + super.beforeModel(...arguments); return this.session.user.then((user) => { if (!user.isOwnerOrAdmin) { return this.transitionTo('home'); } }); - }, + } // trigger a background load of labels for filter dropdown setupController(controller) { - this._super(...arguments); - controller.fetchMembers.perform(); - if (!controller._hasLoadedLabels) { + super.setupController(...arguments); + controller.fetchMembersTask.perform(); + if (!controller.hasLoadedLabels) { this.store.query('label', {limit: 'all'}).then(() => { - controller._hasLoadedLabels = true; + controller.hasLoadedLabels = true; }); } - }, + } deactivate() { - this._super(...arguments); + super.deactivate(...arguments); this.controller.modalLabel && this.controller.modalLabel.rollbackAttributes(); - }, + } + buildRouteInfoMetadata() { return { titleToken: 'Members' }; } - -}); +} diff --git a/app/routes/members/import.js b/app/routes/members/import.js index 6c74252aa..4d80cccee 100644 --- a/app/routes/members/import.js +++ b/app/routes/members/import.js @@ -1,4 +1,3 @@ import Route from '@ember/routing/route'; -export default Route.extend({ -}); +export default class MembersImportRoute extends Route {} diff --git a/app/templates/member.hbs b/app/templates/member.hbs index aad9876a3..0dc2e6997 100644 --- a/app/templates/member.hbs +++ b/app/templates/member.hbs @@ -15,13 +15,13 @@ {{#unless this.member.isNew}} {{/unless}} {{/if}} - + @@ -67,7 +67,7 @@ @@ -75,7 +75,7 @@ @@ -93,7 +93,7 @@ {{/if}} \ No newline at end of file diff --git a/package.json b/package.json index eee0fc2e9..6db6199a8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "ember-ajax": "5.0.0", "ember-assign-helper": "0.2.0", "ember-auto-import": "1.5.3", + "ember-classic-decorator": "1.0.8", "ember-cli": "3.18.0", "ember-cli-app-version": "3.2.0", "ember-cli-babel": "7.20.0", diff --git a/yarn.lock b/yarn.lock index a9a3aae7c..cc6dff5b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -837,7 +837,7 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.1.6", "@babel/types@^7.3.2", "@babel/types@^7.3.4", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.7.2", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5", "@babel/types@^7.9.6": +"@babel/types@^7.1.6", "@babel/types@^7.3.2", "@babel/types@^7.3.4", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.7.2", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5", "@babel/types@^7.9.6": version "7.9.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== @@ -2269,6 +2269,14 @@ babel-plugin-ember-modules-api-polyfill@^2.13.0: dependencies: ember-rfc176-data "^0.3.13" +babel-plugin-filter-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-filter-imports/-/babel-plugin-filter-imports-3.0.0.tgz#a849683837ad29960da17492fb32789ab6b09a11" + integrity sha512-p/chjzVTgCxUqyLM0q/pfWVZS7IJTwGQMwNg0LOvuQpKiTftQgZDtkGB8XvETnUw19rRcL7bJCTopSwibTN2tA== + dependencies: + "@babel/types" "^7.4.0" + lodash "^4.17.11" + babel-plugin-filter-imports@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/babel-plugin-filter-imports/-/babel-plugin-filter-imports-4.0.0.tgz#068f8da15236a96a9602c36dc6f4a6eeca70a4f4" @@ -5195,6 +5203,14 @@ ember-basic-dropdown@^3.0.1: ember-maybe-in-element "^0.4.0" ember-truth-helpers "2.1.0" +ember-classic-decorator@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/ember-classic-decorator/-/ember-classic-decorator-1.0.8.tgz#e290e5b0b1a31a569587a85a9c5c7a2f1242cabb" + integrity sha512-IsCDJ7rLsrFjYtgi9UXUmjzUQJaaJzmy/gKwGGtZ6kZwT5yhzSbScRi0P6Cb0guJPtlMMCE0sAQpJRbXmBb/gA== + dependencies: + babel-plugin-filter-imports "^3.0.0" + ember-cli-babel "^7.11.1" + ember-cli-app-version@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ember-cli-app-version/-/ember-cli-app-version-3.2.0.tgz#7b9ad0e1b63ae0518648356ee24c703e922bc26e"