Jhernandez has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/371768 )

Change subject: Chore: rebuild and reload when dev code changes
......................................................................


Chore: rebuild and reload when dev code changes

When running `npm start` and the server or client code changes, rebuild
the server and / or client and reload any browser viewing the app.

Notes:

- Don't use an asset manifest for developmental builds.
  - Pros: simpler, no opportunity for manifest to become outdated, no
    manifest HTTP request.
  - Cons: production flow differs.

- Switch server to TypeScript Node. The alternative is to check that the
  server is built first like:

  "start": "run-p -s 'compile:server -- -w' serve:client serve",
  "serve": "nodemon -q -i src -x 'sh -c \"[ -e dist/server/index.js ] && node 
dist/server\"'",

- Hot Module Replacement seems like a nice improvement on this patch but
  it's advanced functionality and unfortunately out of scope for this
  task.

- Server rebuilds trigger a browser update by touching a client file.
  webpack-dev-server does provide an API to hook into the server but
  an API to trigger a browser reload does not seem to be exposed:

  webpack.config.devServer.setup(app) {
    // Expose endpoint to manually trigger reloads from the server when
    // it's rebooted.
    app.get("/reload", (req, res) => {
      console.log("reload");
      return res;
    });
  }

  The wp-calypso app appears to use webpack-hot-middleware for this
  functionality:
  
https://github.com/Automattic/wp-calypso/blob/a70587157b/docs/routing.md#single-tree-rendering.
  A future task could explore a better alternative to file touching.

- Add `watch` script for monitoring tests and server and client
  continuously.

- Remove Webpack clean plugin which was replaced by `rm -rf dist/` in
  production builds and is no longer needed in developmental builds,
  which are now entirely in memory.

- Use Webpack development shortcut, `-d`, for parity with production
  pack command: https://github.com/webpack/webpack/issues/2537.

- Fix `test:watch` to ignore the dist directory since builds change this
  directory and tests do not depend on it.

Bug: T173020
Change-Id: Ie9e38362b6cfdad0b3764e10ade20be4b5d6dfd0
---
M package-lock.json
M package.json
M src/server/index.ts
M src/server/templates/page.ts
M webpack.config.js
5 files changed, 1,074 insertions(+), 44 deletions(-)

Approvals:
  Jhernandez: Verified; Looks good to me, approved



diff --git a/package-lock.json b/package-lock.json
index ec29495..aee4267 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,6 +51,15 @@
         "@types/mime": "1.3.1"
       }
     },
+    "@types/touch": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@types/touch/-/touch-3.1.0.tgz";,
+      "integrity": 
"sha512-Bskfd5wztYbQ/mvU4rgHUB3fKcjW2hA6o/F0JN8O+jRPJQDN/2pPV6SmdIiFm2vLyyN/XQzoCULTE05ZOpnNbQ==",
+      "dev": true,
+      "requires": {
+        "@types/node": "8.0.20"
+      }
+    },
     "abbrev": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz";,
@@ -141,6 +150,12 @@
       "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs=",
       "dev": true
     },
+    "ansi-html": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz";,
+      "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+      "dev": true
+    },
     "ansi-regex": {
       "version": "2.1.1",
       "resolved": 
"https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz";,
@@ -191,6 +206,12 @@
       "version": "0.0.1",
       "resolved": 
"https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz";,
       "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": 
"https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz";,
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
       "dev": true
     },
     "array-flatten": {
@@ -308,6 +329,12 @@
       "integrity": 
"sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
       "dev": true
     },
+    "batch": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz";,
+      "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+      "dev": true
+    },
     "big.js": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz";,
@@ -325,6 +352,28 @@
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.7.tgz";,
       "integrity": 
"sha512-LxFiV5mefv0ley0SzqkOPR1bC4EbpPx8LkOz5vMe/Yi15t5hzwgO/G+tc7wOtL4PZTYjwHu8JnEiSLumuSjSfA==",
       "dev": true
+    },
+    "bonjour": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz";,
+      "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+      "dev": true,
+      "requires": {
+        "array-flatten": "2.1.1",
+        "deep-equal": "1.0.1",
+        "dns-equal": "1.0.0",
+        "dns-txt": "2.0.2",
+        "multicast-dns": "6.1.1",
+        "multicast-dns-service-types": "1.1.0"
+      },
+      "dependencies": {
+        "array-flatten": {
+          "version": "2.1.1",
+          "resolved": 
"https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz";,
+          "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
+          "dev": true
+        }
+      }
     },
     "brace-expansion": {
       "version": "1.1.8",
@@ -439,6 +488,12 @@
         "isarray": "1.0.0"
       }
     },
+    "buffer-indexof": {
+      "version": "1.1.0",
+      "resolved": 
"https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.0.tgz";,
+      "integrity": "sha1-9U9kfE9OJSKLqmVqLlfkPV8nCYI=",
+      "dev": true
+    },
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": 
"https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz";,
@@ -455,6 +510,12 @@
       "version": "3.0.0",
       "resolved": 
"https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz";,
       "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.5.0.tgz";,
