diff --git a/package-lock.json b/package-lock.json index 6a1cd758ca..e674eafb81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2489,6 +2489,12 @@ "optimism": "^0.6.6" } }, + "apollo-cache-persist": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/apollo-cache-persist/-/apollo-cache-persist-0.1.1.tgz", + "integrity": "sha512-/7GAyblPR169ryW3ugbtHqiU0UGkhIt10NeaO2gn2ClxjLHF/nIkJD5mx/0OCF2vLNbbnzLZVDeIO1pf72TrEA==", + "dev": true + }, "apollo-client": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.4.7.tgz", @@ -2957,7 +2963,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -6573,7 +6579,7 @@ }, "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { @@ -8905,24 +8911,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.4", - "bundled": true, + "resolved": false, + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "dev": true, "optional": true, "requires": { @@ -8932,12 +8942,14 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": false, + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -8946,34 +8958,40 @@ }, "chownr": { "version": "1.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, "concat-map": { "version": "0.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "2.6.9", - "bundled": true, + "resolved": false, + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "optional": true, "requires": { @@ -8982,25 +9000,29 @@ }, "deep-extend": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, "requires": { @@ -9009,13 +9031,15 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": false, + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, "requires": { @@ -9031,7 +9055,8 @@ }, "glob": { "version": "7.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "optional": true, "requires": { @@ -9045,13 +9070,15 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.21", - "bundled": true, + "resolved": false, + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "dev": true, "optional": true, "requires": { @@ -9060,7 +9087,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, "requires": { @@ -9069,7 +9097,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, "requires": { @@ -9079,18 +9108,21 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { "number-is-nan": "^1.0.0" @@ -9098,13 +9130,15 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -9112,12 +9146,14 @@ }, "minimist": { "version": "0.0.8", - "bundled": true, + "resolved": false, + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, "minipass": { "version": "2.2.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, "requires": { "safe-buffer": "^5.1.1", @@ -9126,7 +9162,8 @@ }, "minizlib": { "version": "1.1.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "dev": true, "optional": true, "requires": { @@ -9135,7 +9172,8 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { "minimist": "0.0.8" @@ -9143,13 +9181,15 @@ }, "ms": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true, "optional": true }, "needle": { "version": "2.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "dev": true, "optional": true, "requires": { @@ -9160,7 +9200,8 @@ }, "node-pre-gyp": { "version": "0.10.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "dev": true, "optional": true, "requires": { @@ -9178,7 +9219,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, "requires": { @@ -9188,13 +9230,15 @@ }, "npm-bundled": { "version": "1.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.1.10", - "bundled": true, + "resolved": false, + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "dev": true, "optional": true, "requires": { @@ -9204,7 +9248,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, "requires": { @@ -9216,18 +9261,21 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { "wrappy": "1" @@ -9235,19 +9283,22 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, "requires": { @@ -9257,19 +9308,22 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.7", - "bundled": true, + "resolved": false, + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "dev": true, "optional": true, "requires": { @@ -9281,7 +9335,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -9289,7 +9344,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, "requires": { @@ -9304,7 +9360,8 @@ }, "rimraf": { "version": "2.6.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "optional": true, "requires": { @@ -9313,42 +9370,49 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.5.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { "code-point-at": "^1.0.0", @@ -9358,7 +9422,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -9367,7 +9432,8 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -9375,13 +9441,15 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", "dev": true, "optional": true, "requires": { @@ -9396,13 +9464,15 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "dev": true, "optional": true, "requires": { @@ -9411,12 +9481,14 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "yallist": { "version": "3.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", "dev": true } } @@ -14094,7 +14166,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, diff --git a/package.json b/package.json index f9e2a157ee..a28cf09616 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,12 @@ "devDependencies": { "@magento/directive-parser": "^0.1.1", "@magento/eslint-config": "^1.2.3", + "@storybook/addon-actions": "^3.4.2", + "@storybook/addons": "^3.4.6", "@storybook/react": "^3.4.2", "apollo-boost": "^0.1.20", "apollo-cache-inmemory": "^1.3.9", + "apollo-cache-persist": "^0.1.1", "apollo-client": "^2.4.5", "apollo-link-context": "^1.0.9", "apollo-server": "^2.0.5", diff --git a/packages/peregrine/src/Router/MagentoRouteHandler.js b/packages/peregrine/src/Router/MagentoRouteHandler.js index 3834e9bd5a..2de21af426 100644 --- a/packages/peregrine/src/Router/MagentoRouteHandler.js +++ b/packages/peregrine/src/Router/MagentoRouteHandler.js @@ -26,9 +26,17 @@ export default class MagentoRouteHandler extends Component { } }; + // TODO: Add the ability to customize the cache name + async addToCache(urls) { + const myCache = await window.caches.open( + `workbox-runtime-${location.origin}/` + ); + await myCache.addAll(urls); + } + componentDidMount() { mountedInstances.add(this); - this.getRouteComponent(this.props.location.pathname); + this.getRouteComponent(); } componentDidUpdate() { @@ -36,8 +44,16 @@ export default class MagentoRouteHandler extends Component { const { pathname } = props.location; const isKnown = state.componentMap.has(pathname); - if (!isKnown) { - this.getRouteComponent(pathname); + // `NOTFOUND` component needs a unique id + // currently it is set to -1 + const isNotFoundComponent = isKnown + ? state.componentMap.get(pathname).id === -1 + : false; + + const shouldReloadRoute = isNotFoundComponent && navigator.onLine; + + if (!isKnown || shouldReloadRoute) { + this.getRouteComponent(); } } @@ -88,6 +104,8 @@ export default class MagentoRouteHandler extends Component { return; } + this.addToCache([pathname]); + this.setState(({ componentMap }) => ({ componentMap: new Map(componentMap).set(pathname, { RootComponent, diff --git a/packages/peregrine/src/Router/__tests__/resolveUnknownRoute.test.js b/packages/peregrine/src/Router/__tests__/resolveUnknownRoute.test.js index d02fd682ae..2a921e9918 100644 --- a/packages/peregrine/src/Router/__tests__/resolveUnknownRoute.test.js +++ b/packages/peregrine/src/Router/__tests__/resolveUnknownRoute.test.js @@ -7,7 +7,48 @@ const urlResolverRes = (type, id) => } }); +const NotFoundManifest = { + NotFound: { + rootChunkID: -1, + rootModuleID: 100, + pageTypes: ['NOTFOUND'] + } +}; + +const mockManifest = { + Category: { + rootChunkID: 2, + rootModuleID: 100, + pageTypes: ['CATEGORY'] + }, + Product: { + rootChunkID: 1, + rootModuleID: 99, + pageTypes: ['PRODUCT'] + }, + ...NotFoundManifest +}; + +const cachedResponse = JSON.stringify({ + 'foo-bar.html': { + ...JSON.parse(urlResolverRes('PRODUCT')) + } +}); + +const isOnline = _value => ({ + get: () => _value, + set: v => (_value = v) +}); + +Object.defineProperty(navigator, 'onLine', isOnline(true)); + +function clearLocalStorage(item) { + localStorage.setItem(item, null); +} + beforeEach(() => { + navigator.onLine = true; + clearLocalStorage('urlResolve'); document.body.innerHTML = ''; resolveUnknownRoute.preloadDone = false; fetch.resetMocks(); @@ -18,7 +59,7 @@ test('Preload path: resolves directly from preload element', async () => { ''; const res = await resolveUnknownRoute({ route: 'foo-bar.html', - apiBase: 'https://store.com' + apiBase: 'https://example.com' }); expect(res).toMatchObject({ type: 'PRODUCT', @@ -26,6 +67,59 @@ test('Preload path: resolves directly from preload element', async () => { }); }); +test('returns NOTFOUND when offline and requested content is not in cache ', async () => { + navigator.onLine = false; + + fetch.mockResponseOnce(JSON.stringify(mockManifest)); + const res = await resolveUnknownRoute({ + route: 'foo-bar.html', + apiBase: 'https://store.com', + __tmp_webpack_public_path__: 'https://dev-server.com/pub' + }); + + expect(res).toHaveProperty('id', NotFoundManifest.NotFound.rootChunkID); +}); + +test('stores response of urlResolver in cache', async () => { + fetch.mockResponseOnce(urlResolverRes('PRODUCT')); + fetch.mockResponseOnce(JSON.stringify(mockManifest)); + + const url = 'foo-bar.html'; + + await resolveUnknownRoute({ + route: url, + apiBase: 'https://store.com', + __tmp_webpack_public_path__: 'https://dev-server.com/pub' + }); + + expect(localStorage.getItem('urlResolve')).not.toBeNull(); +}); + +test('does not call fetchRoute when response is cached', async () => { + localStorage.setItem('urlResolve', cachedResponse); + + fetch.mockResponseOnce(JSON.stringify(mockManifest)); + await resolveUnknownRoute({ + route: 'foo-bar.html', + apiBase: 'https://store.com', + __tmp_webpack_public_path__: 'https://dev-server.com/pub' + }); + + expect(fetch).toHaveBeenCalledTimes(0); +}); + +test('calls fetchRoute when response is not cached', async () => { + fetch.mockResponseOnce(urlResolverRes('PRODUCT')); + fetch.mockResponseOnce(JSON.stringify(mockManifest)); + await resolveUnknownRoute({ + route: 'foo-bar.html', + apiBase: 'https://store.com', + __tmp_webpack_public_path__: 'https://dev-server.com/pub' + }); + + expect(fetch).toHaveBeenCalledTimes(1); +}); + test('urlResolver path: resolve using fetch to GraphQL after one preload', async () => { fetch.mockResponseOnce(urlResolverRes('PRODUCT', 'VA-11')); document.body.innerHTML = diff --git a/packages/peregrine/src/Router/resolveUnknownRoute.js b/packages/peregrine/src/Router/resolveUnknownRoute.js index f186651a47..1c4e46d1e9 100644 --- a/packages/peregrine/src/Router/resolveUnknownRoute.js +++ b/packages/peregrine/src/Router/resolveUnknownRoute.js @@ -42,11 +42,37 @@ export default async function resolveUnknownRoute(opts) { } /** - * @description Calls the GraphQL API for results from the urlResolver query + * @description Checks if route is stored in localStorage, if not call `fetchRoute` * @param {{ route: string, apiBase: string}} opts * @returns {Promise<{type: "PRODUCT" | "CATEGORY" | "CMS_PAGE"}>} */ function remotelyResolveRoute(opts) { + let urlResolve = localStorage.getItem('urlResolve'); + urlResolve = JSON.parse(urlResolve); + + // If it exists in localStorage, use that value + // TODO: This can be handled by workbox once this issue is resolved in the + // graphql repo: https://github.com/magento/graphql-ce/issues/229 + if ((urlResolve && urlResolve[opts.route]) || !navigator.onLine) { + if (urlResolve && urlResolve[opts.route]) { + return Promise.resolve(urlResolve[opts.route].data.urlResolver); + } else { + return Promise.resolve({ + type: 'NOTFOUND', + id: -1 + }); + } + } else { + return fetchRoute(opts); + } +} + +/** + * @description Calls the GraphQL API for results from the urlResolver query + * @param {{ route: string, apiBase: string}} opts + * @returns {Promise<{type: "PRODUCT" | "CATEGORY" | "CMS_PAGE"}>} + */ +function fetchRoute(opts) { const url = new URL('/graphql', opts.apiBase); return fetch(url, { method: 'POST', @@ -66,5 +92,18 @@ function remotelyResolveRoute(opts) { }) }) .then(res => res.json()) - .then(res => res.data.urlResolver); + .then(res => { + storeURLResolveResult(res, opts); + return res.data.urlResolver; + }); +} + +// TODO: This can be handled by workbox once this issue is resolved in the +// graphql repo: https://github.com/magento/graphql-ce/issues/229 +function storeURLResolveResult(res, opts) { + const storedRoute = localStorage.getItem('urlResolve'); + const item = JSON.parse(storedRoute) || {}; + + item[opts.route] = res; + localStorage.setItem('urlResolve', JSON.stringify(item)); } diff --git a/packages/pwa-buildpack/src/WebpackTools/plugins/ServiceWorkerPlugin.js b/packages/pwa-buildpack/src/WebpackTools/plugins/ServiceWorkerPlugin.js index 609e803ffd..f5d9197ef2 100644 --- a/packages/pwa-buildpack/src/WebpackTools/plugins/ServiceWorkerPlugin.js +++ b/packages/pwa-buildpack/src/WebpackTools/plugins/ServiceWorkerPlugin.js @@ -13,7 +13,7 @@ class ServiceWorkerPlugin { ServiceWorkerPlugin.validateOptions('ServiceWorkerPlugin', config); this.config = config; } - applyWorkbox(compiler) { + applyGenerateSW(compiler) { const config = { // `globDirectory` and `globPatterns` must match at least 1 file // otherwise workbox throws an error @@ -28,25 +28,48 @@ class ServiceWorkerPlugin { swDest: this.config.serviceWorkerFileName }; - if (this.config.runtimeCacheAssetPath) { - config.runtimeCaching = [ - { - urlPattern: new RegExp(this.config.runtimeCacheAssetPath), - handler: 'staleWhileRevalidate' - } - ]; + if (this.config.runtimeCacheConfig) { + config.runtimeCaching = this.config.runtimeCacheConfig; } new WorkboxPlugin.GenerateSW(config).apply(compiler); } + + configureInjectManifest() { + let injectManifest; + if (this.config.injectManifestConfig) { + injectManifest = new WorkboxPlugin.InjectManifest( + this.config.injectManifestConfig + ); + } else { + injectManifest = new WorkboxPlugin.InjectManifest({ + swSrc: this.config.paths.src + '/sw.js', + swDest: this.config.paths.dest + '/sw.js' + }); + } + return injectManifest; + } + + applyInjectManifest(compiler) { + this.configureInjectManifest().apply(compiler); + } + apply(compiler) { if (this.config.env.mode === 'development') { // add a WriteFilePlugin to write out the service worker to the filesystem so it can be served by M2, even though it's under dev - if (this.config.enableServiceWorkerDebugging) { + if ( + this.config.enableServiceWorkerDebugging && + !this.config.injectManifest + ) { new WriteFileWebpackPlugin({ test: new RegExp(this.config.serviceWorkerFileName + '$'), log: true }).apply(compiler); - this.applyWorkbox(compiler); + this.applyGenerateSW(compiler); + } else if ( + this.config.enableServiceWorkerDebugging && + this.config.injectManifest + ) { + this.applyInjectManifest(compiler); } else { // TODO: (feature) emit a structured { code, severity, resolution } object // on Environment that might throw and might not @@ -58,5 +81,13 @@ class ServiceWorkerPlugin { this.applyWorkbox(compiler); } } + + applyWorkbox(compiler) { + if (this.config.injectManifest) { + this.applyInjectManifest(compiler); + } else { + this.applyGenerateSW(compiler); + } + } } module.exports = ServiceWorkerPlugin; diff --git a/packages/pwa-buildpack/src/WebpackTools/plugins/__tests__/ServiceWorkerPlugin.spec.js b/packages/pwa-buildpack/src/WebpackTools/plugins/__tests__/ServiceWorkerPlugin.spec.js index 463682f28c..76473a47ad 100644 --- a/packages/pwa-buildpack/src/WebpackTools/plugins/__tests__/ServiceWorkerPlugin.spec.js +++ b/packages/pwa-buildpack/src/WebpackTools/plugins/__tests__/ServiceWorkerPlugin.spec.js @@ -134,3 +134,34 @@ test('.apply generates and writes out a serviceworker when enableServiceWorkerDe }) ); }); + +test('.apply uses `InjectManifest` when `injectManifest` is `true`', () => { + const injectManifestConfig = { + swSrc: 'path/to/sw', + swDest: 'path/to/dest' + }; + const plugin = new ServiceWorkerPlugin({ + env: { + mode: 'development' + }, + enableServiceWorkerDebugging: true, + serviceWorkerFileName: 'sw.js', + injectManifest: true, + paths: { + output: 'path/to/assets' + }, + injectManifestConfig + }); + + const fakeCompiler = {}; + const workboxApply = jest.fn(); + WorkboxPlugin.InjectManifest.mockImplementationOnce(() => ({ + apply: workboxApply + })); + + plugin.apply(fakeCompiler); + + expect(WorkboxPlugin.InjectManifest).toHaveBeenCalledWith( + expect.objectContaining(injectManifestConfig) + ); +}); diff --git a/packages/venia-concept/media/favicon.ico b/packages/venia-concept/media/favicon.ico deleted file mode 100644 index fd57e1d1a9..0000000000 Binary files a/packages/venia-concept/media/favicon.ico and /dev/null differ diff --git a/packages/venia-concept/src/RootComponents/NotFound/index.js b/packages/venia-concept/src/RootComponents/NotFound/index.js new file mode 100644 index 0000000000..c098bd07f4 --- /dev/null +++ b/packages/venia-concept/src/RootComponents/NotFound/index.js @@ -0,0 +1,6 @@ +/** + * @RootComponent + * description = 'Page to display when offline' + * pageTypes = NOTFOUND + */ +export { default } from './notFound'; diff --git a/packages/venia-concept/src/RootComponents/NotFound/notFound.css b/packages/venia-concept/src/RootComponents/NotFound/notFound.css new file mode 100644 index 0000000000..e98375699b --- /dev/null +++ b/packages/venia-concept/src/RootComponents/NotFound/notFound.css @@ -0,0 +1,10 @@ +.root { + padding: 1rem; +} + +.title { + font-size: 1.5rem; + font-weight: 400; + margin: 0 0 1rem; + padding: 0.5rem; +} diff --git a/packages/venia-concept/src/RootComponents/NotFound/notFound.js b/packages/venia-concept/src/RootComponents/NotFound/notFound.js new file mode 100644 index 0000000000..654fc1fa0f --- /dev/null +++ b/packages/venia-concept/src/RootComponents/NotFound/notFound.js @@ -0,0 +1,28 @@ +import React, { Component } from 'react'; +import classify from 'src/classify'; +import defaultClasses from './notFound.css'; + +class NotFound extends Component { + // TODO: Should not be a default here, we just don't have + // the wiring in place to map route info down the tree (yet) + static defaultProps = { + id: 3 + }; + + goBack() { + history.back(); + } + + render() { + const { classes } = this.props; + + return ( +
You are offline. Some features may be unavailable.
+You are online.
+