From f0ffd1f8b1ec28b64b4dadbd01152537ba1e2509 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Mon, 18 Dec 2017 04:47:40 +0000 Subject: [PATCH 01/13] docs(guides): add pwa guide (#1737) Add `progressive-web-application.md` guide which documents how to use the `workbox-webpack-plugin` to build an offline app. More on PWAs in webpack can be added here by future contributors. Resolves #1145 --- .../guides/progressive-web-application.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/content/guides/progressive-web-application.md diff --git a/src/content/guides/progressive-web-application.md b/src/content/guides/progressive-web-application.md new file mode 100644 index 000000000000..5fba61feaa87 --- /dev/null +++ b/src/content/guides/progressive-web-application.md @@ -0,0 +1,157 @@ +--- +title: Progressive Web Application +sort: 14 +contributors: + - johnnyreilly +--- + +T> This guide extends on code examples found in the [Output Management](/guides/output-management) guide. + +Progressive Web Applications (or PWAs) are web apps that deliver an experience similar to native applications. There are many things that can contribute to that. Of these, the most significant is the ability for an app to be able to function when __offline__. This is achieved through the use of a web technology called [Service Workers](https://developers.google.com/web/fundamentals/primers/service-workers/). + +This section will focus on adding an offline experience to our app. We'll achieve this using a Google project called [Workbox](https://github.com/GoogleChrome/workbox) which provides tools that help make offline support for web apps easier to setup. + + +## We Don't Work Offline Now + +So far, we've been viewing the output by going directly to the local file system. Typically though, a real user accesses a web app over a network; their browser talking to a __server__ which will serve up the required assets (e.g. `.html`, `.js`, and `.css` files). + +So let's test what the current experience is like using a simple server. Let's use the [http-server](https://www.npmjs.com/package/http-server) package: `npm install http-server --save-dev`. We'll also amend the `scripts` section of our `package.json` to add in a `start` script: + +__package.json__ + +``` diff +{ + ... + "scripts": { +- "build": "webpack" ++ "build": "webpack", ++ "start": "http-server dist" + }, + ... +} +``` + +If you haven't previously done so, run the command `npm run build` to build your project. Then run the command `npm start`. This should produce the following output: + +``` bash +> http-server dist + +Starting up http-server, serving dist +Available on: + http://xx.x.x.x:8080 + http://127.0.0.1:8080 + http://xxx.xxx.x.x:8080 +Hit CTRL-C to stop the server +``` + +If you open your browser to `http://localhost:8080` (i.e. `http://127.0.0.1`) you should see your webpack application being served up from the `dist` directory. If you stop the server and refresh, the webpack application is no longer available. + +This is what we aim to change. Once we reach the end of this module we should be able to stop the server, hit refresh and still see our application. + + +## Adding Workbox + +Let's add the Workbox webpack plugin and adjust the `webpack.config.js` file: + +``` bash +npm install workbox-webpack-plugin --save-dev +``` + +__webpack.config.js__ + +``` diff + const path = require('path'); + const HtmlWebpackPlugin = require('html-webpack-plugin'); + const CleanWebpackPlugin = require('clean-webpack-plugin'); ++ const WorkboxPlugin = require('workbox-webpack-plugin'); + + module.exports = { + entry: { + app: './src/index.js', + print: './src/print.js' + }, + plugins: [ + new CleanWebpackPlugin(['dist']), + new HtmlWebpackPlugin({ +- title: 'Output Management' ++ title: 'Progressive Web Application' +- }) ++ }), ++ new WorkboxPlugin({ ++ // these options encourage the ServiceWorkers to get in there fast ++ // and not allow any straggling "old" SWs to hang around ++ clientsClaim: true, ++ skipWaiting: true ++ }) + ], + output: { + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'dist') + } + }; +``` + +With that in place, let's see what happens when we do an `npm run build`: + +``` bash +clean-webpack-plugin: /mnt/c/Source/webpack-follow-along/dist has been removed. +Hash: 6588e31715d9be04be25 +Version: webpack 3.10.0 +Time: 782ms + Asset Size Chunks Chunk Names + app.bundle.js 545 kB 0, 1 [emitted] [big] app + print.bundle.js 2.74 kB 1 [emitted] print + index.html 254 bytes [emitted] +precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js 268 bytes [emitted] + sw.js 1 kB [emitted] + [0] ./src/print.js 87 bytes {0} {1} [built] + [1] ./src/index.js 477 bytes {0} [built] + [3] (webpack)/buildin/global.js 509 bytes {0} [built] + [4] (webpack)/buildin/module.js 517 bytes {0} [built] + + 1 hidden module +Child html-webpack-plugin for "index.html": + 1 asset + [2] (webpack)/buildin/global.js 509 bytes {0} [built] + [3] (webpack)/buildin/module.js 517 bytes {0} [built] + + 2 hidden modules +``` + +As you can see, we now have 2 extra files being generated; `sw.js` and the more verbose `precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js`. `sw.js` is the Service Worker file and `precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js` is a file that `sw.js` requires so it can run. Your own generated files will likely be different; but you should have an `sw.js` file there. + +So we're now at the happy point of having produced a Service Worker. What's next? + + +## Registering Our Service Worker + +Let's allow our Service Worker to come out and play by registering it. We'll do that by adding the registration code below: + +__index.js__ + +``` diff + import _ from 'lodash'; + import printMe from './print.js'; + ++ if ('serviceWorker' in navigator) { ++ window.addEventListener('load', () => { ++ navigator.serviceWorker.register('/sw.js').then(registration => { ++ console.log('SW registered: ', registration); ++ }).catch(registrationError => { ++ console.log('SW registration failed: ', registrationError); ++ }); ++ }); ++ } +``` + +Once more `npm run build` to build a version of the app including the registration code. Then serve it with `npm start`. Navigate to `http://localhost:8080` and take a look at the console. Somewhere in there you should see: + +``` bash +SW registered +``` + +Now to test it. Stop your server and refresh your page. If your browser supports Service Workers then you should still be looking at your application. However, it has been served up by your Service Worker and __not__ by the server. + + +## Conclusion + +You have built an offline app using the Workbox project. You've started the journey of turning your web app into a PWA. You may now want to think about taking things further. A good resource to help you with that can be found [here](https://developers.google.com/web/progressive-web-apps/). From 41615e4c6ed95aa00fa888f713fb15e4531f32e4 Mon Sep 17 00:00:00 2001 From: Greg Venech Date: Sun, 17 Dec 2017 23:44:24 -0500 Subject: [PATCH 02/13] docs(guides): highlight css splitting in production Resolves #1741 --- src/content/guides/asset-management.md | 2 +- src/content/guides/production.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/content/guides/asset-management.md b/src/content/guides/asset-management.md index b817b32aad69..907c9b40eda1 100644 --- a/src/content/guides/asset-management.md +++ b/src/content/guides/asset-management.md @@ -137,7 +137,7 @@ bundle.js 560 kB 0 [emitted] [big] main Open up `index.html` in your browser again and you should see that `Hello webpack` is now styled in red. To see what webpack did, inspect the page (don't view the page source, as it won't show you the result) and look at the page's head tags. It should contain our style block that we imported in `index.js`. -T> Note that you can also [split your CSS](/plugins/extract-text-webpack-plugin) for better load times in production. On top of that, loaders exist for pretty much any flavor of CSS you can think of -- [postcss](/loaders/postcss-loader), [sass](/loaders/sass-loader), and [less](/loaders/less-loader) to name a few. +Note that you can, and in most cases should, [split your CSS](/plugins/extract-text-webpack-plugin) for better load times in production. On top of that, loaders exist for pretty much any flavor of CSS you can think of -- [postcss](/loaders/postcss-loader), [sass](/loaders/sass-loader), and [less](/loaders/less-loader) to name a few. ## Loading Images diff --git a/src/content/guides/production.md b/src/content/guides/production.md index 4f6cb1734388..248311c05213 100644 --- a/src/content/guides/production.md +++ b/src/content/guides/production.md @@ -182,6 +182,7 @@ __webpack.prod.js__ T> Avoid `inline-***` and `eval-***` use in production as they can increase bundle size and reduce the overall performance. + ## Specify the Environment Many libraries will key off the `process.env.NODE_ENV` variable to determine what should be included in the library. For example, when not in _production_ some libraries may add additional logging and testing to make debugging easier. However, with `process.env.NODE_ENV === 'production'` they might drop or add significant portions of code to optimize how things run for your actual users. We can use webpack's built in [`DefinePlugin`](/plugins/define-plugin) to define this variable for all our dependencies: @@ -235,6 +236,11 @@ __src/index.js__ ``` +## Split CSS + +As mentioned in __Asset Management__ at the end of the [Loading CSS](/guides/asset-management#loading-css) section, it is typically best practice to split your CSS out to a separate file using the `ExtractTextPlugin`. There are some good examples of how to do this in the plugin's [documentation](/plugins/extract-text-webpack-plugin). The `disable` option can be used in combination with the `--env` flag to allow inline loading in development, which is recommended for Hot Module Replacement and build speed. + + ## CLI Alternatives Some of what has been described above is also achievable via the command line. For example, the `--optimize-minimize` flag will include the `UglifyJSPlugin` behind the scenes. The `--define process.env.NODE_ENV="'production'"` will do the same for the `DefinePlugin` instance described above. And, `webpack -p` will automatically invoke both those flags and thus the plugins to be included. From 1b5cf43832b3f8a77c9d81afb7f2523fcb42fd47 Mon Sep 17 00:00:00 2001 From: Baldur Helgason Date: Mon, 18 Dec 2017 14:28:38 +0000 Subject: [PATCH 03/13] docs(plugins): use `.includes` over `.indexOf` Consistent usage of `.includes` --- src/content/plugins/commons-chunk-plugin.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/plugins/commons-chunk-plugin.md b/src/content/plugins/commons-chunk-plugin.md index 401a1364eea4..e14ea407184f 100644 --- a/src/content/plugins/commons-chunk-plugin.md +++ b/src/content/plugins/commons-chunk-plugin.md @@ -217,7 +217,7 @@ new webpack.optimize.CommonsChunkPlugin({ if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) { return false; } - return module.context && module.context.indexOf("node_modules") !== -1; + return module.context && module.context.includes("node_modules"); } }) ``` @@ -242,7 +242,7 @@ Since the `vendor` and `manifest` chunk use a different definition for `minChunk new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: function(module){ - return module.context && module.context.indexOf("node_modules") !== -1; + return module.context && module.context.includes("node_modules"); } }), new webpack.optimize.CommonsChunkPlugin({ From 41a5096728d75f89f21ab6ac2dd57dce6f3ca5c2 Mon Sep 17 00:00:00 2001 From: "Teffen Ellis (Eric)" Date: Tue, 19 Dec 2017 22:30:37 -0500 Subject: [PATCH 04/13] docs(config): omit invalid `detailed` option in stats.md (#1757) --- src/content/configuration/stats.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/content/configuration/stats.md b/src/content/configuration/stats.md index 9d64a45cc83d..cc80eb405162 100644 --- a/src/content/configuration/stats.md +++ b/src/content/configuration/stats.md @@ -31,7 +31,6 @@ stats: "errors-only" | `"minimal"` | *none* | Only output when errors or new compilation happen | | `"none"` | `false` | Output nothing | | `"normal"` | `true` | Standard output | -| `"detailed"` | *none* | Detailed output (since webpack 3.0.0) | | `"verbose"` | *none* | Output everything | For more granular control, it is possible to specify exactly what information you want. Please note that all of the options in this object are optional. From 7078bd7b108e55808fbcdc92de3138d1c56492c4 Mon Sep 17 00:00:00 2001 From: Pierre Neter Date: Wed, 20 Dec 2017 10:40:42 +0700 Subject: [PATCH 05/13] fix(mobile): correctly sort pages in mobile sidebar (#1759) --- src/components/Site/Site.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Site/Site.jsx b/src/components/Site/Site.jsx index 89dd33b59c88..600fe112cc24 100644 --- a/src/components/Site/Site.jsx +++ b/src/components/Site/Site.jsx @@ -44,9 +44,17 @@ const Site = ({ title: section.path.title, url: section.url, pages: section.pages.map(page => ({ + file: page.file, title: page.file.title, url: page.url - })) + })).sort(({ file: { attributes: a }}, { file: { attributes: b }}) => { + let group1 = a.group.toLowerCase(); + let group2 = b.group.toLowerCase(); + + if (group1 < group2) return -1; + if (group1 > group2) return 1; + return a.sort - b.sort; + }) })) } /> From a5566a163d985d14f81cfb6582b2906ca141e6c7 Mon Sep 17 00:00:00 2001 From: Greg Venech Date: Fri, 22 Dec 2017 22:54:14 -0500 Subject: [PATCH 06/13] chore(vote): port voting app, update deps, and simplify config (#1717) Port the voting app to a it's own repository and expose that section of the site more prominently (in the header). This commit also... - Simplifies the webpack config slightly and allows external styles. - Updates issue template to highlight content from other repositories. - Updates some outdated dependencies. --- .github/ISSUE_TEMPLATE.md | 2 + package.json | 23 +- src/components/Navigation/Links.json | 6 +- src/components/Vote/App.jsx | 411 ------------------------- src/components/Vote/App.scss | 228 -------------- src/components/Vote/Button/Button.jsx | 132 -------- src/components/Vote/Button/Button.scss | 57 ---- src/components/Vote/Influence.jsx | 21 -- src/components/Vote/Influence.scss | 19 -- src/components/Vote/Vote.jsx | 33 +- src/components/Vote/Vote.scss | 7 + src/components/Vote/api.dev.js | 161 ---------- src/components/Vote/api.js | 139 --------- webpack.config.js | 12 +- 14 files changed, 40 insertions(+), 1211 deletions(-) delete mode 100644 src/components/Vote/App.jsx delete mode 100644 src/components/Vote/App.scss delete mode 100644 src/components/Vote/Button/Button.jsx delete mode 100644 src/components/Vote/Button/Button.scss delete mode 100644 src/components/Vote/Influence.jsx delete mode 100644 src/components/Vote/Influence.scss delete mode 100644 src/components/Vote/api.dev.js delete mode 100644 src/components/Vote/api.js diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index fa6f49039a4a..b5d199815e2c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,6 @@ - [ ] Check the current issues to ensure you aren't creating a duplicate. - [ ] Consider making small typo fixes and such directly as pull requests. +- [ ] For the voting application, go to https://github.com/webpack-contrib/voting-app. +- [ ] For loader/plugin docs, consider opening an issue in the corresponding repository. - [ ] No existing issue? Go ahead and open a new one. - __Remove these instructions from your PR as they are for your eyes only.__ diff --git a/package.json b/package.json index 471f466b6aae..c8f71e076491 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "antwar-helpers": "^0.19.0", "antwar-interactive": "^0.19.0", "async": "^2.5.0", - "autoprefixer": "^7.1.3", + "autoprefixer": "^7.2.3", "babel-core": "^6.26.0", "babel-eslint": "^7.2.3", "babel-loader": "^7.1.2", @@ -53,12 +53,12 @@ "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.6.0", "babel-preset-react": "^6.24.1", - "copy-webpack-plugin": "^4.0.1", + "copy-webpack-plugin": "^4.3.0", "css-loader": "^0.28.5", "duplexer": "^0.1.1", "eslint": "4.5.0", "eslint-loader": "^1.9.0", - "eslint-plugin-markdown": "^1.0.0-beta.6", + "eslint-plugin-markdown": "^1.0.0-beta.7", "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.2", "fontgen-loader": "^0.2.1", @@ -67,22 +67,22 @@ "github": "^10.0.0", "html-webpack-plugin": "^2.30.1", "http-server": "^0.10.0", - "hyperlink": "^3.0.0", + "hyperlink": "^3.0.1", "loader-utils": "^1.1.0", "lodash": "^4.17.4", "markdown-loader": "^2.0.1", "markdownlint": "^0.6.0", "markdownlint-cli": "^0.3.1", - "marked": "^0.3.6", + "marked": "^0.3.7", "mkdirp": "^0.5.1", "modularscale-sass": "^3.0.3", - "moment": "^2.18.1", + "moment": "^2.20.1", "ncp": "^2.0.0", "node-sass": "^4.5.3", "npm-run-all": "^4.1.1", "postcss-loader": "^2.0.6", "prism-languages": "^0.3.3", - "prismjs": "^1.6.0", + "prismjs": "^1.9.0", "raw-loader": "^0.5.1", "request": "^2.81.0", "sass-loader": "^6.0.6", @@ -92,14 +92,14 @@ "tap-parser": "^6.0.1", "through2": "^2.0.3", "url-loader": "^0.5.9", - "webpack": "^3.5.5", - "webpack-dev-server": "^2.7.1", + "webpack": "^3.10.0", + "webpack-dev-server": "^2.9.7", "webpack-merge": "^4.1.0", "yaml-frontmatter-loader": "^0.1.0" }, "dependencies": { - "ajv": "^5.2.2", - "preact": "^8.2.5", + "ajv": "^5.5.2", + "preact": "^8.2.7", "preact-compat": "3.17.0", "prop-types": "^15.5.10", "react": "^15.6.1", @@ -107,6 +107,7 @@ "react-router": "^4.2.0", "react-router-dom": "^4.2.2", "tool-list": "^0.11.0", + "webpack.vote": "^0.1.0", "whatwg-fetch": "^2.0.3" } } diff --git a/src/components/Navigation/Links.json b/src/components/Navigation/Links.json index 4546aa78e1fb..aafac7beec4f 100644 --- a/src/components/Navigation/Links.json +++ b/src/components/Navigation/Links.json @@ -15,8 +15,12 @@ "title": "Contribute", "url": "contribute" }, + { + "title": "Vote", + "url": "vote" + }, { "title": "Blog", "url": "//medium.com/webpack" } -] \ No newline at end of file +] diff --git a/src/components/Vote/App.jsx b/src/components/Vote/App.jsx deleted file mode 100644 index cfba8704f200..000000000000 --- a/src/components/Vote/App.jsx +++ /dev/null @@ -1,411 +0,0 @@ -import React from 'react'; -import 'whatwg-fetch'; -import * as api from "./api"; -import VoteButton from './Button/Button'; -import Influence from './Influence'; -import GithubMark from '../../assets/github-logo.svg'; - -function updateByProperty(array, property, propertyValue, update) { - return array.map(item => { - if(item[property] === propertyValue) { - return update(item); - } else { - return item; - } - }); -} - -export default class VoteApp extends React.Component { - constructor(props) { - super(props); - this.state = { - selfInfo: undefined, - listInfo: undefined, - isFetchingSelf: false, - isVoting: 0 - }; - } - - isBrowserSupported() { - return typeof localStorage === 'object'; - } - - componentDidMount() { - if(!this.isBrowserSupported()) - return; - - let { selfInfo, listInfo } = this.state; - - if(api.isLoginActive()) { - this.setState({ - isLoginActive: true - }); - api.continueLogin().then(token => { - window.localStorage.voteAppToken = token; - }); - } else { - if(!selfInfo) { - this.updateSelf(); - } - if(!listInfo) { - this.updateList(); - } - } - } - - componentWillReceiveProps(props) { - if(!this.isBrowserSupported()) - return; - - this.updateList(props); - } - - updateSelf() { - let { voteAppToken } = localStorage; - if(voteAppToken) { - this.setState({ - isFetchingSelf: true - }); - api.getSelf(voteAppToken).catch(e => { - this.setState({ - selfInfo: null, - isFetchingSelf: false - }); - }).then(result => { - this.setState({ - selfInfo: result, - isFetchingSelf: false - }); - }); - } - } - - updateList(props = this.props) { - let { name } = props; - let { voteAppToken } = localStorage; - this.setState({ - isFetchingList: true - }); - api.getList(voteAppToken, name).catch(e => { - this.setState({ - listInfo: null, - isFetchingList: false - }); - }).then(result => { - this.setState({ - listInfo: result, - isFetchingList: false - }); - }); - } - - localVote(itemId, voteName, diffValue, currencyName, score) { - let { selfInfo, listInfo } = this.state; - this.setState({ - isVoting: this.state.isVoting + 1, - listInfo: listInfo && { - ...listInfo, - items: updateByProperty(listInfo.items, "id", itemId, item => ({ - ...item, - votes: updateByProperty(item.votes, "name", voteName, vote => ({ - ...vote, - votes: vote.votes + diffValue - })), - userVotes: updateByProperty(item.userVotes, "name", voteName, vote => ({ - ...vote, - votes: vote.votes + diffValue - })), - score: item.score + score * diffValue - })) - }, - selfInfo: selfInfo && { - ...selfInfo, - currencies: updateByProperty(selfInfo.currencies, "name", currencyName, currency => ({ - ...currency, - used: currency.used + diffValue, - remaining: currency.remaining - diffValue - })) - } - }); - } - - vote(itemId, voteName, diffValue, currencyName, score) { - if(!diffValue) return; - this.localVote(itemId, voteName, diffValue, currencyName, score); - let { voteAppToken } = localStorage; - api.vote(voteAppToken, itemId, voteName, diffValue).catch(e => { - console.error(e); - // revert local vote - this.localVote(itemId, voteName, -diffValue, currencyName, score); - this.setState({ - isVoting: this.state.isVoting - 1 - }); - }).then(() => { - this.setState({ - isVoting: this.state.isVoting - 1 - }); - }); - } - - render() { - let { name } = this.props; - - if(!this.isBrowserSupported()) - return
Your browser is not supported.
; - - let { selfInfo, listInfo, isVoting, isFetchingList, isFetchingSelf, isCreating, isLoginActive, editItem, editItemTitle, editItemDescription } = this.state; - - let { voteAppToken } = localStorage; - - if(isLoginActive) { - return
Logging in...
; - } - - const inProgress = isFetchingList || isFetchingSelf || isCreating || isVoting; - - let maxVoteInfo = listInfo && listInfo.possibleVotes.map(() => 0); - - if(listInfo) listInfo.items.forEach(item => { - if(item.userVotes) { - maxVoteInfo.forEach((max, idx) => { - let votes = item.userVotes[idx].votes; - if(votes > max) - maxVoteInfo[idx] = votes; - }); - } - }); - return ( -
-
- Vote -
-
-
-
-
- - -
-
- DISCLAIMER: Since this feature is its Alpha stages, the formula for calculating influence may change. -
-
-
- {this.renderSelf(inProgress)} -
-
-
- { listInfo &&
-

{listInfo.displayName}

-
{listInfo.description}
-
    - { listInfo.items.map(item =>
  • -
    -
    -
    {item.score}
    - {listInfo.possibleVotes.map((voteSettings, idx) => { - let vote = item.votes[idx]; - let userVote = item.userVotes && item.userVotes[idx]; - let currencyInfo = selfInfo && voteSettings.currency && this.findByName(selfInfo.currencies, voteSettings.currency); - let maximum = voteSettings.maximum || 1000; // infinity - let minimum = voteSettings.minimum || 0; - let value = (userVote && userVote.votes) ? userVote.votes: 0; - if(currencyInfo && currencyInfo.remaining + value < maximum) maximum = currencyInfo.remaining + value; - return
    - { - this.vote(item.id, voteSettings.name, diffValue, voteSettings.currency, voteSettings.score); - }} - /> -
    ; - })} -
    - { editItem !== item.id &&
    - {item.title} - {item.description} - { listInfo.isAdmin &&
    - - - - - -
    } -
    } - { editItem === item.id &&
    -
    - this.setState({ editItemTitle: e.target.value })} /> -
    -
    -