chore: internalize meteor inject initial (from meteor 3) (#31367)
parent
ebbad299b9
commit
f57e21576d
@ -0,0 +1,50 @@ |
||||
// Hijack core node API and attach data to the response dynamically
|
||||
// We are simply using this hack because, there is no way to alter
|
||||
// Meteor's html content on the server side
|
||||
|
||||
Inject._hijackWrite = function (res) { |
||||
const originalWrite = res.write; |
||||
res.write = function (chunk, encoding) { |
||||
// prevent hijacking other http requests
|
||||
if (!res.iInjected && encoding === undefined && /^<!DOCTYPE html>/.test(chunk)) { |
||||
chunk = chunk.toString(); |
||||
|
||||
for (id in Inject.rawModHtmlFuncs) { |
||||
chunk = Inject.rawModHtmlFuncs[id](chunk, res); |
||||
if (!_.isString(chunk)) { |
||||
throw new Error(`Inject func id "${id}" must return HTML, not ${typeof chunk}\n${JSON.stringify(chunk, null, 2)}`); |
||||
} |
||||
} |
||||
|
||||
res.iInjected = true; |
||||
} |
||||
|
||||
originalWrite.call(res, chunk, encoding); |
||||
}; |
||||
}; |
||||
|
||||
WebApp.connectHandlers.use(function (req, res, next) { |
||||
// We only separate this to make testing easier
|
||||
Inject._hijackWrite(res); |
||||
|
||||
next(); |
||||
}); |
||||
|
||||
// meteor algorithm to check if this is a meteor serving http request or not
|
||||
Inject.appUrl = function (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 id 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 (typeof RoutePolicy !== 'undefined' && RoutePolicy.classify(url)) return false; |
||||
|
||||
// we currently return app HTML on all URLs by default
|
||||
return true; |
||||
}; |
||||
@ -0,0 +1,156 @@ |
||||
function escapeReplaceString(str) { |
||||
/* |
||||
* When using string.replace(str, newSubStr), the dollar sign ("$") is |
||||
* considered a special character in newSubStr, and needs to be escaped |
||||
* as "$$". We have to do this twice, for escaping the newSubStr in |
||||
* this function, and for the resulting string which is passed back. |
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
|
||||
*/ |
||||
return str.replace(/\$/g, '$$$$'); |
||||
} |
||||
|
||||
Inject = { |
||||
// stores in a script type=application/ejson tag, accessed with Injected.obj('id')
|
||||
obj(id, data, res) { |
||||
this._checkForObjOrFunction(data, 'Inject.obj(id, data [,res]) expects `data` to be an Object or Function'); |
||||
|
||||
if (res) { |
||||
this._resAssign(res, 'objList', id, data); |
||||
} else { |
||||
this.objList[id] = data; |
||||
} |
||||
}, |
||||
objList: {}, |
||||
|
||||
// Inserts a META called `id`, whose `content` can be accessed with Injected.meta()
|
||||
meta(id, data, res) { |
||||
this._checkForTextOrFunction(data, 'Inject.meta(id, data [,res]) expects `data` to be an String or Function'); |
||||
|
||||
if (res) { |
||||
this._resAssign(res, 'metaList', id, data); |
||||
} else { |
||||
this.metaList[id] = data; |
||||
} |
||||
}, |
||||
metaList: {}, |
||||
|
||||
rawHead(id, textOrFunc, res) { |
||||
this._checkForTextOrFunction(textOrFunc, 'Inject.rawHead(id, content [,res]) expects `content` to be an String or Function'); |
||||
|
||||
if (res) { |
||||
this._resAssign(res, 'rawHeads', id, textOrFunc); |
||||
} else { |
||||
this.rawHeads[id] = textOrFunc; |
||||
} |
||||
}, |
||||
rawHeads: {}, |
||||
|
||||
rawBody(id, textOrFunc, res) { |
||||
this._checkForTextOrFunction(textOrFunc, 'Inject.rawBody(id, content [,res]) expects `content` to be an String or Function'); |
||||
|
||||
if (res) { |
||||
this._resAssign(res, 'rawBodies', id, textOrFunc); |
||||
} else { |
||||
this.rawBodies[id] = textOrFunc; |
||||
} |
||||
}, |
||||
rawBodies: {}, |
||||
|
||||
// The callback receives the entire HTML page and must return a modified version
|
||||
rawModHtml(id, func) { |
||||
if (!_.isFunction(func)) { |
||||
const message = `Inject func id "${id}" should be a function, not ${typeof func}`; |
||||
throw new Error(message); |
||||
} |
||||
|
||||
this.rawModHtmlFuncs[id] = func; |
||||
}, |
||||
rawModHtmlFuncs: {}, |
||||
|
||||
_injectObjects(html, res) { |
||||
const objs = _.extend({}, Inject.objList, res.Inject && res.Inject.objList); |
||||
if (_.isEmpty(objs)) { |
||||
return html; |
||||
} |
||||
|
||||
let obj; |
||||
let injectHtml = ''; |
||||
for (id in objs) { |
||||
obj = _.isFunction(objs[id]) ? objs[id](res) : objs[id]; |
||||
injectHtml += ` <script id='${id.replace("'", ''')}' type='application/ejson'>${EJSON.stringify(obj)}</script>\n`; |
||||
} |
||||
|
||||
return html.replace('<head>', `<head>\n${escapeReplaceString(injectHtml)}`); |
||||
}, |
||||
|
||||
_injectMeta(html, res) { |
||||
const metas = _.extend({}, Inject.metaList, res.Inject && res.Inject.metaList); |
||||
if (_.isEmpty(metas)) return html; |
||||
|
||||
let injectHtml = ''; |
||||
for (id in metas) { |
||||
const meta = this._evalToText(metas[id], res, html); |
||||
(injectHtml += ` <meta id='${id.replace("'", ''')}' content='${meta.replace("'", ''')}'>\n`), res; |
||||
} |
||||
|
||||
return html.replace('<head>', `<head>\n${escapeReplaceString(injectHtml)}`); |
||||
}, |
||||
|
||||
_injectHeads(html, res) { |
||||
const heads = _.extend({}, Inject.rawHeads, res.Inject && res.Inject.rawHeads); |
||||
if (_.isEmpty(heads)) return html; |
||||
|
||||
let injectHtml = ''; |
||||
for (id in heads) { |
||||
const head = this._evalToText(heads[id], res, html); |
||||
injectHtml += `${head}\n`; |
||||
} |
||||
|
||||
return html.replace('<head>', `<head>\n${escapeReplaceString(injectHtml)}`); |
||||
}, |
||||
|
||||
_injectBodies(html, res) { |
||||
const bodies = _.extend({}, Inject.rawBodies, res.Inject && res.Inject.rawBodies); |
||||
if (_.isEmpty(bodies)) return html; |
||||
|
||||
let injectHtml = ''; |
||||
for (id in bodies) { |
||||
const body = this._evalToText(bodies[id], res, html); |
||||
injectHtml += `${body}\n`; |
||||
} |
||||
|
||||
return html.replace('<body>', `<body>\n${escapeReplaceString(injectHtml)}`); |
||||
}, |
||||
|
||||
// ensure object exists and store there
|
||||
_resAssign(res, key, id, value) { |
||||
if (!res.Inject) res.Inject = {}; |
||||
if (!res.Inject[key]) res.Inject[key] = {}; |
||||
res.Inject[key][id] = value; |
||||
}, |
||||
|
||||
_checkForTextOrFunction(arg, message) { |
||||
if (!(_.isString(arg) || _.isFunction(arg))) { |
||||
throw new Error(message); |
||||
} |
||||
}, |
||||
|
||||
_checkForObjOrFunction(arg, message) { |
||||
if (!(_.isObject(arg) || _.isFunction(arg))) { |
||||
throw new Error(message); |
||||
} |
||||
}, |
||||
|
||||
// we don't handle errors here. Let them to handle in a higher level
|
||||
_evalToText(textOrFunc, res, html) { |
||||
if (_.isFunction(textOrFunc)) { |
||||
return textOrFunc(res, html); |
||||
} |
||||
return textOrFunc; |
||||
}, |
||||
}; |
||||
|
||||
Inject.rawModHtml('injectHeads', Inject._injectHeads.bind(Inject)); |
||||
Inject.rawModHtml('injectMeta', Inject._injectMeta.bind(Inject)); |
||||
Inject.rawModHtml('injectBodies', Inject._injectBodies.bind(Inject)); |
||||
Inject.rawModHtml('injectObjects', Inject._injectObjects.bind(Inject)); |
||||
@ -0,0 +1,16 @@ |
||||
Package.describe({ |
||||
summary: 'Allow injection of arbitrary data to initial Meteor HTML page', |
||||
version: '1.0.5', |
||||
git: 'https://github.com/meteorhacks/meteor-inject-initial.git', |
||||
name: 'meteorhacks:inject-initial', |
||||
}); |
||||
|
||||
Package.onUse(function (api) { |
||||
api.use(['routepolicy', 'webapp'], 'server'); |
||||
api.use(['ejson', 'underscore'], ['server']); |
||||
|
||||
api.addFiles('lib/inject-server.js', 'server'); |
||||
api.addFiles('lib/inject-core.js', 'server'); |
||||
|
||||
api.export('Inject', 'server'); |
||||
}); |
||||
Loading…
Reference in new issue