├── .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 |
--------------------------------------------------------------------------------