diff --git a/History.md b/History.md index 2821ddcdfa..c96bc8f5b1 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,15 @@ ## vNEXT +## v0.6.5 + +* linker! namespacing, exports, unipackages, weak and unordered dependencies, + etc. sourcemaps (including for coffee). standard-app-packages. don't + implicitly use all app packages. lots of stuff moved from server.js (now + boot.js) to webapp package. plugins! + +* Log + * Fix Mongo selectors of the form: {$regex: /foo/}. * Calling `findOne()` on the server no longer loads the full query result @@ -10,7 +19,16 @@ * Upgraded dependencies: * Node from 0.8.18 to 0.8.24 - * MongoDB from 2.4.3 to 2.4.4 + * MongoDB from 2.4.3 to 2.4.4, now with SSL support + * CleanCSS from 0.8.3 to 1.0.11 + * Underscore from 1.4.4 to 1.5.1 + * Fibers from 1.0.0 to 1.0.1 + +* When removing the last NPM dependency, clean up the `.npm` dir + +* `$ROOT_URL` may now have a path part + +* `new Meteor.Collection("name", {connection: null})` works * Make server-side Mongo inserts, updates, and removes run asynchronously when a callback is passed. @@ -26,6 +44,29 @@ - `Meteor.connect` - `DDP.connect` - `Meteor.http` - `HTTP` +* The `observe` callback `movedTo` now has the fourth argument `before`. + +* The `client/compatibility` thing added in 0.6.3 could be used from package.js + by passing the `raw` option to `add_files`; this is renamed to `bare` + +* Fix EPIPEs during dev mode hot code reload + +* Fix bug where we would never quiesce if we tried to revive subs that errored + out (5e7138d) + +* Implement "meteor bundle --debug" #748 + +bugs to describe: + #1151 (Meteor.disconnect etc) + #1106 + #1143 + #1191 + #1226 + #1181 + /sockjs/info cache buster (for Chrome bug) + +Patches contributed by GitHub users btipling, mizzao, timhaines and zol. + ## v0.6.4.1 diff --git a/docs/.meteor/release b/docs/.meteor/release index 2e41fbd3cc..71be59a840 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.6.5-rc2 +galaxy-6 diff --git a/docs/client/api.html b/docs/client/api.html index 618d2f3a7a..7e83680985 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2348,6 +2348,10 @@ Matches any value. Matches a primitive of the given type. {{/dtdd}} +{{#dtdd "Match.Integer"}} +Matches a signed 32-bit integer. Doesn't match `Infinity`, `-Infinity`, or `NaN`. +{{/dtdd}} + {{#dtdd "[pattern]"}} A one-element array matches an array of elements, each of which match *pattern*. For example, `[Number]` matches a (possibly empty) array of numbers; @@ -2371,8 +2375,19 @@ Matches any plain Object with any keys; equivalent to `Match.ObjectIncluding({})`. {{/dtdd}} -{{#dtdd "Match.Optional(pattern)"}} -Matches either `undefined` or something that matches *pattern*. +{{#dtdd "Match.Optional(pattern)"}} Matches either +`undefined` or something that matches pattern. If used in an object this matches +only if the key is not set as opposed to the value being set to `undefined`. + + // In an object + var pat = { name: Match.Optional(String) }; + check({ name: "something" }, pat) // OK + check({}, pat) // OK + check({ name: undefined }, pat) // Throws an exception + + // Outside an object + check(undefined, Match.Optional(String)); // OK + {{/dtdd}} {{#dtdd "Match.OneOf(pattern1, pattern2, ...)"}} diff --git a/packages/check/match.js b/packages/check/match.js index 1e6461fa7d..60783c6c97 100644 --- a/packages/check/match.js +++ b/packages/check/match.js @@ -1,5 +1,4 @@ // XXX docs -// XXX on linker branch, export Match and check // Things we explicitly do NOT support: // - heterogenous arrays @@ -11,7 +10,13 @@ check = function (value, pattern) { var argChecker = currentArgumentChecker.get(); if (argChecker) argChecker.checking(value); - checkSubtree(value, pattern); + try { + checkSubtree(value, pattern); + } catch (err) { + if ((err instanceof Match.Error) && err.path) + err.message += " in field " + err.path; + throw err; + } }; Match = { @@ -28,11 +33,17 @@ Match = { ObjectIncluding: function (pattern) { return new ObjectIncluding(pattern); }, + // Matches only signed 32-bit integers + Integer: ['__integer__'], - // XXX should we record the path down the tree in the error message? // XXX matchers should know how to describe themselves for errors Error: Meteor.makeErrorType("Match.Error", function (msg) { this.message = "Match error: " + msg; + // The path of the value that failed to match. Initially empty, this gets + // populated by catching and rethrowing the exception as it goes back up the + // stack. + // E.g.: "vals[3].entity.created" + this.path = ""; // If this gets sent over DDP, don't give full internal details but at least // provide something better than 500 Internal server error. this.sanitizedError = new Meteor.Error(400, "Match failed"); @@ -119,6 +130,20 @@ var checkSubtree = function (value, pattern) { throw new Match.Error("Expected null, got " + EJSON.stringify(value)); } + // Match.Integer is special type encoded with array + if (pattern === Match.Integer) { + // There is no consistent and reliable way to check if variable is a 64-bit + // integer. One of the popular solutions is to get reminder of division by 1 + // but this method fails on really large floats with big precision. + // E.g.: 1.348192308491824e+23 % 1 === 0 in V8 + // Bitwise operators work consistantly but always cast variable to 32-bit + // signed integer according to JavaScript specs. + if (typeof value === "number" && (value | 0) === value) + return + throw new Match.Error("Expected Integer, got " + + (value instanceof Object ? EJSON.stringify(value) : value)); + } + // "Object" is shorthand for Match.ObjectIncluding({}); if (pattern === Object) pattern = Match.ObjectIncluding({}); @@ -132,8 +157,15 @@ var checkSubtree = function (value, pattern) { throw new Match.Error("Expected array, got " + EJSON.stringify(value)); } - _.each(value, function (valueElement) { - checkSubtree(valueElement, pattern[0]); + _.each(value, function (valueElement, index) { + try { + checkSubtree(valueElement, pattern[0]); + } catch (err) { + if (err instanceof Match.Error) { + err.path = _prependPath(index, err.path); + } + throw err; + } }); return; } @@ -206,14 +238,20 @@ var checkSubtree = function (value, pattern) { }); _.each(value, function (subValue, key) { - if (_.has(requiredPatterns, key)) { - checkSubtree(subValue, requiredPatterns[key]); - delete requiredPatterns[key]; - } else if (_.has(optionalPatterns, key)) { - checkSubtree(subValue, optionalPatterns[key]); - } else { - if (!unknownKeysAllowed) - throw new Match.Error("Unknown key '" + key + "'"); + try { + if (_.has(requiredPatterns, key)) { + checkSubtree(subValue, requiredPatterns[key]); + delete requiredPatterns[key]; + } else if (_.has(optionalPatterns, key)) { + checkSubtree(subValue, optionalPatterns[key]); + } else { + if (!unknownKeysAllowed) + throw new Match.Error("Unknown key"); + } + } catch (err) { + if (err instanceof Match.Error) + err.path = _prependPath(key, err.path); + throw err; } }); @@ -266,3 +304,25 @@ _.extend(ArgumentChecker.prototype, { self.description); } }); + +var _jsKeywords = ["do", "if", "in", "for", "let", "new", "try", "var", "case", + "else", "enum", "eval", "false", "null", "this", "true", "void", "with", + "break", "catch", "class", "const", "super", "throw", "while", "yield", + "delete", "export", "import", "public", "return", "static", "switch", + "typeof", "default", "extends", "finally", "package", "private", "continue", + "debugger", "function", "arguments", "interface", "protected", "implements", + "instanceof"]; + +// Assumes the base of path is already escaped properly +// returns key + base +var _prependPath = function (key, base) { + if ((typeof key) === "number" || key.match(/^[0-9]+$/)) + key = "[" + key + "]"; + else if (!key.match(/^[a-z_$][0-9a-z_$]*$/i) || _.contains(_jsKeywords, key)) + key = JSON.stringify([key]); + + if (base && base[0] !== "[") + return key + '.' + base; + return key + base; +}; + diff --git a/packages/check/match_test.js b/packages/check/match_test.js index 1253167883..2707e4aebd 100644 --- a/packages/check/match_test.js +++ b/packages/check/match_test.js @@ -122,6 +122,24 @@ Tinytest.add("check - check", function (test) { x: Number, k: Match.OneOf(null, Boolean)})]}); + + // Match.Integer + matches(-1, Match.Integer); + matches(0, Match.Integer); + matches(1, Match.Integer); + matches(-2147483648, Match.Integer); // INT_MIN + matches(2147483647, Match.Integer); // INT_MAX + fails(123.33, Match.Integer); + fails(.33, Match.Integer); + fails(1.348192308491824e+23, Match.Integer); + fails(NaN, Match.Integer); + fails(Infinity, Match.Integer); + fails(-Infinity, Match.Integer); + fails({}, Match.Integer); + fails([], Match.Integer); + fails(function () {}, Match.Integer); + fails(new Date, Match.Integer); + // Test that "arguments" is treated like an array. var argumentsMatches = function () { matches(arguments, [Number]); @@ -199,3 +217,43 @@ Tinytest.add("check - argument checker", function (test) { check(x, Boolean); }, true, true); }); + +Tinytest.add("check - Match error path", function (test) { + var match = function (value, pattern, expectedPath) { + try { + check(value, pattern); + } catch (err) { + // XXX just for FF 3.6, its JSON stringification prefers "\u000a" to "\n" + err.path = err.path.replace(/\\u000a/, "\\n"); + if (err.path != expectedPath) + test.fail({ + type: "match-error-path", + message: "The path of Match.Error doesn't match.", + pattern: JSON.stringify(pattern), + value: JSON.stringify(value), + path: err.path, + expectedPath: expectedPath + }); + } + }; + + match({ foo: [ { bar: 3 }, {bar: "something"} ] }, { foo: [ { bar: Number } ] }, "foo[1].bar"); + // Complicated case with arrays, $, whitespace and quotes! + match([{ $FoO: { "bar baz\n\"'": 3 } }], [{ $FoO: { "bar baz\n\"'": String } }], "[0].$FoO[\"bar baz\\n\\\"'\"]"); + // Numbers only, can be accessed w/o quotes + match({ "1231": 123 }, { "1231": String }, "[1231]"); + match({ "1234abcd": 123 }, { "1234abcd": String }, "[\"1234abcd\"]"); + match({ $set: { people: "nice" } }, { $set: { people: [String] } }, "$set.people"); + match({ _underscore: "should work" }, { _underscore: Number }, "_underscore"); + // Nested array looks nice + match([[["something", "here"], []], [["string", 123]]], [[[String]]], "[1][0][1]"); + // Object nested in arrays should look nice, too! + match([[[{ foo: "something" }, { foo: "here"}], + [{ foo: "asdf" }]], + [[{ foo: 123 }]]], + [[[{ foo: String }]]], "[1][0][0].foo"); + + // JS keyword + match({ "return": 0 }, { "return": String }, "[\"return\"]"); +}); + diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index dd955a5754..23ad68999b 100644 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -49,7 +49,7 @@ Ctl.Commands.push({ if (appConfig.admin) { bindPathPrefix = "/" + Ctl.myAppName(); proxyConfig = { - securePort: null, + securePort: 44333, insecurePort: 9414, bindHost: "localhost", bindPathPrefix: bindPathPrefix @@ -73,7 +73,8 @@ Ctl.Commands.push({ "email": { url: appConfig.MAIL_URL } - } + }, + proxyServiceName: appConfig.proxyServiceName || "proxy" }; // Merge in any values that might have been added to the app's config in @@ -94,7 +95,8 @@ Ctl.Commands.push({ bindEnv: "PORT", routeEnv: "ROUTE" } - } + }, + tags: ["runner"] }]); console.log("Started a server."); } else { diff --git a/packages/facebook/facebook_server.js b/packages/facebook/facebook_server.js index 088f7eaf85..f9f7c1d696 100644 --- a/packages/facebook/facebook_server.js +++ b/packages/facebook/facebook_server.js @@ -59,7 +59,8 @@ var getTokenResponse = function (query) { } }).content; } catch (err) { - throw new Error("Failed to complete OAuth handshake with Facebook. " + err.message); + throw _.extend(new Error("Failed to complete OAuth handshake with Facebook. " + err.message), + {response: err.response}); } // If 'responseContent' parses as JSON, it is an error. @@ -89,7 +90,8 @@ var getIdentity = function (accessToken) { return HTTP.get("https://graph.facebook.com/me", { params: {access_token: accessToken}}).data; } catch (err) { - throw new Error("Failed to fetch identity from Facebook. " + err.message); + throw _.extend(new Error("Failed to fetch identity from Facebook. " + err.message), + {response: err.response}); } }; diff --git a/packages/github/github_server.js b/packages/github/github_server.js index 669b51dd42..78e342f421 100644 --- a/packages/github/github_server.js +++ b/packages/github/github_server.js @@ -43,7 +43,8 @@ var getAccessToken = function (query) { } }); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Github. " + err.message); + throw _.extend(new Error("Failed to complete OAuth handshake with Github. " + err.message), + {response: err.response}); } if (response.data.error) { // if the http response was a json object with an error attribute throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error); @@ -60,7 +61,8 @@ var getIdentity = function (accessToken) { params: {access_token: accessToken} }).data; } catch (err) { - throw new Error("Failed to fetch identity from GitHub. " + err.message); + throw _.extend(new Error("Failed to fetch identity from Github. " + err.message), + {response: err.response}); } }; diff --git a/packages/google/google_server.js b/packages/google/google_server.js index eba4263e5b..27c98321bb 100644 --- a/packages/google/google_server.js +++ b/packages/google/google_server.js @@ -51,7 +51,8 @@ var getTokens = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Google. " + err.message); + throw _.extend(new Error("Failed to complete OAuth handshake with Google. " + err.message), + {response: err.response}); } if (response.data.error) { // if the http response was a json object with an error attribute @@ -71,7 +72,8 @@ var getIdentity = function (accessToken) { "https://www.googleapis.com/oauth2/v1/userinfo", {params: {access_token: accessToken}}).data; } catch (err) { - throw new Error("Failed to fetch identity from Google. " + err.message); + throw _.extend(new Error("Failed to fetch identity from Google. " + err.message), + {response: err.response}); } }; diff --git a/packages/json/json_native.js b/packages/json/json_native.js new file mode 100644 index 0000000000..bbb4e00f84 --- /dev/null +++ b/packages/json/json_native.js @@ -0,0 +1,3 @@ +// Do we already have a global JSON object? Export it as our JSON object. +if (window.JSON) + JSON = window.JSON; diff --git a/packages/json/package.js b/packages/json/package.js index 87602a5998..abb6a2ec71 100644 --- a/packages/json/package.js +++ b/packages/json/package.js @@ -3,10 +3,12 @@ Package.describe({ internal: true }); -// We need to figure out how to serve this file only to browsers that -// don't have JSON.stringify (eg, IE7 and earlier -- or is that IE8?) +// We need to figure out how to serve this file only to browsers that don't have +// JSON.stringify (eg, IE7 and earlier, and IE8 outside of "standards mode") Package.on_use(function (api) { // Node always has JSON; we only need this in some browsers. + api.export('JSON', 'client'); + api.add_files('json_native.js', 'client'); api.add_files('json2.js', 'client'); }); diff --git a/packages/less/plugin/compile-less.js b/packages/less/plugin/compile-less.js index cee7fc5e7e..41980738a8 100644 --- a/packages/less/plugin/compile-less.js +++ b/packages/less/plugin/compile-less.js @@ -1,8 +1,18 @@ var fs = Npm.require('fs'); var path = Npm.require('path'); var less = Npm.require('less'); +var Future = Npm.require('fibers/future'); Plugin.registerSourceHandler("less", function (compileStep) { + // XXX annoying that this is replicated in .css, .less, and .styl + if (! compileStep.archMatches('browser')) { + // XXX in the future, might be better to emit some kind of a + // warning if a stylesheet is included on the server, rather than + // silently ignoring it. but that would mean you can't stick .css + // at the top level of your app, which is kind of silly. + return; + } + var source = compileStep.read().toString('utf8'); var options = { // Use fs.readFileSync to process @imports. This is the bundler, so @@ -13,24 +23,28 @@ Plugin.registerSourceHandler("less", function (compileStep) { paths: [path.dirname(compileStep._fullInputPath)] // for @import }; + var f = new Future; + var css; try { - less.render(source, options, function (err, css) { - if (err) { - // XXX better error handling, once the Plugin interface support it - throw new Error(err.message); - } - - compileStep.addStylesheet({ - path: compileStep.inputPath + ".css", - data: css - }); - }); + less.render(source, options, f.resolver()); + css = f.wait(); } catch (e) { // less.render() is supposed to report any errors via its // callback. But sometimes, it throws them instead. This is // probably a bug in less. Be prepared for either behavior. - throw new Error(source_path + ": Less compiler error: " + e.message); + compileStep.error({ + message: "Less compiler error: " + e.message, + sourcePath: e.filename || compileStep.inputPath, + line: e.line - 1, // dunno why, but it matches + column: e.column + 1 + }); + return; } + + compileStep.addStylesheet({ + path: compileStep.inputPath + ".css", + data: css + }); });; // Register lessimport files with the dependency watcher, without actually diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 72d56d65cb..119a78792d 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1287,18 +1287,17 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with var getSelfConnectionUrl = function () { if (Meteor.isClient) { - return "/"; + return Meteor._relativeToSiteRootUrl("/"); } else { return Meteor.absoluteUrl(); } }; if (Meteor.isServer) { - var reversed = {}; Meteor.methods({ reverse: function (arg) { - reversed[arg] = true; - return arg.split("").reverse().join(""); + // Return something notably different from reverse.meteor.com. + return arg.split("").reverse().join("") + " LOCAL"; } }); } @@ -1325,22 +1324,10 @@ testAsyncMulti("livedata connection - reconnect to a different server", [ if (self.doTest) { self.conn.reconnect({url: getSelfConnectionUrl()}); self.conn.call("reverse", "bar", expect(function (err, res) { - test.equal(res, "rab"); - })); - } - }, - function (test, expect) { - var self = this; - var id = Random.id(); - if (self.doTest) { - self.conn.call("reverse", id, expect(function (err, res) { - if (Meteor.isServer) { - test.isTrue(reversed[id]); - } + test.equal(res, "rab LOCAL"); })); } } - ]); Tinytest.addAsync("livedata connection - version negotiation requires renegotiating", diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index 7ea0f656e8..3fd1cd0c87 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -600,6 +600,8 @@ if (Meteor.isClient) { ]); } +var selfUrl = Meteor.isServer + ? Meteor.absoluteUrl() : Meteor._relativeToSiteRootUrl('/'); if (Meteor.isServer) { Meteor.methods({ @@ -613,7 +615,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - connect works from both client and server", [ function (test, expect) { var self = this; - self.conn = DDP.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(selfUrl); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); @@ -637,7 +639,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - method call on server blocks in a fiber way", [ function (test, expect) { var self = this; - self.conn = DDP.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(selfUrl); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); diff --git a/packages/livedata/sockjs-0.3.4.js b/packages/livedata/sockjs-0.3.4.js index a19b0d29d1..a3a2f687eb 100644 --- a/packages/livedata/sockjs-0.3.4.js +++ b/packages/livedata/sockjs-0.3.4.js @@ -23,9 +23,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +// Commented out JSO implementation (use json package instead). // JSON2 by Douglas Crockford (minified). -var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c // [*] Including lib/index.js // Public object diff --git a/packages/livedata/stream_client_sockjs.js b/packages/livedata/stream_client_sockjs.js index d8c4bfb810..8605e6275c 100644 --- a/packages/livedata/stream_client_sockjs.js +++ b/packages/livedata/stream_client_sockjs.js @@ -101,7 +101,7 @@ _.extend(LivedataTest.ClientStream.prototype, { self._clearConnectionAndHeartbeatTimers(); if (self.socket) { self.socket.onmessage = self.socket.onclose - = self.socket.onerror = function () {}; + = self.socket.onerror = self.socket.onheartbeat = function () {}; self.socket.close(); self.socket = null; } diff --git a/packages/logging/logging.js b/packages/logging/logging.js index e33361c11d..7c742f31e4 100644 --- a/packages/logging/logging.js +++ b/packages/logging/logging.js @@ -247,7 +247,7 @@ Log.format = function (obj, options) { '-', timeStamp, utcOffsetStr, - timeInexact ? '?' : ' ', + timeInexact ? '? ' : ' ', appInfo, sourceInfo, stderrIndicator].join(''); diff --git a/packages/logging/logging_test.js b/packages/logging/logging_test.js index 4305abf7e0..4e138b3ef1 100644 --- a/packages/logging/logging_test.js +++ b/packages/logging/logging_test.js @@ -51,7 +51,7 @@ Tinytest.add("logging - log", function (test) { [0, "0", "falsy - 0"], [null, "null", "falsy - null"], [undefined, "undefined", "falsy - undefined"], - ["2013-06-13T01:15:16.000Z", new Date("2013-06-13T01:15:16.000Z"), "date"], + [new Date("2013-06-13T01:15:16.000Z"), new Date("2013-06-13T01:15:16.000Z"), "date"], [/[^regexp]{0,1}/g, "/[^regexp]{0,1}/g", "regexp"], [true, "true", "boolean - true"], [false, "false", "boolean - false"], @@ -72,7 +72,13 @@ Tinytest.add("logging - log", function (test) { var recieved = intercepted[index]; var obj = EJSON.parse(recieved); - if (_.isDate(expected)) + // IE8 and old Safari don't support this date format. Skip it. + if (expected && expected.toString && + (expected.toString() === "NaN" || + expected.toString() === "Invalid Date")) + return; + + if (_.isDate(testcase[0])) obj.message = new Date(obj.message); test.equal(obj.message, expected, 'Logging ' + testName); }); @@ -137,7 +143,7 @@ Tinytest.add("logging - format", function (test) { test.equal( Log.format({message: "message", time: time, timeInexact: true, level: level}), - level.charAt(0).toUpperCase() + "20120908-07:06:05.004" + utcOffsetStr + "?message"); + level.charAt(0).toUpperCase() + "20120908-07:06:05.004" + utcOffsetStr + "? message"); test.equal( Log.format({foo1: "bar1", foo2: "bar2", time: time, level: level}), diff --git a/packages/meetup/meetup_server.js b/packages/meetup/meetup_server.js index cdd2b49a71..fe4aa80e8e 100644 --- a/packages/meetup/meetup_server.js +++ b/packages/meetup/meetup_server.js @@ -31,7 +31,8 @@ var getAccessToken = function (query) { state: query.state }}); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Meetup. " + err.message); + throw _.extend(new Error("Failed to complete OAuth handshake with Meetup. " + err.message), + {response: err.response}); } if (response.data.error) { // if the http response was a json object with an error attribute @@ -48,7 +49,8 @@ var getIdentity = function (accessToken) { {params: {member_id: 'self', access_token: accessToken}}); return response.data.results && response.data.results[0]; } catch (err) { - throw new Error("Failed to fetch identity from Meetup: " + err.message); + throw _.extend(new Error("Failed to fetch identity from Meetup. " + err.message), + {response: err.response}); } }; diff --git a/packages/meteor/plugin/basic-file-types.js b/packages/meteor/plugin/basic-file-types.js index 14141699a3..3c87328198 100644 --- a/packages/meteor/plugin/basic-file-types.js +++ b/packages/meteor/plugin/basic-file-types.js @@ -3,11 +3,12 @@ source file. */ Plugin.registerSourceHandler("css", function (compileStep) { - // XXX use archinfo rather than rolling our own - if (! compileStep.arch.match(/^browser(\.|$)/)) { + // XXX annoying that this is replicated in .css, .less, and .styl + if (! compileStep.archMatches('browser')) { // XXX in the future, might be better to emit some kind of a // warning if a stylesheet is included on the server, rather than - // silently ignoring it + // silently ignoring it. but that would mean you can't stick .css + // at the top level of your app, which is kind of silly. return; } diff --git a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json index a8aafc87d9..ea5601e289 100644 --- a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json @@ -1,10 +1,10 @@ { "dependencies": { "mongodb": { - "version": "1.3.12", + "from": "https://github.com/mongodb/node-mongodb-native/tarball/02a5723aa51e9fdbad743a1117655e535880b3db", "dependencies": { "bson": { - "version": "0.2.1" + "version": "0.2.2" }, "kerberos": { "version": "0.0.3" diff --git a/packages/mongo-livedata/package.js b/packages/mongo-livedata/package.js index adc83c3862..00ae205b58 100644 --- a/packages/mongo-livedata/package.js +++ b/packages/mongo-livedata/package.js @@ -12,7 +12,10 @@ Package.describe({ internal: true }); -Npm.depends({mongodb: "1.3.12"}); +// Use soon-to-be-released mongo driver version 1.3.16. It includes a +// fix for a connection storm that impacted production hosting. +// Change this to 1.3.16 once NPM is updated. +Npm.depends({mongodb: "https://github.com/mongodb/node-mongodb-native/tarball/02a5723aa51e9fdbad743a1117655e535880b3db"}); Package.on_use(function (api) { api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging', diff --git a/packages/oauth1/oauth1_binding.js b/packages/oauth1/oauth1_binding.js index b3455e26a9..8047f387c6 100644 --- a/packages/oauth1/oauth1_binding.js +++ b/packages/oauth1/oauth1_binding.js @@ -123,7 +123,8 @@ OAuth1Binding.prototype._call = function(method, url, headers, params, callback) } }, callback); } catch (err) { - throw new Error("Failed to send OAuth1 request to " + url + ". " + err.message); + throw _.extend(new Error("Failed to send OAuth1 request to " + url + ". " + err.message), + {response: err.response}); } }; diff --git a/packages/stylus/package.js b/packages/stylus/package.js index 95e74bac9f..850d7cd49a 100644 --- a/packages/stylus/package.js +++ b/packages/stylus/package.js @@ -12,7 +12,7 @@ Package._transitional_registerBuildPlugin({ }); Package.on_test(function (api) { - api.use(['tinytest', 'stylus', 'test-helpers']) + api.use(['tinytest', 'stylus', 'test-helpers']); api.use('spark'); api.add_files(['stylus_tests.styl', 'stylus_tests.js'], 'client'); }); diff --git a/packages/stylus/plugin/compile-stylus.js b/packages/stylus/plugin/compile-stylus.js index 74b786d81d..7ddc33dcf5 100644 --- a/packages/stylus/plugin/compile-stylus.js +++ b/packages/stylus/plugin/compile-stylus.js @@ -1,21 +1,34 @@ var fs = Npm.require('fs'); var stylus = Npm.require('stylus'); var nib = Npm.require('nib'); +var Future = Npm.require('fibers/future'); Plugin.registerSourceHandler("styl", function (compileStep) { + // XXX annoying that this is replicated in .css, .less, and .styl + if (! compileStep.archMatches('browser')) { + // XXX in the future, might be better to emit some kind of a + // warning if a stylesheet is included on the server, rather than + // silently ignoring it. but that would mean you can't stick .css + // at the top level of your app, which is kind of silly. + return; + } + + var f = new Future; stylus(compileStep.read().toString('utf8')) .use(nib()) .set('filename', compileStep.inputPath) - .render(function(err, output) { - if (err) { - // XXX better error handling, once the Plugin interface support it - throw new Error('Stylus compiler error: ' + err.message); - } + .render(f.resolver()); - compileStep.addStylesheet({ - path: compileStep.inputPath + ".css", - data: output - }); + try { + var css = f.wait(); + } catch (e) { + compileStep.error({ + message: "Stylus compiler error: " + e.message }); + return; } -); + compileStep.addStylesheet({ + path: compileStep.inputPath + ".css", + data: css + }); +}); diff --git a/packages/test-in-console/driver.js b/packages/test-in-console/driver.js index cbbd26b354..50e03156f3 100644 --- a/packages/test-in-console/driver.js +++ b/packages/test-in-console/driver.js @@ -21,7 +21,6 @@ var log = function (/*arguments*/) { }; -var finished = 0; var passed = 0; var failed = 0; var expected = 0; @@ -96,6 +95,14 @@ Meteor.startup(function () { if (resultSet[name].status !== "FAIL") resultSet[name].status = "EXPECTED"; break; + case "exception": + log(name, ":", "!!!!!!!!! FAIL !!!!!!!!!!!"); + if (event.details && event.details.stack) + log(event.details.stack); + else + log("Test failed with exception"); + failed++; + break; case "finish": switch (resultSet[name].status) { case "OK": @@ -120,7 +127,6 @@ Meteor.startup(function () { default: log(name, ": unknown state for the test to be in"); } - finished++; break; default: resultSet[name].status = "FAIL"; diff --git a/packages/universal-events/package.js b/packages/universal-events/package.js index 55585255e7..3ce9c2f1ea 100644 --- a/packages/universal-events/package.js +++ b/packages/universal-events/package.js @@ -4,7 +4,7 @@ Package.describe({ }); Package.on_use(function (api) { - api.use(['underscore'], 'client'); + api.use(['underscore', 'domutils'], 'client'); api.export('UniversalEventListener', 'client'); api.add_files(['listener.js', 'events-w3c.js', diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 651cd66f95..5f085dfb14 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -452,7 +452,8 @@ var runWebAppServer = function () { // bind via the proxy, but we'll have to find it ourselves via // ultraworld. var galaxy = findGalaxy(); - galaxy.subscribe('servicesByName', 'proxy'); + var proxyServiceName = deployConfig.proxyServiceName || "proxy"; + galaxy.subscribe('servicesByName', proxyServiceName); var Proxies = new Meteor.Collection('services', { manager: galaxy }); @@ -462,7 +463,7 @@ var runWebAppServer = function () { WebAppInternals.bindToProxy(_.extend({ proxyEndpoint: proxyService.providers.proxy }, bind.viaProxy)); - } + } }; Proxies.find().observe({ added: doBinding, @@ -520,6 +521,29 @@ WebAppInternals.bindToProxy = function (proxyConfig) { var route = process.env.ROUTE; var host = route.split(":")[0]; var port = +route.split(":")[1]; + + var completedBindings = { + ddp: false, + http: false, + https: proxyConfig.securePort !== null ? false : undefined + }; + + var bindingDoneCallback = function (binding) { + return function (err, resp) { + if (err) + throw err; + + completedBindings[binding] = true; + var completedAll = _.every(_.keys(completedBindings), function (binding) { + return (completedBindings[binding] || + completedBindings[binding] === undefined); + }); + if (completedAll) + Log("Bound to proxy."); + return completedAll; + }; + }; + proxy.call('bindDdp', { pid: pid, bindTo: ddpBindTo, @@ -528,7 +552,7 @@ WebAppInternals.bindToProxy = function (proxyConfig) { port: port, pathPrefix: bindPathPrefix + '/websocket' } - }); + }, bindingDoneCallback("ddp")); proxy.call('bindHttp', { pid: pid, bindTo: { @@ -541,7 +565,7 @@ WebAppInternals.bindToProxy = function (proxyConfig) { port: port, pathPrefix: bindPathPrefix } - }); + }, bindingDoneCallback("http")); if (proxyConfig.securePort !== null) { proxy.call('bindHttp', { pid: pid, @@ -556,9 +580,8 @@ WebAppInternals.bindToProxy = function (proxyConfig) { port: port, pathPrefix: bindPathPrefix } - }); + }, bindingDoneCallback("https")); } - Log("Bound to proxy"); }; runWebAppServer(); diff --git a/packages/weibo/weibo_server.js b/packages/weibo/weibo_server.js index c5e11cd475..1d2f525638 100644 --- a/packages/weibo/weibo_server.js +++ b/packages/weibo/weibo_server.js @@ -46,7 +46,8 @@ var getTokenResponse = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Weibo. " + err.message); + throw _.extend(new Error("Failed to complete OAuth handshake with Weibo. " + err.message), + {response: err.response}); } // result.headers["content-type"] is 'text/plain;charset=UTF-8', so @@ -66,7 +67,8 @@ var getIdentity = function (accessToken, userId) { "https://api.weibo.com/2/users/show.json", {params: {access_token: accessToken, uid: userId}}).data; } catch (err) { - throw new Error("Failed to fetch identity from Weibo. " + err.message); + throw _.extend(new Error("Failed to fetch identity from Weibo. " + err.message), + {response: err.response}); } }; diff --git a/tools/deploy-galaxy.js b/tools/deploy-galaxy.js index e147361712..20ac3dfd87 100644 --- a/tools/deploy-galaxy.js +++ b/tools/deploy-galaxy.js @@ -130,15 +130,18 @@ exports.deleteApp = function (app, context) { // so we can be careful to not rely on any of the app dir context when // in --star mode. exports.deploy = function (options) { - var galaxy = getGalaxy(options.context); - var Package = getPackage(options.context); - var tmpdir = files.mkdtemp('deploy'); var buildDir = path.join(tmpdir, 'build'); var topLevelDirName = path.basename(options.appDir); var bundlePath = path.join(buildDir, topLevelDirName); var bundler = require('./bundler.js'); var starball; + + // Don't try to connect to galaxy before the bundle is done. Because bundling + // doesn't yield, this will cause the connection to timeout. Eventually we'd + // like to have bundle yield, so that we can connect (and make sure auth + // works) before bundling. + if (!options.starball) { process.stdout.write('Deploying ' + options.app + '. Bundling...\n'); var bundleResult = bundler.bundle(options.appDir, bundlePath, @@ -166,6 +169,10 @@ exports.deploy = function (options) { } process.stdout.write('Uploading...\n'); + + var galaxy = getGalaxy(options.context); + var Package = getPackage(options.context); + var created = true; var appConfig = { METEOR_SETTINGS: options.settings diff --git a/tools/library.js b/tools/library.js index fc9c039c55..4238bce28b 100644 --- a/tools/library.js +++ b/tools/library.js @@ -110,9 +110,25 @@ _.extend(Library.prototype, { for (var i = 0; i < self.localPackageDirs.length; ++i) { var packageDir = path.join(self.localPackageDirs[i], name); - // XXX or unipackage.json? see also watchLocalPackageDirs - if (fs.existsSync(path.join(packageDir, 'package.js'))) + // A directory is a package if it either contains 'package.js' (a package + // source tree) or 'unipackage.json' (a compiled unipackage). (Actually, + // for now, unipackages contain a dummy package.js too.) + // + // XXX support for putting unipackages in a local package dir is + // incomplete! They will be properly loaded, but other packages that + // depend on them have no way of knowing when they change! unipackages + // that are the .build of a source tree work fine (they have a + // buildinfo.json and can be rebuilt), and warehouse unipackages work fine + // too (users are not supposed to edit them (they are read-only on disk), + // and their pathname specifies a version). But if you, eg, have a + // unipackage of coffeescript in a local package directory, build another + // package dependending on it, and substitute another version of the + // unipackage in the same location, nothing will ever rebuild your + // package! + if (fs.existsSync(path.join(packageDir, 'package.js')) || + fs.existsSync(path.join(packageDir, 'unipackage.json'))) { return packageDir; + } } // Try the Meteor distribution, if we have one. @@ -120,6 +136,8 @@ _.extend(Library.prototype, { if (version) { packageDir = path.join(warehouse.getWarehouseDir(), 'packages', name, version); + // The warehouse is theoretically constructed carefully enough that the + // directory really should not exist unless it is complete. if (! fs.existsSync(packageDir)) throw new Error("Package missing from warehouse: " + name + " version " + version); @@ -157,6 +175,20 @@ _.extend(Library.prototype, { return self.loadedPackages[name].pkg; } + // Check for invalid package names. Currently package names can only + // contain ASCII alphanumerics and dash, and must contain at least + // one non-digit-or-dash. + // + // We don't support '.' because it is used as the separator between + // a package name and a slice. This might want to change. + // + // XXX revisit this later. What about unicode package names? + if (/[^A-Za-z0-9\-]/.test(name) || !/[A-Za-z]/.test(name) ) { + if (throwOnError === false) + return null; + throw new Error("Invalid package name: " + name); + } + var packageDir = self.findPackageDirectory(name); if (! packageDir) { @@ -224,8 +256,15 @@ _.extend(Library.prototype, { if (! buildmessage.jobHasMessages() && // ensure no errors! pkg.canBeSavedAsUnipackage()) { // Save it, for a fast load next time - files.add_to_gitignore(packageDir, '.build*'); - pkg.saveAsUnipackage(buildDir, { buildOfPath: packageDir }); + try { + files.add_to_gitignore(packageDir, '.build*'); + pkg.saveAsUnipackage(buildDir, { buildOfPath: packageDir }); + } catch (e) { + // If we can't write to this directory, we don't get to cache our + // output, but otherwise life is good. + if (!(e && (e.code === 'EACCES' || e.code === 'EPERM'))) + throw e; + } } }); } @@ -272,7 +311,7 @@ _.extend(Library.prototype, { // Register local package directories with a watchSet. We want to know if a // package is created or deleted, which includes both its top-level source - // directory or its package.js file. + // directory and its main package metadata file. watchLocalPackageDirs: function (watchSet) { var self = this; _.each(self.localPackageDirs, function (packageDir) { @@ -283,7 +322,8 @@ _.extend(Library.prototype, { _.each(packages, function (p) { watch.readAndWatchFile(watchSet, path.join(packageDir, p, 'package.js')); - // XXX unipackage.json too? + watch.readAndWatchFile(watchSet, + path.join(packageDir, p, 'unipackage.json')); }); }); }, diff --git a/tools/meteor.js b/tools/meteor.js index df708d31df..290729f6b7 100644 --- a/tools/meteor.js +++ b/tools/meteor.js @@ -638,9 +638,10 @@ Fiber(function () { // Find upgraders (in order) necessary to upgrade the app for the new // release (new metadata file formats, etc, or maybe even updating renamed - // APIs). - var oldManifest = warehouse.ensureReleaseExistsAndReturnManifest( - appRelease); + // APIs). (If this is a pre-engine app with no .meteor/release file, run + // all upgraders.) + var oldManifest = appRelease === null ? {} + : warehouse.ensureReleaseExistsAndReturnManifest(appRelease); // We can only run upgrades from pinned apps. if (oldManifest) { var upgraders = _.difference(context.releaseManifest.upgraders || [], diff --git a/tools/packages.js b/tools/packages.js index 7ee8dc248a..707efade99 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -231,6 +231,12 @@ _.extend(Slice.prototype, { var handler = !fileOptions.isAsset && self._getSourceHandler(ext); var contents = watch.readAndWatchFile(self.watchSet, absPath); + if (contents === null) { + buildmessage.error("File not found: " + source.relPath); + // recover by ignoring + return; + } + if (! handler) { // If we don't have an extension handler, serve this file as a // static resource on the client, or ignore it on the server. @@ -359,6 +365,9 @@ _.extend(Slice.prototype, { packageName: self.pkg.name, rootOutputPath: self.pkg.serveRoot, arch: self.arch, + archMatches: function (pattern) { + return archinfo.matches(self.arch, pattern); + }, fileOptions: fileOptions, declaredExports: _.pluck(self.declaredExports, 'name'), read: function (n) { @@ -1792,6 +1801,28 @@ _.extend(Package.prototype, { var otherSliceRegExp = (sliceName === "server" ? /^client\/$/ : /^server\/$/); + // The paths that we've called checkForInfiniteRecursion on. + var seenPaths = {}; + // Used internally by fs.realpathSync as an optimization. + var realpathCache = {}; + var checkForInfiniteRecursion = function (relDir) { + var absPath = path.join(self.sourceRoot, relDir); + try { + var realpath = fs.realpathSync(absPath, realpathCache); + } catch (e) { + if (!e || e.code !== 'ELOOP') + throw e; + // else leave realpath undefined + } + if (realpath === undefined || _.has(seenPaths, realpath)) { + buildmessage.error("Symlink cycle detected at " + relDir); + // recover by returning no files + return true; + } + seenPaths[realpath] = true; + return false; + }; + // Read top-level subdirectories. Ignore subdirectories that have // special handling. var sourceDirectories = readAndWatchDirectory('', { @@ -1800,13 +1831,17 @@ _.extend(Package.prototype, { /^public\/$/, /^private\/$/, otherSliceRegExp].concat(sourceExclude) }); + checkForInfiniteRecursion(''); - // XXX avoid infinite recursion with bad symlinks while (!_.isEmpty(sourceDirectories)) { var dir = sourceDirectories.shift(); + // remove trailing slash dir = dir.substr(0, dir.length - 1); + if (checkForInfiniteRecursion(dir)) + return []; // pretend we found no files + // Find source files in this directory. Array.prototype.push.apply(sources, readAndWatchDirectory(dir, { include: sourceInclude, @@ -1845,7 +1880,6 @@ _.extend(Package.prototype, { include: [new RegExp('^' + assetDir + '/$')] }); - // XXX avoid infinite recursion with bad symlinks if (!_.isEmpty(assetDirs)) { if (!_.isEqual(assetDirs, [assetDir + '/'])) throw new Error("Surprising assetDirs: " + JSON.stringify(assetDirs)); @@ -1855,6 +1889,9 @@ _.extend(Package.prototype, { // remove trailing slash dir = dir.substr(0, dir.length - 1); + if (checkForInfiniteRecursion(dir)) + return []; // pretend we found no files + // Find asset files in this directory. var assetsAndSubdirs = readAndWatchDirectory(dir, { include: [/.?/], diff --git a/tools/run.js b/tools/run.js index 6982cb9f2a..832d68c226 100644 --- a/tools/run.js +++ b/tools/run.js @@ -561,9 +561,10 @@ exports.run = function (context, options) { Status.running = true; - var rootUrl = process.env.ROOT_URL || ('http://localhost:' + outerPort); + var rootUrl = process.env.ROOT_URL || + ('http://localhost:' + outerPort + '/'); if (firstRun) { - process.stdout.write("=> Meteor server running on: " + rootUrl + "/\n"); + process.stdout.write("=> Meteor server running on: " + rootUrl + "\n"); firstRun = false; lastThingThatPrintedWasRestartMessage = false; } else { diff --git a/tools/watch.js b/tools/watch.js index d4b05bdb37..ea685773cb 100644 --- a/tools/watch.js +++ b/tools/watch.js @@ -228,9 +228,12 @@ var readDirectory = function (options) { // XXX Does the treatment of symlinks make sense? var stats = fs.statSync(path.join(options.absPath, entry)); } catch (e) { - // Disappeared after the readdirSync (or a dangling symlink)? Eh, pretend - // it was never there in the first place. - return; + if (e && (e.code === 'ENOENT')) { + // Disappeared after the readdirSync (or a dangling symlink)? Eh, + // pretend it was never there in the first place. + return; + } + throw e; } // XXX if we're on windows, I guess it's possible for files to end with '/'. if (stats.isDirectory())