diff --git a/README.md b/README.md index 1cd7553747..df1861c3e8 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,7 @@ Interested in contributing to Meteor? * Core framework design mailing list: https://groups.google.com/group/meteor-core * Contribution guidelines: https://github.com/meteor/meteor/tree/devel/Contributing.md + +We are hiring! Visit https://www.meteor.com/jobs/working-at-meteor to +learn more about working full-time on the Meteor project. + diff --git a/docs/.meteor/packages b/docs/.meteor/packages index 46c52f81e7..3273520f32 100644 --- a/docs/.meteor/packages +++ b/docs/.meteor/packages @@ -12,3 +12,4 @@ jquery-waypoints less spiderable appcache +reload-safetybelt diff --git a/docs/client/api.html b/docs/client/api.html index adc97f191e..405a90fac4 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -92,15 +92,17 @@ different collections. We hope to lift this restriction in a future release. ]; }); -Alternatively, a publish function can directly control its published -record set by calling the functions [`added`](#publish_added) (to add a -new document to the published record set), [`changed`](#publish_changed) -(to change or clear some fields on a document already in the published -record set), and [`removed`](#publish_removed) (to remove documents from -the published record set). Publish functions that use these functions -should also call [`ready`](#publish_ready) once the initial record set -is complete. These methods are provided by `this` in your publish -function. +Alternatively, a publish function can directly control its published record set +by calling the functions [`added`](#publish_added) (to add a new document to the +published record set), [`changed`](#publish_changed) (to change or clear some +fields on a document already in the published record set), and +[`removed`](#publish_removed) (to remove documents from the published record +set). These methods are provided by `this` in your publish function. + +If a publish function does not return a cursor or array of cursors, it is +assumed to be using the low-level `added`/`changed`/`removed` interface, and it +**must also call [`ready`](#publish_ready) once the initial record set is +complete**. Example: @@ -156,6 +158,19 @@ Example: Counts.findOne(Session.get("roomId")).count + " messages."); + // server: sometimes publish a query, sometimes publish nothing + Meteor.publish("secretData", function () { + if (this.userId === 'superuser') { + return SecretData.find(); + } else { + // Declare that no data is being published. If you leave this line + // out, Meteor will never consider the subscription ready because + // it thinks you're using the added/changed/removed interface where + // you have to explicitly call this.ready(). + return []; + } + }); + {{#warning}} Meteor will emit a warning message if you call `Meteor.publish` in a project that includes the `autopublish` package. Your publish function diff --git a/docs/client/api.js b/docs/client/api.js index e1d5d24746..b389f4fad9 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1087,7 +1087,7 @@ Template.api.loginWithPassword = { { name: "password", type: "String", - descr: "The user's password. This is __not__ sent in plain text over the wire — it is secured with [SRP](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)." + descr: "The user's password." }, { name: "callback", diff --git a/meteor b/meteor index 93ddb8cc13..4ce7672a42 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.38 +BUNDLE_VERSION=0.3.38 # warning! 0.3.39-40 used on packaging branch # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index bc04cc8754..3767a86bb7 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -41,9 +41,9 @@ Meteor.loginWithPassword = function (selector, password, callback) { }, callback); } else if (error) { - callback(error); + callback && callback(error); } else { - callback(); + callback && callback(); } } }); @@ -69,9 +69,9 @@ var srpUpgradePath = function (options, callback) { details = EJSON.parse(options.upgradeError.details); } catch (e) {} if (!(details && details.format === 'srp')) { - callback(new Meteor.Error(400, - "Password is old. Please reset your " + - "password.")); + callback && callback( + new Meteor.Error(400, "Password is old. Please reset your " + + "password.")); } else { Accounts.callLoginMethod({ methodArguments: [{ @@ -133,7 +133,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { plaintextPassword: oldPassword }, function (err) { if (err) { - callback(err); + callback && callback(err); } else { // Now that we've successfully migrated from srp to // bcrypt, try changing the password again. diff --git a/packages/observe-sequence/observe_sequence.js b/packages/observe-sequence/observe_sequence.js index e4c1b4dea2..b4a7dc9b71 100644 --- a/packages/observe-sequence/observe_sequence.js +++ b/packages/observe-sequence/observe_sequence.js @@ -127,7 +127,7 @@ ObserveSequence = { }); diffArray(lastSeqArray, seqArray, callbacks); - } else if (isMinimongoCursor(seq)) { + } else if (isStoreCursor(seq)) { var cursor = seq; seqArray = []; @@ -188,7 +188,7 @@ ObserveSequence = { return []; } else if (seq instanceof Array) { return seq; - } else if (isMinimongoCursor(seq)) { + } else if (isStoreCursor(seq)) { return seq.fetch(); } else { throw badSequenceError(); @@ -201,9 +201,9 @@ var badSequenceError = function () { "arrays, cursors or falsey values."); }; -var isMinimongoCursor = function (seq) { - var minimongo = Package.minimongo; - return !!minimongo && (seq instanceof minimongo.LocalCollection.Cursor); +var isStoreCursor = function (cursor) { + return cursor && _.isObject(cursor) && + _.isFunction(cursor.observe) && _.isFunction(cursor.fetch); }; // Calculates the differences between `lastSeqArray` and diff --git a/packages/reload-safetybelt/.gitignore b/packages/reload-safetybelt/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/reload-safetybelt/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/reload-safetybelt/package.js b/packages/reload-safetybelt/package.js new file mode 100644 index 0000000000..d551a93de3 --- /dev/null +++ b/packages/reload-safetybelt/package.js @@ -0,0 +1,16 @@ +Package.describe({ + summary: "Reload safety belt for multi-server deployments", + internal: true +}); + +Package.on_use(function (api) { + api.use("webapp", "server"); + api.add_files("reload-safety-belt.js", "server"); + api.add_files("safetybelt.js", "server", { isAsset: true }); +}); + +Package.on_test(function (api) { + api.add_files("safetybelt.js", "server", { isAsset: true }); + api.use(["reload-safetybelt", "tinytest", "http", "webapp"]); + api.add_files("reload-safety-belt-tests.js", "server"); +}); diff --git a/packages/reload-safetybelt/reload-safety-belt-tests.js b/packages/reload-safetybelt/reload-safety-belt-tests.js new file mode 100644 index 0000000000..117a0110a5 --- /dev/null +++ b/packages/reload-safetybelt/reload-safety-belt-tests.js @@ -0,0 +1,10 @@ +var script = Assets.getText("safetybelt.js"); + +Tinytest.add("reload-safetybelt - safety belt is added", function (test) { + test.isTrue(_.some( + WebAppInternals.additionalStaticJs, + function (js, pathname) { + return js === script; + } + )); +}); diff --git a/packages/reload-safetybelt/reload-safety-belt.js b/packages/reload-safetybelt/reload-safety-belt.js new file mode 100644 index 0000000000..4c255ab3a7 --- /dev/null +++ b/packages/reload-safetybelt/reload-safety-belt.js @@ -0,0 +1,6 @@ +// The reload safetybelt is some js that will be loaded after everything else in +// the HTML. In some multi-server deployments, when you update, you have a +// chance of hitting an old server for the HTML and the new server for the JS or +// CSS. This prevents you from displaying the page in that case, and instead +// reloads it, presumably all on the new version now. +WebAppInternals.addStaticJs(Assets.getText("safetybelt.js")); diff --git a/packages/reload-safetybelt/safetybelt.js b/packages/reload-safetybelt/safetybelt.js new file mode 100644 index 0000000000..32e0a9953b --- /dev/null +++ b/packages/reload-safetybelt/safetybelt.js @@ -0,0 +1,6 @@ +if (typeof Package === 'undefined' || + ! Package.webapp || + ! Package.webapp.WebApp || + ! Package.webapp.WebApp._isCssLoaded()) { + document.location.reload(); +} diff --git a/packages/webapp/boilerplate.html b/packages/webapp/boilerplate.html index e71c29c5ba..d3a012b206 100644 --- a/packages/webapp/boilerplate.html +++ b/packages/webapp/boilerplate.html @@ -9,11 +9,17 @@ {{/if}} {{#each js}} {{/each}} -{{#if inlineScriptsAllowed}} - -{{else}} - -{{/if}} +{{#each additionalStaticJs}} + {{#if ../inlineScriptsAllowed}} + + {{else}} + + {{/if}} +{{/each}} + {{{head}}} diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 6168674e20..a1f2353648 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -19,18 +19,6 @@ WebAppInternals = {}; var bundledJsCssPrefix; -// The reload safetybelt is some js that will be loaded after everything else in -// the HTML. In some multi-server deployments, when you update, you have a -// chance of hitting an old server for the HTML and the new server for the JS or -// 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" + - " document.location.reload(); \n"; - // Keepalives so that when the outer server dies unceremoniously and // doesn't kill us, we quit ourselves. A little gross, but better than // pidfiles. @@ -235,6 +223,158 @@ WebApp._timeoutAdjustmentRequestCallback = function (req, res) { _.each(finishListeners, function (l) { res.on('finish', l); }); }; +// Will be updated by main before we listen. +var boilerplateFunc = null; +var boilerplateBaseData = null; +var memoizedBoilerplate = {}; + +// Given a request (as returned from `categorizeRequest`), return the +// boilerplate HTML to serve for that request. Memoizes on HTML +// attributes (used by, eg, appcache) and whether inline scripts are +// currently allowed. +var getBoilerplate = function (request) { + var htmlAttributes = getHtmlAttributes(request); + + // The only thing that changes from request to request (for now) are + // the HTML attributes (used by, eg, appcache) and whether inline + // scripts are allowed, so we can memoize based on that. + var boilerplateKey = JSON.stringify({ + inlineScriptsAllowed: inlineScriptsAllowed, + htmlAttributes: htmlAttributes + }); + + if (! _.has(memoizedBoilerplate, boilerplateKey)) { + var boilerplateData = _.extend({ + htmlAttributes: htmlAttributes, + inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed() + }, boilerplateBaseData); + + memoizedBoilerplate[boilerplateKey] = "\n" + + Blaze.toHTML(Blaze.With(boilerplateData, boilerplateFunc)); + } + return memoizedBoilerplate[boilerplateKey]; +}; + +// Serve static files from the manifest or added with +// `addStaticJs`. Exported for tests. +// Options are: +// - staticFiles: object mapping pathname of file in manifest -> { +// path, cacheable, sourceMapUrl, type } +// - clientDir: root directory for static files from client manifest +WebAppInternals.staticFilesMiddleware = function (options, req, res, next) { + if ('GET' != req.method && 'HEAD' != req.method) { + next(); + return; + } + var pathname = connect.utils.parseUrl(req).pathname; + var staticFiles = options.staticFiles; + var clientDir = options.clientDir; + + try { + pathname = decodeURIComponent(pathname); + } catch (e) { + next(); + return; + } + + var serveStaticJs = function (s) { + res.writeHead(200, { + 'Content-type': 'application/javascript; charset=UTF-8' + }); + res.write(s); + res.end(); + }; + + if (pathname === "/meteor_runtime_config.js" && + ! WebAppInternals.inlineScriptsAllowed()) { + serveStaticJs("__meteor_runtime_config__ = " + + JSON.stringify(__meteor_runtime_config__) + ";"); + return; + } else if (_.has(additionalStaticJs, pathname) && + ! WebAppInternals.inlineScriptsAllowed()) { + serveStaticJs(additionalStaticJs[pathname]); + return; + } + + if (!_.has(staticFiles, pathname)) { + next(); + return; + } + + // We don't need to call pause because, unlike 'static', once we call into + // 'send' and yield to the event loop, we never call another handler with + // 'next'. + + var info = staticFiles[pathname]; + + // Cacheable files are files that should never change. Typically + // named by their hash (eg meteor bundled js and css files). + // We cache them ~forever (1yr). + // + // We cache non-cacheable files anyway. This isn't really correct, as users + // can change the files and changes won't propagate immediately. However, if + // we don't cache them, browsers will 'flicker' when rerendering + // images. Eventually we will probably want to rewrite URLs of static assets + // to include a query parameter to bust caches. That way we can both get + // good caching behavior and allow users to change assets without delay. + // https://github.com/meteor/meteor/issues/773 + var maxAge = info.cacheable + ? 1000 * 60 * 60 * 24 * 365 + : 1000 * 60 * 60 * 24; + + // Set the X-SourceMap header, which current Chrome understands. + // (The files also contain '//#' comments which FF 24 understands and + // Chrome doesn't understand yet.) + // + // Eventually we should set the SourceMap header but the current version of + // Chrome and no version of FF supports it. + // + // To figure out if your version of Chrome should support the SourceMap + // header, + // - go to chrome://version. Let's say the Chrome version is + // 28.0.1500.71 and the Blink version is 537.36 (@153022) + // - go to http://src.chromium.org/viewvc/blink/branches/chromium/1500/Source/core/inspector/InspectorPageAgent.cpp?view=log + // where the "1500" is the third part of your Chrome version + // - find the first revision that is no greater than the "153022" + // number. That's probably the first one and it probably has + // a message of the form "Branch 1500 - blink@r149738" + // - If *that* revision number (149738) is at least 151755, + // then Chrome should support SourceMap (not just X-SourceMap) + // (The change is https://codereview.chromium.org/15832007) + // + // You also need to enable source maps in Chrome: open dev tools, click + // the gear in the bottom right corner, and select "enable source maps". + // + // Firefox 23+ supports source maps but doesn't support either header yet, + // so we include the '//#' comment for it: + // https://bugzilla.mozilla.org/show_bug.cgi?id=765993 + // In FF 23 you need to turn on `devtools.debugger.source-maps-enabled` + // in `about:config` (it is on by default in FF 24). + if (info.sourceMapUrl) + res.setHeader('X-SourceMap', info.sourceMapUrl); + + if (info.type === "js") { + res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); + } else if (info.type === "css") { + res.setHeader("Content-Type", "text/css; charset=UTF-8"); + } + + send(req, path.join(clientDir, info.path)) + .maxage(maxAge) + .hidden(true) // if we specified a dotfile in the manifest, serve it + .on('error', function (err) { + Log.error("Error serving static file " + err); + res.writeHead(500); + res.end(); + }) + .on('directory', function () { + Log.error("Unexpected directory " + info.path); + res.writeHead(500); + res.end(); + }) + .pipe(res); +}; + var runWebAppServer = function () { var shuttingDown = false; // read the control for the client we'll be serving up @@ -318,115 +458,10 @@ var runWebAppServer = function () { // Serve static files from the manifest. // This is inspired by the 'static' middleware. app.use(function (req, res, next) { - if ('GET' != req.method && 'HEAD' != req.method) { - next(); - return; - } - var pathname = connect.utils.parseUrl(req).pathname; - - try { - pathname = decodeURIComponent(pathname); - } catch (e) { - next(); - return; - } - - var serveStaticJs = function (s) { - res.writeHead(200, { - 'Content-type': 'application/javascript; charset=UTF-8' - }); - res.write(s); - res.end(); - }; - - if (pathname === "/meteor_runtime_config.js" && - ! WebAppInternals.inlineScriptsAllowed()) { - serveStaticJs("__meteor_runtime_config__ = " + - JSON.stringify(__meteor_runtime_config__) + ";"); - return; - } else if (pathname === "/meteor_reload_safetybelt.js" && - ! WebAppInternals.inlineScriptsAllowed()) { - serveStaticJs(RELOAD_SAFETYBELT); - return; - } - - if (!_.has(staticFiles, pathname)) { - next(); - return; - } - - // We don't need to call pause because, unlike 'static', once we call into - // 'send' and yield to the event loop, we never call another handler with - // 'next'. - - var info = staticFiles[pathname]; - - // Cacheable files are files that should never change. Typically - // named by their hash (eg meteor bundled js and css files). - // We cache them ~forever (1yr). - // - // We cache non-cacheable files anyway. This isn't really correct, as users - // can change the files and changes won't propagate immediately. However, if - // we don't cache them, browsers will 'flicker' when rerendering - // images. Eventually we will probably want to rewrite URLs of static assets - // to include a query parameter to bust caches. That way we can both get - // good caching behavior and allow users to change assets without delay. - // https://github.com/meteor/meteor/issues/773 - var maxAge = info.cacheable - ? 1000 * 60 * 60 * 24 * 365 - : 1000 * 60 * 60 * 24; - - // Set the X-SourceMap header, which current Chrome understands. - // (The files also contain '//#' comments which FF 24 understands and - // Chrome doesn't understand yet.) - // - // Eventually we should set the SourceMap header but the current version of - // Chrome and no version of FF supports it. - // - // To figure out if your version of Chrome should support the SourceMap - // header, - // - go to chrome://version. Let's say the Chrome version is - // 28.0.1500.71 and the Blink version is 537.36 (@153022) - // - go to http://src.chromium.org/viewvc/blink/branches/chromium/1500/Source/core/inspector/InspectorPageAgent.cpp?view=log - // where the "1500" is the third part of your Chrome version - // - find the first revision that is no greater than the "153022" - // number. That's probably the first one and it probably has - // a message of the form "Branch 1500 - blink@r149738" - // - If *that* revision number (149738) is at least 151755, - // then Chrome should support SourceMap (not just X-SourceMap) - // (The change is https://codereview.chromium.org/15832007) - // - // You also need to enable source maps in Chrome: open dev tools, click - // the gear in the bottom right corner, and select "enable source maps". - // - // Firefox 23+ supports source maps but doesn't support either header yet, - // so we include the '//#' comment for it: - // https://bugzilla.mozilla.org/show_bug.cgi?id=765993 - // In FF 23 you need to turn on `devtools.debugger.source-maps-enabled` - // in `about:config` (it is on by default in FF 24). - if (info.sourceMapUrl) - res.setHeader('X-SourceMap', info.sourceMapUrl); - - if (info.type === "js") { - res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); - } else if (info.type === "css") { - res.setHeader("Content-Type", "text/css; charset=UTF-8"); - } - - send(req, path.join(clientDir, info.path)) - .maxage(maxAge) - .hidden(true) // if we specified a dotfile in the manifest, serve it - .on('error', function (err) { - Log.error("Error serving static file " + err); - res.writeHead(500); - res.end(); - }) - .on('directory', function () { - Log.error("Unexpected directory " + info.path); - res.writeHead(500); - res.end(); - }) - .pipe(res); + return WebAppInternals.staticFilesMiddleware({ + staticFiles: staticFiles, + clientDir: clientDir + }, req, res, next); }); // Packages and apps can add handlers to this via WebApp.connectHandlers. @@ -447,10 +482,6 @@ var runWebAppServer = function () { res.end("An error message"); }); - // Will be updated by main before we listen. - var boilerplateFunc = null; - var boilerplateBaseData = null; - var boilerplateByAttributes = {}; app.use(function (req, res, next) { if (! appUrl(req.url)) return next(); @@ -460,7 +491,6 @@ var runWebAppServer = function () { if (!boilerplateBaseData) throw new Error("boilerplateBaseData should be set before listening!"); - var headers = { 'Content-Type': 'text/html; charset=utf-8' }; @@ -480,27 +510,18 @@ var runWebAppServer = function () { 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); - boilerplateByAttributes[attributeKey] = "\n" + - Blaze.toHTML(Blaze.With(boilerplateData, boilerplateFunc)); - } catch (e) { - Log.error("Error running template: " + e.stack); - res.writeHead(500, headers); - res.end(); - return undefined; - } + var boilerplate; + try { + boilerplate = getBoilerplate(request); + } catch (e) { + Log.error("Error running template: " + e); + res.writeHead(500, headers); + res.end(); + return undefined; } res.writeHead(200, headers); - res.write(boilerplateByAttributes[attributeKey]); + res.write(boilerplate); res.end(); return undefined; }); @@ -599,13 +620,23 @@ var runWebAppServer = function () { var expectKeepalives = _.contains(argv, '--keepalive'); boilerplateBaseData = { + // 'htmlAttributes' and 'inlineScriptsAllowed' are set at render + // time, because they are allowed to change from request to + // request. css: [], js: [], head: '', body: '', - inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(), + additionalStaticJs: _.map( + additionalStaticJs, + function (contents, pathname) { + return { + pathname: pathname, + contents: contents + }; + } + ), 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 || '' @@ -1002,3 +1033,16 @@ WebAppInternals.setInlineScriptsAllowed = function (value) { WebAppInternals.setBundledJsCssPrefix = function (prefix) { bundledJsCssPrefix = prefix; }; + +// Packages can call `WebAppInternals.addStaticJs` to specify static +// JavaScript to be included in the app. This static JS will be inlined, +// unless inline scripts have been disabled, in which case it will be +// served under `/`. +var additionalStaticJs = {}; +WebAppInternals.addStaticJs = function (contents) { + additionalStaticJs["/" + sha1(contents) + ".js"] = contents; +}; + +// Exported for tests +WebAppInternals.getBoilerplate = getBoilerplate; +WebAppInternals.additionalStaticJs = additionalStaticJs; diff --git a/packages/webapp/webapp_tests.js b/packages/webapp/webapp_tests.js index 1f555c362c..d300e863cd 100644 --- a/packages/webapp/webapp_tests.js +++ b/packages/webapp/webapp_tests.js @@ -1,4 +1,48 @@ var url = Npm.require("url"); +var crypto = Npm.require("crypto"); +var http = Npm.require("http"); + +var additionalScript = "(function () { var foo = 1; })"; +WebAppInternals.addStaticJs(additionalScript); +var hash = crypto.createHash('sha1'); +hash.update(additionalScript); +var additionalScriptPathname = hash.digest('hex') + ".js"; + +// Mock the 'res' object that gets passed to connect handlers. This mock +// just records any utf8 data written to the response and returns it +// when you call `mockResponse.getBody()`. +var MockResponse = function () { + this.buffer = ""; + this.statusCode = null; +}; + +MockResponse.prototype.writeHead = function (statusCode) { + this.statusCode = statusCode; +}; + +MockResponse.prototype.setHeader = function (name, value) { + // nothing +}; + +MockResponse.prototype.write = function (data, encoding) { + if (! encoding || encoding === "utf8") { + this.buffer = this.buffer + data; + } +}; + +MockResponse.prototype.end = function (data, encoding) { + if (! encoding || encoding === "utf8") { + if (data) { + this.buffer = this.buffer + data; + } + } +}; + +MockResponse.prototype.getBody = function () { + return this.buffer; +}; + + Tinytest.add("webapp - content-type header", function (test) { var cssResource = _.find( @@ -21,3 +65,64 @@ Tinytest.add("webapp - content-type header", function (test) { test.equal(resp.headers["content-type"].toLowerCase(), "application/javascript; charset=utf-8"); }); + +Tinytest.add("webapp - additional static javascript", function (test) { + var origInlineScriptsAllowed = WebAppInternals.inlineScriptsAllowed(); + + var staticFilesOpts = { + staticFiles: {}, + clientDir: "/" + }; + + // It's okay to set this global state because we're not going to yield + // before settng it back to what it was originally. + WebAppInternals.setInlineScriptsAllowed(true); + + Meteor._noYieldsAllowed(function () { + var boilerplate = WebAppInternals.getBoilerplate({ + browser: "doesn't-matter", + url: "also-doesnt-matter" + }); + + // When inline scripts are allowed, the script should be inlined. + test.isTrue(boilerplate.indexOf(additionalScript) !== -1); + + // And the script should not be served as its own separate resource, + // meaning that the static file handler should pass on this request. + var res = new MockResponse(); + var req = new http.IncomingMessage(); + req.headers = {}; + req.method = "GET"; + req.url = "/" + additionalScriptPathname; + var nextCalled = false; + WebAppInternals.staticFilesMiddleware( + staticFilesOpts, req, res, function () { + nextCalled = true; + }); + test.isTrue(nextCalled); + + // When inline scripts are disallowed, the script body should not be + // inlined, and the script should be included in a