├── Procfile ├── .gitignore ├── preprocessor.js ├── src ├── components │ ├── about.js │ ├── hello.js │ ├── app.js │ ├── __tests__ │ │ ├── about-test.js │ │ ├── app-test.js │ │ ├── hello-test.js │ │ └── home-test.js │ ├── home.js │ └── test-helpers │ │ └── index.js └── index.js ├── .travis.yml ├── server.js ├── .jshintrc ├── README.md ├── public ├── index.html ├── worker.js └── serviceworker-cache-polyfill.js ├── LICENSE └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public/build.js 2 | node_modules/ -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | var ReactTools = require('react-tools'); 2 | 3 | module.exports = { 4 | process: function(src) { 5 | return ReactTools.transform(src); 6 | } 7 | }; -------------------------------------------------------------------------------- /src/components/about.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var About = React.createClass({ 4 | render: function() { 5 | return ( 6 |

About

7 | ); 8 | } 9 | }); 10 | 11 | module.exports = About; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10.33 4 | deploy: 5 | provider: heroku 6 | api_key: 7 | secure: WGRkrI37gApFtQlHwQgkEzyronfh5SAvjomv/ZNrjoqQswZkeSrH/LsZPJhX5sgA8Q7bwpiuzjSkDD1Xk/twGC11Amf3j3g7BIM5li8S42DzH2IEZ+svs+cmF+l7rtUvgbJnyROMhqEtAx+AzKUP0BAnwwhGSJnwo14vH1MzNg0= 8 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.use(express.static(__dirname + '/public')); 5 | 6 | var host = process.env.IP || '0.0.0.0', 7 | port = process.env.PORT || 3000; 8 | 9 | var server = app.listen(port, host, function() { 10 | console.log('Listening at http://%s:%s', host, port); 11 | }); -------------------------------------------------------------------------------- /src/components/hello.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Router = require('react-router'), 3 | State = Router.State; 4 | 5 | var Hello = React.createClass({ 6 | mixins: [State], 7 | render: function() { 8 | var name = this.getParams() ? this.getParams().name : 'World'; 9 | return ( 10 |

Hello, {name}!

11 | ); 12 | } 13 | }); 14 | 15 | module.exports = Hello; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "node": true, 4 | "maxlen": 100, 5 | "newcap": false, 6 | "undef": true, 7 | "unused": true, 8 | "onecase": true, 9 | "lastsemic": true, 10 | "indent": 2, 11 | "esnext": true, 12 | "globals": { 13 | "importScripts": false, 14 | "self": false, 15 | "caches": false, 16 | "fetch": false, 17 | "Router": false, 18 | "Routes": false, 19 | "Response": false, 20 | "Url": false, 21 | "React": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Router = require('react-router'), 3 | RouteHandler = Router.RouteHandler; 4 | 5 | var App = React.createClass({ 6 | render: function() { 7 | return ( 8 | 9 | 10 | 11 | React Worker 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | }); 22 | 23 | module.exports = App; -------------------------------------------------------------------------------- /src/components/__tests__/about-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../about.js'); 2 | 3 | describe('About', function() { 4 | 5 | it('has the proper heading', function() { 6 | 7 | var React = require('react/addons'); 8 | var About = require('../about.js'); 9 | var TestUtils = React.addons.TestUtils; 10 | 11 | var about = TestUtils.renderIntoDocument( 12 | 13 | ); 14 | 15 | var h1 = TestUtils.findRenderedDOMComponentWithTag( 16 | about, 'h1'); 17 | 18 | expect(h1.getDOMNode().textContent).toEqual('About'); 19 | 20 | }); 21 | 22 | }); -------------------------------------------------------------------------------- /src/components/__tests__/app-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../app.js'); 2 | jest.dontMock('../test-helpers'); 3 | 4 | describe('App', function() { 5 | 6 | it('has the proper title', function() { 7 | 8 | var React = require('react/addons'); 9 | var App = require('../app.js'); 10 | var TestUtils = React.addons.TestUtils; 11 | var testHelpers = require('../test-helpers'); 12 | 13 | var stubbed = testHelpers.makeStubbedDescriptor(App); 14 | var app = TestUtils.renderIntoDocument(stubbed); 15 | 16 | var title = TestUtils.findRenderedDOMComponentWithTag( 17 | app, 'title'); 18 | 19 | expect(title.getDOMNode().textContent).toEqual('React Worker'); 20 | 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /src/components/__tests__/hello-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../hello.js'); 2 | jest.dontMock('../test-helpers'); 3 | 4 | describe('Hello', function() { 5 | 6 | it('has the proper heading', function() { 7 | 8 | var React = require('react/addons'); 9 | var Hello = require('../hello.js'); 10 | var TestUtils = React.addons.TestUtils; 11 | var testHelpers = require('../test-helpers'); 12 | 13 | var stubbed = testHelpers.makeStubbedDescriptor(Hello); 14 | var hello = TestUtils.renderIntoDocument(stubbed); 15 | 16 | var h1 = TestUtils.findRenderedDOMComponentWithTag( 17 | hello, 'h1'); 18 | 19 | expect(h1.getDOMNode().textContent).toEqual('Hello, World!'); 20 | 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/MicheleBertoli/react-worker.svg?branch=master)](https://travis-ci.org/MicheleBertoli/react-worker) 2 | 3 | React Worker 4 | ============ 5 | 6 | Using [Service Workers](http://www.w3.org/TR/2014/WD-service-workers-20141118/) to render [React](http://facebook.github.io/react/) components. 7 | 8 | Rendered components are cached (thanks to [Sandro Paganotti](https://github.com/sandropaganotti)) and the cache is cleared every time `build.js` is updated. 9 | 10 | [Demo](https://react-worker.herokuapp.com/) 11 | 12 | Run 13 | --------------- 14 | 15 | ``` 16 | $ npm install 17 | $ npm start 18 | ``` 19 | 20 | Test 21 | --------------- 22 | 23 | ``` 24 | $ npm test 25 | ``` 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | React = require('react'); 2 | Router = require('react-router'); 3 | Url = require('url'); 4 | 5 | var Route = Router.Route, 6 | DefaultRoute = Router.DefaultRoute, 7 | HistoryLocation = Router.HistoryLocation; 8 | 9 | var App = require('./components/app'), 10 | Home = require('./components/home'), 11 | About = require('./components/about'), 12 | Hello = require('./components/hello'); 13 | 14 | Routes = ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | if (typeof window !== 'undefined') { 23 | Router.run(Routes, HistoryLocation, function(Handler) { 24 | React.render(, document); 25 | }); 26 | } -------------------------------------------------------------------------------- /src/components/home.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Home = React.createClass({ 4 | getInitialState: function() { 5 | return { 6 | click: 0 7 | } 8 | }, 9 | handleClick: function() { 10 | this.setState({ 11 | click: this.state.click + 1 12 | }); 13 | }, 14 | render: function() { 15 | return ( 16 |
17 |

It works!

18 |
    19 |
  • 20 | About 21 |
  • 22 |
  • 23 | Hello World 24 |
  • 25 |
  • 26 | 27 |
  • 28 |
29 |
30 | ); 31 | } 32 | }); 33 | 34 | module.exports = Home; -------------------------------------------------------------------------------- /src/components/__tests__/home-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../home.js'); 2 | 3 | describe('Home', function() { 4 | 5 | var React = require('react/addons'); 6 | var Home = require('../home.js'); 7 | var TestUtils = React.addons.TestUtils; 8 | 9 | var home; 10 | var button; 11 | 12 | beforeEach(function() { 13 | 14 | home = TestUtils.renderIntoDocument( 15 | 16 | ); 17 | button = TestUtils.findRenderedDOMComponentWithTag( 18 | home, 'button'); 19 | 20 | }); 21 | 22 | it('shows zero clicks on load', function() { 23 | 24 | expect(button.getDOMNode().textContent).toEqual('Clicks: 0'); 25 | 26 | }); 27 | 28 | it('counts the number of clicks', function() { 29 | 30 | TestUtils.Simulate.click(button); 31 | expect(button.getDOMNode().textContent).toEqual('Clicks: 1'); 32 | 33 | }); 34 | 35 | }); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Worker 6 | 7 | 8 | 9 | 10 | Fork me on GitHub 11 | 12 | 15 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michele Bertoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-worker", 3 | "version": "1.0.0", 4 | "description": "Playing with React and Service Workers", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/MicheleBertoli/react-worker.git" 8 | }, 9 | "author": "Michele Bertoli ", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/MicheleBertoli/react-worker/issues" 13 | }, 14 | "homepage": "https://github.com/MicheleBertoli/react-worker", 15 | "dependencies": { 16 | "browserify": "^7.0.3", 17 | "express": "^4.10.6", 18 | "react": "^0.12.2", 19 | "react-router": "^0.11.6", 20 | "react-tools": "^0.12.2", 21 | "reactify": "^0.17.1" 22 | }, 23 | "devDependencies": { 24 | "jest-cli": "^0.2.1", 25 | "jshint": "^2.5.11" 26 | }, 27 | "scripts": { 28 | "build": "browserify ./src -o ./public/build.js", 29 | "postinstall": "npm run build", 30 | "start": "node server", 31 | "hint": "jshint ./public/worker.js", 32 | "pretest": "npm run hint", 33 | "test": "jest" 34 | }, 35 | "browserify": { 36 | "transform": [ 37 | "reactify" 38 | ] 39 | }, 40 | "jest": { 41 | "scriptPreprocessor": "/preprocessor.js", 42 | "unmockedModulePathPatterns": [ 43 | "/node_modules/react" 44 | ] 45 | }, 46 | "engines": { 47 | "node": "0.10.33" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('serviceworker-cache-polyfill.js'); 2 | importScripts('build.js'); 3 | 4 | var cacheKey = 'pages'; 5 | var options = { 6 | headers: { 7 | 'Content-Type': 'text/html' 8 | } 9 | }; 10 | 11 | self.addEventListener('fetch', function(event) { 12 | if (/\.js$/.test(event.request.url)) { 13 | _static(event); 14 | } else { 15 | _app(event); 16 | } 17 | }); 18 | 19 | self.addEventListener('activate', function() { 20 | caches.open(cacheKey).then(function(cache) { 21 | cache.keys().then(function(requests) { 22 | requests.forEach(function(request) { 23 | cache.delete(request); 24 | }); 25 | }); 26 | }); 27 | }); 28 | 29 | function _static(event) { 30 | event.respondWith( 31 | fetch(event.request.url) 32 | ); 33 | } 34 | 35 | function _app(event) { 36 | event.respondWith( 37 | caches.match(event.request).then(function(page) { 38 | return page || _route(event); 39 | }) 40 | ); 41 | } 42 | 43 | function _route(event) { 44 | return new Promise(function(resolve) { 45 | Router.run(Routes, _path(event.request.url), function(Handler) { 46 | var html = _render(Handler); 47 | var response = new Response('' + html, options); 48 | _store(event.request, response); 49 | resolve(response); 50 | }); 51 | }); 52 | } 53 | 54 | function _path(url) { 55 | return Url.parse(url).path; 56 | } 57 | 58 | function _render(Handler) { 59 | var handler = React.createFactory(Handler); 60 | return React.renderToString(handler()); 61 | } 62 | 63 | function _store(request, response) { 64 | return caches.open(cacheKey).then(function(cache) { 65 | return cache.put(request, response.clone()); 66 | }); 67 | } -------------------------------------------------------------------------------- /src/components/test-helpers/index.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/rpflorence/1f72da0cd9e507ebec29 2 | 3 | var React = require('react'), 4 | merge = require('react/lib/merge'); 5 | 6 | function makeStubbedDescriptor(component, props, contextStubs) { 7 | var TestWrapper = React.createClass({ 8 | childContextTypes: { 9 | currentPath: React.PropTypes.string, 10 | makePath: React.PropTypes.func.isRequired, 11 | makeHref: React.PropTypes.func.isRequired, 12 | transitionTo: React.PropTypes.func.isRequired, 13 | replaceWith: React.PropTypes.func.isRequired, 14 | goBack: React.PropTypes.func.isRequired, 15 | isActive: React.PropTypes.func.isRequired, 16 | activeRoutes: React.PropTypes.array.isRequired, 17 | activeParams: React.PropTypes.object.isRequired, 18 | activeQuery: React.PropTypes.object.isRequired, 19 | location: React.PropTypes.object, 20 | routes: React.PropTypes.array.isRequired, 21 | namedRoutes: React.PropTypes.object.isRequired, 22 | scrollBehavior: React.PropTypes.object, 23 | routeHandlers: React.PropTypes.array.isRequired, 24 | getRouteAtDepth: React.PropTypes.func.isRequired, 25 | getRouteComponents: React.PropTypes.func.isRequired, 26 | getCurrentParams: React.PropTypes.func.isRequired 27 | }, 28 | 29 | getChildContext: function() { 30 | return merge({ 31 | currentPath: '__STUB__', 32 | makePath: function() {}, 33 | makeHref: function() { return '__STUB__'; }, 34 | transitionTo: function() {}, 35 | replaceWith: function() {}, 36 | goBack: function() {}, 37 | isActive: function() {}, 38 | activeRoutes: [], 39 | activeParams: {}, 40 | activeQuery: {}, 41 | location: {}, 42 | routes: [], 43 | namedRoutes: {}, 44 | scrollBehavior: {}, 45 | routeHandlers: [{}], 46 | getRouteAtDepth: function() {}, 47 | getRouteComponents: function() { return {}; }, 48 | getCurrentParams: function() {} 49 | }, contextStubs); 50 | }, 51 | 52 | render: function() { 53 | this.props.ref = '__subject__'; 54 | return component(this.props); 55 | } 56 | }); 57 | 58 | return TestWrapper(props); 59 | } 60 | 61 | module.exports.makeStubbedDescriptor = makeStubbedDescriptor; -------------------------------------------------------------------------------- /public/serviceworker-cache-polyfill.js: -------------------------------------------------------------------------------- 1 | if (!Cache.prototype.add) { 2 | Cache.prototype.add = function add(request) { 3 | return this.addAll([request]); 4 | }; 5 | } 6 | 7 | if (!Cache.prototype.addAll) { 8 | Cache.prototype.addAll = function addAll(requests) { 9 | var cache = this; 10 | 11 | // Since DOMExceptions are not constructable: 12 | function NetworkError(message) { 13 | this.name = 'NetworkError'; 14 | this.code = 19; 15 | this.message = message; 16 | } 17 | NetworkError.prototype = Object.create(Error.prototype); 18 | 19 | return Promise.resolve().then(function() { 20 | if (arguments.length < 1) throw new TypeError(); 21 | 22 | // Simulate sequence<(Request or USVString)> binding: 23 | var sequence = []; 24 | 25 | requests = requests.map(function(request) { 26 | if (request instanceof Request) { 27 | return request; 28 | } 29 | else { 30 | return String(request); // may throw TypeError 31 | } 32 | }); 33 | 34 | return Promise.all( 35 | requests.map(function(request) { 36 | if (typeof request === 'string') { 37 | request = new Request(request); 38 | } 39 | 40 | var scheme = new URL(request.url).protocol; 41 | 42 | if (scheme !== 'http:' && scheme !== 'https:') { 43 | throw new NetworkError("Invalid scheme"); 44 | } 45 | 46 | return fetch(request.clone()); 47 | }) 48 | ); 49 | }).then(function(responses) { 50 | // TODO: check that requests don't overwrite one another 51 | // (don't think this is possible to polyfill due to opaque responses) 52 | return Promise.all( 53 | responses.map(function(response, i) { 54 | return cache.put(requests[i], response); 55 | }) 56 | ); 57 | }).then(function() { 58 | return undefined; 59 | }); 60 | }; 61 | } 62 | 63 | if (!CacheStorage.prototype.match) { 64 | // This is probably vulnerable to race conditions (removing caches etc) 65 | CacheStorage.prototype.match = function match(request, opts) { 66 | var caches = this; 67 | 68 | return this.keys().then(function(cacheNames) { 69 | var match; 70 | 71 | return cacheNames.reduce(function(chain, cacheName) { 72 | return chain.then(function() { 73 | return match || caches.open(cacheName).then(function(cache) { 74 | return cache.match(request, opts); 75 | }).then(function(response) { 76 | match = response; 77 | return match; 78 | }); 79 | }); 80 | }, Promise.resolve()); 81 | }); 82 | }; 83 | } --------------------------------------------------------------------------------