├── 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 | [](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 |
11 |
12 |
13 | Worker registered, refresh to see the magic!
14 |
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 | }
--------------------------------------------------------------------------------