├── .watchmanconfig ├── .bowerrc ├── lib ├── utils │ ├── pad-right.js │ └── color-for-size.js ├── helpers │ ├── analyze-path.js │ ├── copy-file.js │ ├── uglify.js │ ├── make-dir.js │ ├── walk-tree.js │ ├── log-file-stats.js │ └── analyze.js ├── commands │ └── asset-sizes.js ├── plugins │ └── tree-logger.js ├── models │ ├── asset-cache.js │ └── lib-quantifier.js ├── shim-addon-model.js └── shim-app.js ├── .npmignore ├── testem.js ├── .ember-cli ├── .gitignore ├── ember-cli-build.js ├── .jshintrc ├── .editorconfig ├── .travis.yml ├── README.md ├── LICENSE.md ├── package.json └── index.js /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /lib/utils/pad-right.js: -------------------------------------------------------------------------------- 1 | module.exports = function padRight(str, l, p) { 2 | var s = str; 3 | while (s.length < l) { 4 | s += p; 5 | } 6 | return s; 7 | }; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .jshintrc 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "PhantomJS" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /lib/utils/color-for-size.js: -------------------------------------------------------------------------------- 1 | module.exports = function colorForSize(size) { 2 | var color = 'grey'; 3 | if (size > 1024) { 4 | color = 'white'; 5 | } 6 | if (size > 1024 * 5) { 7 | color = 'green'; 8 | } 9 | if (size > (1024 * 10)) { 10 | color = 'yellow'; 11 | } 12 | if (size > (1024 * 50)) { 13 | color = 'magenta'; 14 | } 15 | if (size > (1024 * 500)) { 16 | color = 'red'; 17 | } 18 | 19 | return color; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/helpers/analyze-path.js: -------------------------------------------------------------------------------- 1 | var chalk = require("chalk"); 2 | var walkTree = require('./walk-tree'); 3 | var debug = require('debug')('asset-sizes:analyze-path'); 4 | var analyzeFile = require('./analyze'); 5 | 6 | module.exports = function analyzePath(projectRoot, srcPath, callback, options) { 7 | debug(chalk.white(srcPath)); 8 | walkTree(srcPath, function(info) { 9 | info.root = projectRoot; 10 | analyzeFile(info, callback, options); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/helpers/copy-file.js: -------------------------------------------------------------------------------- 1 | var mkDir = require('./make-dir'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var debug = require('debug')('asset-sizes:copy-file'); 5 | var chalk = require('chalk'); 6 | 7 | module.exports = function copyFile(src, base, newBase) { 8 | var file = src.substr(base.length + 1); 9 | var dest = path.join(newBase, file); 10 | var destPath = path.parse(dest); 11 | 12 | mkDir(destPath.dir); 13 | debug('reading: ' + src); 14 | var f = fs.readFileSync(src, { encoding: 'utf8' }); 15 | 16 | debug('writing: ' + dest); 17 | fs.writeFileSync(dest, f); 18 | }; 19 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | - EMBER_TRY_SCENARIO=default 14 | - EMBER_TRY_SCENARIO=ember-1-13 15 | - EMBER_TRY_SCENARIO=ember-release 16 | - EMBER_TRY_SCENARIO=ember-beta 17 | - EMBER_TRY_SCENARIO=ember-canary 18 | 19 | matrix: 20 | fast_finish: true 21 | allow_failures: 22 | - env: EMBER_TRY_SCENARIO=ember-canary 23 | 24 | before_install: 25 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH 26 | - "npm config set spin false" 27 | - "npm install -g npm@^2" 28 | 29 | install: 30 | - npm install -g bower 31 | - npm install 32 | - bower install 33 | 34 | script: 35 | - ember try $EMBER_TRY_SCENARIO test 36 | -------------------------------------------------------------------------------- /lib/helpers/uglify.js: -------------------------------------------------------------------------------- 1 | var uglify = require('uglify-js'); 2 | var chalk = require('chalk'); 3 | var debug = require('debug')('asset-sizes:uglify'); 4 | 5 | module.exports = function uglifyFile(src, options) { 6 | debug(chalk.grey('Uglifying JS: ') + chalk.white(src)); 7 | try { 8 | var start = new Date(); 9 | var result = uglify.minify(src, options); 10 | var end = new Date(); 11 | var total = end - start; 12 | 13 | if (total > 20000) { 14 | debug(chalk.yellow('\t[WARN] `' + src + '` took: ' + total + 'ms (more then 20,000ms)')); 15 | } 16 | 17 | return result.code; 18 | 19 | } catch(e) { 20 | e.filename = src; 21 | debug( 22 | chalk.yellow('[WARN] Unable to minify `' + src + 23 | '`.\nThe non-minified gzipped size will instead be used in the results') + 24 | chalk.grey('\tError: ' + e.message) 25 | ); 26 | throw e; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember-cli-asset-sizes-shim 2 | 3 | 4 | - list vendor modules and size / gzip / gzip + min (ordered by size) 5 | - size of modules added to an app by an addon (per addon) (size / gzip / gzip + min) 6 | - list final build sizes for JS/CSS (size / gzip / gzip + min) 7 | - list other assets by size 8 | - list sizes at build time 9 | - detect js/css payload size regressions 10 | 11 | 12 | ## Installation 13 | 14 | * `git clone` this repository 15 | * `npm install` 16 | * `bower install` 17 | 18 | ## Running 19 | 20 | * `ember server` 21 | * Visit your app at http://localhost:4200. 22 | 23 | ## Running Tests 24 | 25 | * `npm test` (Runs `ember try:testall` to test your addon against multiple Ember versions) 26 | * `ember test` 27 | * `ember test --server` 28 | 29 | ## Building 30 | 31 | * `ember build` 32 | 33 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 34 | -------------------------------------------------------------------------------- /lib/helpers/make-dir.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | var path = require("path"); 3 | var fs = require("fs"); 4 | var os = require('os') 5 | var debug = require('debug')('asset-sizes:make-dir'); 6 | 7 | module.exports = function makeDir(dir) { 8 | debug(dir); 9 | var dirs = dir.split(path.sep); 10 | var base = path.sep === '/' ? '/' : ''; 11 | 12 | dirs.forEach(function (segment) { 13 | if (segment) { 14 | // on win32 node doesn't put \ after drive letter in join 15 | if (os.platform() === 'win32' && segment.length === 2 && segment.indexOf(':') === 1) { 16 | segment += '\\'; 17 | } 18 | base = path.join(base, segment); 19 | 20 | if (!fs.existsSync(base)) { 21 | debug('make: ', base); 22 | try { 23 | fs.mkdirSync(base); 24 | } catch (e) { 25 | if (e.code !== "EEXIST") { 26 | throw e; 27 | } 28 | } 29 | } 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/helpers/walk-tree.js: -------------------------------------------------------------------------------- 1 | var chalk = require("chalk"); 2 | var path = require("path"); 3 | var fs = require("fs"); 4 | var debug = require('debug')('asset-sizes:walk'); 5 | 6 | module.exports = function walkTree(src, callback, leaf) { 7 | debug(leaf ? chalk.grey(leaf) : chalk.cyan('Tree Root ') + chalk.grey(src)); 8 | leaf = leaf || ''; 9 | var fullPath = leaf ? path.join(src, leaf) : src; 10 | 11 | try { 12 | var stat = fs.statSync(fullPath); 13 | } catch (e) { 14 | debug(chalk.red('\t404 [ERROR] `' + fullPath + '`was not found.')); 15 | return; 16 | } 17 | 18 | if (stat) { 19 | 20 | if (stat.isDirectory()) { 21 | var dir = fs.readdirSync(fullPath); 22 | 23 | dir.forEach(function(name) { 24 | walkTree(src, callback, path.join(leaf, name)); 25 | }); 26 | return; 27 | } 28 | 29 | if (stat.isFile()) { 30 | callback({ 31 | src: fullPath, 32 | stats: stat 33 | }) 34 | } 35 | 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lib/helpers/log-file-stats.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var filesize = require('filesize'); 3 | 4 | function padRight(str, l, p) { 5 | var s = str; 6 | while (s.length < l) { 7 | s += p; 8 | } 9 | return s; 10 | } 11 | 12 | function getHumanSize(size) { 13 | return padRight(filesize(size), 10, ' '); 14 | } 15 | 16 | module.exports = function logStats(stats, ret, prefix) { 17 | prefix = prefix || ''; 18 | var smallest = stats.sizes.final; 19 | var smallColor = 'grey'; 20 | if (smallest > 1024) { 21 | smallColor = 'white'; 22 | } 23 | if (smallest > 1024 * 5) { 24 | smallColor = 'green'; 25 | } 26 | if (smallest > (1024 * 10)) { 27 | smallColor = 'yellow'; 28 | } 29 | if (smallest > (1024 * 50)) { 30 | smallColor = 'magenta'; 31 | } 32 | if (smallest > (1024 * 500)) { 33 | smallColor = 'red'; 34 | } 35 | 36 | var name = stats.realFilePath.length > 30 ? stats.name : stats.realFilePath; 37 | var msg = prefix + chalk[smallColor](getHumanSize(smallest)) + ' ' + chalk.cyan(padRight(name, 32, ' ')) + 38 | chalk.grey( 39 | 'original: ' + getHumanSize(stats.sizes.original) + 40 | ' gzip+min: ' + getHumanSize(stats.sizes.final) 41 | ); 42 | 43 | if (ret) { 44 | return msg; 45 | } 46 | console.log(msg); 47 | }; 48 | -------------------------------------------------------------------------------- /lib/commands/asset-sizes.js: -------------------------------------------------------------------------------- 1 | var BuildCommand = require('ember-cli/lib/commands/build'); 2 | 3 | var Command = BuildCommand.extend({ 4 | name: 'asset-sizes', 5 | description: 'Builds a production version of your app and places it into the output path (dist/ by default) while logging asset sizes.', 6 | aliases: ['a'], 7 | 8 | availableOptions: [ 9 | { name: 'sizes', type: Boolean, default: true, aliases: ['s'] }, 10 | { name: 'environment', type: String, default: 'production', aliases: ['e', { 'dev': 'development' }, { 'prod': 'production' }] }, 11 | { name: 'trace', type: String, aliases: ['t'] }, 12 | { name: 'trace-all', type: Boolean, default: false, aliases: ['ta'] }, 13 | { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['o'] }, 14 | 15 | // these are on the build command, but don't really serve us well here 16 | { name: 'suppress-sizes', type: Boolean, default: true }, 17 | { name: 'watch', type: Boolean, default: false, aliases: ['w'] }, 18 | { name: 'watcher', type: String } 19 | ], 20 | 21 | run: function(commandOptions) { 22 | if (!this.tasks.ShowAssetSizes) { 23 | this.tasks.ShowAssetSizes = function() { this.run = function() { return true; }}; 24 | } 25 | 26 | return this._super.run.call(this, commandOptions); 27 | 28 | } 29 | 30 | }); 31 | 32 | Command.overrideCore = true; 33 | 34 | module.exports = Command; 35 | -------------------------------------------------------------------------------- /lib/plugins/tree-logger.js: -------------------------------------------------------------------------------- 1 | var Plugin = require("broccoli-plugin"); 2 | var path = require("path"); 3 | var fs = require("fs"); 4 | var Promise = require("rsvp").Promise; // jshint ignore:line 5 | var analyzePath = require('../helpers/analyze-path'); 6 | var debug = require('debug')('asset-sizes:tree-logger'); 7 | var chalk = require('chalk'); 8 | var copyFile = require('../helpers/copy-file'); 9 | 10 | // Create a subclass from Plugin 11 | TreeLogger.prototype = Object.create(Plugin.prototype); 12 | TreeLogger.prototype.constructor = TreeLogger; 13 | 14 | function TreeLogger(inputNodes, options) { 15 | options = options || { 16 | annotation: "Tree Stats", 17 | cache: [] 18 | }; 19 | 20 | Plugin.call(this, inputNodes, { 21 | annotation: options.annotation 22 | }); 23 | 24 | this.options = options; 25 | 26 | } 27 | 28 | TreeLogger.prototype.build = function buildLoggedTree() { 29 | var _self = this; 30 | debug(chalk.grey(this.options.annotation)); 31 | 32 | if (this.inputPaths.length > 1) { 33 | debug(chalk.red('[' + this.annotation + '] Received too many input paths')); 34 | } 35 | 36 | var inputPath = this.inputPaths[0]; 37 | var outputPath = path.join(this.outputPath); 38 | 39 | analyzePath(inputPath, inputPath, function(info) { 40 | _self.options.cache.push(info); 41 | copyFile(info.path, inputPath, outputPath); 42 | }); 43 | 44 | return Promise.resolve(); 45 | }; 46 | 47 | module.exports = TreeLogger; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-cli-asset-sizes-shim", 3 | "version": "0.1.2", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "ember build", 11 | "start": "ember server", 12 | "test": "ember try:testall" 13 | }, 14 | "repository": "", 15 | "engines": { 16 | "node": ">= 0.10.0" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "broccoli-asset-rev": "^2.2.0", 22 | "ember-cli": "^2.5.0", 23 | "ember-cli-app-version": "^1.0.0", 24 | "ember-cli-dependency-checker": "^1.2.0", 25 | "ember-cli-release": "0.2.8", 26 | "ember-cli-sri": "^2.1.0", 27 | "loader.js": "^4.0.0" 28 | }, 29 | "keywords": [ 30 | "ember-addon" 31 | ], 32 | "dependencies": { 33 | "amd-name-resolver": "0.0.5", 34 | "broccoli-babel-transpiler": "^5.5.0", 35 | "broccoli-concat": "^2.2.0", 36 | "broccoli-funnel": "^1.0.1", 37 | "broccoli-merge-trees": "^1.1.1", 38 | "broccoli-plugin": "^1.2.1", 39 | "chalk": "^1.1.3", 40 | "clean-css": "^3.4.12", 41 | "cli-table": "^0.3.1", 42 | "debug": "^2.2.0", 43 | "ember-cli-preprocess-registry": "^3.0.0", 44 | "filesize": "^3.3.0", 45 | "lodash": "^4.11.1", 46 | "rsvp": "^3.2.1", 47 | "symlink-or-copy": "^1.1.3", 48 | "uglify-js": "^2.6.2" 49 | }, 50 | "ember-addon": {} 51 | } 52 | -------------------------------------------------------------------------------- /lib/models/asset-cache.js: -------------------------------------------------------------------------------- 1 | var LibQuantifier = require('./lib-quantifier'); 2 | var debug = require('debug')('asset-sizes:cache'); 3 | 4 | function AssetCache(options) { 5 | this._cache = {}; 6 | 7 | options = options || {}; 8 | this._options = options; 9 | 10 | options.logAssets = options.sizes || options.traceAll; 11 | options.addonToTrace = options.traceAsset; 12 | 13 | this.isActive = options.traceAll || options.logAssets || options.addonToTrace; 14 | } 15 | 16 | AssetCache.prototype.lookup = function lookup(name) { 17 | var cached = this._cache[name]; 18 | 19 | if (!cached) { 20 | debug('generated cache for ' + name); 21 | this._cache[name] = cached = new LibQuantifier(name, this._options); 22 | } 23 | 24 | return cached; 25 | }; 26 | 27 | AssetCache.prototype.forEach = function(callback) { 28 | var cache = this._cache; 29 | 30 | Object.keys(cache).forEach(function(item, index) { 31 | callback(cache[item], index); 32 | }); 33 | }; 34 | 35 | AssetCache.prototype.analyze = function analyze() { 36 | debug('analyze'); 37 | var libs = []; 38 | var finalVendor = this.finalVendor; 39 | 40 | this.forEach(function(lib) { 41 | lib.analyze(finalVendor); 42 | libs.push(lib); 43 | }); 44 | 45 | libs.sort(function compare(a, b) { 46 | if (a.stats.size > b.stats.size) { 47 | return -1; 48 | } 49 | if (a.stats.size < b.stats.size) { 50 | return 1; 51 | } 52 | // a must be equal to b 53 | return 0; 54 | }); 55 | 56 | libs.forEach(function(lib) { 57 | lib.log(); 58 | }); 59 | 60 | }; 61 | 62 | module.exports = AssetCache; 63 | -------------------------------------------------------------------------------- /lib/shim-addon-model.js: -------------------------------------------------------------------------------- 1 | var mergeTrees = require('broccoli-merge-trees'); 2 | var p = require('ember-cli-preprocess-registry/preprocessors'); 3 | var preprocessCss = p.preprocessCss; 4 | var debug = require('debug')('asset-sizes:addon-shim'); 5 | var chalk = require('chalk'); 6 | 7 | module.exports = function shim(model, assetCache) { 8 | 9 | /* 10 | These aren't exactly the babelOptions we were looking for... 11 | 12 | ... but they are close enough for the Imperial Storm Troopers. 13 | */ 14 | 15 | 16 | model.prototype.__babelOptions = function fakeGetBabelOptions() { 17 | debug(chalk.red('WARN: Retrieving stubbed babel options')); 18 | var babelOptions = { babel: {} }; 19 | 20 | if (this._addonInstalled('ember-cli-babel')) { 21 | var amdNameResolver = require('amd-name-resolver').moduleResolve; 22 | babelOptions = { 23 | babel: { 24 | compileModules: true, 25 | modules: 'amdStrict', 26 | moduleIds: true, 27 | resolveModuleSource: amdNameResolver 28 | } 29 | }; 30 | } 31 | 32 | delete babelOptions.babel.compileModules; 33 | delete babelOptions.babel.includePolyfill; 34 | return babelOptions; 35 | }; 36 | 37 | model.prototype._addonInstalled = function(addonName) { 38 | return !!this.registry.availablePlugins[addonName]; 39 | }; 40 | 41 | 42 | 43 | 44 | 45 | model.prototype.compileAddon = function compileAddon(tree) { 46 | this._requireBuildPackages(); 47 | 48 | var addonJs = this.processedAddonJsFiles(tree); 49 | var templatesTree = this.compileTemplates(tree); 50 | var options = this.__babelOptions(); 51 | 52 | if (assetCache.isActive) { 53 | var addonCache = assetCache.lookup(this.name); 54 | 55 | templatesTree = addonCache.observe(templatesTree, 'addon-templates', options); 56 | addonJs = addonCache.observe(addonJs, 'addon-js', options); 57 | } 58 | 59 | var trees = [addonJs, templatesTree].filter(Boolean); 60 | 61 | return mergeTrees(trees, { 62 | annotation: 'Addon#compileAddon(' + this.name + ') ' 63 | }); 64 | }; 65 | 66 | model.prototype.compileStyles = function compileStyles(tree) { 67 | this._requireBuildPackages(); 68 | 69 | if (tree) { 70 | var output = preprocessCss(tree, '/', '/', { 71 | outputPaths: { 'addon': this.name + '.css' }, 72 | registry: this.registry 73 | }); 74 | if (assetCache.isActive) { 75 | return assetCache.lookup(this.name).observe(output, 'addon-css', {}); 76 | } 77 | return output; 78 | } 79 | }; 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | var chalk = require('chalk'); 4 | var analyzePath = require('./lib/helpers/analyze-path'); 5 | var logFile = require('./lib/helpers/log-file-stats'); 6 | var path = require('path'); 7 | var AssetSizesCommand = require('./lib/commands/asset-sizes'); 8 | var shimApp = require('./lib/shim-app'); 9 | var shimAddonModel = require('./lib/shim-addon-model'); 10 | 11 | module.exports = { 12 | name: 'ember-cli-asset-sizes-shim', 13 | 14 | /* 15 | We override default commands to add tracing flags and expose them to the environment 16 | */ 17 | includedCommands: function() { 18 | return { 19 | 'asset-sizes': AssetSizesCommand 20 | } 21 | }, 22 | 23 | 24 | /* 25 | Make sure this returns false before publishing 26 | */ 27 | isDevelopingAddon: function() { 28 | return false; 29 | }, 30 | 31 | 32 | /* 33 | We use output ready to do the analysis so that we have access to the final build as well. 34 | */ 35 | outputReady: function(result) { 36 | var assetCache = this.app.__cacheForAssetStats; 37 | 38 | // bail if the user didn't wan't us to log or trace anything 39 | if (!assetCache.isActive) { 40 | return; 41 | } 42 | 43 | // asset analytics 44 | console.log(chalk.white('\nAsset Analytics') + chalk.grey('\n––––––––––––––––––––––––––––')); 45 | 46 | assetCache._options.root = this.project.root; 47 | assetCache.analyze(); 48 | 49 | if (assetCache._options.logAssets) { 50 | console.log(chalk.white('\nFinal Build Analytics\n==================')); 51 | var logs = []; 52 | 53 | analyzePath(result.directory, result.directory, function(info) { 54 | logs.push(info); 55 | }, { minify: false }); 56 | 57 | logs.sort(function compare(a, b) { 58 | if (a.stats.size > b.stats.size) { 59 | return -1; 60 | } 61 | if (a.stats.size < b.stats.size) { 62 | return 1; 63 | } 64 | // a must be equal to b 65 | return 0; 66 | }); 67 | 68 | logs.forEach(function(log) { 69 | logFile(log); 70 | }); 71 | 72 | } 73 | 74 | }, 75 | 76 | init: function() { 77 | if (this._super && this._super.apply) { 78 | this._super.apply(this, arguments); 79 | } 80 | 81 | var pathToApp = path.join(this.project.root, 'node_modules/ember-cli/lib/broccoli/ember-app'); 82 | var pathToAddonModel = path.join(this.project.root, 'node_modules/ember-cli/lib/models/addon'); 83 | var EmberApp = require(pathToApp); 84 | var Addon = require(pathToAddonModel); 85 | 86 | var assetCache = shimApp(EmberApp); 87 | shimAddonModel(Addon, assetCache); 88 | } 89 | 90 | }; 91 | -------------------------------------------------------------------------------- /lib/helpers/analyze.js: -------------------------------------------------------------------------------- 1 | var chalk = require("chalk"); 2 | var path = require("path"); 3 | var fs = require("fs"); 4 | var zlib = require('zlib'); 5 | var CleanCSS = require('clean-css'); 6 | var uglifyFile = require('./uglify'); 7 | var merge = require('lodash/merge'); 8 | var debug = require('debug')('asset-sizes:analyze'); 9 | 10 | module.exports = function analyzeFile(info, callback, options) { 11 | options = merge({ minify: true, gzip: true }, options || {}); 12 | 13 | debug(chalk.grey(info.src) + chalk.yellow(' ' + JSON.stringify(options))); 14 | var parsed = path.parse(info.src); 15 | 16 | if (parsed.ext === '.map') { 17 | debug(chalk.grey('Skipping analysis for .map file')); 18 | return; 19 | } 20 | 21 | var moduleName = parsed.name; 22 | var dirPath = parsed.dir; 23 | var realDirPath = dirPath.substr(info.root.length + 1); 24 | var realFilePath = path.join(realDirPath, parsed.base); 25 | 26 | var opts = { level: 9 }; 27 | debug('reading file ' + chalk.white(realFilePath) + ' from ' + chalk.cyan(info.src)); 28 | var fileString = fs.readFileSync(info.src, 'utf8'); 29 | var gzipped = options.gzip ? zlib.gzipSync(fileString, opts) : ''; 30 | var uglified = ''; 31 | var both = ''; 32 | 33 | var isJS = parsed.ext === '.js'; 34 | var isCSS = parsed.ext === '.css'; 35 | 36 | if (options.minify) { 37 | var uglifyOptions = { 38 | mangle: true, 39 | compress: true, 40 | sourceMapIncludeSources: false 41 | }; 42 | 43 | if (isJS) { 44 | try { 45 | debug(chalk.white('[Analyze]'), chalk.yellow('Uglifying JS')); 46 | uglified = uglifyFile(info.src, uglifyOptions); 47 | both = zlib.gzipSync(uglified, opts); 48 | } catch (e) { 49 | debug(chalk.red('Uglify Failed, falling back to un-uglified values.')); 50 | uglified = fileString; 51 | both = gzipped 52 | } 53 | } 54 | 55 | if (isCSS) { 56 | debug(chalk.yellow('Minifying CSS')); 57 | uglified = new CleanCSS().minify(fileString).styles; 58 | both = zlib.gzipSync(uglified, opts); 59 | } 60 | } 61 | 62 | var sizes = { 63 | original: info.stats.size, 64 | gzipped: gzipped.length || info.stats.size, 65 | minified: uglified.length || info.stats.size, 66 | both: both.length || gzipped.length || info.stats.size, 67 | final: info.stats.size 68 | }; 69 | 70 | if (options.gzip) { 71 | sizes.final = sizes.gzipped; 72 | } 73 | if (options.minify && (isJS || isCSS)) { 74 | sizes.final = sizes.both; 75 | } 76 | 77 | if (callback) { 78 | callback({ 79 | name: moduleName, 80 | ext: parsed.ext, 81 | path: info.src, 82 | isJS: isJS, 83 | isCSS: isCSS, 84 | realDirPath: realDirPath, 85 | realFilePath: realFilePath, 86 | stats: info.stats, 87 | 88 | sizes: sizes 89 | }); 90 | } 91 | 92 | }; 93 | -------------------------------------------------------------------------------- /lib/models/lib-quantifier.js: -------------------------------------------------------------------------------- 1 | var TreeLogger = require('../plugins/tree-logger'); 2 | var mergeTrees = require('broccoli-merge-trees'); 3 | var Babel = require('broccoli-babel-transpiler'); 4 | var analyzePath = require('../helpers/analyze-path'); 5 | var path = require('path'); 6 | var filesize = require('filesize'); 7 | var logFile = require('../helpers/log-file-stats'); 8 | var chalk = require('chalk'); 9 | var colorForSize = require('../utils/color-for-size'); 10 | var padRight = require('../utils/pad-right'); 11 | var debug = require('debug')('asset-sizes:quantifier'); 12 | 13 | var TranspiledTypes = ['addon-js', 'addon-templates', 'templates', 'app']; 14 | var BannedTypes = ['addon', 'test-support', 'addon-test-support']; 15 | 16 | function shouldAnalyzeTree(tree, type) { 17 | return tree && (BannedTypes.indexOf(type) === -1); 18 | } 19 | 20 | function getHumanSize(size) { 21 | return padRight(filesize(size), 10, ' '); 22 | } 23 | 24 | function reduceAssetType(Type, type) { 25 | return Type.reduce(function(chain, info) { 26 | chain.original += info.sizes.original; 27 | chain.final += info.sizes.final; 28 | 29 | return chain; 30 | }, { original: 0, final: 0, type: type }); 31 | } 32 | 33 | function LibQuantifier(name, options) { 34 | this.name = name; 35 | this.options = options; 36 | this._cache = {}; 37 | } 38 | 39 | LibQuantifier.prototype.analyze = function analyze(finalVendor) { 40 | debug('analyze ' + this.name); 41 | var addonName = this.name; 42 | var cache = this._cache; 43 | var options = this.options; 44 | var infos = []; 45 | 46 | // vendor / imported code (app.import) 47 | if (cache.imports) { 48 | var imports = []; 49 | 50 | cache.imports.forEach(function(item) { 51 | var fullPath = path.join(finalVendor.destPath, item.path); 52 | 53 | debug(chalk.white('looking up `' + item.asset + '` at ' + chalk.grey(fullPath))); 54 | analyzePath(options.root, fullPath, function(info) { 55 | imports.push(info); 56 | }); 57 | }); 58 | 59 | delete cache.imports; 60 | cache.vendor = imports; 61 | } 62 | 63 | Object.keys(cache).forEach(function(type) { 64 | if (cache[type]) { 65 | if (type === 'addon-test-support' || type === 'test-support') { 66 | return; 67 | } 68 | infos.push(reduceAssetType(cache[type], type)); 69 | } 70 | }); 71 | 72 | this.stats = { 73 | name: addonName, 74 | size: infos.reduce(function(value, type) { 75 | return value + type.final; 76 | }, 0), 77 | types: infos, 78 | vendor: cache.vendor, 79 | shouldLog: options.traceAll || options.addonToTrace === addonName 80 | }; 81 | 82 | }; 83 | 84 | LibQuantifier.prototype.log = function log() { 85 | debug('log ' + this.name); 86 | var info = this.stats; 87 | var totalSize = info.size; 88 | 89 | function sortLargestToSmallest(a, b) { 90 | if (a.final > b.final) { 91 | return -1; 92 | } 93 | if (a.final < b.final) { 94 | return 1; 95 | } 96 | // a must be equal to b 97 | return 0; 98 | } 99 | 100 | // sort types largest to smallest 101 | info.types.sort(sortLargestToSmallest); 102 | 103 | // sort vendor imports largest to smallest 104 | if (info.shouldLog && info.vendor) { 105 | info.vendor.sort(sortLargestToSmallest); 106 | } 107 | 108 | console.log( 109 | ' ' + chalk[colorForSize(totalSize)](getHumanSize(totalSize)) + 110 | chalk.cyan(info.name) 111 | ); 112 | 113 | if (info.shouldLog) { 114 | info.types.forEach(function(typeInfo) { 115 | if (typeInfo.type === 'addon-test-support' || typeInfo.type === 'test-support') { 116 | return; 117 | } 118 | 119 | var smallest = typeInfo.final; 120 | 121 | console.log( 122 | ' ' + chalk[colorForSize(smallest)](getHumanSize(smallest)) + 123 | chalk.cyan(padRight(typeInfo.type, 12, ' ')) + chalk.grey( 124 | 'original: ' + getHumanSize(typeInfo.original) + 125 | ' gzip+min: ' + getHumanSize(smallest) 126 | ) 127 | ); 128 | 129 | if (typeInfo.type === 'vendor') { 130 | info.vendor.forEach(function(fileInfo) { 131 | logFile(fileInfo, false, ' '); 132 | }); 133 | } 134 | 135 | }); 136 | } 137 | 138 | 139 | }; 140 | 141 | LibQuantifier.prototype.lookup = function lookup(type) { 142 | var cached = this._cache[type]; 143 | 144 | if (!cached) { 145 | debug('generated cache for ' + type); 146 | this._cache[type] = cached = []; 147 | } 148 | 149 | return cached; 150 | }; 151 | 152 | LibQuantifier.prototype.observe = function observeTree(tree, type, options) { 153 | if (!shouldAnalyzeTree(tree, type)) { 154 | debug('Skipping observation for ' + this.name + ':' + type); 155 | return tree; 156 | } 157 | 158 | debug('Observing ' + this.name + ':' + type); 159 | var cache = this.lookup(type); 160 | var transpiled = tree; 161 | 162 | if (options.babel && TranspiledTypes.indexOf(type) !== -1) { 163 | debug(chalk.yellow('Will Transpile Tree')); 164 | transpiled = new Babel(mergeTrees([tree]), options.babel); 165 | } 166 | 167 | return new TreeLogger([transpiled], { name: type, cache: cache, annotation: 'stats for ' + this.name + ':' + type }); 168 | }; 169 | 170 | module.exports = LibQuantifier; 171 | -------------------------------------------------------------------------------- /lib/shim-app.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var AssetCache = require('./models/asset-cache'); 3 | var Funnel = require('broccoli-funnel'); 4 | var mergeTrees = require('broccoli-merge-trees'); 5 | var p = require('ember-cli-preprocess-registry/preprocessors'); 6 | var preprocessTemplates = p.preprocessTemplates; 7 | 8 | var EOL = require('os').EOL; 9 | var Babel = require('broccoli-babel-transpiler'); 10 | var concat = require('broccoli-concat'); 11 | var merge = require('lodash/merge'); 12 | var path = require('path'); 13 | var debug = require('debug')('asset-sizes:app-shim'); 14 | 15 | function parseArgs() { 16 | debug('parsing args'); 17 | var args = process.argv; 18 | var command = args[2]; 19 | 20 | if (['a', 'asset-sizes'].indexOf(command) === -1) { 21 | return { 22 | traceAsset: false, 23 | assetSizes: false, 24 | traceAll: false 25 | } 26 | } 27 | 28 | var commands = ['--sizes', '--trace', '--trace-all']; 29 | var flagHash = { 30 | '-s': 'sizes', 31 | '-t': 'trace', 32 | '-ta': 'traceAll', 33 | '--sizes': 'sizes', 34 | '--trace': 'trace', 35 | '--trace-all': 'traceAll' 36 | }; 37 | 38 | var options = { 39 | sizes: true, 40 | trace: '', 41 | traceAll: false 42 | }; 43 | 44 | for (var i = 3; i < args.length; i++) { 45 | var arg = args[i]; 46 | var next = args[i + 1]; 47 | 48 | // check if alias 49 | if (flagHash[arg]) { 50 | if (!next || flagHash[next]) { 51 | options[flagHash[arg]] = true; 52 | continue; 53 | } 54 | options[flagHash[arg]] = next; 55 | i++; 56 | continue; 57 | } 58 | 59 | for (var j = 0; j < commands.length; j++) { 60 | var c = commands[j]; 61 | 62 | if (arg.indexOf(c) === 0) { 63 | options[flagHash[c]] = c.substr(c.indexOf('=')); 64 | break; 65 | } 66 | } 67 | 68 | } 69 | 70 | debug('args: ' + chalk.yellow(JSON.stringify(options))); 71 | return options; 72 | } 73 | 74 | module.exports = function shimEmberApp(EmberApp) { 75 | 76 | /* 77 | Assets collected along the way 78 | */ 79 | var assetOptions = parseArgs(); 80 | var assetCache = new AssetCache(assetOptions); 81 | 82 | /* 83 | Expose for us to abuse 84 | */ 85 | EmberApp.prototype.__cacheForAssetStats = assetCache; 86 | 87 | 88 | /* 89 | Collect addon files 90 | */ 91 | EmberApp.prototype.addonTreesFor = function(type) { 92 | var babelOptions = this._prunedBabelOptions(); 93 | 94 | return this.project.addons.map(function(addon) { 95 | if (addon.treeFor) { 96 | var tree = addon.treeFor(type); 97 | 98 | if (assetCache.isActive && type !== 'templates' && type !== 'vendor') { 99 | return assetCache 100 | .lookup(addon.name) 101 | .observe(tree, type, { babel: babelOptions }); 102 | } 103 | 104 | return tree; 105 | } 106 | }).filter(Boolean); 107 | }; 108 | 109 | 110 | 111 | 112 | /* 113 | We override app.import to enable us to know the context in which 114 | a file was imported. 115 | */ 116 | var importToApp = EmberApp.prototype.import; 117 | var includingFromAddon = false; 118 | 119 | EmberApp.prototype.import = function scopedImport(asset, options) { 120 | var importer = includingFromAddon || 'App'; 121 | var assetPath = this._getAssetPath(asset); 122 | 123 | if (assetCache.isActive) { 124 | assetCache.lookup(importer) 125 | .lookup('imports') 126 | .push({ 127 | asset: asset, 128 | path: assetPath, 129 | options: options 130 | }); 131 | } 132 | 133 | return importToApp.call(this, asset, options); 134 | }; 135 | 136 | 137 | /* 138 | We overwrite this to have it notify us of what addon is 139 | currently importing things. 140 | */ 141 | EmberApp.prototype._notifyAddonIncluded = function() { 142 | this.initializeAddons(); 143 | 144 | var addonNames = this.project.addons.map(function(addon) { 145 | return addon.name; 146 | }); 147 | 148 | if (this.options && this.options.addons && this.options.addons.blacklist) { 149 | this.options.addons.blacklist.forEach(function(addonName) { 150 | if (addonNames.indexOf(addonName) === -1) { 151 | throw new Error('Addon "' + addonName + '" defined in blacklist is not found'); 152 | } 153 | }); 154 | } 155 | 156 | if (this.options && this.options.addons && this.options.addons.whitelist) { 157 | this.options.addons.whitelist.forEach(function(addonName) { 158 | if (addonNames.indexOf(addonName) === -1) { 159 | throw new Error('Addon "' + addonName + '" defined in whitelist is not found'); 160 | } 161 | }); 162 | } 163 | 164 | this.project.addons = this.project.addons.filter(function(addon) { 165 | addon.app = this; 166 | 167 | if (!this.shouldIncludeAddon || this.shouldIncludeAddon(addon)) { 168 | if (addon.included) { 169 | includingFromAddon = addon.name; 170 | addon.included(this); 171 | includingFromAddon = false; 172 | } 173 | 174 | return addon; 175 | } 176 | }, this); 177 | }; 178 | 179 | 180 | 181 | /* 182 | We overwrite this to be able to individually load and pre-process templates. 183 | */ 184 | var cachedProcessTemplatesTree = EmberApp.prototype._processedTemplatesTree; 185 | EmberApp.prototype._processedTemplatesTree = function() { 186 | if (assetCache.isActive) { 187 | cachedProcessTemplatesTree.call(this); 188 | } 189 | 190 | var _context = this; 191 | var babelOptions = this._prunedBabelOptions(); 192 | 193 | function processTree(tree) { 194 | var templates = _context.addonPreprocessTree('template', tree); 195 | 196 | return _context.addonPostprocessTree('template', preprocessTemplates(templates, { 197 | registry: _context.registry, 198 | annotation: 'postprocessTree(templates)' 199 | })); 200 | } 201 | 202 | var addonTrees = this.project.addons.map(function(addon) { 203 | if (addon.treeFor) { 204 | var tree = addon.treeFor('templates'); 205 | 206 | if (!tree) { 207 | return tree; 208 | } 209 | 210 | return assetCache 211 | .lookup(addon.name) 212 | .observe(processTree(tree), 'templates', { babel: babelOptions }); 213 | } 214 | }).filter(Boolean); 215 | 216 | var mergedTemplates = mergeTrees(addonTrees, { 217 | overwrite: true, 218 | annotation: 'TreeMerger (templates)' 219 | }); 220 | 221 | var addonTemplates = new Funnel(mergedTemplates, { 222 | srcDir: '/', 223 | destDir: this.name + '/templates', 224 | annotation: 'ProcessedTemplateTree' 225 | }); 226 | 227 | var appTemplates = assetCache 228 | .lookup('App') 229 | .observe(processTree(this._templatesTree()), 'templates', { babel: babelOptions }); 230 | 231 | return mergeTrees([ 232 | addonTemplates, 233 | appTemplates 234 | ], { 235 | annotation: 'TreeMerger (pod & standard templates)', 236 | overwrite: true 237 | }); 238 | 239 | }; 240 | 241 | 242 | 243 | 244 | /* 245 | We do this to gain access to the final vendor tree 246 | */ 247 | EmberApp.prototype.javascript = function hookedJavascriptTree() { 248 | var deprecate = this.project.ui.writeDeprecateLine.bind(this.project.ui); 249 | var applicationJs = this.appAndDependencies(); 250 | var appOutputPath = this.options.outputPaths.app.js; 251 | var appJs = applicationJs; 252 | 253 | // Note: If ember-cli-babel is installed we have already performed the transpilation at this point 254 | if (!this._addonInstalled('ember-cli-babel')) { 255 | appJs = new Babel( 256 | new Funnel(applicationJs, { 257 | include: [escapeRegExp(this.name + '/') + '**/*.js'], 258 | annotation: 'Funnel: App JS Files' 259 | }), 260 | merge(this._prunedBabelOptions()) 261 | ); 262 | } 263 | 264 | appJs = mergeTrees([ 265 | appJs, 266 | this._processedEmberCLITree() 267 | ], { 268 | annotation: 'TreeMerger (appJS & processedEmberCLITree)', 269 | overwrite: true 270 | }); 271 | 272 | appJs = this.concatFiles(appJs, { 273 | inputFiles: [this.name + '/**/*.js'], 274 | headerFiles: [ 275 | 'vendor/ember-cli/app-prefix.js' 276 | ], 277 | footerFiles: [ 278 | 'vendor/ember-cli/app-suffix.js', 279 | 'vendor/ember-cli/app-config.js', 280 | 'vendor/ember-cli/app-boot.js' 281 | ], 282 | outputFile: appOutputPath, 283 | annotation: 'Concat: App' 284 | }); 285 | 286 | if (this.legacyFilesToAppend.length > 0) { 287 | deprecate('Usage of EmberApp.legacyFilesToAppend is deprecated. Please use EmberApp.import instead for the following files: \'' + this.legacyFilesToAppend.join('\', \'') + '\''); 288 | this.legacyFilesToAppend.forEach(function(legacyFile) { 289 | this.import(legacyFile); 290 | }.bind(this)); 291 | } 292 | 293 | this.import('vendor/ember-cli/vendor-prefix.js', {prepend: true}); 294 | this.import('vendor/addons.js'); 295 | this.import('vendor/ember-cli/vendor-suffix.js'); 296 | 297 | // this is the hook, we stash the Funnel so we can use it's output later 298 | // to calculate individual vendor import sizes 299 | var finalVendor = applicationJs; 300 | if (assetCache.isActive) { 301 | finalVendor = new Funnel(applicationJs, {}); 302 | assetCache.finalVendor = finalVendor; 303 | } 304 | 305 | var vendorFiles = []; 306 | for (var outputFile in this._scriptOutputFiles) { 307 | var inputFiles = this._scriptOutputFiles[outputFile]; 308 | 309 | vendorFiles.push( 310 | this.concatFiles(finalVendor, { 311 | inputFiles: inputFiles, 312 | outputFile: outputFile, 313 | separator: EOL + ';', 314 | annotation: 'Concat: Vendor ' + outputFile 315 | }) 316 | ); 317 | } 318 | 319 | return mergeTrees(vendorFiles.concat(appJs), { 320 | annotation: 'TreeMerger (vendor & appJS)' 321 | }); 322 | }; 323 | 324 | 325 | 326 | /* 327 | We return this in case we need to do anything else special like it 328 | (Hint: like pass it into the model shim) 329 | */ 330 | 331 | return assetCache; 332 | 333 | }; 334 | --------------------------------------------------------------------------------