Created
December 13, 2015 16:54
-
-
Save skosch/603ad7f490beb0110013 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// meteor algorithm to check if this is a meteor serving http request or not | |
function IsAppUrl(req) { | |
var url = req.url; | |
if(url === '/favicon.ico' || url === '/robots.txt') { | |
return false; | |
} | |
// NOTE: app.manifest is not a web standard like favicon.ico and | |
// robots.txt. It is a file name we have chosen to use for HTML5 | |
// appcache URLs. It is included here to prevent using an appcache | |
// then removing it from poisoning an app permanently. Eventually, | |
// once we have server side routing, this won't be needed as | |
// unknown URLs with return a 404 automatically. | |
if(url === '/app.manifest') { | |
return false; | |
} | |
// Avoid serving app HTML for declared routes such as /sockjs/. | |
if(RoutePolicy.classify(url)) { | |
return false; | |
} | |
return true; | |
} | |
const {Router} = ReactRouter; | |
const {RoutingContext} = ReactRouter; | |
const url = Npm.require('url'); | |
const Fiber = Npm.require('fibers'); | |
const cookieParser = Npm.require('cookie-parser'); | |
let webpackStats; | |
ReactRouterSSR.LoadWebpackStats = function(stats) { | |
webpackStats = stats; | |
}; | |
ReactRouterSSR.Run = function(routes, clientOptions, serverOptions) { | |
if (!clientOptions) { | |
clientOptions = {}; | |
} | |
if (!serverOptions) { | |
serverOptions = {}; | |
} | |
if (!serverOptions.webpackStats) { | |
serverOptions.webpackStats = webpackStats; | |
} | |
Meteor.bindEnvironment(function() { | |
// Parse cookies for the login token | |
WebApp.rawConnectHandlers.use(cookieParser()); | |
WebApp.connectHandlers.use(Meteor.bindEnvironment(function(req, res, next) { | |
if (!IsAppUrl(req)) { | |
next(); | |
return; | |
} | |
const history = ReactRouter.history.createMemoryHistory(req.url); | |
var path = req.url; | |
var loginToken = req.cookies.meteor_login_token; | |
var headers = req.headers; | |
var css = null; | |
var responseAlreadySent = false; | |
var context = new FastRender._Context(loginToken, { headers: headers }); | |
try { | |
FastRender.frContext.withValue(context, function() { | |
const originalSubscribe = Meteor.subscribe; | |
Meteor.subscribe = function(name, ...args) { | |
if (Package.mongo && !Package.autopublish) { | |
Mongo.Collection._isSSR = false; | |
const publishResult = Meteor.server.publish_handlers[name].apply(context, args); | |
Mongo.Collection._isSSR = true; | |
Mongo.Collection._fakePublish(publishResult); | |
} | |
return context.subscribe.apply(context, arguments); | |
}; | |
if (Package.mongo && !Package.autopublish) { | |
Mongo.Collection._isSSR = true; | |
Mongo.Collection._publishSelectorsSSR = {}; | |
} | |
const originalUserId = Meteor.userId; | |
const originalUser = Meteor.user; | |
// This should be the state of the client when he remount the app | |
Meteor.userId = () => context.userId; | |
Meteor.user = () => Meteor.users.findOne({_id: context.userId}); | |
if (serverOptions.preRender) { | |
serverOptions.preRender(req, res); | |
} | |
global.__STYLE_COLLECTOR_MODULES__ = []; | |
global.__STYLE_COLLECTOR__ = ''; | |
global.__CHUNK_COLLECTOR__ = []; | |
ReactRouter.match({routes, location: req.url}, (err, redirectLocation, renderProps) => { | |
if (err) { | |
responseAlreadySent = true; | |
// Redirect back to app (this doesn't work) | |
res.writeHead(500, {"Content-Type": "text/plain"}); | |
res.write(err.message + "\n"); | |
res.end(); | |
//res.status(500).send(err.message); | |
} else if (redirectLocation) { | |
responseAlreadySent = true; | |
res.writeHead(302, {Location: redirectLocation.pathname + redirectLocation.search}); | |
res.end(); | |
//res.redirect(302, redirectLocation.pathname + redirectLocation.search); | |
} else if (renderProps) { | |
html = ReactDOMServer.renderToString(<RoutingContext {...renderProps} {...serverOptions.props} />); | |
} else { | |
responseAlreadySent = true; | |
res.writeHead(404, {"Content-Type": "text/plain"}); | |
res.write("404 Not Found\n"); | |
res.end(); | |
//res.status(404).send("Not found"); | |
} | |
}); | |
/*html = ReactDOMServer.renderToString( | |
<Router | |
history={history} | |
children={routes} | |
{...serverOptions.props} /> | |
);*/ | |
css = global.__STYLE_COLLECTOR__; | |
if (serverOptions.postRender) { | |
serverOptions.postRender(req, res); | |
} | |
Meteor.subscribe = originalSubscribe; | |
Meteor.userId = originalUserId; | |
Meteor.user = originalUser; | |
if (Package.mongo && !Package.autopublish) { | |
Mongo.Collection._isSSR = false; | |
} | |
}); | |
res.pushData('fast-render-data', context.getData()); | |
} catch(err) { | |
console.error('error while server-rendering', err.message, err.stack); | |
console.error("the request was", req.url); | |
} | |
if (responseAlreadySent) { | |
return; | |
} | |
var originalWrite = res.write; | |
res.write = function(data) { | |
if(typeof data === 'string' && data.indexOf('<!DOCTYPE html>') === 0) { | |
if (!serverOptions.dontMoveScripts) { | |
data = moveScripts(data); | |
} | |
let title = global.__CURRENT_TITLE__ || 'Default title goes here'; | |
data = data.replace('</head>', ` | |
<meta name="description" content="Default description goes here" /> | |
<meta charset="utf-8" /> | |
<title>` + title + `</title>` + '</head>'); | |
if (css) { | |
data = data.replace('</head>', '<style id="' + (clientOptions.styleCollectorId || 'css-style-collector-data') + '">' + css + '</style></head>'); | |
} | |
data = data.replace('<body>', '<body><div id="' + (clientOptions.rootElement || 'react-app') + '">' + html + '</div>'); | |
if (typeof serverOptions.webpackStats !== 'undefined') { | |
const chunkNames = serverOptions.webpackStats.assetsByChunkName; | |
const publicPath = serverOptions.webpackStats.publicPath; | |
// go through all chunks and if it's either a general common chunk, or a common chunk that holds code we need, | |
// add it to the list of script tags | |
const addChunkName = function(chunkName) { | |
var chunkSrc = (typeof chunkNames[chunkName] === 'string') ? | |
chunkNames[chunkName] : | |
chunkNames[chunkName][0]; // first one is the name, latter ones might be e.g. sourcemaps | |
data = data.replace('<head>', '<head><script type="text/javascript" src="' + publicPath + chunkSrc + '"></script>'); | |
}; | |
for (var i = 0; i < global.__CHUNK_COLLECTOR__.length; ++i) { | |
let currentCollectedChunkName = global.__CHUNK_COLLECTOR__[i]; | |
if (typeof chunkNames[currentCollectedChunkName] !== 'undefined') { | |
var chunkSrc = (typeof chunkNames[currentCollectedChunkName] === 'string')? | |
chunkNames[currentCollectedChunkName] : | |
chunkNames[currentCollectedChunkName][0]; | |
// add, e.g. the "admin" chunk itself ... | |
addChunkName(currentCollectedChunkName); | |
// ... and then on top of that, also look for chunks like "common.admin" and add them now | |
for (let currentCommonChunkName in chunkNames) { | |
if (chunkNames.hasOwnProperty(currentCommonChunkName) && currentCommonChunkName.startsWith("common") && currentCommonChunkName.substr(7).split("-").indexOf(currentCollectedChunkName) > -1) { | |
addChunkName(currentCommonChunkName); | |
} | |
} | |
} | |
} | |
// and prepend the common module, if it exists, as well | |
for (let currentChunkName in chunkNames) { | |
if (chunkNames.hasOwnProperty(currentChunkName) && currentChunkName === "common") { | |
addChunkName(currentChunkName); | |
} | |
} | |
} | |
} | |
originalWrite.call(this, data); | |
}; | |
next(); | |
})); | |
})(); | |
}; | |
// Thank you FlowRouter for this wonderful idea :) | |
// https://github.com/kadirahq/flow-router/blob/ssr/server/route.js | |
const Cheerio = Npm.require('cheerio'); | |
function moveScripts(data) { | |
const $ = Cheerio.load(data, { | |
decodeEntities: false | |
}); | |
const heads = $('head script'); | |
$('body').append(heads); | |
// Remove empty lines caused by removing scripts | |
$('head').html($('head').html().replace(/(^[ \t]*\n)/gm, '')); | |
return $.html(); | |
} | |
if (Package.mongo && !Package.autopublish) { | |
// Protect against returning data that has not been published | |
const originalFind = Mongo.Collection.prototype.find; | |
const originalFindOne = Mongo.Collection.prototype.findOne; | |
Mongo.Collection.prototype.findOne = function() { | |
let args = Array.prototype.slice.call(arguments); | |
if (!Mongo.Collection._isSSR) { | |
return originalFindOne.apply(this, args); | |
} | |
// make sure users are always returned (we'll restrict this to the request-sender right before rendering) | |
if (this._name === 'users') { | |
return originalFindOne.apply(this, args); | |
} | |
// Make sure to return nothing if no publish has been called | |
if (!Mongo.Collection._publishSelectorsSSR[this._name] || !Mongo.Collection._publishSelectorsSSR[this._name].length) { | |
return originalFindOne.apply(this, [undefined]); | |
} | |
if (args.length) { | |
if (typeof args[0] === 'string') { | |
args[0] = { _id: args[0] }; | |
} | |
args[0] = { $and: [args[0], { $or: Mongo.Collection._publishSelectorsSSR[this._name] }] }; | |
} else { | |
args.push({ $or: Mongo.Collection._publishSelectorsSSR[this._name] }); | |
} | |
return originalFindOne.apply(this, args); | |
}; | |
Mongo.Collection.prototype.find = function() { | |
let args = Array.prototype.slice.call(arguments); | |
if (!Mongo.Collection._isSSR) { | |
return originalFind.apply(this, args); | |
} | |
// Make sure to return nothing if no publish has been called | |
if (!Mongo.Collection._publishSelectorsSSR[this._name] || !Mongo.Collection._publishSelectorsSSR[this._name].length) { | |
return originalFind.apply(this, [undefined]); | |
} | |
if (args.length) { | |
args[0] = { $and: [args[0], { $or: Mongo.Collection._publishSelectorsSSR[this._name] }] }; | |
} else { | |
args.push({ $or: Mongo.Collection._publishSelectorsSSR[this._name] }); | |
} | |
return originalFind.apply(this, args); | |
}; | |
Mongo.Collection._fakePublish = function(result) { | |
if (Array.isArray(result)) { | |
result.forEach(subResult => Mongo.Collection._fakePublish(subResult)); | |
return; | |
} | |
const name = result._cursorDescription.collectionName; | |
const selector = result._cursorDescription.selector; | |
if (!Mongo.Collection._publishSelectorsSSR[name]) { | |
Mongo.Collection._publishSelectorsSSR[name] = []; | |
} | |
Mongo.Collection._publishSelectorsSSR[name].push(selector); | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment