Niedzielski has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/378267 )

Change subject: New: add page summaries fetcher and UI
......................................................................

New: add page summaries fetcher and UI

Bug: T173323
Change-Id: I7456e654e24db34343bd8e108bc990be8fff31fe
---
M docs/development.md
M package-lock.json
M package.json
M src/client/tsconfig.json
M src/common/components/app/app.tsx
M src/common/components/link.tsx
A src/common/components/page-summary/page-summary.css
A src/common/components/page-summary/page-summary.tsx
M src/common/components/paper/paper.tsx
A src/common/data-clients/page-data-client.ts
M src/common/models/page.ts
M src/common/pages/wiki.tsx
M src/common/routers/api.ts
M src/common/routers/route.ts
M src/common/routers/router.ts
A src/common/types/isomorphic-unfetch.d.ts
M src/common/types/preact.d.ts
M src/server/components/page.tsx
M src/server/index.tsx
M src/server/tsconfig.json
M tsconfig.json
M webpack.config.ts
22 files changed, 227 insertions(+), 967 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/marvin refs/changes/67/378267/1

diff --git a/docs/development.md b/docs/development.md
index 41111d2..bf1b6f3 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -124,8 +124,13 @@
 - Static constants should be written in `SHOUTING_SNAKE_CASE`. All other
   variables should be written in `camelCase`.
 - Preact components should be written in PascalCase.
-- Preact component properties variables and types, especially
-  `ComponentProps` implementations, should be called "props" and "Props".
+
+#### Abbreviations
+
+Marvin uses the following abbreviations:
+
+- Properties => props
+- Parameters => params
 
 ### Filenaming
 
diff --git a/package-lock.json b/package-lock.json
index 319c07a..4b41755 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -711,7 +711,6 @@
       "requires": {
         "anymatch": "1.3.2",
         "async-each": "1.0.1",
-        "fsevents": "1.1.2",
         "glob-parent": "2.0.0",
         "inherits": "2.0.3",
         "is-binary-path": "1.0.1",
@@ -1445,6 +1444,14 @@
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz";,
       "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
     },
+    "encoding": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz";,
+      "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+      "requires": {
+        "iconv-lite": "0.4.18"
+      }
+    },
     "end-of-stream": {
       "version": "1.4.0",
       "resolved": 
"https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz";,
@@ -2119,905 +2126,6 @@
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
       "dev": true
     },
-    "fsevents": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz";,
-      "integrity": 
"sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "nan": "2.7.0",
-        "node-pre-gyp": "0.6.36"
-      },
-      "dependencies": {
-        "abbrev": {
-          "version": "1.1.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "ajv": {
-          "version": "4.11.8",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "co": "4.6.0",
-            "json-stable-stringify": "1.0.1"
-          }
-        },
-        "ansi-regex": {
-          "version": "2.1.1",
-          "bundled": true,
-          "dev": true
-        },
-        "aproba": {
-          "version": "1.1.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "are-we-there-yet": {
-          "version": "1.1.4",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "delegates": "1.0.0",
-            "readable-stream": "2.2.9"
-          }
-        },
-        "asn1": {
-          "version": "0.2.3",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "assert-plus": {
-          "version": "0.2.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "asynckit": {
-          "version": "0.4.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "aws-sign2": {
-          "version": "0.6.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "aws4": {
-          "version": "1.6.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "balanced-match": {
-          "version": "0.4.2",
-          "bundled": true,
-          "dev": true
-        },
-        "bcrypt-pbkdf": {
-          "version": "1.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "tweetnacl": "0.14.5"
-          }
-        },
-        "block-stream": {
-          "version": "0.0.9",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "inherits": "2.0.3"
-          }
-        },
-        "boom": {
-          "version": "2.10.1",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "hoek": "2.16.3"
-          }
-        },
-        "brace-expansion": {
-          "version": "1.1.7",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "balanced-match": "0.4.2",
-            "concat-map": "0.0.1"
-          }
-        },
-        "buffer-shims": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true
-        },
-        "caseless": {
-          "version": "0.12.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "co": {
-          "version": "4.6.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "code-point-at": {
-          "version": "1.1.0",
-          "bundled": true,
-          "dev": true
-        },
-        "combined-stream": {
-          "version": "1.0.5",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "delayed-stream": "1.0.0"
-          }
-        },
-        "concat-map": {
-          "version": "0.0.1",
-          "bundled": true,
-          "dev": true
-        },
-        "console-control-strings": {
-          "version": "1.1.0",
-          "bundled": true,
-          "dev": true
-        },
-        "core-util-is": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true
-        },
-        "cryptiles": {
-          "version": "2.0.5",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "boom": "2.10.1"
-          }
-        },
-        "dashdash": {
-          "version": "1.14.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "assert-plus": "1.0.0"
-          },
-          "dependencies": {
-            "assert-plus": {
-              "version": "1.0.0",
-              "bundled": true,
-              "dev": true,
-              "optional": true
-            }
-          }
-        },
-        "debug": {
-          "version": "2.6.8",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "deep-extend": {
-          "version": "0.4.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "delayed-stream": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true
-        },
-        "delegates": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "ecc-jsbn": {
-          "version": "0.1.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "jsbn": "0.1.1"
-          }
-        },
-        "extend": {
-          "version": "3.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "extsprintf": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true
-        },
-        "forever-agent": {
-          "version": "0.6.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "form-data": {
-          "version": "2.1.4",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "asynckit": "0.4.0",
-            "combined-stream": "1.0.5",
-            "mime-types": "2.1.15"
-          }
-        },
-        "fs.realpath": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true
-        },
-        "fstream": {
-          "version": "1.0.11",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "graceful-fs": "4.1.11",
-            "inherits": "2.0.3",
-            "mkdirp": "0.5.1",
-            "rimraf": "2.6.1"
-          }
-        },
-        "fstream-ignore": {
-          "version": "1.0.5",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "fstream": "1.0.11",
-            "inherits": "2.0.3",
-            "minimatch": "3.0.4"
-          }
-        },
-        "gauge": {
-          "version": "2.7.4",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "aproba": "1.1.1",
-            "console-control-strings": "1.1.0",
-            "has-unicode": "2.0.1",
-            "object-assign": "4.1.1",
-            "signal-exit": "3.0.2",
-            "string-width": "1.0.2",
-            "strip-ansi": "3.0.1",
-            "wide-align": "1.1.2"
-          }
-        },
-        "getpass": {
-          "version": "0.1.7",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "assert-plus": "1.0.0"
-          },
-          "dependencies": {
-            "assert-plus": {
-              "version": "1.0.0",
-              "bundled": true,
-              "dev": true,
-              "optional": true
-            }
-          }
-        },
-        "glob": {
-          "version": "7.1.2",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "fs.realpath": "1.0.0",
-            "inflight": "1.0.6",
-            "inherits": "2.0.3",
-            "minimatch": "3.0.4",
-            "once": "1.4.0",
-            "path-is-absolute": "1.0.1"
-          }
-        },
-        "graceful-fs": {
-          "version": "4.1.11",
-          "bundled": true,
-          "dev": true
-        },
-        "har-schema": {
-          "version": "1.0.5",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "har-validator": {
-          "version": "4.2.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ajv": "4.11.8",
-            "har-schema": "1.0.5"
-          }
-        },
-        "has-unicode": {
-          "version": "2.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "hawk": {
-          "version": "3.1.3",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "boom": "2.10.1",
-            "cryptiles": "2.0.5",
-            "hoek": "2.16.3",
-            "sntp": "1.0.9"
-          }
-        },
-        "hoek": {
-          "version": "2.16.3",
-          "bundled": true,
-          "dev": true
-        },
-        "http-signature": {
-          "version": "1.1.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "assert-plus": "0.2.0",
-            "jsprim": "1.4.0",
-            "sshpk": "1.13.0"
-          }
-        },
-        "inflight": {
-          "version": "1.0.6",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "once": "1.4.0",
-            "wrappy": "1.0.2"
-          }
-        },
-        "inherits": {
-          "version": "2.0.3",
-          "bundled": true,
-          "dev": true
-        },
-        "ini": {
-          "version": "1.3.4",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "number-is-nan": "1.0.1"
-          }
-        },
-        "is-typedarray": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "isarray": {
-          "version": "1.0.0",
-          "bundled": true,
-          "dev": true
-        },
-        "isstream": {
-          "version": "0.1.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "jodid25519": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "jsbn": "0.1.1"
-          }
-        },
-        "jsbn": {
-          "version": "0.1.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "json-schema": {
-          "version": "0.2.3",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "json-stable-stringify": {
-          "version": "1.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "jsonify": "0.0.0"
-          }
-        },
-        "json-stringify-safe": {
-          "version": "5.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "jsonify": {
-          "version": "0.0.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "jsprim": {
-          "version": "1.4.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "assert-plus": "1.0.0",
-            "extsprintf": "1.0.2",
-            "json-schema": "0.2.3",
-            "verror": "1.3.6"
-          },
-          "dependencies": {
-            "assert-plus": {
-              "version": "1.0.0",
-              "bundled": true,
-              "dev": true,
-              "optional": true
-            }
-          }
-        },
-        "mime-db": {
-          "version": "1.27.0",
-          "bundled": true,
-          "dev": true
-        },
-        "mime-types": {
-          "version": "2.1.15",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "mime-db": "1.27.0"
-          }
-        },
-        "minimatch": {
-          "version": "3.0.4",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "brace-expansion": "1.1.7"
-          }
-        },
-        "minimist": {
-          "version": "0.0.8",
-          "bundled": true,
-          "dev": true
-        },
-        "mkdirp": {
-          "version": "0.5.1",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "minimist": "0.0.8"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "node-pre-gyp": {
-          "version": "0.6.36",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "mkdirp": "0.5.1",
-            "nopt": "4.0.1",
-            "npmlog": "4.1.0",
-            "rc": "1.2.1",
-            "request": "2.81.0",
-            "rimraf": "2.6.1",
-            "semver": "5.3.0",
-            "tar": "2.2.1",
-            "tar-pack": "3.4.0"
-          }
-        },
-        "nopt": {
-          "version": "4.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "abbrev": "1.1.0",
-            "osenv": "0.1.4"
-          }
-        },
-        "npmlog": {
-          "version": "4.1.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "are-we-there-yet": "1.1.4",
-            "console-control-strings": "1.1.0",
-            "gauge": "2.7.4",
-            "set-blocking": "2.0.0"
-          }
-        },
-        "number-is-nan": {
-          "version": "1.0.1",
-          "bundled": true,
-          "dev": true
-        },
-        "oauth-sign": {
-          "version": "0.8.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "object-assign": {
-          "version": "4.1.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "once": {
-          "version": "1.4.0",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "wrappy": "1.0.2"
-          }
-        },
-        "os-homedir": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "os-tmpdir": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "osenv": {
-          "version": "0.1.4",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "os-homedir": "1.0.2",
-            "os-tmpdir": "1.0.2"
-          }
-        },
-        "path-is-absolute": {
-          "version": "1.0.1",
-          "bundled": true,
-          "dev": true
-        },
-        "performance-now": {
-          "version": "0.2.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "process-nextick-args": {
-          "version": "1.0.7",
-          "bundled": true,
-          "dev": true
-        },
-        "punycode": {
-          "version": "1.4.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "qs": {
-          "version": "6.4.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "rc": {
-          "version": "1.2.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "deep-extend": "0.4.2",
-            "ini": "1.3.4",
-            "minimist": "1.2.0",
-            "strip-json-comments": "2.0.1"
-          },
-          "dependencies": {
-            "minimist": {
-              "version": "1.2.0",
-              "bundled": true,
-              "dev": true,
-              "optional": true
-            }
-          }
-        },
-        "readable-stream": {
-          "version": "2.2.9",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "buffer-shims": "1.0.0",
-            "core-util-is": "1.0.2",
-            "inherits": "2.0.3",
-            "isarray": "1.0.0",
-            "process-nextick-args": "1.0.7",
-            "string_decoder": "1.0.1",
-            "util-deprecate": "1.0.2"
-          }
-        },
-        "request": {
-          "version": "2.81.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "aws-sign2": "0.6.0",
-            "aws4": "1.6.0",
-            "caseless": "0.12.0",
-            "combined-stream": "1.0.5",
-            "extend": "3.0.1",
-            "forever-agent": "0.6.1",
-            "form-data": "2.1.4",
-            "har-validator": "4.2.1",
-            "hawk": "3.1.3",
-            "http-signature": "1.1.1",
-            "is-typedarray": "1.0.0",
-            "isstream": "0.1.2",
-            "json-stringify-safe": "5.0.1",
-            "mime-types": "2.1.15",
-            "oauth-sign": "0.8.2",
-            "performance-now": "0.2.0",
-            "qs": "6.4.0",
-            "safe-buffer": "5.0.1",
-            "stringstream": "0.0.5",
-            "tough-cookie": "2.3.2",
-            "tunnel-agent": "0.6.0",
-            "uuid": "3.0.1"
-          }
-        },
-        "rimraf": {
-          "version": "2.6.1",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "glob": "7.1.2"
-          }
-        },
-        "safe-buffer": {
-          "version": "5.0.1",
-          "bundled": true,
-          "dev": true
-        },
-        "semver": {
-          "version": "5.3.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "set-blocking": {
-          "version": "2.0.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "signal-exit": {
-          "version": "3.0.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "sntp": {
-          "version": "1.0.9",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "hoek": "2.16.3"
-          }
-        },
-        "sshpk": {
-          "version": "1.13.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "asn1": "0.2.3",
-            "assert-plus": "1.0.0",
-            "bcrypt-pbkdf": "1.0.1",
-            "dashdash": "1.14.1",
-            "ecc-jsbn": "0.1.1",
-            "getpass": "0.1.7",
-            "jodid25519": "1.0.2",
-            "jsbn": "0.1.1",
-            "tweetnacl": "0.14.5"
-          },
-          "dependencies": {
-            "assert-plus": {
-              "version": "1.0.0",
-              "bundled": true,
-              "dev": true,
-              "optional": true
-            }
-          }
-        },
-        "string-width": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "code-point-at": "1.1.0",
-            "is-fullwidth-code-point": "1.0.0",
-            "strip-ansi": "3.0.1"
-          }
-        },
-        "string_decoder": {
-          "version": "1.0.1",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "safe-buffer": "5.0.1"
-          }
-        },
-        "stringstream": {
-          "version": "0.0.5",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "strip-ansi": {
-          "version": "3.0.1",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "ansi-regex": "2.1.1"
-          }
-        },
-        "strip-json-comments": {
-          "version": "2.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "tar": {
-          "version": "2.2.1",
-          "bundled": true,
-          "dev": true,
-          "requires": {
-            "block-stream": "0.0.9",
-            "fstream": "1.0.11",
-            "inherits": "2.0.3"
-          }
-        },
-        "tar-pack": {
-          "version": "3.4.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "debug": "2.6.8",
-            "fstream": "1.0.11",
-            "fstream-ignore": "1.0.5",
-            "once": "1.4.0",
-            "readable-stream": "2.2.9",
-            "rimraf": "2.6.1",
-            "tar": "2.2.1",
-            "uid-number": "0.0.6"
-          }
-        },
-        "tough-cookie": {
-          "version": "2.3.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "punycode": "1.4.1"
-          }
-        },
-        "tunnel-agent": {
-          "version": "0.6.0",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "safe-buffer": "5.0.1"
-          }
-        },
-        "tweetnacl": {
-          "version": "0.14.5",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "uid-number": {
-          "version": "0.0.6",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "util-deprecate": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true
-        },
-        "uuid": {
-          "version": "3.0.1",
-          "bundled": true,
-          "dev": true,
-          "optional": true
-        },
-        "verror": {
-          "version": "1.3.6",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "extsprintf": "1.0.2"
-          }
-        },
-        "wide-align": {
-          "version": "1.1.2",
-          "bundled": true,
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "string-width": "1.0.2"
-          }
-        },
-        "wrappy": {
-          "version": "1.0.2",
-          "bundled": true,
-          "dev": true
-        }
-      }
-    },
     "function-bind": {
       "version": "1.1.0",
       "resolved": 
"https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz";,
@@ -3363,8 +2471,7 @@
     "iconv-lite": {
       "version": "0.4.18",
       "resolved": 
"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz";,
-      "integrity": 
"sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==",
-      "dev": true
+      "integrity": 
"sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA=="
     },
     "icss-replace-symbols": {
       "version": "1.1.0",
@@ -3831,8 +2938,7 @@
     "is-stream": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz";,
-      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-      "dev": true
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
     },
     "is-svg": {
       "version": "2.1.0",
@@ -3874,6 +2980,15 @@
       "dev": true,
       "requires": {
         "isarray": "1.0.0"
+      }
+    },
+    "isomorphic-unfetch": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-2.0.0.tgz";,
+      "integrity": "sha1-9QFApMFj11grXzfxWRloxPgJpkU=",
+      "requires": {
+        "node-fetch": "1.7.3",
+        "unfetch": "3.0.0"
       }
     },
     "jest-docblock": {
@@ -4684,13 +3799,6 @@
       "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
       "dev": true
     },