+      "integrity": "sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo=",
       "dev": true
     },
     "caller-path": {
@@ -477,6 +538,24 @@
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz";,
       "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
       "dev": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz";,
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "requires": {
+        "camelcase": "2.1.1",
+        "map-obj": "1.0.1"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "2.1.1",
+          "resolved": 
"https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz";,
+          "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+          "dev": true
+        }
+      }
     },
     "center-align": {
       "version": "0.1.3",
@@ -537,12 +616,6 @@
       "version": "0.3.3",
       "resolved": 
"https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz";,
       "integrity": 
"sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
-      "dev": true
-    },
-    "clean-webpack-plugin": {
-      "version": "0.1.16",
-      "resolved": 
"https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-0.1.16.tgz";,
-      "integrity": "sha1-QiqOFQvz1av9PRS/rLBw6A+y4j8=",
       "dev": true
     },
     "cli": {
@@ -617,6 +690,41 @@
         "readable-stream": "2.3.3"
       }
     },
+    "compressible": {
+      "version": "2.0.11",
+      "resolved": 
"https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz";,
+      "integrity": "sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo=",
+      "dev": true,
+      "requires": {
+        "mime-db": "1.29.0"
+      }
+    },
+    "compression": {
+      "version": "1.7.0",
+      "resolved": 
"https://registry.npmjs.org/compression/-/compression-1.7.0.tgz";,
+      "integrity": "sha1-AwyfGY8WQ6BX13anOOki2kNzAS0=",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.3",
+        "bytes": "2.5.0",
+        "compressible": "2.0.11",
+        "debug": "2.6.8",
+        "on-headers": "1.0.1",
+        "safe-buffer": "5.1.1",
+        "vary": "1.1.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.8",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz";,
+          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": 
"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz";,
@@ -649,6 +757,12 @@
         "write-file-atomic": "1.3.4",
         "xdg-basedir": "2.0.0"
       }
+    },
+    "connect-history-api-fallback": {
+      "version": "1.3.0",
+      "resolved": 
"https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz";,
+      "integrity": "sha1-5R0X+PDvDbkKZP20feMFFVbp8Wk=",
+      "dev": true
     },
     "console-browserify": {
       "version": "1.1.0",
@@ -756,6 +870,15 @@
         "randombytes": "2.0.5"
       }
     },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": 
"https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz";,
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "1.0.2"
+      }
+    },
     "d": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz";,
@@ -783,6 +906,12 @@
       "version": "1.2.0",
       "resolved": 
"https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz";,
       "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "deep-equal": {
+      "version": "1.0.1",
+      "resolved": 
"https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz";,
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
       "dev": true
     },
     "deep-extend": {
@@ -842,6 +971,12 @@
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz";,
       "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
     },
+    "detect-node": {
+      "version": "2.0.3",
+      "resolved": 
"https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz";,
+      "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=",
+      "dev": true
+    },
     "diff": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz";,
@@ -857,6 +992,31 @@
         "bn.js": "4.11.7",
         "miller-rabin": "4.0.0",
         "randombytes": "2.0.5"
+      }
+    },
+    "dns-equal": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz";,
+      "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+      "dev": true
+    },
+    "dns-packet": {
+      "version": "1.2.1",
+      "resolved": 
"https://registry.npmjs.org/dns-packet/-/dns-packet-1.2.1.tgz";,
+      "integrity": 
"sha512-eisukPHpsFmhEIDnm2mECIiT0huapmdkC0AH1Lvt613Kz2v1kwolrkecvguFazrqnpxvgYdtcMFTsmQzAeRXZQ==",
+      "dev": true,
+      "requires": {
+        "ip": "1.1.5",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "dns-txt": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz";,
+      "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+      "dev": true,
+      "requires": {
+        "buffer-indexof": "1.1.0"
       }
     },
     "doctrine": {
@@ -1339,11 +1499,26 @@
         "through": "2.3.8"
       }
     },
+    "eventemitter3": {
+      "version": "1.2.0",
+      "resolved": 
"https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz";,
+      "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=",
+      "dev": true
+    },
     "events": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz";,
       "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
       "dev": true
+    },
+    "eventsource": {
+      "version": "0.1.6",
+      "resolved": 
"https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz";,
+      "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=",
+      "dev": true,
+      "requires": {
+        "original": "1.0.0"
+      }
     },
     "evp_bytestokey": {
       "version": "1.0.0",
@@ -1465,6 +1640,15 @@
       "resolved": 
"https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz";,
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
       "dev": true
+    },
+    "faye-websocket": {
+      "version": "0.10.0",
+      "resolved": 
"https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz";,
+      "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+      "dev": true,
+      "requires": {
+        "websocket-driver": "0.6.5"
+      }
     },
     "figures": {
       "version": "2.0.0",
@@ -1746,6 +1930,12 @@
       "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
       "dev": true
     },
+    "handle-thing": {
+      "version": "1.2.5",
+      "resolved": 
"https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz";,
+      "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
+      "dev": true
+    },
     "has": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz";,
@@ -1806,6 +1996,24 @@
       "integrity": 
"sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
       "dev": true
     },
