├── .appveyor.yml ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── images └── node.png ├── index.ejs ├── package-lock.json ├── package.json ├── src ├── Routes.js ├── components │ ├── App.js │ ├── app.css │ └── contents │ │ ├── Argon.js │ │ ├── Base.js │ │ ├── Boron.js │ │ ├── Carbon.js │ │ ├── List.js │ │ └── style.css ├── index.js └── offline.js ├── webpack.config.js ├── webpack.dev.config.js └── webpack.prod.config.js /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: stable 3 | platform: 4 | - x86 5 | - x64 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm install 9 | test_script: npm test 10 | build: off 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", "stage-1", ["env", { 4 | "modules": false 5 | }] 6 | ], 7 | "env": { 8 | "development": { 9 | "plugins": ["react-hot-loader/babel"] 10 | }, 11 | "production": { 12 | "presets": ["react-optimize"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | end_of_line = lf 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "sky/react", 3 | "rules": { 4 | "react/prop-types": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cover 3 | build 4 | node_modules 5 | npm-debug.log 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | yarn: true 5 | directories: 6 | - node_modules 7 | node_js: 8 | - stable 9 | - 6 10 | os: 11 | - linux 12 | before_script: 13 | - yarn install 14 | - yarn link || true 15 | - npm test 16 | - npm run lint 17 | after_success: 18 | - npm install codecov -g 19 | - npm run postcover 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

PWA-sample

