$30 off During Our Annual Pro Sale. View Details »

EmberConf 2017 – Confessions of an Ember Addon Author

EmberConf 2017 – Confessions of an Ember Addon Author

This talk was presented at EmberConf 2017.

Video: https://www.youtube.com/watch?v=ln_DvmQsvis

Addons are one of the best things about the Ember eco-system. With one command, you can opt into using a well tested addon that does some of the heavy-lifting for you when building complex applications. The next best thing is that sharing your solution for solving problems is very simple; it's not a big leap going from Ember developer to addon author!

A healthy addon eco-system is one of the key strengths of Ember, and in this talk we'll discover some best practices, tips and tricks and other exciting confessions from a self-confessed addon addict!

Lauren Tan

March 29, 2017
Tweet

More Decks by Lauren Tan

Other Decks in Programming

Transcript

  1. EmberConf 2017
    Confessions of an Ember Addon Author
    Confessions
    of an Ember
    Addon
    Author
    PRESENTED BY
    Lauren Tan
    sugarpirate_
    poteto
    Cinemagraph by /u/orbojunglist

    View Slide

  2. EmberConf 2017
    Confessions of an Ember Addon Author
    Lauren Tan
    sugarpirate_
    poteto

    View Slide

  3. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  4. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  5. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  6. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  7. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  8. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  9. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  10. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  11. EmberConf 2017
    Confessions of an Ember Addon Author
    What makes an addon good?

    View Slide

  12. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  13. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  14. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  15. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  16. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  17. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  18. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  19. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  20. EmberConf 2017
    Confessions of an Ember Addon Author
    Documentation
    Driven
    Development

    View Slide

  21. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  22. EmberConf 2017
    Confessions of an Ember Addon Author
    If it's not documented,
    it doesn't exist
    YES, IT'S TRUE

    View Slide

  23. EmberConf 2017
    Confessions of an Ember Addon Author
    defmodule KVServer.Command do
    @doc ~S"""
    Parses the given `line` into a command.
    "## Examples
    iex> KVServer.Command.parse "CREATE shopping\r\n"
    {:ok, {:create, "shopping"}}
    """
    def parse(line) do
    :not_implemented
    end
    end

    View Slide

  24. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  25. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  26. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  27. EmberConf 2017
    Confessions of an Ember Addon Author
    If you build it, they
    will might (not)
    come
    YOU'VE BEEN LIED TO

    View Slide

  28. EmberConf 2017
    Confessions of an Ember Addon Author
    Any successful
    project requires
    two things
    Nathan Marz, 2014

    View Slide

  29. EmberConf 2017
    Confessions of an Ember Addon Author
    It solves a useful
    problem
    TWO THINGS

    View Slide

  30. EmberConf 2017
    Confessions of an Ember Addon Author
    People are
    convinced your
    solution is the best
    for their problem
    TWO THINGS

    View Slide

  31. EmberConf 2017
    Confessions of an Ember Addon Author
    https://youtu.be/_0T5OSSzxms

    View Slide

  32. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  33. EmberConf 2017
    Confessions of an Ember Addon Author
    ȗ 

    View Slide

  34. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  35. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  36. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  37. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  38. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  39. EmberConf 2017
    Confessions of an Ember Addon Author
    Addon Structure
    Cinemagraph by /u/Hyperoperation

    View Slide

  40. EmberConf 2017
    Confessions of an Ember Addon Author
    Ember Land

    View Slide

  41. EmberConf 2017
    Confessions of an Ember Addon Author
    Node Land

    View Slide

  42. EmberConf 2017
    Confessions of an Ember Addon Author
    addon
    ├── -private
    │ ├── closure-action.js
    │ ├── create-multi-array-helper.js
    │ └── create-needle-haystack-helper.js
    ├── helpers
    │ ├── append.js
    │ ├── ""...
    │ └── without.js
    ├── index.js
    └── utils
    ├── comparison.js
    ├── get-index.js
    ├── includes.js
    ├── is-equal.js
    ├── is-object.js
    └── is-promise.js
    Internals

    View Slide

  43. EmberConf 2017
    Confessions of an Ember Addon Author
    export { default, append } from 'ember-composable-helpers/helpers/append';

    View Slide

  44. EmberConf 2017
    Confessions of an Ember Addon Author
    app
    └── helpers
    ├── append.js
    ├── ""...
    └── without.js Merged into
    consuming app

    View Slide

  45. EmberConf 2017
    Confessions of an Ember Addon Author
    "ember-addon": {
    "configPath": "tests/dummy/config",
    "after": [
    "ember-changeset"
    ]
    }

    View Slide

  46. EmberConf 2017
    Confessions of an Ember Addon Author
    Internals are not
    really private
    A WORD OF CAUTION
    import isPromise from 'ember-changeset/utils/is-promise';
    import ACTION from 'ember-composable-helpers/-private/closure-action';

    View Slide

  47. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  48. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  49. EmberConf 2017
    Confessions of an Ember Addon Author
    export default function validate<%= classifiedModuleName %>("/* options = {} "*/) {
    return ("/* key, newValue, oldValue, changes, content "*/) "=> {
    return true;
    };
    }

    View Slide

  50. EmberConf 2017
    Confessions of an Ember Addon Author
    dasherizedPackageName
    classifiedPackageName
    dasherizedModuleName
    classifiedModuleName
    camelizedModuleName
    https:"//ember-cli.com/api/classes/Blueprint.html

    View Slide

  51. EmberConf 2017
    Confessions of an Ember Addon Author
    config
    blueprintsPath
    includedCommands
    serverMiddleware
    testemMiddleware
    postBuild
    preBuild
    outputReady
    buildError
    included
    shouldIncludeChildAddon
    setupPreprocessorRegistry
    preprocessTree
    postprocessTree
    lintTree
    contentFor
    treeFor
    Addon
    hooks are
    powerful
    WORKING IN NODE LAND
    https://ember-cli.com/api/classes/Addon.html
    Cinemagraph by /u/orbojunglist

    View Slide

  52. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  53. EmberConf 2017
    Confessions of an Ember Addon Author
    I. setupPreprocessorRegistry
    II. treeForAddon

    View Slide

  54. EmberConf 2017
    Confessions of an Ember Addon Author
    SETUP PREPROCESSOR REGISTRY
    Strip test selectors
    from templates
    https://ember-cli.com/api/classes/Addon.html#method_setupPreprocessorRegistry

    View Slide

  55. EmberConf 2017
    Confessions of an Ember Addon Author

    Click me
    "

    View Slide

  56. EmberConf 2017
    Confessions of an Ember Addon Author
    TREE FOR ADDON
    Remove addon
    from build
    https://ember-cli.com/api/classes/Addon.html#method_treeForAddon

    View Slide

  57. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  58. EmberConf 2017
    Confessions of an Ember Addon Author
    Remove unused
    helpers from build
    TREE FOR ADDON

    View Slide

  59. EmberConf 2017
    Confessions of an Ember Addon Author
    treeForAddon: function() {
    "// see: https:"//github.com/ember-cli/ember-cli/issues/4463
    var tree = this._super.treeForAddon.apply(this, arguments);
    return this.filterHelpers(tree, new RegExp('^modules\/' + this.name + '\/helpers\/', 'i'));
    },
    filterHelpers: function(tree, regex) {
    var whitelist = this.whitelist;
    var blacklist = this.blacklist;
    var _this = this;
    "// exit early if no opts defined
    if (whitelist.length ""=== 0 "&& blacklist.length ""=== 0) {
    return new Funnel(tree);
    }
    return new Funnel(tree, {
    exclude: [function(name) {
    return _this.exclusionFilter(name, regex, {
    whitelist: whitelist,
    blacklist: blacklist
    });
    }]
    });
    }

    View Slide

  60. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  61. EmberConf 2017
    Confessions of an Ember Addon Author
    Augment
    ember-cli
    INCLUDED COMMANDS

    View Slide

  62. EmberConf 2017
    Confessions of an Ember Addon Author
    includedCommands: function() {
    return {
    'deploy': require('./lib/deploy'),
    'deploy:activate': require('./lib/activate'),
    'deploy:list': require('./lib/list')
    };
    }

    View Slide

  63. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  64. EmberConf 2017
    Confessions of an Ember Addon Author
    Cinemagraph by /u/ibru
    Developer Experience

    View Slide

  65. EmberConf 2017
    Confessions of an Ember Addon Author
    Tell a story
    WRITING GOOD DOCUMENTATION

    View Slide

  66. EmberConf 2017
    Confessions of an Ember Addon Author
    “I used ES6 classes and my startup failed AMA”

    View Slide

  67. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  68. EmberConf 2017
    Confessions of an Ember Addon Author
    What is the
    public API?
    WRITING GOOD DOCUMENTATION

    View Slide

  69. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  70. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  71. EmberConf 2017
    Confessions of an Ember Addon Author
    Show, don't
    tell
    WRITING GOOD DOCUMENTATION

    View Slide

  72. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  73. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  74. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  75. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  76. EmberConf 2017
    Confessions of an Ember Addon Author
    Cinemagraph by /u/barracuda415
    Testing Your Addon

    View Slide

  77. EmberConf 2017
    Confessions of an Ember Addon Author
    module.exports = {
    scenarios: [
    {
    name: 'ember-1.13',
    bower: { "/* ""..."*/ },
    npm: { "/* ""..."*/ }
    },
    {
    name: 'ember-lts-2.8',
    bower: { "/* ""..."*/ },
    npm: { "/* ""..."*/ }
    },
    {
    name: 'ember-release',
    bower: { "/* ""..."*/ },
    npm: { "/* ""..."*/ }
    },
    {
    name: 'ember-beta',
    bower: { "/* ""..."*/ },
    npm: { "/* ""..."*/ }
    },
    {
    name: 'ember-canary',
    bower: { "/* ""..."*/ },
    npm: { "/* ""..."*/ }
    },
    {
    name: 'ember-default',
    bower: { "/* ""..."*/ },
    npm: { "/* ""..."*/ }
    }
    ]
    };
    Ensure
    Compatibility
    With Ember-Try

    View Slide

  78. EmberConf 2017
    Confessions of an Ember Addon Author
    env:
    - EMBER_TRY_SCENARIO=ember-lts-2.4
    - EMBER_TRY_SCENARIO=ember-lts-2.8
    - EMBER_TRY_SCENARIO=ember-release
    - EMBER_TRY_SCENARIO=ember-beta
    - EMBER_TRY_SCENARIO=ember-canary
    - EMBER_TRY_SCENARIO=ember-default

    View Slide

  79. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  80. EmberConf 2017
    Confessions of an Ember Addon Author
    Test With
    A Real
    Browser
    addons:
    apt:
    sources:
    - google-chrome
    packages:
    - google-chrome-stable
    before_install:
    # Setup for Chrome
    - export DISPLAY=:99.0
    - sh -e /etc/init.d/xvfb start
    https://github.com/machty/ember-concurrency/blob/master/.travis.yml

    View Slide

  81. EmberConf 2017
    Confessions of an Ember Addon Author
    Configuration

    View Slide

  82. EmberConf 2017
    Confessions of an Ember Addon Author
    File in /app

    View Slide

  83. EmberConf 2017
    Confessions of an Ember Addon Author
    "// app/transitions.js
    export default function() {
    this.transition(
    this.fromRoute('people.index'),
    this.toRoute('people.detail'),
    this.use('toLeft'),
    this.reverse('toRight')
    );
    };

    View Slide

  84. EmberConf 2017
    Confessions of an Ember Addon Author
    if (owner.factoryFor) {
    let maybeConfig = owner.factoryFor('transitions:main');
    config = maybeConfig "&& maybeConfig.class;
    } else {
    config = owner._lookupFactory('transitions:main');
    }

    View Slide

  85. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  86. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  87. EmberConf 2017
    Confessions of an Ember Addon Author
    define("my-app/transitions", ["exports"], function (exports) {
    "// ""...
    });

    View Slide

  88. EmberConf 2017
    Confessions of an Ember Addon Author
    export default {
    url: 'http:"//localhost:3000',
    timeout: 1000,
    isThing: true
    }

    View Slide

  89. EmberConf 2017
    Confessions of an Ember Addon Author
    config/environment.js

    View Slide

  90. EmberConf 2017
    Confessions of an Ember Addon Author
    metricsAdapters: [
    {
    name: 'Mixpanel',
    environments: ['development'],
    config: {
    token: 'abcd1234'
    }
    },
    {
    name: 'GoogleAnalytics',
    environments: ['production'],
    config: {
    id: 'UA-XXXXXXXX-X'
    }
    }
    ]

    View Slide

  91. EmberConf 2017
    Confessions of an Ember Addon Author
    "// app/initializers/metrics.js
    import config from '"../config/environment';
    export function initialize() {
    const application = arguments[1] "|| arguments[0];
    const { metricsAdapters = [] } = config;
    const { environment = 'development' } = config;
    const options = { metricsAdapters, environment };
    application.register('config:metrics', options, { instantiate: false });
    application.inject('service:metrics', 'options', 'config:metrics');
    }
    export default {
    name: 'metrics',
    initialize
    };

    View Slide

  92. EmberConf 2017
    Confessions of an Ember Addon Author
    import Ember from 'ember';
    const { Service, get } = Ember;
    export default Service.extend({
    init() {
    this._super(""...arguments);
    let { metricsAdapters, environment } = get(this, 'options');
    "// do stuff
    }
    });

    View Slide

  93. EmberConf 2017
    Confessions of an Ember Addon Author
    ember-cli-build.js

    View Slide

  94. EmberConf 2017
    Confessions of an Ember Addon Author
    module.exports = function(defaults) {
    var app = new EmberApp(defaults, {
    'ember-composable-helpers': {
    only: ['inc', 'dec', 'pipe'],
    except: ['pipe', 'filter-by']
    }
    });
    }

    View Slide

  95. EmberConf 2017
    Confessions of an Ember Addon Author
    included: function(app) {
    this._super.included.apply(this, arguments);
    "// see: https:"//github.com/ember-cli/ember-cli/issues/3718
    if (typeof app.import ""!== 'function' "&& app.app) {
    app = app.app;
    }
    this.app = app;
    this.app.options = this.app.options "|| {};
    var config = this.app.options[this.name] "|| {};
    this.whitelist = this.generateWhitelist(config);
    this.blacklist = this.generateBlacklist(config);
    }

    View Slide

  96. EmberConf 2017
    Confessions of an Ember Addon Author
    Extensibility

    View Slide

  97. EmberConf 2017
    Confessions of an Ember Addon Author
    Ember Land

    View Slide

  98. EmberConf 2017
    Confessions of an Ember Addon Author
    "// addon/metrics-adapters/base.js
    import Ember from 'ember';
    const { Object: EmberObject } = Ember;
    export default EmberObject.extend({
    "// base methods
    });
    Not Managed
    By Ember's
    Container

    View Slide

  99. EmberConf 2017
    Confessions of an Ember Addon Author
    "// addon/metrics-adapters/google-analytics.js
    import BaseAdapter from './base';
    export default BaseAdapter.extend({
    "// custom code
    });

    View Slide

  100. EmberConf 2017
    Confessions of an Ember Addon Author
    /**
    * Looks up the adapter from the resolver. Prioritizes the consuming app's
    * adapters over the addon's adapters.
    *
    * @method _lookupAdapter
    * @param {String} adapterName
    * @private
    * @return {Adapter} a local adapter or an adapter from the addon
    "*/
    _lookupAdapter(adapterName) {
    assert('[ember-metrics] Could not find metrics adapter without a name.', adapterName);
    const dasherizedAdapterName = dasherize(adapterName);
    const availableAdapter = getOwner(this).lookup(`ember-metrics@metrics-adapter:${dasherizedAdapterName}`);
    const localAdapter = getOwner(this).lookup(`metrics-adapter:${dasherizedAdapterName}`);
    return localAdapter ? localAdapter : availableAdapter;
    }

    View Slide

  101. EmberConf 2017
    Confessions of an Ember Addon Author
    /**
    * Instantiates an adapter if one is found.
    *
    * @method _activateAdapter
    * @param {Object}
    * @private
    * @return {Adapter}
    "*/
    _activateAdapter({ name, config } = {}) {
    const Adapter = this._lookupAdapter(name);
    assert(`[ember-metrics] Could not find metrics adapter ${name}.`, Adapter);
    return Adapter.create({ this, config });
    }

    View Slide

  102. EmberConf 2017
    Confessions of an Ember Addon Author
    Not-Ember Land

    View Slide

  103. EmberConf 2017
    Confessions of an Ember Addon Author
    "/* globals requirejs, requireModule "*/
    import Ember from 'ember';
    import defaultMessages from 'ember-changeset-validations/utils/messages';
    const { A: emberArray, isPresent } = Ember;
    const { keys } = Object;
    const matchRegex = /validations\/messages$/gi;
    let cachedRef = null;
    /**
    * Find and load messages module on consuming app. Defaults to addon messages.
    * To define a custom message map, create `my-app/app/validations/messages.js`
    * and export an object.
    *
    * @param {Object} moduleMap
    * @param {Boolean} useCache Pass `false` to ignore cached key
    * @return {Object}
    "*/
    export default function getMessages(moduleMap = requirejs.entries, useCache = true) {
    if (useCache "&& isPresent(cachedRef)) {
    return cachedRef;
    }
    let moduleKey = emberArray(keys(moduleMap))
    .find((module) "=> isPresent(module.match(matchRegex)));
    let messagesModule = isPresent(moduleKey) ? requireModule(moduleKey).default : defaultMessages;
    cachedRef = messagesModule;
    return messagesModule;
    }
    Probably
    A Hack

    View Slide

  104. EmberConf 2017
    Confessions of an Ember Addon Author
    "/* globals requirejs, requireModule "*/
    import Ember from 'ember';
    import defaultMessages from 'ember-changeset-validations/utils/messages';
    const { A: emberArray, isPresent } = Ember;
    const { keys } = Object;
    const matchRegex = /validations\/messages$/gi;
    let cachedRef = null;
    /**
    * Find and load messages module on consuming app. Defaults to addon messages.
    * To define a custom message map, create `my-app/app/validations/messages.js`
    * and export an object.
    *
    * @param {Object} moduleMap
    * @param {Boolean} useCache Pass `false` to ignore cached key
    * @return {Object}
    "*/
    export default function getMessages(moduleMap = requirejs.entries, useCache = true) {
    if (useCache "&& isPresent(cachedRef)) {
    return cachedRef;
    }
    let moduleKey = emberArray(keys(moduleMap))
    .find((module) "=> isPresent(module.match(matchRegex)));
    let messagesModule = isPresent(moduleKey) ? requireModule(moduleKey).default : defaultMessages;
    cachedRef = messagesModule;
    return messagesModule;
    }
    Probably
    A Hack
    Don't do this

    View Slide

  105. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  106. EmberConf 2017
    Confessions of an Ember Addon Author
    Child Addons

    View Slide

  107. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  108. EmberConf 2017
    Confessions of an Ember Addon Author
    {
    "name": "my-ember-service-worker-plugin",
    "version": "0.0.0",
    "keywords": [
    "ember-addon",
    "ember-service-worker-plugin"
    ]
    }

    View Slide

  109. EmberConf 2017
    Confessions of an Ember Addon Author
    postprocessTree(type, appTree) {
    if (type ""!== 'all') {
    return appTree;
    }
    let plugins = this._findPluginsFor(this.project);
    "// Add the project itself as a possible plugin, this way user can add custom
    "// service-worker code in their app, without needing to build a plugin.
    plugins = [this].concat(plugins, this.project);
    "// other setup
    },
    _findPluginsFor(project) {
    let addons = project.addons "|| [];
    return addonUtils.filterByKeyword(addons, 'ember-service-worker-plugin');
    }

    View Slide

  110. EmberConf 2017
    Confessions of an Ember Addon Author
    Ember Internals

    View Slide

  111. EmberConf 2017
    Confessions of an Ember Addon Author
    https://kapeli.com/dash

    View Slide

  112. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  113. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  114. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  115. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  116. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  117. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  118. EmberConf 2017
    Confessions of an Ember Addon Author
    Kitten
    OPEN SOURCE LEVEL

    View Slide

  119. EmberConf 2017
    Confessions of an Ember Addon Author
    Cat
    OPEN SOURCE LEVEL

    View Slide

  120. EmberConf 2017
    Confessions of an Ember Addon Author
    Veteran
    OPEN SOURCE LEVEL

    View Slide

  121. EmberConf 2017
    Confessions of an Ember Addon Author
    wycats
    OPEN SOURCE LEVEL

    View Slide

  122. EmberConf 2017
    Confessions of an Ember Addon Author
    Relinquish control
    KEEPING YOUR ADDON ALIVE

    View Slide

  123. EmberConf 2017
    Confessions of an Ember Addon Author
    The issue backlog
    can be
    overwhelming
    STAYING SANE

    View Slide

  124. EmberConf 2017
    Confessions of an Ember Addon Author
    It's okay to say no
    AVOID BURNOUT

    View Slide

  125. EmberConf 2017
    Confessions of an Ember Addon Author
    Please
    SEMVER

    View Slide

  126. EmberConf 2017
    Confessions of an Ember Addon Author

    View Slide

  127. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  128. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  129. EmberConf 2017
    Confessions of an Ember Addon Author
    I. Addon Anatomy
    II. One Weird Trick
    III. Open Sourcery

    View Slide

  130. EmberConf 2017
    Confessions of an Ember Addon Author
    Lauren Tan
    sugarpirate_
    poteto

    View Slide

  131. EmberConf 2017
    Confessions of an Ember Addon Author
    Cinemagraph by /u/fezzo
    Thank you
    sugarpirate_
    poteto

    View Slide