Skip to content
This repository was archived by the owner on Sep 24, 2019. It is now read-only.

Commit f76074b

Browse files
Rewrote big part of the application to make it more testable.
1 parent 6e438c7 commit f76074b

24 files changed

+1020
-267
lines changed

.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["stage-3"]
3+
}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ lib
66

77
# Logs
88
npm-debug.log
9+
10+
# Internal
11+
coverage

.travis.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
language: node_js
2+
cache: yarn
3+
node_js:
4+
- "4"
5+
- "5"
6+
- "6"

bin/cli.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
const micro = require('micro');
33

44
const flags = require('../lib/flags');
5-
const snapshotServer = require('../lib/server');
5+
const SnapshotServer = require('../lib/server');
66

7-
const server = micro(snapshotServer);
7+
const server = micro(new SnapshotServer(flags));
88
server.listen(flags.port);
99
console.log(`🌏 Server listening on port ${flags.port}`)

package.json

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-snapshot-server",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "A zero-configuration server that serves server-rendered snapshots of Create React App",
55
"main": "lib/index.js",
66
"repository": "https://github.com/scrnhq/react-snapshot-server",
@@ -10,35 +10,43 @@
1010
"react-snapshot-server": "./bin/cli.js"
1111
},
1212
"scripts": {
13-
"compile": "async-to-gen --out-dir lib/ src/"
13+
"compile": "babel src -d lib",
14+
"test": "jest --forceExit",
15+
"test:watch": "jest --watch"
1416
},
1517
"eslintConfig": {
1618
"extends": "react-app"
1719
},
1820
"dependencies": {
19-
"app-root-path": "^2.0.1",
2021
"args": "^2.3.0",
21-
"async-to-gen": "^1.3.2",
22+
"fs-extra": "^2.0.0",
2223
"fs-promise": "^2.0.0",
2324
"jsdom": "^9.11.0",
24-
"jsonfile": "^2.4.0",
2525
"micro": "^7.0.6",
2626
"mime-types": "^2.1.14",
27-
"mkpath": "^1.0.0",
2827
"path-type": "^2.0.0",
2928
"react": "^15.4.2",
3029
"react-dom": "^15.4.2",
31-
"react-scripts": "0.9.0",
32-
"send": "^0.14.2",
33-
"shelljs": "^0.7.6"
30+
"send": "^0.14.2"
3431
},
3532
"devDependencies": {
33+
"babel-cli": "^6.23.0",
3634
"babel-eslint": "^7.0.0",
35+
"babel-jest": "^19.0.0",
36+
"babel-polyfill": "^6.23.0",
37+
"babel-preset-stage-3": "^6.22.0",
3738
"eslint": "^3.8.1",
3839
"eslint-config-react-app": "^0.5.1",
3940
"eslint-plugin-flowtype": "^2.21.0",
4041
"eslint-plugin-import": "^2.0.1",
4142
"eslint-plugin-jsx-a11y": "^2.2.3",
42-
"eslint-plugin-react": "^6.4.1"
43+
"eslint-plugin-react": "^6.4.1",
44+
"jest": "^19.0.2",
45+
"mock-fs": "^4.0.0",
46+
"react-scripts": "0.9.0",
47+
"request-promise": "^4.1.1",
48+
"rewire": "^2.5.2",
49+
"test-listen": "^1.0.1",
50+
"tmp": "^0.0.31"
4351
}
4452
}

src/.npmignore

-1
This file was deleted.

src/browser.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const React = require('react');
2+
const { renderToString } = require('react-dom/server');
3+
4+
const browserConfig = {
5+
features: {
6+
FetchExternalResources: ['script'],
7+
ProcessExternalResources: ['script'],
8+
SkipExternalResources: false,
9+
},
10+
};
11+
12+
const Browser = function(jsdom, pathname, html) {
13+
this.jsdom = jsdom;
14+
this.html = html;
15+
this.pathname = pathname;
16+
}
17+
18+
Browser.prototype.created = function(resolve, reject, error, window) {
19+
if (error) { console.error(error) }
20+
this.jsdom.changeURL(window, 'http://' + this.pathname);
21+
}
22+
23+
Browser.prototype.done = async function(resolve, reject, error, window) {
24+
const app = window.app;
25+
26+
let initialProps;
27+
28+
if (typeof app.getInitialProps === 'function') {
29+
initialProps = await app.getInitialProps();
30+
} else {
31+
initialProps = {};
32+
}
33+
34+
const content = renderToString(
35+
React.createElement(app, initialProps)
36+
);
37+
38+
window.document.getElementById('root').innerHTML = content;
39+
40+
resolve(window.document.documentElement.outerHTML);
41+
}
42+
43+
Browser.prototype.execute = function() {
44+
return new Promise((resolve, reject) => {
45+
this.jsdom.env({
46+
html: this.html,
47+
...browserConfig,
48+
virtualConsole: this.jsdom.createVirtualConsole().sendTo(console),
49+
created: this.created.bind(this, resolve, reject),
50+
done: this.done.bind(this, resolve, reject),
51+
});
52+
});
53+
}
54+
55+
const BrowserFactory = function(jsdom) {
56+
return Browser.bind(null, jsdom);
57+
};
58+
59+
module.exports = BrowserFactory;

