diff --git a/package.json b/package.json index 3279d35..e62b2c7 100644 --- a/package.json +++ b/package.json @@ -24,22 +24,24 @@ "main": "server.js", "dependencies": { "ejs": "^2.4.1", + "es6-promise": "^3.2.1", + "eslint-config-jonnybuchanan": "2.0.3", "events": "1.1.0", "express": "^4.13.4", "firebase": "2.4.2", "history": "2.1.1", "isomorphic-fetch": "^2.2.1", + "nwb": "0.8.1", "react": "15.0.2", "react-dom": "15.0.2", + "react-resolver": "^3.0.1", "react-router": "2.4.0", "react-timeago": "3.0.0", "reactfire": "0.7.0", "scroll-behavior": "0.5.0", "setimmediate": "1.0.4", - "url-parse": "^1.1.1", - "eslint-config-jonnybuchanan": "2.0.3", - "nwb": "0.8.1", "sw-precache": "^3.1.1", - "sw-toolbox": "^3.1.1" + "sw-toolbox": "^3.1.1", + "url-parse": "^1.1.1" } } diff --git a/public/service-worker.js b/public/service-worker.js index 212d4fb..0e51c13 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,2 +1,249 @@ -// This file is intentionally without code. -// It's present so that service worker registration will work when serving from the 'public' directory. \ No newline at end of file +/** + * Copyright 2015 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This generated service worker JavaScript will precache your site's resources. +// The code needs to be saved in a .js file at the top-level of your site, and registered +// from your pages in order to be used. See +// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js +// for an example of how you can register this script and handle various service worker events. + +/* eslint-env worker, serviceworker */ +/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren */ +'use strict'; + + + + +importScripts("sw-toolbox.js","runtime-caching.js"); + + +/* eslint-disable quotes, comma-spacing */ +var PrecacheConfig = [["build/app.js","313d71d0a78527fb7beef3c3ff0138eb"],["build/app.js.map","44e5c1202bd9dc38796ebd8cb1c14040"],["build/vendor.js","f7535c3346e07c644907b4c1fec47a1b"],["css/style.css","ebd9bc483b1656c536c56c3035432bdd"],["img/android-chrome-144x144.png","31f44c8f8845e41196b389f2fdae392d"],["img/android-chrome-192x192.png","7c3470aa18f85e4454a35ef3ff8b8f6e"],["img/android-chrome-36x36.png","fc5a14316848badbd501e198f8607088"],["img/android-chrome-48x48.png","f8576bca4be18a4367e4847a09fc6945"],["img/android-chrome-72x72.png","6b26a8a135b07174d298489cc010083a"],["img/android-chrome-96x96.png","04cda150a70eb58221af3fdd4f7d4a6f"],["img/apple-touch-icon-114x114.png","e18affb685f0457672283a88c04084c9"],["img/apple-touch-icon-120x120.png","cd14469c7457cfc6d3aaf15d34faeddf"],["img/apple-touch-icon-144x144.png","95a8cb7d006c59252dd68ba73d31632a"],["img/apple-touch-icon-152x152.png","15dd03590ff7289c09cf10027597e699"],["img/apple-touch-icon-180x180.png","0b101591e8e263c6bff9133c7772194a"],["img/apple-touch-icon-57x57.png","628a477075d84a8d0996392aa6dec37c"],["img/apple-touch-icon-60x60.png","6b9fe001bc9e35320f9bb4eb28b1e6f1"],["img/apple-touch-icon-72x72.png","5830f2a4f9249b3bc3998481cc00825d"],["img/apple-touch-icon-76x76.png","812e9eb119b6bdd8f465a2d1118465b9"],["img/apple-touch-icon-precomposed.png","e45a9a06a4a9b850e3089c4e6e3ebc8d"],["img/apple-touch-icon.png","0b101591e8e263c6bff9133c7772194a"],["img/browserconfig.xml","f337354b6f80663075e7b32058c65149"],["img/favicon-16x16.png","9d784dc3f4da5477156423f5f106c1c6"],["img/favicon-32x32.png","21ea2cf9cd43cdc1f808cca76a1f6fa4"],["img/favicon-96x96.png","11e36fff4c95b572ffaeef9a848da568"],["img/favicon.ico","eaa33e22fc5dab05262d316b59160a45"],["img/logo.png","930a492dadf1ccb881bd91d424c8bf9e"],["img/mstile-144x144.png","3e9a3c273f9ac3b7a158132445534860"],["img/mstile-150x150.png","b0af3ec429e6828dc0606d8bb8e1421f"],["img/mstile-310x150.png","499b08d0d170e6ed89491d7e9691a8e8"],["img/mstile-310x310.png","625111493ee72a39db1420c9c235dfb3"],["img/mstile-70x70.png","4cdf64d2b55d8116c4ce8dd361a95772"],["img/safari-pinned-tab.svg","9bfe87bb482c5d6facab0d0084ce1e80"],["img/splashscreen-icon-384x384.png","e3080842f30a9137e1464f01ffb97e71"],["index-static.html","6b98cb5ee877097cadbd77807342c96c"],["manifest.json","3c0f2d46b398124d8358a69be42ee0c2"],["runtime-caching.js","87003e567d298b1b58cf2f57b4fb0ee2"],["service-worker.js","432e0ae0b3a06471d0c62c6844b88d07"],["sw-toolbox.js","42dd9073ba0a0c8e0ae2230432870678"]]; +/* eslint-enable quotes, comma-spacing */ +var CacheNamePrefix = 'sw-precache-v1-sw-precache-' + (self.registration ? self.registration.scope : '') + '-'; + + +var IgnoreUrlParametersMatching = [/^utm_/]; + + + +var addDirectoryIndex = function (originalUrl, index) { + var url = new URL(originalUrl); + if (url.pathname.slice(-1) === '/') { + url.pathname += index; + } + return url.toString(); + }; + +var getCacheBustedUrl = function (url, now) { + now = now || Date.now(); + + var urlWithCacheBusting = new URL(url); + urlWithCacheBusting.search += (urlWithCacheBusting.search ? '&' : '') + + 'sw-precache=' + now; + + return urlWithCacheBusting.toString(); + }; + +var isPathWhitelisted = function (whitelist, absoluteUrlString) { + // If the whitelist is empty, then consider all URLs to be whitelisted. + if (whitelist.length === 0) { + return true; + } + + // Otherwise compare each path regex to the path of the URL passed in. + var path = (new URL(absoluteUrlString)).pathname; + return whitelist.some(function(whitelistedPathRegex) { + return path.match(whitelistedPathRegex); + }); + }; + +var populateCurrentCacheNames = function (precacheConfig, + cacheNamePrefix, baseUrl) { + var absoluteUrlToCacheName = {}; + var currentCacheNamesToAbsoluteUrl = {}; + + precacheConfig.forEach(function(cacheOption) { + var absoluteUrl = new URL(cacheOption[0], baseUrl).toString(); + var cacheName = cacheNamePrefix + absoluteUrl + '-' + cacheOption[1]; + currentCacheNamesToAbsoluteUrl[cacheName] = absoluteUrl; + absoluteUrlToCacheName[absoluteUrl] = cacheName; + }); + + return { + absoluteUrlToCacheName: absoluteUrlToCacheName, + currentCacheNamesToAbsoluteUrl: currentCacheNamesToAbsoluteUrl + }; + }; + +var stripIgnoredUrlParameters = function (originalUrl, + ignoreUrlParametersMatching) { + var url = new URL(originalUrl); + + url.search = url.search.slice(1) // Exclude initial '?' + .split('&') // Split into an array of 'key=value' strings + .map(function(kv) { + return kv.split('='); // Split each 'key=value' string into a [key, value] array + }) + .filter(function(kv) { + return ignoreUrlParametersMatching.every(function(ignoredRegex) { + return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. + }); + }) + .map(function(kv) { + return kv.join('='); // Join each [key, value] array into a 'key=value' string + }) + .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each + + return url.toString(); + }; + + +var mappings = populateCurrentCacheNames(PrecacheConfig, CacheNamePrefix, self.location); +var AbsoluteUrlToCacheName = mappings.absoluteUrlToCacheName; +var CurrentCacheNamesToAbsoluteUrl = mappings.currentCacheNamesToAbsoluteUrl; + +function deleteAllCaches() { + return caches.keys().then(function(cacheNames) { + return Promise.all( + cacheNames.map(function(cacheName) { + return caches.delete(cacheName); + }) + ); + }); +} + +self.addEventListener('install', function(event) { + var now = Date.now(); + + event.waitUntil( + caches.keys().then(function(allCacheNames) { + return Promise.all( + Object.keys(CurrentCacheNamesToAbsoluteUrl).filter(function(cacheName) { + return allCacheNames.indexOf(cacheName) === -1; + }).map(function(cacheName) { + var urlWithCacheBusting = getCacheBustedUrl(CurrentCacheNamesToAbsoluteUrl[cacheName], + now); + + return caches.open(cacheName).then(function(cache) { + var request = new Request(urlWithCacheBusting, {credentials: 'same-origin'}); + return fetch(request).then(function(response) { + if (response.ok) { + return cache.put(CurrentCacheNamesToAbsoluteUrl[cacheName], response); + } + + console.error('Request for %s returned a response with status %d, so not attempting to cache it.', + urlWithCacheBusting, response.status); + // Get rid of the empty cache if we can't add a successful response to it. + return caches.delete(cacheName); + }); + }); + }) + ).then(function() { + return Promise.all( + allCacheNames.filter(function(cacheName) { + return cacheName.indexOf(CacheNamePrefix) === 0 && + !(cacheName in CurrentCacheNamesToAbsoluteUrl); + }).map(function(cacheName) { + return caches.delete(cacheName); + }) + ); + }); + }).then(function() { + if (typeof self.skipWaiting === 'function') { + // Force the SW to transition from installing -> active state + self.skipWaiting(); + } + }) + ); +}); + +if (self.clients && (typeof self.clients.claim === 'function')) { + self.addEventListener('activate', function(event) { + event.waitUntil(self.clients.claim()); + }); +} + +self.addEventListener('message', function(event) { + if (event.data.command === 'delete_all') { + console.log('About to delete all caches...'); + deleteAllCaches().then(function() { + console.log('Caches deleted.'); + event.ports[0].postMessage({ + error: null + }); + }).catch(function(error) { + console.log('Caches not deleted:', error); + event.ports[0].postMessage({ + error: error + }); + }); + } +}); + + +self.addEventListener('fetch', function(event) { + if (event.request.method === 'GET') { + var urlWithoutIgnoredParameters = stripIgnoredUrlParameters(event.request.url, + IgnoreUrlParametersMatching); + + var cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters]; + var directoryIndex = 'index.html'; + if (!cacheName && directoryIndex) { + urlWithoutIgnoredParameters = addDirectoryIndex(urlWithoutIgnoredParameters, directoryIndex); + cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters]; + } + + var navigateFallback = ''; + // Ideally, this would check for event.request.mode === 'navigate', but that is not widely + // supported yet: + // https://code.google.com/p/chromium/issues/detail?id=540967 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1209081 + if (!cacheName && navigateFallback && event.request.headers.has('accept') && + event.request.headers.get('accept').includes('text/html') && + /* eslint-disable quotes, comma-spacing */ + isPathWhitelisted([], event.request.url)) { + /* eslint-enable quotes, comma-spacing */ + var navigateFallbackUrl = new URL(navigateFallback, self.location); + cacheName = AbsoluteUrlToCacheName[navigateFallbackUrl.toString()]; + } + + if (cacheName) { + event.respondWith( + // Rely on the fact that each cache we manage should only have one entry, and return that. + caches.open(cacheName).then(function(cache) { + return cache.keys().then(function(keys) { + return cache.match(keys[0]).then(function(response) { + if (response) { + return response; + } + // If for some reason the response was deleted from the cache, + // raise and exception and fall back to the fetch() triggered in the catch(). + throw Error('The cache ' + cacheName + ' is empty.'); + }); + }); + }).catch(function(e) { + console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); + return fetch(event.request); + }) + ); + } + } +}); + + + + diff --git a/server.js b/server.js index dda4625..0664015 100644 --- a/server.js +++ b/server.js @@ -2,6 +2,8 @@ var express = require('express') var React = require('react') var renderToString = require('react-dom/server').renderToString var ReactRouter = require('react-router') +var Resolver = require('react-resolver').Resolver +var RouterContext = React.createFactory(ReactRouter.RouterContext) require('babel/register') var routes = require('./src/routes') @@ -16,18 +18,27 @@ app.get('*', function(req, res) { ReactRouter.match({ routes: routes, location: req.url - }, function(err, redirectLocation, props) { + }, function(err, redirectLocation, renderProps) { if (err) { res.status(500).send(err.message) } else if (redirectLocation) { res.redirect(302, redirectLocation.pathname + redirectLocation.search) } - else if (props) { - var markup = renderToString( - React.createElement(ReactRouter.RouterContext, props, null) - ) - res.render('index', { markup: markup }) + else if (renderProps) { + // https://github.com/allenkim67/isomorphic-demo/blob/2be59306196e84f66041dc7a03bbd0c805d371aa/server.js + Resolver + .resolve(function() {return RouterContext(renderProps)}) + .then(function(resolverRes) { + console.log(resolverRes.data) + var markup = renderToString( + React.createElement(resolverRes.Resolved, renderProps, null) + ) + res.render('index', { + markup: markup, + scriptTag: '' + }) + }) } else { res.sendStatus(404) diff --git a/src/Comment.js b/src/Comment.js index db91788..77e843b 100644 --- a/src/Comment.js +++ b/src/Comment.js @@ -2,13 +2,14 @@ var React = require('react') var ReactFireMixin = require('reactfire') var CommentThreadStore = require('./stores/CommentThreadStore') -var HNService = require('./services/HNService') +// var HNService = require('./services/HNService') var HNServiceRest = require('./services/HNServiceRest') var SettingsStore = require('./stores/SettingsStore') var CommentMixin = require('./mixins/CommentMixin') var cx = require('./utils/buildClassName') +var resolve = require('react-resolver').resolve /** * A comment in a thread. @@ -86,21 +87,22 @@ var Comment = React.createClass({ } }, - bindFirebaseRef() { - if (SettingsStore.offlineMode) { - HNServiceRest.itemRef(this.props.id).then(function(res) { - return res.json() - }).then(function(snapshot) { - this.replaceState({ comment: snapshot }) - }.bind(this)) - } - else { - this.bindAsObject(HNService.itemRef(this.props.id), 'comment', this.handleFirebaseRefCancelled) - } - - if (this.timeout) { - this.timeout = null - } + bindFirebaseRef(props) { + console.log('bindFirebaseRef', props) + // if (SettingsStore.offlineMode) { + // HNServiceRest.itemRef(props.id).then(function(res) { + // return res.json() + // }).then(function(snapshot) { + // this.replaceState({ comment: snapshot }) + // }.bind(this)) + // } + // else { + // this.bindAsObject(HNService.itemRef(props.id), 'comment', this.handleFirebaseRefCancelled) + // } + + // if (this.timeout) { + // this.timeout = null + // } }, /** @@ -186,4 +188,18 @@ var Comment = React.createClass({ } }) -module.exports = Comment +/* +What I'm attempting to do here is resolve comment so that we +go through react-resolver anytime we need that data instead of +directly through bindFirebaseRef per the resolver examples I have +seen. Data seems to get returned client-side, but nothing correctly +when doing the server-side render. + */ +module.exports = resolve('comment', function(props) { + return HNServiceRest.itemRef(props.id).then(function(res) { + return res.json() + }).then(function(snapshot) { + console.log('Comment snapshot:', snapshot) + return snapshot + }) +})(Comment) diff --git a/src/index.js b/src/index.js index 0812b31..4d67a8a 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ require('setimmediate') var createHashHistory = require('history/lib/createHashHistory') var React = require('react') -var {render} = require('react-dom') +// var {render} = require('react-dom') var Router = require('react-router/lib/Router') var useRouterHistory = require('react-router/lib/useRouterHistory') var withScroll = require('scroll-behavior/lib/withStandardScroll') @@ -11,4 +11,10 @@ var routes = require('./routes') var history = withScroll(useRouterHistory(createHashHistory)()) -render(, document.getElementById('app')) +var Resolver = require('react-resolver').Resolver + +Resolver.render(function() { return }, document.getElementById('app')) + +// Router.run(routes, Router.HistoryLocation, function(Handler) { +// Resolver.render(function() { return }, document.getElementById('app')) +// }) diff --git a/src/services/HNServiceRest.js b/src/services/HNServiceRest.js index 701ce2a..0c02ebd 100644 --- a/src/services/HNServiceRest.js +++ b/src/services/HNServiceRest.js @@ -1,4 +1,5 @@ /*global fetch*/ +require('es6-promise').polyfill() require('isomorphic-fetch') /* A version of HNService which concumes the Firebase REST diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js index ee4c861..c800a1c 100644 --- a/src/stores/SettingsStore.js +++ b/src/stores/SettingsStore.js @@ -10,7 +10,7 @@ var SettingsStore = { showDeleted: false, titleFontSize: 18, listSpacing: 16, - offlineMode: false, + offlineMode: true, // Temporarily enabled by default for testing load() { var json = storage.get(STORAGE_KEY) diff --git a/src/views/index.ejs b/src/views/index.ejs index 202afd3..fbae3fc 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -66,5 +66,6 @@ console.log('service worker is not supported'); } + <%- scriptTag %> \ No newline at end of file