-    "nan": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz";,
-      "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=",
-      "dev": true,
-      "optional": true
-    },
     "native-promise-only": {
       "version": "0.8.1",
       "resolved": 
"https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz";,
@@ -4750,6 +3858,15 @@
             "isarray": "0.0.1"
           }
         }
+      }
+    },
+    "node-fetch": {
+      "version": "1.7.3",
+      "resolved": 
"https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz";,
+      "integrity": 
"sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+      "requires": {
+        "encoding": "0.1.12",
+        "is-stream": "1.1.0"
       }
     },
     "node-forge": {
@@ -6726,6 +5843,15 @@
       "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
       "dev": true
     },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": 
"https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz";,
+      "integrity": 
"sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
     "string-length": {
       "version": "1.0.1",
       "resolved": 
"https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz";,
@@ -6771,15 +5897,6 @@
         "define-properties": "1.1.2",
         "es-abstract": "1.8.0",
         "function-bind": "1.1.0"
-      }
-    },
-    "string_decoder": {
-      "version": "1.0.3",
-      "resolved": 
"https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz";,
-      "integrity": 
"sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "5.1.1"
       }
     },
     "strip-ansi": {
@@ -7198,6 +6315,11 @@
       "integrity": "sha1-7Mo6A+VrmvFzhbqsgSrIO5lKli8=",
       "dev": true
     },