+    "hpack.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz";,
+      "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "obuf": "1.1.1",
+        "readable-stream": "2.3.3",
+        "wbuf": "1.7.2"
+      }
+    },
+    "html-entities": {
+      "version": "1.2.1",
+      "resolved": 
"https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz";,
+      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+      "dev": true
+    },
     "htmlparser2": {
       "version": "3.8.3",
       "resolved": 
"https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz";,
@@ -1845,6 +2053,12 @@
         }
       }
     },
+    "http-deceiver": {
+      "version": "1.2.7",
+      "resolved": 
"https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz";,
+      "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
+      "dev": true
+    },
     "http-errors": {
       "version": "1.6.1",
       "resolved": 
"https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz";,
@@ -1860,6 +2074,45 @@
           "version": "1.1.0",
           "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz";,
           "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM="
+        }
+      }
+    },
+    "http-proxy": {
+      "version": "1.16.2",
+      "resolved": 
"https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz";,
+      "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=",
+      "dev": true,
+      "requires": {
+        "eventemitter3": "1.2.0",
+        "requires-port": "1.0.0"
+      }
+    },
+    "http-proxy-middleware": {
+      "version": "0.17.4",
+      "resolved": 
"https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz";,
+      "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=",
+      "dev": true,
+      "requires": {
+        "http-proxy": "1.16.2",
+        "is-glob": "3.1.0",
+        "lodash": "4.17.4",
+        "micromatch": "2.3.11"
+      },
+      "dependencies": {
+        "is-extglob": {
+          "version": "2.1.1",
+          "resolved": 
"https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz";,
+          "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz";,
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "2.1.1"
+          }
         }
       }
     },
@@ -1917,6 +2170,26 @@
       "resolved": 
"https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz";,
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
       "dev": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz";,
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "requires": {
+        "repeating": "2.0.1"
+      },
+      "dependencies": {
+        "repeating": {
+          "version": "2.0.1",
+          "resolved": 
"https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz";,
+          "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+          "dev": true,
+          "requires": {
+            "is-finite": "1.0.2"
+          }
+        }
+      }
     },
     "indexof": {
       "version": "0.0.1",
@@ -2019,6 +2292,15 @@
         }
       }
     },
+    "internal-ip": {
+      "version": "1.2.0",
+      "resolved": 
"https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz";,
+      "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=",
+      "dev": true,
+      "requires": {
+        "meow": "3.7.0"
+      }
+    },
     "interpret": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz";,
@@ -2029,6 +2311,12 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz";,
       "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
+    "ip": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz";,
+      "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
       "dev": true
     },
     "ipaddr.js": {
@@ -2232,6 +2520,12 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz";,
       "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz";,
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
       "dev": true
     },
     "isarray": {
@@ -2638,6 +2932,12 @@
       "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
       "dev": true
     },
+    "loglevel": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.4.1.tgz";,
+      "integrity": "sha1-lbOD+Ro8J1b9SrCTZn5DCRYfK80=",
+      "dev": true
+    },
     "lolex": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.2.tgz";,
@@ -2649,6 +2949,16 @@
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz";,
       "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
       "dev": true
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": 
"https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz";,
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "0.4.1",
+        "signal-exit": "3.0.2"
+      }
     },
     "lowercase-keys": {
       "version": "1.0.0",
@@ -2670,6 +2980,12 @@
       "version": "1.3.0",
       "resolved": 
"https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz";,
       "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz";,
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
       "dev": true
     },
     "map-stream": {
@@ -2700,6 +3016,105 @@
       "requires": {
         "errno": "0.1.4",
         "readable-stream": "2.3.3"
+      }
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz";,
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "requires": {
+        "camelcase-keys": "2.1.0",
+        "decamelize": "1.2.0",
+        "loud-rejection": "1.6.0",
+        "map-obj": "1.0.1",
+        "minimist": "1.2.0",
+        "normalize-package-data": "2.4.0",
+        "object-assign": "4.1.1",
+        "read-pkg-up": "1.0.1",
+        "redent": "1.0.0",
+        "trim-newlines": "1.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz";,
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "dev": true,
+          "requires": {
+            "path-exists": "2.1.0",
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "load-json-file": {
+          "version": "1.1.0",
+          "resolved": 
"https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz";,
+          "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "parse-json": "2.2.0",
+            "pify": "2.3.0",
+            "pinkie-promise": "2.0.1",
+            "strip-bom": "2.0.0"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": 
"https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz";,
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": 
"https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz";,
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "path-type": {
+          "version": "1.1.0",
+          "resolved": 
"https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz";,
+          "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "pify": "2.3.0",
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "read-pkg": {
+          "version": "1.1.0",
+          "resolved": 
"https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz";,
+          "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+          "dev": true,
+          "requires": {
+            "load-json-file": "1.1.0",
+            "normalize-package-data": "2.4.0",
+            "path-type": "1.1.0"
+          }
+        },
+        "read-pkg-up": {
+          "version": "1.0.1",
+          "resolved": 
"https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz";,
+          "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+          "dev": true,
+          "requires": {
+            "find-up": "1.1.2",
+            "read-pkg": "1.1.0"
+          }
+        },
+        "strip-bom": {
+          "version": "2.0.0",
+          "resolved": 
"https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz";,
+          "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+          "dev": true,
+          "requires": {
+            "is-utf8": "0.2.1"
+          }
+        }
       }
     },
     "merge-descriptors": {
@@ -2885,6 +3300,22 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz";,
       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
     },
+    "multicast-dns": {
+      "version": "6.1.1",
+      "resolved": 
"https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.1.1.tgz";,
+      "integrity": "sha1-bn3oalcIcqsXBYrepxYLvsqBTd4=",
+      "dev": true,
+      "requires": {
+        "dns-packet": "1.2.1",
+        "thunky": "0.1.0"
+      }
+    },
+    "multicast-dns-service-types": {
+      "version": "1.1.0",
+      "resolved": 
"https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz";,
+      "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
+      "dev": true
+    },
     "mute-stream": {
       "version": "0.0.7",
       "resolved": 
"https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz";,
@@ -2952,6 +3383,12 @@
         }
       }
     },
+    "node-forge": {
+      "version": "0.6.33",
+      "resolved": 
"https://registry.npmjs.org/node-forge/-/node-forge-0.6.33.tgz";,
+      "integrity": "sha1-RjgRh59XPUUVWtap9D3ClujoXrw=",
+      "dev": true
+    },
     "node-libs-browser": {
       "version": "2.0.0",
       "resolved": 
"https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.0.0.tgz";,
@@ -3004,7 +3441,6 @@
         "lodash.defaults": "3.1.2",
         "minimatch": "3.0.4",
         "ps-tree": "1.1.0",
-        "touch": "1.0.0",
         "undefsafe": "0.0.3",
         "update-notifier": "0.5.0"
       }
@@ -3091,6 +3527,12 @@
         "is-extendable": "0.1.1"
       }
     },
