diff --git a/History.md b/History.md index a067d9aec4..3d6eb26976 100644 --- a/History.md +++ b/History.md @@ -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 diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index ce579583d2..3d4bec1c4c 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -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; }); diff --git a/packages/observe-sequence/package.js b/packages/observe-sequence/package.js index 30be5f0ca5..5009db78d2 100644 --- a/packages/observe-sequence/package.js +++ b/packages/observe-sequence/package.js @@ -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) { diff --git a/packages/star-translate/translator.js b/packages/star-translate/translator.js index 77d08756de..bff404d034 100644 --- a/packages/star-translate/translator.js +++ b/packages/star-translate/translator.js @@ -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" diff --git a/packages/webapp/boilerplate.html b/packages/webapp/boilerplate.html new file mode 100644 index 0000000000..a4fb29f8fb --- /dev/null +++ b/packages/webapp/boilerplate.html @@ -0,0 +1,22 @@ + +
+{{#each css}} {{/each}} + +{{#if inlineScriptsAllowed}} + +{{else}} + +{{/if}} +{{#each js}} +{{/each}} +{{#if inlineScriptsAllowed}} + +{{else}} + +{{/if}} +{{{head}}} + + +{{{body}}} + + diff --git a/packages/webapp/package.js b/packages/webapp/package.js index fab6a96434..2a50bf1aef 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -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 "" tag. + api.add_files('boilerplate.html', 'server', {isAsset: true}); api.add_files('webapp_client.js', 'client'); }); diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index ad3fe4e1cb..793d5be4b3 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -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 '' 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] = "\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##/, - ""); - boilerplateHtml = boilerplateHtml.replace( - /##RELOAD_SAFETYBELT##/, - ""); - } else { - boilerplateHtml = boilerplateHtml.replace( - /##RUNTIME_CONFIG##/, - "" - ); - boilerplateHtml = boilerplateHtml.replace( - /##RELOAD_SAFETYBELT##/, - ""); + _.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; diff --git a/tools/bundler.js b/tools/bundler.js index 0468cfb6d2..775d524b21 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -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 +// or 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('\n' + - '\n' + - '\n'); - _.each(self.css, function (css) { - html.push(' \n'); - }); - html.push('\n\n##RUNTIME_CONFIG##\n\n'); - _.each(self.js, function (js) { - html.push(' \n'); - }); - html.push('\n\n##RELOAD_SAFETYBELT##'); - html.push('\n\n'); - html.push(self.head.join('\n')); // unescaped! - html.push('\n' + - '\n' + - '\n'); - html.push(self.body.join('\n')); // unescaped! - html.push('\n' + - '\n' + - '\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"; diff --git a/tools/tests/old/test-bundler-options.js b/tools/tests/old/test-bundler-options.js index 1304dad392..d82ab956cc 100644 --- a/tools/tests/old/test-bundler-options.js +++ b/tools/tests/old/test-bundler-options.js @@ -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'");