Skip to content

Instantly share code, notes, and snippets.

@skosch
Created December 13, 2015 16:54
Show Gist options
  • Save skosch/603ad7f490beb0110013 to your computer and use it in GitHub Desktop.
Save skosch/603ad7f490beb0110013 to your computer and use it in GitHub Desktop.
// 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