+    "obuf": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz";,
+      "integrity": "sha1-EEEktsYCxnlogaBCVB0220OlJk4=",
+      "dev": true
+    },
     "on-finished": {
       "version": "2.3.0",
       "resolved": 
"https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz";,
@@ -3098,6 +3540,12 @@
       "requires": {
         "ee-first": "1.1.1"
       }
+    },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": 
"https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz";,
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=",
+      "dev": true
     },
     "once": {
       "version": "1.4.0",
@@ -3115,6 +3563,16 @@
       "dev": true,
       "requires": {
         "mimic-fn": "1.1.0"
+      }
+    },
+    "opn": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz";,
+      "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=",
+      "dev": true,
+      "requires": {
+        "object-assign": "4.1.1",
+        "pinkie-promise": "2.0.1"
       }
     },
     "optionator": {
@@ -3136,6 +3594,27 @@
           "resolved": 
"https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz";,
           "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
           "dev": true
+        }
+      }
+    },
+    "original": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/original/-/original-1.0.0.tgz";,
+      "integrity": "sha1-kUf5P6FpbQS+YeAb1QuurKZWvTs=",
+      "dev": true,
+      "requires": {
+        "url-parse": "1.0.5"
+      },
+      "dependencies": {
+        "url-parse": {
+          "version": "1.0.5",
+          "resolved": 
"https://registry.npmjs.org/url-parse/-/url-parse-1.0.5.tgz";,
+          "integrity": "sha1-CFSGBCKv3P7+tsllxmLUgAFpkns=",
+          "dev": true,
+          "requires": {
+            "querystringify": "0.0.4",
+            "requires-port": "1.0.0"
+          }
         }
       }
     },
@@ -3198,6 +3677,12 @@
       "requires": {
         "p-limit": "1.1.0"
       }
+    },
+    "p-map": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.1.1.tgz";,
+      "integrity": "sha1-BfXkrpegaDcbwqXMhr+9vBnErno=",
+      "dev": true
     },
     "package-json": {
       "version": "1.2.0",
@@ -3347,6 +3832,25 @@
       "integrity": "sha1-WbcIwcAZCi9pLxx2GMRGsFL9F2I=",
       "dev": true
     },
+    "portfinder": {
+      "version": "1.0.13",
+      "resolved": 
"https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz";,
+      "integrity": "sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=",
+      "dev": true,
+      "requires": {
+        "async": "1.5.2",
+        "debug": "2.6.7",
+        "mkdirp": "0.5.1"
+      },
+      "dependencies": {
+        "async": {
+          "version": "1.5.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz";,
+          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+          "dev": true
+        }
+      }
+    },
     "prelude-ls": {
       "version": "1.1.2",
       "resolved": 
"https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz";,
@@ -3453,6 +3957,12 @@
       "version": "0.2.1",
       "resolved": 
"https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz";,
       "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "querystringify": {
+      "version": "0.0.4",
+      "resolved": 
"https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz";,
+      "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=",
       "dev": true
     },
     "randomatic": {
@@ -3588,6 +4098,33 @@
         "set-immediate-shim": "1.0.1"
       }
     },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz";,
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "requires": {
+        "indent-string": "2.1.0",
+        "strip-indent": "1.0.1"
+      },
+      "dependencies": {
+        "get-stdin": {
+          "version": "4.0.1",
+          "resolved": 
"https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz";,
+          "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+          "dev": true
+        },
+        "strip-indent": {
+          "version": "1.0.1",
+          "resolved": 
"https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz";,
+          "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+          "dev": true,
+          "requires": {
+            "get-stdin": "4.0.1"
+          }
+        }
+      }
+    },
     "regex-cache": {
       "version": "0.4.3",
       "resolved": 
"https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz";,
@@ -3655,6 +4192,12 @@
         "caller-path": "0.1.0",
         "resolve-from": "1.0.1"
       }
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz";,
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+      "dev": true
     },
     "resolve-from": {
       "version": "1.0.1",
@@ -3736,6 +4279,21 @@
       "integrity": "sha1-7dOQk6MYQ3DLhZJDsr3yVefY6mc=",
       "dev": true
     },
