├── .jshintignore ├── time-logger ├── time-logger-style.css ├── time-entry-form-style.css ├── time-logger.html ├── time-entries.html ├── time-logger-nav.html ├── time-entries-style.css ├── time-logger-nav.directive.js ├── v2 │ ├── time-entry-smart.html │ ├── time-entry-smart-style.css │ └── time-entry-smart.directive.js ├── moment.filter.js ├── time-logger.directive.js ├── time-entry-form.html ├── time-logger-nav-style.css ├── app.js ├── app-2.js ├── time-entries.directive.js ├── time-entry-form.directive.js └── timeLogger.service.js ├── .gitignore ├── time-logger-app.js ├── .editorconfig ├── time-logger-app-2.js ├── .jshintrc ├── index.html ├── index-2.html ├── webpack.config.js ├── package.json └── README.md /.jshintignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | node_modules/** 3 | -------------------------------------------------------------------------------- /time-logger/time-logger-style.css: -------------------------------------------------------------------------------- 1 | .time-logger { 2 | width: 960px; 3 | margin: auto; 4 | } 5 | 6 | .time-logger__title { 7 | margin:0; 8 | background: #bada55; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .tmp/ 3 | node_modules/ 4 | npm-debug.log 5 | common.js 6 | common.js.map 7 | time-logger.js 8 | time-logger.js.map 9 | time-logger-2.js 10 | time-logger-2.js.map 11 | *.iml 12 | -------------------------------------------------------------------------------- /time-logger-app.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 'use strict'; 3 | var angular = require('angular'); 4 | var appElement = document.querySelector('body'); 5 | angular.bootstrap(appElement, [ 6 | require('./time-logger/app').name 7 | ], { strictDi: true }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /time-logger-app-2.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 'use strict'; 3 | var angular = require('angular'); 4 | var appElement = document.querySelector('body'); 5 | angular.bootstrap(appElement, [ 6 | require('./time-logger/app-2').name 7 | ], { strictDi: true }); 8 | -------------------------------------------------------------------------------- /time-logger/time-entry-form-style.css: -------------------------------------------------------------------------------- 1 | .time-entry-form { 2 | background: #bada55; 3 | padding: 1em; 4 | } 5 | 6 | .time-entry-form__time, .time-entry-form__category, .time-entry-form__description, .time-entry-form__submit { 7 | font-size: 1.5em; 8 | } 9 | -------------------------------------------------------------------------------- /time-logger/time-logger.html: -------------------------------------------------------------------------------- 1 |
2 |

Time Logger

3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /time-logger/time-entries.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /time-logger/time-logger-nav.html: -------------------------------------------------------------------------------- 1 |
2 |

{{date | moment : 'MMMM Do, YYYY'}}

3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /time-logger/time-entries-style.css: -------------------------------------------------------------------------------- 1 | .time-logger-entries { 2 | list-style: none; 3 | } 4 | 5 | .time-logger-entries li { 6 | font-size: 1.5em; 7 | line-height: 1.5; 8 | margin: 0.3em auto; 9 | } 10 | 11 | .time-logger-entries .label { 12 | color: white; 13 | background-color: #777; 14 | border: solid 1px transparent; 15 | border-radius: 0.2em; 16 | padding: 0.2em; 17 | } 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true, 13 | "devel": true, 14 | "validthis": true, 15 | "expr": false, 16 | "esnext": true, 17 | "unused": true, 18 | "globals": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /time-logger/time-logger-nav.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./time-logger-nav-style.css'); 3 | function TimeLoggerNavDirective() { 4 | return { 5 | restrict: 'E', 6 | template: require('./time-logger-nav.html'), 7 | scope: { 8 | date: '=' 9 | } 10 | }; 11 | } 12 | 13 | module.exports = function(ngModule) { 14 | ngModule.directive('timeLoggerNav', TimeLoggerNavDirective); 15 | }; 16 | -------------------------------------------------------------------------------- /time-logger/v2/time-entry-smart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
hh:mm? #category? description
5 |
6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Time Logger 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Time Logger 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /time-logger/v2/time-entry-smart-style.css: -------------------------------------------------------------------------------- 1 | .time-entry-smart { 2 | background: #bada55; 3 | padding: 1em; 4 | } 5 | 6 | .time-entry-smart__entry { 7 | font-size: 1.5em; 8 | } 9 | 10 | .time-entry-smart__entry.ng-valid { 11 | border-color: green; 12 | } 13 | 14 | .time-entry-smart .valid:after { 15 | content: "\1F44D"; 16 | } 17 | 18 | .time-entry-smart__entry.ng-invalid { 19 | border-color: red; 20 | } 21 | 22 | .time-entry-smart .invalid:after { 23 | content: "\1F44E"; 24 | } 25 | -------------------------------------------------------------------------------- /time-logger/moment.filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var moment = require('moment'); 3 | function MomentFilter() { 4 | return function(value, format) { 5 | // TODO add a default format 6 | if (moment.isMoment(value)) { 7 | return value.format(format); 8 | } else if (moment.isDate(value)) { 9 | return moment(value).format(format); 10 | } else { 11 | return ''; 12 | } 13 | }; 14 | } 15 | 16 | module.exports = function(ngModule) { 17 | ngModule.filter('moment', MomentFilter); 18 | }; 19 | -------------------------------------------------------------------------------- /time-logger/time-logger.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./time-logger-style.css'); 3 | var moment = require('moment'); 4 | function TimeLoggerDirective() { 5 | return { 6 | restrict: 'E', 7 | template: require('./time-logger.html'), 8 | controllerAs: 'vm', 9 | /* @ngInject */ 10 | controller: function($routeParams) { 11 | var vm = this; 12 | vm.selectedDate = moment($routeParams.date, 'YYYY-MM-DD'); 13 | } 14 | }; 15 | } 16 | 17 | module.exports = function(ngModule) { 18 | ngModule.directive('timeLogger', TimeLoggerDirective); 19 | }; 20 | -------------------------------------------------------------------------------- /time-logger/time-entry-form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /time-logger/time-logger-nav-style.css: -------------------------------------------------------------------------------- 1 | .time-logger-nav { 2 | font-size: 2em; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | 8 | .time-logger-nav .prev, .time-logger-nav .next { 9 | text-decoration: none; 10 | color: black; 11 | border: solid 1px black; 12 | border-radius: 5px; 13 | padding: 4px; 14 | background-color: #eee; 15 | } 16 | 17 | .time-logger-nav .prev:hover, .time-logger-nav .next:hover { 18 | border-width: 2px; 19 | padding: 3px; 20 | } 21 | 22 | .time-logger-nav .prev { 23 | order: 1; 24 | } 25 | 26 | .time-logger-nav .date { 27 | order: 2; 28 | margin: 10px; 29 | } 30 | 31 | .time-logger-nav .next { 32 | order: 3; 33 | } 34 | -------------------------------------------------------------------------------- /time-logger/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var angular = require('angular'); 3 | var moment = require('moment'); 4 | var ngModule = angular.module('time-logger', [require('angular-route')]); 5 | 6 | ngModule.config(function($routeProvider) { 7 | $routeProvider.when('/:date', { 8 | template: '' 9 | }); 10 | $routeProvider.otherwise('/' + moment().format('YYYY-MM-DD')); 11 | }); 12 | 13 | require('./moment.filter')(ngModule); 14 | require('./timeLogger.service')(ngModule); 15 | 16 | require('./time-logger.directive')(ngModule); 17 | require('./time-logger-nav.directive')(ngModule); 18 | require('./time-entries.directive')(ngModule); 19 | 20 | require('./time-entry-form.directive')(ngModule); 21 | 22 | module.exports = ngModule; 23 | -------------------------------------------------------------------------------- /time-logger/app-2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var angular = require('angular'); 3 | var moment = require('moment'); 4 | var ngModule = angular.module('time-logger', [require('angular-route')]); 5 | 6 | ngModule.config(function($routeProvider) { 7 | $routeProvider.when('/:date', { 8 | template: '' 9 | }); 10 | $routeProvider.otherwise('/' + moment().format('YYYY-MM-DD')); 11 | }); 12 | 13 | require('./moment.filter')(ngModule); 14 | require('./timeLogger.service')(ngModule); 15 | 16 | require('./time-logger.directive')(ngModule); 17 | require('./time-logger-nav.directive')(ngModule); 18 | require('./time-entries.directive')(ngModule); 19 | 20 | require('./v2/time-entry-smart.directive')(ngModule); 21 | 22 | module.exports = ngModule; 23 | -------------------------------------------------------------------------------- /time-logger/time-entries.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./time-entries-style.css'); 3 | function TimeEntriesDirective(timeLogger) { 4 | return { 5 | restrict: 'E', 6 | scope: { 7 | date: '=' 8 | }, 9 | template: require('./time-entries.html'), 10 | bindToController: true, 11 | controllerAs: 'vm', 12 | /* @ngInject */ 13 | controller: function($rootScope) { 14 | var vm = this; 15 | vm.entries = []; 16 | function refreshList() { 17 | timeLogger.getAllForDate(vm.date) 18 | .then(function(entries) { 19 | vm.entries = entries; 20 | }); 21 | } 22 | 23 | refreshList(); 24 | 25 | $rootScope.$on('newEntry', function() { 26 | refreshList(); 27 | }); 28 | } 29 | }; 30 | } 31 | 32 | module.exports = function(ngModule) { 33 | ngModule.directive('timeEntries', TimeEntriesDirective); 34 | }; 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | context: path.join(__dirname), 7 | entry: { 8 | 'time-logger': './time-logger-app.js', 9 | 'time-logger-2': './time-logger-app-2.js' 10 | }, 11 | output: { 12 | path: path.join(__dirname), 13 | filename: '[name].js' 14 | }, 15 | devtool: 'source-map', 16 | module: { 17 | preLoaders: [ 18 | { test: /\.js$/, exclude: /node_modules/, loader: 'jshint-loader' } 19 | ], 20 | loaders: [ 21 | {test: /\.js$/, loader: 'ng-annotate'}, 22 | {test: /\.css$/, loaders: ['style', 'css']}, 23 | {test: /\.html$/, loader: 'raw'} 24 | ] 25 | }, 26 | plugins: [ 27 | new webpack.optimize.CommonsChunkPlugin('common.js') 28 | ], 29 | externals: { 30 | angular: true, 31 | 'angular-route': '"ngRoute"', 32 | 'moment': true 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /time-logger/time-entry-form.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./time-entry-form-style.css'); 3 | function TimeEntryFormDirective() { 4 | return { 5 | restrict: 'E', 6 | scope: { 7 | date: '=' 8 | }, 9 | template: require('./time-entry-form.html'), 10 | bindToController: true, 11 | controllerAs: 'vm', 12 | /* @ngInject */ 13 | controller: function(timeLogger, momentFilter, $rootScope) { 14 | var vm = this; 15 | function createBlankEntry() { 16 | return { 17 | date: vm.date.format('YYYY-MM-DD'), 18 | time: momentFilter(new Date(), 'H:mm'), 19 | category: '', 20 | description: '' 21 | }; 22 | } 23 | vm.newEntry = createBlankEntry(); 24 | vm.addTimeEntry = function() { 25 | timeLogger.addTimeEntry(vm.newEntry) 26 | .then(function(newEntry) { 27 | $rootScope.$broadcast('newEntry', newEntry); 28 | vm.newEntry = createBlankEntry(); 29 | }); 30 | }; 31 | } 32 | }; 33 | } 34 | 35 | module.exports = function(ngModule) { 36 | ngModule.directive('timeEntryForm', TimeEntryFormDirective); 37 | }; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-time-logger", 3 | "version": "0.0.0", 4 | "description": "A sample application used to understand Webpack", 5 | "homepage": "https://github.com/Workfront/webpack-time-logger", 6 | "repository": { 7 | "type": "git+https", 8 | "url": "https://github.com/Workfront/webpack-time-logger.git" 9 | }, 10 | "private": true, 11 | "scripts": { 12 | "clean": "rimraf time-logger.js time-logger.js.map time-logger-2.js time-logger-2.js.map common.js common.js.map", 13 | "start": "webpack && http-server -p 8080", 14 | "watch": "webpack-dev-server --hot --inline --config webpack.config.js" 15 | }, 16 | "author": "Jeremy Lund ", 17 | "license": "Apache-2.0", 18 | "devDependencies": { 19 | "css-loader": "0.23.0", 20 | "http-server": "0.8.5", 21 | "jshint": "^2.8.0", 22 | "jshint-loader": "^0.8.3", 23 | "ng-annotate-loader": "0.0.10", 24 | "raw-loader": "0.5.1", 25 | "rimraf": "2.4.4", 26 | "style-loader": "0.13.0", 27 | "uuid": "2.0.1", 28 | "webpack": "1.12.9", 29 | "webpack-dev-server": "1.14.0" 30 | }, 31 | "dependencies": { 32 | "angular": "1.3.19", 33 | "angular-route": "1.3.19", 34 | "moment": "2.10.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /time-logger/v2/time-entry-smart.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./time-entry-smart-style.css'); 3 | function TimeEntrySmartDirective() { 4 | return { 5 | restrict: 'E', 6 | scope: { 7 | date: '=' 8 | }, 9 | template: require('./time-entry-smart.html'), 10 | bindToController: true, 11 | controllerAs: 'vm', 12 | /* @ngInject */ 13 | controller: function(timeLogger, momentFilter, $rootScope) { 14 | var vm = this; 15 | vm.entryPattern = /^(((1?[0-9]|2[0-3]):[0-5][0-9])\s+)?(#(.*?)\s+)?(.*?)\s*$/; 16 | vm.entryText = ''; 17 | vm.addTimeEntry = function() { 18 | var matches = vm.entryPattern.exec(vm.entryText); 19 | var timeEntry = { 20 | date: vm.date.format('YYYY-MM-DD'), 21 | time: matches[2] || momentFilter(new Date(), 'H:mm'), 22 | category: matches[5], 23 | description: matches[6] 24 | }; 25 | timeLogger.addTimeEntry(timeEntry) 26 | .then(function(newEntry) { 27 | $rootScope.$broadcast('newEntry', newEntry); 28 | vm.entryText = ''; 29 | }); 30 | }; 31 | } 32 | }; 33 | } 34 | 35 | module.exports = function(ngModule) { 36 | // NOTE: named the same as the old directive since we're using it in the same place 37 | ngModule.directive('timeEntryForm', TimeEntrySmartDirective); 38 | }; 39 | -------------------------------------------------------------------------------- /time-logger/timeLogger.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var angular = require('angular'); 3 | var moment = require('moment'); 4 | var uuid = require('uuid'); 5 | function TimeLoggerService($q, $window) { 6 | var localStorage = $window.localStorage; 7 | var listKey = '$TIME-ENTRIES$'; 8 | 9 | function load() { 10 | return localStorage.getItem(listKey) ? angular.fromJson(localStorage.getItem(listKey)) : []; 11 | } 12 | 13 | function persist(list) { 14 | localStorage.setItem(listKey, angular.toJson(list)); 15 | return list; 16 | } 17 | 18 | function getAll() { 19 | return $q.when(load()); 20 | } 21 | 22 | function addTimeEntry(entry) { 23 | var entryWithId = angular.copy(entry); 24 | entryWithId.id = uuid.v4(); 25 | return getAll() 26 | .then(function (list) { 27 | list.push(entryWithId); 28 | persist(list); 29 | return entryWithId; 30 | }); 31 | } 32 | 33 | function getAllForDate(date) { 34 | return getAll() 35 | .then(function(list) { 36 | var results = list.filter(function(entry) { 37 | return moment(entry.date, 'YYYY-MM-DD').isSame(date); 38 | }); 39 | results.sort(function(entry1, entry2) { 40 | var time1 = moment(entry1.time, 'hh:mm'); 41 | var time2 = moment(entry2.time, 'hh:mm'); 42 | if (time1.isBefore(time2)) { 43 | return -1; 44 | } else if (time2.isBefore(time1)) { 45 | return 1; 46 | } else { 47 | return 0; 48 | } 49 | }); 50 | return results; 51 | }); 52 | } 53 | 54 | return { 55 | getAll: getAll, 56 | getAllForDate: getAllForDate, 57 | addTimeEntry: addTimeEntry 58 | }; 59 | } 60 | 61 | module.exports = function (ngModule) { 62 | ngModule.factory('timeLogger', TimeLoggerService); 63 | }; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-time-logger 2 | A sample application used to understand Webpack. 3 | 4 | ## Quick Start 5 | 6 | * Checkout this repository 7 | * run `npm install` 8 | 9 | ## Work through the tutorial 10 | 11 | The following sections walk through a tutorial using Webpack to build, enhance, and fix issues common in web application development. Each step builds on the previous steps, so you should work through them in order. To start the tutorial, simple checkout the **tutorial-start** tag. NOTE: once you complete the first section, you will have a working application to play with. If you want to start working at a particular step, simply checkout the tag associated with that step (listed in the section title), and run `npm install` to make sure that you have all of the dependencies for that step. The final tag is **tutorial-end**. 12 | 13 | ### Making our Angular code minification-safe. (tag: **tutorial-start** or **minsafe**) 14 | 15 | From a terminal run `npm run watch`. Open a browser to `http://localhost:8080/`. Nothing should show up. 16 | 17 | Open up your browser's developer tools. Observe that there is the following error in the browser console: 18 | 19 | > Uncaught Error: [$injector:modulerr] Failed to instantiate module time-logger due to: 20 | Error: [$injector:strictdi] function($routeProvider) is not using explicit annotation and cannot be invoked in strict mode 21 | 22 | This error has occured because we have not made our Angular code minification safe by adding the appropriate Angular annotations. 23 | Webpack can perform this responsibility through the use of a Webpack loader: 24 | 25 | * In a terminal, type `npm i ng-annotate-loader -DE`. This will install the necessary Webpack loader. 26 | * In `webpack.config.js` make the following changes 27 | 28 | ```js 29 | var path = require('path'); 30 | 31 | // ... 32 | module: { 33 | loaders: [ 34 | // Add this line 35 | {test: /\.js$/, loader: 'ng-annotate'}, 36 | {test: /\.css$/, loaders: ['style', 'css']}, 37 | {test: /\.html$/, loader: 'raw'} 38 | ] 39 | } 40 | ``` 41 | 42 | * Save the file, then run `npm run watch`. Now reload the browser, and you'll see the running application. 43 | 44 | NOTE: An earlier version of this tutorial recommended using `ng-annotate-webpack-plugin`. 45 | I now recommend `ng-annotate-loader` instead as it handles generated source maps correctly. 46 | 47 | ### Add Linting (tag: **linting**) 48 | 49 | Webpack can execute JSHint with every file change using the Webpack loader that runs as a pre-loader. 50 | 51 | * In a terminal, type `npm i jshint-loader jshint@2.8.0 -D`. 52 | * In `webpack.config.js`, add a preloaders section preceding the loaders section, like so: 53 | 54 | ```js 55 | module: { 56 | // Add this preLoaders section 57 | preLoaders: [ 58 | { test: /\.js$/, exclude: /node_modules/, loader: 'jshint-loader' } 59 | ], 60 | loaders: [ 61 | // ... some loaders 62 | ] 63 | } 64 | ``` 65 | 66 | * Save the file, then run `npm run watch`. Observe that it finds a linting error, a missing semicolon, in `timeLogger.service.js`, line 51. 67 | * Go to the file with the error, add the necessary semicolon, and then run `npm run watch` again. Observe that there are no more linting errors! 68 | 69 | ### Embed HTML Partials in Bundle (tag: **html-partial**) 70 | 71 | * In the browser dev tools, observe that your browser makes two requests: one for `time-logger.js` (the bundle), and one for `time-logger-nav.html` (NOTE: you may need to reload the browser page to see the two files). `time-logger-nav.html` is the HTML partial for the navigation bar at the top of the application. Since it's a necessary part of the application, we would like to include it in the bundle. 72 | 73 | * Open `time-logger/time-logger-nav.directive.js` in your editor. Make the following change: 74 | 75 | ```js 76 | function TimeLoggerNavDirective() { 77 | return { 78 | restrict: 'E', 79 | //templateUrl: 'time-logger/time-logger-nav.html', 80 | template: require('./time-logger-nav.html'), // require the HTML into the bundle. 81 | scope: { 82 | date: '=' 83 | } 84 | }; 85 | } 86 | ``` 87 | 88 | * The browser window should have reloaded when you made this change, and you should only see the request for `time-logger.js` in the browser dev tools. 89 | 90 | #### Why Did this Work? 91 | 92 | * Open `webpack.config.js`. Notice that we have the following loader rule: 93 | 94 | ```js 95 | loaders: [ 96 | // ... other loaders 97 | {test: /\.html$/, loader: 'raw'} 98 | ] 99 | ``` 100 | 101 | This tells Webpack to load HTML files using the raw loader. The raw loader simply loads the file contents and returns it as a string. 102 | 103 | ### Adding Multiple Entry Points (tag: **multiple-entries**) 104 | 105 | Our client has asked for a new feature. Instead of having and entry form with three fields at the bottom of the time logger, they would also like a page where people can enter data for all three fields using a simple input field. If they enter no time field, then it will default to the current time (rounded to the minute). If they add some text that includes the hash "#" tag, that will be a category. Finally, the rest of the input will be the description. 106 | 107 | #### What Do We Already Have? 108 | 109 | We already have the code to implement this feature in our source code. The entry point for the new feature is found in `time-logger-app-2.js`, pointing to the new version of the application (`app-2.js`). `app-2.js` replaces the reference to the current Angular entry form directive (`time-entry-form.directive.js`) with the "smart" version (`v2/time-entry-smart.directive.js`). 110 | 111 | #### What Do We Need to Do? 112 | 113 | We simply need to tell Webpack to build the new entry point in addition to the current entry point. Change the entry and output sections in `webpack.config.js` to look like the following: 114 | 115 | ```js 116 | entry: { 117 | 'time-logger': './time-logger-app.js', 118 | 'time-logger-2': './time-logger-app-2.js' 119 | }, 120 | output: { 121 | path: path.join(__dirname), 122 | filename: '[name].js' 123 | }, 124 | ``` 125 | 126 | We have told Webpack that we have two entry points: `./time-logger-app.js` and `./time-logger-app-2.js`, and have named the bundles `time-logger` and `time-logger-2`, respectively. We are using one of Webpack's metadata properties, `[name]` to tell Webpack to use the bundle name property as the filename. So, we end up with two bundle output files: `time-logger.js` and `time-logger-2.js`. 127 | 128 | * Run `npm run watch`, and refresh your browser. The app should work as before. 129 | * Navigate to `http://localhost:8080/index-2.html`. You should see the same time logger with the "smart" entry form. 130 | 131 | ### Optimize the Application Using CommonsChunkPlugin (tag: **commons-chunk**) 132 | 133 | * In the browser dev tools, look for the `time-logger.js` or `time-logger-2.js` file request. Notice that the size of the file is about 1.4MB. This file is too big, especially when both versions of the applications share mostly the same code. Let's combine the common code into a separate bundle. 134 | * Before we make the changes, stop the webpack dev server and run `webpack` in the terminal. Observe how long it takes to run the webpack, and look at the files sizes. `time-logger-1.js` and `time-logger-2.js` should both be the same size, which is around 1.4MB. 135 | * In `webpack.config.js`, make the following changes: 136 | 137 | ```js 138 | var path = require('path'); 139 | // Add the next line. 140 | var webpack = require('webpack'); 141 | 142 | // ... 143 | 144 | plugins: [ 145 | // ... other plugin definitions 146 | // Add this line. 147 | new webpack.optimize.CommonsChunkPlugin('common.js') 148 | ] 149 | ``` 150 | 151 | This will look through all of our entry points and combine any common code into a bundle called `common.js`. 152 | 153 | Of course, we also need to tell the web page to load this file along with the app-specific bundle. This can be done a number of ways, but we're going to do by just adding a script tag to the HTML pages. 154 | 155 | ```html 156 | 157 | 158 | 159 | ``` 160 | 161 | ```html 162 | 163 | 164 | 165 | ``` 166 | 167 | * Run `webpack` again. Notice that the build is not only slightly faster, but look at the file sizes. Since `time-logger-1.js` and `time-logger-2.js` contain the code unique to each app, they are very small. There is a new file now, `common.js`, that contains the bulk of the code. We made this optimization without changing a single line of application code! 168 | * Run `npm run watch`, then refresh the browser window. In the browser dev tools, you should now see two requests: one for `common.js` and one for the time-logger app file (`time-logger-1.js` or `time-logger-2.js`, depending on which page you're on). 169 | 170 | ### Optimize the Application Using Externals (tag: **externals**) 171 | 172 | In the last section, we improved reduced the amount of code that the application may need to load by combining common code into a single file. But, `common.js` is still pretty big (around 1.4MB). In this section, we'll look at reducing the code size through externals. In Webpack, externals are dependencies that are needed by your bundle, but will be provided to the bundle, rather than baked into the bundle. 173 | 174 | Moment, Angular, and Angular-Route are dependencies that are not unique to our bundles, and our application may benefit from loading them from a CDN or some other source. 175 | 176 | * In `webpack.config.js`, add an `externals` section with the following changes: 177 | 178 | ```js 179 | plugins: [ 180 | // ... plugin definitions 181 | ], 182 | externals: { 183 | angular: true, 184 | 'angular-route': '"ngRoute"', 185 | 'moment': true 186 | } 187 | ``` 188 | 189 | We are telling Webpack to not include angular, angular-route, and moment in the bundle, and we are also telling Webpack how to access the externals. `angular: true` tells Webpack that the dependency is accessible on the global scope using the same name. So, it will access angular using the value `window.angular`. The same is true for moment. 190 | 191 | The angular-route dependency is a little different. Angular-route normally returns the module name as the dependency, so that's what we're returning here. 192 | 193 | * Since we told Webpack that we'd provide angular, angular-route, and moment, add them to the HTML pages: 194 | 195 | ```html 196 | 197 | 198 | 199 | 200 | 201 | 202 | ``` 203 | 204 | ```html 205 | 206 | 207 | 208 | 209 | 210 | 211 | ``` 212 | 213 | * Run `webpack`. Notice how fast Webpack runs now (because it doesn't need to process angular, angular-route, or moment), and how much smaller the files are. 214 | * Run `webpack -p`. The `-p` switch runs UglifyJS on the files after building them, which results in much smaller files. Look at the file sizes! NOTE: you might need to scroll back up a little bit to see the results. UglifyJS like to output a lot of junk. Just ignore it. 215 | * Run `npm run watch` and refresh the browser to make sure your application still works! 216 | 217 | --------------------------------------------------------------------------------