+    "unfetch": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.0.0.tgz";,
+      "integrity": "sha1-jR4FE6Ts0OX/LUGmund3Gq6LZII="
+    },
     "uniq": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz";,
diff --git a/package.json b/package.json
index 10e9a83..ba0eccd 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
   },
   "dependencies": {
     "express": "^4.15.3",
+    "isomorphic-unfetch": "^2.0.0",
     "path-to-regexp": "^2.0.0",
     "preact": "^8.2.1",
     "preact-render-to-string": "^3.6.3"
diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json
index 89f6fd7..27816ce 100644
--- a/src/client/tsconfig.json
+++ b/src/client/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "extends": "../../tsconfig.json",
   "include": [
-    "."
+    "../common", "."
   ],
   "compilerOptions": {
     "target": "ES5",
diff --git a/src/common/components/app/app.tsx 
b/src/common/components/app/app.tsx
index df006bd..c7aa9a3 100644
--- a/src/common/components/app/app.tsx
+++ b/src/common/components/app/app.tsx
@@ -1,11 +1,15 @@
 import "./app.css";
-import { ComponentProps, h } from "preact";
 import { about, index, wiki } from "../../../common/routers/api";
+import { FunctionalProps } from "../../types/preact";
 import Link from "../link";
+import { h } from "preact";
 
-export default function App({ children }: ComponentProps<any>): JSX.Element {
+export default function App({
+  children,
+  ...props
+}: FunctionalProps<void>): JSX.Element {
   return (
-    <div class="App">
+    <div class="App" {...props as JSX.HTMLAttributes}>
       <ul>
         <li>
           <Link href={index.url()}>Home</Link>
@@ -17,9 +21,19 @@
           <Link href={wiki.url({ title: "Banana" })}>Banana</Link>
         </li>
         <li>
+          <Link
+            href={wiki.url({ title: "Bill_%26_Ted%27s_Excellent_Adventure" })}
+          >
+            Bill &amp; Ted's Excellent Adventure
+          </Link>
+        </li>
+        <li>
           <Link href={wiki.url({ title: "Cucumber" })}>Cucumber</Link>
         </li>
         <li>
+          <Link href={wiki.url({ title: "Ice_cream" })}>Ice cream</Link>
+        </li>
+        <li>
           <Link href="/404">404</Link>
         </li>
       </ul>
diff --git a/src/common/components/link.tsx b/src/common/components/link.tsx
index 60dc612..a8e20b0 100644
--- a/src/common/components/link.tsx
+++ b/src/common/components/link.tsx
@@ -1,7 +1,8 @@
-import { ComponentProps, h } from "preact";
+import { FunctionalProps } from "../types/preact";
 import { History } from "history";
+import { h } from "preact";
 
-export interface Props extends ComponentProps<any> {
+export interface Props extends FunctionalProps<void> {
   href: string;
 }
 
@@ -16,7 +17,7 @@
   const { children, ...anchorProps } = props;
   return (
     <a
-      {...anchorProps}
+      {...anchorProps as JSX.HTMLAttributes}
       onClick={event => {
         // todo: check that link is internal (relative).
         if (context.history) {
diff --git a/src/common/components/page-summary/page-summary.css 
b/src/common/components/page-summary/page-summary.css
new file mode 100644
index 0000000..fb07c7c
--- /dev/null
+++ b/src/common/components/page-summary/page-summary.css
@@ -0,0 +1,24 @@
+.PageSummary {
+}
+
+.PageSummary-header {}
+.PageSummary-title {}
+.PageSummary-description {}
+.PageSummary-description::first-letter {
+  text-transform: uppercase;
+}
+
+.PageSummary-body {
+}
+.PageSummary-thumbnail {
+  float: left;
+  margin: 0 var(--space) var(--space) 0;
+}
+.PageSummary-extract {
+}
+
+.PageSummary-footer {
+  clear: both;
+}
+.PageSummary-lastModified {
+}
diff --git a/src/common/components/page-summary/page-summary.tsx 
b/src/common/components/page-summary/page-summary.tsx
new file mode 100644
index 0000000..662a187
--- /dev/null
+++ b/src/common/components/page-summary/page-summary.tsx
@@ -0,0 +1,44 @@
+import "./page-summary.css";
+import { FunctionalProps } from "../../types/preact";
+import { PageSummary as PageSummaryModel } from "../../models/page";
+import Paper from "../paper/paper";
+import { h } from "preact";
+
+export interface Props extends FunctionalProps<void> {
+  summary: PageSummaryModel;
+}
+
+export const PageSummary = ({ summary, ...props }: Props): JSX.Element => (
+  <Paper className="PageSummary" {...props}>
+    <div className="PageSummary-header">
+      <h2
+        className="PageSummary-title"
+        dangerouslySetInnerHTML={{ __html: summary.titleHTML }}
+      />
+      {summary.descriptionText && (
+        <p className="PageSummary-description">{summary.descriptionText}</p>
+      )}
+    </div>
+    <div className="PageSummary-body">
+      {summary.thumbnail && (
+        <img
+          className="PageSummary-thumbnail"
+          src={summary.thumbnail.URL}
+          width={summary.thumbnail.width}
+          height={summary.thumbnail.height}
+        />
+      )}
+      {summary.extractHTML && (
+        <p
+          className="PageSummary-extract"
+          dangerouslySetInnerHTML={{ __html: summary.extractHTML }}
+        />
+      )}
+    </div>
+    <div className="PageSummary-footer">
+      <p className="PageSummary-lastModified">
+        {summary.lastModified.toLocaleString()}
+      </p>
+    </div>
+  </Paper>
+);
diff --git a/src/common/components/paper/paper.tsx 
b/src/common/components/paper/paper.tsx
index 6e8ba60..1028da4 100644
--- a/src/common/components/paper/paper.tsx
+++ b/src/common/components/paper/paper.tsx
@@ -1,6 +1,14 @@
 import "./paper.css";
-import { ComponentProps, h } from "preact";
+import { FunctionalProps } from "../../types/preact";
+import { h } from "preact";
 
-export default function Paper({ children }: ComponentProps<any>): JSX.Element {
-  return <div class="Paper">{children}</div>;
+export default function Paper({
+  children,
+  ...props
+}: FunctionalProps<void>): JSX.Element {
+  return (
+    <div className="Paper" {...props as JSX.HTMLAttributes}>
+      {children}
+    </div>
+  );
 }
diff --git a/src/common/data-clients/page-data-client.ts 
b/src/common/data-clients/page-data-client.ts
new file mode 100644
index 0000000..fabff7f
--- /dev/null
+++ b/src/common/data-clients/page-data-client.ts
@@ -0,0 +1,19 @@
+import * as fetch from "isomorphic-unfetch";
+import { PageSummary } from "../models/page";
+import { unmarshalPageSummary } from "../marshallers/page-unmarshaller";
+
+export interface Params {
+  /**
+   * URL encoded wiki page title. e.g.: Banana, Ice_cream,
+   * Bill_%26_Ted%27s_Excellent_Adventure.
+   */
+  title: string;
+}
+
+const url = ({ title }: Params) =>
+  `https://en.wikipedia.org/api/rest_v1/page/summary/${title}`;
+
+export const requestPageSummary = (params: Params): Promise<PageSummary> =>
+  fetch(url(params))
+    .then((response: Response) => response.json())
+    .then(unmarshalPageSummary);
diff --git a/src/common/models/page.ts b/src/common/models/page.ts
index 1411bc6..6be38a1 100644
--- a/src/common/models/page.ts
+++ b/src/common/models/page.ts
@@ -21,6 +21,10 @@
   localeDirection: string;
   pageID: number;
   lastModified: Date;
+  /**
+   * Page title in English. e.g.: Banana, Ice cream, Bill & Ted's Excellent
+   * Adventure.
+   */
   titleText: string;
   titleHTML: string;
   descriptionText: string;
diff --git a/src/common/pages/wiki.tsx b/src/common/pages/wiki.tsx
index 995f443..c67efa9 100644
--- a/src/common/pages/wiki.tsx
+++ b/src/common/pages/wiki.tsx
@@ -1,21 +1,26 @@
 import { ComponentProps, h } from "preact";
 import App from "../components/app/app";
-import { RouteParameters } from "../routers/route";
+import { PageSummary } from "../components/page-summary/page-summary";
+import { PageSummary as PageSummaryModel } from "../models/page";
+import Paper from "../components/paper/paper";
+import { RouteParams } from "../routers/route";
+import { requestPageSummary } from "../data-clients/page-data-client";
 
-export interface Parameters extends RouteParameters {
+export interface Params extends RouteParams {
   title: string;
 }
 
 export interface Props extends ComponentProps<any> {
-  title: string;
+  summary: PageSummaryModel;
 }
 
-export const initialProps = ({ title }: Parameters): Promise<Props> => {
-  return Promise.resolve({ title });
-};
+export const initialProps = ({ title }: Params): Promise<Props> =>
+  requestPageSummary({ title }).then(summary => ({ summary }));
 
-export const Component = ({ title }: Props): JSX.Element => (
+export const Component = ({ summary }: Props): JSX.Element => (
   <App>
-    <p>{title}</p>
+    <Paper>
+      <PageSummary summary={summary} />
+    </Paper>
   </App>
 );
diff --git a/src/common/routers/api.ts b/src/common/routers/api.ts
index c4855b2..726e4fb 100644
--- a/src/common/routers/api.ts
+++ b/src/common/routers/api.ts
@@ -1,5 +1,5 @@
 import { AnyRoute, Route, newRoute } from "./route";
-import { Props as WikiProps } from "../pages/wiki";
+import { Params as WikiParams, Props as WikiProps } from "../pages/wiki";
 
 export const index: Route = newRoute({
   path: "/",
@@ -15,7 +15,7 @@
   chunkName: "pages/about"
 });
 
-export const wiki: Route<WikiProps, void, { title: string }> = newRoute({
+export const wiki: Route<WikiProps, void, WikiParams> = newRoute({
   path: "/wiki/:title",
   endpoint: () => import(/* webpackChunkName: "pages/wiki" */ "../pages/wiki"),
   chunkName: "pages/wiki"
diff --git a/src/common/routers/route.ts b/src/common/routers/route.ts
index af75f4b..cbd5cb3 100644
--- a/src/common/routers/route.ts
+++ b/src/common/routers/route.ts
@@ -1,7 +1,7 @@
 import * as pathToRegExp from "path-to-regexp";
 import { AnyComponent } from "preact";
 
-export interface RouteParameters {
+export interface RouteParams {
   [name: string]: string;
 }
 
@@ -13,7 +13,7 @@
    * A function that returns a Promise for the dependencies needed to construct
    * the view component such as a remote resource.
    */
-  initialProps?: (parameters: RouteParameters) => Promise<Props>;
+  initialProps?: (parameters: RouteParams) => Promise<Props>;
 }
 
 export interface RouteConfiguration<Props = void, State = void> {
@@ -28,7 +28,7 @@
   status: number;
 
   /** Generates a URL from parameters. */
-  url: (properties?: Parameters) => string;
+  url: (parameters?: Parameters) => string;
 }
 
 export type AnyRoute = Route<any, any, any>;
diff --git a/src/common/routers/router.ts b/src/common/routers/router.ts
index 695329d..af6e684 100644
--- a/src/common/routers/router.ts
+++ b/src/common/routers/router.ts
@@ -1,9 +1,5 @@
 import * as pathToRegExp from "path-to-regexp";
-import {
-  AnyRoute,
-  Endpoint,
-  RouteParameters
-} from "../../common/routers/route";
+import { AnyRoute, Endpoint, RouteParams } from "../../common/routers/route";
 import { AnyComponent } from "preact";
 
 export interface RouteResponse<Props, State> {
@@ -40,10 +36,10 @@
 const newRouteParameters = (
   parameterNames: pathToRegExp.Key[],
   parameterValues: string[]
-): RouteParameters =>
+): RouteParams =>
   parameterNames.reduce(
     (
-      parameters: RouteParameters,
+      parameters: RouteParams,
       parameterName: pathToRegExp.Key,
       index: number
     ) => {
@@ -55,7 +51,7 @@
 
 function requestInitialProps<Props>(
   endpoint: Endpoint<Props, any>,
-  parameters: RouteParameters
+  parameters: RouteParams
 ): Promise<Props | {}> {
   if (endpoint.initialProps) {
     return endpoint.initialProps(parameters);
@@ -66,7 +62,7 @@
 const respond = (
   route: ParsedRoute,
   url: string,
-  parameters: RouteParameters
+  parameters: RouteParams
 ): Promise<RouteResponse<any, any>> =>
   route.endpoint().then((endpoint: Endpoint<any, any>) =>
     requestInitialProps(endpoint, parameters).then((props: any) => ({
diff --git a/src/common/types/isomorphic-unfetch.d.ts 
b/src/common/types/isomorphic-unfetch.d.ts
new file mode 100644
index 0000000..7dd0676
--- /dev/null
+++ b/src/common/types/isomorphic-unfetch.d.ts
@@ -0,0 +1,5 @@
+declare const unfetch: typeof fetch;
+
+declare module "isomorphic-unfetch" {
+  export = unfetch;
+}
diff --git a/src/common/types/preact.d.ts b/src/common/types/preact.d.ts
index 7ac7b19..46709d3 100644
--- a/src/common/types/preact.d.ts
+++ b/src/common/types/preact.d.ts
@@ -1,2 +1,14 @@
+import { ComponentProps, FunctionalComponent } from "preact";
+
 // Delete pending https://github.com/developit/preact/pull/869.
-export type Children = (JSX.Element | JSX.Element[] | string)[];
+export type ComponentChild = JSX.Element | string;
+export type ComponentChildren = (ComponentChild | ComponentChild[])[];
+
+// A subset of JSX.HTMLAttributes that make sense to expose for any component.
+export interface ComponentAttributes {
+  className?: string;
+}
+
+export interface FunctionalProps<T>
+  extends ComponentProps<FunctionalComponent<T>>,
+    ComponentAttributes {}
diff --git a/src/server/components/page.tsx b/src/server/components/page.tsx
index 18099e9..60bc7ff 100644
--- a/src/server/components/page.tsx
+++ b/src/server/components/page.tsx
@@ -1,5 +1,5 @@
 import { Manifest, asset, scripts, style } from "../assets/manifest";
-import { Children } from "../../common/types/preact";
+import { ComponentChildren } from "../../common/types/preact";
 import { h } from "preact";
 
 export interface Props {
@@ -8,7 +8,7 @@
   manifest: Manifest;
   chunkName: string;
   // HTML to render in the body of the page
-  children?: Children;
+  children?: ComponentChildren;
 }
 
 export function Page({
@@ -24,7 +24,7 @@
       <head>
         <meta charSet="utf-8" />
         <meta http-equiv="x-ua-compatible" content="ie=edge" />
-        <meta name="viewport" content="width=device-width,initial-scale=1" />
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
         <title>{title ? `${title} - ` : ""}Marvin</title>
         <base href="/" />
         <link rel="stylesheet" href={style(manifest)} />
diff --git a/src/server/index.tsx b/src/server/index.tsx
index aea614a..c28215f 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -24,12 +24,12 @@
 
 server.use("/public", express.static("dist/public"));
 
-const render = ({ chunkName, Component }: RouteResponse<any, any>) => {
+const render = ({ chunkName, Component, props }: RouteResponse<any, any>) => {
   return (
     "<!doctype html>" + // eslint-disable-line prefer-template
     renderToString(
       <Page title="" manifest={manifest} chunkName={chunkName}>
-        <Component />
+        <Component {...props} />
       </Page>
     )
   );
diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json
index 423de29..0bd1839 100644
--- a/src/server/tsconfig.json
+++ b/src/server/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "extends": "../../tsconfig.json",
   "include": [
-    "."
+    "../common", "."
   ],
   "compilerOptions": {
     "target": "ES2015",
diff --git a/tsconfig.json b/tsconfig.json
index 4d3ea6e..6d9edb1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,9 +5,6 @@
     "test/**/*.test.ts",
     "test/**/*.test.js"
   ],
-  "include": [
-    "src/common"
-  ],
   "compilerOptions": {
     "jsx": "react",
     "jsxFactory": "h",
diff --git a/webpack.config.ts b/webpack.config.ts
index c9e514a..8694499 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -57,10 +57,10 @@
     // each entry which breaks caching. This chunk changes whenever any code
     // anywhere changes.
 
-    // Client package dependencies (these should match package.json's
+    // Client package dependencies (these should be a subset of package.json's
     // `dependencies`). This chunk changes when one of the specified
     // dependencies changes.
-    vendor: ["history", "path-to-regexp", "preact"]
+    vendor: ["history", "isomorphic-unfetch", "path-to-regexp", "preact"]
   },
 
   stats: STATS,
@@ -184,6 +184,8 @@
   new webpack.NamedModulesPlugin(),
 
   new ExtractTextPlugin({
+    // `contenthash` is not actually a chunk hash:
+    // 
https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/504#issuecomment-306581954.
     filename: PRODUCTION ? "[name].[contenthash].css" : "[name].css"
   }),
 
@@ -206,7 +208,8 @@
   // during execution for module resolution, dynamic importing, and more.
   // Without this distinct runtime chunk, it's instead bundled into each entry,
   // including vendor, which breaks caching. This chunk changes whenever any
-  // other file changes.
+  // other file changes. See
+  // https://webpack.js.org/plugins/commons-chunk-plugin/#manifest-file.
   new webpack.optimize.CommonsChunkPlugin({
     // This name should NOT match any `configuration.entry`.
     name: "runtime"

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I7456e654e24db34343bd8e108bc990be8fff31fe
Gerrit-PatchSet: 1
Gerrit-Project: marvin
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <sniedziel...@wikimedia.org>
Gerrit-Reviewer: Sniedzielski <sniedziel...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to