+    "select-hose": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz";,
+      "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
+      "dev": true
+    },
+    "selfsigned": {
+      "version": "1.10.1",
+      "resolved": 
"https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.1.tgz";,
+      "integrity": "sha1-v4y3uDJWxFUeMTR8YxF3jbme7FI=",
+      "dev": true,
+      "requires": {
+        "node-forge": "0.6.33"
+      }
+    },
     "semver": {
       "version": "5.4.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz";,
@@ -3769,6 +4327,32 @@
         "on-finished": "2.3.0",
         "range-parser": "1.2.0",
         "statuses": "1.3.1"
+      }
+    },
+    "serve-index": {
+      "version": "1.9.0",
+      "resolved": 
"https://registry.npmjs.org/serve-index/-/serve-index-1.9.0.tgz";,
+      "integrity": "sha1-0rKA/FYNYW7oG0i/D6gqvtJIXOc=",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.3",
+        "batch": "0.6.1",
+        "debug": "2.6.8",
+        "escape-html": "1.0.3",
+        "http-errors": "1.6.1",
+        "mime-types": "2.1.16",
+        "parseurl": "1.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.8",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz";,
+          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
       }
     },
     "serve-static": {
@@ -3899,6 +4483,41 @@
       "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=",
       "dev": true
     },
+    "sockjs": {
+      "version": "0.3.18",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.18.tgz";,
+      "integrity": "sha1-2bKJMWyn33dZXvKZ4HXw+TfrQgc=",
+      "dev": true,
+      "requires": {
+        "faye-websocket": "0.10.0",
+        "uuid": "2.0.3"
+      }
+    },
+    "sockjs-client": {
+      "version": "1.1.4",
+      "resolved": 
"https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz";,
+      "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.7",
+        "eventsource": "0.1.6",
+        "faye-websocket": "0.11.1",
+        "inherits": "2.0.3",
+        "json3": "3.3.2",
+        "url-parse": "1.1.9"
+      },
+      "dependencies": {
+        "faye-websocket": {
+          "version": "0.11.1",
+          "resolved": 
"https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz";,
+          "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=",
+          "dev": true,
+          "requires": {
+            "websocket-driver": "0.6.5"
+          }
+        }
+      }
+    },
     "source-list-map": {
       "version": "2.0.0",
       "resolved": 
"https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz";,
@@ -3940,6 +4559,57 @@
       "resolved": 
"https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz";,
       "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
       "dev": true
+    },
+    "spdy": {
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz";,
+      "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.8",
+        "handle-thing": "1.2.5",
+        "http-deceiver": "1.2.7",
+        "safe-buffer": "5.1.1",
+        "select-hose": "2.0.0",
+        "spdy-transport": "2.0.20"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.8",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz";,
+          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "spdy-transport": {
+      "version": "2.0.20",
+      "resolved": 
"https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.0.20.tgz";,
+      "integrity": "sha1-c15yBUxIayNU/onnAiVgBKOazk0=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.8",
+        "detect-node": "2.0.3",
+        "hpack.js": "2.1.6",
+        "obuf": "1.1.1",
+        "readable-stream": "2.3.3",
+        "safe-buffer": "5.1.1",
+        "wbuf": "1.7.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.8",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz";,
+          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
     },
     "split": {
       "version": "0.3.3",
@@ -4156,6 +4826,18 @@
       "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
       "dev": true
     },
+    "thunky": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz";,
+      "integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=",
+      "dev": true
+    },
+    "time-stamp": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz";,
+      "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=",
+      "dev": true
+    },
     "timed-out": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz";,
@@ -4188,13 +4870,19 @@
       "dev": true
     },
     "touch": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/touch/-/touch-1.0.0.tgz";,
-      "integrity": "sha1-RJy+LbrlqMgDjjDXH6D/RklHxN4=",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz";,
+      "integrity": 
"sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
       "dev": true,
       "requires": {
         "nopt": "1.0.10"
       }
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz";,
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true
     },
     "tryit": {
       "version": "1.0.3",
@@ -4457,6 +5145,24 @@
         }
       }
     },