src/flags.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const args = require('args');
2+
const path = require('path');
23

34
args.option('port', 'The port on which the server will be running', 3000)
4-
.option('snapshot_duration', 'Time in minutes that a snapshot is valid', 10);
5+
.option('validity', 'Time in minutes that a snapshot is valid', 10)
6+
.option('path', 'The path to the build directory', path.join(process.cwd(), 'build'));
57

68
const flags = args.parse(process.argv);
79

src/manifest.js

+36-37
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,44 @@
1-
const __dir = process.cwd();
21
const path = require('path');
3-
const fs = require('fs-promise');
4-
const makePath = require('mkpath');
5-
const jsonfile = require('jsonfile');
62

7-
const MANIFEST_DIR = path.join(__dir, 'build', 'snapshots');
8-
const MANIFEST_PATH = path.join(MANIFEST_DIR, 'manifest.json')
3+
const Manifest = function(fs, date, manifestPath, validity) {
4+
this.fs = fs;
5+
this.date = date;
6+
this.manifestPath = manifestPath;
7+
this.validity = validity;
8+
this.manifest = this.open(manifestPath);
9+
}
910

10-
makePath.sync(MANIFEST_DIR);
11+
Manifest.prototype.open = function() {
12+
if (this.fs.existsSync(this.manifestPath)) {
13+
return JSON.parse(this.fs.readFileSync(this.manifestPath));
14+
}
1115

12-
const manifest = boot();
16+
this.write({});
17+
return {};
18+
}
1319

