diff --git a/guides/rules/require-ember-lifeline.md b/guides/rules/require-ember-lifeline.md new file mode 100644 index 0000000..3c87acf --- /dev/null +++ b/guides/rules/require-ember-lifeline.md @@ -0,0 +1,9 @@ +# Require Ember Lifeline + +[Ember lifeline](https://github.com/rwjblue/ember-lifeline) + +Ember applications have long life-cycles. A user may navigate to several pages and use many different features before they leave the application. This makes JavaScript and Ember development unlike Rails development, where the lifecycle of a request is short and the environment disposed of after each request. It makes Ember development much more like iOS or video game development than traditional server-side web development. + +It is good to note that this isn't something inherent to just Ember. Any single-page app framework or solution (Angular, React, Vue, Backbone...) must deal with the lifecycles of objects, and specifically with how async tasks can be bounded by a lifecycle. + +Ember lifeline introduces several utility methods to help manage async, object lifecycles, and the Ember runloop. Please review the documentation to understand the APIs that can be used to replace the equivalent `Ember.run` method. \ No newline at end of file diff --git a/lib/rules/require-ember-lifeline.js b/lib/rules/require-ember-lifeline.js new file mode 100644 index 0000000..4a743b1 --- /dev/null +++ b/lib/rules/require-ember-lifeline.js @@ -0,0 +1,70 @@ +/** + * @fileOverview Require the use of ember-lifeline's lifecycle aware methods over Ember.run.*. + */ + +const { getCaller, cleanCaller } = require('../utils/caller'); +const { getEmberImportBinding } = require('../utils/imports'); +const { collectObjectPatternBindings } = require('../utils/destructed-binding'); + +let DISALLOWED_OBJECTS = ['Ember.run', 'run']; +let RUN_METHODS = ['later', 'next', 'debounce', 'throttle']; +const LIFELINE_METHODS = ['runTask', 'runTask', 'debounceTask', 'throttleTask']; + +function getMessage(actualMethodUsed) { + let method = actualMethodUsed.split('.').pop(); + let lifelineEquivalent = LIFELINE_METHODS[RUN_METHODS.indexOf(method)]; + + return `Please use ${lifelineEquivalent} from ember-lifeline instead of ${actualMethodUsed}.`; +} + +function mergeDisallowedCalls(objects) { + return objects + .reduce((calls, obj) => { + RUN_METHODS.forEach((method) => { + calls.push(`${obj}.${method}`); + }); + + return calls; + }, []); +} + +module.exports = { + docs: { + description: 'Please use the lifecycle-aware tasks from ember-lifeline instead of Ember.run.*.', + category: 'Best Practices', + recommended: false + }, + meta: { + message: getMessage + }, + create(context) { + let emberImportBinding; + let disallowedCalls = mergeDisallowedCalls(DISALLOWED_OBJECTS); + + return { + ImportDefaultSpecifier(node) { + emberImportBinding = getEmberImportBinding(node); + }, + + ObjectPattern(node) { + if (emberImportBinding) { + disallowedCalls = disallowedCalls.concat( + mergeDisallowedCalls( + collectObjectPatternBindings(node, { + [emberImportBinding]: ['run'] + }) + ) + ); + } + }, + + MemberExpression(node) { + let caller = cleanCaller(getCaller(node)); + + if (disallowedCalls.includes(caller)) { + context.report(node, getMessage(caller)); + } + } + }; + } +}; diff --git a/tests/lib/rules/require-ember-lifeline.js b/tests/lib/rules/require-ember-lifeline.js new file mode 100644 index 0000000..d3bd932 --- /dev/null +++ b/tests/lib/rules/require-ember-lifeline.js @@ -0,0 +1,539 @@ +const rule = require('../../../lib/rules/require-ember-lifeline'); +const getMessage = rule.meta.message; +const RuleTester = require('eslint').RuleTester; +const ruleTester = new RuleTester(); +const parserOptions = { + ecmaVersion: 6, + sourceType: 'module' +}; + +ruleTester.run('require-ember-lifeline', rule, { + valid: [ + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + this.runTask(() => { + doSomeWork(); + }); + } + });`, + parserOptions + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + this.runTask(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + this.debounceTask(() => { + doSomeWork(); + }); + } + });`, + parserOptions + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + this.debounceTask(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + this.throttleTask(() => { + doSomeWork(); + }); + } + });`, + parserOptions + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + this.throttleTask(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions + } + ], + invalid: [ + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + Ember.run.later(() => { + doSomeWork(); + }, 100); + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.later') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + Ember.run.later(() => { + doSomeWork(); + }, 100); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.later') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + run.later(() => { + doSomeWork(); + }, 100); + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.later') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + run.later(() => { + doSomeWork(); + }, 100); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.later') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + init() { + foo.later(() => { + doSomeWork(); + }, 100); + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.later') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + actions: { + bar() { + foo.later(() => { + doSomeWork(); + }, 100); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.later') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + Ember.run.next(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.next') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + Ember.run.next(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.next') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + run.next(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.next') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + run.next(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.next') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + init() { + foo.next(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.next') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + actions: { + bar() { + foo.next(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.next') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + Ember.run.debounce(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.debounce') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + Ember.run.debounce(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.debounce') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + run.debounce(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.debounce') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + run.debounce(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.debounce') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + init() { + foo.debounce(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.debounce') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + actions: { + bar() { + foo.debounce(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.debounce') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + Ember.run.throttle(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.throttle') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + Ember.run.throttle(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('Ember.run.throttle') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + init() { + run.throttle(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.throttle') + }] + }, + { + code: ` + import Ember from 'ember'; + + export default Ember.Component({ + actions: { + foo() { + run.throttle(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('run.throttle') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + init() { + foo.throttle(() => { + doSomeWork(); + }); + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.throttle') + }] + }, + { + code: ` + import Ember from 'ember'; + + const { + run: foo + } = Ember; + + export default Ember.Component({ + actions: { + bar() { + foo.throttle(() => { + doSomeWork(); + }); + } + } + });`, + parserOptions, + errors: [{ + message: getMessage('foo.throttle') + }] + } + ] +});