+    "url-parse": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.1.9.tgz";,
+      "integrity": "sha1-xn8dd11R8KGJEd17P/rSe7nlvRk=",
+      "dev": true,
+      "requires": {
+        "querystringify": "1.0.0",
+        "requires-port": "1.0.0"
+      },
+      "dependencies": {
+        "querystringify": {
+          "version": "1.0.0",
+          "resolved": 
"https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz";,
+          "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=",
+          "dev": true
+        }
+      }
+    },
     "util": {
       "version": "0.10.3",
       "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz";,
@@ -4543,6 +5249,15 @@
         "graceful-fs": "4.1.11"
       }
     },
+    "wbuf": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.2.tgz";,
+      "integrity": "sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=",
+      "dev": true,
+      "requires": {
+        "minimalistic-assert": "1.0.0"
+      }
+    },
     "webpack": {
       "version": "3.4.1",
       "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.4.1.tgz";,
@@ -4584,6 +5299,270 @@
         }
       }
     },
+    "webpack-dev-middleware": {
+      "version": "1.12.0",
+      "resolved": 
"https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz";,
+      "integrity": "sha1-007++y7dp+HTtdvgcolRMhllFwk=",
+      "dev": true,
+      "requires": {
+        "memory-fs": "0.4.1",
+        "mime": "1.3.4",
+        "path-is-absolute": "1.0.1",
+        "range-parser": "1.2.0",
+        "time-stamp": "2.0.0"
+      }
+    },
+    "webpack-dev-server": {
+      "version": "2.7.1",
+      "resolved": 
"https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz";,
+      "integrity": "sha1-IVgPWgjNBlxxFEz29hw0W8pZqLg=",
+      "dev": true,
+      "requires": {
+        "ansi-html": "0.0.7",
+        "bonjour": "3.5.0",
+        "chokidar": "1.7.0",
+        "compression": "1.7.0",
+        "connect-history-api-fallback": "1.3.0",
+        "del": "3.0.0",
+        "express": "4.15.3",
+        "html-entities": "1.2.1",
+        "http-proxy-middleware": "0.17.4",
+        "internal-ip": "1.2.0",
+        "ip": "1.1.5",
+        "loglevel": "1.4.1",
+        "opn": "4.0.2",
+        "portfinder": "1.0.13",
+        "selfsigned": "1.10.1",
+        "serve-index": "1.9.0",
+        "sockjs": "0.3.18",
+        "sockjs-client": "1.1.4",
+        "spdy": "3.4.7",
+        "strip-ansi": "3.0.1",
+        "supports-color": "3.2.3",
+        "webpack-dev-middleware": "1.12.0",
+        "yargs": "6.6.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": 
"https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz";,
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz";,
+          "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+          "dev": true,
+          "requires": {
+            "string-width": "1.0.2",
+            "strip-ansi": "3.0.1",
+            "wrap-ansi": "2.1.0"
+          }
+        },
+        "del": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz";,
+          "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=",
+          "dev": true,
+          "requires": {
+            "globby": "6.1.0",
+            "is-path-cwd": "1.0.0",
+            "is-path-in-cwd": "1.0.0",
+            "p-map": "1.1.1",
+            "pify": "3.0.0",
+            "rimraf": "2.6.1"
+          }
+        },
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz";,
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "dev": true,
+          "requires": {
+            "path-exists": "2.1.0",
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "globby": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz";,
+          "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+          "dev": true,
+          "requires": {
+            "array-union": "1.0.2",
+            "glob": "7.1.2",
+            "object-assign": "4.1.1",
+            "pify": "2.3.0",
+            "pinkie-promise": "2.0.1"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz";,
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        },
+        "has-flag": {
+          "version": "1.0.0",
+          "resolved": 
"https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz";,
+          "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+          "dev": true
+        },
+        "load-json-file": {
+          "version": "1.1.0",
+          "resolved": 
"https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz";,
+          "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "parse-json": "2.2.0",
+            "pify": "2.3.0",
+            "pinkie-promise": "2.0.1",
+            "strip-bom": "2.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz";,
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        },
+        "os-locale": {
+          "version": "1.4.0",
+          "resolved": 
"https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz";,
+          "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+          "dev": true,
+          "requires": {
+            "lcid": "1.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": 
"https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz";,
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "path-type": {
+          "version": "1.1.0",
+          "resolved": 
"https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz";,
+          "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "pify": "2.3.0",
+            "pinkie-promise": "2.0.1"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz";,
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        },
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz";,
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+          "dev": true
+        },
+        "read-pkg": {
+          "version": "1.1.0",
+          "resolved": 
"https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz";,
+          "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+          "dev": true,
+          "requires": {
+            "load-json-file": "1.1.0",
+            "normalize-package-data": "2.4.0",
+            "path-type": "1.1.0"
+          }
+        },
+        "read-pkg-up": {
+          "version": "1.0.1",
+          "resolved": 
"https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz";,
+          "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+          "dev": true,
+          "requires": {
+            "find-up": "1.1.2",
+            "read-pkg": "1.1.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": 
"https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz";,
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "1.1.0",
+            "is-fullwidth-code-point": "1.0.0",
+            "strip-ansi": "3.0.1"
+          }
+        },
+        "strip-bom": {
+          "version": "2.0.0",
+          "resolved": 
"https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz";,
+          "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+          "dev": true,
+          "requires": {
+            "is-utf8": "0.2.1"
+          }
+        },
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": 
"https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz";,
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "dev": true,
+          "requires": {
+            "has-flag": "1.0.0"
+          }
+        },
+        "which-module": {
+          "version": "1.0.0",
+          "resolved": 
"https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz";,
+          "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "6.6.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz";,
+          "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
+          "dev": true,
+          "requires": {
+            "camelcase": "3.0.0",
+            "cliui": "3.2.0",
+            "decamelize": "1.2.0",
+            "get-caller-file": "1.0.2",
+            "os-locale": "1.4.0",
+            "read-pkg-up": "1.0.1",
+            "require-directory": "2.1.1",
+            "require-main-filename": "1.0.1",
+            "set-blocking": "2.0.0",
+            "string-width": "1.0.2",
+            "which-module": "1.0.0",
+            "y18n": "3.2.1",
+            "yargs-parser": "4.2.1"
+          }
+        },
+        "yargs-parser": {
+          "version": "4.2.1",
+          "resolved": 
"https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz";,
+          "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
+          "dev": true,
+          "requires": {
+            "camelcase": "3.0.0"
+          }
+        }
+      }
+    },
     "webpack-sources": {
       "version": "1.0.1",
       "resolved": 
"https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz";,
@@ -4594,6 +5573,21 @@
         "source-map": "0.5.6"
       }
     },