14-
/**
15-
* Boot the manifest, create if necessary, and return the contents.
16-
*
17-
* @return {Object}
18-
*/
19-
function boot() {
20-
if (fs.existsSync(MANIFEST_PATH)) {
21-
return jsonfile.readFileSync(MANIFEST_PATH);
22-
}
20+
Manifest.prototype.write = function(manifest) {
21+
const dirname = path.dirname(this.manifestPath)
2322

24-
jsonfile.writeFile(MANIFEST_PATH, {}, function (err) {
25-
if (err) { console.error(err) }
26-
});
23+
if (!this.fs.existsSync(dirname)) {
24+
this.fs.mkdirSync(dirname);
25+
}
2726

28-
return {};
27+
const manifestToWrite = JSON.stringify(manifest);
28+
this.fs.writeFile(this.manifestPath, manifestToWrite)
29+
.catch(error => console.error(error));
2930
}
3031

3132
/**
32-
* Add a snapshot to the manifest.
33+
* Add an entry to the manifest.
3334
*
3435
* @param {String} path
3536
* @param {Number} expirationDate
3637
*/
37-
function add(path, expirationDate) {
38-
manifest[path] = expirationDate;
38+
Manifest.prototype.add = function(path) {
39+
this.manifest[path] = this.calculateValidity();
3940

40-
jsonfile.writeFile(MANIFEST_PATH, manifest, function (err) {
41-
if (err) { console.error(err) }
42-
});
41+
this.write(this.manifest);
4342
}
4443

4544
/**
@@ -48,8 +47,8 @@ function add(path, expirationDate) {
4847
* @param {String} path
4948
* @return {Boolean}
5049
*/
51-
function exists(path) {
52-
return (path in manifest);
50+
Manifest.prototype.exists = function(path) {
51+
return (path in this.manifest);
5352
}
5453

5554
/**
@@ -59,18 +58,18 @@ function exists(path) {
5958
* @param {String} path
6059
* @return {Boolean}
6160
*/
62-
function valid(path) {
63-
if (!exists(path)) {
61+
Manifest.prototype.isValid = function(path) {
62+
if (!this.exists(path)) {
6463
return false;
6564
}
6665

67-
const expirationDate = manifest[path];
66+
const expirationDate = this.manifest[path];
6867

69-
return Date.now() < expirationDate;
68+
return this.date.now() < expirationDate;
7069
}
7170

72-
module.exports = {
73-
add,
74-
valid,
75-
exists,
71+
Manifest.prototype.calculateValidity = function(validity) {
72+
return this.date.now() + (this.validity * 60 * 1000);
7673
}
74+
75+
module.exports = Manifest;

src/server.js

+59-33
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,67 @@
11
const path = require('path');
2-
const { parse } = require('url');
3-
4-
const __dir = process.cwd();
5-
62
const stream = require('send');
3+
const jsdom = require('jsdom');
4+
const { parse } = require('url');
75
const fs = require('fs-promise');
6+
const { send } = require('micro');
87
const mime = require('mime-types');
9-
const snapshot = require('./snapshot');
10-
11-
module.exports = async (req, res) => {
12-
const url = req.headers.host + req.url;
13-
const { pathname } = parse(req.url);
14-
const filepath = path.join(__dir, 'build', pathname);
15-
const dirname = path.dirname(pathname);
16-
const extname = path.extname(pathname);
17-
18-
if (extname !== '' && (dirname === '/' || dirname.startsWith('/static'))) {
19-
if (fs.existsSync(filepath)) {
20-
res.setHeader('Content-Type', mime.contentType(extname));
21-
return stream(req, filepath).pipe(res);
22-
}
23-
}
248

25-
if (!snapshot.exists(pathname)) {
26-
const snap = await snapshot.create(url);
27-
await snapshot.save(pathname, snap);
28-
console.log(`📝 Created a fresh new snapshot for ${pathname}`);
29-
return snap;
30-
}
9+
const Manifest = require('./manifest');
10+
const Snapshot = require('./snapshot');
11+
const BrowserFactory = require('./browser');
12+
const browser = new BrowserFactory(jsdom);
3113

32-
if (!snapshot.valid(pathname)) {
33-
const snap = await snapshot.create(url);
34-
await snapshot.save(pathname, snap);
35-
console.log(`⏰ Snapshot expired, creating a fresh new snapshot for ${pathname}`);
36-
return snap;
14+
const handleErrors = fn => async (req, res, ...other) => {
15+
try {
16+
return await fn(req, res, ...other);
17+
} catch (err) {
18+
console.error(err.stack);
19+
send(res, 500, 'Internal server error');
3720
}
21+
}
22+
23+
const Server = function(flags) {
24+
const BUILD_DIRECTORY = flags.path;
25+
const SNAPSHOT_DIRECTORY = path.join(BUILD_DIRECTORY, 'snapshots');
26+
const MANIFEST_PATH = path.join(SNAPSHOT_DIRECTORY, 'manifest.json');
27+
const APPLICATION_PATH = path.join(BUILD_DIRECTORY, 'index.html');
28+
29+
const manifest = new Manifest(fs, Date, MANIFEST_PATH, flags.validity);
30+
const snapshot = new Snapshot(manifest, fs, browser, SNAPSHOT_DIRECTORY, APPLICATION_PATH);
31+
32+
return handleErrors(async (req, res) => {
33+
const url = req.headers.host + req.url;
34+
const { pathname } = parse(req.url);
35+
const filepath = path.join(BUILD_DIRECTORY, pathname);
36+
const extname = path.extname(pathname);
37+
38+
if (extname !== '') {
39+
if (fs.existsSync(filepath)) {
40+
res.setHeader('Content-Type', mime.contentType(extname));
41+
return stream(req, filepath).pipe(res);
42+
} else {
43+
return send(res, 404);
44+
}
45+
}
46+
47+
if (!snapshot.exists(pathname)) {
48+
const snap = await snapshot.create(url);
49+
await snapshot.save(pathname, snap);
50+
console.log(`📝 Created a fresh new snapshot for ${pathname}`);
51+
return snap;
52+
}
53+
54+
if (!snapshot.isValid(pathname)) {
55+
const snap = await snapshot.create(url);
56+
await snapshot.save(pathname, snap);
57+
console.log(`⏰ Snapshot expired, creating a fresh new snapshot for ${pathname}`);
58+
return snap;
59+
}
60+
61+
console.log(`🔍 Found snapshot for ${pathname}`);
62+
res.setHeader('Content-Type', 'text/html');
63+
return stream(req, snapshot.getPath(pathname)).pipe(res);
64+
});
65+
}
3866

39-
console.log(`🔍 Found snapshot for ${pathname}`);
40-
return snapshot.send(req, res, pathname);
41-
};
67+
module.exports = Server;

0 commit comments

Comments
 (0)