mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Move boilerplate HTML from tools to webapp
This breaks a strong dependency between the webapp package and the bundler. Now we can change the standard page format, add more hooks, etc without changing the tools (and hot code push works properly). This is an incompatible change to the definition of the browser-program-pre1 format: it no longer contains a "page" field pointing to a boilerplate defined using ad hoc ##FOO## substitution. Instead, the manifest can contain "head" and "body" entries for data added with compileStep.appendDocument(). (Note that in Blaze, <body> in a template file no longer calls appendDocument.) WebApp.addHtmlAttributeHook callbacks now return attribute objects (like those that Spacebars supports) rather than strings.
This commit is contained in:
@@ -2,6 +2,14 @@
|
||||
|
||||
* Log out a user's other sessions when they change their password.
|
||||
|
||||
* Move boilerplate HTML from tools to webapp. Changes internal
|
||||
Webapp.addHtmlAttributeHook API incompatibly.
|
||||
|
||||
|
||||
## v0.8.0
|
||||
|
||||
(Currently being stabilized. Features Blaze.)
|
||||
|
||||
|
||||
## v0.7.2
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ var browserEnabled = function(request) {
|
||||
|
||||
WebApp.addHtmlAttributeHook(function (request) {
|
||||
if (browserEnabled(request))
|
||||
return 'manifest="/app.manifest"';
|
||||
return { manifest: "/app.manifest" };
|
||||
else
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -7,9 +7,7 @@ Package.on_use(function (api) {
|
||||
api.use('deps');
|
||||
api.use('minimongo'); // for idStringify
|
||||
api.export('ObserveSequence');
|
||||
// XXX this does also run on the server but as long as deps is not
|
||||
// documented to run there let's not try
|
||||
api.add_files(['observe_sequence.js'], 'client');
|
||||
api.add_files(['observe_sequence.js']);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
|
||||
@@ -101,6 +101,9 @@ StarTranslator._writeClientProg = function (bundlePath, clientProgPath) {
|
||||
var clientManifest = {
|
||||
"format": "browser-program-pre1",
|
||||
"manifest": origClientManifest.manifest,
|
||||
// XXX Haven't updated this for the app.html -> head/body change, but
|
||||
// surely we don't need to because code in pre-star apps doesn't
|
||||
// even read this file?
|
||||
"page": "app.html",
|
||||
"static": "static",
|
||||
"staticCacheable": "static_cacheable"
|
||||
|
||||
22
packages/webapp/boilerplate.html
Normal file
22
packages/webapp/boilerplate.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<html {{htmlAttributes}}>
|
||||
<head>
|
||||
{{#each css}} <link rel="stylesheet" href="{{bundledJsCssPrefix}}{{url}}">{{/each}}
|
||||
|
||||
{{#if inlineScriptsAllowed}}
|
||||
<script type='text/javascript'>__meteor_runtime_config__ = {{meteorRuntimeConfig}};</script>
|
||||
{{else}}
|
||||
<script type='text/javascript' src='{{rootUrlPathPrefix}}/meteor_runtime_config.js'></script>
|
||||
{{/if}}
|
||||
{{#each js}} <script type="text/javascript" src="{{bundledJsCssPrefix}}{{url}}"></script>
|
||||
{{/each}}
|
||||
{{#if inlineScriptsAllowed}}
|
||||
<script type='text/javascript'>{{{reloadSafetyBelt}}}</script>
|
||||
{{else}}
|
||||
<script type='text/javascript' src='{{rootUrlPathPrefix}}/meteor_reload_safetybelt.js'></script>
|
||||
{{/if}}
|
||||
{{{head}}}
|
||||
</head>
|
||||
<body>
|
||||
{{{body}}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,9 @@ Npm.depends({connect: "2.9.0",
|
||||
useragent: "2.0.7"});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['logging', 'underscore', 'routepolicy'], 'server');
|
||||
api.use(['logging', 'underscore', 'routepolicy', 'spacebars-compiler',
|
||||
'spacebars', 'htmljs'],
|
||||
'server');
|
||||
api.use(['underscore'], 'client');
|
||||
api.use(['application-configuration', 'follower-livedata'], {
|
||||
unordered: true
|
||||
@@ -21,5 +23,10 @@ Package.on_use(function (api) {
|
||||
api.export(['WebApp', 'main', 'WebAppInternals'], 'server');
|
||||
api.export(['WebApp'], 'client');
|
||||
api.add_files('webapp_server.js', 'server');
|
||||
// This is a spacebars template, but we process it manually with the spacebars
|
||||
// compiler rather than letting the 'templating' package (which isn't fully
|
||||
// supported on the server yet) handle it. That also means that it doesn't
|
||||
// contain the outer "<template>" tag.
|
||||
api.add_files('boilerplate.html', 'server', {isAsset: true});
|
||||
api.add_files('webapp_client.js', 'client');
|
||||
});
|
||||
|
||||
@@ -25,10 +25,10 @@ var bundledJsCssPrefix;
|
||||
// CSS. This prevents you from displaying the page in that case, and instead
|
||||
// reloads it, presumably all on the new version now.
|
||||
var RELOAD_SAFETYBELT = "\n" +
|
||||
"if (typeof Package === 'undefined' || \n" +
|
||||
" ! Package.webapp || \n" +
|
||||
" ! Package.webapp.WebApp || \n" +
|
||||
" ! Package.webapp.WebApp._isCssLoaded()) \n" +
|
||||
"if (typeof Package === 'undefined' ||\n" +
|
||||
" ! Package.webapp ||\n" +
|
||||
" ! Package.webapp.WebApp ||\n" +
|
||||
" ! Package.webapp.WebApp._isCssLoaded())\n" +
|
||||
" document.location.reload(); \n";
|
||||
|
||||
// Keepalives so that when the outer server dies unceremoniously and
|
||||
@@ -131,14 +131,17 @@ WebApp.categorizeRequest = function (req) {
|
||||
// be added to the '<html>' tag. Each function is passed a 'request' object (see
|
||||
// #BrowserIdentification) and should return a string,
|
||||
var htmlAttributeHooks = [];
|
||||
var htmlAttributes = function (template, request) {
|
||||
var attributes = '';
|
||||
var getHtmlAttributes = function (request) {
|
||||
var combinedAttributes = {};
|
||||
_.each(htmlAttributeHooks || [], function (hook) {
|
||||
var attribute = hook(request);
|
||||
if (attribute !== null && attribute !== undefined && attribute !== '')
|
||||
attributes += ' ' + attribute;
|
||||
var attributes = hook(request);
|
||||
if (attributes === null)
|
||||
return;
|
||||
if (typeof attributes !== 'object')
|
||||
throw Error("HTML attribute hook must return null or object");
|
||||
_.extend(combinedAttributes, attributes);
|
||||
});
|
||||
return template.replace('##HTML_ATTRIBUTES##', attributes);
|
||||
return combinedAttributes;
|
||||
};
|
||||
WebApp.addHtmlAttributeHook = function (hook) {
|
||||
htmlAttributeHooks.push(hook);
|
||||
@@ -432,13 +435,17 @@ var runWebAppServer = function () {
|
||||
});
|
||||
|
||||
// Will be updated by main before we listen.
|
||||
var boilerplateHtml = null;
|
||||
var boilerplateTemplate = null;
|
||||
var boilerplateBaseData = null;
|
||||
var boilerplateByAttributes = {};
|
||||
app.use(function (req, res, next) {
|
||||
if (! appUrl(req.url))
|
||||
return next();
|
||||
|
||||
if (!boilerplateHtml)
|
||||
throw new Error("boilerplateHtml should be set before listening!");
|
||||
if (!boilerplateTemplate)
|
||||
throw new Error("boilerplateTemplate should be set before listening!");
|
||||
if (!boilerplateBaseData)
|
||||
throw new Error("boilerplateBaseData should be set before listening!");
|
||||
|
||||
|
||||
var headers = {
|
||||
@@ -459,9 +466,31 @@ var runWebAppServer = function () {
|
||||
res.end();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var htmlAttributes = getHtmlAttributes(request);
|
||||
|
||||
// The only thing that changes from request to request (for now) are the
|
||||
// HTML attributes (used by, eg, appcache), so we can memoize based on that.
|
||||
var attributeKey = JSON.stringify(htmlAttributes);
|
||||
if (!_.has(boilerplateByAttributes, attributeKey)) {
|
||||
try {
|
||||
var boilerplateData = _.extend({htmlAttributes: htmlAttributes},
|
||||
boilerplateBaseData);
|
||||
var boilerplateInstance = boilerplateTemplate.extend({
|
||||
data: boilerplateData
|
||||
});
|
||||
var boilerplateHtmlJs = boilerplateInstance.render();
|
||||
boilerplateByAttributes[attributeKey] = "<!DOCTYPE html>\n" +
|
||||
HTML.toHTML(boilerplateHtmlJs, boilerplateInstance);
|
||||
} catch (e) {
|
||||
res.writeHead(500, headers);
|
||||
res.end();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, headers);
|
||||
var requestSpecificHtml = htmlAttributes(boilerplateHtml, request);
|
||||
res.write(requestSpecificHtml);
|
||||
res.write(boilerplateByAttributes[attributeKey]);
|
||||
res.end();
|
||||
return undefined;
|
||||
});
|
||||
@@ -559,37 +588,46 @@ var runWebAppServer = function () {
|
||||
// '--keepalive' is a use of the option.
|
||||
var expectKeepalives = _.contains(argv, '--keepalive');
|
||||
|
||||
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
|
||||
boilerplateHtml = fs.readFileSync(boilerplateHtmlPath, 'utf8');
|
||||
boilerplateBaseData = {
|
||||
css: [],
|
||||
js: [],
|
||||
head: '',
|
||||
body: '',
|
||||
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(),
|
||||
meteorRuntimeConfig: JSON.stringify(__meteor_runtime_config__),
|
||||
reloadSafetyBelt: RELOAD_SAFETYBELT,
|
||||
rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '',
|
||||
bundledJsCssPrefix: bundledJsCssPrefix ||
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''
|
||||
};
|
||||
|
||||
// Include __meteor_runtime_config__ in the app html, as an inline script if
|
||||
// it's not forbidden by CSP.
|
||||
if (WebAppInternals.inlineScriptsAllowed()) {
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RUNTIME_CONFIG##/,
|
||||
"<script type='text/javascript'>__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";</script>");
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RELOAD_SAFETYBELT##/,
|
||||
"<script type='text/javascript'>"+RELOAD_SAFETYBELT+"</script>");
|
||||
} else {
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RUNTIME_CONFIG##/,
|
||||
"<script type='text/javascript' src='##ROOT_URL_PATH_PREFIX##/meteor_runtime_config.js'></script>"
|
||||
);
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RELOAD_SAFETYBELT##/,
|
||||
"<script type='text/javascript' src='##ROOT_URL_PATH_PREFIX##/meteor_reload_safetybelt.js'></script>");
|
||||
_.each(WebApp.clientProgram.manifest, function (item) {
|
||||
if (item.type === 'css' && item.where === 'client') {
|
||||
boilerplateBaseData.css.push({url: item.url});
|
||||
}
|
||||
if (item.type === 'js' && item.where === 'client') {
|
||||
boilerplateBaseData.js.push({url: item.url});
|
||||
}
|
||||
if (item.type === 'head') {
|
||||
boilerplateBaseData.head = fs.readFileSync(
|
||||
path.join(clientDir, item.path), 'utf8');
|
||||
}
|
||||
if (item.type === 'body') {
|
||||
boilerplateBaseData.body = fs.readFileSync(
|
||||
path.join(clientDir, item.path), 'utf8');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##ROOT_URL_PATH_PREFIX##/g,
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
||||
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##BUNDLED_JS_CSS_PREFIX##/g,
|
||||
bundledJsCssPrefix ||
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
||||
var boilerplateTemplateSource = Assets.getText("boilerplate.html");
|
||||
var boilerplateRenderCode = Spacebars.compile(
|
||||
boilerplateTemplateSource, { isBody: true });
|
||||
// Use 'new Function' instead of eval to avoid capturing local variables of
|
||||
// this context.
|
||||
var boilerplateRender = new Function("return " + boilerplateRenderCode)();
|
||||
boilerplateTemplate = UI.Component.extend({
|
||||
kind: "MainPage",
|
||||
render: boilerplateRender
|
||||
});
|
||||
|
||||
// only start listening after all the startup code has run.
|
||||
var localPort = parseInt(process.env.PORT) || 0;
|
||||
|
||||
@@ -59,11 +59,6 @@
|
||||
//
|
||||
// - format: "browser-program-pre1" for this version
|
||||
//
|
||||
// - page: path to the template for the HTML to serve when a browser
|
||||
// loads a page that is part of the application. In the file,
|
||||
// some strings of the format ##FOO## will be replaced with
|
||||
// appropriate values at runtime by the webapp package.
|
||||
//
|
||||
// - manifest: array of resources to serve with HTTP, each an object:
|
||||
// - path: path of file relative to program.json
|
||||
// - where: "client"
|
||||
@@ -74,15 +69,11 @@
|
||||
// - size: size of file in bytes
|
||||
// - hash: sha1 hash of the file contents
|
||||
// - sourceMap: optional path to source map file (relative to program.json)
|
||||
// Additionally there will be an entry with where equal to
|
||||
// "internal", path equal to page (above), and hash equal to the
|
||||
// sha1 of page (before replacements). Currently this is used to
|
||||
// trigger HTML5 appcache reloads at the right time (if the
|
||||
// 'appcache' package is being used).
|
||||
//
|
||||
// Convention:
|
||||
//
|
||||
// page is 'app.html'.
|
||||
// Additionally there may be a manifest entry with where equal to
|
||||
// "internal", type "head" or "body", and a path and hash. These contain
|
||||
// chunks of HTML which should be inserted in the boilerplate HTML page's
|
||||
// <head> or <body> respectively.
|
||||
//
|
||||
//
|
||||
// == Format of a program when arch is "os.*" ==
|
||||
@@ -876,42 +867,6 @@ _.extend(ClientTarget.prototype, {
|
||||
self.css[0].setUrlToHash(".css", "?meteor_css_resource=true");
|
||||
},
|
||||
|
||||
// XXX Instead of packaging the boilerplate in the client program, the
|
||||
// template should be part of WebApp, and we should make sure that all
|
||||
// information that it needs is in the manifest (ie, make sure to include head
|
||||
// and body). Then it will just need to do one level of templating instead
|
||||
// of two. Alternatively, use spacebars with unipackage.load here.
|
||||
generateHtmlBoilerplate: function () {
|
||||
var self = this;
|
||||
|
||||
var html = [];
|
||||
html.push('<!DOCTYPE html>\n' +
|
||||
'<html##HTML_ATTRIBUTES##>\n' +
|
||||
'<head>\n');
|
||||
_.each(self.css, function (css) {
|
||||
html.push(' <link rel="stylesheet" href="##BUNDLED_JS_CSS_PREFIX##');
|
||||
html.push(_.escape(css.url));
|
||||
html.push('">\n');
|
||||
});
|
||||
html.push('\n\n##RUNTIME_CONFIG##\n\n');
|
||||
_.each(self.js, function (js) {
|
||||
html.push(' <script type="text/javascript" src="##BUNDLED_JS_CSS_PREFIX##');
|
||||
html.push(_.escape(js.url));
|
||||
html.push('"></script>\n');
|
||||
});
|
||||
html.push('\n\n##RELOAD_SAFETYBELT##');
|
||||
html.push('\n\n');
|
||||
html.push(self.head.join('\n')); // unescaped!
|
||||
html.push('\n' +
|
||||
'</head>\n' +
|
||||
'<body>\n');
|
||||
html.push(self.body.join('\n')); // unescaped!
|
||||
html.push('\n' +
|
||||
'</body>\n' +
|
||||
'</html>\n');
|
||||
return new Buffer(html.join(''), 'utf8');
|
||||
},
|
||||
|
||||
// Output the finished target to disk
|
||||
//
|
||||
// Returns the path (relative to 'builder') of the control file for
|
||||
@@ -920,7 +875,6 @@ _.extend(ClientTarget.prototype, {
|
||||
var self = this;
|
||||
|
||||
builder.reserve("program.json");
|
||||
builder.reserve("app.html");
|
||||
|
||||
// Helper to iterate over all resources that we serve over HTTP.
|
||||
var eachResource = function (f) {
|
||||
@@ -988,20 +942,24 @@ _.extend(ClientTarget.prototype, {
|
||||
manifest.push(manifestItem);
|
||||
});
|
||||
|
||||
// HTML boilerplate (the HTML served to make the client load the
|
||||
// JS and CSS files and start the app)
|
||||
var htmlBoilerplate = self.generateHtmlBoilerplate();
|
||||
builder.write('app.html', { data: htmlBoilerplate });
|
||||
manifest.push({
|
||||
path: 'app.html',
|
||||
where: 'internal',
|
||||
hash: Builder.sha1(htmlBoilerplate)
|
||||
_.each(['head', 'body'], function (type) {
|
||||
var data = self[type].join('\n');
|
||||
if (data) {
|
||||
var dataBuffer = new Buffer(data, 'utf8');
|
||||
var dataFile = builder.writeToGeneratedFilename(
|
||||
type + '.html', { data: dataBuffer });
|
||||
manifest.push({
|
||||
path: dataFile,
|
||||
where: 'internal',
|
||||
type: type,
|
||||
hash: Builder.sha1(dataBuffer)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Control file
|
||||
builder.writeJson('program.json', {
|
||||
format: "browser-program-pre1",
|
||||
page: 'app.html',
|
||||
manifest: manifest
|
||||
});
|
||||
return "program.json";
|
||||
|
||||
@@ -15,6 +15,12 @@ var tmpDir = function () {
|
||||
};
|
||||
|
||||
var runTest = function () {
|
||||
var readManifest = function (tmpOutputDir) {
|
||||
return JSON.parse(fs.readFileSync(
|
||||
path.join(tmpOutputDir, "programs", "client", "program.json"),
|
||||
"utf8")).manifest;
|
||||
};
|
||||
|
||||
console.log("nodeModules: 'skip'");
|
||||
assert.doesNotThrow(function () {
|
||||
var tmpOutputDir = tmpDir();
|
||||
@@ -38,10 +44,13 @@ var runTest = function () {
|
||||
.isDirectory());
|
||||
|
||||
// verify that contents are minified
|
||||
var appHtml = fs.readFileSync(path.join(tmpOutputDir, "programs",
|
||||
"client", "app.html"), 'utf8');
|
||||
assert(/src=\"##BUNDLED_JS_CSS_PREFIX##\/[0-9a-f]{40,40}.js\"/.test(appHtml));
|
||||
assert(!(/src=\"##BUNDLED_JS_CSS_PREFIX##\/packages/.test(appHtml)));
|
||||
var manifest = readManifest(tmpOutputDir);
|
||||
_.each(manifest, function (item) {
|
||||
if (item.type !== 'js')
|
||||
return;
|
||||
// Just a hash, and no "packages/".
|
||||
assert(/^[0-9a-f]{40,40}\.js$/.test(item.path));
|
||||
});
|
||||
});
|
||||
|
||||
console.log("nodeModules: 'skip', no minify");
|
||||
@@ -58,14 +67,25 @@ var runTest = function () {
|
||||
// sanity check -- main.js has expected contents.
|
||||
assert.strictEqual(fs.readFileSync(path.join(tmpOutputDir, "main.js"), "utf8"),
|
||||
bundler._mainJsContents);
|
||||
|
||||
// verify that contents are not minified
|
||||
var appHtml = fs.readFileSync(path.join(tmpOutputDir, "programs",
|
||||
"client", "app.html"), 'utf8');
|
||||
assert(!(/src=\"##BUNDLED_JS_CSS_PREFIX##\/[0-9a-f]{40,40}.js\"/.test(appHtml)));
|
||||
assert(/src=\"##BUNDLED_JS_CSS_PREFIX##\/packages\/meteor/.test(appHtml));
|
||||
assert(/src=\"##BUNDLED_JS_CSS_PREFIX##\/packages\/deps/.test(appHtml));
|
||||
// verify that tests aren't included
|
||||
assert(!(/src=\"##BUNDLED_JS_CSS_PREFIX##\/package-tests\/meteor/.test(appHtml)));
|
||||
var manifest = readManifest(tmpOutputDir);
|
||||
var foundMeteor = false;
|
||||
var foundDeps = false;
|
||||
_.each(manifest, function (item) {
|
||||
if (item.type !== 'js')
|
||||
return;
|
||||
// No minified hash.
|
||||
assert(!/^[0-9a-f]{40,40}\.js$/.test(item.path));
|
||||
// No tests.
|
||||
assert(!/:tests/.test(item.path));
|
||||
if (item.path === 'packages/meteor.js')
|
||||
foundMeteor = true;
|
||||
if (item.path === 'packages/deps.js')
|
||||
foundDeps = true;
|
||||
});
|
||||
assert(foundMeteor);
|
||||
assert(foundDeps);
|
||||
});
|
||||
|
||||
console.log("nodeModules: 'skip', no minify, testPackages: ['meteor']");
|
||||
@@ -82,10 +102,12 @@ var runTest = function () {
|
||||
// sanity check -- main.js has expected contents.
|
||||
assert.strictEqual(fs.readFileSync(path.join(tmpOutputDir, "main.js"), "utf8"),
|
||||
bundler._mainJsContents);
|
||||
|
||||
// verify that tests for the meteor package are included
|
||||
var appHtml = fs.readFileSync(path.join(tmpOutputDir, "programs",
|
||||
"client", "app.html"));
|
||||
assert(/src=\"##BUNDLED_JS_CSS_PREFIX##\/packages\/meteor:tests\.js/.test(appHtml));
|
||||
var manifest = readManifest(tmpOutputDir);
|
||||
assert(_.find(manifest, function (item) {
|
||||
return item.type === 'js' && item.path === 'packages/meteor:tests.js';
|
||||
}));
|
||||
});
|
||||
|
||||
console.log("nodeModules: 'copy'");
|
||||
|
||||
Reference in New Issue
Block a user