+    "websocket-driver": {
+      "version": "0.6.5",
+      "resolved": 
"https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz";,
+      "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=",
+      "dev": true,
+      "requires": {
+        "websocket-extensions": "0.1.1"
+      }
+    },
+    "websocket-extensions": {
+      "version": "0.1.1",
+      "resolved": 
"https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz";,
+      "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=",
+      "dev": true
+    },
     "which": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz";,
diff --git a/package.json b/package.json
index 4f14227..e747c5f 100644
--- a/package.json
+++ b/package.json
@@ -4,21 +4,22 @@
   "description": "An API driven skin for MediaWiki",
   "scripts": {
     "--- PRIMARY ---": "# Frequent user scripts.",
-    "start": "run-p -s 'client:build -- -w' 'server:build -- -w' server:watch",
+    "start": "run-p -s 'client:watch -- -d' server:watch",
+    "watch": "run-p -s start test:watch",
     "build": "NODE_ENV=production npm-run-all --silent clean --parallel 
server:build 'client:build -- -p'",
     "format": "npm run -s lint -- --fix",
     "lint": "eslint --cache --max-warnings 0 --ext ts,tsx,js,json .",
     "test": "run-p -s lint build mocha",
-    "test:watch": "nodemon -e js,json,ts,tsx -q -x 'npm test -s'",
+    "test:watch": "nodemon -i dist -e js,json,ts,tsx -q -x 'npm test -s'",
     "--- SECONDARY ---": "# Useful but rarely used scripts.",
     "mocha": "mocha '{src,test}/**/*.test.{ts,js}'",
     "clean": "rm -rf dist/",
     "--- HOOKS ---": "# Triggered scripts.",
     "precommit": "npm test -s",
     "--- INTERNAL ---": "# Private scripts.",
-    "server:watch": "nodemon -e js,json,ts,tsx -q -x 'npm run -s server:run'",
+    "server:watch": "nodemon -i dist -i src/client -e js,json,ts,tsx -q -x 
'ts-node src/server'",
     "server:build": "tsc -p src/server",
-    "server:run": "node dist/server",
+    "client:watch": "webpack-dev-server -w",
     "client:build": "webpack"
   },
   "repository": {
@@ -49,8 +50,8 @@
     "@types/express": "^4.0.36",
     "@types/mocha": "^2.2.41",
     "@types/node": "^8.0.20",
+    "@types/touch": "^3.1.0",
     "assets-webpack-plugin": "^3.5.1",
-    "clean-webpack-plugin": "^0.1.16",
     "eslint": "^4.4.1",
     "eslint-config-node-services": "^2.2.2",
     "eslint-config-prettier": "^2.3.0",
@@ -64,10 +65,12 @@
     "npm-run-all": "^4.0.2",
     "prettier": "^1.5.3",
     "sinon": "^3.0.0",
+    "touch": "^3.1.0",
     "ts-loader": "^2.3.2",
     "ts-node": "^3.3.0",
     "typescript": "^2.4.2",
     "typescript-eslint-parser": "^5.0.1",
-    "webpack": "^3.4.1"
+    "webpack": "^3.4.1",
+    "webpack-dev-server": "^2.7.1"
   }
 }
diff --git a/src/server/index.ts b/src/server/index.ts
index ac2f9ad..803eabf 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -2,16 +2,12 @@
 import page, { AssetsManifest } from "./templates/page";
 import app from "../common/components/app";
 
