Dissecting an Ember CLI Build

Dissecting an Ember CLI Build

Talk from EmberConf 2016

As the tooling ecosystem continues to evolve, developers nowadays can easily scaffold out a new Ember app and start being productive right away, without ever thinking of all the intricacies that go on behind a typical build command. But there comes a time when manipulating trees or nodes in Broccoli may be required to support a custom project architecture, or you may find yourself having to extend the build for specialized environments through addons.

Whether you face any one of those scenarios, or you simply want to know more of what goes on behind the curtain, this talk is for you.

48c14a3bb73c1cd19478d62991c659b9?s=128

Estelle DeBlois

March 29, 2016
Tweet

Transcript

  1. EMBER CONF 2016 DISSECTING AN EMBER CLI BUILD Estelle DeBlois

    @brzpegasus @edeblois
  2. None
  3. https://wickedgoodember.com/ WICKED GOOD EMBER, JUNE 27–28, 2016

  4. https://wickedgoodember.com/ “emberconf2016”

  5. None
  6. You use Ember CLI You are familiar with the project

    structure You know what addons are
  7. Ember without Ember CLI is like Peanut Butter without Jelly

  8. var EmberApp = require(‘ember-cli/lib/broccoli/ember-app’); module.exports = function(defaults) { var app

    = new EmberApp(defaults, { // Add options here }); return app.toTree(); }; ember-cli-build.js
  9. var EmberApp = require(‘ember-cli/lib/broccoli/ember-app’); module.exports = function(defaults) { var app

    = new EmberApp(defaults, { // Add options here }); return app.toTree(); }; ember-cli-build.js
  10. ADDONS CUSTOMIZING THROUGH

  11. What if the functionality doesn’t exist yet?

  12. CUSTOMIZING AS AN APP DEVELOPER

  13. let worker = new Worker(‘assets/workers/worker.js’);

  14. ember-conf !"" app !"" bower_components !"" config !"" dist !""

    node_modules !"" public !"" tests !"" vendor #"" workers #"" worker.js
  15. ember-conf !"" app !"" bower_components !"" config !"" dist !""

    node_modules !"" public !"" tests !"" vendor #"" workers #"" worker.js minify
  16. CUSTOMIZING AS AN ADDON AUTHOR

  17. None
  18. None
  19. None
  20. None
  21. BROCCOLI UNDERSTANDING

  22. WHAT IS A TREE?

  23. WHAT IS A TREE? A directory of files

  24. WHAT IS A TREE? A directory of files A string

    representing a directory path
  25. WHAT IS A TREE? A directory of files A string

    representing a directory path An object that implements the Plugin API
  26. WHAT IS A PLUGIN?

  27. WHAT IS A PLUGIN? Takes one or more input trees

  28. WHAT IS A PLUGIN? Takes one or more input trees

    Performs some transformation
  29. WHAT IS A PLUGIN? Takes one or more input trees

    Performs some transformation Returns an output tree
  30. ¯\_(ϑ)_/¯

  31. tree.read(readTree)

  32. tree.read(readTree) .rebuild()

  33. tree.read(readTree) .rebuild() Plugin.prototype.build()

  34. tree.read(readTree) .rebuild() tree Plugin.prototype.build()

  35. node tree Plugin.prototype.build() tree.read(readTree) .rebuild()

  36. (˽°□°)˽Ɨ

  37. potayto, potahto

  38. DIRECTED ACYCLIC GRAPH

  39. Source Node Transform Node

  40. ember-conf !"" app $ !"" components $ !"" controllers $

    !"" helpers $ !"" models $ !"" routes $ !"" styles $ #"" templates !"" bower_components !"" config !"" node_modules !"" public !"" tests #"" vendor source directories SOURCE NODE
  41. ember-conf !"" app $ !"" components $ !"" controllers $

    !"" helpers $ !"" models $ !"" routes $ !"" styles $ #"" templates !"" bower_components !"" config !"" node_modules !"" public !"" tests #"" vendor source directories / SOURCE NODE
  42. SOURCE NODE var source = require(‘broccoli-source’);

  43. var app = new source.WatchedDir(‘app’); var vendor = new source.UnwatchedDir(‘vendor’);

    SOURCE NODE var source = require(‘broccoli-source’);
  44. TRANSFORM NODE var Plugin = require(‘broccoli-plugin’);

  45. TRANSFORM NODE var Plugin = require(‘broccoli-plugin’); var node = new

    Plugin(inputNodes, options);
  46. TRANSFORM NODE Plugin.prototype.build();

  47. app !"" file1.js #"" file2.js output !"" file1.js #"" file2.js

    ES2015 ES5
  48. BABEL var WatchedDir = require(‘broccoli-source’).WatchedDir; var appNode = new WatchedDir(‘app’);

    “app”
  49. BABEL var Transpiler = require(‘broccoli-babel-transpiler’); var transpiledNode = new Transpiler(appNode);

    “app”
  50. Plugins can be chained

  51. “app” BABEL

  52. “app” BABEL CONCAT broccoli-concat

  53. “app” BABEL “public” CONCAT

  54. “app” BABEL “public” CONCAT MERGE broccoli-merge-trees

  55. “app” BABEL “public” CONCAT MERGE output node

  56. EMBER CLI USING BROCCOLI WITH

  57. module.exports = function(defaults) { }; ember-cli-build.js

  58. var WatchedDir = require(‘broccoli-source’).WatchedDir; var Transpiler = require(‘broccoli-babel-transpiler’); var Concat

    = require(‘broccoli-concat’); var TreeMerger = require(‘broccoli-merge-trees’); module.exports = function(defaults) { };
  59. var WatchedDir = require(‘broccoli-source’).WatchedDir; var Transpiler = require(‘broccoli-babel-transpiler’); var Concat

    = require(‘broccoli-concat’); var TreeMerger = require(‘broccoli-merge-trees’); module.exports = function(defaults) { var appNode = new WatchedDir(‘app’); var publicNode = new WatchedDir(‘public’); };
  60. var WatchedDir = require(‘broccoli-source’).WatchedDir; var Transpiler = require(‘broccoli-babel-transpiler’); var Concat

    = require(‘broccoli-concat’); var TreeMerger = require(‘broccoli-merge-trees’); module.exports = function(defaults) { var appNode = new WatchedDir(‘app’); var publicNode = new WatchedDir(‘public’); var transpiledNode = new Transpiler(appNode); var concatNode = new Concat(transpiledNode, { /* … */ }); return new TreeMerger([concatNode, publicNode]); };
  61. var buildFile = load(‘ember-cli-build.js’);

  62. var buildFile = load(‘ember-cli-build.js’); var outputNode = buildFile(); new TreeMerger([concatNode,

    publicNode]);
  63. var buildFile = load(‘ember-cli-build.js’); var outputNode = buildFile(); var Builder

    = require(‘broccoli’).Builder; var builder = new Builder(outputNode, options); builder.build();
  64. “app” BABEL “public” CONCAT MERGE output node

  65. “app” BABEL “public” CONCAT MERGE output node

  66. “app” BABEL “public” CONCAT MERGE output node

  67. “app” BABEL “public” CONCAT MERGE output node

  68. “app” BABEL “public” CONCAT MERGE output node read()

  69. “app” BABEL “public” CONCAT MERGE output node build() read()

  70. “app” BABEL “public” CONCAT MERGE output node build() build() read()

  71. “app” BABEL “public” CONCAT MERGE output node build() build() read()

    read()
  72. “app” BABEL “public” CONCAT MERGE output node build() build() build()

    read() read()
  73. ember serve

  74. None
  75. None
  76. None
  77. None
  78. None
  79. None
  80. None
  81. None
  82. None
  83. None
  84. $ BROCCOLI_VIZ=true ember build $ dot -Tpng graph.0.dot -o graph.0.png

  85. None
  86. None
  87. a WatchedDir WatchedDir Merge Concat Babel “app” “public”

  88. “app” BABEL “public” CONCAT MERGE a WatchedDir WatchedDir Merge Concat

    Babel “app” “public”
  89. None
  90. TreeMerger (allTrees)

  91. None
  92. None
  93. var EmberApp = require(‘ember-cli/lib/broccoli/ember-app’); module.exports = function(defaults) { var app

    = new EmberApp(defaults, { // Add options here }); return app.toTree(); }; ember-cli-build.js
  94. EMBER CLI EXTENDING

  95. Add assets to the app Modify existing assets

  96. ember-conf !"" app $ !"" components $ !"" controllers $

    !"" helpers $ !"" models $ !"" routes $ !"" styles $ #"" templates !"" bower_components !"" config !"" node_modules !"" public !"" tests #"" vendor app styles templates bower public tests vendor SOURCE NODES EMBER APP
  97. app.import(app.bowerDirectory + ‘/moment/moment.js’);

  98. var EmberApp = require(‘ember-cli/lib/broccoli/ember-app’); module.exports = function(defaults) { var app

    = new EmberApp(defaults, { // Add options here }); app.import(app.bowerDirectory + ‘/moment/moment.js’); return app.toTree(); }; ember-cli-build.js
  99. module.exports = { name: ‘my-addon’, included: function(app) { this._super.included(app); app.import(app.bowerDirectory

    + ‘/moment/moment.js’); } }; my-addon/index.js
  100. treeFor* HOOKS

  101. app styles templates public tests vendor CONSUMING APP SOURCE NODES

  102. treeForApp treeForStyles treeForTemplates treeForPublic treeForTestSupport treeForVendor app styles templates public

    tests vendor ADDON HOOKS CONSUMING APP SOURCE NODES
  103. module.exports = { name: ‘my-addon’, treeForApp: function() { return new

    UnwatchedDir(path.join(__dirname, ‘custom’)); } }; my-addon/index.js
  104. treeForApp treeForStyles treeForTemplates treeForPublic treeForTestSupport treeForVendor app styles templates public

    tests vendor ADDON HOOKS CONSUMING APP ADDON my-addon !"" custom $ !"" components $ !"" controllers $ !"" helpers $ !"" models $ !"" routes $ !"" styles $ #"" templates !"" public !"" test-support #"" vendor app
  105. treeForAddon ADDON HOOKS CONSUMING APP ADDON my-addon #"" addon !""

    components !"" controllers !"" helpers !"" models !"" routes !"" styles #"" templates vendor.js vendor.css
  106. None
  107. treeFor* HOOKS (PART DEUX)

  108. None
  109. None
  110. “addon”

  111. “addon” FUNNEL

  112. FUNNEL “addon” var Funnel = require(‘broccoli-funnel’); /

  113. treeForAddon: function(addonNode) { var node = this._super.treeForAddon(addonNode); return new Funnel(node,

    { exclude: [ // exclusion logic ] }); } my-addon/index.js
  114. None
  115. console.log(someNode);

  116. None
  117. var debug = require(‘broccoli-stew’).debug;

  118. treeForAddon: function(addonNode) { var node = this._super.treeForAddon(addonNode); var filteredNode =

    new Funnel(node, { exclude: [ // exclusion logic ] }); return debug(filteredNode, { name: ‘filtered-metrics’ }); } my-addon/index.js
  119. ember-conf !"" app !"" bower_components !"" config !"" DEBUG-filtered-metrics $

    #"" modules $ #"" ember-metrics $ #"" metrics-adapters $ !"" base.js $ #"" google-analytics.js !"" dist !"" node_modules !"" public !"" tests #"" vendor
  120. Add assets to the app Modify existing assets

  121. preprocessTree/postprocessTree HOOKS

  122. TEMPLATE PREPROCESSORS .hbs .js

  123. treeForTemplates() + “templates” TEMPLATE PREPROCESSORS .hbs .js

  124. treeForTemplates() + “templates” TEMPLATE PREPROCESSORS .hbs .js preprocessTree(“template”, node) postprocessTree(“template”,

    processedNode)
  125. treeForStyles() + “styles” CSS PREPROCESSORS .scss .css preprocessTree(“css”, node) postprocessTree(“css”,

    processedNode)
  126. treeForApp() + “app” JS PREPROCESSORS ES2015 ES5 preprocessTree(“js”, node) postprocessTree(“js”,

    processedNode)
  127. treeForTestSupport() + “tests” JS PREPROCESSORS ES2015 ES5 preprocessTree(“tests”, node) postprocessTree(“tests”,

    processedNode)
  128. pre/postprocessTree(“template”, templatesNode) pre/postprocessTree(“css”, stylesNode) pre/postprocessTree(“js”, appJsNode) pre/postprocessTree(“tests”, testJsNode)

  129. postprocessTree(“all”, outputNode) pre/postprocessTree(“template”, templatesNode) pre/postprocessTree(“css”, stylesNode) pre/postprocessTree(“js”, appJsNode) pre/postprocessTree(“tests”, testJsNode)

  130. postprocessTree: function(type, node) { if (type === ‘all’) { //

    add fingerprinting node = assetRev(node, this.options); } return node; } my-addon/index.js
  131. preBuild() postBuild(results) outputReady(results) buildError() config() contentFor() lintTree() setupPreprocessorRegistry()

  132. preBuild() postBuild(results) outputReady(results) buildError() config() contentFor() lintTree() setupPreprocessorRegistry()

  133. None
  134. (˽°□°)˽Ɨ

  135. ϛ( ^_^ϛ)

  136. EMBER CONF 2016 THANKS Estelle DeBlois @brzpegasus @edeblois