Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing code to update our code - Wicked Good Ember

Writing code to update our code - Wicked Good Ember

After upgrading Ember we might have times where we start to receive warnings about things that we should update in our code because of deprecations.
We can write scripts with tools like sed or Perl to fix our code but, when we are working with multiple teams and projects, we can see that those scripts start to fail or are not easy to maintain.
In this talk we’ll cover a different approach to update our code through direct operations on its abstract syntax tree (AST) using recast and how to distribute our solutions as addons so they can be used by other teams.
Specifically we’ll see how AST transform are used for scenarios like:
- Route generation in ember-cli
- Update QUnit tests via ember-watson
- Upgrade Ember-Data deprecations

Adolfo Builes

June 16, 2015
Tweet

Other Decks in Programming

Transcript

  1. Finally, I'll get to discover whether @abuiles is really a

    cat. — Jeffrey Biles (@JeffreyBiles) Wicked Good Ember
  2. test('Creating a new friends', function() { visit('/friends/new'); andThen(function() { //

    GLOBAL equal(currentPath(), 'friends.new'); // GLOBAL ok(true, 'called'); }); }); Wicked Good Ember
  3. import {module, test} from 'qunit'; module('Acceptance: FriendsNew', { beforeEach: function(assert)

    { }, afterEach: function() { } }); test('Creating a new friends', function(assert) { visit('/friends/new'); andThen(function() { assert.equal(currentPath(), 'friends.new'); assert.ok(true, 'called'); }); }); Wicked Good Ember
  4. Perl Pie find . -type f | xargs perl -pi

    -e "s/ expect\(/assert\.expect(/g" find . -type f | xargs perl -pi -e "s/ ok\(/assert\.ok(/g" find . -type f | xargs perl -pi -e "s/ equal\(/assert\.equal(/g" find . -type f | xargs perl -pi -e "s/ deepEqual\(/assert\.deepEqual(/g" Wicked Good Ember
  5. Regexp based replacements if (plural === name) { newContent =

    oldContent.replace( /(map\(function\(\) {[\s\S]+)}\)/, "$1 this.resource('" + name + "', function() { });" + EOL + "})" ); } else { newContent = oldContent.replace( /(map\(function\(\) {[\s\S]+)}\)/, "$1 this.resource('" + name + "', { path: '" + plural + "/:" + name + "_id' }, function() { });" + EOL + "})" ); } Wicked Good Ember
  6. this.route('friends'); + Program body [1] + ExpressionStatement + expression +

    CallExpression + callee |-+ MemberExpression | + computed: false | + object | |-+ ThisExpression | + property | + Identifier | + name: route + arguments [1] + Literal + value: friends + raw: 'friends' Wicked Good Ember
  7. { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": {

    "type": "CallExpression", "callee": { "type": "MemberExpression", "property": { "type": "Identifier", "name": "route" }... }, "arguments": [ { "type": "Literal", "value": "friends", "raw": "'friends'" } ] } } ] } Wicked Good Ember
  8. We need an ECMAScript parser • Traverse • Identify •

    Replace • Create • Print Wicked Good Ember
  9. { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": {

    "type": "CallExpression", "callee": { "type": "MemberExpression", "property": { "type": "Identifier", "name": "route" } }, "arguments": [ { "type": "Literal", "value": "friends", "raw": "'friends'" } ] } } ] } Wicked Good Ember
  10. Visitor Pattern recast.visit(ast, { visitExpressionStatement: function(path) { // magic code

    here }, visitImportDeclaration: function(path) { if (isImportFor('ember-qunit', path.node)) { sections.addQUnitImport = false; } if (isImportFor('qunit', path.node)) { sections.addQUnitImport = false; } this.traverse(path); } }) Wicked Good Ember
  11. visitExpressionStatement: function(path) { var node = path.node; // module('Acceptance: foo',

    ..) if (isModule(node)) { sections.modules.push(node); } // test('it works', function() {}) if (isTest(node)) { sections.tests.push(node); } if (isSkip(node)) { sections.skips.push(node); } // ok(true, 'the truth') if (isAssertion(node)) { sections.assertions.push(node); } this.traverse(path); } Wicked Good Ember
  12. var types = recast.types.namedTypes; // ok(true, 'the truth'); function isAssertion(node)

    { // node.expression.type === 'CallExpression' return types.CallExpression.check(node.expression) && ASSERTIONS.indexOf(node.expression.callee.name) != -1; } var ASSERTIONS = [ 'deepEqual', 'equal', 'expect', 'ok', ... ]; Wicked Good Ember
  13. function transformAssertions(node) { node.expression.callee.name = 'assert.' + node.expression.callee.name; } //

    output for this would be something like ok(true, 'the truth') => assert.ok(true, 'the truth') Wicked Good Ember
  14. function transformTestCallback(callback){ if (types.FunctionExpression.check(callback)) { if (callback.params.length === 0) {

    callback.params.push(builders.identifier('assert')); } } } // example test('it works', function(){}); => test('it works', function(callback){}); Wicked Good Ember
  15. var builders = recast.types.builders; builders.identifier('assert') => { name: 'assert', loc:

    null, type: 'Identifier', comments: null, typeAnnotation: null } Wicked Good Ember
  16. builders.importDeclaration(specifiers, builders.moduleSpecifier('qunit'); => { specifiers: [ { id: { name:

    'module', type: 'Identifier', }, ... ], source: { value: 'qunit', type: 'ModuleSpecifier', } type: 'ImportDeclaration' } // output import {module, test} from 'qunit'; Wicked Good Ember
  17. • ✓ Use Ember.computed/Ember.observer instead of prototype extensions // ember

    watson:convert-prototype-extensions // before isActive: function(){ }.property('subscribed', 'paid') // after isActive: Ember.computed('subscribed', 'paid', function() { }) Wicked Good Ember
  18. • ✓ Update model lookups on relationships to use kebab-

    case // ember watson:convert-ember-data-model-lookups // before, using a camelCase string export default DS.Model.extend({ postComments: DS.hasMany('postComment', {async: true}) }); // after export default DS.Model.extend({ postComments: DS.hasMany('post-comment', {async: true}) }); Wicked Good Ember
  19. • ✓ async will be the default in ED, add

    async:false to every rel without async info // ember watson:convert-ember-data-async-false-relationships // before posts: DS.hasMany('post') // after posts: DS.hasMany('post', {async: false}) Wicked Good Ember
  20. • ☐ fix initializers/instance initializers //before App.initializer({ name: "clock", initialize:

    function(container, application) { application.register("clock:main", Clock); var clock = container.lookup("clock:main"); clock.setStartTime(Date.now()); } }); // after App.initializer({ }); App.instanceInitializer({ }); Wicked Good Ember
  21. • ☐ Upgrade CP syntax //before isActive: Ember.computed('subscribed', 'paid', function()

    { }) //after isActive: Ember.computed('subscribed', 'paid', { get() {}, set() {} }) Wicked Good Ember
  22. • ☐ Migrate from proxy controllers (HtmlBarCast anyone?) // before

    Ember.ObjectController.extend({}) // after Ember.Controller.extend({ // all sort of craziness here }) Wicked Good Ember
  23. • ☐ Static Analysis, identify bad practices (something similar to

    rubocop). setupController() { // lol you won't have a model } Wicked Good Ember