-let assets: AssetsManifest = {};
-try {
-  assets = require("../../dist/public/assets-manifest.json");
-} catch (e) {
-  if (process.env.NODE_ENV === "production") {
-    throw e;
-  } else {
-    console.error("Unable to load the static assets manifest file"); // 
eslint-disable-line no-console
-  }
-}
+const isProd: boolean = process.env.NODE_ENV === "production";
+
+// The asset manifest built or the webpack-dev-server URL (which has no 
manifest).
+const assets: AssetsManifest | string = isProd
+  ? require("../../dist/public/assets-manifest.json")
+  : "http://localhost:8080";;
 
 const { PORT = 3000 } = process.env;
 const server = express();
@@ -28,4 +24,15 @@
 
 server.listen(PORT, () => {
   console.log(`Server started on port http://localhost:${PORT}/`); // 
eslint-disable-line no-console
+
+  if (!isProd) {
+    const touch = require("touch");
+
+    // The server is now listening and ready to receive requests. If 
developmental, touch the client
+    // sources to trigger a webpack-dev-server file watch event which triggers 
a browser reload. If
+    // the server was previously running, the browser will be updated with the 
latest results. The
+    // negative offset accounts for: 
https://github.com/webpack/watchpack/issues/25.
+    const nowish: number = Date.now() - 10 * 1000;
+    touch("src/client/index.ts", { time: nowish });
+  }
 });
diff --git a/src/server/templates/page.ts b/src/server/templates/page.ts
index 584324f..3cabd61 100644
--- a/src/server/templates/page.ts
+++ b/src/server/templates/page.ts
@@ -10,15 +10,24 @@
   // HTML to render in the body of the page
   body: string,
   // Manifest of filename entry points to bundled assets.
-  assets: AssetsManifest
+  assets: AssetsManifest | string
 }
 
-export default function page({ title, body = "", assets }: PageParams): string 
{
-  const scripts = [];
-  if (assets.index && assets.index.js) {
-    scripts.push(assets.index.js);
-  }
+/**
+ * @return {!string} The path to the asset identified by entry and extension 
(e.g., index.js);
+ *                   either a URL (development) or a filesystem path 
(production).
+ */
+const asset = (
+  manifest: AssetsManifest | string,
+  entry: string,
+  extension: string
+): string =>
+  typeof manifest === "string"
+    ? `${manifest}/${entry}.${extension}`
+    : manifest[entry][extension];
 
+export default function page({ title, body = "", assets }: PageParams): string 
{
+  const script: string = asset(assets, "index", "js");
   return `
 <!DOCTYPE html>
 <html lang="en">
@@ -30,7 +39,7 @@
   </head>
   <body>
     <div id="root">${body}</div>
-    ${scripts.map(s => `<script type="text/javascript" 
src="./${s}"></script>`)}
+    <script type="text/javascript" src="${script}"></script>
   </body>
 </html>`;
 }
diff --git a/webpack.config.js b/webpack.config.js
index 7766e02..1475dab 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,7 +1,6 @@
 /* eslint-env node */
 const path = require("path");
 const webpack = require("webpack");
-const CleanWebpackPlugin = require("clean-webpack-plugin");
 const AssetsPlugin = require("assets-webpack-plugin");
 
 const isProd = process.env.NODE_ENV === "production";
@@ -35,7 +34,9 @@
     // global build hashing, to improve caching from browsers.
     // See: https://webpack.js.org/guides/caching/#output-filenames
     chunkFilename: "[name].[chunkhash].js",
-    filename: "[name].[chunkhash].js"
+
+    // Use constant filenames for developmental server.
+    filename: isProd ? "[name].[chunkhash].js" : "[name].js"
   },
 
   resolve: {
@@ -57,19 +58,35 @@
 
   devtool: isProd ? "source-map" : "cheap-module-eval-source-map",
 
-  plugins: [
-    // Use path names instead of autogenerated ids for asset names
-    new webpack.NamedModulesPlugin(),
+  // For development builds, serve the packaged result over 
http://localhost:8080/ and live reload
+  // the browser when the bundle is rebuilt.
+  devServer: isProd
+    ? undefined
+    : {
+        // Forbid static files. All responses are in memory.
+        contentBase: false,
 
-    // Clean the build folder on restarts
-    new CleanWebpackPlugin(paths.client.output, { verbose: false }),
+        // Log warnings and errors in the browser console.
+        clientLogLevel: "warning",
 
-    // Generate a json manifest with the entry points and assets names to use
-    // in the server to pass to the HTML page template
+        // Show warnings and errors as an obtrusive opaque overlay in the 
browser.
+        overlay: { warnings: true, errors: true },
+
+        stats: WARNINGS_STATS_PRESET
+      },
+
+  // Use path names instead of autogenerated ids for asset names
+  plugins: [new webpack.NamedModulesPlugin()]
+};
+
+if (isProd) {
+  // Generate a json manifest with the entry points and assets names to use
+  // in the server to pass to the HTML page template
+  module.exports.plugins.push(
     new AssetsPlugin({
       prettyPrint: true,
       filename: "assets-manifest.json",
       path: paths.client.output
     })
-  ]
-};
+  );
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/371768
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie9e38362b6cfdad0b3764e10ade20be4b5d6dfd0
Gerrit-PatchSet: 3
Gerrit-Project: marvin
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <[email protected]>
Gerrit-Reviewer: Jhernandez <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: Sniedzielski <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to