3 |
4 | 5 | ## Usage 6 | ```sh 7 | $ npm start 8 | ``` 9 | 10 | ## Build 11 | ```sh 12 | $ npm run build 13 | $ cd dist 14 | $ serve # npm i -g serve 15 | ``` 16 | 17 | ## Article 18 | http://abouthiroppy.hatenablog.jp/entry/2017/07/28/101318 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /images/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiroppy/pwa-sample/fafb5fa041492fddb003fd3b4063cee4c704c8dd/images/node.png -------------------------------------------------------------------------------- /index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | pwa sample 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwa", 3 | "author": "abouthiroppy coverage.lcov && codecov", 13 | "webpack": "webpack", 14 | "start": "webpack-dev-server", 15 | "build": "cross-env NODE_ENV=production npm run webpack" 16 | }, 17 | "ava": { 18 | "files": [ 19 | "test/**/*.js" 20 | ], 21 | "tap": true, 22 | "failFast": true, 23 | "concurrency": 5 24 | }, 25 | "devDependencies": { 26 | "ava": "^0.21.0", 27 | "babel-core": "^6.25.0", 28 | "babel-loader": "^7.1.1", 29 | "babel-preset-env": "^1.6.0", 30 | "babel-preset-react": "^6.24.1", 31 | "babel-preset-react-optimize": "^1.0.1", 32 | "babel-preset-stage-1": "^6.24.1", 33 | "babili-webpack-plugin": "^0.1.2", 34 | "bundle-loader": "^0.5.5", 35 | "conventional-changelog-cli": "^1.3.2", 36 | "cross-env": "^5.0.1", 37 | "css-loader": "^0.28.4", 38 | "eslint": "^4.3.0", 39 | "eslint-config-sky": "^1.6.2", 40 | "file-loader": "^0.11.2", 41 | "html-webpack-plugin": "^2.29.0", 42 | "nyc": "^11.0.3", 43 | "offline-plugin": "^4.8.3", 44 | "react-hot-loader": "^3.0.0-beta.7", 45 | "style-loader": "^0.18.2", 46 | "webpack": "^3.4.1", 47 | "webpack-dev-server": "^2.6.1", 48 | "webpack-merge": "^4.1.0", 49 | "webpack-pwa-manifest": "^3.1.5", 50 | "workbox-build": "^1.1.0" 51 | }, 52 | "dependencies": { 53 | "history": "^4.6.3", 54 | "lazy-route": "^1.0.7", 55 | "material-ui": "^0.18.7", 56 | "normalize.css": "^7.0.0", 57 | "react": "^15.6.1", 58 | "react-dom": "^15.6.1", 59 | "react-router": "^4.1.2", 60 | "react-router-dom": "^4.1.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | import LazyRoute from 'lazy-route'; 4 | import App from './components/App'; 5 | 6 | // we can not use Switch... 7 | // https://github.com/mhaagens/lazy-route/issues/4 8 | 9 | const Routes = () => ( 10 | 11 | 15 | 20 | } 21 | /> 22 | 25 | 30 | } 31 | /> 32 | 35 | 40 | } 41 | /> 42 | 45 | 50 | } 51 | /> 52 | 53 | ); 54 | 55 | export default Routes; 56 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | // App Shell 2 | 3 | import React from 'react'; 4 | import FontIcon from 'material-ui/FontIcon'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import { Toolbar, ToolbarTitle } from 'material-ui/Toolbar'; 7 | import Drawer from 'material-ui/Drawer'; 8 | import styles from './app.css'; 9 | 10 | class App extends React.Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { opened: false }; 15 | } 16 | 17 | // handleToggle = () => { 18 | // this.setState({opened: !this.state.opened}); 19 | // } 20 | 21 | render() { 22 | const { 23 | children 24 | } = this.props; 25 | 26 | return ( 27 | 28 |
29 | 30 | 31 | 32 | 33 | { 34 | children 35 | } 36 | 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /src/components/app.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 560px; 3 | margin: auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/contents/Argon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from './Base'; 3 | 4 | const Argon = () => ( 5 | 6 | ); 7 | 8 | export default Argon; 9 | -------------------------------------------------------------------------------- /src/components/contents/Base.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './style.css'; 3 | 4 | const Base = (props) => ( 5 |
6 |

{props.title}

7 |
8 | ); 9 | 10 | export default Base; 11 | -------------------------------------------------------------------------------- /src/components/contents/Boron.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from './Base'; 3 | 4 | const Boron = () => ( 5 | 6 | ); 7 | 8 | export default Boron; 9 | -------------------------------------------------------------------------------- /src/components/contents/Carbon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from './Base'; 3 | 4 | const Carbon = () => ( 5 | 6 | ); 7 | 8 | export default Carbon; 9 | -------------------------------------------------------------------------------- /src/components/contents/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styles from './style.css'; 4 | 5 | const List = () => ( 6 |
7 |
8 |
    9 |
  • Argon
  • 10 |
  • Boron
  • 11 |
  • Carbon
  • 12 |
13 |
14 | ); 15 | 16 | export default List; 17 | -------------------------------------------------------------------------------- /src/components/contents/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | } 4 | 5 | .node { 6 | background: url(../../../images/node.png); 7 | background-size: contain; 8 | background-repeat: no-repeat; 9 | height: 100px; 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Router } from 'react-router'; 6 | import { AppContainer } from 'react-hot-loader'; 7 | import createHistory from 'history/createBrowserHistory'; 8 | import Routes from './Routes'; 9 | import './offline'; 10 | 11 | import 'normalize.css'; 12 | 13 | const root = document.getElementById('root'); 14 | 15 | const history = createHistory(); 16 | const render = () => { 17 | ReactDOM.render(( 18 | 19 | 20 | 21 | 22 | 23 | ), root); 24 | }; 25 | 26 | render(); 27 | 28 | if (module.hot) { 29 | module.hot.accept('./components/App', () => { 30 | render(); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/offline.js: -------------------------------------------------------------------------------- 1 | import * as OfflinePluginRuntime from 'offline-plugin/runtime'; 2 | 3 | OfflinePluginRuntime.install({ 4 | onInstalled: () => { 5 | }, 6 | onUpdating: () => { 7 | }, 8 | onUpdateReady: () => { 9 | OfflinePluginRuntime.applyUpdate(); 10 | }, 11 | onUpdated: () => { 12 | window.location.reload(); 13 | }, 14 | onUpdateFailed: () => { 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const merge = require('webpack-merge'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const WebpackPwaManifest = require('webpack-pwa-manifest'); 10 | 11 | const pkg = require('./package.json'); 12 | 13 | const config = process.env.NODE_ENV !== 'production' ? 14 | require('./webpack.dev.config') : 15 | require('./webpack.prod.config'); 16 | 17 | const localIdentName = process.env.NODE_ENV !== 'production' ? 18 | '[path]__[name]__[local]__[hash:base64:5]' : 19 | '[hash:base64:5]'; 20 | 21 | const common = { 22 | bail : true, 23 | entry: { 24 | vendor: Object.keys(pkg.dependencies), 25 | bundle: './src/index.js' 26 | }, 27 | output: { 28 | path: path.resolve('dist') 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test : /\.js$/, 34 | use : 'babel-loader', 35 | exclude: path.join(__dirname, 'node_modules') 36 | }, 37 | { 38 | test: /\.css$/, 39 | use : [ 40 | 'style-loader', 41 | { 42 | loader : 'css-loader', 43 | options: { 44 | modules : true, 45 | importLoaders: 1, 46 | localIdentName 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | test: /\.(eot|woff|woff2|ttf|svg|png|jpg)$/, 53 | use : [ 54 | { 55 | loader : 'file-loader', 56 | options: { 57 | name : '[name]-[hash].[ext]', 58 | limit: 10000 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | }, 65 | plugins: [ 66 | new webpack.NamedModulesPlugin(), 67 | new webpack.DefinePlugin({ 68 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 69 | }), 70 | new HtmlWebpackPlugin({ 71 | template: 'index.ejs' 72 | }), 73 | new WebpackPwaManifest({ 74 | name : 'My PWA Sample', 75 | icons : [], 76 | short_name : 'MyPWA', 77 | description : 'This is a sample App!', 78 | background_color: '#f5f5f5' 79 | }) 80 | ] 81 | }; 82 | 83 | module.exports = merge.smart(common, config); 84 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | devtool: 'cheap-module-eval-source-map', 7 | entry : { 8 | hot: 'react-hot-loader/patch' 9 | }, 10 | output: { 11 | filename : '[name].js', 12 | chunkFilename: '[name].bundle.js' 13 | }, 14 | plugins: [ 15 | new webpack.HotModuleReplacementPlugin(), 16 | new webpack.optimize.CommonsChunkPlugin({ 17 | 18 | // names: ['vendor', 'manifest'], 19 | names : ['vendor'], 20 | filename : '[name].js', 21 | minChunks: Infinity 22 | }) 23 | ], 24 | devServer: { 25 | hot : true, 26 | port : 8080, 27 | inline : true, 28 | contentBase : '.', 29 | historyApiFallback: { 30 | disableDotRule: true 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const BabiliPlugin = require('babili-webpack-plugin'); 5 | const OfflinePlugin = require('offline-plugin'); 6 | 7 | module.exports = { 8 | output: { 9 | filename : '[name].[hash:8].js', 10 | chunkFilename: '[name].bundle.[hash:8].js' 11 | }, 12 | plugins: [ 13 | new webpack.optimize.CommonsChunkPlugin({ 14 | 15 | // names: ['vendor', 'manifest'], 16 | 17 | names : ['vendor'], 18 | filename : '[name].[hash:8].js', 19 | minChunks: Infinity 20 | }), 21 | new webpack.LoaderOptionsPlugin({ 22 | minimize: true, 23 | debug : false 24 | }), 25 | new BabiliPlugin(), 26 | new OfflinePlugin({ 27 | safeToUseOptionalCaches: true, 28 | caches : { 29 | main: [ 30 | 'index.html', 31 | 'bundle*.js', 32 | '*.bundle*.js', // for ensure 33 | 'vendor*.js' // if you want to debug as development env, don't include vendor.js because memory is exceeded 34 | ], 35 | additional: [ // Assets in this section are loaded after main section is successfully loaded 36 | '*.png' 37 | 38 | // '*.woff', 39 | // '*.woff2' 40 | ], 41 | optional: [ 42 | ':rest:' 43 | ] 44 | }, 45 | ServiceWorker: { events: true }, 46 | AppCache : { 47 | events: true 48 | } 49 | }) 50 | ] 51 | }; 52 | --------------------------------------------------------------------------------