diff --git a/History.md b/History.md index 90b44b833d..2821ddcdfa 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,8 @@ ## vNEXT +* Fix Mongo selectors of the form: {$regex: /foo/}. + * Calling `findOne()` on the server no longer loads the full query result into memory. @@ -18,6 +20,18 @@ * Delete login tokens from server when user logs out. +* Renames (may require doc updates): + - `Meteor.default_connection` - `Meteor.connection` + - `Meteor.default_server` - `Meteor.server` + - `Meteor.connect` - `DDP.connect` + - `Meteor.http` - `HTTP` + + +## v0.6.4.1 + +* Update mongodb driver to use version 0.2.1 of the bson module. + + ## v0.6.4 * Separate OAuth flow logic from Accounts into separate packages. The diff --git a/docs/.meteor/packages b/docs/.meteor/packages index 90b56a07bc..f4302d6960 100644 --- a/docs/.meteor/packages +++ b/docs/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages jquery underscore showdown diff --git a/docs/.meteor/release b/docs/.meteor/release index d2b13eb644..2e41fbd3cc 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.6.4 +0.6.5-rc2 diff --git a/docs/client/api.html b/docs/client/api.html index f748f3bf32..618d2f3a7a 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -12,6 +12,15 @@ on the client, just on the server, or *Anywhere*. {{> api_box isClient}} {{> api_box isServer}} + +{{#note}} +`Meteor.isServer` can be used to limit where code runs, but it does not +prevent code from being sent to the client. Any sensitive code that you +don't want served to the client, such as code containing passwords or +authentication mechanisms, should be kept in the `server` directory. +{{/note}} + + {{> api_box startup}} On a server, the function will run as soon as the server process is @@ -413,8 +422,8 @@ the server. The return value is an object with the following fields: values are `connected` (the connection is up and running), `connecting` (disconnected and trying to open a new connection), `failed` (permanently failed to connect; e.g., the client - and server support different versions of DDP) and `waiting` (failed - to connect and waiting to try to reconnect). + and server support different versions of DDP), `waiting` (failed + to connect and waiting to try to reconnect) and `offline` (user has disconnected the connection). {{/dtdd}} {{#dtdd name="retryCount" type="Number"}} @@ -441,11 +450,24 @@ to get realtime updates. {{> api_box reconnect}} +{{> api_box disconnect}} + +Call this method to temporarily disconnect from the server and stop all +live data updates. While the client is disconnected it will not receive +updates to collections, method calls will be queued until the +connection is reestablished, and hot code push will be disabled. + +Call [Meteor.reconnect](#meteor_reconnect) to reestablish the connection +and resume data transfer. + +This can be used to save battery on mobile devices when real time +updates are not required. + {{> api_box connect}} To call methods on another Meteor application or subscribe to its data -sets, call `Meteor.connect` with the URL of the application. -`Meteor.connect` returns an object which provides: +sets, call `DDP.connect` with the URL of the application. +`DDP.connect` returns an object which provides: * `subscribe` - Subscribe to a record set. See @@ -463,6 +485,8 @@ sets, call `Meteor.connect` with the URL of the application. [Meteor.status](#meteor_status). * `reconnect` - See [Meteor.reconnect](#meteor_reconnect). +* `disconnect` - + See [Meteor.disconnect](#meteor_disconnect). * `onReconnect` - Set this to a function to be called as the first step of reconnecting. This function can call methods which will be executed before any other outstanding methods. For example, this can be used to re-establish @@ -473,11 +497,6 @@ When you call `Meteor.subscribe`, `Meteor.status`, `Meteor.call`, and `Meteor.apply`, you are using a connection back to that default server. -{{#warning}} -In this release, `Meteor.connect` can only be called on the client. -Servers can not yet connect to other servers. -{{/warning}} -

Collections

Meteor stores data in *collections*. To get started, declare a @@ -2795,9 +2814,9 @@ For example, the `toJSONValue` method for return this.toHexString(); }; -

Meteor.http

+

HTTP

-`Meteor.http` provides an HTTP API on the client and server. To use +`HTTP` provides an HTTP request API on the client and server. To use these functions, add the HTTP package to your project with `$ meteor add http`. @@ -2868,8 +2887,8 @@ Example server method: Meteor.methods({checkTwitter: function (userId) { check(userId, String); this.unblock(); - var result = Meteor.http.call("GET", "http://api.twitter.com/xyz", - {params: {user: userId}}); + var result = HTTP.call("GET", "http://api.twitter.com/xyz", + {params: {user: userId}}); if (result.statusCode === 200) return true return false; @@ -2877,13 +2896,13 @@ Example server method: Example asynchronous HTTP call: - Meteor.http.call("POST", "http://api.twitter.com/xyz", - {data: {some: "json", stuff: 1}}, - function (error, result) { - if (result.statusCode === 200) { - Session.set("twizzled", true); - } - }); + HTTP.call("POST", "http://api.twitter.com/xyz", + {data: {some: "json", stuff: 1}}, + function (error, result) { + if (result.statusCode === 200) { + Session.set("twizzled", true); + } + }); {{> api_box http_get}} diff --git a/docs/client/api.js b/docs/client/api.js index e14de34dab..39503a281e 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -447,9 +447,17 @@ Template.api.reconnect = { "This method does nothing if the client is already connected."] }; +Template.api.disconnect = { + id: "meteor_disconnect", + name: "Meteor.disconnect()", + locus: "Client", + descr: [ + "Disconnect the client from the server."] +}; + Template.api.connect = { id: "meteor_connect", - name: "Meteor.connect(url)", + name: "DDP.connect(url)", locus: "Client", descr: ["Connect to the server of a different Meteor application to subscribe to its document sets and invoke its remote methods."], args: [ @@ -1066,6 +1074,11 @@ Template.api.loginWithExternalService = { name: "requestOfflineToken", type: "Boolean", descr: "If true, asks the user for permission to act on their behalf when offline. This stores an additional offline token in the `services` field of the user document. Currently only supported with Google." + }, + { + name: "forceApprovalPrompt", + type: "Boolean", + descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google." } ] }; @@ -1572,7 +1585,7 @@ Template.api.equals = { Template.api.httpcall = { id: "meteor_http_call", - name: "Meteor.http.call(method, url [, options] [, asyncCallback])", + name: "HTTP.call(method, url [, options] [, asyncCallback])", locus: "Anywhere", descr: ["Perform an outbound HTTP request."], args: [ @@ -1619,30 +1632,30 @@ Template.api.httpcall = { Template.api.http_get = { id: "meteor_http_get", - name: "Meteor.http.get(url, [options], [asyncCallback])", + name: "HTTP.get(url, [options], [asyncCallback])", locus: "Anywhere", - descr: ["Send an HTTP GET request. Equivalent to `Meteor.http.call(\"GET\", ...)`."] + descr: ["Send an HTTP GET request. Equivalent to `HTTP.call(\"GET\", ...)`."] }; Template.api.http_post = { id: "meteor_http_post", - name: "Meteor.http.post(url, [options], [asyncCallback])", + name: "HTTP.post(url, [options], [asyncCallback])", locus: "Anywhere", - descr: ["Send an HTTP POST request. Equivalent to `Meteor.http.call(\"POST\", ...)`."] + descr: ["Send an HTTP POST request. Equivalent to `HTTP.call(\"POST\", ...)`."] }; Template.api.http_put = { id: "meteor_http_put", - name: "Meteor.http.put(url, [options], [asyncCallback])", + name: "HTTP.put(url, [options], [asyncCallback])", locus: "Anywhere", - descr: ["Send an HTTP PUT request. Equivalent to `Meteor.http.call(\"PUT\", ...)`."] + descr: ["Send an HTTP PUT request. Equivalent to `HTTP.call(\"PUT\", ...)`."] }; Template.api.http_del = { id: "meteor_http_del", - name: "Meteor.http.del(url, [options], [asyncCallback])", + name: "HTTP.del(url, [options], [asyncCallback])", locus: "Anywhere", - descr: ["Send an HTTP DELETE request. Equivalent to `Meteor.http.call(\"DELETE\", ...)`. (Named `del` to avoid conflict with JavaScript's `delete`.)"] + descr: ["Send an HTTP DELETE request. Equivalent to `HTTP.call(\"DELETE\", ...)`. (Named `del` to avoid conflict with JavaScript's `delete`.)"] }; diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 107ff6d416..2735fb5a91 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -684,12 +684,13 @@ To get started, run $ meteor bundle myapp.tgz -This command will generate a fully-contained Node.js application in -the form of a tarball. To run this application, you need to provide -Node.js 0.8 and a MongoDB server. You can then run the application by -invoking node, specifying the HTTP port for the application to listen -on, and the MongoDB endpoint. If you don't already have a MongoDB -server, we can recommend our friends at [MongoHQ](http://mongohq.com). +This command will generate a fully-contained Node.js application in the form of +a tarball. To run this application, you need to provide Node.js 0.8 and a +MongoDB server. (The current release of Meteor has been tested with Node +0.8.24.) You can then run the application by invoking node, specifying the HTTP +port for the application to listen on, and the MongoDB endpoint. If you don't +already have a MongoDB server, we can recommend our friends at +[MongoHQ](http://mongohq.com). $ PORT=3000 MONGO_URL=mongodb://localhost:27017/myapp node bundle/main.js @@ -704,7 +705,7 @@ have `npm` available, and run the following: $ cd bundle/server/node_modules $ rm -r fibers - $ npm install fibers@1.0.0 + $ npm install fibers@1.0.1 {{/warning}} {{/better_markdown}} diff --git a/docs/client/docs.js b/docs/client/docs.js index 60fe71184c..a04f6659a1 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -139,7 +139,8 @@ var toc = [ {name: "Server connections", id: "connections"}, [ "Meteor.status", "Meteor.reconnect", - "Meteor.connect" + "Meteor.disconnect", + "DDP.connect" ], {name: "Collections", id: "collections"}, [ @@ -305,12 +306,12 @@ var toc = [ ], - "Meteor.http", [ - "Meteor.http.call", - {name: "Meteor.http.get", id: "meteor_http_get"}, - {name: "Meteor.http.post", id: "meteor_http_post"}, - {name: "Meteor.http.put", id: "meteor_http_put"}, - {name: "Meteor.http.del", id: "meteor_http_del"} + "HTTP", [ + "HTTP.call", + {name: "HTTP.get", id: "meteor_http_get"}, + {name: "HTTP.post", id: "meteor_http_post"}, + {name: "HTTP.put", id: "meteor_http_put"}, + {name: "HTTP.del", id: "meteor_http_del"} ], "Email", [ "Email.send" diff --git a/examples/leaderboard/.meteor/packages b/examples/leaderboard/.meteor/packages index bd558a2ce0..a5eb137152 100644 --- a/examples/leaderboard/.meteor/packages +++ b/examples/leaderboard/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages autopublish insecure preserve-inputs diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index d2b13eb644..f43621550c 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.6.4 +0.6.4.1 diff --git a/examples/other/defer-in-inactive-tab/.meteor/packages b/examples/other/defer-in-inactive-tab/.meteor/packages index 1a791704ad..5d992a7f9a 100644 --- a/examples/other/defer-in-inactive-tab/.meteor/packages +++ b/examples/other/defer-in-inactive-tab/.meteor/packages @@ -3,3 +3,4 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages diff --git a/examples/other/login-demo/.meteor/packages b/examples/other/login-demo/.meteor/packages index 89caf7d837..b03b9ed442 100644 --- a/examples/other/login-demo/.meteor/packages +++ b/examples/other/login-demo/.meteor/packages @@ -5,3 +5,4 @@ preserve-inputs accounts-google +standard-app-packages diff --git a/examples/other/quiescence/.meteor/packages b/examples/other/quiescence/.meteor/packages index 19052c13e7..e50106e265 100644 --- a/examples/other/quiescence/.meteor/packages +++ b/examples/other/quiescence/.meteor/packages @@ -6,3 +6,4 @@ insecure preserve-inputs random +standard-app-packages diff --git a/examples/other/template-demo/.meteor/packages b/examples/other/template-demo/.meteor/packages index 12c5f051c0..0aed446952 100644 --- a/examples/other/template-demo/.meteor/packages +++ b/examples/other/template-demo/.meteor/packages @@ -4,3 +4,4 @@ # but you can also edit it by hand. autopublish +standard-app-packages diff --git a/examples/parties/.meteor/packages b/examples/parties/.meteor/packages index b2c4da929f..985d19f9fe 100644 --- a/examples/parties/.meteor/packages +++ b/examples/parties/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages preserve-inputs accounts-ui accounts-password diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index d2b13eb644..f43621550c 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.6.4 +0.6.4.1 diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages index abc9ef7fad..001db3bdc2 100644 --- a/examples/todos/.meteor/packages +++ b/examples/todos/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages underscore backbone spiderable diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index d2b13eb644..f43621550c 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.6.4 +0.6.4.1 diff --git a/examples/unfinished/accounts-ui-viewer/.meteor/packages b/examples/unfinished/accounts-ui-viewer/.meteor/packages index 634f653e55..426d145113 100644 --- a/examples/unfinished/accounts-ui-viewer/.meteor/packages +++ b/examples/unfinished/accounts-ui-viewer/.meteor/packages @@ -13,3 +13,4 @@ accounts-github accounts-password underscore accounts-facebook +standard-app-packages diff --git a/examples/unfinished/azrael/.meteor/packages b/examples/unfinished/azrael/.meteor/packages index b065bfb303..83900fa851 100644 --- a/examples/unfinished/azrael/.meteor/packages +++ b/examples/unfinished/azrael/.meteor/packages @@ -6,3 +6,4 @@ underscore jquery jquery-layout +standard-app-packages diff --git a/examples/unfinished/benchmark/.meteor/packages b/examples/unfinished/benchmark/.meteor/packages index 233dc66a33..fad7faf9d2 100644 --- a/examples/unfinished/benchmark/.meteor/packages +++ b/examples/unfinished/benchmark/.meteor/packages @@ -7,3 +7,4 @@ insecure preserve-inputs bootstrap random +standard-app-packages diff --git a/examples/unfinished/coffeeless/.meteor/packages b/examples/unfinished/coffeeless/.meteor/packages index 4e0a9f67fb..27463e07b2 100644 --- a/examples/unfinished/coffeeless/.meteor/packages +++ b/examples/unfinished/coffeeless/.meteor/packages @@ -5,3 +5,4 @@ less coffeescript +standard-app-packages diff --git a/examples/unfinished/controls/.meteor/packages b/examples/unfinished/controls/.meteor/packages index 12c5f051c0..0aed446952 100644 --- a/examples/unfinished/controls/.meteor/packages +++ b/examples/unfinished/controls/.meteor/packages @@ -4,3 +4,4 @@ # but you can also edit it by hand. autopublish +standard-app-packages diff --git a/examples/unfinished/jsparse-docs/.meteor/packages b/examples/unfinished/jsparse-docs/.meteor/packages index 12c5f051c0..0aed446952 100644 --- a/examples/unfinished/jsparse-docs/.meteor/packages +++ b/examples/unfinished/jsparse-docs/.meteor/packages @@ -4,3 +4,4 @@ # but you can also edit it by hand. autopublish +standard-app-packages diff --git a/examples/unfinished/jsparse-docs/jsparse-docs.js b/examples/unfinished/jsparse-docs/jsparse-docs.js index dc1aee019d..72b161df71 100644 --- a/examples/unfinished/jsparse-docs/jsparse-docs.js +++ b/examples/unfinished/jsparse-docs/jsparse-docs.js @@ -1,6 +1,6 @@ -if (Meteor.is_client) { +if (Meteor.isClient) { Template.page.nodespec = function (fn) { var parts = [fn()]; var replaceParts = function(regex, replacementFunc) { @@ -64,4 +64,4 @@ if (Meteor.is_client) { return new Handlebars.SafeString('
 
'); }; -} \ No newline at end of file +} diff --git a/examples/unfinished/leaderboard-remote/.meteor/packages b/examples/unfinished/leaderboard-remote/.meteor/packages index 12c5f051c0..0aed446952 100644 --- a/examples/unfinished/leaderboard-remote/.meteor/packages +++ b/examples/unfinished/leaderboard-remote/.meteor/packages @@ -4,3 +4,4 @@ # but you can also edit it by hand. autopublish +standard-app-packages diff --git a/examples/unfinished/leaderboard-remote/client/leaderboard-remote.js b/examples/unfinished/leaderboard-remote/client/leaderboard-remote.js index 29825f1ae3..81c4ca9c96 100644 --- a/examples/unfinished/leaderboard-remote/client/leaderboard-remote.js +++ b/examples/unfinished/leaderboard-remote/client/leaderboard-remote.js @@ -1,4 +1,4 @@ -Leaderboard = Meteor.connect("http://leader2.meteor.com/sockjs"); +Leaderboard = DDP.connect("http://leader2.meteor.com/sockjs"); // XXX I'd rather this be Leaderboard.Players.. can this API be easier? Players = new Meteor.Collection("players", {manager: Leaderboard}); diff --git a/examples/unfinished/parse-inspector/.meteor/packages b/examples/unfinished/parse-inspector/.meteor/packages index 3ca84ae156..1d7becdbfe 100644 --- a/examples/unfinished/parse-inspector/.meteor/packages +++ b/examples/unfinished/parse-inspector/.meteor/packages @@ -6,3 +6,4 @@ autopublish preserve-inputs jsparse +standard-app-packages diff --git a/examples/unfinished/parse-inspector/parse-inspector.js b/examples/unfinished/parse-inspector/parse-inspector.js index ca79543d51..d33a108de1 100644 --- a/examples/unfinished/parse-inspector/parse-inspector.js +++ b/examples/unfinished/parse-inspector/parse-inspector.js @@ -1,6 +1,6 @@ -if (Meteor.is_client) { +if (Meteor.isClient) { Meteor.startup(function () { if (! Session.get("input")) Session.set("input", "var x = 3"); diff --git a/examples/unfinished/shark/.meteor/packages b/examples/unfinished/shark/.meteor/packages index 946f839ee6..fecf241fc1 100644 --- a/examples/unfinished/shark/.meteor/packages +++ b/examples/unfinished/shark/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages autopublish insecure preserve-inputs diff --git a/examples/unfinished/todos-backbone/.meteor/packages b/examples/unfinished/todos-backbone/.meteor/packages index a4ddbca3cd..c24acdbd36 100644 --- a/examples/unfinished/todos-backbone/.meteor/packages +++ b/examples/unfinished/todos-backbone/.meteor/packages @@ -4,4 +4,5 @@ # but you can also edit it by hand. jquery -backbone \ No newline at end of file +backbone +standard-app-packages diff --git a/examples/unfinished/todos-underscore/.meteor/packages b/examples/unfinished/todos-underscore/.meteor/packages index 23fded3598..6bbedad3c8 100644 --- a/examples/unfinished/todos-underscore/.meteor/packages +++ b/examples/unfinished/todos-underscore/.meteor/packages @@ -6,3 +6,4 @@ jquery jquery-layout jquery-history +standard-app-packages diff --git a/examples/wordplay/.meteor/packages b/examples/wordplay/.meteor/packages index 55c9ad0dd1..1c4346821c 100644 --- a/examples/wordplay/.meteor/packages +++ b/examples/wordplay/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages insecure jquery preserve-inputs diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index d2b13eb644..f43621550c 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.6.4 +0.6.4.1 diff --git a/meteor b/meteor index ca0159ce1c..d41941d01f 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.11 +BUNDLE_VERSION=0.3.13 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index b6841af28f..3dd0c5cd35 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -4,7 +4,7 @@ // This is reactive. Meteor.userId = function () { - return Meteor.default_connection.userId(); + return Meteor.connection.userId(); }; var loggingIn = false; @@ -45,7 +45,7 @@ Meteor.user = function () { // - Updating the Meteor.loggingIn() reactive data source // - Calling the method in 'wait' mode // - On success, saving the resume token to localStorage -// - On success, calling Meteor.default_connection.setUserId() +// - On success, calling Meteor.connection.setUserId() // - Setting up an onReconnect handler which logs in with // the resume token // @@ -57,6 +57,7 @@ Meteor.user = function () { // its error will be passed to the callback). // - userCallback: Will be called with no arguments once the user is fully // logged in, or with the error on error. +// Accounts.callLoginMethod = function (options) { options = _.extend({ methodName: 'login', @@ -78,19 +79,19 @@ Accounts.callLoginMethod = function (options) { // getting the results of subscription rerun, we WILL NOT re-send this // method (because we never re-send methods whose results we've received) // but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce" - // time. This will lead to _makeClientLoggedIn(result.id) even though we + // time. This will lead to makeClientLoggedIn(result.id) even though we // haven't actually sent a login method! // // But by making sure that we send this "resume" login in that case (and - // calling _makeClientLoggedOut if it fails), we'll end up with an accurate + // calling makeClientLoggedOut if it fails), we'll end up with an accurate // client-side userId. (It's important that livedata_connection guarantees // that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback // will occur before the callback from the resume login call.) var onResultReceived = function (err, result) { if (err || !result || !result.token) { - Meteor.default_connection.onReconnect = null; + Meteor.connection.onReconnect = null; } else { - Meteor.default_connection.onReconnect = function() { + Meteor.connection.onReconnect = function() { reconnected = true; Accounts.callLoginMethod({ methodArguments: [{resume: result.token}], @@ -100,7 +101,7 @@ Accounts.callLoginMethod = function (options) { _suppressLoggingIn: true, userCallback: function (error) { if (error) { - Accounts._makeClientLoggedOut(); + makeClientLoggedOut(); } options.userCallback(error); }}); @@ -138,7 +139,7 @@ Accounts.callLoginMethod = function (options) { } // Make the client logged in. (The user data should already be loaded!) - Accounts._makeClientLoggedIn(result.id, result.token); + makeClientLoggedIn(result.id, result.token); options.userCallback(); }; @@ -151,15 +152,15 @@ Accounts.callLoginMethod = function (options) { loggedInAndDataReadyCallback); }; -Accounts._makeClientLoggedOut = function() { - Accounts._unstoreLoginToken(); - Meteor.default_connection.setUserId(null); - Meteor.default_connection.onReconnect = null; +makeClientLoggedOut = function() { + unstoreLoginToken(); + Meteor.connection.setUserId(null); + Meteor.connection.onReconnect = null; }; -Accounts._makeClientLoggedIn = function(userId, token) { - Accounts._storeLoginToken(userId, token); - Meteor.default_connection.setUserId(userId); +makeClientLoggedIn = function(userId, token) { + storeLoginToken(userId, token); + Meteor.connection.setUserId(userId); }; Meteor.logout = function (callback) { @@ -167,7 +168,7 @@ Meteor.logout = function (callback) { if (error) { callback && callback(error); } else { - Accounts._makeClientLoggedOut(); + makeClientLoggedOut(); callback && callback(); } }); @@ -182,6 +183,7 @@ var loginServicesHandle = Meteor.subscribe("meteor.loginServiceConfiguration"); // A reactive function returning whether the loginServiceConfiguration // subscription is ready. Used by accounts-ui to hide the login button // until we have all the configuration loaded +// Accounts.loginServicesConfigured = function () { return loginServicesHandle.ready(); }; diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index f7a4ebd569..0db0271db9 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -1,10 +1,8 @@ -// @export Accounts -if (typeof Accounts === 'undefined') - Accounts = {}; +Accounts = {}; -if (!Accounts._options) { - Accounts._options = {}; -} +// Currently this is read directly by packages like accounts-password +// and accounts-ui-unstyled. +Accounts._options = {}; // Set up config for the accounts system. Call this on both the client // and the server. @@ -21,6 +19,7 @@ if (!Accounts._options) { // client signups. // - forbidClientAccountCreation {Boolean} // Do not allow clients to create accounts directly. +// Accounts.config = function(options) { // validate option keys var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation"]; @@ -45,6 +44,7 @@ Accounts.config = function(options) { // Users table. Don't use the normal autopublish, since we want to hide // some fields. Code to autopublish this is in accounts_server.js. // XXX Allow users to configure this collection name. +// Meteor.users = new Meteor.Collection("users", {_preventAutopublish: true}); // There is an allow call in accounts_server that restricts this // collection. diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 19df6a4de8..7ef3d8a4c0 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -12,7 +12,7 @@ Meteor.userId = function () { // user expects. The way to make this work in a publish is to do // Meteor.find(this.userId()).observe and recompute when the user // record changes. - var currentInvocation = Meteor._CurrentInvocation.get(); + var currentInvocation = DDP._CurrentInvocation.get(); if (!currentInvocation) throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions."); return currentInvocation.userId; @@ -36,20 +36,21 @@ Meteor.user = function () { // - `undefined`, meaning don't handle; // - {id: userId, token: *}, if the user logged in successfully. // - throw an error, if the user failed to log in. +// Accounts.registerLoginHandler = function(handler) { - Accounts._loginHandlers.push(handler); + loginHandlers.push(handler); }; // list of all registered handlers. -Accounts._loginHandlers = []; +loginHandlers = []; // Try all of the registered login handlers until one of them doesn' // return `undefined`, meaning it handled this call to `login`. Return // that return value, which ought to be a {id/token} pair. var tryAllLoginHandlers = function (options) { - for (var i = 0; i < Accounts._loginHandlers.length; ++i) { - var handler = Accounts._loginHandlers[i]; + for (var i = 0; i < loginHandlers.length; ++i) { + var handler = loginHandlers[i]; var result = handler(options); if (result !== undefined) return result; @@ -80,7 +81,7 @@ Meteor.methods({ logout: function() { if (this._sessionData.loginToken && this.userId) - Accounts._removeLoginToken(this.userId, this._sessionData.loginToken); + removeLoginToken(this.userId, this._sessionData.loginToken); this.setUserId(null); } }); @@ -109,11 +110,12 @@ Accounts.registerLoginHandler(function(options) { }); // Semi-public. Used by other login methods to generate tokens. +// Accounts._generateStampedLoginToken = function () { return {token: Random.id(), when: +(new Date)}; }; -Accounts._removeLoginToken = function (userId, loginToken) { +removeLoginToken = function (userId, loginToken) { Meteor.users.update(userId, { $pull: { "services.resume.loginTokens": { "token": loginToken } @@ -125,6 +127,7 @@ Accounts._removeLoginToken = function (userId, loginToken) { /// /// CREATE USER HOOKS /// + var onCreateUserHook = null; Accounts.onCreateUser = function (func) { if (onCreateUserHook) @@ -140,6 +143,8 @@ var defaultCreateUserHook = function (options, user) { user.profile = options.profile; return user; }; + +// Called by accounts-password Accounts.insertUserDoc = function (options, user) { // - clone user document, to protect from modification // - add createdAt timestamp @@ -223,6 +228,7 @@ Accounts.validateNewUser = function (func) { // (eg, profile) // @returns {Object} Object with token and id keys, like the result // of the "login" method. +// Accounts.updateOrCreateUserFromExternalService = function( serviceName, serviceData, options) { options = _.clone(options || {}); @@ -306,60 +312,64 @@ Meteor.publish(null, function() { // Accounts.addAutopublishFields Notably, this isn't implemented with // multiple publishes since DDP only merges only across top-level // fields, not subfields (such as 'services.facebook.accessToken') -Accounts._autopublishFields = { +var autopublishFields = { loggedInUser: ['profile', 'username', 'emails'], otherUsers: ['profile', 'username'] }; // Add to the list of fields or subfields to be automatically -// published if autopublish is on +// published if autopublish is on. Must be called from top-level +// code (ie, before Meteor.startup hooks run). // // @param opts {Object} with: // - forLoggedInUser {Array} Array of fields published to the logged-in user // - forOtherUsers {Array} Array of fields published to users that aren't logged in Accounts.addAutopublishFields = function(opts) { - Accounts._autopublishFields.loggedInUser.push.apply( - Accounts._autopublishFields.loggedInUser, opts.forLoggedInUser); - Accounts._autopublishFields.otherUsers.push.apply( - Accounts._autopublishFields.otherUsers, opts.forOtherUsers); + autopublishFields.loggedInUser.push.apply( + autopublishFields.loggedInUser, opts.forLoggedInUser); + autopublishFields.otherUsers.push.apply( + autopublishFields.otherUsers, opts.forOtherUsers); }; -Meteor.default_server.onAutopublish(function () { - // ['profile', 'username'] -> {profile: 1, username: 1} - var toFieldSelector = function(fields) { - return _.object(_.map(fields, function(field) { - return [field, 1]; - })); - }; +if (Package.autopublish) { + // Use Meteor.startup to give other packages a chance to call + // addAutopublishFields. + Meteor.startup(function () { + // ['profile', 'username'] -> {profile: 1, username: 1} + var toFieldSelector = function(fields) { + return _.object(_.map(fields, function(field) { + return [field, 1]; + })); + }; + + Meteor.server.publish(null, function () { + if (this.userId) { + return Meteor.users.find( + {_id: this.userId}, + {fields: toFieldSelector(autopublishFields.loggedInUser)}); + } else { + return null; + } + }, /*suppress autopublish warning*/{is_auto: true}); + + // XXX this publish is neither dedup-able nor is it optimized by our special + // treatment of queries on a specific _id. Therefore this will have O(n^2) + // run-time performance every time a user document is changed (eg someone + // logging in). If this is a problem, we can instead write a manual publish + // function which filters out fields based on 'this.userId'. + Meteor.server.publish(null, function () { + var selector; + if (this.userId) + selector = {_id: {$ne: this.userId}}; + else + selector = {}; - Meteor.default_server.publish(null, function () { - if (this.userId) { return Meteor.users.find( - {_id: this.userId}, - {fields: toFieldSelector(Accounts._autopublishFields.loggedInUser)}); - } else { - return null; - } - }, /*suppress autopublish warning*/{is_auto: true}); - - // XXX this publish is neither dedup-able nor is it optimized by our - // special treatment of queries on a specific _id. Therefore this - // will have O(n^2) run-time performance every time a user document - // is changed (eg someone logging in). If this is a problem, we can - // instead write a manual publish function which filters out fields - // based on 'this.userId'. - Meteor.default_server.publish(null, function () { - var selector; - if (this.userId) - selector = {_id: {$ne: this.userId}}; - else - selector = {}; - - return Meteor.users.find( - selector, - {fields: toFieldSelector(Accounts._autopublishFields.otherUsers)}); - }, /*suppress autopublish warning*/{is_auto: true}); -}); + selector, + {fields: toFieldSelector(autopublishFields.otherUsers)}); + }, /*suppress autopublish warning*/{is_auto: true}); + }); +} // Publish all login service configuration fields other than secret. Meteor.publish("meteor.loginServiceConfiguration", function () { @@ -374,8 +384,13 @@ Meteor.methods({ // Don't let random users configure a service we haven't added yet (so // that when we do later add it, it's set up with their configuration // instead of ours). - if (!Accounts[options.service]) + // XXX if service configuration is oauth-specific then this code should + // be in accounts-oauth; if it's not then the registry should be + // in this package + if (!(Accounts.oauth + && _.contains(Accounts.oauth.serviceNames(), options.service))) { throw new Meteor.Error(403, "Service unknown"); + } if (ServiceConfiguration.configurations.findOne({service: options.service})) throw new Meteor.Error(403, "Service " + options.service + " already configured"); ServiceConfiguration.configurations.insert(options); diff --git a/packages/accounts-base/localstorage_token.js b/packages/accounts-base/localstorage_token.js index 34ac30998e..91205a6742 100644 --- a/packages/accounts-base/localstorage_token.js +++ b/packages/accounts-base/localstorage_token.js @@ -3,6 +3,8 @@ // seconds to synchronize login state between multiple tabs in the same // browser. +var lastLoginTokenWhenPolled; + // Login with a Meteor access token. This is the only public function // here. Meteor.loginWithToken = function (token, callback) { @@ -14,8 +16,8 @@ Meteor.loginWithToken = function (token, callback) { // Semi-internal API. Call this function to re-enable auto login after // if it was disabled at startup. Accounts._enableAutoLogin = function () { - Accounts._preventAutoLogin = false; - Accounts._pollStoredLoginToken(); + autoLoginEnabled = true; + pollStoredLoginToken(); }; @@ -35,29 +37,32 @@ Accounts._isolateLoginTokenForTest = function () { userIdKey = userIdKey + Random.id(); }; -Accounts._storeLoginToken = function(userId, token) { +storeLoginToken = function(userId, token) { Meteor._localStorage.setItem(userIdKey, userId); Meteor._localStorage.setItem(loginTokenKey, token); // to ensure that the localstorage poller doesn't end up trying to // connect a second time - Accounts._lastLoginTokenWhenPolled = token; + lastLoginTokenWhenPolled = token; }; -Accounts._unstoreLoginToken = function() { +unstoreLoginToken = function() { Meteor._localStorage.removeItem(userIdKey); Meteor._localStorage.removeItem(loginTokenKey); // to ensure that the localstorage poller doesn't end up trying to // connect a second time - Accounts._lastLoginTokenWhenPolled = null; + lastLoginTokenWhenPolled = null; }; -Accounts._storedLoginToken = function() { +// This is private, but it is exported for now because it is used by a +// test in accounts-password. +// +var storedLoginToken = Accounts._storedLoginToken = function() { return Meteor._localStorage.getItem(loginTokenKey); }; -Accounts._storedUserId = function() { +var storedUserId = function() { return Meteor._localStorage.getItem(userIdKey); }; @@ -66,19 +71,19 @@ Accounts._storedUserId = function() { /// AUTO-LOGIN /// -if (!Accounts._preventAutoLogin) { +if (autoLoginEnabled) { // Immediately try to log in via local storage, so that any DDP // messages are sent after we have established our user account - var token = Accounts._storedLoginToken(); + var token = storedLoginToken(); if (token) { // On startup, optimistically present us as logged in while the // request is in flight. This reduces page flicker on startup. - var userId = Accounts._storedUserId(); - userId && Meteor.default_connection.setUserId(userId); + var userId = storedUserId(); + userId && Meteor.connection.setUserId(userId); Meteor.loginWithToken(token, function (err) { if (err) { Meteor._debug("Error logging in with token: " + err); - Accounts._makeClientLoggedOut(); + makeClientLoggedOut(); } }); } @@ -86,21 +91,21 @@ if (!Accounts._preventAutoLogin) { // Poll local storage every 3 seconds to login if someone logged in in // another tab -Accounts._lastLoginTokenWhenPolled = token; -Accounts._pollStoredLoginToken = function() { - if (Accounts._preventAutoLogin) +lastLoginTokenWhenPolled = token; +var pollStoredLoginToken = function() { + if (! autoLoginEnabled) return; - var currentLoginToken = Accounts._storedLoginToken(); + var currentLoginToken = storedLoginToken(); // != instead of !== just to make sure undefined and null are treated the same - if (Accounts._lastLoginTokenWhenPolled != currentLoginToken) { + if (lastLoginTokenWhenPolled != currentLoginToken) { if (currentLoginToken) Meteor.loginWithToken(currentLoginToken); // XXX should we pass a callback here? else Meteor.logout(); } - Accounts._lastLoginTokenWhenPolled = currentLoginToken; + lastLoginTokenWhenPolled = currentLoginToken; }; -setInterval(Accounts._pollStoredLoginToken, 3000); +setInterval(pollStoredLoginToken, 3000); diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index d751cd0537..70ec142040 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -5,12 +5,14 @@ Package.describe({ Package.on_use(function (api) { api.use('underscore', ['client', 'server']); api.use('localstorage', 'client'); - api.use('accounts-urls', ['client', 'server']); api.use('deps', 'client'); api.use('check', 'server'); api.use('random', ['client', 'server']); api.use('service-configuration', ['client', 'server']); + // needed for getting the currently logged-in user + api.use('livedata', ['client', 'server']); + // need this because of the Meteor.users collection but in the future // we'd probably want to abstract this away api.use('mongo-livedata', ['client', 'server']); @@ -19,12 +21,21 @@ Package.on_use(function (api) { // {{currentUser}}. If not, no biggie. api.use('handlebars', 'client', {weak: true}); + // Allow us to detect 'autopublish', and publish some Meteor.users fields if + // it's loaded. + api.use('autopublish', 'server', {weak: true}); + + api.export('Accounts'); + api.add_files('accounts_common.js', ['client', 'server']); api.add_files('accounts_server.js', 'server'); + api.add_files('url_client.js', 'client'); + api.add_files('url_server.js', 'server'); // accounts_client must be before localstorage_token, because // localstorage_token attempts to call functions in accounts_client (eg - // Accounts.callLoginMethod) on startup. + // Accounts.callLoginMethod) on startup. And localstorage_token must be after + // url_client, which sets autoLoginEnabled. api.add_files('accounts_client.js', 'client'); api.add_files('localstorage_token.js', 'client'); }); diff --git a/packages/accounts-urls/url_client.js b/packages/accounts-base/url_client.js similarity index 89% rename from packages/accounts-urls/url_client.js rename to packages/accounts-base/url_client.js index 0130250ce2..2aad97a986 100644 --- a/packages/accounts-urls/url_client.js +++ b/packages/accounts-base/url_client.js @@ -1,6 +1,4 @@ -// @export Accounts -if (typeof Accounts === 'undefined') - Accounts = {}; +autoLoginEnabled = true; // reads a reset password token from the url's hash fragment, if it's // there. if so prevent automatically logging in since it could be @@ -13,7 +11,7 @@ if (typeof Accounts === 'undefined') var match; match = window.location.hash.match(/^\#\/reset-password\/(.*)$/); if (match) { - Accounts._preventAutoLogin = true; + autoLoginEnabled = false; Accounts._resetPasswordToken = match[1]; window.location.hash = ''; } @@ -30,7 +28,7 @@ if (match) { // in line with the hash fragment approach) match = window.location.hash.match(/^\#\/verify-email\/(.*)$/); if (match) { - Accounts._preventAutoLogin = true; + autoLoginEnabled = false; Accounts._verifyEmailToken = match[1]; window.location.hash = ''; } @@ -40,7 +38,7 @@ if (match) { // reset password links. match = window.location.hash.match(/^\#\/enroll-account\/(.*)$/); if (match) { - Accounts._preventAutoLogin = true; + autoLoginEnabled = false; Accounts._enrollAccountToken = match[1]; window.location.hash = ''; } diff --git a/packages/accounts-urls/url_server.js b/packages/accounts-base/url_server.js similarity index 73% rename from packages/accounts-urls/url_server.js rename to packages/accounts-base/url_server.js index 6cb4e85fb2..2aac2cc4fc 100644 --- a/packages/accounts-urls/url_server.js +++ b/packages/accounts-base/url_server.js @@ -1,9 +1,6 @@ -// @export Accounts -if (typeof Accounts === 'undefined') - Accounts = {}; +// XXX These should probably not actually be public? -if (!Accounts.urls) - Accounts.urls = {}; +Accounts.urls = {}; Accounts.urls.resetPassword = function (token) { return Meteor.absoluteUrl('#/reset-password/' + token); diff --git a/packages/accounts-facebook/facebook.js b/packages/accounts-facebook/facebook.js new file mode 100644 index 0000000000..4517bf1d5a --- /dev/null +++ b/packages/accounts-facebook/facebook.js @@ -0,0 +1,26 @@ +Accounts.oauth.registerService('facebook'); + +if (Meteor.isClient) { + Meteor.loginWithFacebook = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Facebook.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + // publish all fields including access token, which can legitimately + // be used from the client (if transmitted over ssl or on + // localhost). https://developers.facebook.com/docs/concepts/login/access-tokens-and-types/, + // "Sharing of Access Tokens" + forLoggedInUser: ['services.facebook'], + forOtherUsers: [ + // https://www.facebook.com/help/167709519956542 + 'services.facebook.id', 'services.facebook.username', 'services.facebook.gender' + ] + }); +} diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js deleted file mode 100644 index 149bfa8a0e..0000000000 --- a/packages/accounts-facebook/facebook_client.js +++ /dev/null @@ -1,4 +0,0 @@ -Meteor.loginWithFacebook = function(options, callback) { - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - Facebook.requestCredential(options, credentialRequestCompleteCallback); -}; \ No newline at end of file diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js deleted file mode 100644 index 171ca036f6..0000000000 --- a/packages/accounts-facebook/facebook_common.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Accounts.facebook) { - Accounts.facebook = {}; -} diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js deleted file mode 100644 index 2699808eaa..0000000000 --- a/packages/accounts-facebook/facebook_server.js +++ /dev/null @@ -1,13 +0,0 @@ -Accounts.oauth.registerService('facebook'); - -Accounts.addAutopublishFields({ - // publish all fields including access token, which can legitimately - // be used from the client (if transmitted over ssl or on - // localhost). https://developers.facebook.com/docs/concepts/login/access-tokens-and-types/, - // "Sharing of Access Tokens" - forLoggedInUser: ['services.facebook'], - forOtherUsers: [ - // https://www.facebook.com/help/167709519956542 - 'services.facebook.id', 'services.facebook.username', 'services.facebook.gender' - ] -}); diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 667f56bb32..cca0adb01f 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -11,7 +11,5 @@ Package.on_use(function(api) { api.add_files('facebook_login_button.css', 'client'); - api.add_files('facebook_common.js', ['client', 'server']); - api.add_files('facebook_server.js', 'server'); - api.add_files('facebook_client.js', 'client'); + api.add_files("facebook.js"); }); diff --git a/packages/accounts-github/github.js b/packages/accounts-github/github.js new file mode 100644 index 0000000000..bf71477695 --- /dev/null +++ b/packages/accounts-github/github.js @@ -0,0 +1,22 @@ +Accounts.oauth.registerService('github'); + +if (Meteor.isClient) { + Meteor.loginWithGithub = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Github.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + // not sure whether the github api can be used from the browser, + // thus not sure if we should be sending access tokens; but we do it + // for all other oauth2 providers, and it may come in handy. + forLoggedInUser: ['services.github'], + forOtherUsers: ['services.github.username'] + }); +} diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js deleted file mode 100644 index 7649f2bebb..0000000000 --- a/packages/accounts-github/github_client.js +++ /dev/null @@ -1,4 +0,0 @@ -Meteor.loginWithGithub = function(options, callback) { - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - Github.requestCredential(options, credentialRequestCompleteCallback); -}; \ No newline at end of file diff --git a/packages/accounts-github/github_common.js b/packages/accounts-github/github_common.js deleted file mode 100644 index 0e9b508596..0000000000 --- a/packages/accounts-github/github_common.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Accounts.github) { - Accounts.github = {}; -} diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js deleted file mode 100644 index ca57098047..0000000000 --- a/packages/accounts-github/github_server.js +++ /dev/null @@ -1,9 +0,0 @@ -Accounts.oauth.registerService('github'); - -Accounts.addAutopublishFields({ - // not sure whether the github api can be used from the browser, - // thus not sure if we should be sending access tokens; but we do it - // for all other oauth2 providers, and it may come in handy. - forLoggedInUser: ['services.github'], - forOtherUsers: ['services.github.username'] -}); diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index 3855b12c23..ed090b2863 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -11,7 +11,5 @@ Package.on_use(function(api) { api.add_files('github_login_button.css', 'client'); - api.add_files('github_common.js', ['client', 'server']); - api.add_files('github_server.js', 'server'); - api.add_files('github_client.js', 'client'); + api.add_files("github.js"); }); diff --git a/packages/accounts-google/google.js b/packages/accounts-google/google.js new file mode 100644 index 0000000000..08538f7ae3 --- /dev/null +++ b/packages/accounts-google/google.js @@ -0,0 +1,30 @@ +Accounts.oauth.registerService('google'); + +if (Meteor.isClient) { + Meteor.loginWithGoogle = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Google.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + forLoggedInUser: _.map( + // publish access token since it can be used from the client (if + // transmitted over ssl or on + // localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent + // refresh token probably shouldn't be sent down. + Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token + function (subfield) { return 'services.google.' + subfield; }), + + forOtherUsers: _.map( + // even with autopublish, no legitimate web app should be + // publishing all users' emails + _.without(Google.whitelistedFields, 'email', 'verified_email'), + function (subfield) { return 'services.google.' + subfield; }) + }); +} diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js deleted file mode 100644 index 96641ac3bd..0000000000 --- a/packages/accounts-google/google_client.js +++ /dev/null @@ -1,4 +0,0 @@ -Meteor.loginWithGoogle = function(options, callback) { - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - Google.requestCredential(options, credentialRequestCompleteCallback); -}; \ No newline at end of file diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js deleted file mode 100644 index f3945c2c23..0000000000 --- a/packages/accounts-google/google_common.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Accounts.google) { - Accounts.google = {}; -} diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js deleted file mode 100644 index 5c8575fc18..0000000000 --- a/packages/accounts-google/google_server.js +++ /dev/null @@ -1,17 +0,0 @@ -Accounts.oauth.registerService('google'); - -Accounts.addAutopublishFields({ - forLoggedInUser: _.map( - // publish access token since it can be used from the client (if - // transmitted over ssl or on - // localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent - // refresh token probably shouldn't be sent down. - Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token - function (subfield) { return 'services.google.' + subfield; }), - - forOtherUsers: _.map( - // even with autopublish, no legitimate web app should be - // publishing all users' emails - _.without(Google.whitelistedFields, 'email', 'verified_email'), - function (subfield) { return 'services.google.' + subfield; }) -}); diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index aac4af35c2..355f8bb0f1 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -12,7 +12,5 @@ Package.on_use(function(api) { api.add_files('google_login_button.css', 'client'); - api.add_files('google_common.js', ['client', 'server']); - api.add_files('google_server.js', 'server'); - api.add_files('google_client.js', 'client'); + api.add_files("google.js"); }); diff --git a/packages/accounts-meetup/meetup.js b/packages/accounts-meetup/meetup.js new file mode 100644 index 0000000000..08e8ec0938 --- /dev/null +++ b/packages/accounts-meetup/meetup.js @@ -0,0 +1,22 @@ +Accounts.oauth.registerService('meetup'); + +if (Meteor.isClient) { + Meteor.loginWithMeetup = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Meetup.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + // publish all fields including access token, which can legitimately + // be used from the client (if transmitted over ssl or on + // localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit + forLoggedInUser: ['services.meetup'], + forOtherUsers: ['services.meetup.id'] + }); +} diff --git a/packages/accounts-meetup/meetup_client.js b/packages/accounts-meetup/meetup_client.js deleted file mode 100644 index 6d0d674a2f..0000000000 --- a/packages/accounts-meetup/meetup_client.js +++ /dev/null @@ -1,4 +0,0 @@ -Meteor.loginWithMeetup = function(options, callback) { - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - Meetup.requestCredential(options, credentialRequestCompleteCallback); -}; \ No newline at end of file diff --git a/packages/accounts-meetup/meetup_common.js b/packages/accounts-meetup/meetup_common.js deleted file mode 100644 index 4eb47fff80..0000000000 --- a/packages/accounts-meetup/meetup_common.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Accounts.meetup) { - Accounts.meetup = {}; -} diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js deleted file mode 100644 index 4b727ac031..0000000000 --- a/packages/accounts-meetup/meetup_server.js +++ /dev/null @@ -1,11 +0,0 @@ -Accounts.oauth.registerService('meetup'); - -Accounts.addAutopublishFields({ - // publish all fields including access token, which can legitimately - // be used from the client (if transmitted over ssl or on - // localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit - forLoggedInUser: ['services.meetup'], - forOtherUsers: ['services.meetup.id'] -}); - - diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index 4616c99246..52e5e012d0 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -11,7 +11,5 @@ Package.on_use(function(api) { api.add_files('meetup_login_button.css', 'client'); - api.add_files('meetup_common.js', ['client', 'server']); - api.add_files('meetup_server.js', 'server'); - api.add_files('meetup_client.js', 'client'); + api.add_files("meetup.js"); }); diff --git a/packages/accounts-oauth/oauth_client.js b/packages/accounts-oauth/oauth_client.js index 5b189fdc05..f3d161e8c0 100644 --- a/packages/accounts-oauth/oauth_client.js +++ b/packages/accounts-oauth/oauth_client.js @@ -19,9 +19,9 @@ Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) { Accounts.oauth.credentialRequestCompleteHandler = function(callback) { return function (credentialTokenOrError) { if(credentialTokenOrError && credentialTokenOrError instanceof Error) { - callback(credentialTokenOrError); + callback && callback(credentialTokenOrError); } else { Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback); } }; -} +}; diff --git a/packages/accounts-oauth/oauth_common.js b/packages/accounts-oauth/oauth_common.js index d47da20292..031cd0eace 100644 --- a/packages/accounts-oauth/oauth_common.js +++ b/packages/accounts-oauth/oauth_common.js @@ -1 +1,24 @@ -Accounts.oauth = {}; \ No newline at end of file +Accounts.oauth = {}; + +var services = {}; + +// Helper for registering OAuth based accounts packages. +// On the server, adds an index to the user collection. +Accounts.oauth.registerService = function (name) { + if (_.has(services, name)) + throw new Error("Duplicate service: " + name); + services[name] = true; + + if (Meteor.server) { + // Accounts.updateOrCreateUserFromExternalService does a lookup by this id, + // so this should be a unique index. You might want to add indexes for other + // fields returned by your service (eg services.github.login) but you can do + // that in your app. + Meteor.users._ensureIndex('services.' + name + '.id', + {unique: 1, sparse: 1}); + } +}; + +Accounts.oauth.serviceNames = function () { + return _.keys(services); +}; diff --git a/packages/accounts-oauth/oauth_server.js b/packages/accounts-oauth/oauth_server.js index 775811289e..ca6dfa4aff 100644 --- a/packages/accounts-oauth/oauth_server.js +++ b/packages/accounts-oauth/oauth_server.js @@ -1,24 +1,3 @@ -// Helper for registering OAuth based accounts packages. -// Adds an index to the user collection. -Accounts.oauth.registerService = function (name) { - // Accounts.updateOrCreateUserFromExternalService does a lookup by this id, - // so this should be a unique index. You might want to add indexes for other - // fields returned by your service (eg services.github.login) but you can do - // that in your app. - Meteor.users._ensureIndex('services.' + name + '.id', - {unique: 1, sparse: 1}); - -}; - -// For test cleanup only. (Mongo has a limit as to how many indexes it can have -// per collection.) -Accounts.oauth._unregisterService = function (name) { - var index = {}; - index['services.' + name + '.id'] = 1; - Meteor.users._dropIndex(index); -}; - - // Listen to calls to `login` with an oauth option set. This is where // users actually get logged in to meteor via oauth. Accounts.registerLoginHandler(function (options) { diff --git a/packages/accounts-oauth/package.js b/packages/accounts-oauth/package.js index d3fb7b984b..1c1d949896 100644 --- a/packages/accounts-oauth/package.js +++ b/packages/accounts-oauth/package.js @@ -13,7 +13,7 @@ Package.on_use(function (api) { api.imply('accounts-base', ['client', 'server']); api.use('oauth', 'server'); - api.add_files('oauth_common.js', ['client', 'server']); + api.add_files('oauth_common.js'); api.add_files('oauth_client.js', 'client'); api.add_files('oauth_server.js', 'server'); }); diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index 1745e3a909..30a542aa12 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -3,20 +3,20 @@ // the string "intercept", storing them in an array that can then // be retrieved using the getInterceptedEmails method // -var oldEmailSend = Email.send; var interceptedEmails = {}; // (email address) -> (array of contents) -Email.send = function (options) { +EmailTest.hookSend(function (options) { var to = options.to; if (to.indexOf('intercept') === -1) { - oldEmailSend(options); + return true; // go ahead and send } else { if (!interceptedEmails[to]) interceptedEmails[to] = []; interceptedEmails[to].push(options.text); + return false; // skip sending } -}; +}); Meteor.methods({ getInterceptedEmails: function (email) { diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index a8d2c7c16a..676ac1c91f 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -11,16 +11,17 @@ Package.on_use(function(api) { api.use('random', ['server']); api.use('check', ['server']); api.use('underscore'); + api.use('livedata', ['client', 'server']); api.add_files('email_templates.js', 'server'); api.add_files('password_server.js', 'server'); api.add_files('password_client.js', 'client'); - api.add_files('password_common.js', ['server', 'client']); }); Package.on_test(function(api) { api.use(['accounts-password', 'tinytest', 'test-helpers', 'deps', - 'accounts-base', 'random', 'email', 'underscore', 'check']); + 'accounts-base', 'random', 'email', 'underscore', 'check', + 'livedata']); api.add_files('password_tests_setup.js', 'server'); api.add_files('password_tests.js', ['client', 'server']); api.add_files('email_tests_setup.js', 'server'); diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 13612c596c..77731e9ec1 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -8,7 +8,7 @@ // @param password {String} // @param callback {Function(error|undefined)} Meteor.loginWithPassword = function (selector, password, callback) { - var srp = new Meteor._srp.Client(password); + var srp = new SRP.Client(password); var request = srp.startExchange(); if (typeof selector === 'string') @@ -50,7 +50,7 @@ Accounts.createUser = function (options, callback) { if (!options.password) throw new Error("Must set options.password"); - var verifier = Meteor._srp.generateVerifier(options.password); + var verifier = SRP.generateVerifier(options.password); // strip old password, replacing with the verifier object delete options.password; options.srp = verifier; @@ -77,7 +77,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { return; } - var verifier = Meteor._srp.generateVerifier(newPassword); + var verifier = SRP.generateVerifier(newPassword); if (!oldPassword) { Meteor.apply('changePassword', [{srp: verifier}], function (error, result) { @@ -89,7 +89,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { } }); } else { // oldPassword - var srp = new Meteor._srp.Client(oldPassword); + var srp = new SRP.Client(oldPassword); var request = srp.startExchange(); request.user = {id: Meteor.user()._id}; Meteor.apply('beginPasswordExchange', [request], function (error, result) { @@ -142,7 +142,7 @@ Accounts.resetPassword = function(token, newPassword, callback) { if (!newPassword) throw new Error("Need to pass newPassword"); - var verifier = Meteor._srp.generateVerifier(newPassword); + var verifier = SRP.generateVerifier(newPassword); Accounts.callLoginMethod({ methodName: 'resetPassword', methodArguments: [token, verifier], diff --git a/packages/accounts-password/password_common.js b/packages/accounts-password/password_common.js deleted file mode 100644 index 7ad0470e16..0000000000 --- a/packages/accounts-password/password_common.js +++ /dev/null @@ -1 +0,0 @@ -Accounts.password = {}; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 663f7c15b4..9fe9e5ff93 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -62,7 +62,7 @@ Meteor.methods({beginPasswordExchange: function (request) { throw new Meteor.Error(403, "User has no password set"); var verifier = user.services.password.srp; - var srp = new Meteor._srp.Server(verifier); + var srp = new SRP.Server(verifier); var challenge = srp.issueChallenge({A: request.A}); // save off results in the current session so we can verify them @@ -82,7 +82,7 @@ Accounts.registerLoginHandler(function (options) { // we're always called from within a 'login' method, so this should // be safe. - var currentInvocation = Meteor._CurrentInvocation.get(); + var currentInvocation = DDP._CurrentInvocation.get(); var serialized = currentInvocation._sessionData.srpChallenge; if (!serialized || serialized.M !== options.srp.M) throw new Meteor.Error(403, "Incorrect password"); @@ -127,7 +127,7 @@ Accounts.registerLoginHandler(function (options) { // Just check the verifier output when the same identity and salt // are passed. Don't bother with a full exchange. var verifier = user.services.password.srp; - var newVerifier = Meteor._srp.generateVerifier(options.password, { + var newVerifier = SRP.generateVerifier(options.password, { identity: verifier.identity, salt: verifier.salt}); if (verifier.verifier !== newVerifier.verifier) @@ -155,7 +155,7 @@ Meteor.methods({changePassword: function (options) { // password. For now, we don't allow changePassword without knowing the old // password. M: String, - srp: Match.Optional(Meteor._srp.matchVerifier), + srp: Match.Optional(SRP.matchVerifier), password: Match.Optional(String) }); @@ -170,7 +170,7 @@ Meteor.methods({changePassword: function (options) { var verifier = options.srp; if (!verifier && options.password) { - verifier = Meteor._srp.generateVerifier(options.password); + verifier = SRP.generateVerifier(options.password); } if (!verifier) throw new Meteor.Error(400, "Invalid verifier"); @@ -192,7 +192,7 @@ Accounts.setPassword = function (userId, newPassword) { var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); - var newVerifier = Meteor._srp.generateVerifier(newPassword); + var newVerifier = SRP.generateVerifier(newPassword); Meteor.users.update({_id: user._id}, { $set: {'services.password.srp': newVerifier}}); @@ -217,6 +217,7 @@ Meteor.methods({forgotPassword: function (options) { // send the user an email with a link that when opened allows the user // to set a new password, without the old password. +// Accounts.sendResetPasswordEmail = function (userId, email) { // Make sure the user exists, and email is one of their addresses. var user = Meteor.users.findOne(userId); @@ -252,8 +253,9 @@ Accounts.sendResetPasswordEmail = function (userId, email) { // to choose their password. The email must be one of the addresses in the // user's emails field, or undefined to pick the first email automatically. // -// This is not called automatically, it must be called manually if you +// This is not called automatically. It must be called manually if you // want to use enrollment emails. +// Accounts.sendEnrollmentEmail = function (userId, email) { // XXX refactor! This is basically identical to sendResetPasswordEmail. @@ -293,7 +295,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) { // the users password, and log them in. Meteor.methods({resetPassword: function (token, newVerifier) { check(token, String); - check(newVerifier, Meteor._srp.matchVerifier); + check(newVerifier, SRP.matchVerifier); var user = Meteor.users.findOne({ "services.password.reset.token": ""+token}); @@ -329,6 +331,7 @@ Meteor.methods({resetPassword: function (token, newVerifier) { // send the user an email with a link that when opened marks that // address as verified +// Accounts.sendVerificationEmail = function (userId, address) { // XXX Also generate a link using which someone can delete this // account if they own said address but weren't those who created @@ -428,7 +431,7 @@ var createUser = function (options) { username: Match.Optional(String), email: Match.Optional(String), password: Match.Optional(String), - srp: Match.Optional(Meteor._srp.matchVerifier) + srp: Match.Optional(SRP.matchVerifier) })); var username = options.username; @@ -442,7 +445,7 @@ var createUser = function (options) { if (options.password) { if (options.srp) throw new Meteor.Error(400, "Don't pass both password and srp in options"); - options.srp = Meteor._srp.generateVerifier(options.password); + options.srp = SRP.generateVerifier(options.password); } var user = {services: {}}; @@ -493,6 +496,7 @@ Meteor.methods({createUser: function (options) { // which is always empty when called from the createUser method? eg, "admin: // true", which we want to prevent the client from setting, but which a custom // method calling Accounts.createUser could set? +// Accounts.createUser = function (options, callback) { options = _.clone(options); options.generateLoginToken = false; diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index a29c8f00ef..f9b2bb43bd 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -11,11 +11,8 @@ Package.on_use(function(api) { api.use('twitter', ['client', 'server']); api.use('http', ['client', 'server']); - api.use('templating', 'client'); api.add_files('twitter_login_button.css', 'client'); - api.add_files('twitter_common.js', ['client', 'server']); - api.add_files('twitter_server.js', 'server'); - api.add_files('twitter_client.js', 'client'); + api.add_files("twitter.js"); }); diff --git a/packages/accounts-twitter/twitter.js b/packages/accounts-twitter/twitter.js new file mode 100644 index 0000000000..176d82a655 --- /dev/null +++ b/packages/accounts-twitter/twitter.js @@ -0,0 +1,24 @@ +Accounts.oauth.registerService('twitter'); + +if (Meteor.isClient) { + Meteor.loginWithTwitter = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Twitter.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + var autopublishedFields = _.map( + // don't send access token. https://dev.twitter.com/discussions/5025 + Twitter.whitelistedFields.concat(['id', 'screenName']), + function (subfield) { return 'services.twitter.' + subfield; }); + + Accounts.addAutopublishFields({ + forLoggedInUser: autopublishedFields, + forOtherUsers: autopublishedFields + }); +} diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js deleted file mode 100644 index 887d4ad510..0000000000 --- a/packages/accounts-twitter/twitter_client.js +++ /dev/null @@ -1,4 +0,0 @@ -Meteor.loginWithTwitter = function(options, callback) { - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - Twitter.requestCredential(options, credentialRequestCompleteCallback); -}; \ No newline at end of file diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js deleted file mode 100644 index b1428d3bf4..0000000000 --- a/packages/accounts-twitter/twitter_common.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Accounts.twitter) { - Accounts.twitter = {}; -} diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js deleted file mode 100644 index fea172a744..0000000000 --- a/packages/accounts-twitter/twitter_server.js +++ /dev/null @@ -1,11 +0,0 @@ -Accounts.oauth.registerService('twitter'); - -var autopublishedFields = _.map( - // don't send access token. https://dev.twitter.com/discussions/5025 - Twitter.whitelistedFields.concat(['id', 'screenName']), - function (subfield) { return 'services.twitter.' + subfield; }); - -Accounts.addAutopublishFields({ - forLoggedInUser: autopublishedFields, - forOtherUsers: autopublishedFields -}); diff --git a/packages/accounts-ui-unstyled/accounts_ui.js b/packages/accounts-ui-unstyled/accounts_ui.js index 75ed55e7c4..5f241ab997 100644 --- a/packages/accounts-ui-unstyled/accounts_ui.js +++ b/packages/accounts-ui-unstyled/accounts_ui.js @@ -1,13 +1,9 @@ -if (!Accounts.ui) - Accounts.ui = {}; - -if (!Accounts.ui._options) { - Accounts.ui._options = { - requestPermissions: {}, - requestOfflineToken: {} - }; -} +Accounts.ui = {}; +Accounts.ui._options = { + requestPermissions: {}, + requestOfflineToken: {} +}; Accounts.ui.config = function(options) { // validate options keys @@ -62,7 +58,7 @@ Accounts.ui.config = function(options) { } }; -Accounts.ui._passwordSignupFields = function () { +passwordSignupFields = function () { return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; }; diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index e7d2e88009..d173faaeb1 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -1,6 +1,3 @@ -if (!Accounts._loginButtons) - Accounts._loginButtons = {}; - // for convenience var loginButtonsSession = Accounts._loginButtonsSession; @@ -26,20 +23,105 @@ Template._loginButtons.preserve({ 'input[id]': Spark._labelFromIdOrName }); +// +// helpers +// + +displayName = function () { + var user = Meteor.user(); + if (!user) + return ''; + + if (user.profile && user.profile.name) + return user.profile.name; + if (user.username) + return user.username; + if (user.emails && user.emails[0] && user.emails[0].address) + return user.emails[0].address; + + return ''; +}; + +// returns an array of the login services used by this app. each +// element of the array is an object (eg {name: 'facebook'}), since +// that makes it useful in combination with handlebars {{#each}}. +// +// don't cache the output of this function: if called during startup (before +// oauth packages load) it might not include them all. +// +// NOTE: It is very important to have this return password last +// because of the way we render the different providers in +// login_buttons_dropdown.html +getLoginServices = function () { + var self = this; + + // First look for OAuth services. + var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; + + // Be equally kind to all login services. This also preserves + // backwards-compatibility. (But maybe order should be + // configurable?) + services.sort(); + + // Add password, if it's there; it must come last. + if (hasPasswordService()) + services.push('password'); + + return _.map(services, function(name) { + return {name: name}; + }); +}; + +hasPasswordService = function () { + return !!Package['accounts-password']; +}; + +dropdown = function () { + return hasPasswordService() || getLoginServices().length > 1; +}; + +// XXX improve these. should this be in accounts-password instead? +// +// XXX these will become configurable, and will be validated on +// the server as well. +validateUsername = function (username) { + if (username.length >= 3) { + return true; + } else { + loginButtonsSession.errorMessage("Username must be at least 3 characters long"); + return false; + } +}; +validateEmail = function (email) { + if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') + return true; + + if (email.indexOf('@') !== -1) { + return true; + } else { + loginButtonsSession.errorMessage("Invalid email"); + return false; + } +}; +validatePassword = function (password) { + if (password.length >= 6) { + return true; + } else { + loginButtonsSession.errorMessage("Password must be at least 6 characters long"); + return false; + } +}; + // // loginButtonLoggedOut template // -Template._loginButtonsLoggedOut.dropdown = function () { - return Accounts._loginButtons.dropdown(); -}; +Template._loginButtonsLoggedOut.dropdown = dropdown; -Template._loginButtonsLoggedOut.services = function () { - return Accounts._loginButtons.getLoginServices(); -}; +Template._loginButtonsLoggedOut.services = getLoginServices; Template._loginButtonsLoggedOut.singleService = function () { - var services = Accounts._loginButtons.getLoginServices(); + var services = getLoginServices(); if (services.length !== 1) throw new Error( "Shouldn't be rendering this template with more than one configured service"); @@ -57,9 +139,7 @@ Template._loginButtonsLoggedOut.configurationLoaded = function () { // decide whether we should show a dropdown rather than a row of // buttons -Template._loginButtonsLoggedIn.dropdown = function () { - return Accounts._loginButtons.dropdown(); -}; +Template._loginButtonsLoggedIn.dropdown = dropdown; @@ -67,9 +147,7 @@ Template._loginButtonsLoggedIn.dropdown = function () { // loginButtonsLoggedInSingleLogoutButton template // -Template._loginButtonsLoggedInSingleLogoutButton.displayName = function () { - return Accounts._loginButtons.displayName(); -}; +Template._loginButtonsLoggedInSingleLogoutButton.displayName = displayName; @@ -90,113 +168,5 @@ Template._loginButtonsMessages.infoMessage = function () { // loginButtonsLoggingInPadding template // -Template._loginButtonsLoggingInPadding.dropdown = function () { - return Accounts._loginButtons.dropdown(); -}; +Template._loginButtonsLoggingInPadding.dropdown = dropdown; - -// -// helpers -// - -Accounts._loginButtons.displayName = function () { - var user = Meteor.user(); - if (!user) - return ''; - - if (user.profile && user.profile.name) - return user.profile.name; - if (user.username) - return user.username; - if (user.emails && user.emails[0] && user.emails[0].address) - return user.emails[0].address; - - return ''; -}; - -// returns an array of the login services used by this app. each -// element of the array is an object (eg {name: 'facebook'}), since -// that makes it useful in combination with handlebars {{#each}}. -// -// NOTE: It is very important to have this return password last -// because of the way we render the different providers in -// login_buttons_dropdown.html -Accounts._loginButtons.getLoginServices = function () { - var self = this; - var services = []; - - // find all methods of the form: `Meteor.loginWithFoo`, where - // `Foo` corresponds to a login service - // - // XXX we should consider having a client-side - // Accounts.oauth.registerService function which records the - // active services and encapsulates boilerplate code now found in - // files such as facebook_client.js. This would have the added - // benefit of allow us to unify facebook_{client,common,server}.js - // into one file, which would encourage people to build more login - // services packages. - _.each(_.keys(Meteor), function(methodName) { - var match; - if ((match = methodName.match(/^loginWith(.*)/))) { - var serviceName = match[1].toLowerCase(); - - // HACKETY HACK. needed to not match - // Meteor.loginWithToken. See XXX above. - if (Accounts[serviceName]) - services.push(match[1].toLowerCase()); - } - }); - - // Be equally kind to all login services. This also preserves - // backwards-compatibility. (But maybe order should be - // configurable?) - services.sort(); - - // ensure password is last - if (_.contains(services, 'password')) - services = _.without(services, 'password').concat(['password']); - - return _.map(services, function(name) { - return {name: name}; - }); -}; - -Accounts._loginButtons.hasPasswordService = function () { - return Accounts.password; -}; - -Accounts._loginButtons.dropdown = function () { - return Accounts._loginButtons.hasPasswordService() || Accounts._loginButtons.getLoginServices().length > 1; -}; - -// XXX improve these. should this be in accounts-password instead? -// -// XXX these will become configurable, and will be validated on -// the server as well. -Accounts._loginButtons.validateUsername = function (username) { - if (username.length >= 3) { - return true; - } else { - loginButtonsSession.errorMessage("Username must be at least 3 characters long"); - return false; - } -}; -Accounts._loginButtons.validateEmail = function (email) { - if (Accounts.ui._passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') - return true; - - if (email.indexOf('@') !== -1) { - return true; - } else { - loginButtonsSession.errorMessage("Invalid email"); - return false; - } -}; -Accounts._loginButtons.validatePassword = function (password) { - if (password.length >= 6) { - return true; - } else { - loginButtonsSession.errorMessage("Password must be at least 6 characters long"); - return false; - } -}; diff --git a/packages/accounts-ui-unstyled/login_buttons_dialogs.js b/packages/accounts-ui-unstyled/login_buttons_dialogs.js index 67bc3c4c79..59d4ef1365 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dialogs.js +++ b/packages/accounts-ui-unstyled/login_buttons_dialogs.js @@ -53,7 +53,7 @@ Template._resetPasswordDialog.events({ var resetPassword = function () { loginButtonsSession.resetMessages(); var newPassword = document.getElementById('reset-password-new-password').value; - if (!Accounts._loginButtons.validatePassword(newPassword)) + if (!validatePassword(newPassword)) return; Accounts.resetPassword( @@ -94,7 +94,7 @@ Template._enrollAccountDialog.events({ var enrollAccount = function () { loginButtonsSession.resetMessages(); var password = document.getElementById('enroll-account-password').value; - if (!Accounts._loginButtons.validatePassword(password)) + if (!validatePassword(password)) return; Accounts.resetPassword( @@ -141,7 +141,7 @@ Template._loginButtonsMessagesDialog.events({ Template._loginButtonsMessagesDialog.visible = function () { var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage'); - return !Accounts._loginButtons.dropdown() && hasMessage; + return !dropdown() && hasMessage; }; diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.js b/packages/accounts-ui-unstyled/login_buttons_dropdown.js index 41238c6e58..89f61f4334 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.js +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.js @@ -26,9 +26,7 @@ Template._loginButtonsLoggedInDropdown.events({ } }); -Template._loginButtonsLoggedInDropdown.displayName = function () { - return Accounts._loginButtons.displayName(); -}; +Template._loginButtonsLoggedInDropdown.displayName = displayName; Template._loginButtonsLoggedInDropdown.inChangePasswordFlow = function () { return loginButtonsSession.get('inChangePasswordFlow'); @@ -175,26 +173,21 @@ Template._loginButtonsLoggedOutDropdown.dropdownVisible = function () { return loginButtonsSession.get('dropdownVisible'); }; -Template._loginButtonsLoggedOutDropdown.hasPasswordService = function () { - return Accounts._loginButtons.hasPasswordService(); -}; +Template._loginButtonsLoggedOutDropdown.hasPasswordService = hasPasswordService; // return all login services, with password last -Template._loginButtonsLoggedOutAllServices.services = function () { - return Accounts._loginButtons.getLoginServices(); -}; +Template._loginButtonsLoggedOutAllServices.services = getLoginServices; Template._loginButtonsLoggedOutAllServices.isPasswordService = function () { return this.name === 'password'; }; Template._loginButtonsLoggedOutAllServices.hasOtherServices = function () { - return Accounts._loginButtons.getLoginServices().length > 1; + return getLoginServices().length > 1; }; -Template._loginButtonsLoggedOutAllServices.hasPasswordService = function () { - return Accounts._loginButtons.hasPasswordService(); -}; +Template._loginButtonsLoggedOutAllServices.hasPasswordService = + hasPasswordService; Template._loginButtonsLoggedOutPasswordService.fields = function () { var loginFields = [ @@ -202,15 +195,15 @@ Template._loginButtonsLoggedOutPasswordService.fields = function () { visible: function () { return _.contains( ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], - Accounts.ui._passwordSignupFields()); + passwordSignupFields()); }}, {fieldName: 'username', fieldLabel: 'Username', visible: function () { - return Accounts.ui._passwordSignupFields() === "USERNAME_ONLY"; + return passwordSignupFields() === "USERNAME_ONLY"; }}, {fieldName: 'email', fieldLabel: 'Email', inputType: 'email', visible: function () { - return Accounts.ui._passwordSignupFields() === "EMAIL_ONLY"; + return passwordSignupFields() === "EMAIL_ONLY"; }}, {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', visible: function () { @@ -223,17 +216,17 @@ Template._loginButtonsLoggedOutPasswordService.fields = function () { visible: function () { return _.contains( ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], - Accounts.ui._passwordSignupFields()); + passwordSignupFields()); }}, {fieldName: 'email', fieldLabel: 'Email', inputType: 'email', visible: function () { return _.contains( ["USERNAME_AND_EMAIL", "EMAIL_ONLY"], - Accounts.ui._passwordSignupFields()); + passwordSignupFields()); }}, {fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email', visible: function () { - return Accounts.ui._passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL"; + return passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL"; }}, {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', visible: function () { @@ -247,7 +240,7 @@ Template._loginButtonsLoggedOutPasswordService.fields = function () { // the "forgot password" flow. return _.contains( ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], - Accounts.ui._passwordSignupFields()); + passwordSignupFields()); }} ]; @@ -273,7 +266,7 @@ Template._loginButtonsLoggedOutPasswordService.showCreateAccountLink = function Template._loginButtonsLoggedOutPasswordService.showForgotPasswordLink = function () { return _.contains( ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"], - Accounts.ui._passwordSignupFields()); + passwordSignupFields()); }; Template._loginButtonsFormField.inputType = function () { @@ -313,7 +306,7 @@ Template._loginButtonsChangePassword.fields = function () { // the "forgot password" flow. return _.contains( ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], - Accounts.ui._passwordSignupFields()); + passwordSignupFields()); }} ]; }; @@ -357,19 +350,19 @@ var login = function () { var loginSelector; if (username !== null) { - if (!Accounts._loginButtons.validateUsername(username)) + if (!validateUsername(username)) return; else loginSelector = {username: username}; } else if (email !== null) { - if (!Accounts._loginButtons.validateEmail(email)) + if (!validateEmail(email)) return; else loginSelector = {email: email}; } else if (usernameOrEmail !== null) { // XXX not sure how we should validate this. but this seems good enough (for now), // since an email must have at least 3 characters anyways - if (!Accounts._loginButtons.validateUsername(usernameOrEmail)) + if (!validateUsername(usernameOrEmail)) return; else loginSelector = usernameOrEmail; @@ -393,7 +386,7 @@ var signup = function () { var username = trimmedElementValueById('login-username'); if (username !== null) { - if (!Accounts._loginButtons.validateUsername(username)) + if (!validateUsername(username)) return; else options.username = username; @@ -401,7 +394,7 @@ var signup = function () { var email = trimmedElementValueById('login-email'); if (email !== null) { - if (!Accounts._loginButtons.validateEmail(email)) + if (!validateEmail(email)) return; else options.email = email; @@ -409,7 +402,7 @@ var signup = function () { // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); - if (!Accounts._loginButtons.validatePassword(password)) + if (!validatePassword(password)) return; else options.password = password; @@ -450,7 +443,7 @@ var changePassword = function () { // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); - if (!Accounts._loginButtons.validatePassword(password)) + if (!validatePassword(password)) return; if (!matchPasswordAgainIfPresent()) diff --git a/packages/accounts-ui-unstyled/login_buttons_session.js b/packages/accounts-ui-unstyled/login_buttons_session.js index fdf3d2ad25..78324d0e07 100644 --- a/packages/accounts-ui-unstyled/login_buttons_session.js +++ b/packages/accounts-ui-unstyled/login_buttons_session.js @@ -27,7 +27,10 @@ var validateKey = function (key) { var KEY_PREFIX = "Meteor.loginButtons."; -// XXX we should have a better pattern for code private to a package like this one +// XXX This should probably be package scope rather than exported +// (there was even a comment to that effect here from before we had +// namespacing) but accounts-ui-viewer uses it, so leave it as is for +// now Accounts._loginButtonsSession = { set: function(key, value) { validateKey(key); diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js index 1acad8282a..258ae5bb49 100644 --- a/packages/accounts-ui-unstyled/package.js +++ b/packages/accounts-ui-unstyled/package.js @@ -3,12 +3,19 @@ Package.describe({ }); Package.on_use(function (api) { - api.use(['deps', 'service-configuration', 'accounts-urls', 'accounts-base', + api.use(['deps', 'service-configuration', 'accounts-base', 'underscore', 'templating', 'handlebars', 'spark', 'session'], 'client'); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); + // Allow us to call Accounts.oauth.serviceNames, if there are any OAuth + // services. + api.use('accounts-oauth', {weak: true}); + // Allow us to directly test if accounts-password (which doesn't use + // Accounts.oauth.registerService) exists. + api.use('accounts-password', {weak: true}); + api.add_files([ 'accounts_ui.js', diff --git a/packages/accounts-urls/package.js b/packages/accounts-urls/package.js deleted file mode 100644 index e1fd55e3b9..0000000000 --- a/packages/accounts-urls/package.js +++ /dev/null @@ -1,9 +0,0 @@ -Package.describe({ - summary: "Generate and consume reset password and verify account URLs", - internal: true -}); - -Package.on_use(function (api) { - api.add_files('url_client.js', 'client'); - api.add_files('url_server.js', 'server'); -}); diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index 22329991f2..06e0df739f 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -11,7 +11,5 @@ Package.on_use(function(api) { api.add_files('weibo_login_button.css', 'client'); - api.add_files('weibo_common.js', ['client', 'server']); - api.add_files('weibo_server.js', 'server'); - api.add_files('weibo_client.js', 'client'); + api.add_files("weibo.js"); }); diff --git a/packages/accounts-weibo/weibo.js b/packages/accounts-weibo/weibo.js new file mode 100644 index 0000000000..f55ec19bc7 --- /dev/null +++ b/packages/accounts-weibo/weibo.js @@ -0,0 +1,21 @@ +Accounts.oauth.registerService('weibo'); + +if (Meteor.isClient) { + Meteor.loginWithWeibo = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Weibo.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + // publish all fields including access token, which can legitimately + // be used from the client (if transmitted over ssl or on localhost) + forLoggedInUser: ['services.weibo'], + forOtherUsers: ['services.weibo.screenName'] + }); +} diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js deleted file mode 100644 index 644c0176e6..0000000000 --- a/packages/accounts-weibo/weibo_client.js +++ /dev/null @@ -1,4 +0,0 @@ -Meteor.loginWithWeibo = function(options, callback) { - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - Weibo.requestCredential(options, credentialRequestCompleteCallback); -}; \ No newline at end of file diff --git a/packages/accounts-weibo/weibo_common.js b/packages/accounts-weibo/weibo_common.js deleted file mode 100644 index 19ec575ef6..0000000000 --- a/packages/accounts-weibo/weibo_common.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Accounts.weibo) { - Accounts.weibo = {}; -} diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js deleted file mode 100644 index 18a3a12ae7..0000000000 --- a/packages/accounts-weibo/weibo_server.js +++ /dev/null @@ -1,9 +0,0 @@ -Accounts.oauth.registerService('weibo'); - -Accounts.addAutopublishFields({ - // publish all fields including access token, which can legitimately - // be used from the client (if transmitted over ssl or on localhost) - forLoggedInUser: ['services.weibo'], - forOtherUsers: ['services.weibo.screenName'] -}); - diff --git a/packages/appcache/appcache-client.js b/packages/appcache/appcache-client.js index 3c5afd527c..024b71d01f 100644 --- a/packages/appcache/appcache-client.js +++ b/packages/appcache/appcache-client.js @@ -13,7 +13,7 @@ var updatingAppcache = false; var reloadRetry = null; var appcacheUpdated = false; -Meteor._reload.onMigrate('appcache', function(retry) { +Reload._onMigrate('appcache', function(retry) { if (appcacheUpdated) return [true]; @@ -61,7 +61,7 @@ window.applicationCache.addEventListener('obsolete', (function() { } else { appcacheUpdated = true; - Meteor._reload.reload(); + Reload._reload(); } }), false); diff --git a/packages/appcache/package.js b/packages/appcache/package.js index bcbca06ff0..27700a35ec 100644 --- a/packages/appcache/package.js +++ b/packages/appcache/package.js @@ -6,7 +6,6 @@ Package.on_use(function (api) { api.use('webapp', 'server'); api.use('reload', 'client'); api.use('routepolicy', 'server'); - api.use('startup', 'client'); api.use('underscore', 'server'); api.add_files('appcache-client.js', 'client'); api.add_files('appcache-server.js', 'server'); diff --git a/packages/audit-argument-checks/audit_argument_checks.js b/packages/audit-argument-checks/audit_argument_checks.js deleted file mode 100644 index a1a2afe602..0000000000 --- a/packages/audit-argument-checks/audit_argument_checks.js +++ /dev/null @@ -1 +0,0 @@ -Meteor._LivedataServer._auditArgumentChecks = true; diff --git a/packages/audit-argument-checks/package.js b/packages/audit-argument-checks/package.js index 9252ea10a1..bc7b2b5592 100644 --- a/packages/audit-argument-checks/package.js +++ b/packages/audit-argument-checks/package.js @@ -2,7 +2,4 @@ Package.describe({ summary: "Try to detect inadequate input sanitization" }); -Package.on_use(function (api) { - api.use(['livedata'], ['server']); - api.add_files(['audit_argument_checks.js'], 'server'); -}); +// This package is empty; its presence is detected by livedata. diff --git a/packages/autopublish/autopublish.js b/packages/autopublish/autopublish.js deleted file mode 100644 index bedee57223..0000000000 --- a/packages/autopublish/autopublish.js +++ /dev/null @@ -1 +0,0 @@ -Meteor.default_server.autopublish(); diff --git a/packages/autopublish/package.js b/packages/autopublish/package.js index ae8c12f22a..960a18afd8 100644 --- a/packages/autopublish/package.js +++ b/packages/autopublish/package.js @@ -2,7 +2,5 @@ Package.describe({ summary: "Automatically publish the entire database to all clients" }); -Package.on_use(function (api, where) { - api.use('livedata', 'server'); - api.add_files("autopublish.js", "server"); -}); \ No newline at end of file +// This package is empty; its presence is detected by livedata and +// accounts-base. diff --git a/packages/backbone/backbone.js b/packages/backbone/backbone.js index e770b20e51..21bce6cba2 100644 --- a/packages/backbone/backbone.js +++ b/packages/backbone/backbone.js @@ -37,8 +37,10 @@ Backbone.VERSION = '0.9.2'; // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + // Commented these lines out; we have _ via api.use. + // var _ = root._; + // if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + // // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. var $ = root.jQuery || root.Zepto || root.ender; diff --git a/packages/backbone/package.js b/packages/backbone/package.js index 30ef0d6166..cbc12b735f 100644 --- a/packages/backbone/package.js +++ b/packages/backbone/package.js @@ -2,10 +2,9 @@ Package.describe({ summary: "A minimalist client-side MVC framework" }); -Package.on_use(function (api, where) { +Package.on_use(function (api) { // XXX Backbone requires either jquery or zepto api.use(["jquery", "json", "underscore"]); - where = where || ['client', 'server']; - api.add_files("backbone.js", where); + api.add_files("backbone.js"); }); diff --git a/packages/check/match.js b/packages/check/match.js index 453c731eaf..1e6461fa7d 100644 --- a/packages/check/match.js +++ b/packages/check/match.js @@ -6,7 +6,6 @@ var currentArgumentChecker = new Meteor.EnvironmentVariable; -// @export check check = function (value, pattern) { // Record that check got called, if somebody cared. var argChecker = currentArgumentChecker.get(); @@ -16,26 +15,20 @@ check = function (value, pattern) { }; Match = { - // @export Match.Optional Optional: function (pattern) { return new Optional(pattern); }, - // @export Match.OneOf OneOf: function (/*arguments*/) { return new OneOf(_.toArray(arguments)); }, - // @export Match.Any Any: ['__any__'], - // @export Match.Where Where: function (condition) { return new Where(condition); }, - // @export Match.ObjectIncluding ObjectIncluding: function (pattern) { return new ObjectIncluding(pattern); }, - // @export Match.Error // 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) { @@ -45,7 +38,6 @@ Match = { this.sanitizedError = new Meteor.Error(400, "Match failed"); }), - // @export Match.test // Tests to see if value matches pattern. Unlike check, it merely returns true // or false (unless an error other than Match.Error was thrown). It does not // interact with _failIfArgumentsAreNotAllChecked. @@ -68,7 +60,6 @@ Match = { // `args` (either directly or in the first level of an array), throws an error // (using `description` in the message). // - // @export Match._failIfArgumentsAreNotAllChecked _failIfArgumentsAreNotAllChecked: function (f, context, args, description) { var argChecker = new ArgumentChecker(args, description); var result = currentArgumentChecker.withValue(argChecker, function () { diff --git a/packages/check/package.js b/packages/check/package.js index 2a58b7c400..ca73cefa5e 100644 --- a/packages/check/package.js +++ b/packages/check/package.js @@ -6,6 +6,8 @@ Package.describe({ Package.on_use(function (api) { api.use(['underscore', 'ejson'], ['client', 'server']); + api.export(['check', 'Match']); + api.add_files('match.js', ['client', 'server']); }); diff --git a/packages/coffeescript-test-helper/exporting.coffee b/packages/coffeescript-test-helper/exporting.coffee index 74f105d070..eb75de9f7d 100644 --- a/packages/coffeescript-test-helper/exporting.coffee +++ b/packages/coffeescript-test-helper/exporting.coffee @@ -1,3 +1 @@ -### @export COFFEESCRIPT_EXPORTED ### - COFFEESCRIPT_EXPORTED = 123 diff --git a/packages/coffeescript-test-helper/package.js b/packages/coffeescript-test-helper/package.js index e1a9f2a3f6..b420bc4ae3 100644 --- a/packages/coffeescript-test-helper/package.js +++ b/packages/coffeescript-test-helper/package.js @@ -1,9 +1,10 @@ Package.describe({ - summary: "Used by the coffeescript package's @export tests", + summary: "Used by the coffeescript package's tests", internal: true }); Package.on_use(function (api) { api.use('coffeescript', ['client', 'server']); + api.export('COFFEESCRIPT_EXPORTED'); api.add_files("exporting.coffee", ['client', 'server']); }); diff --git a/packages/coffeescript/plugin/compile-coffeescript.js b/packages/coffeescript/plugin/compile-coffeescript.js index 988eb53188..3270a2912e 100644 --- a/packages/coffeescript/plugin/compile-coffeescript.js +++ b/packages/coffeescript/plugin/compile-coffeescript.js @@ -28,6 +28,8 @@ var stripExportedVars = function (source, exports) { // up on subsequent lines.) // XXX relax these assumptions by doing actual JS parsing (eg with jsparse). // I'd do this now, but there's no easy way to "unparse" a jsparse AST. + // Or alternatively, hack the compiler to allow us to specify unbound + // symbols directly. for (var i = 0; i < lines.length; i++) { var line = lines[i]; @@ -83,8 +85,7 @@ var addSharedHeader = function (source, sourceMap) { // as a var in the package closure, and in "app" mode where it will end up as // a global. // - // This ends in a newline in case the first line is a linker @comment, which - // should be at the beginning of a line. + // This ends in a newline to make the source map easier to adjust. var header = ("__coffeescriptShare = typeof __coffeescriptShare === 'object' " + "? __coffeescriptShare : {}; " + "var share = __coffeescriptShare;\n"); @@ -140,15 +141,14 @@ var handler = function (compileStep) { ); } + var stripped = stripExportedVars(output.js, compileStep.declaredExports); + var sourceWithMap = addSharedHeader(stripped, output.v3SourceMap); + compileStep.addJavaScript({ path: outputFile, sourcePath: compileStep.inputPath, - data: output.js, - linkerFileTransform: function (sourceWithMap, exports) { - var stripped = stripExportedVars(sourceWithMap.source, exports); - return addSharedHeader(stripped, sourceWithMap.sourceMap); - }, - sourceMap: output.v3SourceMap + data: sourceWithMap.source, + sourceMap: sourceWithMap.sourceMap }); }; diff --git a/packages/ctl-helper/ctl-helper.js b/packages/ctl-helper/ctl-helper.js index 1ec6eab249..3e43e5fec3 100644 --- a/packages/ctl-helper/ctl-helper.js +++ b/packages/ctl-helper/ctl-helper.js @@ -1,7 +1,6 @@ var optimist = Npm.require('optimist'); var Future = Npm.require('fibers/future'); -// @export Ctl Ctl = {}; _.extend(Ctl, { @@ -43,7 +42,7 @@ _.extend(Ctl, { process.exit(1); } - return Meteor.connect(process.env['GALAXY']); + return DDP.connect(process.env['GALAXY']); }), jobsCollection: _.once(function () { @@ -87,7 +86,7 @@ _.extend(Ctl, { "\n" + "For now, the GALAXY environment variable must be set to the location of\n" + "your Galaxy management server (Ultraworld.) This string is in the same\n" + - "format as the argument to Meteor.connect().\n" + + "format as the argument to DDP.connect().\n" + "\n" + "Commands:\n"); _.each(Ctl.Commands, function (cmd) { diff --git a/packages/ctl-helper/package.js b/packages/ctl-helper/package.js index 59b9adfa19..52a5394237 100644 --- a/packages/ctl-helper/package.js +++ b/packages/ctl-helper/package.js @@ -6,5 +6,6 @@ Npm.depends({optimist: '0.4.0'}); Package.on_use(function (api) { api.use(['underscore', 'livedata', 'mongo-livedata'], 'server'); + api.export('Ctl', 'server'); api.add_files('ctl-helper.js', 'server'); }); diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 8fe4beebd0..dd955a5754 100644 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -10,6 +10,19 @@ Ctl.Commands.push({ } }); +var mergeObjects = function (obj1, obj2) { + var result = _.clone(obj1); + _.each(obj2, function (v, k) { + // If both objects have an object at this key, then merge those objects. + // Otherwise, choose obj2's value. + if ((v instanceof Object) && (obj1[k] instanceof Object)) + result[k] = mergeObjects(v, obj1[k]); + else + result[k] = v; + }); + return result; +}; + Ctl.Commands.push({ name: "start", @@ -63,6 +76,11 @@ Ctl.Commands.push({ } }; + // Merge in any values that might have been added to the app's config in + // the database. + if (appConfig.deployConfig) + deployConfig = mergeObjects(deployConfig, appConfig.deployConfig); + // XXX args? env? Ctl.prettyCall(Ctl.findGalaxy(), 'run', [Ctl.myAppName(), 'server', { exitPolicy: 'restart', @@ -165,7 +183,6 @@ Ctl.Commands.push({ } }); -// @export main main = function (argv) { return Ctl.main(argv); }; diff --git a/packages/ctl/package.js b/packages/ctl/package.js index b088c6884b..a8966c19cc 100644 --- a/packages/ctl/package.js +++ b/packages/ctl/package.js @@ -4,6 +4,6 @@ Package.describe({ Package.on_use(function (api) { api.use(['underscore', 'livedata', 'mongo-livedata', 'ctl-helper'], 'server'); - + api.export('main', 'server'); api.add_files('ctl.js', 'server'); }); diff --git a/packages/d3/package.js b/packages/d3/package.js index 170e60641a..eac342c7e3 100644 --- a/packages/d3/package.js +++ b/packages/d3/package.js @@ -3,6 +3,6 @@ Package.describe({ }); Package.on_use(function (api) { - api.exportSymbol('d3', 'client'); + api.export('d3', 'client'); api.add_files('d3.v3.js', 'client'); }); diff --git a/packages/deps/deprecated.js b/packages/deps/deprecated.js new file mode 100644 index 0000000000..66971a8b35 --- /dev/null +++ b/packages/deps/deprecated.js @@ -0,0 +1,18 @@ +// Deprecated (Deps-recated?) functions. + +// These functions used to be on the Meteor object (and worked slightly +// differently). +// XXX COMPAT WITH 0.5.7 +Meteor.flush = Deps.flush; +Meteor.autorun = Deps.autorun; + +// We used to require a special "autosubscribe" call to reactively subscribe to +// things. Now, it works with autorun. +// XXX COMPAT WITH 0.5.4 +Meteor.autosubscribe = Deps.autorun; + +// This Deps API briefly existed in 0.5.8 and 0.5.9 +// XXX COMPAT WITH 0.5.9 +Deps.depend = function (d) { + return d.depend(); +}; diff --git a/packages/deps/deps.js b/packages/deps/deps.js index 4387c80815..22cf6a5353 100644 --- a/packages/deps/deps.js +++ b/packages/deps/deps.js @@ -1,4 +1,3 @@ -// @export Deps Deps = {}; Deps.active = false; Deps.currentComputation = null; diff --git a/packages/deps/package.js b/packages/deps/package.js index 2e13963124..ca7d98b7be 100644 --- a/packages/deps/package.js +++ b/packages/deps/package.js @@ -5,11 +5,11 @@ Package.describe({ internal: true }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - - api.use('underscore', where); - api.add_files(['deps.js'], where); +Package.on_use(function (api) { + api.use('underscore'); + api.export('Deps'); + api.add_files('deps.js'); + api.add_files('deprecated.js'); }); Package.on_test(function (api) { diff --git a/packages/dev-bundle-fetcher/dev-bundle.js b/packages/dev-bundle-fetcher/dev-bundle.js index 3051628f10..486dadc741 100644 --- a/packages/dev-bundle-fetcher/dev-bundle.js +++ b/packages/dev-bundle-fetcher/dev-bundle.js @@ -1,4 +1,3 @@ -// @export DevBundleFetcher DevBundleFetcher = { script: function () { return Assets.getText("dev-bundle"); diff --git a/packages/dev-bundle-fetcher/package.js b/packages/dev-bundle-fetcher/package.js index 08e656ef0f..d0824bc5b9 100644 --- a/packages/dev-bundle-fetcher/package.js +++ b/packages/dev-bundle-fetcher/package.js @@ -4,5 +4,6 @@ Package.describe({ }); Package.on_use(function (api) { + api.export('DevBundleFetcher', 'server'); api.add_files(['dev-bundle', 'dev-bundle.js'], ['server']); }); diff --git a/packages/domutils/domutils.js b/packages/domutils/domutils.js index a5d417d984..e270d8f622 100644 --- a/packages/domutils/domutils.js +++ b/packages/domutils/domutils.js @@ -1,4 +1,3 @@ -// @export DomUtils DomUtils = {}; var qsaFindAllBySelector = function (selector, contextNode) { @@ -571,4 +570,4 @@ DomUtils.extractRange = function (start, end, optContainer) { else parent.removeChild(n); } -}; \ No newline at end of file +}; diff --git a/packages/domutils/package.js b/packages/domutils/package.js index f23c6253e7..86323a768a 100644 --- a/packages/domutils/package.js +++ b/packages/domutils/package.js @@ -14,6 +14,7 @@ Package.on_use(function (api) { api.use('underscore', 'client'); + api.export('DomUtils', 'client'); api.add_files('domutils.js', 'client'); }); diff --git a/packages/ejson/base64.js b/packages/ejson/base64.js index 1d4bcdef7b..795050bd43 100644 --- a/packages/ejson/base64.js +++ b/packages/ejson/base64.js @@ -8,7 +8,7 @@ for (var i = 0; i < BASE_64_CHARS.length; i++) { BASE_64_VALS[BASE_64_CHARS.charAt(i)] = i; }; -EJSON._base64Encode = function (array) { +base64Encode = function (array) { var answer = []; var a = null; var b = null; @@ -74,7 +74,7 @@ EJSON.newBinary = function (len) { return new Uint8Array(new ArrayBuffer(len)); }; -EJSON._base64Decode = function (str) { +base64Decode = function (str) { var len = Math.floor((str.length*3)/4); if (str.charAt(str.length - 1) == '=') { len--; @@ -121,3 +121,7 @@ EJSON._base64Decode = function (str) { } return arr; }; + +EJSONTest.base64Encode = base64Encode; + +EJSONTest.base64Decode = base64Decode; diff --git a/packages/ejson/base64_test.js b/packages/ejson/base64_test.js index 09401de0fb..4847c2e59e 100644 --- a/packages/ejson/base64_test.js +++ b/packages/ejson/base64_test.js @@ -24,8 +24,8 @@ Tinytest.add("base64 - testing the test", function (test) { }); Tinytest.add("base64 - empty", function (test) { - test.equal(EJSON._base64Encode(EJSON.newBinary(0)), ""); - test.equal(EJSON._base64Decode(""), EJSON.newBinary(0)); + test.equal(EJSONTest.base64Encode(EJSON.newBinary(0)), ""); + test.equal(EJSONTest.base64Decode(""), EJSON.newBinary(0)); }); @@ -38,8 +38,8 @@ Tinytest.add("base64 - wikipedia examples", function (test) { {txt: "sure.", res: "c3VyZS4="} ]; _.each(tests, function(t) { - test.equal(EJSON._base64Encode(asciiToArray(t.txt)), t.res); - test.equal(arrayToAscii(EJSON._base64Decode(t.res)), t.txt); + test.equal(EJSONTest.base64Encode(asciiToArray(t.txt)), t.res); + test.equal(arrayToAscii(EJSONTest.base64Decode(t.res)), t.txt); }); }); @@ -49,11 +49,11 @@ Tinytest.add("base64 - non-text examples", function (test) { {array: [0, 0, 1], b64: "AAAB"} ]; _.each(tests, function(t) { - test.equal(EJSON._base64Encode(t.array), t.b64); + test.equal(EJSONTest.base64Encode(t.array), t.b64); var expectedAsBinary = EJSON.newBinary(t.array.length); _.each(t.array, function (val, i) { expectedAsBinary[i] = val; }); - test.equal(EJSON._base64Decode(t.b64), expectedAsBinary); + test.equal(EJSONTest.base64Decode(t.b64), expectedAsBinary); }); }); diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 4b4a53cb12..3bce3f802f 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -1,5 +1,5 @@ -// @export EJSON EJSON = {}; +EJSONTest = {}; var customTypes = {}; // Add a custom type, using a method of your choice to get to and @@ -11,6 +11,7 @@ var customTypes = {}; // - A toJSONValue() method, so that Meteor can serialize it // - a typeName() method, to show how to look it up in our type table. // It is okay if these methods are monkey-patched on. +// EJSON.addType = function (name, factory) { if (_.has(customTypes, name)) throw new Error("Type " + name + " already present"); @@ -41,10 +42,10 @@ var builtinConverters = [ || (obj && _.has(obj, '$Uint8ArrayPolyfill')); }, toJSONValue: function (obj) { - return {$binary: EJSON._base64Encode(obj)}; + return {$binary: base64Encode(obj)}; }, fromJSONValue: function (obj) { - return EJSON._base64Decode(obj.$binary); + return base64Decode(obj.$binary); } }, { // Escaping one level @@ -100,7 +101,7 @@ EJSON._isCustomType = function (obj) { }; -//for both arrays and objects, in-place modification. +// for both arrays and objects, in-place modification. var adjustTypesToJSONValue = EJSON._adjustTypesToJSONValue = function (obj) { if (obj === null) @@ -146,9 +147,10 @@ EJSON.toJSONValue = function (item) { return item; }; -//for both arrays and objects. Tries its best to just +// for both arrays and objects. Tries its best to just // use the object you hand it, but may return something // different if the object you hand it itself needs changing. +// var adjustTypesFromJSONValue = EJSON._adjustTypesFromJSONValue = function (obj) { if (obj === null) diff --git a/packages/ejson/package.js b/packages/ejson/package.js index e8381d0d58..f3b93dbc2b 100644 --- a/packages/ejson/package.js +++ b/packages/ejson/package.js @@ -5,6 +5,8 @@ Package.describe({ Package.on_use(function (api) { api.use(['json', 'underscore']); + api.export('EJSON'); + api.export('EJSONTest', {testOnly: true}); api.add_files('ejson.js', ['client', 'server']); api.add_files('base64.js', ['client', 'server']); }); diff --git a/packages/email/email.js b/packages/email/email.js index 3072dcad00..3efe822340 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -1,10 +1,10 @@ -// @export Email -Email = {}; - var Future = Npm.require('fibers/future'); var urlModule = Npm.require('url'); var MailComposer = Npm.require('mailcomposer').MailComposer; +Email = {}; +EmailTest = {}; + var makePool = function (mailUrlString) { var mailUrl = urlModule.parse(mailUrlString); if (mailUrl.protocol !== 'smtp:') @@ -45,18 +45,26 @@ var maybeMakePool = function () { } }; -Email._next_devmode_mail_id = 0; +var next_devmode_mail_id = 0; +var output_stream = process.stdout; -// Overridden by tests. -Email._output_stream = process.stdout; +// Testing hooks +EmailTest.overrideOutputStream = function (stream) { + next_devmode_mail_id = 0; + output_stream = stream; +}; + +EmailTest.restoreOutputStream = function () { + output_stream = process.stdout; +}; var devModeSend = function (mc) { - var devmode_mail_id = Email._next_devmode_mail_id++; + var devmode_mail_id = next_devmode_mail_id++; // Make sure we use whatever stream was set at the time of the Email.send // call even in the 'end' callback, in case there are multiple concurrent // test runs. - var stream = Email._output_stream; + var stream = output_stream; // This approach does not prevent other writers to stdout from interleaving. stream.write("====== BEGIN MAIL #" + devmode_mail_id + " ======\n"); @@ -74,6 +82,18 @@ var smtpSend = function (mc) { smtpPool._future_wrapped_sendMail(mc).wait(); }; +/** + * Mock out email sending (eg, during a test.) This is private for now. + * + * f receives the arguments to Email.send and should return true to go + * ahead and send the email (or at least, try subsequent hooks), or + * false to skip sending. + */ +var sendHooks = []; +EmailTest.hookSend = function (f) { + sendHooks.push(f); +}; + /** * Send an email. * @@ -94,6 +114,10 @@ var smtpSend = function (mc) { * @param options.headers {Object} custom RFC5322 headers (dictionary) */ Email.send = function (options) { + for (var i = 0; i < sendHooks.length; i++) + if (! sendHooks[i](options)) + return; + var mc = new MailComposer(); // setup message data @@ -122,3 +146,4 @@ Email.send = function (options) { devModeSend(mc); } }; + diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index f8cdcd4610..1c367b2178 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -4,11 +4,9 @@ Tinytest.add("email - dev mode smoke test", function (test) { // This only tests dev mode, so don't run the test if this is deployed. if (process.env.MAIL_URL) return; - var old_stream = Email._output_stream; try { var stream = new streamBuffers.WritableStreamBuffer; - Email._output_stream = stream; - Email._next_devmode_mail_id = 0; + EmailTest.overrideOutputStream(stream); Email.send({ from: "foo@example.com", to: "bar@example.com", @@ -21,7 +19,7 @@ Tinytest.add("email - dev mode smoke test", function (test) { // in case a concurrent test run mutates Email._output_stream too. // XXX brittle if mailcomposer changes header order, etc test.equal(stream.getContentsAsString("utf8"), - "====== BEGIN MAIL #0 ======\n" + + "====== BEGIN MAIL #0 ======\n" + "MIME-Version: 1.0\r\n" + "X-Meteor-Test: a custom header\r\n" + "From: foo@example.com\r\n" + @@ -36,6 +34,6 @@ Tinytest.add("email - dev mode smoke test", function (test) { "From us.\r\n" + "====== END MAIL #0 ======\n"); } finally { - Email._output_stream = old_stream; + EmailTest.restoreOutputStream(); } }); diff --git a/packages/email/package.js b/packages/email/package.js index ae6d05fbdc..e1f2ee0d49 100644 --- a/packages/email/package.js +++ b/packages/email/package.js @@ -8,6 +8,8 @@ Npm.depends({mailcomposer: "0.1.15", simplesmtp: "0.1.25", "stream-buffers": "0. Package.on_use(function (api) { api.use('underscore', 'server'); + api.export('Email', 'server'); + api.export('EmailTest', 'server', {testOnly: true}); api.add_files('email.js', 'server'); }); diff --git a/packages/facebook/facebook_client.js b/packages/facebook/facebook_client.js index d9c2511cde..cfbc06fed2 100644 --- a/packages/facebook/facebook_client.js +++ b/packages/facebook/facebook_client.js @@ -1,4 +1,7 @@ +Facebook = {}; + // Request Facebook credentials for the user +// // @param options {optional} // @param credentialRequestCompleteCallback {Function} Callback function to call on // completion. Takes one argument, credentialToken on success, or Error on diff --git a/packages/facebook/facebook_common.js b/packages/facebook/facebook_common.js deleted file mode 100644 index a062b4555b..0000000000 --- a/packages/facebook/facebook_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Facebook -Facebook = {}; diff --git a/packages/facebook/facebook_server.js b/packages/facebook/facebook_server.js index 59ee6cc729..088f7eaf85 100644 --- a/packages/facebook/facebook_server.js +++ b/packages/facebook/facebook_server.js @@ -1,3 +1,5 @@ +Facebook = {}; + var querystring = Npm.require('querystring'); @@ -47,7 +49,7 @@ var getTokenResponse = function (query) { var responseContent; try { // Request an access token - responseContent = Meteor.http.get( + responseContent = HTTP.get( "https://graph.facebook.com/oauth/access_token", { params: { client_id: config.appId, @@ -84,7 +86,7 @@ var getTokenResponse = function (query) { var getIdentity = function (accessToken) { try { - return Meteor.http.get("https://graph.facebook.com/me", { + 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); diff --git a/packages/facebook/package.js b/packages/facebook/package.js index b8a1f5d8d0..534ed18a28 100644 --- a/packages/facebook/package.js +++ b/packages/facebook/package.js @@ -8,17 +8,18 @@ Package.describe({ Package.on_use(function(api) { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.use('http', ['client', 'server']); + api.use('http', ['server']); api.use('templating', 'client'); api.use('underscore', 'server'); api.use('random', 'client'); api.use('service-configuration', ['client', 'server']); + api.export('Facebook'); + api.add_files( ['facebook_configure.html', 'facebook_configure.js'], 'client'); - api.add_files('facebook_common.js', ['client', 'server']); api.add_files('facebook_server.js', 'server'); api.add_files('facebook_client.js', 'client'); }); diff --git a/packages/force-ssl/package.js b/packages/force-ssl/package.js index 2baeb65573..11eddfc516 100644 --- a/packages/force-ssl/package.js +++ b/packages/force-ssl/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function (api) { api.use('webapp', 'server'); - api.use('underscore', 'server'); + api.use('underscore'); // make sure we come after livedata, so we load after the sockjs // server has been instantiated. api.use('livedata', 'server'); diff --git a/packages/github/github_client.js b/packages/github/github_client.js index 8a74721272..4bf7a27079 100644 --- a/packages/github/github_client.js +++ b/packages/github/github_client.js @@ -1,3 +1,5 @@ +Github = {}; + // Request Github credentials for the user // @param options {optional} // @param credentialRequestCompleteCallback {Function} Callback function to call on diff --git a/packages/github/github_common.js b/packages/github/github_common.js deleted file mode 100644 index c00d6ea155..0000000000 --- a/packages/github/github_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Github -Github = {}; diff --git a/packages/github/github_server.js b/packages/github/github_server.js index 00841cedf8..669b51dd42 100644 --- a/packages/github/github_server.js +++ b/packages/github/github_server.js @@ -1,3 +1,5 @@ +Github = {}; + Oauth.registerService('github', 2, null, function(query) { var accessToken = getAccessToken(query); @@ -26,7 +28,7 @@ var getAccessToken = function (query) { var response; try { - response = Meteor.http.post( + response = HTTP.post( "https://github.com/login/oauth/access_token", { headers: { Accept: 'application/json', @@ -52,7 +54,7 @@ var getAccessToken = function (query) { var getIdentity = function (accessToken) { try { - return Meteor.http.get( + return HTTP.get( "https://api.github.com/user", { headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required params: {access_token: accessToken} @@ -62,6 +64,7 @@ var getIdentity = function (accessToken) { } }; + Github.retrieveCredential = function(credentialToken) { return Oauth.retrieveCredential(credentialToken); }; diff --git a/packages/github/package.js b/packages/github/package.js index 20bf71d284..c8b6ac92aa 100644 --- a/packages/github/package.js +++ b/packages/github/package.js @@ -8,17 +8,18 @@ Package.describe({ Package.on_use(function(api) { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.use('http', ['client', 'server']); + api.use('http', ['server']); api.use('underscore', 'client'); api.use('templating', 'client'); api.use('random', 'client'); api.use('service-configuration', ['client', 'server']); + api.export('Github'); + api.add_files( ['github_configure.html', 'github_configure.js'], 'client'); - api.add_files('github_common.js', ['client', 'server']); api.add_files('github_server.js', 'server'); api.add_files('github_client.js', 'client'); }); diff --git a/packages/google/google_client.js b/packages/google/google_client.js index 83b8c189cd..74c2a0c809 100644 --- a/packages/google/google_client.js +++ b/packages/google/google_client.js @@ -1,3 +1,5 @@ +Google = {}; + // Request Google credentials for the user // @param options {optional} // @param credentialRequestCompleteCallback {Function} Callback function to call on @@ -30,6 +32,7 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) // https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl var accessType = options.requestOfflineToken ? 'offline' : 'online'; + var approvalPrompt = options.forceApprovalPrompt ? 'force' : 'auto'; var loginUrl = 'https://accounts.google.com/o/oauth2/auth' + @@ -38,7 +41,8 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) '&scope=' + flatScope + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + '&state=' + credentialToken + - '&access_type=' + accessType; + '&access_type=' + accessType + + '&approval_prompt=' + approvalPrompt; Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback); }; diff --git a/packages/google/google_common.js b/packages/google/google_common.js deleted file mode 100644 index ccc1d27448..0000000000 --- a/packages/google/google_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Google -Google = {}; diff --git a/packages/google/google_server.js b/packages/google/google_server.js index f6f21da9b4..eba4263e5b 100644 --- a/packages/google/google_server.js +++ b/packages/google/google_server.js @@ -1,3 +1,5 @@ +Google = {}; + // https://developers.google.com/accounts/docs/OAuth2Login#userinfocall Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name', 'family_name', 'picture', 'locale', 'timezone', 'gender']; @@ -40,7 +42,7 @@ var getTokens = function (query) { var response; try { - response = Meteor.http.post( + response = HTTP.post( "https://accounts.google.com/o/oauth2/token", {params: { code: query.code, client_id: config.clientId, @@ -65,7 +67,7 @@ var getTokens = function (query) { var getIdentity = function (accessToken) { try { - return Meteor.http.get( + return HTTP.get( "https://www.googleapis.com/oauth2/v1/userinfo", {params: {access_token: accessToken}}).data; } catch (err) { @@ -73,6 +75,7 @@ var getIdentity = function (accessToken) { } }; + Google.retrieveCredential = function(credentialToken) { return Oauth.retrieveCredential(credentialToken); -}; \ No newline at end of file +}; diff --git a/packages/google/package.js b/packages/google/package.js index 4ef3c79683..8bd523c2a2 100644 --- a/packages/google/package.js +++ b/packages/google/package.js @@ -8,15 +8,16 @@ Package.describe({ Package.on_use(function(api) { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.use('http', ['client', 'server']); + api.use('http', ['server']); api.use(['underscore', 'service-configuration'], ['client', 'server']); api.use(['random', 'templating'], 'client'); + api.export('Google'); + api.add_files( ['google_configure.html', 'google_configure.js'], 'client'); - api.add_files('google_common.js', ['client', 'server']); api.add_files('google_server.js', 'server'); api.add_files('google_client.js', 'client'); }); diff --git a/packages/handlebars/evaluate-handlebars.js b/packages/handlebars/evaluate-handlebars.js index 8aa4bf6bfe..fa3df9bcd2 100644 --- a/packages/handlebars/evaluate-handlebars.js +++ b/packages/handlebars/evaluate-handlebars.js @@ -1,4 +1,3 @@ -// @export Handlebars Handlebars = {}; // XXX we probably forgot to implement the #foo case where foo is not diff --git a/packages/handlebars/package.js b/packages/handlebars/package.js index f3a3b08d34..90e3631cce 100644 --- a/packages/handlebars/package.js +++ b/packages/handlebars/package.js @@ -9,6 +9,8 @@ Package.on_use(function (api) { api.use('underscore'); api.use('spark', 'client'); + api.export('Handlebars'); + // XXX these should be split up into two different slices, not // different code with totally different APIs that is sent depending // on the architecture diff --git a/packages/handlebars/parse-handlebars.js b/packages/handlebars/parse-handlebars.js index 2fdd132112..90e5319bb3 100644 --- a/packages/handlebars/parse-handlebars.js +++ b/packages/handlebars/parse-handlebars.js @@ -1,4 +1,3 @@ -// @export Handlebars Handlebars = {}; /* Our format: diff --git a/packages/html5-tokenizer/html5_tokenizer.js b/packages/html5-tokenizer/html5_tokenizer.js index ffdd9b5ad4..b63944d6df 100644 --- a/packages/html5-tokenizer/html5_tokenizer.js +++ b/packages/html5-tokenizer/html5_tokenizer.js @@ -1,4 +1,3 @@ -// @export HTML5Tokenizer HTML5Tokenizer = { tokenize: function (inputString) { var tokens = []; diff --git a/packages/html5-tokenizer/package.js b/packages/html5-tokenizer/package.js index e2584f6f65..bf2aa067d4 100644 --- a/packages/html5-tokenizer/package.js +++ b/packages/html5-tokenizer/package.js @@ -2,12 +2,11 @@ Package.describe({ summary: "HTML5 tokenizer" }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - +Package.on_use(function (api) { + api.export('HTML5Tokenizer'); api.add_files(['entities.js', 'constants.js', 'buffer.js', 'events.js', 'tokenizer.js', - 'html5_tokenizer.js'], where); + 'html5_tokenizer.js']); }); Package.on_test(function (api) { diff --git a/packages/htmljs/html.js b/packages/htmljs/html.js index 231f295315..aea248fa87 100644 --- a/packages/htmljs/html.js +++ b/packages/htmljs/html.js @@ -69,66 +69,56 @@ var tag_names = 'P PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG SUB SUP ' + 'TABLE TBODY TD TEXTAREA TFOOT TH THEAD TR TT U UL VAR').split(' '); -// @export A, ABBR, ACRONYM, B, BDO, BIG, BLOCKQUOTE, BR, BUTTON, CAPTION -// @export CITE, CODE, COL, COLGROUP, DD, DEL, DFN, DIV, DL, DT, EM, FIELDSET -// @export FORM, H1, H2, H3, H4, H5, H6, HR, I, IFRAME, IMG, INPUT, INS, KBD -// @export LABEL, LEGEND, LI, OBJECT, OL, OPTGROUP, OPTION, P, PARAM, PRE, Q -// @export S, SAMP, SCRIPT, SELECT, SMALL, SPAN, STRIKE, STRONG, SUB, SUP -// @export TABLE, TBODY, TD, TEXTAREA, TFOOT, TH, THEAD, TR, TT, U, UL, VAR - -for (var i = 0; i < tag_names.length; i++) { - var tag = tag_names[i]; - - // 'this' will end up being the global object (eg, 'window' on the client) - this[tag] = (function (tag) { - return function (arg1, arg2) { - var attrs, contents; - if (arg2) { - attrs = arg1; - contents = arg2; +_.each(tag_names, function (tag) { + var f = function (arg1, arg2) { + var attrs, contents; + if (arg2) { + attrs = arg1; + contents = arg2; + } else { + if (arg1 instanceof Array) { + attrs = {}; + contents = arg1; } else { - if (arg1 instanceof Array) { - attrs = {}; - contents = arg1; - } else { - attrs = arg1; - contents = []; - } + attrs = arg1; + contents = []; } - var elt = document.createElement(tag); - for (var a in attrs) { - if (a === 'cls') - elt.setAttribute('class', attrs[a]); - else if (a === '_for') - elt.setAttribute('for', attrs[a]); - else if (a === 'style' && ! styleGetSetSupport) - elt.style.cssText = String(attrs[a]); - else if (event_names[a]) { - if (typeof $ === "undefined") - throw new Error("Event binding is supported only if " + - "jQuery or similar is available"); - ($(elt)[a])(attrs[a]); - } + } + var elt = document.createElement(tag); + for (var a in attrs) { + if (a === 'cls') + elt.setAttribute('class', attrs[a]); + else if (a === '_for') + elt.setAttribute('for', attrs[a]); + else if (a === 'style' && ! styleGetSetSupport) + elt.style.cssText = String(attrs[a]); + else if (event_names[a]) { + if (typeof $ === "undefined") + throw new Error("Event binding is supported only if " + + "jQuery or similar is available"); + ($(elt)[a])(attrs[a]); + } + else + elt.setAttribute(a, attrs[a]); + } + var addChildren = function (children) { + for (var i = 0; i < children.length; i++) { + var c = children[i]; + if (!c && c !== '') + throw new Error("Bad value for element body: " + c); + else if (c instanceof Array) + addChildren(c); + else if (typeof c === "string") + elt.appendChild(document.createTextNode(c)); + else if ('element' in c) + addChildren([c.element]); else - elt.setAttribute(a, attrs[a]); - } - var addChildren = function (children) { - for (var i = 0; i < children.length; i++) { - var c = children[i]; - if (!c && c !== '') - throw new Error("Bad value for element body: " + c); - else if (c instanceof Array) - addChildren(c); - else if (typeof c === "string") - elt.appendChild(document.createTextNode(c)); - else if ('element' in c) - addChildren([c.element]); - else - elt.appendChild(c); - }; + elt.appendChild(c); }; - addChildren(contents); - return elt; }; - })(tag); -}; + addChildren(contents); + return elt; + }; + // Put the function onto the package-scope variable with this name. + eval(tag + " = f;"); +}); diff --git a/packages/htmljs/htmljs_test.js b/packages/htmljs/htmljs_test.js index 32c6a23f40..c9ded674f7 100644 --- a/packages/htmljs/htmljs_test.js +++ b/packages/htmljs/htmljs_test.js @@ -1,6 +1,5 @@ Tinytest.add("htmljs", function (test) { - // Make sure "style" works, which has to be special-cased for IE. test.equal(DIV({style:"display:none"}).style.display, "none"); -}); \ No newline at end of file +}); diff --git a/packages/htmljs/package.js b/packages/htmljs/package.js index 9bf6b9ac14..a0cdb22fe2 100644 --- a/packages/htmljs/package.js +++ b/packages/htmljs/package.js @@ -3,8 +3,19 @@ Package.describe({ }); Package.on_use(function (api) { + api.use('underscore', 'client'); // Note: html.js will optionally use jquery if it's available api.add_files('html.js', 'client'); + api.export([ + 'A', 'ABBR', 'ACRONYM', 'B', 'BDO', 'BIG', 'BLOCKQUOTE', 'BR', 'BUTTON', + 'CAPTION', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD', 'DEL', 'DFN', 'DIV', + 'DL', 'DT', 'EM', 'FIELDSET', 'FORM', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', + 'HR', 'I', 'IFRAME', 'IMG', 'INPUT', 'INS', 'KBD', 'LABEL', 'LEGEND', 'LI', + 'OBJECT', 'OL', 'OPTGROUP', 'OPTION', 'P', 'PARAM', 'PRE', 'Q', 'S', 'SAMP', + 'SCRIPT', 'SELECT', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'SUB', 'SUP', + 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TR', 'TT', 'U', + 'UL', 'VAR' + ], 'client'); }); Package.on_test(function (api) { diff --git a/packages/http/deprecated.js b/packages/http/deprecated.js new file mode 100644 index 0000000000..d49438ab9f --- /dev/null +++ b/packages/http/deprecated.js @@ -0,0 +1,3 @@ +// The HTTP object used to be called Meteor.http. +// XXX COMPAT WITH 0.6.4 +Meteor.http = HTTP; diff --git a/packages/http/httpcall_client.js b/packages/http/httpcall_client.js index 96c1e8c0bb..7c33ca2a09 100644 --- a/packages/http/httpcall_client.js +++ b/packages/http/httpcall_client.js @@ -1,6 +1,4 @@ -Meteor.http = Meteor.http || {}; - -Meteor.http.call = function(method, url, options, callback) { +HTTP.call = function(method, url, options, callback) { ////////// Process arguments ////////// @@ -33,9 +31,8 @@ Meteor.http.call = function(method, url, options, callback) { params_for_body = options.params; var query_match = /^(.*?)(\?.*)?$/.exec(url); - url = Meteor.http._buildUrl(query_match[1], query_match[2], - options.query, params_for_url); - + url = buildUrl(query_match[1], query_match[2], + options.query, params_for_url); if (options.followRedirects === false) throw new Error("Option followRedirects:false not supported on client."); @@ -50,7 +47,7 @@ Meteor.http.call = function(method, url, options, callback) { } if (params_for_body) { - content = Meteor.http._encodeParams(params_for_body); + content = encodeParams(params_for_body); } _.extend(headers, options.headers || {}); @@ -146,11 +143,11 @@ Meteor.http.call = function(method, url, options, callback) { response.headers[m[1].toLowerCase()] = m[2]; }); - Meteor.http._populateData(response); + populateData(response); var error = null; if (response.statusCode >= 400) - error = Meteor.http._makeErrorByStatus(response.statusCode, response.content); + error = makeErrorByStatus(response.statusCode, response.content); callback(error, response); } diff --git a/packages/http/httpcall_common.js b/packages/http/httpcall_common.js index 11bb1cfc75..a2fe4bdd2b 100644 --- a/packages/http/httpcall_common.js +++ b/packages/http/httpcall_common.js @@ -1,7 +1,4 @@ - -Meteor.http = Meteor.http || {}; - -Meteor.http._makeErrorByStatus = function(statusCode, content) { +makeErrorByStatus = function(statusCode, content) { var MAX_LENGTH = 160; // if you change this, also change the appropriate test var truncate = function(str, length) { @@ -15,22 +12,21 @@ Meteor.http._makeErrorByStatus = function(statusCode, content) { return new Error(message); }; -Meteor.http._encodeParams = function(params) { +encodeParams = function(params) { var buf = []; _.each(params, function(value, key) { if (buf.length) buf.push('&'); - buf.push(Meteor.http._encodeString(key), '=', - Meteor.http._encodeString(value)); + buf.push(encodeString(key), '=', encodeString(value)); }); return buf.join('').replace(/%20/g, '+'); }; -Meteor.http._encodeString = function(str) { +encodeString = function(str) { return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); }; -Meteor.http._buildUrl = function(before_qmark, from_qmark, opt_query, opt_params) { +buildUrl = function(before_qmark, from_qmark, opt_query, opt_params) { var url_without_query = before_qmark; var query = from_qmark ? from_qmark.slice(1) : null; @@ -39,7 +35,7 @@ Meteor.http._buildUrl = function(before_qmark, from_qmark, opt_query, opt_params if (opt_params) { query = query || ""; - var prms = Meteor.http._encodeParams(opt_params); + var prms = encodeParams(opt_params); if (query && prms) query += '&'; query += prms; @@ -53,7 +49,7 @@ Meteor.http._buildUrl = function(before_qmark, from_qmark, opt_query, opt_params }; // Fill in `response.data` if the content-type is JSON. -Meteor.http._populateData = function(response) { +populateData = function(response) { // Read Content-Type header, up to a ';' if there is one. // A typical header might be "application/json; charset=utf-8" // or just "application/json". @@ -71,15 +67,20 @@ Meteor.http._populateData = function(response) { } }; -Meteor.http.get = function (/* varargs */) { - return Meteor.http.call.apply(this, ["GET"].concat(_.toArray(arguments))); +HTTP = {}; + +HTTP.get = function (/* varargs */) { + return HTTP.call.apply(this, ["GET"].concat(_.toArray(arguments))); }; -Meteor.http.post = function (/* varargs */) { - return Meteor.http.call.apply(this, ["POST"].concat(_.toArray(arguments))); + +HTTP.post = function (/* varargs */) { + return HTTP.call.apply(this, ["POST"].concat(_.toArray(arguments))); }; -Meteor.http.put = function (/* varargs */) { - return Meteor.http.call.apply(this, ["PUT"].concat(_.toArray(arguments))); + +HTTP.put = function (/* varargs */) { + return HTTP.call.apply(this, ["PUT"].concat(_.toArray(arguments))); }; -Meteor.http.del = function (/* varargs */) { - return Meteor.http.call.apply(this, ["DELETE"].concat(_.toArray(arguments))); + +HTTP.del = function (/* varargs */) { + return HTTP.call.apply(this, ["DELETE"].concat(_.toArray(arguments))); }; diff --git a/packages/http/httpcall_server.js b/packages/http/httpcall_server.js index 51c8d16502..212508367f 100644 --- a/packages/http/httpcall_server.js +++ b/packages/http/httpcall_server.js @@ -1,12 +1,10 @@ -Meteor.http = Meteor.http || {}; - var path = Npm.require('path'); var request = Npm.require('request'); var url_util = Npm.require('url'); -// Meteor.http._call always runs asynchronously; Meteor.http.call, defined -// below, wraps _call and runs synchronously when no callback is provided. -Meteor.http._call = function(method, url, options, callback) { +// _call always runs asynchronously; HTTP.call, defined below, +// wraps _call and runs synchronously when no callback is provided. +var _call = function(method, url, options, callback) { ////////// Process arguments ////////// @@ -40,8 +38,8 @@ Meteor.http._call = function(method, url, options, callback) { else params_for_body = options.params; - var new_url = Meteor.http._buildUrl( - url_parts.protocol+"//"+url_parts.host+url_parts.pathname, + var new_url = buildUrl( + url_parts.protocol + "//" + url_parts.host + url_parts.pathname, url_parts.search, options.query, params_for_url); if (options.auth) { @@ -52,7 +50,7 @@ Meteor.http._call = function(method, url, options, callback) { } if (params_for_body) { - content = Meteor.http._encodeParams(params_for_body); + content = encodeParams(params_for_body); headers['Content-Type'] = "application/x-www-form-urlencoded"; } @@ -95,10 +93,10 @@ Meteor.http._call = function(method, url, options, callback) { response.content = body; response.headers = res.headers; - Meteor.http._populateData(response); + populateData(response); if (response.statusCode >= 400) - error = Meteor.http._makeErrorByStatus(response.statusCode, response.content); + error = makeErrorByStatus(response.statusCode, response.content); } callback(error, response); @@ -106,4 +104,4 @@ Meteor.http._call = function(method, url, options, callback) { }); }; -Meteor.http.call = Meteor._wrapAsync(Meteor.http._call); +HTTP.call = Meteor._wrapAsync(_call); diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js index fddf48b032..13fd694c1c 100644 --- a/packages/http/httpcall_tests.js +++ b/packages/http/httpcall_tests.js @@ -41,12 +41,12 @@ testAsyncMulti("httpcall - basic", [ }; - Meteor.http.call("GET", url_prefix()+url, options, expect(callback)); + HTTP.call("GET", url_prefix()+url, options, expect(callback)); if (Meteor.isServer) { // test sync version try { - var result = Meteor.http.call("GET", url_prefix()+url, options); + var result = HTTP.call("GET", url_prefix()+url, options); callback(undefined, result); } catch (e) { callback(e, e.response); @@ -89,12 +89,12 @@ testAsyncMulti("httpcall - errors", [ test.isFalse(result); test.isFalse(error.response); }; - Meteor.http.call("GET", "http://asfd.asfd/", expect(unknownServerCallback)); + HTTP.call("GET", "http://asfd.asfd/", expect(unknownServerCallback)); if (Meteor.isServer) { // test sync version try { - var unknownServerResult = Meteor.http.call("GET", "http://asfd.asfd/"); + var unknownServerResult = HTTP.call("GET", "http://asfd.asfd/"); unknownServerCallback(undefined, unknownServerResult); } catch (e) { unknownServerCallback(e, e.response); @@ -119,12 +119,12 @@ testAsyncMulti("httpcall - errors", [ test.isTrue(error.response.content.length > 180); test.isTrue(error.message.length < 180); // make sure we truncate. }; - Meteor.http.call("GET", url_prefix()+"/fail", expect(error500Callback)); + HTTP.call("GET", url_prefix()+"/fail", expect(error500Callback)); if (Meteor.isServer) { // test sync version try { - var error500Result = Meteor.http.call("GET", url_prefix()+"/fail"); + var error500Result = HTTP.call("GET", url_prefix()+"/fail"); error500Callback(undefined, error500Result); } catch (e) { error500Callback(e, e.response); @@ -143,7 +143,7 @@ testAsyncMulti("httpcall - timeout", [ test.isFalse(error.response); }; var timeoutUrl = url_prefix()+"/slow-"+Random.id(); - Meteor.http.call( + HTTP.call( "GET", timeoutUrl, { timeout: 500 }, expect(timeoutCallback)); @@ -151,7 +151,7 @@ testAsyncMulti("httpcall - timeout", [ if (Meteor.isServer) { // test sync version try { - var timeoutResult = Meteor.http.call("GET", timeoutUrl, { timeout: 500 }); + var timeoutResult = HTTP.call("GET", timeoutUrl, { timeout: 500 }); timeoutCallback(undefined, timeoutResult); } catch (e) { timeoutCallback(e, e.response); @@ -168,7 +168,7 @@ testAsyncMulti("httpcall - timeout", [ test.equal(data.method, "GET"); }; var noTimeoutUrl = url_prefix()+"/foo-"+Random.id(); - Meteor.http.call( + HTTP.call( "GET", noTimeoutUrl, { timeout: 2000 }, expect(noTimeoutCallback)); @@ -176,7 +176,7 @@ testAsyncMulti("httpcall - timeout", [ if (Meteor.isServer) { // test sync version try { - var noTimeoutResult = Meteor.http.call("GET", noTimeoutUrl, { timeout: 2000 }); + var noTimeoutResult = HTTP.call("GET", noTimeoutUrl, { timeout: 2000 }); noTimeoutCallback(undefined, noTimeoutResult); } catch (e) { noTimeoutCallback(e, e.response); @@ -189,7 +189,7 @@ testAsyncMulti("httpcall - redirect", [ function(test, expect) { // Test that we follow redirects by default - Meteor.http.call("GET", url_prefix()+"/redirect", expect( + HTTP.call("GET", url_prefix()+"/redirect", expect( function(error, result) { test.isFalse(error); test.isTrue(result); @@ -205,7 +205,7 @@ testAsyncMulti("httpcall - redirect", [ _.each([false, true], function(followRedirects) { var do_it = function(should_work) { var maybe_expect = should_work ? expect : _.identity; - Meteor.http.call( + HTTP.call( "GET", url_prefix()+"/redirect", {followRedirects: followRedirects}, maybe_expect(function(error, result) { @@ -241,7 +241,7 @@ testAsyncMulti("httpcall - methods", [ // non-get methods var test_method = function(meth, func_name) { func_name = func_name || meth.toLowerCase(); - Meteor.http[func_name]( + HTTP[func_name]( url_prefix()+"/foo", expect(function(error, result) { test.isFalse(error); @@ -266,7 +266,7 @@ testAsyncMulti("httpcall - methods", [ function(test, expect) { // contents and data - Meteor.http.call( + HTTP.call( "POST", url_prefix()+"/foo", { content: "Hello World!" }, expect(function(error, result) { @@ -277,7 +277,7 @@ testAsyncMulti("httpcall - methods", [ test.equal(data.body, "Hello World!"); })); - Meteor.http.call( + HTTP.call( "POST", url_prefix()+"/data-test", { data: {greeting: "Hello World!"} }, expect(function(error, result) { @@ -290,7 +290,7 @@ testAsyncMulti("httpcall - methods", [ test.matches(data.headers['content-type'], /^application\/json\b/); })); - Meteor.http.call( + HTTP.call( "POST", url_prefix()+"/data-test-explicit", { data: {greeting: "Hello World!"}, headers: {'Content-Type': 'text/stupid'} }, @@ -319,7 +319,7 @@ testAsyncMulti("httpcall - http auth", [ // https://bugzilla.mozilla.org/show_bug.cgi?id=654348 var password = 'rocks'; //var password = Random.id().replace(/[^0-9a-zA-Z]/g, ''); - Meteor.http.call( + HTTP.call( "GET", url_prefix()+"/login?"+password, { auth: "meteor:"+password }, expect(function(error, result) { @@ -333,7 +333,7 @@ testAsyncMulti("httpcall - http auth", [ // test fail on malformed username:password test.throws(function() { - Meteor.http.call( + HTTP.call( "GET", url_prefix()+"/login?"+password, { auth: "fooooo" }, function() { throw new Error("can't get here"); }); @@ -343,7 +343,7 @@ testAsyncMulti("httpcall - http auth", [ testAsyncMulti("httpcall - headers", [ function(test, expect) { - Meteor.http.call( + HTTP.call( "GET", url_prefix()+"/foo-with-headers", {headers: { "Test-header": "Value", "another": "Value2" } }, @@ -359,7 +359,7 @@ testAsyncMulti("httpcall - headers", [ test.equal(data.headers['another'], "Value2"); })); - Meteor.http.call( + HTTP.call( "GET", url_prefix()+"/headers", expect(function(error, result) { test.isFalse(error); @@ -383,7 +383,7 @@ testAsyncMulti("httpcall - params", [ } else { opts = opt_opts; } - Meteor.http.call( + HTTP.call( method, url_prefix()+url, _.extend({ params: params }, opts), expect(function(error, result) { @@ -431,7 +431,7 @@ if (Meteor.isServer) { WebApp.suppressConnectErrors(); var do_test = function (path, code, match) { - Meteor.http.get( + HTTP.get( url_base() + path, {headers: {'x-suppress-error': 'true'}}, expect(function(error, result) { diff --git a/packages/http/package.js b/packages/http/package.js index 4e35051dee..f70dd7a6b3 100644 --- a/packages/http/package.js +++ b/packages/http/package.js @@ -4,9 +4,11 @@ Package.describe({ Package.on_use(function (api) { api.use('underscore'); + api.export('HTTP'); api.add_files('httpcall_common.js', ['client', 'server']); api.add_files('httpcall_client.js', 'client'); api.add_files('httpcall_server.js', 'server'); + api.add_files('deprecated.js', ['client', 'server']); }); Package.on_test(function (api) { diff --git a/packages/insecure/insecure.js b/packages/insecure/insecure.js deleted file mode 100644 index 22a74ca954..0000000000 --- a/packages/insecure/insecure.js +++ /dev/null @@ -1 +0,0 @@ -Meteor.Collection.insecure = true; diff --git a/packages/insecure/package.js b/packages/insecure/package.js index fe2e744a38..851536e071 100644 --- a/packages/insecure/package.js +++ b/packages/insecure/package.js @@ -2,7 +2,4 @@ Package.describe({ summary: "Allow all database writes by default" }); -Package.on_use(function (api) { - api.use(['mongo-livedata']); - api.add_files(['insecure.js'], 'server'); -}); +// This package is empty; its presence is detected by mongo-livedata. diff --git a/packages/jquery/package.js b/packages/jquery/package.js index 14b8d7033b..5439d096cf 100644 --- a/packages/jquery/package.js +++ b/packages/jquery/package.js @@ -2,9 +2,9 @@ Package.describe({ summary: "Manipulate the DOM using CSS selectors" }); -Package.on_use(function (api, where) { - api.add_files('jquery.js', 'client'); +Package.on_use(function (api) { + api.add_files(['jquery.js', 'post.js'], 'client'); - api.exportSymbol('$', where); - api.exportSymbol('jQuery', where); + api.export('$', 'client'); + api.export('jQuery', 'client'); }); diff --git a/packages/jquery/post.js b/packages/jquery/post.js new file mode 100644 index 0000000000..c784a3799b --- /dev/null +++ b/packages/jquery/post.js @@ -0,0 +1,4 @@ +// Put jQuery and $ in our exported package-scope variables and remove window.$. +// (Sadly, we don't call noConflict(true), which would also remove +// window.jQuery, because bootstrap very specifically relies on window.jQuery.) +$ = jQuery = window.jQuery.noConflict(); diff --git a/packages/js-analyze/js_analyze.js b/packages/js-analyze/js_analyze.js index 3a142fbfb5..5e612b25f0 100644 --- a/packages/js-analyze/js_analyze.js +++ b/packages/js-analyze/js_analyze.js @@ -4,7 +4,6 @@ var estraverse = Npm.require('estraverse'); var Syntax = estraverse.Syntax; -// @export JSAnalyze JSAnalyze = {}; JSAnalyze.READ = 1; diff --git a/packages/js-analyze/package.js b/packages/js-analyze/package.js index 58db2fd97d..71087fefc3 100644 --- a/packages/js-analyze/package.js +++ b/packages/js-analyze/package.js @@ -1,5 +1,10 @@ +// IF YOU MAKE ANY CHANGES TO THIS PACKAGE THAT COULD AFFECT ITS OUTPUT, YOU +// MUST UPDATE BUILT_BY IN tools/packages.js. Otherwise packages may not be +// rebuilt with the new changes. + Package.describe({ - summary: "JavaScript code analysis for Meteor" + summary: "JavaScript code analysis for Meteor", + internal: true }); // Use some packages from the Esprima project. If it turns out we need these on @@ -22,6 +27,7 @@ Npm.depends({ // would be impossible to load at link time (or all transitive dependencies // packages would need to function without the analysis provided by this // package). -Package.on_use(function (api, where) { +Package.on_use(function (api) { + api.export('JSAnalyze', 'server'); api.add_files('js_analyze.js', 'server'); }); diff --git a/packages/json/package.js b/packages/json/package.js index a390732c87..87602a5998 100644 --- a/packages/json/package.js +++ b/packages/json/package.js @@ -6,8 +6,7 @@ Package.describe({ // 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?) -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - - api.add_files('json2.js', where); +Package.on_use(function (api) { + // Node always has JSON; we only need this in some browsers. + api.add_files('json2.js', 'client'); }); diff --git a/packages/jsparse/lexer.js b/packages/jsparse/lexer.js index db7e2e1676..6a4fdb75b7 100644 --- a/packages/jsparse/lexer.js +++ b/packages/jsparse/lexer.js @@ -255,7 +255,6 @@ Lexeme.prototype.toString = function () { // Thie flag can be read and set manually to affect the // parsing of the next token. -// @export JSLexer JSLexer = function (code) { this.code = code; this.pos = 0; diff --git a/packages/jsparse/package.js b/packages/jsparse/package.js index 3b09def1be..9b09d1bef0 100644 --- a/packages/jsparse/package.js +++ b/packages/jsparse/package.js @@ -4,6 +4,7 @@ Package.describe({ }); Package.on_use(function (api) { + api.export(['JSLexer', 'JSParser', 'ParseNode']); api.add_files(['lexer.js', 'parserlib.js', 'stringify.js', 'parser.js'], ['client', 'server']); }); diff --git a/packages/jsparse/parser.js b/packages/jsparse/parser.js index a57c753ad5..7fd4f0ed93 100644 --- a/packages/jsparse/parser.js +++ b/packages/jsparse/parser.js @@ -26,7 +26,6 @@ var makeSet = function (array) { }; -// @export JSParser JSParser = function (code, options) { this.lexer = new JSLexer(code); this.oldToken = null; diff --git a/packages/jsparse/parserlib.js b/packages/jsparse/parserlib.js index 25e50c8f5e..e17a5d9c05 100644 --- a/packages/jsparse/parserlib.js +++ b/packages/jsparse/parserlib.js @@ -6,7 +6,6 @@ var isArray = function (obj) { return obj && (typeof obj === 'object') && (typeof obj.length === 'number'); }; -// @export ParseNode ParseNode = function (name, children) { this.name = name; this.children = children; diff --git a/packages/less/package.js b/packages/less/package.js index 57f8dd152a..a6c8d7fe62 100644 --- a/packages/less/package.js +++ b/packages/less/package.js @@ -13,5 +13,6 @@ Package._transitional_registerBuildPlugin({ Package.on_test(function (api) { api.use(['test-helpers', 'tinytest', 'less']); + api.use(['spark']); api.add_files(['less_tests.less', 'less_tests.js'], 'client'); }); diff --git a/packages/livedata/client_convenience.js b/packages/livedata/client_convenience.js index b049d630c4..b6992d1ac7 100644 --- a/packages/livedata/client_convenience.js +++ b/packages/livedata/client_convenience.js @@ -1,8 +1,7 @@ -_.extend(Meteor, { - default_connection: null, - refresh: function (notification) { - } -}); +// Meteor.refresh can be called on the client (if you're in common code) but it +// only has an effect on the server. +Meteor.refresh = function (notification) { +}; if (Meteor.isClient) { // By default, try to connect back to the same endpoint as the page @@ -12,17 +11,28 @@ if (Meteor.isClient) { if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL) ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL; } - Meteor.default_connection = - Meteor.connect(ddpUrl, true /* restart_on_update */); + Meteor.connection = + DDP.connect(ddpUrl, true /* restart_on_update */); - // Proxy the public methods of Meteor.default_connection so they can + // Proxy the public methods of Meteor.connection so they can // be called directly on Meteor. - _.each(['subscribe', 'methods', 'call', 'apply', 'status', 'reconnect'], + _.each(['subscribe', 'methods', 'call', 'apply', 'status', 'reconnect', + 'disconnect'], function (name) { - Meteor[name] = _.bind(Meteor.default_connection[name], - Meteor.default_connection); + Meteor[name] = _.bind(Meteor.connection[name], Meteor.connection); }); } else { - /* Never set up a default connection on the server. Don't even map - subscribe/call/etc onto Meteor. */ + // Never set up a default connection on the server. Don't even map + // subscribe/call/etc onto Meteor. + Meteor.connection = null; } + +// Meteor.connection used to be called +// Meteor.default_connection. Provide backcompat as a courtesy even +// though it was never documented. +// XXX COMPAT WITH 0.6.4 +Meteor.default_connection = Meteor.connection; + +// We should transition from Meteor.connect to DDP.connect. +// XXX COMPAT WITH 0.6.4 +Meteor.connect = DDP.connect; diff --git a/packages/livedata/common.js b/packages/livedata/common.js new file mode 100644 index 0000000000..18663f000e --- /dev/null +++ b/packages/livedata/common.js @@ -0,0 +1 @@ +LivedataTest = {}; diff --git a/packages/livedata/crossbar.js b/packages/livedata/crossbar.js index 327760e730..e4c8be26db 100644 --- a/packages/livedata/crossbar.js +++ b/packages/livedata/crossbar.js @@ -1,4 +1,4 @@ -Meteor._InvalidationCrossbar = function () { +DDPServer._InvalidationCrossbar = function () { var self = this; self.next_id = 1; @@ -7,7 +7,7 @@ Meteor._InvalidationCrossbar = function () { self.listeners = {}; }; -_.extend(Meteor._InvalidationCrossbar.prototype, { +_.extend(DDPServer._InvalidationCrossbar.prototype, { // Listen for notification that match 'trigger'. A notification // matches if it has the key-value pairs in trigger as a // subset. When a notification matches, call 'callback', passing two @@ -96,4 +96,4 @@ _.extend(Meteor._InvalidationCrossbar.prototype, { }); // singleton -Meteor._InvalidationCrossbar = new Meteor._InvalidationCrossbar; +DDPServer._InvalidationCrossbar = new DDPServer._InvalidationCrossbar; diff --git a/packages/livedata/crossbar_tests.js b/packages/livedata/crossbar_tests.js index 9ac0d01a0d..d5eed6cedd 100644 --- a/packages/livedata/crossbar_tests.js +++ b/packages/livedata/crossbar_tests.js @@ -6,15 +6,15 @@ // deep meaning to the matching function, and it could be changed later // as long as it preserves that property. Tinytest.add('livedata - crossbar', function (test) { - test.isTrue(Meteor._InvalidationCrossbar._matches( + test.isTrue(DDPServer._InvalidationCrossbar._matches( {collection: "C"}, {collection: "C"})); - test.isTrue(Meteor._InvalidationCrossbar._matches( + test.isTrue(DDPServer._InvalidationCrossbar._matches( {collection: "C", id: "X"}, {collection: "C"})); - test.isTrue(Meteor._InvalidationCrossbar._matches( + test.isTrue(DDPServer._InvalidationCrossbar._matches( {collection: "C"}, {collection: "C", id: "X"})); - test.isTrue(Meteor._InvalidationCrossbar._matches( + test.isTrue(DDPServer._InvalidationCrossbar._matches( {collection: "C", id: "X"}, {collection: "C"})); - test.isFalse(Meteor._InvalidationCrossbar._matches( + test.isFalse(DDPServer._InvalidationCrossbar._matches( {collection: "C", id: "X"}, {collection: "C", id: "Y"})); }); diff --git a/packages/livedata/livedata_common.js b/packages/livedata/livedata_common.js index e2543581fc..b39b65e491 100644 --- a/packages/livedata/livedata_common.js +++ b/packages/livedata/livedata_common.js @@ -1,6 +1,10 @@ -Meteor._SUPPORTED_DDP_VERSIONS = [ 'pre1' ]; +DDP = {}; -Meteor._MethodInvocation = function (options) { +SUPPORTED_DDP_VERSIONS = [ 'pre1' ]; + +LivedataTest.SUPPORTED_DDP_VERSIONS = SUPPORTED_DDP_VERSIONS; + +MethodInvocation = function (options) { var self = this; // true if we're running not the actual method, but a stub (that is, @@ -12,9 +16,6 @@ Meteor._MethodInvocation = function (options) { // zero-latency connection to the user. this.isSimulation = options.isSimulation; - // XXX Backwards compatibility only. Remove this before 1.0. - this.is_simulation = this.isSimulation; - // call this function to allow other method invocations (from the // same client) to continue running without waiting for this one to // complete. @@ -35,7 +36,7 @@ Meteor._MethodInvocation = function (options) { this._sessionData = options.sessionData; }; -_.extend(Meteor._MethodInvocation.prototype, { +_.extend(MethodInvocation.prototype, { unblock: function () { var self = this; self._calledUnblock = true; @@ -50,7 +51,7 @@ _.extend(Meteor._MethodInvocation.prototype, { } }); -Meteor._parseDDP = function (stringMessage) { +parseDDP = function (stringMessage) { try { var msg = JSON.parse(stringMessage); } catch (e) { @@ -84,7 +85,7 @@ Meteor._parseDDP = function (stringMessage) { return msg; }; -Meteor._stringifyDDP = function (msg) { +stringifyDDP = function (msg) { var copy = EJSON.clone(msg); // swizzle 'changed' messages from 'fields undefined' rep to 'fields // and cleared' rep @@ -112,39 +113,8 @@ Meteor._stringifyDDP = function (msg) { return JSON.stringify(copy); }; -Meteor._CurrentInvocation = new Meteor.EnvironmentVariable; - -// Note: The DDP server assumes that Meteor.Error EJSON-serializes as an object -// containing 'error' and optionally 'reason' and 'details'. -// The DDP client manually puts these into Meteor.Error objects. (We don't use -// EJSON.addType here because the type is determined by location in the -// protocol, not text on the wire.) -Meteor.Error = Meteor.makeErrorType( - "Meteor.Error", - function (error, reason, details) { - var self = this; - - // Currently, a numeric code, likely similar to a HTTP code (eg, - // 404, 500). That is likely to change though. - self.error = error; - - // Optional: A short human-readable summary of the error. Not - // intended to be shown to end users, just developers. ("Not Found", - // "Internal Server Error") - self.reason = reason; - - // Optional: Additional information about the error, say for - // debugging. It might be a (textual) stack trace if the server is - // willing to provide one. The corresponding thing in HTTP would be - // the body of a 404 or 500 response. (The difference is that we - // never expect this to be shown to end users, only developers, so - // it doesn't need to be pretty.) - self.details = details; - - // This is what gets displayed at the top of a stack trace. Current - // format is "[404]" (if no reason is set) or "File not found [404]" - if (self.reason) - self.message = self.reason + ' [' + self.error + ']'; - else - self.message = '[' + self.error + ']'; - }); +// This is private but it's used in a few places. accounts-base uses +// it to get the current user. accounts-password uses it to stash SRP +// state in the DDP session. Meteor.setTimeout and friends clear +// it. We can probably find a better way to factor this. +DDP._CurrentInvocation = new Meteor.EnvironmentVariable; diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 32779e194e..04fb380f90 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -1,5 +1,4 @@ if (Meteor.isServer) { - // XXX namespacing var path = Npm.require('path'); var Fiber = Npm.require('fibers'); var Future = Npm.require(path.join('fibers', 'future')); @@ -11,13 +10,13 @@ if (Meteor.isServer) { // reloadOnUpdate: should we try to reload when the server says // there's new code available? // reloadWithOutstanding: is it OK to reload if there are outstanding methods? -Meteor._LivedataConnection = function (url, options) { +var Connection = function (url, options) { var self = this; options = _.extend({ reloadOnUpdate: false, // The rest of these options are only for testing. reloadWithOutstanding: false, - supportedDDPVersions: Meteor._SUPPORTED_DDP_VERSIONS, + supportedDDPVersions: SUPPORTED_DDP_VERSIONS, onConnectionFailure: function (reason) { Meteor._debug("Failed DDP connection: " + reason); }, @@ -33,7 +32,7 @@ Meteor._LivedataConnection = function (url, options) { if (typeof url === "object") { self._stream = url; } else { - self._stream = new Meteor._DdpClientStream(url); + self._stream = new LivedataTest.ClientStream(url); } self._lastSessionId = null; @@ -162,8 +161,8 @@ Meteor._LivedataConnection = function (url, options) { self._userIdDeps = (typeof Deps !== "undefined") && new Deps.Dependency; // Block auto-reload while we're waiting for method responses. - if (Meteor._reload && !options.reloadWithOutstanding) { - Meteor._reload.onMigrate(function (retry) { + if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) { + Reload._onMigrate(function (retry) { if (!self._readyToMigrate()) { if (self._retryMigrate) throw new Error("Two migrations in progress?"); @@ -177,7 +176,7 @@ Meteor._LivedataConnection = function (url, options) { var onMessage = function (raw_msg) { try { - var msg = Meteor._parseDDP(raw_msg); + var msg = parseDDP(raw_msg); } catch (e) { Meteor._debug("Exception while parsing DDP", e); return; @@ -200,7 +199,7 @@ Meteor._LivedataConnection = function (url, options) { } else { var error = "Version negotiation failed; server requested version " + msg.version; - self._stream.forceDisconnect(error); + self._stream.disconnect({_permanent: true, _error: error}); options.onConnectionFailure(error); } } @@ -280,12 +279,12 @@ Meteor._LivedataConnection = function (url, options) { } - if (Meteor._reload && options.reloadOnUpdate) { + if (Meteor.isClient && Package.reload && options.reloadOnUpdate) { self._stream.on('update_available', function () { // Start trying to migrate to a new version. Until all packages // signal that they're ready for a migration, the app will // continue running normally. - Meteor._reload.reload(); + Reload._reload(); }); } @@ -384,7 +383,7 @@ _.extend(MethodInvoker.prototype, { } }); -_.extend(Meteor._LivedataConnection.prototype, { +_.extend(Connection.prototype, { // 'name' is the name of the data on the wire that should go in the // store. 'wrappedStore' should be an object with methods beginUpdate, update, // endUpdate, saveOriginals, retrieveOriginals. see Collection for an example. @@ -629,7 +628,7 @@ _.extend(Meteor._LivedataConnection.prototype, { // to do a RPC, so we use the return value of the stub as our return // value. - var enclosing = Meteor._CurrentInvocation.get(); + var enclosing = DDP._CurrentInvocation.get(); var alreadyInSimulation = enclosing && enclosing.isSimulation; var stub = self._methodHandlers[name]; @@ -637,7 +636,7 @@ _.extend(Meteor._LivedataConnection.prototype, { var setUserId = function(userId) { self.setUserId(userId); }; - var invocation = new Meteor._MethodInvocation({ + var invocation = new MethodInvocation({ isSimulation: true, userId: self.userId(), setUserId: setUserId, sessionData: self._sessionData @@ -649,7 +648,7 @@ _.extend(Meteor._LivedataConnection.prototype, { try { // Note that unlike in the corresponding server code, we never audit // that stubs check() their arguments. - var ret = Meteor._CurrentInvocation.withValue(invocation,function () { + var ret = DDP._CurrentInvocation.withValue(invocation, function () { if (Meteor.isServer) { // Because saveOriginals and retrieveOriginals aren't reentrant, // don't allow stubs to yield. @@ -809,7 +808,7 @@ _.extend(Meteor._LivedataConnection.prototype, { // Sends the DDP stringification of the given message object _send: function (obj) { var self = this; - self._stream.send(Meteor._stringifyDDP(obj)); + self._stream.send(stringifyDDP(obj)); }, status: function (/*passthrough args*/) { @@ -822,9 +821,14 @@ _.extend(Meteor._LivedataConnection.prototype, { return self._stream.reconnect.apply(self._stream, arguments); }, + disconnect: function (/*passthrough args*/) { + var self = this; + return self._stream.disconnect.apply(self._stream, arguments); + }, + close: function () { var self = this; - return self._stream.forceDisconnect(); + return self._stream.disconnect({_permanent: true}); }, /// @@ -902,8 +906,8 @@ _.extend(Meteor._LivedataConnection.prototype, { // Mark all named subscriptions which are ready (ie, we already called the // ready callback) as needing to be revived. - // XXX We should also block reconnect quiescence until autopublish is done - // re-publishing to avoid flicker! + // XXX We should also block reconnect quiescence until unnamed subscriptions + // (eg, autopublish) are done re-publishing to avoid flicker! self._subsBeingRevived = {}; _.each(self._subscriptions, function (sub, id) { if (sub.ready) @@ -1062,7 +1066,7 @@ _.extend(Meteor._LivedataConnection.prototype, { + msg.id); } serverDoc.document = msg.fields || {}; - serverDoc.document._id = Meteor.idParse(msg.id); + serverDoc.document._id = LocalCollection._idParse(msg.id); } else { self._pushUpdate(updates, msg.collection, msg); } @@ -1379,26 +1383,28 @@ _.extend(Meteor._LivedataConnection.prototype, { } }); -_.extend(Meteor, { - // @param url {String} URL to Meteor app, - // e.g.: - // "subdomain.meteor.com", - // "http://subdomain.meteor.com", - // "/", - // "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" - connect: function (url, _reloadOnUpdate) { - var ret = new Meteor._LivedataConnection( - url, {reloadOnUpdate: _reloadOnUpdate}); - Meteor._LivedataConnection._allConnections.push(ret); // hack. see below. - return ret; - } -}); +LivedataTest.Connection = Connection; + +// @param url {String} URL to Meteor app, +// e.g.: +// "subdomain.meteor.com", +// "http://subdomain.meteor.com", +// "/", +// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" +// +DDP.connect = function (url, _reloadOnUpdate) { + var ret = new Connection( + url, {reloadOnUpdate: _reloadOnUpdate}); + allConnections.push(ret); // hack. see below. + return ret; +}; // Hack for `spiderable` package: a way to see if the page is done // loading all the data it needs. -Meteor._LivedataConnection._allConnections = []; -Meteor._LivedataConnection._allSubscriptionsReady = function () { - return _.all(Meteor._LivedataConnection._allConnections, function (conn) { +// +allConnections = []; +DDP._allSubscriptionsReady = function () { + return _.all(allConnections, function (conn) { return _.all(conn._subscriptions, function (sub) { return sub.ready; }); diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 4aa48723da..72d56d65cb 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1,16 +1,15 @@ - var newConnection = function (stream) { // Some of these tests leave outstanding methods with no result yet // returned. This should not block us from re-running tests when sources // change. - return new Meteor._LivedataConnection(stream, {reloadWithOutstanding: true}); + return new LivedataTest.Connection(stream, {reloadWithOutstanding: true}); }; var makeConnectMessage = function (session) { var msg = { msg: 'connect', - version: Meteor._SUPPORTED_DDP_VERSIONS[0], - support: Meteor._SUPPORTED_DDP_VERSIONS + version: LivedataTest.SUPPORTED_DDP_VERSIONS[0], + support: LivedataTest.SUPPORTED_DDP_VERSIONS }; if (session) @@ -69,7 +68,7 @@ var startAndConnect = function(test, stream) { var SESSION_ID = '17'; Tinytest.add("livedata stub - receive data", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -97,7 +96,7 @@ Tinytest.add("livedata stub - receive data", function (test) { }); Tinytest.add("livedata stub - subscribe", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -146,7 +145,7 @@ Tinytest.add("livedata stub - subscribe", function (test) { Tinytest.add("livedata stub - reactive subscribe", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -263,14 +262,12 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) { Tinytest.add("livedata stub - this", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); conn.methods({test_this: function() { test.isTrue(this.isSimulation); - // XXX Backwards compatibility only. Remove this before 1.0. - test.isTrue(this.is_simulation); this.unblock(); // should be a no-op }}); @@ -289,7 +286,7 @@ Tinytest.add("livedata stub - this", function (test) { if (Meteor.isClient) { Tinytest.add("livedata stub - methods", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -344,7 +341,7 @@ if (Meteor.isClient) { test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); // data methods do not show up (not quiescent yet) - stream.receive({msg: 'added', collection: collName, id: Meteor.idStringify(docId), + stream.receive({msg: 'added', collection: collName, id: LocalCollection._idStringify(docId), fields: {value: 'tuesday'}}); test.equal(coll.find({}).count(), 1); test.equal(coll.find({value: 'friday!'}).count(), 1); @@ -391,7 +388,7 @@ if (Meteor.isClient) { } Tinytest.add("livedata stub - mutating method args", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -432,7 +429,7 @@ var observeCursor = function (test, cursor) { // method calls another method in simulation. see not sent. if (Meteor.isClient) { Tinytest.add("livedata stub - methods calling methods", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -472,7 +469,7 @@ if (Meteor.isClient) { // get data from the method. data from this doc does not show up yet, but data // from another doc does. - stream.receive({msg: 'added', collection: coll_name, id: Meteor.idStringify(docId), + stream.receive({msg: 'added', collection: coll_name, id: LocalCollection._idStringify(docId), fields: {value: 'tuesday'}}); o.expectCallbacks(); test.equal(coll.findOne(docId), {_id: docId, a: 1}); @@ -495,7 +492,7 @@ if (Meteor.isClient) { }); } Tinytest.add("livedata stub - method call before connect", function (test) { - var stream = new Meteor._StubStream; + var stream = new StubStream; var conn = newConnection(stream); var callbackOutput = []; @@ -516,7 +513,7 @@ Tinytest.add("livedata stub - method call before connect", function (test) { }); Tinytest.add("livedata stub - reconnect", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -645,7 +642,7 @@ Tinytest.add("livedata stub - reconnect", function (test) { if (Meteor.isClient) { Tinytest.add("livedata stub - reconnect method which only got result", function (test) { - var stream = new Meteor._StubStream; + var stream = new StubStream; var conn = newConnection(stream); startAndConnect(test, stream); @@ -685,7 +682,7 @@ if (Meteor.isClient) { // Get some data. stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {baz: 42}}); + id: LocalCollection._idStringify(stubWrittenId), fields: {baz: 42}}); // It doesn't show up yet. test.equal(coll.find().count(), 1); test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); @@ -720,7 +717,7 @@ if (Meteor.isClient) { test.equal(callbackOutput, ['bla']); test.equal(onResultReceivedOutput, ['bla']); stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {baz: 42}}); + id: LocalCollection._idStringify(stubWrittenId), fields: {baz: 42}}); test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, baz: 42}); o.expectCallbacks({added: 1}); @@ -752,7 +749,7 @@ if (Meteor.isClient) { // Get some data. stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId2), fields: {baz: 42}}); + id: LocalCollection._idStringify(stubWrittenId2), fields: {baz: 42}}); // It doesn't show up yet. test.equal(coll.find().count(), 2); test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); @@ -796,7 +793,7 @@ if (Meteor.isClient) { // Receive data matching our stub. It doesn't take effect yet. stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId2), fields: {foo: 'bar'}}); + id: LocalCollection._idStringify(stubWrittenId2), fields: {foo: 'bar'}}); o.expectCallbacks(); // slowMethod is done writing, so we get full reconnect quiescence (but no @@ -818,7 +815,7 @@ if (Meteor.isClient) { }); } Tinytest.add("livedata stub - reconnect method which only got data", function (test) { - var stream = new Meteor._StubStream; + var stream = new StubStream; var conn = newConnection(stream); startAndConnect(test, stream); @@ -904,7 +901,7 @@ Tinytest.add("livedata stub - reconnect method which only got data", function (t }); if (Meteor.isClient) { Tinytest.add("livedata stub - multiple stubs same doc", function (test) { - var stream = new Meteor._StubStream; + var stream = new StubStream; var conn = newConnection(stream); startAndConnect(test, stream); @@ -951,7 +948,7 @@ if (Meteor.isClient) { // Get some data... slightly different than what we wrote. stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {foo: 'barb', other: 'field', + id: LocalCollection._idStringify(stubWrittenId), fields: {foo: 'barb', other: 'field', other2: 'bla'}}); // It doesn't show up yet. test.equal(coll.find().count(), 1); @@ -969,7 +966,7 @@ if (Meteor.isClient) { // More data. Not quite what we wrote. Also ignored for now. stream.receive({msg: 'changed', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {baz: 43}, cleared: ['other']}); + id: LocalCollection._idStringify(stubWrittenId), fields: {baz: 43}, cleared: ['other']}); test.equal(coll.find().count(), 1); test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar', baz: 42}); @@ -991,7 +988,7 @@ if (Meteor.isClient) { Tinytest.add("livedata stub - unsent methods don't block quiescence", function (test) { // This test is for https://github.com/meteor/meteor/issues/555 - var stream = new Meteor._StubStream; + var stream = new StubStream; var conn = newConnection(stream); startAndConnect(test, stream); @@ -1055,7 +1052,7 @@ if (Meteor.isClient) { }); } Tinytest.add("livedata stub - reactive resub", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -1124,7 +1121,7 @@ Tinytest.add("livedata stub - reactive resub", function (test) { Tinytest.add("livedata connection - reactive userId", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); test.equal(conn.userId(), null); @@ -1133,7 +1130,7 @@ Tinytest.add("livedata connection - reactive userId", function (test) { }); Tinytest.add("livedata connection - two wait methods", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -1244,7 +1241,7 @@ Tinytest.add("livedata connection - two wait methods", function (test) { }); Tinytest.add("livedata connection - onReconnect prepends messages correctly with a wait method", function(test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -1309,7 +1306,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata connection - reconnect to a different server", [ function (test, expect) { var self = this; - self.conn = Meteor.connect("reverse.meteor.com"); + self.conn = DDP.connect("reverse.meteor.com"); pollUntil(expect, function () { return self.conn.status().connected; }, 5000, 100, true); // poll until connected, but don't fail if we don't connect @@ -1348,13 +1345,13 @@ testAsyncMulti("livedata connection - reconnect to a different server", [ Tinytest.addAsync("livedata connection - version negotiation requires renegotiating", function (test, onComplete) { - var connection = new Meteor._LivedataConnection(getSelfConnectionUrl(), { + var connection = new LivedataTest.Connection(getSelfConnectionUrl(), { reloadWithOutstanding: true, - supportedDDPVersions: ["garbled", Meteor._SUPPORTED_DDP_VERSIONS[0]], + supportedDDPVersions: ["garbled", LivedataTest.SUPPORTED_DDP_VERSIONS[0]], onConnectionFailure: function () { test.fail(); onComplete(); }, onConnected: function () { - test.equal(connection._version, Meteor._SUPPORTED_DDP_VERSIONS[0]); - connection._stream.forceDisconnect(); + test.equal(connection._version, LivedataTest.SUPPORTED_DDP_VERSIONS[0]); + connection._stream.disconnect({_permanent: true}); onComplete(); } }); @@ -1362,7 +1359,7 @@ Tinytest.addAsync("livedata connection - version negotiation requires renegotiat Tinytest.addAsync("livedata connection - version negotiation error", function (test, onComplete) { - var connection = new Meteor._LivedataConnection(getSelfConnectionUrl(), { + var connection = new LivedataTest.Connection(getSelfConnectionUrl(), { reloadWithOutstanding: true, supportedDDPVersions: ["garbled", "more garbled"], onConnectionFailure: function () { @@ -1379,7 +1376,7 @@ Tinytest.addAsync("livedata connection - version negotiation error", }); Tinytest.add("livedata connection - onReconnect prepends messages correctly without a wait method", function(test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -1423,7 +1420,7 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with }); Tinytest.add("livedata connection - onReconnect with sent messages", function(test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -1466,7 +1463,7 @@ Tinytest.add("livedata connection - onReconnect with sent messages", function(te Tinytest.add("livedata stub - reconnect double wait method", function (test) { - var stream = new Meteor._StubStream; + var stream = new StubStream; var conn = newConnection(stream); startAndConnect(test, stream); @@ -1530,7 +1527,7 @@ Tinytest.add("livedata stub - reconnect double wait method", function (test) { }); Tinytest.add("livedata stub - subscribe errors", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); startAndConnect(test, stream); @@ -1572,7 +1569,7 @@ Tinytest.add("livedata stub - subscribe errors", function (test) { if (Meteor.isClient) { Tinytest.add("livedata stub - stubs before connected", function (test) { - var stream = new Meteor._StubStream(); + var stream = new StubStream(); var conn = newConnection(stream); var collName = Random.id(); diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index e3331d48ef..5c396c4a00 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -1,18 +1,24 @@ +DDPServer = {}; + var Fiber = Npm.require('fibers'); // This file contains classes: -// * LivedataSession - The server's connection to a single DDP client -// * LivedataSubscription - A single subscription for a single client -// * LivedataServer - An entire server that may talk to > 1 client. A DDP endpoint. +// * Session - The server's connection to a single DDP client +// * Subscription - A single subscription for a single client +// * Server - An entire server that may talk to > 1 client. A DDP endpoint. +// +// Session and Subscription are file scope. For now, until we freeze +// the interface, Server is package scope (in the future it should be +// exported.) // Represents a single document in a SessionCollectionView -Meteor._SessionDocumentView = function () { +var SessionDocumentView = function () { var self = this; self.existsIn = {}; // set of subscriptionHandle self.dataByKey = {}; // key-> [ {subscriptionHandle, value} by precedence] }; -_.extend(Meteor._SessionDocumentView.prototype, { +_.extend(SessionDocumentView.prototype, { getFields: function () { var self = this; @@ -91,14 +97,17 @@ _.extend(Meteor._SessionDocumentView.prototype, { }); // Represents a client's view of a single collection -Meteor._SessionCollectionView = function (collectionName, sessionCallbacks) { +var SessionCollectionView = function (collectionName, sessionCallbacks) { var self = this; self.collectionName = collectionName; self.documents = {}; self.callbacks = sessionCallbacks; }; -_.extend(Meteor._SessionCollectionView.prototype, { +LivedataTest.SessionCollectionView = SessionCollectionView; + + +_.extend(SessionCollectionView.prototype, { isEmpty: function () { var self = this; @@ -144,7 +153,7 @@ _.extend(Meteor._SessionCollectionView.prototype, { var added = false; if (!docView) { added = true; - docView = new Meteor._SessionDocumentView(); + docView = new SessionDocumentView(); self.documents[id] = docView; } docView.existsIn[subscriptionHandle] = true; @@ -198,11 +207,12 @@ _.extend(Meteor._SessionCollectionView.prototype, { } } }); + /******************************************************************************/ -/* LivedataSession */ +/* Session */ /******************************************************************************/ -Meteor._LivedataSession = function (server, version) { +var Session = function (server, version) { var self = this; self.id = Random.id(); @@ -249,7 +259,7 @@ Meteor._LivedataSession = function (server, version) { self._pendingReady = []; }; -_.extend(Meteor._LivedataSession.prototype, { +_.extend(Session.prototype, { sendReady: function (subscriptionIds) { @@ -304,8 +314,8 @@ _.extend(Meteor._LivedataSession.prototype, { if (_.has(self.collectionViews, collectionName)) { return self.collectionViews[collectionName]; } - var ret = new Meteor._SessionCollectionView(collectionName, - self.getSendCallbacks()); + var ret = new SessionCollectionView(collectionName, + self.getSendCallbacks()); self.collectionViews[collectionName] = ret; return ret; }, @@ -343,8 +353,8 @@ _.extend(Meteor._LivedataSession.prototype, { self.last_connect_time = +(new Date); _.each(self.out_queue, function (msg) { if (Meteor._printSentDDP) - Meteor._debug("Sent DDP", Meteor._stringifyDDP(msg)); - self.socket.send(Meteor._stringifyDDP(msg)); + Meteor._debug("Sent DDP", stringifyDDP(msg)); + self.socket.send(stringifyDDP(msg)); }); self.out_queue = []; @@ -361,7 +371,7 @@ _.extend(Meteor._LivedataSession.prototype, { var self = this; // Make a shallow copy of the set of universal handlers and start them. If // additional universal publishers start while we're running them (due to - // yielding), they will run separately as part of _LivedataServer.publish. + // yielding), they will run separately as part of Server.publish. var handlers = _.clone(self.server.universal_publish_handlers); _.each(handlers, function (handler) { self._startSubscription(handler); @@ -426,9 +436,9 @@ _.extend(Meteor._LivedataSession.prototype, { send: function (msg) { var self = this; if (Meteor._printSentDDP) - Meteor._debug("Sent DDP", Meteor._stringifyDDP(msg)); + Meteor._debug("Sent DDP", stringifyDDP(msg)); if (self.socket) - self.socket.send(Meteor._stringifyDDP(msg)); + self.socket.send(stringifyDDP(msg)); else self.out_queue.push(msg); }, @@ -546,7 +556,7 @@ _.extend(Meteor._LivedataSession.prototype, { // set up to mark the method as satisfied once all observers // (and subscriptions) have reacted to any writes that were // done. - var fence = new Meteor._WriteFence; + var fence = new DDPServer._WriteFence; fence.onAllCommitted(function () { // Retire the fence so that future writes are allowed. // This means that callbacks like timers are free to use @@ -584,15 +594,15 @@ _.extend(Meteor._LivedataSession.prototype, { self._setUserId(userId); }; - var invocation = new Meteor._MethodInvocation({ + var invocation = new MethodInvocation({ isSimulation: false, userId: self.userId, setUserId: setUserId, unblock: unblock, sessionData: self.sessionData }); try { - var result = Meteor._CurrentWriteFence.withValue(fence, function () { - return Meteor._CurrentInvocation.withValue(invocation, function () { + var result = DDPServer._CurrentWriteFence.withValue(fence, function () { + return DDP._CurrentInvocation.withValue(invocation, function () { return maybeAuditArgumentChecks( handler, invocation, msg.params, "call to '" + msg.method + "'"); }); @@ -714,7 +724,7 @@ _.extend(Meteor._LivedataSession.prototype, { _startSubscription: function (handler, subId, params, name) { var self = this; - var sub = new Meteor._LivedataSubscription( + var sub = new Subscription( self, handler, subId, params, name); if (subId) self._namedSubs[subId] = sub; @@ -761,15 +771,14 @@ _.extend(Meteor._LivedataSession.prototype, { }); /******************************************************************************/ -/* LivedataSubscription */ +/* Subscription */ /******************************************************************************/ // ctor for a sub handle: the input to each publish function -Meteor._LivedataSubscription = function ( +var Subscription = function ( session, handler, subscriptionId, params, name) { var self = this; - // LivedataSession - self._session = session; + self._session = session; // type is Session self._handler = handler; @@ -815,12 +824,12 @@ Meteor._LivedataSubscription = function ( // a ddp consumer that isn't minimongo self._idFilter = { - idStringify: Meteor.idStringify, - idParse: Meteor.idParse + idStringify: LocalCollection._idStringify, + idParse: LocalCollection._idParse }; }; -_.extend(Meteor._LivedataSubscription.prototype, { +_.extend(Subscription.prototype, { _runHandler: function () { var self = this; try { @@ -925,14 +934,14 @@ _.extend(Meteor._LivedataSubscription.prototype, { }); }, - // Returns a new _LivedataSubscription for the same session with the same - // initial creation parameters. This isn't a clone: it doesn't have the same - // _documents cache, stopped state or callbacks; may have a different - // _subscriptionHandle, and gets its userId from the session, not from this - // object. + // Returns a new Subscription for the same session with the same + // initial creation parameters. This isn't a clone: it doesn't have + // the same _documents cache, stopped state or callbacks; may have a + // different _subscriptionHandle, and gets its userId from the + // session, not from this object. _recreate: function () { var self = this; - return new Meteor._LivedataSubscription( + return new Subscription( self._session, self._handler, self._subscriptionId, self._params); }, @@ -1004,11 +1013,10 @@ _.extend(Meteor._LivedataSubscription.prototype, { }); /******************************************************************************/ -/* LivedataServer */ +/* Server */ /******************************************************************************/ - -Meteor._LivedataServer = function () { +Server = function () { var self = this; self.publish_handlers = {}; @@ -1016,12 +1024,9 @@ Meteor._LivedataServer = function () { self.method_handlers = {}; - self.on_autopublish = []; // array of func if AP disabled, null if enabled - self.warned_about_autopublish = false; - self.sessions = {}; // map from id to session - self.stream_server = new Meteor._DdpStreamServer; + self.stream_server = new StreamServer; self.stream_server.register(function (socket) { // socket implements the SockJSConnection interface @@ -1031,7 +1036,7 @@ Meteor._LivedataServer = function () { var msg = {msg: 'error', reason: reason}; if (offendingMessage) msg.offendingMessage = offendingMessage; - socket.send(Meteor._stringifyDDP(msg)); + socket.send(stringifyDDP(msg)); }; socket.on('data', function (raw_msg) { @@ -1040,7 +1045,7 @@ Meteor._LivedataServer = function () { } try { try { - var msg = Meteor._parseDDP(raw_msg); + var msg = parseDDP(raw_msg); } catch (err) { sendError('Parse error'); return; @@ -1099,22 +1104,21 @@ Meteor._LivedataServer = function () { }, 1 * 60 * 1000); }; -_.extend(Meteor._LivedataServer.prototype, { +_.extend(Server.prototype, { _handleConnect: function (socket, msg) { var self = this; // In the future, handle session resumption: something like: // socket.meteor_session = self.sessions[msg.session] - var version = Meteor._LivedataServer._calculateVersion( - msg.support, Meteor._SUPPORTED_DDP_VERSIONS); + var version = calculateVersion(msg.support, SUPPORTED_DDP_VERSIONS); if (msg.version === version) { // Creating a new session - socket.meteor_session = new Meteor._LivedataSession(self, version); + socket.meteor_session = new Session(self, version); self.sessions[socket.meteor_session.id] = socket.meteor_session; - socket.send(Meteor._stringifyDDP({msg: 'connected', + socket.send(stringifyDDP({msg: 'connected', session: socket.meteor_session.id})); // will kick off previous connection, if any socket.meteor_session.connect(socket); @@ -1129,11 +1133,11 @@ _.extend(Meteor._LivedataServer.prototype, { // floor. We don't want to confuse things. socket.removeAllListeners('data'); setTimeout(function () { - socket.send(Meteor._stringifyDDP({msg: 'failed', version: version})); + socket.send(stringifyDDP({msg: 'failed', version: version})); socket.close(); }, timeout); } else { - socket.send(Meteor._stringifyDDP({msg: 'failed', version: version})); + socket.send(stringifyDDP({msg: 'failed', version: version})); socket.close(); } }, @@ -1169,7 +1173,7 @@ _.extend(Meteor._LivedataServer.prototype, { return; } - if (!self.on_autopublish && !options.is_auto) { + if (Package.autopublish && !options.is_auto) { // They have autopublish on, yet they're trying to manually // picking stuff to publish. They probably should turn off // autopublish. (This check isn't perfect -- if you create a @@ -1265,7 +1269,7 @@ _.extend(Meteor._LivedataServer.prototype, { var setUserId = function() { throw new Error("Can't call setUserId on a server initiated method call"); }; - var currentInvocation = Meteor._CurrentInvocation.get(); + var currentInvocation = DDP._CurrentInvocation.get(); if (currentInvocation) { userId = currentInvocation.userId; setUserId = function(userId) { @@ -1273,13 +1277,13 @@ _.extend(Meteor._LivedataServer.prototype, { }; } - var invocation = new Meteor._MethodInvocation({ + var invocation = new MethodInvocation({ isSimulation: false, userId: userId, setUserId: setUserId, sessionData: self.sessionData }); try { - var result = Meteor._CurrentInvocation.withValue(invocation, function () { + var result = DDP._CurrentInvocation.withValue(invocation, function () { return maybeAuditArgumentChecks( handler, invocation, args, "internal call to '" + name + "'"); }); @@ -1300,30 +1304,11 @@ _.extend(Meteor._LivedataServer.prototype, { if (exception) throw exception; return result; - }, - - // A much more elegant way to do this would be: let any autopublish - // provider (eg, mongo-livedata) declare a weak package dependency - // on the autopublish package, then have that package simply set a - // flag that eg the Collection constructor checks, and autopublishes - // if necessary. - autopublish: function () { - var self = this; - _.each(self.on_autopublish || [], function (f) { f(); }); - self.on_autopublish = null; - }, - - onAutopublish: function (f) { - var self = this; - if (self.on_autopublish) - self.on_autopublish.push(f); - else - f(); } }); -Meteor._LivedataServer._calculateVersion = function (clientSupportedVersions, - serverSupportedVersions) { +var calculateVersion = function (clientSupportedVersions, + serverSupportedVersions) { var correctVersion = _.find(clientSupportedVersions, function (version) { return _.contains(serverSupportedVersions, version); }); @@ -1333,6 +1318,9 @@ Meteor._LivedataServer._calculateVersion = function (clientSupportedVersions, return correctVersion; }; +LivedataTest.calculateVersion = calculateVersion; + + // "blind" exceptions other than those that were deliberately thrown to signal // errors to the client var wrapInternalException = function (exception, context) { @@ -1358,9 +1346,12 @@ var wrapInternalException = function (exception, context) { return new Meteor.Error(500, "Internal server error"); }; + +// Audit argument checks, if the audit-argument-checks package exists (it is a +// weak dependency of this package). var maybeAuditArgumentChecks = function (f, context, args, description) { args = args || []; - if (Meteor._LivedataServer._auditArgumentChecks) { + if (Package['audit-argument-checks']) { return Match._failIfArgumentsAreNotAllChecked( f, context, args, description); } diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index ec754d6a23..7ea0f656e8 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -32,8 +32,7 @@ if (Meteor.isServer) { Tinytest.add("livedata - version negotiation", function (test) { var versionCheck = function (clientVersions, serverVersions, expected) { test.equal( - Meteor._LivedataServer._calculateVersion(clientVersions, - serverVersions), + LivedataTest.calculateVersion(clientVersions, serverVersions), expected); }; @@ -287,9 +286,9 @@ testAsyncMulti("livedata - compound methods", [ } ]); -// Replaces the LivedataConnection's `_livedata_data` method to push -// incoming messages on a given collection to an array. This can be -// used to verify that the right data is sent on the wire +// Replaces the Connection's `_livedata_data` method to push incoming +// messages on a given collection to an array. This can be used to +// verify that the right data is sent on the wire // // @param messages {Array} The array to which to append the messages // @return {Function} A function to call to undo the eavesdropping @@ -321,7 +320,7 @@ if (Meteor.isClient) { function(test, expect) { var messages = []; var undoEavesdrop = eavesdropOnCollection( - Meteor.default_connection, "objectsWithUsers", messages); + Meteor.connection, "objectsWithUsers", messages); // A helper for testing incoming set and unset messages // XXX should this be extracted as a general helper together with @@ -505,8 +504,8 @@ if (Meteor.isClient) { testAsyncMulti("livedata - publisher errors", (function () { // Use a separate connection so that we can safely check to see if // conn._subscriptions is empty. - var conn = new Meteor._LivedataConnection('/', - {reloadWithOutstanding: true}); + var conn = new LivedataTest.Connection('/', + {reloadWithOutstanding: true}); var collName = Random.id(); var coll = new Meteor.Collection(collName, {connection: conn}); var errorFromRerun; @@ -572,7 +571,7 @@ if (Meteor.isClient) { // sub.stop does NOT call onError. test.isFalse(gotErrorFromStopper); test.equal(_.size(conn._subscriptions), 0); // white-box test - conn._stream.forceDisconnect(); + conn._stream.disconnect({_permanent: true}); } ];})()); @@ -614,7 +613,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - connect works from both client and server", [ function (test, expect) { var self = this; - self.conn = Meteor.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(Meteor.absoluteUrl()); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); @@ -638,7 +637,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - method call on server blocks in a fiber way", [ function (test, expect) { var self = this; - self.conn = Meteor.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(Meteor.absoluteUrl()); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); @@ -658,7 +657,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - connect fails to unknown place", [ function (test, expect) { var self = this; - self.conn = Meteor.connect("example.com"); + self.conn = DDP.connect("example.com"); Meteor.setTimeout(expect(function () { test.isFalse(self.conn.status().connected, "Not connected"); }), 500); diff --git a/packages/livedata/package.js b/packages/livedata/package.js index 50ed7bd94c..709ee84e39 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -16,17 +16,33 @@ Package.on_use(function (api) { // XXX split this package into multiple packages or multiple slices instead api.use(['webapp', 'routepolicy'], 'server', {weak: true}); + // Detect whether or not the user wants us to audit argument checks. + api.use(['audit-argument-checks'], 'server', {weak: true}); + + // Allow us to detect 'autopublish', so we can print a warning if the user + // runs Meteor.publish while it's loaded. + api.use('autopublish', 'server', {weak: true}); + + api.export('DDP'); + api.export('DDPServer', 'server'); + + api.export('LivedataTest', {testOnly: true}); + // Transport api.use('reload', 'client'); - api.add_files(['sockjs-0.3.4.js', - 'stream_client_sockjs.js'], 'client'); + api.add_files('common.js'); + api.add_files(['sockjs-0.3.4.js', 'stream_client_sockjs.js'], 'client'); api.add_files('stream_client_nodejs.js', 'server'); api.add_files('stream_client_common.js', ['client', 'server']); api.add_files('stream_server.js', 'server'); - // we depend on LocalCollection._diffObjects and ._applyChanges. + // we depend on LocalCollection._diffObjects, _applyChanges, + // _idParse, _idStringify. api.use('minimongo', ['client', 'server']); + + api.add_files('livedata_server.js', 'server'); + api.add_files('writefence.js', 'server'); api.add_files('crossbar.js', 'server'); @@ -34,8 +50,6 @@ Package.on_use(function (api) { api.add_files('livedata_connection.js', ['client', 'server']); - api.add_files('livedata_server.js', 'server'); - api.add_files('client_convenience.js', 'client'); api.add_files('server_convenience.js', 'server'); @@ -45,7 +59,7 @@ Package.on_test(function (api) { api.use('livedata', ['client', 'server']); api.use('mongo-livedata', ['client', 'server']); api.use('test-helpers', ['client', 'server']); - api.use(['underscore', 'tinytest', 'random', 'deps']); + api.use(['underscore', 'tinytest', 'random', 'deps', 'minimongo']); api.add_files('livedata_connection_tests.js', ['client', 'server']); api.add_files('livedata_tests.js', ['client', 'server']); diff --git a/packages/livedata/server_convenience.js b/packages/livedata/server_convenience.js index ba03a585cc..ed1dc6d05a 100644 --- a/packages/livedata/server_convenience.js +++ b/packages/livedata/server_convenience.js @@ -1,38 +1,41 @@ -_.extend(Meteor, { - default_server: null, - refresh: function (notification) { - } -}); - -// Only create a server (and map publish, methods, call, etc onto Meteor) if we -// are in an environment with a HTTP server (as opposed to, eg, a command-line -// tool). +// Only create a server if we are in an environment with a HTTP server +// (as opposed to, eg, a command-line tool). +// if (Package.webapp) { if (process.env.DDP_DEFAULT_CONNECTION_URL) { __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL = process.env.DDP_DEFAULT_CONNECTION_URL; } - Meteor.default_server = new Meteor._LivedataServer; + Meteor.server = new Server; Meteor.refresh = function (notification) { - var fence = Meteor._CurrentWriteFence.get(); + var fence = DDPServer._CurrentWriteFence.get(); if (fence) { // Block the write fence until all of the invalidations have // landed. var proxy_write = fence.beginWrite(); } - Meteor._InvalidationCrossbar.fire(notification, function () { + DDPServer._InvalidationCrossbar.fire(notification, function () { if (proxy_write) proxy_write.committed(); }); }; - // Proxy the public methods of Meteor.default_server so they can + // Proxy the public methods of Meteor.server so they can // be called directly on Meteor. _.each(['publish', 'methods', 'call', 'apply'], function (name) { - Meteor[name] = _.bind(Meteor.default_server[name], - Meteor.default_server); + Meteor[name] = _.bind(Meteor.server[name], Meteor.server); }); +} else { + // No server? Make these empty/no-ops. + Meteor.server = null; + Meteor.refresh = function (notificatio) { + }; } + +// Meteor.server used to be called Meteor.default_server. Provide +// backcompat as a courtesy even though it was never documented. +// XXX COMPAT WITH 0.6.4 +Meteor.default_server = Meteor.server; diff --git a/packages/livedata/session_view_tests.js b/packages/livedata/session_view_tests.js index c1daee49f4..24c7ad2a8d 100644 --- a/packages/livedata/session_view_tests.js +++ b/packages/livedata/session_view_tests.js @@ -1,6 +1,6 @@ var newView = function(test) { var results = []; - var view = new Meteor._SessionCollectionView('test', { + var view = new LivedataTest.SessionCollectionView('test', { added: function (collection, id, fields) { results.push({fun: 'added', id: id, fields: fields}); }, diff --git a/packages/livedata/sockjs-0.3.4.js b/packages/livedata/sockjs-0.3.4.js index 49ed31445c..a19b0d29d1 100644 --- a/packages/livedata/sockjs-0.3.4.js +++ b/packages/livedata/sockjs-0.3.4.js @@ -1970,7 +1970,17 @@ InfoReceiver.prototype = new EventEmitter(['finish']); InfoReceiver.prototype.doXhr = function(base_url, AjaxObject) { var that = this; var t0 = (new Date()).getTime(); - var xo = new AjaxObject('GET', base_url + '/info'); + +// + // https://github.com/sockjs/sockjs-client/pull/129 + // var xo = new AjaxObject('GET', base_url + '/info'); + + var xo = new AjaxObject( + // add cachebusting parameter to url to work around a chrome bug: + // https://code.google.com/p/chromium/issues/detail?id=263981 + // or misbehaving proxies. + 'GET', base_url + '/info?cb=' + utils.random_string(10)) +// var tref = utils.delay(8000, function(){xo.ontimeout();}); diff --git a/packages/livedata/stream_client_common.js b/packages/livedata/stream_client_common.js index 2a387dc2b3..817eb211d3 100644 --- a/packages/livedata/stream_client_common.js +++ b/packages/livedata/stream_client_common.js @@ -1,4 +1,3 @@ - // XXX from Underscore.String (http://epeli.github.com/underscore.string/) var startsWith = function(str, starts) { return str.length >= starts.length && @@ -60,7 +59,19 @@ var translateUrl = function(url, newSchemeBase, subPath) { return url + "/" + subPath; }; -_.extend(Meteor._DdpClientStream.prototype, { +toSockjsUrl = function (url) { + return translateUrl(url, "http", "sockjs"); +}; + +toWebsocketUrl = function (url) { + var ret = translateUrl(url, "ws", "websocket"); + return ret; +}; + +LivedataTest.toSockjsUrl = toSockjsUrl; + + +_.extend(LivedataTest.ClientStream.prototype, { // Register for callbacks. on: function (name, callback) { @@ -157,26 +168,41 @@ _.extend(Meteor._DdpClientStream.prototype, { self._retryNow(); }, - // Permanently disconnect a stream. - forceDisconnect: function (optionalErrorMessage) { + disconnect: function (options) { var self = this; - self._forcedToDisconnect = true; + options = options || {}; + + // Failed is permanent. If we're failed, don't let people go back + // online by calling 'disconnect' then 'reconnect'. + if (self._forcedToDisconnect) + return; + + // If _permanent is set, permanently disconnect a stream. Once a stream + // is forced to disconnect, it can never reconnect. This is for + // error cases such as ddp version mismatch, where trying again + // won't fix the problem. + if (options._permanent) { + self._forcedToDisconnect = true; + } + self._cleanup(); if (self.retryTimer) { clearTimeout(self.retryTimer); self.retryTimer = null; } + self.currentStatus = { - status: "failed", + status: (options._permanent ? "failed" : "offline"), connected: false, retryCount: 0 }; - if (optionalErrorMessage) - self.currentStatus.reason = optionalErrorMessage; + + if (options._permanent && options._error) + self.currentStatus.reason = options._error; + self.statusChanged(); }, - _lostConnection: function () { var self = this; @@ -203,7 +229,9 @@ _.extend(Meteor._DdpClientStream.prototype, { // fired when we detect that we've gone online. try to reconnect // immediately. _online: function () { - this.reconnect(); + // if we've requested to be offline by disconnecting, don't reconnect. + if (this.currentStatus.status != "offline") + this.reconnect(); }, _retryLater: function () { @@ -244,15 +272,3 @@ _.extend(Meteor._DdpClientStream.prototype, { return self.currentStatus; } }); - -_.extend(Meteor._DdpClientStream, { - - _toSockjsUrl: function (url) { - return translateUrl(url, "http", "sockjs"); - }, - - _toWebsocketUrl: function (url) { - var ret = translateUrl(url, "ws", "websocket"); - return ret; - } -}); diff --git a/packages/livedata/stream_client_nodejs.js b/packages/livedata/stream_client_nodejs.js index 1c4b0f308b..d6a99d1d17 100644 --- a/packages/livedata/stream_client_nodejs.js +++ b/packages/livedata/stream_client_nodejs.js @@ -9,7 +9,7 @@ // We don't do any heartbeating. (The logic that did this in sockjs was removed, // because it used a built-in sockjs mechanism. We could do it with WebSocket // ping frames or with DDP-level messages.) -Meteor._DdpClientStream = function (endpoint) { +LivedataTest.ClientStream = function (endpoint) { var self = this; // WebSocket-Node https://github.com/Worlize/WebSocket-Node @@ -47,7 +47,7 @@ Meteor._DdpClientStream = function (endpoint) { self._launchConnection(); }; -_.extend(Meteor._DdpClientStream.prototype, { +_.extend(LivedataTest.ClientStream.prototype, { // data is a utf8 string. Data sent while not connected is dropped on // the floor, and it is up the user of this API to retransmit lost @@ -170,7 +170,7 @@ _.extend(Meteor._DdpClientStream.prototype, { // a protocol and the server doesn't send one back (and sockjs // doesn't). also, related: I guess we have to accept that // 'stream' is ddp-specific - self.client.connect(Meteor._DdpClientStream._toWebsocketUrl(self.endpoint)); + self.client.connect(toWebsocketUrl(self.endpoint)); if (self.connectionTimer) clearTimeout(self.connectionTimer); diff --git a/packages/livedata/stream_client_sockjs.js b/packages/livedata/stream_client_sockjs.js index 07d51781d5..d8c4bfb810 100644 --- a/packages/livedata/stream_client_sockjs.js +++ b/packages/livedata/stream_client_sockjs.js @@ -1,7 +1,7 @@ // @param url {String} URL to Meteor app // "http://subdomain.meteor.com/" or "/" or // "ddp+sockjs://foo-**.meteor.com/sockjs" -Meteor._DdpClientStream = function (url) { +LivedataTest.ClientStream = function (url) { var self = this; self._initCommon(); @@ -34,7 +34,7 @@ Meteor._DdpClientStream = function (url) { self._launchConnection(); }; -_.extend(Meteor._DdpClientStream.prototype, { +_.extend(LivedataTest.ClientStream.prototype, { // data is a utf8 string. Data sent while not connected is dropped on // the floor, and it is up the user of this API to retransmit lost @@ -168,8 +168,7 @@ _.extend(Meteor._DdpClientStream.prototype, { // can connect to random hostnames and get around browser per-host // connection limits. self.socket = new SockJS( - Meteor._DdpClientStream._toSockjsUrl(self.rawUrl), - undefined, { + toSockjsUrl(self.rawUrl), undefined, { debug: false, protocols_whitelist: self._sockjsProtocolsWhitelist() }); self.socket.onmessage = function (data) { diff --git a/packages/livedata/stream_server.js b/packages/livedata/stream_server.js index 2ee54b17d1..e00a42ada9 100644 --- a/packages/livedata/stream_server.js +++ b/packages/livedata/stream_server.js @@ -9,7 +9,7 @@ __meteor_runtime_config__.serverId = var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; -Meteor._DdpStreamServer = function () { +StreamServer = function () { var self = this; self.registration_callbacks = []; self.open_sockets = []; @@ -80,7 +80,7 @@ Meteor._DdpStreamServer = function () { }; -_.extend(Meteor._DdpStreamServer.prototype, { +_.extend(StreamServer.prototype, { // call my callback when a new socket connects. // also call it for all current connections. register: function (callback) { diff --git a/packages/livedata/stream_tests.js b/packages/livedata/stream_tests.js index 880f6e9c67..a73f6cd8f8 100644 --- a/packages/livedata/stream_tests.js +++ b/packages/livedata/stream_tests.js @@ -26,16 +26,70 @@ testAsyncMulti("stream - reconnect", [ })); if (Meteor.status().status !== "connected") - Meteor.default_connection._stream.on('reset', callback); + Meteor.connection._stream.on('reset', callback); else callback(); } ]); +// Disconnecting and reconnecting transitions through the correct statuses. +testAsyncMulti("stream - basic disconnect", [ + function (test, expect) { + var history = []; + var stream = new LivedataTest.ClientStream("/"); + var onTestPass = expect(); + + Deps.autorun(function() { + var status = stream.status(); + + if (_.last(history) != status.status) { + history.push(status.status); + + if (_.isEqual(history, ["connecting", "connected"])) + stream.disconnect(); + + if (_.isEqual(history, ["connecting", "connected", "offline"])) + stream.reconnect(); + + if (_.isEqual(history, ["connecting", "connected", "offline", + "connecting", "connected"])) { + stream.disconnect(); + onTestPass(); + } + } + }); + } +]); + +// Remain offline if the online event is received while offline. +testAsyncMulti("stream - disconnect remains offline", [ + function (test, expect) { + var history = []; + var stream = new LivedataTest.ClientStream("/"); + var onTestComplete = expect(); + + Deps.autorun(function() { + var status = stream.status(); + + if (_.last(history) != status.status) { + history.push(status.status); + + if (_.isEqual(history, ["connecting", "connected"])) + stream.disconnect(); + + if (_.isEqual(history, ["connecting", "connected", "offline"])) { + stream._online(); + test.isTrue(status.status == "offline"); + onTestComplete(); + } + } + }); + } +]); Tinytest.add("stream - sockjs urls are computed correctly", function(test) { var testHasSockjsUrl = function(raw, expectedSockjsUrl) { - var actual = Meteor._DdpClientStream._toSockjsUrl(raw); + var actual = LivedataTest.toSockjsUrl(raw); if (expectedSockjsUrl instanceof RegExp) test.isTrue(actual.match(expectedSockjsUrl), actual); else @@ -73,7 +127,7 @@ testAsyncMulti("stream - /websocket is a websocket endpoint", [ // Verify that /websocket and /websocket/ don't return the main page // _.each(['/websocket', '/websocket/'], function(path) { - Meteor.http.get(Meteor._relativeToSiteRootUrl(path), expect(function(error, result) { + HTTP.get(Meteor._relativeToSiteRootUrl(path), expect(function(error, result) { test.isNotNull(error); test.equal('Can "Upgrade" only to "WebSocket".', result.content); })); @@ -91,12 +145,12 @@ testAsyncMulti("stream - /websocket is a websocket endpoint", [ test.equal(pageContent, result.content); }); - Meteor.http.get(Meteor._relativeToSiteRootUrl('/'), expect(function(error, result) { + HTTP.get(Meteor._relativeToSiteRootUrl('/'), expect(function(error, result) { test.isNull(error); pageContent = result.content; _.each(['/websockets', '/websockets/'], function(path) { - Meteor.http.get(Meteor._relativeToSiteRootUrl(path), wrappedCallback); + HTTP.get(Meteor._relativeToSiteRootUrl(path), wrappedCallback); }); })); } diff --git a/packages/livedata/writefence.js b/packages/livedata/writefence.js index 6a665ab0e9..3315cbfe3c 100644 --- a/packages/livedata/writefence.js +++ b/packages/livedata/writefence.js @@ -4,7 +4,8 @@ var Future = Npm.require(path.join('fibers', 'future')); // A write fence collects a group of writes, and provides a callback // when all of the writes are fully committed and propagated (all // observers have been notified of the write and acknowledged it.) -Meteor._WriteFence = function () { +// +DDPServer._WriteFence = function () { var self = this; self.armed = false; @@ -17,9 +18,10 @@ Meteor._WriteFence = function () { // The current write fence. When there is a current write fence, code // that writes to databases should register their writes with it using // beginWrite(). -Meteor._CurrentWriteFence = new Meteor.EnvironmentVariable; +// +DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable; -_.extend(Meteor._WriteFence.prototype, { +_.extend(DDPServer._WriteFence.prototype, { // Start tracking a write, and return an object to represent it. The // object has a single method, committed(). This method should be // called when the write is fully committed and propagated. You can diff --git a/packages/liverange/liverange.js b/packages/liverange/liverange.js index b68d7aaef6..cbedb0194f 100644 --- a/packages/liverange/liverange.js +++ b/packages/liverange/liverange.js @@ -89,8 +89,6 @@ var wrapEndpoints = function (start, end) { // // XXX Should eventually support LiveRanges where start === end // and start.parentNode is null. -// -// @export LiveRange LiveRange = function (tag, start, end, inner) { if (start.nodeType === 11 /* DocumentFragment */) { end = start.lastChild; diff --git a/packages/liverange/package.js b/packages/liverange/package.js index daead7b09b..15bbd5d941 100644 --- a/packages/liverange/package.js +++ b/packages/liverange/package.js @@ -4,12 +4,14 @@ Package.describe({ }); Package.on_use(function (api) { + api.export('LiveRange', 'client'); api.add_files('liverange.js', 'client'); }); Package.on_test(function (api) { api.use(['tinytest']); - api.use(['liverange', 'test-helpers', 'domutils', 'underscore'], 'client'); + api.use(['liverange', 'test-helpers', 'domutils', 'underscore', 'jquery'], + 'client'); api.add_files([ 'liverange_test_helpers.js', diff --git a/packages/localstorage/localstorage.js b/packages/localstorage/localstorage.js index 1cf6e30731..4dd1e49ba2 100644 --- a/packages/localstorage/localstorage.js +++ b/packages/localstorage/localstorage.js @@ -1,3 +1,5 @@ +// This is not an ideal name, but we can change it later. + if (window.localStorage) { Meteor._localStorage = { getItem: function (key) { diff --git a/packages/logging/.npm/package/npm-shrinkwrap.json b/packages/logging/.npm/package/npm-shrinkwrap.json index 9ef7e8bdfe..f0eafa1381 100644 --- a/packages/logging/.npm/package/npm-shrinkwrap.json +++ b/packages/logging/.npm/package/npm-shrinkwrap.json @@ -7,7 +7,7 @@ "version": "0.9.2" }, "memoizee": { - "version": "0.2.4", + "version": "0.2.5", "dependencies": { "event-emitter": { "version": "0.2.2" diff --git a/packages/logging/logging.js b/packages/logging/logging.js index db33a5ca07..e33361c11d 100644 --- a/packages/logging/logging.js +++ b/packages/logging/logging.js @@ -1,4 +1,3 @@ -// @export Log Log = function () { return Log.info.apply(this, arguments); }; diff --git a/packages/logging/package.js b/packages/logging/package.js index fe3eed719e..f3883e7b98 100644 --- a/packages/logging/package.js +++ b/packages/logging/package.js @@ -7,16 +7,14 @@ Npm.depends({ "cli-color": "0.2.2" }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - api.use(['underscore', 'ejson'], where); - api.add_files('debug.js', where); - api.add_files('logging.js', where); +Package.on_use(function (api) { + api.export('Log'); + api.use(['underscore', 'ejson']); + api.add_files('logging.js'); }); Package.on_test(function (api) { api.use(['tinytest', 'underscore', 'ejson']); api.use('logging', ['client', 'server']); - api.add_files('debug_test.js', 'client'); api.add_files('logging_test.js', ['server', 'client']); }); diff --git a/packages/madewith/madewith.js b/packages/madewith/madewith.js index 297d8d9a29..956296a1ad 100644 --- a/packages/madewith/madewith.js +++ b/packages/madewith/madewith.js @@ -4,7 +4,7 @@ var match = hostname.match(/(.*)\.meteor.com$/); var shortname = match ? match[1] : hostname; // connect to madewith and subscribe to my app's record -var server = Meteor.connect("madewith.meteor.com"); +var server = DDP.connect("madewith.meteor.com"); var sub = server.subscribe("myApp", hostname); // minimongo collection to hold my singleton app record. diff --git a/packages/meetup/meetup_client.js b/packages/meetup/meetup_client.js index 1f8cbc4ee0..4661f93390 100644 --- a/packages/meetup/meetup_client.js +++ b/packages/meetup/meetup_client.js @@ -1,3 +1,4 @@ +Meetup = {}; // Request Meetup credentials for the user // @param options {optional} // @param credentialRequestCompleteCallback {Function} Callback function to call on diff --git a/packages/meetup/meetup_common.js b/packages/meetup/meetup_common.js deleted file mode 100644 index dd0eb2ab7a..0000000000 --- a/packages/meetup/meetup_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Meetup -Meetup = {}; diff --git a/packages/meetup/meetup_server.js b/packages/meetup/meetup_server.js index aa5890d9c7..cdd2b49a71 100644 --- a/packages/meetup/meetup_server.js +++ b/packages/meetup/meetup_server.js @@ -1,3 +1,5 @@ +Meetup = {}; + Oauth.registerService('meetup', 2, null, function(query) { var accessToken = getAccessToken(query); @@ -19,7 +21,7 @@ var getAccessToken = function (query) { var response; try { - response = Meteor.http.post( + response = HTTP.post( "https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: { code: query.code, client_id: config.clientId, @@ -41,7 +43,7 @@ var getAccessToken = function (query) { var getIdentity = function (accessToken) { try { - var response = Meteor.http.get( + var response = HTTP.get( "https://secure.meetup.com/2/members", {params: {member_id: 'self', access_token: accessToken}}); return response.data.results && response.data.results[0]; @@ -53,4 +55,4 @@ var getIdentity = function (accessToken) { Meetup.retrieveCredential = function(credentialToken) { return Oauth.retrieveCredential(credentialToken); -}; \ No newline at end of file +}; diff --git a/packages/meetup/package.js b/packages/meetup/package.js index ee38bfbb8e..8a739fe3e5 100644 --- a/packages/meetup/package.js +++ b/packages/meetup/package.js @@ -8,17 +8,18 @@ Package.describe({ Package.on_use(function(api) { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.use('http', ['client', 'server']); + api.use('http', ['server']); api.use('templating', 'client'); api.use('underscore', 'client'); api.use('random', 'client'); api.use('service-configuration', ['client', 'server']); + api.export('Meetup'); + api.add_files( ['meetup_configure.html', 'meetup_configure.js'], 'client'); - api.add_files('meetup_common.js', ['client', 'server']); api.add_files('meetup_server.js', 'server'); api.add_files('meetup_client.js', 'client'); }); diff --git a/packages/meteor/client_environment.js b/packages/meteor/client_environment.js index 7d066fb028..37305c04e4 100644 --- a/packages/meteor/client_environment.js +++ b/packages/meteor/client_environment.js @@ -1,4 +1,3 @@ -// @export Meteor Meteor = { isClient: true, isServer: false @@ -6,5 +5,5 @@ Meteor = { if (typeof __meteor_runtime_config__ === 'object' && __meteor_runtime_config__.PUBLIC_SETTINGS) { - Meteor.settings = { public: __meteor_runtime_config__.PUBLIC_SETTINGS }; + Meteor.settings = { 'public': __meteor_runtime_config__.PUBLIC_SETTINGS }; } diff --git a/packages/logging/debug.js b/packages/meteor/debug.js similarity index 99% rename from packages/logging/debug.js rename to packages/meteor/debug.js index 0a09dbdcc0..a6f20d5f9c 100644 --- a/packages/logging/debug.js +++ b/packages/meteor/debug.js @@ -10,6 +10,7 @@ var suppress = 0; // _debug. the intent is for this message to go to the terminal and // be very visible. if you change _debug to go someplace else, etc, // please fix the autopublish code to do something reasonable. +// Meteor._debug = function (/* arguments */) { if (suppress) { suppress--; @@ -55,6 +56,7 @@ Meteor._debug = function (/* arguments */) { // Suppress the next 'count' Meteor._debug messsages. Use this to // stop tests from spamming the console. +// Meteor._suppress_log = function (count) { suppress += count; }; diff --git a/packages/logging/debug_test.js b/packages/meteor/debug_test.js similarity index 80% rename from packages/logging/debug_test.js rename to packages/meteor/debug_test.js index fcb832a484..9d3db73a05 100644 --- a/packages/logging/debug_test.js +++ b/packages/meteor/debug_test.js @@ -1,4 +1,4 @@ -Tinytest.add("logging - debug", function (test) { +Tinytest.add("core - debug", function (test) { // Just run a log statement and make sure it doesn't explode. Meteor._suppress_log(3); diff --git a/packages/meteor/dynamics_nodejs.js b/packages/meteor/dynamics_nodejs.js index dfef4e1c90..e3b7fbb256 100644 --- a/packages/meteor/dynamics_nodejs.js +++ b/packages/meteor/dynamics_nodejs.js @@ -50,6 +50,7 @@ _.extend(Meteor.EnvironmentVariable.prototype, { // If it's called inside a fiber, it works normally (the // return value of the function will be passed through, and no new // fiber will be created.) +// Meteor.bindEnvironment = function (func, onException, _this) { var boundValues = _.clone(Fiber.current._meteor_dynamics || []); diff --git a/packages/meteor/errors.js b/packages/meteor/errors.js index 7a63778556..cdeb8a5abb 100644 --- a/packages/meteor/errors.js +++ b/packages/meteor/errors.js @@ -9,6 +9,7 @@ var inherits = function (child, parent) { // Makes an error subclass which properly contains a stack trace in most // environments. constructor can set fields on `this` (and should probably set // `message`, which is what gets displayed at the top of a stack trace). +// Meteor.makeErrorType = function (name, constructor) { var errorClass = function (/*arguments*/) { var self = this; @@ -37,3 +38,44 @@ Meteor.makeErrorType = function (name, constructor) { return errorClass; }; + +// This should probably be in the livedata package, but we don't want +// to require you to use the livedata package to get it. Eventually we +// should probably rename it to DDP.Error and put it back in the +// 'livedata' package (which we should rename to 'ddp' also.) +// +// Note: The DDP server assumes that Meteor.Error EJSON-serializes as an object +// containing 'error' and optionally 'reason' and 'details'. +// The DDP client manually puts these into Meteor.Error objects. (We don't use +// EJSON.addType here because the type is determined by location in the +// protocol, not text on the wire.) +// +Meteor.Error = Meteor.makeErrorType( + "Meteor.Error", + function (error, reason, details) { + var self = this; + + // Currently, a numeric code, likely similar to a HTTP code (eg, + // 404, 500). That is likely to change though. + self.error = error; + + // Optional: A short human-readable summary of the error. Not + // intended to be shown to end users, just developers. ("Not Found", + // "Internal Server Error") + self.reason = reason; + + // Optional: Additional information about the error, say for + // debugging. It might be a (textual) stack trace if the server is + // willing to provide one. The corresponding thing in HTTP would be + // the body of a 404 or 500 response. (The difference is that we + // never expect this to be shown to end users, only developers, so + // it doesn't need to be pretty.) + self.details = details; + + // This is what gets displayed at the top of a stack trace. Current + // format is "[404]" (if no reason is set) or "File not found [404]" + if (self.reason) + self.message = self.reason + ' [' + self.error + ']'; + else + self.message = '[' + self.error + ']'; + }); diff --git a/packages/meteor/fiber_helpers.js b/packages/meteor/fiber_helpers.js index 19b931d370..4c42e73391 100644 --- a/packages/meteor/fiber_helpers.js +++ b/packages/meteor/fiber_helpers.js @@ -35,6 +35,7 @@ Meteor._noYieldsAllowed = function (f) { // XXX break this out into an NPM module? // XXX could maybe use the npm 'schlock' module instead, which would // also support multiple concurrent "read" tasks +// Meteor._SynchronousQueue = function () { var self = this; // List of tasks to run (not including a currently-running task if any). Each @@ -168,6 +169,7 @@ _.extend(Meteor._SynchronousQueue.prototype, { // Sleep. Mostly used for debugging (eg, inserting latency into server // methods). +// Meteor._sleepForMs = function (ms) { var fiber = Fiber.current; setTimeout(function() { diff --git a/packages/meteor/fiber_stubs_client.js b/packages/meteor/fiber_stubs_client.js index 4e419c07da..3babd0e08f 100644 --- a/packages/meteor/fiber_stubs_client.js +++ b/packages/meteor/fiber_stubs_client.js @@ -2,12 +2,14 @@ // to use a queue too, and also to call noYieldsAllowed. // The client has no ability to yield, so noYieldsAllowed is a noop. +// Meteor._noYieldsAllowed = function (f) { return f(); }; // An even simpler queue of tasks than the fiber-enabled one. This one just // runs all the tasks when you call runTask or flush, synchronously. +// Meteor._SynchronousQueue = function () { var self = this; self._tasks = []; diff --git a/packages/meteor/helpers.js b/packages/meteor/helpers.js index 3bd83671d6..0804eff775 100644 --- a/packages/meteor/helpers.js +++ b/packages/meteor/helpers.js @@ -1,5 +1,3 @@ -// XXX namespacing -- find a better home for these? - if (Meteor.isServer) var Future = Npm.require('fibers/future'); @@ -7,9 +5,13 @@ if (typeof __meteor_runtime_config__ === 'object' && __meteor_runtime_config__.meteorRelease) Meteor.release = __meteor_runtime_config__.meteorRelease; +// XXX find a better home for these? Ideally they would be _.get, +// _.ensure, _.delete.. + _.extend(Meteor, { // _get(a,b,c,d) returns a[b][c][d], or else undefined if a[b] or // a[b][c] doesn't exist. + // _get: function (obj /*, arguments */) { for (var i = 1; i < arguments.length; i++) { if (!(arguments[i] in obj)) @@ -21,6 +23,7 @@ _.extend(Meteor, { // _ensure(a,b,c,d) ensures that a[b][c][d] exists. If it does not, // it is created and set to {}. Either way, it is returned. + // _ensure: function (obj /*, arguments */) { for (var i = 1; i < arguments.length; i++) { var key = arguments[i]; @@ -34,6 +37,7 @@ _.extend(Meteor, { // _delete(a, b, c, d) deletes a[b][c][d], then a[b][c] unless it // isn't empty, then a[b] unless it isn't empty. + // _delete: function (obj /*, arguments */) { var stack = [obj]; var leaf = true; @@ -69,6 +73,7 @@ _.extend(Meteor, { // fs.open(pathname, flags, [mode], [callback]) // For maximum effectiveness and least confusion, wrapAsync should be used on // functions where the callback is the only argument of type Function. + // _wrapAsync: function (fn) { return function (/* arguments */) { var self = this; diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 95b38dfe9f..439d80e6f2 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -10,9 +10,11 @@ Package._transitional_registerBuildPlugin({ sources: ['plugin/basic-file-types.js'] }); -Package.on_use(function (api, where) { +Package.on_use(function (api) { api.use('underscore', ['client', 'server']); + api.export('Meteor'); + api.add_files('client_environment.js', 'client'); api.add_files('server_environment.js', 'server'); api.add_files('helpers.js', ['client', 'server']); @@ -21,6 +23,9 @@ Package.on_use(function (api, where) { api.add_files('errors.js', ['client', 'server']); api.add_files('fiber_helpers.js', 'server'); api.add_files('fiber_stubs_client.js', 'client'); + api.add_files('startup_client.js', ['client']); + api.add_files('startup_server.js', ['server']); + api.add_files('debug.js', ['client', 'server']); // dynamic variables, bindEnvironment // XXX move into a separate package? @@ -49,6 +54,8 @@ Package.on_test(function (api) { api.add_files('timers_tests.js', ['client', 'server']); + api.add_files('debug_test.js', 'client'); + api.add_files('bare_test_setup.js', 'client', {bare: true}); api.add_files('bare_tests.js', 'client'); }); diff --git a/packages/meteor/server_environment.js b/packages/meteor/server_environment.js index 4a9eb01991..2146a87e39 100644 --- a/packages/meteor/server_environment.js +++ b/packages/meteor/server_environment.js @@ -1,4 +1,3 @@ -// @export Meteor Meteor = { isClient: false, isServer: true diff --git a/packages/startup/startup_client.js b/packages/meteor/startup_client.js similarity index 100% rename from packages/startup/startup_client.js rename to packages/meteor/startup_client.js diff --git a/packages/startup/startup_server.js b/packages/meteor/startup_server.js similarity index 100% rename from packages/startup/startup_server.js rename to packages/meteor/startup_server.js diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index 1c3bc1330a..68411cbaf5 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -1,8 +1,9 @@ var withoutInvocation = function (f) { - if (Meteor._CurrentInvocation) { - if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation) + if (Package.livedata) { + var _CurrentInvocation = Package.livedata.DDP._CurrentInvocation; + if (_CurrentInvocation.get() && _CurrentInvocation.get().isSimulation) throw new Error("Can't set timers inside simulations"); - return function () { Meteor._CurrentInvocation.withValue(null, f); }; + return function () { _CurrentInvocation.withValue(null, f); }; } else return f; diff --git a/packages/accounts-urls/.gitignore b/packages/minifiers/.gitignore similarity index 100% rename from packages/accounts-urls/.gitignore rename to packages/minifiers/.gitignore diff --git a/packages/minifiers/.npm/package/.gitignore b/packages/minifiers/.npm/package/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/minifiers/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/minifiers/.npm/package/README b/packages/minifiers/.npm/package/README new file mode 100644 index 0000000000..3d492553a4 --- /dev/null +++ b/packages/minifiers/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/minifiers/.npm/package/npm-shrinkwrap.json b/packages/minifiers/.npm/package/npm-shrinkwrap.json new file mode 100644 index 0000000000..b8040d824d --- /dev/null +++ b/packages/minifiers/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,41 @@ +{ + "dependencies": { + "clean-css": { + "version": "1.0.11", + "dependencies": { + "commander": { + "version": "1.2.0", + "dependencies": { + "keypress": { + "version": "0.1.0" + } + } + } + } + }, + "uglify-js": { + "from": "https://github.com/mishoo/UglifyJS2/tarball/b1febde3e9be32b9d88918ed733efc3796e3f143", + "dependencies": { + "async": { + "version": "0.2.9" + }, + "source-map": { + "version": "0.1.26", + "dependencies": { + "amdefine": { + "version": "0.0.5" + } + } + }, + "optimist": { + "version": "0.3.7", + "dependencies": { + "wordwrap": { + "version": "0.0.2" + } + } + } + } + } + } +} diff --git a/packages/minifiers/minifiers.js b/packages/minifiers/minifiers.js new file mode 100644 index 0000000000..efd8ce0a30 --- /dev/null +++ b/packages/minifiers/minifiers.js @@ -0,0 +1,2 @@ +CleanCSSProcess = Npm.require('clean-css').process; +UglifyJSMinify = Npm.require('uglify-js').minify; diff --git a/packages/minifiers/package.js b/packages/minifiers/package.js new file mode 100644 index 0000000000..91f474d798 --- /dev/null +++ b/packages/minifiers/package.js @@ -0,0 +1,15 @@ +Package.describe({ + summary: "JavaScript and CSS minifiers", + internal: true +}); + +Npm.depends({ + "clean-css": "1.0.11", + // We depend on this commit, which has not been released yet. + "uglify-js": "https://github.com/mishoo/UglifyJS2/tarball/b1febde3e9be32b9d88918ed733efc3796e3f143" +}); + +Package.on_use(function (api) { + api.export(['CleanCSSProcess', 'UglifyJSMinify']); + api.add_files('minifiers.js', 'server'); +}); diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 164217c596..d50b9de011 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -7,7 +7,6 @@ // LiveResultsSet: the return value of a live query. -// @export LocalCollection LocalCollection = function (name) { this.name = name; this.docs = {}; // _id -> document (also containing id) @@ -43,14 +42,12 @@ LocalCollection._applyChanges = function (doc, changeFields) { }); }; -LocalCollection.MinimongoError = function (message) { - var self = this; - self.name = "MinimongoError"; - self.details = message; +var MinimongoError = function (message) { + var e = new Error(message); + e.name = "MinimongoError"; + return e; }; -LocalCollection.MinimongoError.prototype = new Error; - // options may include sort, skip, limit, reactive // sort may be any of these forms: @@ -200,6 +197,8 @@ LocalCollection.Cursor.prototype._publishCursor = function (sub) { if (! self.collection.name) throw new Error("Can't publish a cursor from a collection without a name."); var collection = self.collection.name; + + // XXX minimongo should not depend on mongo-livedata! return Meteor.Collection._publishCursor(self, sub, collection); }; @@ -429,7 +428,7 @@ LocalCollection.prototype.insert = function (doc, callback) { var id = LocalCollection._idStringify(doc._id); if (_.has(self.docs, doc._id)) - throw new LocalCollection.MinimongoError("Duplicate _id '" + doc._id + "'"); + throw MinimongoError("Duplicate _id '" + doc._id + "'"); self._saveOriginal(id, undefined); self.docs[id] = doc; @@ -825,6 +824,7 @@ LocalCollection.prototype.resumeObservers = function () { }; +// NB: used by livedata LocalCollection._idStringify = function (id) { if (id instanceof LocalCollection._ObjectID) { return id.valueOf(); @@ -849,7 +849,7 @@ LocalCollection._idStringify = function (id) { }; - +// NB: used by livedata LocalCollection._idParse = function (id) { if (id === "") { return id; @@ -866,11 +866,6 @@ LocalCollection._idParse = function (id) { } }; -if (typeof Meteor !== 'undefined') { - Meteor.idParse = LocalCollection._idParse; - Meteor.idStringify = LocalCollection._idStringify; -} - LocalCollection._makeChangedFields = function (newDoc, oldDoc) { var fields = {}; LocalCollection._diffObjects(oldDoc, newDoc, { diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 56bf557b9b..21417165bb 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -850,8 +850,8 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({$where: "this.a === 1", a: 2}, {a: 1}); match({$where: "this.a === 1", b: 2}, {a: 1, b: 2}); match({$where: "this.a === 1 && this.b === 2"}, {a: 1, b: 2}); - match({$where: "_.isArray(this.a)"}, {a: []}); - nomatch({$where: "_.isArray(this.a)"}, {a: 1}); + match({$where: "this.a instanceof Array"}, {a: []}); + nomatch({$where: "this.a instanceof Array"}, {a: 1}); // reaching into array match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index afa3ba3f69..7502f33d08 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -3,20 +3,17 @@ Package.describe({ internal: true }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - - // It would be sort of nice if minimongo didn't depend on - // underscore, so we could ship it separately. +Package.on_use(function (api) { + api.export('LocalCollection'); api.use(['underscore', 'json', 'ejson', 'ordered-dict', 'deps', - 'random', 'ordered-dict'], where); + 'random', 'ordered-dict']); api.add_files([ 'minimongo.js', 'selector.js', 'modify.js', 'diff.js', 'objectid.js' - ], where); + ]); }); Package.on_test(function (api) { diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 617e12c1bf..7cf8fa0a5d 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -258,7 +258,8 @@ var VALUE_OPERATORS = { "$regex": function (operand, options) { if (options !== undefined) { // Options passed in $options (even the empty string) always overrides - // options in the RegExp object itself. + // options in the RegExp object itself. (See also + // Meteor.Collection._rewriteSelector.) // Be clear that we only support the JS-supported options, not extended // ones (eg, Mongo supports x and s). Ideally we would implement x and s @@ -329,7 +330,7 @@ LocalCollection._f = { return 9; if (EJSON.isBinary(v)) return 5; - if (v instanceof Meteor.Collection.ObjectID) + if (v instanceof LocalCollection._ObjectID) return 7; return 3; // object diff --git a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json index 414c6ec42f..a8aafc87d9 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.7", + "version": "1.3.12", "dependencies": { "bson": { - "version": "0.1.9" + "version": "0.2.1" }, "kerberos": { "version": "0.0.3" diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index 4529b45352..b9c50f97b1 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -754,22 +754,28 @@ if (Meteor.isServer) { }); Tinytest.add("collection - global insecure", function (test) { - // note: This test alters the global insecure status! This may - // collide with itself if run multiple times (but is better than - // the old test which had the same problem) - var oldGlobalInsecure = Meteor.Collection.insecure; + // note: This test alters the global insecure status, by sneakily hacking + // the global Package object! This may collide with itself if run multiple + // times (but is better than the old test which had the same problem) + var insecurePackage = Package.insecure; - Meteor.Collection.insecure = true; + Package.insecure = {}; var collection = new Meteor.Collection(null); test.equal(collection._isInsecure(), true); - Meteor.Collection.insecure = false; + Package.insecure = undefined; + test.equal(collection._isInsecure(), false); + + delete Package.insecure; test.equal(collection._isInsecure(), false); collection._insecure = true; test.equal(collection._isInsecure(), true); - Meteor.Collection.insecure = oldGlobalInsecure; + if (insecurePackage) + Package.insecure = insecurePackage; + else + delete Package.insecure; }); } diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 13dfe8c100..1cea1d650e 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -1,4 +1,4 @@ -// connection, if given, is a LivedataClient or LivedataServer +// options.connection, if given, is a LivedataClient or LivedataServer // XXX presently there is no way to destroy/clean up a Collection Meteor.Collection = function (name, options) { @@ -55,16 +55,18 @@ Meteor.Collection = function (name, options) { else if (options.connection) self._connection = options.connection; else if (Meteor.isClient) - self._connection = Meteor.default_connection; + self._connection = Meteor.connection; else - self._connection = Meteor.default_server; + self._connection = Meteor.server; if (!options._driver) { - if (name && self._connection === Meteor.default_server && - Meteor._getRemoteCollectionDriver) - options._driver = Meteor._getRemoteCollectionDriver(); - else - options._driver = Meteor._LocalCollectionDriver; + if (name && self._connection === Meteor.server && + typeof MongoInternals !== "undefined" && + MongoInternals.defaultRemoteCollectionDriver) { + options._driver = MongoInternals.defaultRemoteCollectionDriver(); + } else { + options._driver = LocalCollectionDriver; + } } self._collection = options._driver.open(name, self._connection); @@ -101,7 +103,7 @@ Meteor.Collection = function (name, options) { // Apply an update. // XXX better specify this interface (not in terms of a wire message)? update: function (msg) { - var mongoId = Meteor.idParse(msg.id); + var mongoId = LocalCollection._idParse(msg.id); var doc = self._collection.findOne(mongoId); // Is this a "replace the whole doc" message coming from the quiescence @@ -174,12 +176,12 @@ Meteor.Collection = function (name, options) { self._defineMutationMethods(); // autopublish - if (!options._preventAutopublish && - self._connection && self._connection.onAutopublish) - self._connection.onAutopublish(function () { - var handler = function () { return self.find(); }; - self._connection.publish(null, handler, {is_auto: true}); - }); + if (Package.autopublish && !options._preventAutopublish && self._connection + && self._connection.publish) { + self._connection.publish(null, function () { + return self.find(); + }, {is_auto: true}); + } }; /// @@ -261,17 +263,15 @@ Meteor.Collection._rewriteSelector = function (selector) { var ret = {}; _.each(selector, function (value, key) { + // Mongo supports both {field: /foo/} and {field: {$regex: /foo/}} if (value instanceof RegExp) { - ret[key] = {$regex: value.source}; - var regexOptions = ''; - // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options - // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. - if (value.ignoreCase) - regexOptions += 'i'; - if (value.multiline) - regexOptions += 'm'; - if (regexOptions) - ret[key].$options = regexOptions; + ret[key] = convertRegexpToMongoSelector(value); + } else if (value && value.$regex instanceof RegExp) { + ret[key] = convertRegexpToMongoSelector(value.$regex); + // if value is {$regex: /foo/, $options: ...} then $options + // override the ones set on $regex. + if (value.$options !== undefined) + ret[key].$options = value.$options; } else if (_.contains(['$or','$and','$nor'], key)) { // Translate lower levels of $and/$or/$nor @@ -279,12 +279,32 @@ Meteor.Collection._rewriteSelector = function (selector) { return Meteor.Collection._rewriteSelector(v); }); } - else + else { ret[key] = value; + } }); return ret; }; +// convert a JS RegExp object to a Mongo {$regex: ..., $options: ...} +// selector +var convertRegexpToMongoSelector = function (regexp) { + check(regexp, RegExp); // safety belt + + var selector = {$regex: regexp.source}; + var regexOptions = ''; + // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options + // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. + if (regexp.ignoreCase) + regexOptions += 'i'; + if (regexp.multiline) + regexOptions += 'm'; + if (regexOptions) + selector.$options = regexOptions; + + return selector; +}; + var throwIfSelectorIsNotId = function (selector, methodName) { if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) { throw new Meteor.Error( @@ -363,11 +383,11 @@ _.each(["insert", "update", "remove"], function (name) { }; } - if (self._connection && self._connection !== Meteor.default_server) { + if (self._connection && self._connection !== Meteor.server) { // just remote to another endpoint, propagate return value or // exception. - var enclosing = Meteor._CurrentInvocation.get(); + var enclosing = DDP._CurrentInvocation.get(); var alreadyInSimulation = enclosing && enclosing.isSimulation; if (!alreadyInSimulation && name !== "insert") { // If we're about to actually send an RPC, we should throw an error if @@ -508,10 +528,10 @@ Meteor.Collection.prototype._defineMutationMethods = function() { // allow/deny semantics. If false, use insecure mode semantics. self._restricted = false; - // Insecure mode (default to allowing writes). Defaults to 'undefined' - // which means use the global Meteor.Collection.insecure. This - // property can be overriden by tests or packages wishing to change - // insecure mode behavior of their collections. + // Insecure mode (default to allowing writes). Defaults to 'undefined' which + // means insecure iff the insecure package is loaded. This property can be + // overriden by tests or packages wishing to change insecure mode behavior of + // their collections. self._insecure = undefined; self._validators = { @@ -586,7 +606,7 @@ Meteor.Collection.prototype._defineMutationMethods = function() { // Minimongo on the server gets no stubs; instead, by default // it wait()s until its result is ready, yielding. // This matches the behavior of macromongo on the server better. - if (Meteor.isClient || self._connection === Meteor.default_server) + if (Meteor.isClient || self._connection === Meteor.server) self._connection.methods(m); } }; @@ -609,7 +629,7 @@ Meteor.Collection.prototype._updateFetch = function (fields) { Meteor.Collection.prototype._isInsecure = function () { var self = this; if (self._insecure === undefined) - return Meteor.Collection.insecure; + return !!Package.insecure; return self._insecure; }; diff --git a/packages/mongo-livedata/local_collection_driver.js b/packages/mongo-livedata/local_collection_driver.js index bb90392772..ec78544154 100644 --- a/packages/mongo-livedata/local_collection_driver.js +++ b/packages/mongo-livedata/local_collection_driver.js @@ -1,5 +1,4 @@ -// XXX namespacing -Meteor._LocalCollectionDriver = function () { +LocalCollectionDriver = function () { var self = this; self.noConnCollections = {}; }; @@ -10,7 +9,7 @@ var ensureCollection = function (name, collections) { return collections[name]; }; -_.extend(Meteor._LocalCollectionDriver.prototype, { +_.extend(LocalCollectionDriver.prototype, { open: function (name, conn) { var self = this; if (!name) @@ -27,4 +26,4 @@ _.extend(Meteor._LocalCollectionDriver.prototype, { }); // singleton -Meteor._LocalCollectionDriver = new Meteor._LocalCollectionDriver; +LocalCollectionDriver = new LocalCollectionDriver; diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index 9311321825..e3fcf835d9 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -12,6 +12,8 @@ var MongoDB = Npm.require('mongodb'); var Fiber = Npm.require('fibers'); var Future = Npm.require(path.join('fibers', 'future')); +MongoInternals = {}; + var replaceNames = function (filter, thing) { if (typeof thing === "object") { if (_.isArray(thing)) { @@ -82,7 +84,7 @@ var replaceTypes = function (document, atomTransformer) { }; -_Mongo = function (url) { +MongoConnection = function (url) { var self = this; self._connectCallbacks = []; self._liveResultsSets = {}; @@ -122,7 +124,7 @@ _Mongo = function (url) { }); }; -_Mongo.prototype.close = function() { +MongoConnection.prototype.close = function() { var self = this; // Use Future.wrap so that errors get thrown. This happens to // work even outside a fiber since the 'close' method is not @@ -130,7 +132,7 @@ _Mongo.prototype.close = function() { Future.wrap(_.bind(self.db.close, self.db))(true).wait(); }; -_Mongo.prototype._withDb = function (callback) { +MongoConnection.prototype._withDb = function (callback) { var self = this; if (self.db) { callback(self.db); @@ -140,7 +142,7 @@ _Mongo.prototype._withDb = function (callback) { }; // Returns the Mongo Collection object; may yield. -_Mongo.prototype._getCollection = function (collectionName) { +MongoConnection.prototype._getCollection = function (collectionName) { var self = this; var future = new Future; @@ -150,7 +152,8 @@ _Mongo.prototype._getCollection = function (collectionName) { return future.wait(); }; -_Mongo.prototype._createCappedCollection = function (collectionName, byteSize) { +MongoConnection.prototype._createCappedCollection = function (collectionName, + byteSize) { var self = this; var future = new Future(); self._withDb(function (db) { @@ -165,9 +168,9 @@ _Mongo.prototype._createCappedCollection = function (collectionName, byteSize) { // the write, and after observers have been notified (or at least, // after the observer notifiers have added themselves to the write // fence), you should call 'committed()' on the object returned. -_Mongo.prototype._maybeBeginWrite = function () { +MongoConnection.prototype._maybeBeginWrite = function () { var self = this; - var fence = Meteor._CurrentWriteFence.get(); + var fence = DDPServer._CurrentWriteFence.get(); if (fence) return fence.beginWrite(); else @@ -185,7 +188,7 @@ _Mongo.prototype._maybeBeginWrite = function () { // After making a write (with insert, update, remove), observers are // notified asynchronously. If you want to receive a callback once all // of the observer notifications have landed for your write, do the -// writes inside a write fence (set Meteor._CurrentWriteFence to a new +// writes inside a write fence (set DDPServer._CurrentWriteFence to a new // _WriteFence, and then set a callback on the write fence.) // // Since our execution environment is single-threaded, this is @@ -208,7 +211,8 @@ var writeCallback = function (write, refresh, callback) { }); }; -_Mongo.prototype._insert = function (collection_name, document, callback) { +MongoConnection.prototype._insert = function (collection_name, document, + callback) { var self = this; if (collection_name === "___meteor_failure_test_collection") { var e = new Error("Failure test"); @@ -236,7 +240,7 @@ _Mongo.prototype._insert = function (collection_name, document, callback) { // Cause queries that may be affected by the selector to poll in this write // fence. -_Mongo.prototype._refresh = function (collectionName, selector) { +MongoConnection.prototype._refresh = function (collectionName, selector) { var self = this; var refreshKey = {collection: collectionName}; // If we know which documents we're removing, don't poll queries that are @@ -253,7 +257,8 @@ _Mongo.prototype._refresh = function (collectionName, selector) { } }; -_Mongo.prototype._remove = function (collection_name, selector, callback) { +MongoConnection.prototype._remove = function (collection_name, selector, + callback) { var self = this; if (collection_name === "___meteor_failure_test_collection") { @@ -281,8 +286,8 @@ _Mongo.prototype._remove = function (collection_name, selector, callback) { } }; -_Mongo.prototype._update = function (collection_name, selector, mod, - options, callback) { +MongoConnection.prototype._update = function (collection_name, selector, mod, + options, callback) { var self = this; if (! callback && options instanceof Function) { @@ -330,13 +335,13 @@ _Mongo.prototype._update = function (collection_name, selector, mod, }; _.each(["insert", "update", "remove"], function (method) { - _Mongo.prototype[method] = function (/* arguments */) { + MongoConnection.prototype[method] = function (/* arguments */) { var self = this; return Meteor._wrapAsync(self["_" + method]).apply(self, arguments); }; }); -_Mongo.prototype.find = function (collectionName, selector, options) { +MongoConnection.prototype.find = function (collectionName, selector, options) { var self = this; if (arguments.length === 1) @@ -346,7 +351,8 @@ _Mongo.prototype.find = function (collectionName, selector, options) { self, new CursorDescription(collectionName, selector, options)); }; -_Mongo.prototype.findOne = function (collection_name, selector, options) { +MongoConnection.prototype.findOne = function (collection_name, selector, + options) { var self = this; if (arguments.length === 1) selector = {}; @@ -358,7 +364,8 @@ _Mongo.prototype.findOne = function (collection_name, selector, options) { // We'll actually design an index API later. For now, we just pass through to // Mongo's, but make it synchronous. -_Mongo.prototype._ensureIndex = function (collectionName, index, options) { +MongoConnection.prototype._ensureIndex = function (collectionName, index, + options) { var self = this; options = _.extend({safe: true}, options); @@ -369,7 +376,7 @@ _Mongo.prototype._ensureIndex = function (collectionName, index, options) { var indexName = collection.ensureIndex(index, options, future.resolver()); future.wait(); }; -_Mongo.prototype._dropIndex = function (collectionName, index) { +MongoConnection.prototype._dropIndex = function (collectionName, index) { var self = this; // This function is only used by test code, not within a method, so we don't @@ -474,8 +481,8 @@ Cursor.prototype.observeChanges = function (callbacks) { self._cursorDescription, ordered, callbacks); }; -_Mongo.prototype._createSynchronousCursor = function (cursorDescription, - useTransform) { +MongoConnection.prototype._createSynchronousCursor = function(cursorDescription, + useTransform) { var self = this; var collection = self._getCollection(cursorDescription.collectionName); @@ -539,7 +546,7 @@ _.extend(SynchronousCursor.prototype, { // ignore this one. (Do this before the transform, since transform might // return some unrelated value.) We don't do this for tailable cursors, // because we want to maintain O(1) memory usage. - var strId = Meteor.idStringify(doc._id); + var strId = LocalCollection._idStringify(doc._id); if (self._visitedIds[strId]) continue; self._visitedIds[strId] = true; } @@ -637,7 +644,7 @@ ObserveHandle.prototype.stop = function () { self._liveResultsSet = null; }; -_Mongo.prototype._observeChanges = function ( +MongoConnection.prototype._observeChanges = function ( cursorDescription, ordered, callbacks) { var self = this; @@ -732,12 +739,12 @@ var LiveResultsSet = function (cursorDescription, mongoHandle, ordered, // database for changes. If this selector specifies specific IDs, specify them // here, so that updates to different specific IDs don't cause us to poll. var listenOnTrigger = function (trigger) { - var listener = Meteor._InvalidationCrossbar.listen( + var listener = DDPServer._InvalidationCrossbar.listen( trigger, function (notification, complete) { // When someone does a transaction that might affect us, schedule a poll // of the database. If that transaction happens inside of a write fence, // block the fence until we've polled and notified observers. - var fence = Meteor._CurrentWriteFence.get(); + var fence = DDPServer._CurrentWriteFence.get(); if (fence) self._pendingWrites.push(fence.beginWrite()); // Ensure a poll is scheduled... but if we already know that one is, @@ -962,7 +969,7 @@ _.extend(LiveResultsSet.prototype, { if (_.isEmpty(self._observeHandles) && self._addHandleTasksScheduledButNotPerformed === 0) { // The last observe handle was stopped; call our stop callbacks, which: - // - removes us from the _Mongo's _liveResultsSets map + // - removes us from the MongoConnection's _liveResultsSets map // - stops the poll timer // - removes us from the invalidation crossbar _.each(self._stopCallbacks, function (c) { c(); }); @@ -982,9 +989,9 @@ _.extend(LiveResultsSet.prototype, { // - If you disconnect and reconnect from Mongo, it will essentially restart // the query, which will lead to duplicate results. This is pretty bad, // but if you include a field called 'ts' which is inserted as -// new Meteor._Mongo._Timestamp(0, 0) (which is initialized to the current -// Mongo-style timestamp), we'll be able to find the place to restart -// properly. (This field is specifically understood by Mongo with an +// new MongoInternals.MongoTimestamp(0, 0) (which is initialized to the +// current Mongo-style timestamp), we'll be able to find the place to +// restart properly. (This field is specifically understood by Mongo with an // optimization which allows it to find the right place to start without // an index on ts. It's how the oplog works.) // - No callbacks are triggered synchronously with the call (there's no @@ -1001,7 +1008,7 @@ _.extend(LiveResultsSet.prototype, { // enough to accurately evaluate the query against the write fence, we // should be able to do this... Of course, minimongo doesn't even support // Mongo Timestamps yet. -_Mongo.prototype._observeChangesTailable = function ( +MongoConnection.prototype._observeChangesTailable = function ( cursorDescription, ordered, callbacks) { var self = this; @@ -1069,7 +1076,9 @@ _Mongo.prototype._observeChangesTailable = function ( }; }; -_.extend(Meteor, { - _Mongo: _Mongo -}); -Meteor._Mongo._Timestamp = MongoDB.Timestamp; +// XXX We probably need to find a better way to expose this. Right now +// it's only used by tests, but in fact you need it in normal +// operation to interact with capped collections (eg, Galaxy uses it). +MongoInternals.MongoTimestamp = MongoDB.Timestamp; + +MongoInternals.Connection = MongoConnection; diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index ed339bbcff..0cc15c0979 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -124,8 +124,8 @@ Tinytest.addAsync("mongo-livedata - basics, " + idGeneration, function (test, on if (Meteor.isClient) { f(); } else { - var fence = new Meteor._WriteFence; - Meteor._CurrentWriteFence.withValue(fence, f); + var fence = new DDPServer._WriteFence; + DDPServer._CurrentWriteFence.withValue(fence, f); fence.armAndWait(); } @@ -281,8 +281,8 @@ Tinytest.addAsync("mongo-livedata - fuzz test, " + idGeneration, function(test, if (Meteor.isClient) { f(); } else { - var fence = new Meteor._WriteFence; - Meteor._CurrentWriteFence.withValue(fence, f); + var fence = new DDPServer._WriteFence; + DDPServer._CurrentWriteFence.withValue(fence, f); fence.armAndWait(); } }; @@ -360,8 +360,8 @@ var runInFence = function (f) { if (Meteor.isClient) { f(); } else { - var fence = new Meteor._WriteFence; - Meteor._CurrentWriteFence.withValue(fence, f); + var fence = new DDPServer._WriteFence; + DDPServer._CurrentWriteFence.withValue(fence, f); fence.armAndWait(); } }; @@ -744,7 +744,8 @@ testAsyncMulti('mongo-livedata - document goes through a transform, ' + idGenera testAsyncMulti('mongo-livedata - document with binary data, ' + idGeneration, [ function (test, expect) { - var bin = EJSON._base64Decode( + // XXX probably shouldn't use EJSON's private test symbols + var bin = EJSONTest.base64Decode( "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyBy" + "ZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJv" + "bSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhl" + @@ -875,8 +876,12 @@ if (Meteor.isServer) { Tinytest.add('mongo-livedata - rewrite selector', function (test) { test.equal(Meteor.Collection._rewriteSelector({x: /^o+B/im}), {x: {$regex: '^o+B', $options: 'im'}}); + test.equal(Meteor.Collection._rewriteSelector({x: {$regex: /^o+B/im}}), + {x: {$regex: '^o+B', $options: 'im'}}); test.equal(Meteor.Collection._rewriteSelector({x: /^o+B/}), {x: {$regex: '^o+B'}}); + test.equal(Meteor.Collection._rewriteSelector({x: {$regex: /^o+B/}}), + {x: {$regex: '^o+B'}}); test.equal(Meteor.Collection._rewriteSelector('foo'), {_id: 'foo'}); @@ -885,13 +890,15 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {'$or': [ {x: /^o/}, {y: /^p/}, - {z: 'q'} + {z: 'q'}, + {w: {$regex: /^r/}} ]} ), {'$or': [ {x: {$regex: '^o'}}, {y: {$regex: '^p'}}, - {z: 'q'} + {z: 'q'}, + {w: {$regex: '^r'}} ]} ); @@ -900,22 +907,35 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {'$or': [ {'$and': [ {x: /^a/i}, - {y: /^b/} + {y: /^b/}, + {z: {$regex: /^c/i}}, + {w: {$regex: '^[abc]', $options: 'i'}}, // make sure we don't break vanilla selectors + {v: {$regex: /O/, $options: 'i'}}, // $options should override the ones on the RegExp object + {u: {$regex: /O/m, $options: 'i'}} // $options should override the ones on the RegExp object ]}, {'$nor': [ - {s: /^c/}, - {t: /^d/i} + {s: /^d/}, + {t: /^e/i}, + {u: {$regex: /^f/i}}, + // even empty string overrides built-in flags + {v: {$regex: /^g/i, $options: ''}} ]} ]} ), {'$or': [ {'$and': [ {x: {$regex: '^a', $options: 'i'}}, - {y: {$regex: '^b'}} + {y: {$regex: '^b'}}, + {z: {$regex: '^c', $options: 'i'}}, + {w: {$regex: '^[abc]', $options: 'i'}}, + {v: {$regex: 'O', $options: 'i'}}, + {u: {$regex: 'O', $options: 'i'}} ]}, {'$nor': [ - {s: {$regex: '^c'}}, - {t: {$regex: '^d', $options: 'i'}} + {s: {$regex: '^d'}}, + {t: {$regex: '^e', $options: 'i'}}, + {u: {$regex: '^f', $options: 'i'}}, + {v: {$regex: '^g', $options: ''}} ]} ]} ); @@ -969,7 +989,7 @@ if (Meteor.isServer) { return C.find({a: 0}); }); - self.conn = Meteor.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(Meteor.absoluteUrl()); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); @@ -1022,7 +1042,7 @@ if (Meteor.isServer) { return self.C.find(); }); - self.conn = Meteor.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(Meteor.absoluteUrl()); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); diff --git a/packages/mongo-livedata/observe_changes_tests.js b/packages/mongo-livedata/observe_changes_tests.js index 1b1dea01bb..94efe37af8 100644 --- a/packages/mongo-livedata/observe_changes_tests.js +++ b/packages/mongo-livedata/observe_changes_tests.js @@ -206,7 +206,8 @@ if (Meteor.isServer) { self.xs = []; self.expects = []; self.insert = function (fields) { - coll.insert(_.extend({ts: new Meteor._Mongo._Timestamp(0, 0)}, fields)); + coll.insert(_.extend({ts: new MongoInternals.MongoTimestamp(0, 0)}, + fields)); }; // Tailable observe shouldn't show things that are in the initial diff --git a/packages/mongo-livedata/package.js b/packages/mongo-livedata/package.js index df8acf2517..adc83c3862 100644 --- a/packages/mongo-livedata/package.js +++ b/packages/mongo-livedata/package.js @@ -8,11 +8,11 @@ // minutiae. Package.describe({ - summary: "Adaptor for using MongoDB and Minimongo over Livedata", + summary: "Adaptor for using MongoDB and Minimongo over DDP", internal: true }); -Npm.depends({mongodb: "1.3.7"}); +Npm.depends({mongodb: "1.3.12"}); Package.on_use(function (api) { api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging', @@ -20,6 +20,19 @@ Package.on_use(function (api) { ['client', 'server']); api.use('check', ['client', 'server']); + // Allow us to detect 'insecure'. + api.use('insecure', {weak: true}); + + // Allow us to detect 'autopublish', and publish collections if it's loaded. + api.use('autopublish', 'server', {weak: true}); + + // defaultRemoteCollectionDriver gets its deployConfig from something that is + // (for questionable reasons) initialized by the webapp package. + api.use('webapp', 'server', {weak: true}); + + // Stuff that should be exposed via a real API, but we haven't yet. + api.export('MongoInternals', 'server'); + api.add_files('mongo_driver.js', 'server'); api.add_files('local_collection_driver.js', ['client', 'server']); api.add_files('remote_collection_driver.js', 'server'); @@ -29,7 +42,8 @@ Package.on_use(function (api) { Package.on_test(function (api) { api.use('mongo-livedata'); api.use('check'); - api.use(['tinytest', 'underscore', 'test-helpers', 'ejson', 'random']); + api.use(['tinytest', 'underscore', 'test-helpers', 'ejson', 'random', + 'livedata']); // XXX test order dependency: the allow_tests "partial allow" test // fails if it is run before mongo_livedata_tests. api.add_files('mongo_livedata_tests.js', ['client', 'server']); @@ -37,4 +51,3 @@ Package.on_test(function (api) { api.add_files('collection_tests.js', ['client', 'server']); api.add_files('observe_changes_tests.js', ['client', 'server']); }); - diff --git a/packages/mongo-livedata/remote_collection_driver.js b/packages/mongo-livedata/remote_collection_driver.js index ed4f6a5cf7..6ba05d0679 100644 --- a/packages/mongo-livedata/remote_collection_driver.js +++ b/packages/mongo-livedata/remote_collection_driver.js @@ -1,10 +1,9 @@ -// XXX namespacing -Meteor._RemoteCollectionDriver = function (mongo_url) { +MongoInternals.RemoteCollectionDriver = function (mongo_url) { var self = this; - self.mongo = new Meteor._Mongo(mongo_url); + self.mongo = new MongoConnection(mongo_url); }; -_.extend(Meteor._RemoteCollectionDriver.prototype, { +_.extend(MongoInternals.RemoteCollectionDriver.prototype, { open: function (name) { var self = this; var ret = {}; @@ -19,17 +18,18 @@ _.extend(Meteor._RemoteCollectionDriver.prototype, { }); -// Create the singleton _RemoteCollectionDriver only on demand, so we +// Create the singleton RemoteCollectionDriver only on demand, so we // only require Mongo configuration if it's actually used (eg, not if // you're only trying to receive data from a remote DDP server.) -Meteor._getRemoteCollectionDriver = _.once(function () { +MongoInternals.defaultRemoteCollectionDriver = _.once(function () { // XXX kind of hacky - var mongoUrl = (typeof __meteor_bootstrap__ !== 'undefined' && - Meteor._get(__meteor_bootstrap__.deployConfig, - 'packages', 'mongo-livedata', 'url')); + var mongoUrl = ( + typeof __meteor_bootstrap__ !== 'undefined' && + Meteor._get(__meteor_bootstrap__, + 'deployConfig', 'packages', 'mongo-livedata', 'url')); // XXX bad error since it could also be set directly in METEOR_DEPLOY_CONFIG if (! mongoUrl) throw new Error("MONGO_URL must be set in environment"); - return new Meteor._RemoteCollectionDriver(mongoUrl); + return new MongoInternals.RemoteCollectionDriver(mongoUrl); }); diff --git a/packages/oauth/oauth_client.js b/packages/oauth/oauth_client.js index 4d59040721..2b3c2a61a0 100644 --- a/packages/oauth/oauth_client.js +++ b/packages/oauth/oauth_client.js @@ -1,3 +1,5 @@ +Oauth = {}; + // Open a popup window pointing to a OAuth handshake page // // @param credentialToken {String} The OAuth credentialToken generated by the client diff --git a/packages/oauth/oauth_common.js b/packages/oauth/oauth_common.js deleted file mode 100644 index 187cbcab0b..0000000000 --- a/packages/oauth/oauth_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Oauth -Oauth = {}; diff --git a/packages/oauth/oauth_server.js b/packages/oauth/oauth_server.js index e71d005a23..e61e152582 100644 --- a/packages/oauth/oauth_server.js +++ b/packages/oauth/oauth_server.js @@ -1,10 +1,16 @@ var Fiber = Npm.require('fibers'); +Oauth = {}; +OauthTest = {}; + RoutePolicy.declare('/_oauth/', 'network'); -Oauth._services = {}; +var registeredServices = {}; -// Maps from service version to handler function. +// Internal: Maps from service version to handler function. The +// 'oauth1' and 'oauth2' packages manipulate this directly to register +// for callbacks. +// Oauth._requestHandlers = {}; @@ -23,11 +29,12 @@ Oauth._requestHandlers = {}; // - {serviceData:, (optional options:)} where serviceData should end // up in the user's services[name] field // - `null` if the user declined to give permissions +// Oauth.registerService = function (name, version, urls, handleOauthRequest) { - if (Oauth._services[name]) + if (registeredServices[name]) throw new Error("Already registered the " + name + " OAuth service"); - Oauth._services[name] = { + registeredServices[name] = { serviceName: name, version: version, urls: urls, @@ -35,9 +42,9 @@ Oauth.registerService = function (name, version, urls, handleOauthRequest) { }; }; -// For test cleanup only. -Oauth._unregisterService = function (name) { - delete Oauth._services[name]; +// For test cleanup. +OauthTest.unregisterService = function (name) { + delete registeredServices[name]; }; @@ -46,7 +53,11 @@ Oauth._unregisterService = function (name) { // results are stored in this map which is then read when the login // method is called. Maps credentialToken --> return value of `login` // +// NB: the oauth1 and oauth2 packages manipulate this directly. might +// be nice for them to have a setter instead +// // XXX we should periodically clear old entries +// Oauth._loginResultForCredentialToken = {}; Oauth.hasCredential = function(credentialToken) { @@ -64,12 +75,11 @@ WebApp.connectHandlers.use(function(req, res, next) { // Need to create a Fiber since we're using synchronous http calls and nothing // else is wrapping this in a fiber automatically Fiber(function () { - Oauth._middleware(req, res, next); + middleware(req, res, next); }).run(); }); - -Oauth._middleware = function (req, res, next) { +middleware = function (req, res, next) { // Make sure to catch any exceptions because otherwise we'd crash // the runner try { @@ -80,7 +90,7 @@ Oauth._middleware = function (req, res, next) { return; } - var service = Oauth._services[serviceName]; + var service = registeredServices[serviceName]; // Skip everything if there's no service set by the oauth middleware if (!service) @@ -119,6 +129,8 @@ Oauth._middleware = function (req, res, next) { } }; +OauthTest.middleware = middleware; + // Handle /_oauth/* paths and extract the service name // // @returns {String|null} e.g. "facebook", or null if this isn't an @@ -145,6 +157,7 @@ var ensureConfigured = function(serviceName) { }; }; +// Internal: used by the oauth1 and oauth2 packages Oauth._renderOauthResults = function(res, query) { // We support ?close and ?redirect=URL. Any other query should // just serve a blank page diff --git a/packages/oauth/package.js b/packages/oauth/package.js index 53468511c6..85659c45fc 100644 --- a/packages/oauth/package.js +++ b/packages/oauth/package.js @@ -8,7 +8,9 @@ Package.on_use(function (api) { api.use('webapp', 'server'); api.use(['underscore', 'service-configuration'], 'server'); - api.add_files('oauth_common.js', ['client', 'server']); + api.export('Oauth'); + api.export('OauthTest', 'server', {testOnly: true}); + api.add_files('oauth_client.js', 'client'); api.add_files('oauth_server.js', 'server'); }); diff --git a/packages/oauth1/oauth1_binding.js b/packages/oauth1/oauth1_binding.js index 699e52a9b2..b3455e26a9 100644 --- a/packages/oauth1/oauth1_binding.js +++ b/packages/oauth1/oauth1_binding.js @@ -1,7 +1,6 @@ var crypto = Npm.require("crypto"); var querystring = Npm.require("querystring"); -// @export OAuth1Binding // An OAuth1 wrapper around http calls which helps get tokens and // takes care of HTTP headers // @@ -52,7 +51,7 @@ OAuth1Binding.prototype.prepareAccessToken = function(query) { self.accessTokenSecret = tokens.oauth_token_secret; }; -OAuth1Binding.prototype.call = function(method, url, params) { +OAuth1Binding.prototype.call = function(method, url, params, callback) { var self = this; var headers = self._buildHeader({ @@ -63,16 +62,15 @@ OAuth1Binding.prototype.call = function(method, url, params) { params = {}; } - var response = self._call(method, url, headers, params); - return response; + return self._call(method, url, headers, params, callback); }; -OAuth1Binding.prototype.get = function(url, params) { - return this.call('GET', url, params); +OAuth1Binding.prototype.get = function(url, params, callback) { + return this.call('GET', url, params, callback); }; -OAuth1Binding.prototype.post = function(url, params) { - return this.call('POST', url, params); +OAuth1Binding.prototype.post = function(url, params, callback) { + return this.call('POST', url, params, callback); }; OAuth1Binding.prototype._buildHeader = function(headers) { @@ -107,7 +105,7 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64'); }; -OAuth1Binding.prototype._call = function(method, url, headers, params) { +OAuth1Binding.prototype._call = function(method, url, headers, params, callback) { var self = this; // Get the signature @@ -118,12 +116,12 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) { // Make signed request try { - return Meteor.http.call(method, url, { + return HTTP.call(method, url, { params: params, headers: { Authorization: authString } - }); + }, callback); } catch (err) { throw new Error("Failed to send OAuth1 request to " + url + ". " + err.message); } diff --git a/packages/oauth1/oauth1_common.js b/packages/oauth1/oauth1_common.js deleted file mode 100644 index 8518a46fce..0000000000 --- a/packages/oauth1/oauth1_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Oauth1 -Oauth1 = {}; diff --git a/packages/oauth1/oauth1_server.js b/packages/oauth1/oauth1_server.js index a5c9297681..2e2a530020 100644 --- a/packages/oauth1/oauth1_server.js +++ b/packages/oauth1/oauth1_server.js @@ -1,5 +1,7 @@ // A place to store request tokens pending verification -Oauth1._requestTokens = {}; +var requestTokens = {}; + +OAuth1Test = {requestTokens: requestTokens}; // connect middleware Oauth._requestHandlers['1'] = function (service, query, res) { @@ -20,7 +22,7 @@ Oauth._requestHandlers['1'] = function (service, query, res) { oauthBinding.prepareRequestToken(query.requestTokenAndRedirect); // Keep track of request token so we can verify it on the next step - Oauth1._requestTokens[query.state] = oauthBinding.requestToken; + requestTokens[query.state] = oauthBinding.requestToken; // redirect to provider login, which will redirect back to "step 2" below var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; @@ -32,8 +34,8 @@ Oauth._requestHandlers['1'] = function (service, query, res) { // token and access token secret and log in as user // Get the user's request token so we can verify it and clear it - var requestToken = Oauth1._requestTokens[query.state]; - delete Oauth1._requestTokens[query.state]; + var requestToken = requestTokens[query.state]; + delete requestTokens[query.state]; // Verify user authorized access and the oauth_token matches // the requestToken from previous step diff --git a/packages/oauth1/oauth1_tests.js b/packages/oauth1/oauth1_tests.js index 51f286d5ad..d7eb0e1979 100644 --- a/packages/oauth1/oauth1_tests.js +++ b/packages/oauth1/oauth1_tests.js @@ -40,7 +40,7 @@ Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test) }); // simulate logging in using twitterfoo - Oauth1._requestTokens[credentialToken] = twitterfooAccessToken; + OAuth1Test.requestTokens[credentialToken] = twitterfooAccessToken; var req = { method: "POST", @@ -50,7 +50,7 @@ Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test) oauth_token: twitterfooAccessToken } }; - Oauth._middleware(req, new http.ServerResponse(req)); + OauthTest.middleware(req, new http.ServerResponse(req)); // Test that right data is placed on the loginResult map test.equal( @@ -67,7 +67,7 @@ Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test) Oauth._loginResultForCredentialToken[credentialToken].options.option1, twitterOption1); } finally { - Oauth._unregisterService(serviceName); + OauthTest.unregisterService(serviceName); } }); diff --git a/packages/oauth1/package.js b/packages/oauth1/package.js index 7d234e6ba7..c57d98f515 100644 --- a/packages/oauth1/package.js +++ b/packages/oauth1/package.js @@ -8,9 +8,12 @@ Package.on_use(function (api) { api.use('service-configuration', ['client', 'server']); api.use('oauth', ['client', 'server']); api.use('underscore', 'server'); + api.use('http', 'server'); + + api.export('OAuth1Binding', 'server'); + api.export('OAuth1Test', 'server', {testOnly: true}); api.add_files('oauth1_binding.js', 'server'); - api.add_files('oauth1_common.js', ['client', 'server']); api.add_files('oauth1_server.js', 'server'); }); diff --git a/packages/oauth2/oauth2_common.js b/packages/oauth2/oauth2_common.js deleted file mode 100644 index a2d3d97127..0000000000 --- a/packages/oauth2/oauth2_common.js +++ /dev/null @@ -1 +0,0 @@ -Oauth2 = {}; diff --git a/packages/oauth2/oauth2_tests.js b/packages/oauth2/oauth2_tests.js index 5bc23e3bb5..00cd88e7d3 100644 --- a/packages/oauth2/oauth2_tests.js +++ b/packages/oauth2/oauth2_tests.js @@ -20,7 +20,7 @@ Tinytest.add("oauth2 - loginResultForCredentialToken is stored", function (test) var req = {method: "POST", url: "/_oauth/" + serviceName + "?close", query: {state: credentialToken}}; - Oauth._middleware(req, new http.ServerResponse(req)); + OauthTest.middleware(req, new http.ServerResponse(req)); // Test that the login result for that user is prepared test.equal( @@ -31,6 +31,6 @@ Tinytest.add("oauth2 - loginResultForCredentialToken is stored", function (test) Oauth._loginResultForCredentialToken[credentialToken].options.option1, foobookOption1); } finally { - Oauth._unregisterService(serviceName); + OauthTest.unregisterService(serviceName); } }); diff --git a/packages/oauth2/package.js b/packages/oauth2/package.js index cdb8db2f84..c2b6f6dccf 100644 --- a/packages/oauth2/package.js +++ b/packages/oauth2/package.js @@ -7,7 +7,6 @@ Package.on_use(function (api) { api.use('service-configuration', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.add_files('oauth2_common.js', ['client', 'server']); api.add_files('oauth2_server.js', 'server'); }); diff --git a/packages/ordered-dict/ordered_dict.js b/packages/ordered-dict/ordered_dict.js index c757f5d3a7..702c65a2ea 100644 --- a/packages/ordered-dict/ordered_dict.js +++ b/packages/ordered-dict/ordered_dict.js @@ -1,4 +1,3 @@ -// @export OrderedDict // This file defines an ordered dictionary abstraction that is useful for // maintaining a dataset backed by observeChanges. It supports ordering items // by specifying the item they now come before. diff --git a/packages/ordered-dict/package.js b/packages/ordered-dict/package.js index d7e4136f8e..6118b8d588 100644 --- a/packages/ordered-dict/package.js +++ b/packages/ordered-dict/package.js @@ -5,5 +5,6 @@ Package.describe({ Package.on_use(function (api) { api.use('underscore'); + api.export('OrderedDict'); api.add_files('ordered_dict.js', ['client', 'server']); }); diff --git a/packages/past/client_past_test.js b/packages/past/client_past_test.js deleted file mode 100644 index 1f89691fbe..0000000000 --- a/packages/past/client_past_test.js +++ /dev/null @@ -1,4 +0,0 @@ -Tinytest.add("past - client", function (test) { - test.isTrue(Meteor.is_client); - test.isFalse(Meteor.is_server); -}); diff --git a/packages/past/package.js b/packages/past/package.js deleted file mode 100644 index 6d81f62056..0000000000 --- a/packages/past/package.js +++ /dev/null @@ -1,18 +0,0 @@ -Package.describe({ - summary: "Backwards compatibility.", - internal: true -}); - -Package.on_use(function (api) { - api.use('deps'); - api.use('random'); - api.add_files('past.js', ['client', 'server']); -}); - -Package.on_test(function (api) { - api.use('past'); - api.use('tinytest'); - - api.add_files('client_past_test.js', 'client'); - api.add_files('server_past_test.js', 'server'); -}); diff --git a/packages/past/past.js b/packages/past/past.js deleted file mode 100644 index 7bdffe3263..0000000000 --- a/packages/past/past.js +++ /dev/null @@ -1,43 +0,0 @@ -// This file is used to set up aliases and methods to preserve backwards -// compatibility on some deprecated methods. Care should be taken when -// adding aliases and methods that the target will not be undefined, as -// the past package is loaded early. In some cases, it may be best to -// define the alias in the package it refers to. - -// Old under_score version of camelCase public API names. -Meteor.is_client = Meteor.isClient; -Meteor.is_server = Meteor.isServer; - -// See also the "this.is_simulation" assignment in livedata/livedata_common.js -// and the retry_count and retry_time fields of self.current_status in -// stream/stream_client.js. - - -// We used to require a special "autosubscribe" call to reactively subscribe to -// things. Now, it works with autorun. -Meteor.autosubscribe = Deps.autorun; - -// "new deps" back-compat -Meteor.flush = Deps.flush; -Meteor.autorun = Deps.autorun; - -// Deps API that briefly existed in 0.5.9 -Deps.depend = function (d) { - return d.depend(); -}; - -// Instead of the "random" package with Random.id(), we used to have this -// Meteor.uuid() implementing the RFC 4122 v4 UUID. -Meteor.uuid = function () { - var HEX_DIGITS = "0123456789abcdef"; - var s = []; - for (var i = 0; i < 36; i++) { - s[i] = Random.choice(HEX_DIGITS); - } - s[14] = "4"; - s[19] = HEX_DIGITS.substr((parseInt(s[19],16) & 0x3) | 0x8, 1); - s[8] = s[13] = s[18] = s[23] = "-"; - - var uuid = s.join(""); - return uuid; -}; diff --git a/packages/past/server_past_test.js b/packages/past/server_past_test.js deleted file mode 100644 index 412d74c3ea..0000000000 --- a/packages/past/server_past_test.js +++ /dev/null @@ -1,4 +0,0 @@ -Tinytest.add("past - server", function (test) { - test.isFalse(Meteor.is_client); - test.isTrue(Meteor.is_server); -}); diff --git a/packages/preserve-inputs/package.js b/packages/preserve-inputs/package.js index f8eca26f9b..f979a2d5d9 100644 --- a/packages/preserve-inputs/package.js +++ b/packages/preserve-inputs/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Automatically preserve all form fields with a unique id" }); -Package.on_use(function (api, where) { +Package.on_use(function (api) { api.use(['underscore', 'spark']); api.add_files("preserve-inputs.js", "client"); }); diff --git a/packages/preserve-inputs/preserve-inputs.js b/packages/preserve-inputs/preserve-inputs.js index e7e6a15643..8d507049ab 100644 --- a/packages/preserve-inputs/preserve-inputs.js +++ b/packages/preserve-inputs/preserve-inputs.js @@ -4,5 +4,4 @@ var selector = _.map(inputTags, function (t) { return t.replace(/^.*$/, '$&[id], $&[name]'); }).join(', '); - -Spark._globalPreserves[selector] = Spark._labelFromIdOrName; +Spark._addGlobalPreserve(selector, Spark._labelFromIdOrName); diff --git a/packages/random/deprecated.js b/packages/random/deprecated.js new file mode 100644 index 0000000000..977909579c --- /dev/null +++ b/packages/random/deprecated.js @@ -0,0 +1,17 @@ +// Before this package existed, we used to use this Meteor.uuid() +// implementing the RFC 4122 v4 UUID. It is no longer documented +// and will go away. +// XXX COMPAT WITH 0.5.6 +Meteor.uuid = function () { + var HEX_DIGITS = "0123456789abcdef"; + var s = []; + for (var i = 0; i < 36; i++) { + s[i] = Random.choice(HEX_DIGITS); + } + s[14] = "4"; + s[19] = HEX_DIGITS.substr((parseInt(s[19],16) & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = "-"; + + var uuid = s.join(""); + return uuid; +}; diff --git a/packages/random/package.js b/packages/random/package.js index 0480c09ac6..f8d5b5ba25 100644 --- a/packages/random/package.js +++ b/packages/random/package.js @@ -3,10 +3,11 @@ Package.describe({ internal: true }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; +Package.on_use(function (api) { api.use('underscore'); - api.add_files('random.js', where); + api.export('Random'); + api.add_files('random.js'); + api.add_files('deprecated.js'); }); Package.on_test(function(api) { diff --git a/packages/random/random.js b/packages/random/random.js index 0e02f3ca73..5faddb9d4c 100644 --- a/packages/random/random.js +++ b/packages/random/random.js @@ -149,7 +149,6 @@ var pid = (typeof process !== 'undefined' && process.pid) || 1; // XXX On the server, use the crypto module (OpenSSL) instead of this PRNG. // (Make Random.fraction be generated from Random.hexString instead of the // other way around, and generate Random.hexString from crypto.randomBytes.) -// @export Random Random = create([ new Date(), height, width, agent, pid, Math.random() ]); diff --git a/packages/reactive-dict/package.js b/packages/reactive-dict/package.js index d78973dbfd..df9b3702e5 100644 --- a/packages/reactive-dict/package.js +++ b/packages/reactive-dict/package.js @@ -3,11 +3,12 @@ Package.describe({ internal: true }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - - api.use(['underscore', 'deps', 'ejson'], where); - api.add_files('reactive-dict.js', where); +Package.on_use(function (api) { + api.use(['underscore', 'deps', 'ejson']); + // If we are loading mongo-livedata, let you store ObjectIDs in it. + api.use('mongo-livedata', {weak: true}); + api.export('ReactiveDict'); + api.add_files('reactive-dict.js'); }); Package.on_test(function (api) { diff --git a/packages/reactive-dict/reactive-dict.js b/packages/reactive-dict/reactive-dict.js index 5dd70e2109..55e68b89dd 100644 --- a/packages/reactive-dict/reactive-dict.js +++ b/packages/reactive-dict/reactive-dict.js @@ -11,8 +11,8 @@ var parse = function (serialized) { return EJSON.parse(serialized); }; -// migrationData, if present, should be data previously returned from getMigrationData() -// @export ReactiveDict +// migrationData, if present, should be data previously returned from +// getMigrationData() ReactiveDict = function (migrationData) { this.keys = migrationData || {}; // key -> value this.keyDeps = {}; // key -> Dependency @@ -62,6 +62,9 @@ _.extend(ReactiveDict.prototype, { equals: function (key, value) { var self = this; + // XXX hardcoded awareness of the 'mongo-livedata' package is not ideal + var ObjectID = Package['mongo-livedata'] && Meteor.Collection.ObjectID; + // We don't allow objects (or arrays that might include objects) for // .equals, because JSON.stringify doesn't canonicalize object key // order. (We can make equals have the right return value by parsing the @@ -76,7 +79,7 @@ _.extend(ReactiveDict.prototype, { typeof value !== 'boolean' && typeof value !== 'undefined' && !(value instanceof Date) && - !(typeof Meteor.Collection !== 'undefined' && value instanceof Meteor.Collection.ObjectID) && + !(ObjectID && value instanceof ObjectID) && value !== null) throw new Error("ReactiveDict.equals: value must be scalar"); var serializedValue = stringify(value); diff --git a/packages/reload/deprecated.js b/packages/reload/deprecated.js new file mode 100644 index 0000000000..a815626d42 --- /dev/null +++ b/packages/reload/deprecated.js @@ -0,0 +1,8 @@ +// Reload functionality used to live on Meteor._reload. Be nice and try not to +// break code that uses it, even though it's internal. +// XXX COMPAT WITH 0.6.4 +Meteor._reload = { + onMigrate: Reload._onMigrate, + migrationData: Reload._migrationData, + reload: Reload._reload +}; diff --git a/packages/reload/package.js b/packages/reload/package.js index 8716bfdc81..cff2922211 100644 --- a/packages/reload/package.js +++ b/packages/reload/package.js @@ -5,5 +5,7 @@ Package.describe({ Package.on_use(function (api) { api.use(['underscore', 'logging', 'json'], 'client'); + api.export('Reload', 'client'); api.add_files('reload.js', 'client'); + api.add_files('deprecated.js', 'client'); }); diff --git a/packages/reload/reload.js b/packages/reload/reload.js index c9fc393626..2c2f412e7e 100644 --- a/packages/reload/reload.js +++ b/packages/reload/reload.js @@ -26,8 +26,6 @@ * the client's session to render properly. */ -Meteor._reload = {}; - var KEY_NAME = 'Meteor_Reload'; // after how long should we consider this no longer an automatic // reload, but a fresh restart. This only happens if a reload is @@ -73,6 +71,8 @@ var providers = []; ////////// External API ////////// +Reload = {}; + // Packages that support migration should register themselves by // calling this function. When it's time to migrate, callback will // be called with one argument, the "retry function." If the package @@ -86,7 +86,8 @@ var providers = []; // will be polled once again for its migration data. If they are all // ready this time, then the migration will happen. name must be set if there // is migration data. -Meteor._reload.onMigrate = function (name, callback) { +// +Reload._onMigrate = function (name, callback) { if (!callback) { // name not provided, so first arg is callback. callback = name; @@ -97,7 +98,8 @@ Meteor._reload.onMigrate = function (name, callback) { // Called by packages when they start up. // Returns the object that was saved, or undefined if none saved. -Meteor._reload.migrationData = function (name) { +// +Reload._migrationData = function (name) { return old_data[name]; }; @@ -106,8 +108,9 @@ Meteor._reload.migrationData = function (name) { // migrate it over. This function returns immediately. The reload // will happen at some point in the future once all of the packages // are ready to migrate. +// var reloading = false; -Meteor._reload.reload = function () { +Reload._reload = function () { if (reloading) return; reloading = true; diff --git a/packages/routepolicy/package.js b/packages/routepolicy/package.js index b9d4514ce3..745ff13e8a 100644 --- a/packages/routepolicy/package.js +++ b/packages/routepolicy/package.js @@ -8,6 +8,8 @@ Package.on_use(function (api) { // Resolve circular dependency with webapp. We can only use WebApp via // Package.webapp and only after initial load. api.use('webapp', 'server', {unordered: true}); + api.export('RoutePolicy', 'server'); + api.export('RoutePolicyTest', 'server', {testOnly: true}); api.add_files('routepolicy.js', 'server'); }); diff --git a/packages/routepolicy/routepolicy.js b/packages/routepolicy/routepolicy.js index 04edb5122a..57e354e15f 100644 --- a/packages/routepolicy/routepolicy.js +++ b/packages/routepolicy/routepolicy.js @@ -24,14 +24,14 @@ // routes would break tinytest... so allow policy instances to be // constructed for testing. -// XXX export is the only way to share with our tests. annoying! -// @export _RoutePolicyConstructor -_RoutePolicyConstructor = function () { +RoutePolicyTest = {}; + +var RoutePolicyConstructor = RoutePolicyTest.Constructor = function () { var self = this; self.urlPrefixTypes = {}; }; -_.extend(_RoutePolicyConstructor.prototype, { +_.extend(RoutePolicyConstructor.prototype, { urlPrefixMatches: function (urlPrefix, url) { return url.substr(0, urlPrefix.length) === urlPrefix; @@ -121,5 +121,4 @@ _.extend(_RoutePolicyConstructor.prototype, { } }); -// @export RoutePolicy -RoutePolicy = new _RoutePolicyConstructor(); +RoutePolicy = new RoutePolicyConstructor(); diff --git a/packages/routepolicy/routepolicy_tests.js b/packages/routepolicy/routepolicy_tests.js index c6efdfe6fc..2c8ccc892e 100644 --- a/packages/routepolicy/routepolicy_tests.js +++ b/packages/routepolicy/routepolicy_tests.js @@ -1,5 +1,5 @@ Tinytest.add("routepolicy - declare", function (test) { - var policy = new _RoutePolicyConstructor(); + var policy = new RoutePolicyTest.Constructor(); policy.declare('/sockjs/', 'network'); policy.declare('/bigphoto.jpg', 'static-online'); @@ -37,7 +37,7 @@ Tinytest.add("routepolicy - static conflicts", function (test) { "url": "/bigphoto.jpg" } ]; - var policy = new _RoutePolicyConstructor(); + var policy = new RoutePolicyTest.Constructor(); test.equal( policy.checkForConflictWithStatic('/sockjs/', 'network', manifest), @@ -51,7 +51,7 @@ Tinytest.add("routepolicy - static conflicts", function (test) { }); Tinytest.add("routepolicy - checkUrlPrefix", function (test) { - var policy = new _RoutePolicyConstructor(); + var policy = new RoutePolicyTest.Constructor(); policy.declare('/sockjs/', 'network'); test.equal( diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index cf7e246a46..b1e338e30c 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -4,5 +4,7 @@ Package.describe({ }); Package.on_use(function(api) { + api.use('mongo-livedata', ['client', 'server']); + api.export('ServiceConfiguration'); api.add_files('service_configuration_common.js', ['client', 'server']); }); diff --git a/packages/service-configuration/service_configuration_common.js b/packages/service-configuration/service_configuration_common.js index e9349565a5..42d5cf75a6 100644 --- a/packages/service-configuration/service_configuration_common.js +++ b/packages/service-configuration/service_configuration_common.js @@ -1,5 +1,4 @@ if (typeof ServiceConfiguration === 'undefined') { - // @export ServiceConfiguration ServiceConfiguration = {}; } diff --git a/packages/session/package.js b/packages/session/package.js index 66d393e255..d0689d05ee 100644 --- a/packages/session/package.js +++ b/packages/session/package.js @@ -6,11 +6,12 @@ Package.describe({ Package.on_use(function (api) { api.use(['underscore', 'reactive-dict', 'ejson'], 'client'); - // XXX what I really want to do is ensure that if 'reload' is going to - // be loaded, it should be loaded before 'session'. Session can work - // with or without reload. - api.use('reload', 'client'); + // Session can work with or without reload, but if reload is present + // it should load first so we can detect it at startup and populate + // the session. + api.use('reload', 'client', {weak: true}); + api.export('Session', 'client'); api.add_files('session.js', 'client'); }); @@ -18,5 +19,6 @@ Package.on_test(function (api) { api.use('tinytest'); api.use('session', 'client'); api.use('deps'); + api.use('mongo-livedata'); api.add_files('session_tests.js', 'client'); }); diff --git a/packages/session/session.js b/packages/session/session.js index e1a37ba780..9426008d1a 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -1,16 +1,15 @@ var migratedKeys = {}; -if (Meteor._reload) { - var migrationData = Meteor._reload.migrationData('session'); +if (Package.reload) { + var migrationData = Package.reload.Reload._migrationData('session'); if (migrationData && migrationData.keys) { migratedKeys = migrationData.keys; } } -// @export Session Session = new ReactiveDict(migratedKeys); -if (Meteor._reload) { - Meteor._reload.onMigrate('session', function () { +if (Package.reload) { + Package.reload.Reload._onMigrate('session', function () { return [true, {keys: Session.keys}]; }); } diff --git a/packages/showdown/package.js b/packages/showdown/package.js index cfc75b3904..f062abb9e2 100644 --- a/packages/showdown/package.js +++ b/packages/showdown/package.js @@ -8,18 +8,11 @@ Package.describe({ var _ = Npm.require('underscore'); -Package.on_use(function (api, where) { - where = where || ["client", "server"]; - where = where instanceof Array ? where : [where]; +Package.on_use(function (api) { + api.add_files("showdown.js"); + api.export('Showdown'); - api.add_files("showdown.js", where); - api.exportSymbol('Showdown', where); - - // XXX what we really want to do is, load template-integration after - // handlebars, iff handlebars was included in the project. - if (where === "client" || - (where instanceof Array && _.indexOf(where, "client") !== -1)) { - api.use("handlebars", "client"); - api.add_files("template-integration.js", "client"); - } + // Define {{markdown}} if handlebars got included. + api.use("handlebars", "client", {weak: true}); + api.add_files("template-integration.js", "client"); }); diff --git a/packages/showdown/template-integration.js b/packages/showdown/template-integration.js index 77808f6c83..f33acb084d 100644 --- a/packages/showdown/template-integration.js +++ b/packages/showdown/template-integration.js @@ -1,4 +1,6 @@ -Handlebars.registerHelper('markdown', function (options) { - var converter = new Showdown.converter(); - return converter.makeHtml(options.fn(this)); -}); +if (Package.handlebars) { + Package.handlebars.Handlebars.registerHelper('markdown', function (options) { + var converter = new Showdown.converter(); + return converter.makeHtml(options.fn(this)); + }); +} diff --git a/packages/spacebars/package.js b/packages/spacebars/package.js index 55fec49a81..b0eb01fcb3 100644 --- a/packages/spacebars/package.js +++ b/packages/spacebars/package.js @@ -2,15 +2,15 @@ Package.describe({ summary: "Handlebars-like template language for Meteor" }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - - api.use('random', where); - api.use('underscore', where); - api.use('jsparse', where); - api.use('html5-tokenizer', where); +Package.on_use(function (api) { + api.export('Spacebars'); + + api.use('random'); + api.use('underscore'); + api.use('jsparse'); + api.use('html5-tokenizer'); api.use('ui'); - api.add_files(['spacebars.js'], where); + api.add_files(['spacebars.js']); }); Package.on_test(function (api) { diff --git a/packages/spacebars/spacebars.js b/packages/spacebars/spacebars.js index e102558498..b1823317ec 100644 --- a/packages/spacebars/spacebars.js +++ b/packages/spacebars/spacebars.js @@ -1,5 +1,4 @@ -// @export Spacebars Spacebars = {}; var makeStacheTagStartRegex = function (r) { @@ -995,4 +994,4 @@ Spacebars.escapeHtmlComment = function (str) { if ((typeof str) === 'string') return str.replace(/--/g, ''); return str; -}; \ No newline at end of file +}; diff --git a/packages/spark/package.js b/packages/spark/package.js index 4c5223dbe8..afcbc42d58 100644 --- a/packages/spark/package.js +++ b/packages/spark/package.js @@ -8,6 +8,9 @@ Package.on_use(function (api) { 'ordered-dict', 'deps', 'ejson'], 'client'); + api.export('Spark', 'client'); + api.export('SparkTest', 'client', {testOnly: true}); + api.add_files(['spark.js', 'patch.js', 'convenience.js', 'utils.js'], 'client'); }); @@ -16,7 +19,7 @@ Package.on_test(function (api) { api.use('webapp', 'server'); api.use(['tinytest', 'underscore', 'liverange', 'deps', 'domutils', 'minimongo', 'random']); - api.use(['spark', 'test-helpers'], 'client'); + api.use(['spark', 'test-helpers', 'jquery'], 'client'); api.add_files('test_form_responder.js', 'server'); diff --git a/packages/spark/patch.js b/packages/spark/patch.js index 07a09d5f9e..7fcfbeb72f 100644 --- a/packages/spark/patch.js +++ b/packages/spark/patch.js @@ -1,12 +1,11 @@ - -Spark._patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations, - results) { +patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations, + results) { var copyFunc = function(t, s) { - LiveRange.transplantTag(Spark._TAG, t, s); + LiveRange.transplantTag(TAG, t, s); }; - var patcher = new Spark._Patcher( + var patcher = new Patcher( tgtParent, srcParent, tgtBefore, tgtAfter); @@ -67,7 +66,7 @@ Spark._patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations // initial contents but may affect the tag's .value in IE) or of // SELECT (which is specially handled in _copyAttributes). // Otherwise recurse! - Spark._patch(tgt, src, null, null, preservations); + patch(tgt, src, null, null, preservations); } } return false; // tell visitNodes not to recurse @@ -105,7 +104,7 @@ Spark._patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations // in their place. // // Constructor: -Spark._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { +Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { this.tgtParent = tgtParent; this.srcParent = srcParent; @@ -161,7 +160,7 @@ Spark._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { // copyCallback is called on every new matched (tgt, src) pair // right after copying attributes. It's a good time to transplant // liveranges and patch children. -Spark._Patcher.prototype.match = function( +Patcher.prototype.match = function( tgtNode, srcNode, copyCallback, onlyAdvance) { // last nodes "kept" (matched/identified with each other) @@ -246,7 +245,7 @@ Spark._Patcher.prototype.match = function( while (true) { if (! (firstIter && onlyAdvance)) { if (tgt.nodeType === 1) /* ELEMENT */ - Spark._Patcher._copyAttributes(tgt, src); + Patcher._copyAttributes(tgt, src); if (copyCallback) copyCallback(tgt, src); } @@ -275,7 +274,7 @@ Spark._Patcher.prototype.match = function( // After a match, skip ahead to later siblings of the last kept nodes, // without performing any replacements. -Spark._Patcher.prototype.skipToSiblings = function(tgt, src) { +Patcher.prototype.skipToSiblings = function(tgt, src) { var lastTgt = this.lastKeptTgtNode; var lastSrc = this.lastKeptSrcNode; @@ -295,7 +294,7 @@ Spark._Patcher.prototype.skipToSiblings = function(tgt, src) { // // Patchers are single-use, so no more methods can be called // on the Patcher. -Spark._Patcher.prototype.finish = function() { +Patcher.prototype.finish = function() { return this.match(null, null); }; @@ -305,7 +304,7 @@ Spark._Patcher.prototype.finish = function() { // // Precondition: tgtBefore and tgtAfter have same parent; either may be falsy, // but not both, unless optTgtParent is provided. Same with srcBefore/srcAfter. -Spark._Patcher.prototype._replaceNodes = function( +Patcher.prototype._replaceNodes = function( tgtBefore, tgtAfter, srcBefore, srcAfter, optTgtParent, optSrcParent) { var tgtParent = optTgtParent || (tgtBefore || tgtAfter).parentNode; @@ -347,7 +346,7 @@ Spark._Patcher.prototype._replaceNodes = function( // // This is complicated by form controls and the fact that old IE // can't keep the difference straight between properties and attributes. -Spark._Patcher._copyAttributes = function(tgt, src) { +Patcher._copyAttributes = function(tgt, src) { var srcAttrs = src.attributes; var tgtAttrs = tgt.attributes; @@ -571,3 +570,5 @@ Spark._Patcher._copyAttributes = function(tgt, src) { tgt.removeAttribute("checked"); } }; + +SparkTest.Patcher = Patcher; diff --git a/packages/spark/patch_tests.js b/packages/spark/patch_tests.js index 4d2d698ae8..a5360b4c5f 100644 --- a/packages/spark/patch_tests.js +++ b/packages/spark/patch_tests.js @@ -1,6 +1,6 @@ Tinytest.add("spark - patch - basic", function(test) { - var Patcher = Spark._Patcher; + var Patcher = SparkTest.Patcher; var div = function(html) { var n = document.createElement("DIV"); @@ -144,7 +144,7 @@ Tinytest.add("spark - patch - copyAttributes", function(test) { if (! node) { node = n; } else { - Spark._Patcher._copyAttributes(node, n); + SparkTest.Patcher._copyAttributes(node, n); } lastAttrs = {}; _.each(allAttrNames, function(v,k) { diff --git a/packages/spark/spark.js b/packages/spark/spark.js index 7117cdc97d..7c3cf27911 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -25,10 +25,10 @@ // timer' button again. the problem is almost certainly in afterFlush // (not hard to see what it is.) -// @export Spark Spark = {}; +SparkTest = {}; -Spark._currentRenderer = (function () { +var currentRenderer = (function () { var current = null; return { get: function () { @@ -43,32 +43,49 @@ Spark._currentRenderer = (function () { }; })(); -Spark._TAG = "_spark_" + Random.id(); +TAG = "_spark_" + Random.id(); +SparkTest.TAG = TAG; + +// We also export this as Spark._TAG due to a historical accident. I +// don't know if anything uses it (possibly some of Chris Mather's +// stuff?) but let's keep exporting it since without it it would be +// very difficult for code outside the spark package to, eg, walk +// spark's liverange hierarchy. +Spark._TAG = TAG; + // XXX document contract for each type of annotation? -Spark._ANNOTATION_NOTIFY = "notify"; -Spark._ANNOTATION_DATA = "data"; -Spark._ANNOTATION_ISOLATE = "isolate"; -Spark._ANNOTATION_EVENTS = "events"; -Spark._ANNOTATION_WATCH = "watch"; -Spark._ANNOTATION_LABEL = "label"; -Spark._ANNOTATION_LANDMARK = "landmark"; -Spark._ANNOTATION_LIST = "list"; -Spark._ANNOTATION_LIST_ITEM = "item"; +var ANNOTATION_NOTIFY = "notify"; +var ANNOTATION_DATA = "data"; +var ANNOTATION_ISOLATE = "isolate"; +var ANNOTATION_EVENTS = "events"; +var ANNOTATION_WATCH = "watch"; +var ANNOTATION_LABEL = "label"; +var ANNOTATION_LANDMARK = "landmark"; +var ANNOTATION_LIST = "list"; +var ANNOTATION_LIST_ITEM = "item"; // XXX why do we need, eg, _ANNOTATION_ISOLATE? it has no semantics? -// Set in tests to turn on extra UniversalEventListener sanity checks -Spark._checkIECompliance = false; +// Use from tests to turn on extra UniversalEventListener sanity checks +var checkIECompliance = false; +SparkTest.setCheckIECompliance = function (value) { + checkIECompliance = value; +}; + +// Private interface to 'preserve-inputs' package +var globalPreserves = {}; +Spark._addGlobalPreserve = function (selector, value) { + globalPreserves[selector] = value; +}; -Spark._globalPreserves = {}; var makeRange = function (type, start, end, inner) { - var range = new LiveRange(Spark._TAG, start, end, inner); + var range = new LiveRange(TAG, start, end, inner); range.type = type; return range; }; var findRangeOfType = function (type, node) { - var range = LiveRange.findRange(Spark._TAG, node); + var range = LiveRange.findRange(TAG, node); while (range && range.type !== type) range = range.findParent(); @@ -84,9 +101,9 @@ var findParentOfType = function (type, range) { }; var notifyWatchers = function (start, end) { - var tempRange = new LiveRange(Spark._TAG, start, end, true /* innermost */); + var tempRange = new LiveRange(TAG, start, end, true /* innermost */); for (var walk = tempRange; walk; walk = walk.findParent()) - if (walk.type === Spark._ANNOTATION_WATCH) + if (walk.type === ANNOTATION_WATCH) walk.notify(); tempRange.destroy(); }; @@ -105,7 +122,7 @@ var withEventGuard = function (func) { finally { eventGuardActive = previous; } }; -Spark._Renderer = function () { +Renderer = function () { // Map from annotation ID to an annotation function, which is called // at render time and receives (startNode, endNode). this.annotations = {}; @@ -131,7 +148,7 @@ Spark._Renderer = function () { this.pc = new PreservationController; }; -_.extend(Spark._Renderer.prototype, { +_.extend(Renderer.prototype, { // `what` can be a function that takes a LiveRange, or just a set of // attributes to add to the liverange. type and what are optional. // if no type is passed, no liverange will be created. @@ -209,7 +226,7 @@ _.extend(Spark._Renderer.prototype, { materialize: function (htmlFunc) { var self = this; - var html = Spark._currentRenderer.withValue(self, htmlFunc); + var html = currentRenderer.withValue(self, htmlFunc); html = self.annotate(html); // wrap with an anonymous annotation var fragById = {}; @@ -321,7 +338,7 @@ _.extend(Spark._Renderer.prototype, { // if there isn't one returns `html` (the last argument). var withRenderer = function (f) { return function (/* arguments */) { - var renderer = Spark._currentRenderer.get(); + var renderer = currentRenderer.get(); var args = _.toArray(arguments); if (!renderer) return args.pop(); @@ -345,7 +362,7 @@ var withRenderer = function (f) { // can call it when manually inserting nodes? (via, eg, jQuery?) -- of // course in that case 'landmarkRanges' would be empty. var scheduleOnscreenSetup = function (frag, landmarkRanges) { - var renderedRange = new LiveRange(Spark._TAG, frag); + var renderedRange = new LiveRange(TAG, frag); var finalized = false; renderedRange.finalize = function () { finalized = true; @@ -395,7 +412,7 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) { // future: include an argument in the callback to distinguish this // case from the previous var walk = renderedRange; - while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) + while ((walk = findParentOfType(ANNOTATION_LANDMARK, walk))) walk.rendered.call(walk.landmark); // This code can run several times on the same nodes (if the @@ -408,7 +425,7 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) { }; Spark.render = function (htmlFunc) { - var renderer = new Spark._Renderer; + var renderer = new Renderer; var frag = renderer.materialize(htmlFunc); return frag; }; @@ -489,12 +506,12 @@ _.extend(PreservationController.prototype, { // to temporarily put these in the document as well, because CSS selectors // don't care and we will put them back. `tempRange` will hold our place // in the tree `newRange` came from. - var tempRange = new LiveRange(Spark._TAG, newRange.firstNode(), newRange.lastNode()); + var tempRange = new LiveRange(TAG, newRange.firstNode(), newRange.lastNode()); var commentFrag = document.createDocumentFragment(); commentFrag.appendChild(document.createComment("")); var newRangeFrag = tempRange.replaceContents(commentFrag); // `wrapperRange` will mark where we inserted newRange into the document. - var wrapperRange = new LiveRange(Spark._TAG, newRangeFrag); + var wrapperRange = new LiveRange(TAG, newRangeFrag); existingRange.insertBefore(newRangeFrag); _.each(self.roots, function (root) { @@ -524,13 +541,14 @@ _.extend(PreservationController.prototype, { // XXX debugging var pathForRange = function (r) { var path = [], r; - while ((r = findParentOfType(Spark._ANNOTATION_LABEL, r))) + while ((r = findParentOfType(ANNOTATION_LABEL, r))) path.unshift(r.label); return path.join(' :: '); }; // `range` is a region of `document`. Modify it in-place so that it // matches the result of Spark.render(htmlFunc), preserving landmarks. +// Spark.renderToRange = function (range, htmlFunc) { // `range` may be out-of-document and we don't check here. // XXX should we? @@ -545,7 +563,7 @@ Spark.renderToRange = function (range, htmlFunc) { if (! startNode || ! startNode.parentNode) return; - var renderer = new Spark._Renderer(); + var renderer = new Renderer(); // Call 'func' for each landmark in 'range'. Pass two arguments to // 'func', the range, and an extra "notes" object such that two @@ -556,12 +574,12 @@ Spark.renderToRange = function (range, htmlFunc) { var stack = renderer.newLabelStack(); range.visit(function (isStart, r) { - if (r.type === Spark._ANNOTATION_LABEL) { + if (r.type === ANNOTATION_LABEL) { if (isStart) stack.pushLabel(r.label); else stack.popLabel(); - } else if (r.type === Spark._ANNOTATION_LANDMARK && isStart) { + } else if (r.type === ANNOTATION_LANDMARK && isStart) { func(r, stack.getNotes()); } }); @@ -588,7 +606,7 @@ Spark.renderToRange = function (range, htmlFunc) { DomUtils.wrapFragmentForContainer(frag, range.containerNode()); - var tempRange = new LiveRange(Spark._TAG, frag); + var tempRange = new LiveRange(TAG, frag); // find preservation roots from matched landmarks inside the // rerendered region @@ -616,11 +634,11 @@ Spark.renderToRange = function (range, htmlFunc) { // on a "malformed" liverange tree break; - if (walk.type === Spark._ANNOTATION_LANDMARK, walk) + if (walk.type === ANNOTATION_LANDMARK, walk) pc.addRoot(walk.preserve, range, tempRange, walk.containerNode()); } - pc.addRoot(Spark._globalPreserves, range, tempRange); + pc.addRoot(globalPreserves, range, tempRange); // compute preservations (must do this before destroying tempRange) var preservations = pc.computePreservations(range, tempRange); @@ -636,8 +654,8 @@ Spark.renderToRange = function (range, htmlFunc) { // inside constant regions whose DOM nodes we are going // to preserve untouched Spark.finalize(start, end); - Spark._patch(start.parentNode, frag, start.previousSibling, - end.nextSibling, preservations, results); + patch(start.parentNode, frag, start.previousSibling, + end.nextSibling, preservations, results); }); }); @@ -654,6 +672,7 @@ Spark.renderToRange = function (range, htmlFunc) { // Delete all of the liveranges in the range of nodes between `start` // and `end`, and call their 'finalize' function if any. Or instead of // `start` and `end` you may pass a fragment in `start`. +// Spark.finalize = function (start, end) { if (! start.parentNode && start.nodeType !== 11 /* DocumentFragment */) { // Workaround for LiveRanges' current inability to contain @@ -663,7 +682,7 @@ Spark.finalize = function (start, end) { start = frag; end = null; } - var wrapper = new LiveRange(Spark._TAG, start, end); + var wrapper = new LiveRange(TAG, start, end); wrapper.visit(function (isStart, range) { isStart && range.finalize && range.finalize(); }); @@ -676,11 +695,11 @@ Spark.finalize = function (start, end) { Spark.setDataContext = withRenderer(function (dataContext, html, _renderer) { return _renderer.annotate( - html, Spark._ANNOTATION_DATA, { data: dataContext }); + html, ANNOTATION_DATA, { data: dataContext }); }); Spark.getDataContext = function (node) { - var range = findRangeOfType(Spark._ANNOTATION_DATA, node); + var range = findRangeOfType(ANNOTATION_DATA, node); return range && range.data; }; @@ -705,16 +724,16 @@ var getListener = function () { return; var ranges = []; - var walk = findRangeOfType(Spark._ANNOTATION_EVENTS, + var walk = findRangeOfType(ANNOTATION_EVENTS, event.currentTarget); while (walk) { ranges.push(walk); - walk = findParentOfType(Spark._ANNOTATION_EVENTS, walk); + walk = findParentOfType(ANNOTATION_EVENTS, walk); } _.each(ranges, function (r) { r.handler(event); }); - }, Spark._checkIECompliance); + }, checkIECompliance); return universalListener; }; @@ -760,7 +779,7 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { }; html = _renderer.annotate( - html, Spark._ANNOTATION_WATCH, { + html, ANNOTATION_WATCH, { notify: function () { installHandlers(this); } @@ -769,7 +788,7 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { var finalized = false; html = _renderer.annotate( - html, Spark._ANNOTATION_EVENTS, function (range) { + html, ANNOTATION_EVENTS, function (range) { if (! range) return; @@ -808,7 +827,7 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { // Found a matching handler. Call it. var eventData = Spark.getDataContext(event.currentTarget) || {}; var landmarkRange = - findParentOfType(Spark._ANNOTATION_LANDMARK, range); + findParentOfType(ANNOTATION_LANDMARK, range); var landmark = (landmarkRange && landmarkRange.landmark); // Note that the handler can do arbitrary things, like call @@ -835,7 +854,7 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { /******************************************************************************/ Spark.isolate = function (htmlFunc) { - var renderer = Spark._currentRenderer.get(); + var renderer = currentRenderer.get(); if (!renderer) return htmlFunc(); @@ -845,7 +864,7 @@ Spark.isolate = function (htmlFunc) { Deps.autorun(function (handle) { if (firstRun) { retHtml = renderer.annotate( - htmlFunc(), Spark._ANNOTATION_ISOLATE, + htmlFunc(), ANNOTATION_ISOLATE, function (r) { if (! r) { // annotation not used; kill this autorun @@ -924,7 +943,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { var handle = cursor.observeChanges(observerCallbacks); // Get the renderer, if any - var renderer = Spark._currentRenderer.get(); + var renderer = currentRenderer.get(); var maybeAnnotate = renderer ? _.bind(renderer.annotate, renderer) : function (html) { return html; }; @@ -954,7 +973,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { itemDict.forEach(function (elt) { html += maybeAnnotate( itemFunc(transformedDoc(elt.doc)), - Spark._ANNOTATION_LIST_ITEM, + ANNOTATION_LIST_ITEM, function (range) { elt.liveRange = range; }); @@ -965,7 +984,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { handle.stop(); stopped = true; }; - html = maybeAnnotate(html, Spark._ANNOTATION_LIST, function (range) { + html = maybeAnnotate(html, ANNOTATION_LIST, function (range) { if (! range) { // We never ended up on the screen (caller discarded our return // value) @@ -989,7 +1008,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { // Maybe that will make sense if we give render callbacks subrange info. var notifyParentsRendered = function () { var walk = outerRange; - while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) + while ((walk = findParentOfType(ANNOTATION_LANDMARK, walk))) walk.rendered.call(walk.landmark); }; @@ -1008,7 +1027,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { doc._id = id; var frag = Spark.render(_.bind(itemFunc, null, transformedDoc(doc))); DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); - var range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag); + var range = makeRange(ANNOTATION_LIST_ITEM, frag); if (itemDict.empty()) { Spark.finalize(outerRange.replaceContents(frag)); @@ -1103,8 +1122,9 @@ Spark.UNIQUE_LABEL = ['UNIQUE_LABEL']; // label must be a string. // or pass label === null to not drop a label after all (meaning that // this function is a noop) +// Spark.labelBranch = function (label, htmlFunc) { - var renderer = Spark._currentRenderer.get(); + var renderer = currentRenderer.get(); if (! renderer || label === null) return htmlFunc(); @@ -1123,7 +1143,7 @@ Spark.labelBranch = function (label, htmlFunc) { return html; return renderer.annotate( - html, Spark._ANNOTATION_LABEL, { label: label }); + html, ANNOTATION_LABEL, { label: label }); // XXX what happens if the user doesn't use the return value, or // doesn't use it directly, eg, swaps the branches of the tree @@ -1140,7 +1160,7 @@ Spark.labelBranch = function (label, htmlFunc) { }; Spark.createLandmark = function (options, htmlFunc) { - var renderer = Spark._currentRenderer.get(); + var renderer = currentRenderer.get(); if (! renderer) { // no renderer -- create and destroy Landmark inline var landmark = new Spark.Landmark; @@ -1183,7 +1203,7 @@ Spark.createLandmark = function (options, htmlFunc) { var html = htmlFunc(landmark); return renderer.annotate( - html, Spark._ANNOTATION_LANDMARK, function (range) { + html, ANNOTATION_LANDMARK, function (range) { if (! range) { // annotation not used options.destroyed && options.destroyed.call(landmark); @@ -1224,8 +1244,7 @@ Spark.createLandmark = function (options, htmlFunc) { }); }; -// used by unit tests -Spark._getEnclosingLandmark = function (node) { - var range = findRangeOfType(Spark._ANNOTATION_LANDMARK, node); +SparkTest.getEnclosingLandmark = function (node) { + var range = findRangeOfType(ANNOTATION_LANDMARK, node); return range ? range.landmark : null; }; diff --git a/packages/spark/spark_tests.js b/packages/spark/spark_tests.js index 053fa15666..cab0f7ea94 100644 --- a/packages/spark/spark_tests.js +++ b/packages/spark/spark_tests.js @@ -4,7 +4,7 @@ // XXX test variable wrapping (eg TR vs THEAD) inside each branch of Spark.list? -Spark._checkIECompliance = true; +SparkTest.setCheckIECompliance(true); // Tests can use {preserve: idNameLabels} or renderWithPreservation // to cause any element with an id or name to be preserved. This effect @@ -74,9 +74,9 @@ Tinytest.add("spark - assembly", function (test) { test.equal(furtherCanon(f.html()), html); var actualGroups = []; - var tempRange = new LiveRange(Spark._TAG, frag); + var tempRange = new LiveRange(SparkTest.TAG, frag); tempRange.visit(function (isStart, rng) { - if (! isStart && rng.type === Spark._ANNOTATION_DATA) + if (! isStart && rng.type === "data" /* Spark._ANNOTATION_DATA */) actualGroups.push(furtherCanon(canonicalizeHtml( DomUtils.rangeToHtml(rng.firstNode(), rng.lastNode())))); }); @@ -3341,8 +3341,8 @@ Tinytest.add("spark - current landmark", function (test) { test.equal(callbacks, 1); Deps.flush(); test.equal(callbacks, 2); - test.equal(null, Spark._getEnclosingLandmark(d.node())); - var enc = Spark._getEnclosingLandmark(d.node().firstChild); + test.equal(null, SparkTest.getEnclosingLandmark(d.node())); + var enc = SparkTest.getEnclosingLandmark(d.node().firstChild); test.equal(enc.a, 9); test.equal(enc.b, 2); test.isFalse('c' in enc); @@ -3358,32 +3358,32 @@ Tinytest.add("spark - current landmark", function (test) { Deps.flush(); test.equal(callbacks, 4); - test.isTrue(Spark._getEnclosingLandmark(findOuter()).outer); - test.isTrue(Spark._getEnclosingLandmark(findInnerA()).innerA); - test.isTrue(Spark._getEnclosingLandmark(findInnerB()).innerB); - test.equal(1, Spark._getEnclosingLandmark(findOuter()).renderCount); - test.equal(1, Spark._getEnclosingLandmark(findInnerA()).renderCount); - test.equal(1, Spark._getEnclosingLandmark(findInnerB()).renderCount); + test.isTrue(SparkTest.getEnclosingLandmark(findOuter()).outer); + test.isTrue(SparkTest.getEnclosingLandmark(findInnerA()).innerA); + test.isTrue(SparkTest.getEnclosingLandmark(findInnerB()).innerB); + test.equal(1, SparkTest.getEnclosingLandmark(findOuter()).renderCount); + test.equal(1, SparkTest.getEnclosingLandmark(findInnerA()).renderCount); + test.equal(1, SparkTest.getEnclosingLandmark(findInnerB()).renderCount); R.set(4) Deps.flush(); test.equal(callbacks, 5); - test.equal(2, Spark._getEnclosingLandmark(findOuter()).renderCount); - test.equal(2, Spark._getEnclosingLandmark(findInnerA()).renderCount); + test.equal(2, SparkTest.getEnclosingLandmark(findOuter()).renderCount); + test.equal(2, SparkTest.getEnclosingLandmark(findInnerA()).renderCount); R.set(5) Deps.flush(); test.equal(callbacks, 6); - test.equal(3, Spark._getEnclosingLandmark(findOuter()).renderCount); - test.equal(3, Spark._getEnclosingLandmark(findInnerA()).renderCount); - test.equal(1, Spark._getEnclosingLandmark(findInnerB()).renderCount); + test.equal(3, SparkTest.getEnclosingLandmark(findOuter()).renderCount); + test.equal(3, SparkTest.getEnclosingLandmark(findInnerA()).renderCount); + test.equal(1, SparkTest.getEnclosingLandmark(findInnerB()).renderCount); R.set(6) Deps.flush(); test.equal(callbacks, 7); - test.equal(4, Spark._getEnclosingLandmark(findOuter()).renderCount); - test.equal(4, Spark._getEnclosingLandmark(findInnerA()).renderCount); - test.equal(2, Spark._getEnclosingLandmark(findInnerB()).renderCount); + test.equal(4, SparkTest.getEnclosingLandmark(findOuter()).renderCount); + test.equal(4, SparkTest.getEnclosingLandmark(findInnerA()).renderCount); + test.equal(2, SparkTest.getEnclosingLandmark(findInnerB()).renderCount); d.kill(); Deps.flush(); diff --git a/packages/spiderable/spiderable.js b/packages/spiderable/spiderable.js index 34afe99778..2e5bd04c4a 100644 --- a/packages/spiderable/spiderable.js +++ b/packages/spiderable/spiderable.js @@ -42,7 +42,7 @@ WebApp.connectHandlers.use(function (req, res, next) { " && typeof(Meteor.status) !== 'undefined' " + " && Meteor.status().connected) {" + " Deps.flush();" + - " return Meteor._LivedataConnection._allSubscriptionsReady();" + + " return DDP._allSubscriptionsReady();" + " }" + " return false;" + " });" + diff --git a/packages/srp/biginteger.js b/packages/srp/biginteger.js index 676e01f9a8..7566f48663 100644 --- a/packages/srp/biginteger.js +++ b/packages/srp/biginteger.js @@ -1,7 +1,5 @@ /// METEOR WRAPPER -if (typeof Meteor._srp === "undefined") - Meteor._srp = {}; -Meteor._srp.BigInteger = (function () { +BigInteger = (function () { /// BEGIN jsbn.js diff --git a/packages/srp/package.js b/packages/srp/package.js index 6216f24019..f304b8dccc 100644 --- a/packages/srp/package.js +++ b/packages/srp/package.js @@ -6,6 +6,7 @@ Package.describe({ Package.on_use(function (api) { api.use(['random', 'check'], ['client', 'server']); api.use('underscore'); + api.export('SRP'); api.add_files(['biginteger.js', 'sha256.js', 'srp.js'], ['client', 'server']); }); diff --git a/packages/srp/sha256.js b/packages/srp/sha256.js index 9b34c69f36..4743264b4e 100644 --- a/packages/srp/sha256.js +++ b/packages/srp/sha256.js @@ -2,9 +2,7 @@ // // XXX this should get packaged and moved into the Meteor.crypto // namespace, along with other hash functions. -if (typeof Meteor._srp === "undefined") - Meteor._srp = {}; -Meteor._srp.SHA256 = (function () { +SHA256 = (function () { /** diff --git a/packages/srp/srp.js b/packages/srp/srp.js index 5fbd0cd1ae..e04860d577 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -1,6 +1,4 @@ -if (typeof Meteor._srp === "undefined") - Meteor._srp = {}; - +SRP = {}; /////// PUBLIC CLIENT @@ -14,14 +12,14 @@ if (typeof Meteor._srp === "undefined") * testing. Random UUID if not provided. * - SRP parameters (see _defaults and paramsFromOptions below) */ -Meteor._srp.generateVerifier = function (password, options) { +SRP.generateVerifier = function (password, options) { var params = paramsFromOptions(options); var identity = (options && options.identity) || Random.id(); var salt = (options && options.salt) || Random.id(); var x = params.hash(salt + params.hash(identity + ":" + password)); - var xi = new Meteor._srp.BigInteger(x, 16); + var xi = new BigInteger(x, 16); var v = params.g.modPow(xi, params.N); @@ -33,7 +31,7 @@ Meteor._srp.generateVerifier = function (password, options) { }; // For use with check(). -Meteor._srp.matchVerifier = { +SRP.matchVerifier = { identity: String, salt: String, verifier: String @@ -49,7 +47,7 @@ Meteor._srp.matchVerifier = { * passed in for testing. * - SRP parameters (see _defaults and paramsFromOptions below) */ -Meteor._srp.Client = function (password, options) { +SRP.Client = function (password, options) { var self = this; self.params = paramsFromOptions(options); self.password = password; @@ -62,8 +60,8 @@ Meteor._srp.Client = function (password, options) { var a, A; if (options && options.a) { if (typeof options.a === "string") - a = new Meteor._srp.BigInteger(options.a, 16); - else if (options.a instanceof Meteor._srp.BigInteger) + a = new BigInteger(options.a, 16); + else if (options.a instanceof BigInteger) a = options.a; else throw new Error("Invalid parameter: a"); @@ -91,7 +89,7 @@ Meteor._srp.Client = function (password, options) { * * returns { A: 'client public ephemeral key. hex encoded integer.' } */ -Meteor._srp.Client.prototype.startExchange = function () { +SRP.Client.prototype.startExchange = function () { var self = this; return { @@ -110,7 +108,7 @@ Meteor._srp.Client.prototype.startExchange = function () { * returns { M: 'client proof of password. hex encoded integer.' } * throws an error if it got an invalid challenge. */ -Meteor._srp.Client.prototype.respondToChallenge = function (challenge) { +SRP.Client.prototype.respondToChallenge = function (challenge) { var self = this; // shorthand @@ -123,13 +121,13 @@ Meteor._srp.Client.prototype.respondToChallenge = function (challenge) { self.identity = challenge.identity; self.salt = challenge.salt; self.Bstr = challenge.B; - self.B = new Meteor._srp.BigInteger(self.Bstr, 16); + self.B = new BigInteger(self.Bstr, 16); if (self.B.mod(N) === 0) throw new Error("Server sent invalid key: B mod N == 0."); - var u = new Meteor._srp.BigInteger(H(self.Astr + self.Bstr), 16); - var x = new Meteor._srp.BigInteger( + var u = new BigInteger(H(self.Astr + self.Bstr), 16); + var x = new BigInteger( H(self.salt + H(self.identity + ":" + self.password)), 16); var kgx = k.multiply(g.modPow(x, N)); @@ -155,7 +153,7 @@ Meteor._srp.Client.prototype.respondToChallenge = function (challenge) { * * returns true or false. */ -Meteor._srp.Client.prototype.verifyConfirmation = function (confirmation) { +SRP.Client.prototype.verifyConfirmation = function (confirmation) { var self = this; return (self.HAMK && (confirmation.HAMK === self.HAMK)); @@ -175,7 +173,7 @@ Meteor._srp.Client.prototype.verifyConfirmation = function (confirmation) { * passed in for testing. * - SRP parameters (see _defaults and paramsFromOptions below) */ -Meteor._srp.Server = function (verifier, options) { +SRP.Server = function (verifier, options) { var self = this; self.params = paramsFromOptions(options); self.verifier = verifier; @@ -184,14 +182,14 @@ Meteor._srp.Server = function (verifier, options) { var N = self.params.N; var g = self.params.g; var k = self.params.k; - var v = new Meteor._srp.BigInteger(self.verifier.verifier, 16); + var v = new BigInteger(self.verifier.verifier, 16); // construct public and private keys. var b, B; if (options && options.b) { if (typeof options.b === "string") - b = new Meteor._srp.BigInteger(options.b, 16); - else if (options.b instanceof Meteor._srp.BigInteger) + b = new BigInteger(options.b, 16); + else if (options.b instanceof BigInteger) b = options.b; else throw new Error("Invalid parameter: b"); @@ -228,12 +226,12 @@ Meteor._srp.Server = function (verifier, options) { * * Throws an error if issued a bad request. */ -Meteor._srp.Server.prototype.issueChallenge = function (request) { +SRP.Server.prototype.issueChallenge = function (request) { var self = this; // XXX check for missing / bad parameters. self.Astr = request.A; - self.A = new Meteor._srp.BigInteger(self.Astr, 16); + self.A = new BigInteger(self.Astr, 16); if (self.A.mod(self.params.N) === 0) throw new Error("Client sent invalid key: A mod N == 0."); @@ -243,8 +241,8 @@ Meteor._srp.Server.prototype.issueChallenge = function (request) { var H = self.params.hash; // Compute M and HAMK in advance. Don't send to client yet. - var u = new Meteor._srp.BigInteger(H(self.Astr + self.Bstr), 16); - var v = new Meteor._srp.BigInteger(self.verifier.verifier, 16); + var u = new BigInteger(H(self.Astr + self.Bstr), 16); + var v = new BigInteger(self.verifier.verifier, 16); var avu = self.A.multiply(v.modPow(u, N)); self.S = avu.modPow(self.b, N); self.M = H(self.Astr + self.Bstr + self.S.toString(16)); @@ -268,7 +266,7 @@ Meteor._srp.Server.prototype.issueChallenge = function (request) { * - HAMK: server proof of password. hex encoded integer. * OR null if the client's proof doesn't match. */ -Meteor._srp.Server.prototype.verifyResponse = function (response) { +SRP.Server.prototype.verifyResponse = function (response) { var self = this; if (response.M !== self.M) @@ -287,15 +285,15 @@ Meteor._srp.Server.prototype.verifyResponse = function (response) { * Default parameter values for SRP. * */ -Meteor._srp._defaults = { - hash: function (x) { return Meteor._srp.SHA256(x).toLowerCase(); }, - N: new Meteor._srp.BigInteger("EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3", 16), - g: new Meteor._srp.BigInteger("2") +var _defaults = { + hash: function (x) { return SHA256(x).toLowerCase(); }, + N: new BigInteger("EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3", 16), + g: new BigInteger("2") }; -Meteor._srp._defaults.k = new Meteor._srp.BigInteger( - Meteor._srp._defaults.hash( - Meteor._srp._defaults.N.toString(16) + - Meteor._srp._defaults.g.toString(16)), +_defaults.k = new BigInteger( + _defaults.hash( + _defaults.N.toString(16) + + _defaults.g.toString(16)), 16); /** @@ -309,15 +307,15 @@ Meteor._srp._defaults.k = new Meteor._srp.BigInteger( */ var paramsFromOptions = function (options) { if (!options) // fast path - return Meteor._srp._defaults; + return _defaults; - var ret = _.extend({}, Meteor._srp._defaults); + var ret = _.extend({}, _defaults); _.each(['N', 'g', 'k'], function (p) { if (options[p]) { if (typeof options[p] === "string") - ret[p] = new Meteor._srp.BigInteger(options[p], 16); - else if (options[p] instanceof Meteor._srp.BigInteger) + ret[p] = new BigInteger(options[p], 16); + else if (options[p] instanceof BigInteger) ret[p] = options[p]; else throw new Error("Invalid parameter: " + p); @@ -336,5 +334,5 @@ var paramsFromOptions = function (options) { var randInt = function () { - return new Meteor._srp.BigInteger(Random.hexString(36), 16); + return new BigInteger(Random.hexString(36), 16); }; diff --git a/packages/srp/srp_tests.js b/packages/srp/srp_tests.js index ea35bd6e59..d1ea3edc35 100644 --- a/packages/srp/srp_tests.js +++ b/packages/srp/srp_tests.js @@ -1,9 +1,9 @@ Tinytest.add("srp - good exchange", function(test) { var password = 'hi there!'; - var verifier = Meteor._srp.generateVerifier(password); + var verifier = SRP.generateVerifier(password); - var C = new Meteor._srp.Client(password); - var S = new Meteor._srp.Server(verifier); + var C = new SRP.Client(password); + var S = new SRP.Server(verifier); var request = C.startExchange(); var challenge = S.issueChallenge(request); @@ -16,10 +16,10 @@ Tinytest.add("srp - good exchange", function(test) { }); Tinytest.add("srp - bad exchange", function(test) { - var verifier = Meteor._srp.generateVerifier('one password'); + var verifier = SRP.generateVerifier('one password'); - var C = new Meteor._srp.Client('another password'); - var S = new Meteor._srp.Server(verifier); + var C = new SRP.Client('another password'); + var S = new SRP.Server(verifier); var request = C.startExchange(); var challenge = S.issueChallenge(request); @@ -43,11 +43,11 @@ Tinytest.add("srp - fixed values", function(test) { var a = "dc99c646fa4cb7c24314bb6f4ca2d391297acd0dacb0430a13bbf1e37dcf8071"; var b = "cf878e00c9f2b6aa48a10f66df9706e64fef2ca399f396d65f5b0a27cb8ae237"; - var verifier = Meteor._srp.generateVerifier( + var verifier = SRP.generateVerifier( password, {identity: identity, salt: salt}); - var C = new Meteor._srp.Client(password, {a: a}); - var S = new Meteor._srp.Server(verifier, {b: b}); + var C = new SRP.Client(password, {a: a}); + var S = new SRP.Server(verifier, {b: b}); var request = C.startExchange(); test.equal(request.A, "8a75aa61471a92d4c3b5d53698c910af5ef013c42799876c40612d1d5e0dc41d01f669bc022fadcd8a704030483401a1b86b8670191bd9dfb1fb506dd11c688b2f08e9946756263954db2040c1df1894af7af5f839c9215bb445268439157e65e8f100469d575d5d0458e19e8bd4dd4ea2c0b30b1b3f4f39264de4ec596e0bb7"); @@ -88,14 +88,14 @@ Tinytest.add("srp - options", function(test) { b: "2" }, baseOptions); - var verifier = Meteor._srp.generateVerifier('c', verifierOptions);; + var verifier = SRP.generateVerifier('c', verifierOptions);; test.equal(verifier.identity, 'a'); test.equal(verifier.salt, 'b'); test.equal(verifier.verifier, '3'); - var C = new Meteor._srp.Client('c', clientOptions); - var S = new Meteor._srp.Server(verifier, serverOptions); + var C = new SRP.Client('c', clientOptions); + var S = new SRP.Server(verifier, serverOptions); var request = C.startExchange(); test.equal(request.A, '4'); diff --git a/packages/past/.gitignore b/packages/standard-app-packages/.gitignore similarity index 100% rename from packages/past/.gitignore rename to packages/standard-app-packages/.gitignore diff --git a/packages/standard-app-packages/package.js b/packages/standard-app-packages/package.js new file mode 100644 index 0000000000..9699d19763 --- /dev/null +++ b/packages/standard-app-packages/package.js @@ -0,0 +1,45 @@ +Package.describe({ + summary: "Include a standard set of Meteor packages in your app" +}); + +Package.on_use(function(api) { + // The "imply" here means that if your app uses "standard-app-packages", it is + // treated as if it also directly included all of these packages (and it gets + // their exports, plugins, etc). + // + // If you want, you can "meteor remove standard-app-packages" and add some of + // these back in individually. We haven't tested every subset, though :) + api.imply([ + // The normal "every package uses 'meteor'" rule only applies to packages + // built from a package source directory, so we make sure apps get it too. + // Meteor.isServer! The CSS extension handler! And so much more! + 'meteor', + // A standard Meteor app is a web app. (Without this, there will be no + // 'main' function unless you define one yourself.) + 'webapp', + // It's Log! It's better than bad, it's good! + 'logging', + // Deps.autorun and friends. What's Meteor without reactivity? + 'deps', + // The easiest way to get a little reactivity into your app. + 'session', + // DDP: Meteor's client/server protocol. + 'livedata', + // You want to keep your data somewhere? How about MongoDB? + 'mongo-livedata', + // Meteor UI! + 'ui', + // A great template language! + 'spacebars', + // Turn templates into views! + 'templating', + // Easy type assertions? check. + 'check', + // _.isUseful(true) + 'underscore', + // $(".usefulToo") + 'jquery', + // Life isn't always predictable. + 'random' + ]); +}); diff --git a/packages/star-translate/package.js b/packages/star-translate/package.js index 3b48baad7b..35f71360dc 100644 --- a/packages/star-translate/package.js +++ b/packages/star-translate/package.js @@ -4,6 +4,7 @@ Package.describe({ Package.on_use(function (api) { api.use(['dev-bundle-fetcher']); + api.export('StarTranslator'); api.add_files(['translator.js'], 'server'); }); diff --git a/packages/star-translate/translator.js b/packages/star-translate/translator.js index 563da8e625..77d08756de 100644 --- a/packages/star-translate/translator.js +++ b/packages/star-translate/translator.js @@ -2,7 +2,6 @@ var fs = Npm.require('fs'); var path = Npm.require('path'); var ncp = Npm.require('ncp').ncp; -//@export StarTranslator StarTranslator = {}; // Produces a star version of bundlePath in translatedPath, where bundlePath can @@ -81,9 +80,9 @@ StarTranslator._getPlatform = function () { var self = this; // Duplicated from meteor/tools/bundler.js var archToPlatform = { - 'native.linux.x86_32': 'Linux_i686', - 'native.linux.x86_64': 'Linux_x86_64', - 'native.osx.x86_64': 'Darwin_x86_64' + 'os.linux.x86_32': 'Linux_i686', + 'os.linux.x86_64': 'Linux_x86_64', + 'os.osx.x86_64': 'Darwin_x86_64' }; return archToPlatform[self._getArch()]; }; diff --git a/packages/startup/package.js b/packages/startup/package.js index 8e9c54a307..94603f3c82 100644 --- a/packages/startup/package.js +++ b/packages/startup/package.js @@ -1,9 +1,11 @@ Package.describe({ - summary: "Provides Meteor.startup", + summary: "Deprecated package (now empty)", internal: true }); Package.on_use(function (api) { - api.add_files('startup_client.js', 'client'); - api.add_files('startup_server.js', 'server'); + // Deprecated -- Meteor.startup has been folded into the main + // 'meteor' package for now, because it seems reasonable to expect + // that Meteor.startup would always be available without having to + // include a special package. }); diff --git a/packages/stylus/package.js b/packages/stylus/package.js index 15831786c1..95e74bac9f 100644 --- a/packages/stylus/package.js +++ b/packages/stylus/package.js @@ -13,5 +13,6 @@ Package._transitional_registerBuildPlugin({ Package.on_test(function (api) { api.use(['tinytest', 'stylus', 'test-helpers']) + api.use('spark'); api.add_files(['stylus_tests.styl', 'stylus_tests.js'], 'client'); }); diff --git a/packages/templating/deftemplate.js b/packages/templating/deftemplate.js index 4b635824ed..7ef7e83b7a 100644 --- a/packages/templating/deftemplate.js +++ b/packages/templating/deftemplate.js @@ -1,13 +1,11 @@ /* -// @export Template Template = {}; -Meteor._partials = {}; +var registeredPartials = {}; // XXX Handlebars hooking is janky and gross - -Meteor._hook_handlebars = function () { - Meteor._hook_handlebars = function(){}; // install the hook only once +var hookHandlebars = function () { + hookHandlebars = function(){}; // install the hook only once var orig = Handlebars._default_helpers.each; Handlebars._default_helpers.each = function (arg, options) { @@ -74,7 +72,7 @@ var templateObjFromLandmark = function (landmark) { }; // XXX forms hooks into this to add "bind"? -Meteor._template_decl_methods = { +var templateBase = { // methods store data here (event map, etc.). initialized per template. _tmpl_data: null, // these functions must be generic (i.e. use `this`) @@ -105,10 +103,12 @@ Meteor._template_decl_methods = { } }; -Meteor._def_template = function (name, raw_func) { - Meteor._hook_handlebars(); +Template.__define__ = function (name, raw_func) { + hookHandlebars(); - window.Template = window.Template || {}; + if (name === '__define__') + throw new Error("Sorry, '__define__' is a special name and " + + "cannot be used as the name of a template"); // Define the function assigned to Template.. @@ -141,7 +141,7 @@ Meteor._def_template = function (name, raw_func) { // (and receive 'landmark') return raw_func(data, { helpers: _.extend({}, partial, tmplData.helpers || {}), - partials: Meteor._partials, + partials: registeredPartials, name: name }); }); @@ -168,7 +168,7 @@ Meteor._def_template = function (name, raw_func) { // support old Template.foo.events = {...} format var events = - (tmpl.events !== Meteor._template_decl_methods.events ? + (tmpl.events !== templateBase.events ? tmpl.events : tmplData.events); // events need to be inside the landmark, not outside, so // that when an event fires, you can retrieve the enclosing @@ -197,13 +197,13 @@ Meteor._def_template = function (name, raw_func) { "'. Each template needs a unique name."); Template[name] = partial; - _.extend(partial, Meteor._template_decl_methods); + _.extend(partial, templateBase); partial._tmpl_data = {}; - Meteor._partials[name] = partial; + registeredPartials[name] = partial; } // useful for unnamed templates, like body return partial; }; -*/ \ No newline at end of file +*/ diff --git a/packages/templating/package.js b/packages/templating/package.js index 5962948b17..fbb07fcde2 100644 --- a/packages/templating/package.js +++ b/packages/templating/package.js @@ -10,24 +10,29 @@ Package.describe({ Package._transitional_registerBuildPlugin({ name: "compileTemplates", - use: ['underscore', 'spacebars'], + use: ['spacebars'], sources: [ 'plugin/html_scanner.js', 'plugin/compile-templates.js' ] }); +// This on_use describes the *runtime* implications of using this package. Package.on_use(function (api) { // XXX would like to do the following only when the first html file // is encountered api.use(['underscore', 'ui', 'spacebars'], 'client'); + api.export('Template', 'client'); + // provides the runtime logic to instantiate our templates //api.add_files('deftemplate.js', 'client'); - // html_scanner.js emits client code that calls Meteor.startup - api.use('startup', 'client'); + // html_scanner.js emits client code that calls Meteor.startup and + // UI, so anybody using templating (eg apps) need to implicitly use + // 'meteor' and 'ui'. + api.imply(['meteor', 'ui'], 'client'); }); Package.on_test(function (api) { diff --git a/packages/templating/plugin/html_scanner.js b/packages/templating/plugin/html_scanner.js index 159f5a2ccf..c5a12f99b2 100644 --- a/packages/templating/plugin/html_scanner.js +++ b/packages/templating/plugin/html_scanner.js @@ -176,8 +176,3 @@ html_scanner = { } } }; - -// If we are running at bundle time, set module.exports. -// For unit testing in server environment, don't. -if (typeof module !== 'undefined') - module.exports = html_scanner; diff --git a/packages/templating/scanner_tests.js b/packages/templating/scanner_tests.js index 2e0edafef5..e55d7b8962 100644 --- a/packages/templating/scanner_tests.js +++ b/packages/templating/scanner_tests.js @@ -24,9 +24,9 @@ Tinytest.add("templating - html scanner", function (test) { var BODY_PREAMBLE = "Meteor.startup(function(){" + "document.body.appendChild(Spark.render(" + - "Meteor._def_template(null,"; + "Template.__define__(null,"; var BODY_POSTAMBLE = ")));});"; - var TEMPLATE_PREAMBLE = "Meteor._def_template("; + var TEMPLATE_PREAMBLE = "Template.__define__("; var TEMPLATE_POSTAMBLE = ");\n"; var checkResults = function(results, expectJs, expectHead) { diff --git a/packages/templating/templating_tests.js b/packages/templating/templating_tests.js index cc677c90f5..634c36368b 100644 --- a/packages/templating/templating_tests.js +++ b/packages/templating/templating_tests.js @@ -768,7 +768,7 @@ Tinytest.add("templating - events", function (test) { test.isTrue(_.contains(buf, 'a')); test.isTrue(_.contains(buf, 'b')); div.kill(); - Meteor.flush(); + Deps.flush(); }); Tinytest.add("templating - #each rendered callback", function (test) { diff --git a/packages/test-helpers/async_multi.js b/packages/test-helpers/async_multi.js index cc7d1ca84b..ad831def4b 100644 --- a/packages/test-helpers/async_multi.js +++ b/packages/test-helpers/async_multi.js @@ -109,7 +109,6 @@ _.extend(ExpectationManager.prototype, { } }); -// @export testAsyncMulti testAsyncMulti = function (name, funcs) { // XXX Tests on remote browsers are _slow_. We need a better solution. var timeout = 180000; @@ -153,7 +152,6 @@ testAsyncMulti = function (name, funcs) { }); }; -// @export pollUntil pollUntil = function (expect, f, timeout, step, noFail) { noFail = noFail || false; step = step || 100; diff --git a/packages/test-helpers/callback_logger.js b/packages/test-helpers/callback_logger.js index fd864a81d1..8ac1c2575b 100644 --- a/packages/test-helpers/callback_logger.js +++ b/packages/test-helpers/callback_logger.js @@ -10,7 +10,6 @@ var Fiber = Meteor.isServer ? Npm.require('fibers') : null; var TIMEOUT = 1000; -// @export withCallbackLogger // Run the given function, passing it a correctly-set-up callback logger as an // argument. If we're meant to be running asynchronously, the function gets its // own Fiber. diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index d3a8a91442..633c97cce8 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -1,4 +1,3 @@ -// @export canonicalizeHtml canonicalizeHtml = function(html) { var h = html; // kill IE-specific comments inserted by Spark diff --git a/packages/test-helpers/current_style.js b/packages/test-helpers/current_style.js index cfe8906f43..c6d2e6e76d 100644 --- a/packages/test-helpers/current_style.js +++ b/packages/test-helpers/current_style.js @@ -1,4 +1,3 @@ -// @export getStyleProperty // Cross-browser implementation of getting the computed style of an element. getStyleProperty = function(n, prop) { if (n.currentStyle) { diff --git a/packages/test-helpers/event_simulation.js b/packages/test-helpers/event_simulation.js index 6a45aa1b2c..a2865d62cd 100644 --- a/packages/test-helpers/event_simulation.js +++ b/packages/test-helpers/event_simulation.js @@ -1,4 +1,3 @@ -// @export simulateEvent simulateEvent = function (node, event, args) { node = (node instanceof $ ? node[0] : node); @@ -14,7 +13,6 @@ simulateEvent = function (node, event, args) { } }; -// @export focusElement focusElement = function(elem) { // This sequence is for benefit of IE 8 and 9; // test there before changing. @@ -27,14 +25,12 @@ focusElement = function(elem) { throw new Error("focus() didn't set activeElement"); }; -// @export blurElement blurElement = function(elem) { elem.blur(); if (document.activeElement === elem) throw new Error("blur() didn't affect activeElement"); }; -// @export clickElement clickElement = function(elem) { if (elem.click) elem.click(); // supported by form controls cross-browser; most native way diff --git a/packages/test-helpers/onscreendiv.js b/packages/test-helpers/onscreendiv.js index 246f02c9f2..dc0296761b 100644 --- a/packages/test-helpers/onscreendiv.js +++ b/packages/test-helpers/onscreendiv.js @@ -1,4 +1,3 @@ -// @export OnscreenDiv // OnscreenDiv is an object that appends a DIV to the document // body and keeps track of it, providing methods that query it, // mutate, and destroy it. diff --git a/packages/test-helpers/package.js b/packages/test-helpers/package.js index a58cbe988c..04a555bdb5 100644 --- a/packages/test-helpers/package.js +++ b/packages/test-helpers/package.js @@ -3,24 +3,28 @@ Package.describe({ internal: true }); -Package.on_use(function (api, where) { - where = where || ["client", "server"]; - +Package.on_use(function (api) { api.use(['underscore', 'deps', 'ejson', 'tinytest', 'random', 'domutils']); - api.use(['spark'], 'client'); + api.use(['spark', 'jquery'], 'client'); - api.add_files('try_all_permutations.js', where); - api.add_files('async_multi.js', where); - api.add_files('event_simulation.js', where); - api.add_files('seeded_random.js', where); - api.add_files('canonicalize_html.js', where); - api.add_files('stub_stream.js', where); - api.add_files('onscreendiv.js', where); - api.add_files('wrappedfrag.js', where); - api.add_files('current_style.js', where); - api.add_files('reactivevar.js', where); - api.add_files('callback_logger.js', where); + api.export([ + 'pollUntil', 'WrappedFrag', 'try_all_permutations', 'StubStream', + 'SeededRandom', 'ReactiveVar', 'OnscreenDiv', 'clickElement', 'blurElement', + 'focusElement', 'simulateEvent', 'getStyleProperty', 'canonicalizeHtml', + 'withCallbackLogger', 'testAsyncMulti'], {testOnly: true}); + + api.add_files('try_all_permutations.js'); + api.add_files('async_multi.js'); + api.add_files('event_simulation.js'); + api.add_files('seeded_random.js'); + api.add_files('canonicalize_html.js'); + api.add_files('stub_stream.js'); + api.add_files('onscreendiv.js'); + api.add_files('wrappedfrag.js'); + api.add_files('current_style.js'); + api.add_files('reactivevar.js'); + api.add_files('callback_logger.js'); }); Package.on_test(function (api) { diff --git a/packages/test-helpers/reactivevar.js b/packages/test-helpers/reactivevar.js index eeb6eb07d0..7a4184da8c 100644 --- a/packages/test-helpers/reactivevar.js +++ b/packages/test-helpers/reactivevar.js @@ -12,7 +12,6 @@ // Constructor, with optional 'new': // var R = [new] ReactiveVar([initialValue]) -// @export ReactiveVar ReactiveVar = function(initialValue) { if (! (this instanceof ReactiveVar)) return new ReactiveVar(initialValue); diff --git a/packages/test-helpers/seeded_random.js b/packages/test-helpers/seeded_random.js index fcc4315801..0094a20258 100644 --- a/packages/test-helpers/seeded_random.js +++ b/packages/test-helpers/seeded_random.js @@ -1,4 +1,3 @@ -// @export SeededRandom SeededRandom = function(seed) { // seed may be a string or any type if (! (this instanceof SeededRandom)) return new SeededRandom(seed); diff --git a/packages/test-helpers/stub_stream.js b/packages/test-helpers/stub_stream.js index 351619b854..1442024819 100644 --- a/packages/test-helpers/stub_stream.js +++ b/packages/test-helpers/stub_stream.js @@ -1,8 +1,4 @@ -// XXX XXX should really '@export Meteor._StubStream' but we're not -// there yet (other packages need to cooperate and also export -// Meteor.foo rather than Meteor) - -Meteor._StubStream = function () { +StubStream = function () { var self = this; self.sent = []; @@ -10,7 +6,7 @@ Meteor._StubStream = function () { }; -_.extend(Meteor._StubStream.prototype, { +_.extend(StubStream.prototype, { // Methods from Stream on: function (name, callback) { var self = this; diff --git a/packages/test-helpers/try_all_permutations.js b/packages/test-helpers/try_all_permutations.js index 59d0981ef5..2276c10a01 100644 --- a/packages/test-helpers/try_all_permutations.js +++ b/packages/test-helpers/try_all_permutations.js @@ -31,7 +31,6 @@ // try_all_permutations([X], [A, B], [Y]) // try_all_permutations(X, [A, B], Y) -// @export try_all_permutations try_all_permutations = function () { var args = Array.prototype.slice.call(arguments); diff --git a/packages/test-helpers/wrappedfrag.js b/packages/test-helpers/wrappedfrag.js index 34a52bcd7e..1023890cea 100644 --- a/packages/test-helpers/wrappedfrag.js +++ b/packages/test-helpers/wrappedfrag.js @@ -1,4 +1,3 @@ -// @export WrappedFrag // A WrappedFrag provides utility methods pertaining to a given // DocumentFragment that are helpful in tests. For example, // WrappedFrag(frag).html() constructs a sort of cross-browser diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index 9c8d7874e2..24dea7cc5c 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -41,13 +41,13 @@ Session.set("rerunScheduled", false); Meteor.startup(function () { Deps.flush(); - Meteor._runTestsEverywhere(reportResults, function () { + Tinytest._runTestsEverywhere(reportResults, function () { running = false; Meteor.onTestsComplete && Meteor.onTestsComplete(); countDep.changed(); Deps.flush(); - Meteor.default_connection._unsubscribeAll(); + Meteor.connection._unsubscribeAll(); }, Session.get("groupPath")); }); @@ -301,7 +301,7 @@ var changeToPath = function (path) { Session.set("rerunScheduled", true); // pretend there's just been a hot code push // so we run the tests completely fresh. - Meteor._reload.reload(); + Reload._reload(); }; Template.groupNav({ @@ -323,8 +323,8 @@ Template.groupNav({ changeToPath(this.path); }, 'click .rerun': function () { - Session.set("rerunScheduled", true); - Meteor._reload.reload(); + Session.set("rerunScheduled", true); + Reload._reload(); } }); @@ -445,7 +445,7 @@ Template.event({ // messy. needs to be aggressively refactored. forgetEvents({groupPath: this.cookie.groupPath, test: this.cookie.shortName}); - Meteor._debugTest(this.cookie, reportResults); + Tinytest._debugTest(this.cookie, reportResults); } }); diff --git a/packages/test-in-browser/package.js b/packages/test-in-browser/package.js index c3c547f2d1..de3c1b6d5f 100644 --- a/packages/test-in-browser/package.js +++ b/packages/test-in-browser/package.js @@ -12,6 +12,7 @@ Package.on_use(function (api) { api.use('underscore'); api.use('session'); + api.use('reload'); api.use(['ui', 'templating', 'spacebars', 'livedata', 'deps'], 'client'); diff --git a/packages/test-in-console/driver.js b/packages/test-in-console/driver.js index 6335cba63a..cbbd26b354 100644 --- a/packages/test-in-console/driver.js +++ b/packages/test-in-console/driver.js @@ -3,7 +3,6 @@ DONE = false; // Failure count for phantomjs exit code FAILURES = null; -// @export TEST_STATUS TEST_STATUS = { DONE: false, FAILURES: null @@ -73,7 +72,7 @@ Meteor.startup(function () { setTimeout(sendReports, 500); setInterval(sendReports, 2000); - Meteor._runTestsEverywhere( + Tinytest._runTestsEverywhere( function (results) { var name = getName(results); if (!_.has(resultSet, name)) { diff --git a/packages/test-in-console/package.js b/packages/test-in-console/package.js index 0480b1e28d..274a4c3d4f 100644 --- a/packages/test-in-console/package.js +++ b/packages/test-in-console/package.js @@ -6,12 +6,13 @@ Package.describe({ Package.on_use(function (api) { api.use(['tinytest', 'underscore', 'random', 'ejson', 'check']); - api.use('http'); + api.use('http', 'server'); - api.add_files([ - 'driver.js' - ], "client"); - api.add_files([ - 'reporter.js' - ], "server"); + api.export('TEST_STATUS', 'client'); + + api.add_files(['driver.js'], "client"); + api.add_files(['reporter.js'], "server"); + + // This is to be run by phantomjs, not as part of normal package code. + api.add_files('runner.js', 'server', {isAsset: true}); }); diff --git a/packages/test-in-console/reporter.js b/packages/test-in-console/reporter.js index 668abc4a96..ef7e83734a 100644 --- a/packages/test-in-console/reporter.js +++ b/packages/test-in-console/reporter.js @@ -1,3 +1,9 @@ +// A hacky way to extract the phantom runner script from the package. +if (process.env.WRITE_RUNNER_JS) { + Npm.require('fs').writeFileSync( + process.env.WRITE_RUNNER_JS, new Buffer(Assets.getBinary('runner.js'))); +} + var url = null; if (Meteor.settings && Meteor.settings.public && @@ -13,7 +19,7 @@ Meteor.methods({ // XXX Could do a more precise validation here; reports are complex! check(reports, [Object]); if (url) { - Meteor.http.post(url, { + HTTP.post(url, { data: reports }); } diff --git a/packages/tinytest/package.js b/packages/tinytest/package.js index 8172e430fb..af83a91b80 100644 --- a/packages/tinytest/package.js +++ b/packages/tinytest/package.js @@ -4,21 +4,18 @@ Package.describe({ }); Package.on_use(function (api) { - // "past" is always included before app code (see initFromAppDir) but not - // before packages when testing. This makes sure that tests see - // backward-compatibility hooks, at least if they use tinytest. - api.use('past'); - api.use('underscore', ['client', 'server']); api.use('random', ['client', 'server']); + api.export('Tinytest'); + api.add_files('tinytest.js', ['client', 'server']); + api.use('livedata', ['client', 'server']); api.use('mongo-livedata', ['client', 'server']); api.add_files('model.js', ['client', 'server']); api.add_files('tinytest_client.js', 'client'); - api.use('startup', 'server'); api.add_files('tinytest_server.js', 'server'); api.use('check'); diff --git a/packages/tinytest/tinytest.js b/packages/tinytest/tinytest.js index 68e7d9d336..9e6efab35b 100644 --- a/packages/tinytest/tinytest.js +++ b/packages/tinytest/tinytest.js @@ -491,22 +491,22 @@ _.extend(TestRun.prototype, { /* Public API */ /******************************************************************************/ -// @export Tinytest -Tinytest = { - add: function (name, func) { - TestManager.addCase(new TestCase(name, func)); - }, +Tinytest = {}; - addAsync: function (name, func) { - TestManager.addCase(new TestCase(name, func, true)); - } +Tinytest.add = function (name, func) { + TestManager.addCase(new TestCase(name, func)); +}; + +Tinytest.addAsync = function (name, func) { + TestManager.addCase(new TestCase(name, func, true)); }; // Run every test, asynchronously. Runs the test in the current // process only (if called on the server, runs the tests on the // server, and likewise for the client.) Report results via // onReport. Call onComplete when it's done. -Meteor._runTests = function (onReport, onComplete, pathPrefix) { +// +Tinytest._runTests = function (onReport, onComplete, pathPrefix) { var testRun = TestManager.createRun(onReport, pathPrefix); testRun.run(onComplete); }; @@ -514,7 +514,8 @@ Meteor._runTests = function (onReport, onComplete, pathPrefix) { // Run just one test case, and stop the debugger at a particular // error, all as indicated by 'cookie', which will have come from a // failure event output by _runTests. -Meteor._debugTest = function (cookie, onReport, onComplete) { +// +Tinytest._debugTest = function (cookie, onReport, onComplete) { var testRun = TestManager.createRun(onReport); testRun.debug(cookie, onComplete); }; diff --git a/packages/tinytest/tinytest_client.js b/packages/tinytest/tinytest_client.js index b86cbbb83e..1e38015e72 100644 --- a/packages/tinytest/tinytest_client.js +++ b/packages/tinytest/tinytest_client.js @@ -1,7 +1,8 @@ -// Like Meteor._runTests, but runs the tests on both the client and +// Like Tinytest._runTests, but runs the tests on both the client and // the server. Sets a 'server' flag on test results that came from the // server. -Meteor._runTestsEverywhere = function (onReport, onComplete, pathPrefix) { +// +Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix) { var runId = Random.id(); var localComplete = false; var remoteComplete = false; @@ -14,12 +15,12 @@ Meteor._runTestsEverywhere = function (onReport, onComplete, pathPrefix) { } }; - Meteor._runTests(onReport, function () { + Tinytest._runTests(onReport, function () { localComplete = true; maybeDone(); }, pathPrefix); - Meteor.default_connection.registerStore(Meteor._ServerTestResultsCollection, { + Meteor.connection.registerStore(Meteor._ServerTestResultsCollection, { update: function (msg) { // We only should call _runTestsEverywhere once per client-page-load, so // we really only should see one runId here. diff --git a/packages/tinytest/tinytest_server.js b/packages/tinytest/tinytest_server.js index dfd95aeeb3..b99c7758a7 100644 --- a/packages/tinytest/tinytest_server.js +++ b/packages/tinytest/tinytest_server.js @@ -54,7 +54,7 @@ Meteor.methods({ future['return'](); }; - Meteor._runTests(onReport, onComplete, pathPrefix); + Tinytest._runTests(onReport, onComplete, pathPrefix); future.wait(); }, diff --git a/packages/twitter/package.js b/packages/twitter/package.js index 60d1908d53..5180af2fd8 100644 --- a/packages/twitter/package.js +++ b/packages/twitter/package.js @@ -14,11 +14,12 @@ Package.on_use(function(api) { api.use('underscore', 'server'); api.use('service-configuration', ['client', 'server']); + api.export('Twitter'); + api.add_files( ['twitter_configure.html', 'twitter_configure.js'], 'client'); - api.add_files('twitter_common.js', ['client', 'server']); api.add_files('twitter_server.js', 'server'); api.add_files('twitter_client.js', 'client'); }); diff --git a/packages/twitter/twitter_client.js b/packages/twitter/twitter_client.js index 7a6ea679de..73b02f9232 100644 --- a/packages/twitter/twitter_client.js +++ b/packages/twitter/twitter_client.js @@ -1,3 +1,5 @@ +Twitter = {}; + // Request Twitter credentials for the user // @param options {optional} XXX support options.requestPermissions // @param credentialRequestCompleteCallback {Function} Callback function to call on diff --git a/packages/twitter/twitter_common.js b/packages/twitter/twitter_common.js deleted file mode 100644 index 5c41a14894..0000000000 --- a/packages/twitter/twitter_common.js +++ /dev/null @@ -1,9 +0,0 @@ -// @export Twitter -Twitter = {}; - -Twitter._urls = { - requestToken: "https://api.twitter.com/oauth/request_token", - authorize: "https://api.twitter.com/oauth/authorize", - accessToken: "https://api.twitter.com/oauth/access_token", - authenticate: "https://api.twitter.com/oauth/authenticate" -}; diff --git a/packages/twitter/twitter_server.js b/packages/twitter/twitter_server.js index 8d67554229..ce05ea0119 100644 --- a/packages/twitter/twitter_server.js +++ b/packages/twitter/twitter_server.js @@ -1,7 +1,17 @@ +Twitter = {}; + +var urls = { + requestToken: "https://api.twitter.com/oauth/request_token", + authorize: "https://api.twitter.com/oauth/authorize", + accessToken: "https://api.twitter.com/oauth/access_token", + authenticate: "https://api.twitter.com/oauth/authenticate" +}; + + // https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials Twitter.whitelistedFields = ['profile_image_url', 'profile_image_url_https', 'lang']; -Oauth.registerService('twitter', 1, Twitter._urls, function(oauthBinding) { +Oauth.registerService('twitter', 1, urls, function(oauthBinding) { var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data; var serviceData = { diff --git a/packages/ui.old/.gitignore b/packages/ui.old/.gitignore deleted file mode 100644 index 677a6fc263..0000000000 --- a/packages/ui.old/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build* diff --git a/packages/ui.old/attrs.js b/packages/ui.old/attrs.js deleted file mode 100644 index 9b1df25584..0000000000 --- a/packages/ui.old/attrs.js +++ /dev/null @@ -1,159 +0,0 @@ - -var ATTRIBUTE_NAME_REGEX = /^[^\s"'>/=/]+$/; - -var isValidAttributeName = function (str) { - return ATTRIBUTE_NAME_REGEX.test(str); -}; - -var makeAttributeHandler = function (component, name, value) { - return new (component.constructor._attributeHandlers[name] || - AttributeHandler)(name, value); -}; - -AttributeManager = function (component, dictOrFunc) { - var self = this; - self.component = component; - - var dict, func; - - if (typeof dictOrFunc === 'function') { - func = dictOrFunc; - // Calculate the initial value without capturing any - // dependencies. Once the element exists, we'll recalculate - // it in an autorun. This makes the overall logic simpler. - Deps.nonreactive(function () { - dict = func(); - }); - } else { - // non-reactive attrs - func = null; - dict = dictOrFunc; - } - - if ((! dict) || (typeof dict !== 'object')) - throw new Error("Expected object containing attribute names/values"); - - self.func = func; - self.handlers = {}; - - var handlers = self.handlers; - for (var attrName in dict) { - // perform a sanity check, since we'll be inserting - // attrName directly into the HTML stream - if (! isValidAttributeName(attrName)) - throw new Error("Illegal HTML attribute name: " + attrName); - - handlers[attrName] = makeAttributeHandler( - component, attrName, dict[attrName]); - } -}; - -_extend(AttributeManager.prototype, { - element: null, - isReactive: function () { - return !! this.func; - }, - wire: function (n) { - this.element = n; - }, - getInitialHTML: function () { - var self = this; - var handlers = self.handlers; - - var strs = []; - for (var attrName in handlers) - strs.push(handlers[attrName].getHTML()); - - return strs.join(' '); - }, - start: function () { - var self = this; - if (! self.isReactive()) - throw new Error("Can't start a non-reactive AttributeManager"); - - var component = self.component; - var element = self.element; - var handlers = self.handlers; - - component.autorun(function (c) { - if (component.stage !== Component.BUILT || - ! component.containsElement(element)) { - c.stop(); - return; - } - - // capture dependencies of this line: - var newDict = self.func(); - - // update all handlers. - // - // don't GC handlers for properties that - // go away (which would be necessary if someone really attaches - // O(N) different attributes to an element over time). - for (var k in handlers) { - var h = handlers[k]; - var oldValue = h.value; - h.value = newDict.hasOwnProperty(k) ? newDict[k] : null; - h.update(element, oldValue, h.value); - } - for (var k in newDict) { - if (! handlers.hasOwnProperty(k)) { - // need a new handler - var attrName = k; - - if (! isValidAttributeName(attrName)) - throw new Error("Illegal HTML attribute name: " + attrName); - - var h = makeAttributeHandler( - component, attrName, newDict[attrName]); - - handlers[attrName] = h; - h.add(element); - } - } - }); - } -}); - -AttributeHandler = function (name, value) { - this.name = name; - this.value = value; -}; - -_extend(AttributeHandler.prototype, { - getHTML: function () { - var value = this.value; - if (value == null) - return ''; - - return this.name + '="' + - UI.encodeSpecialEntities(this.stringifyValue(value), true) + '"'; - }, - stringifyValue: function (value) { - return String(value); - }, - add: function (element) { - this.update(element, null, this.value); - }, - update: function (element, oldValue, value) { - if (value == null) { - if (oldValue != null) - element.removeAttribute(this.name); - } else { - element.setAttribute(this.name, this.stringifyValue(value)); - } - } -}); - -// @export AttributeHandler -AttributeHandler.extend = function (options) { - var curType = this; - var subType = function AttributeHandlerSubtype(/*arguments*/) { - AttributeHandler.apply(this, arguments); - }; - subType.prototype = new curType; - subType.extend = curType.extend; - if (options) - _extend(subType.prototype, options); - return subType; -}; \ No newline at end of file diff --git a/packages/ui.old/base.js b/packages/ui.old/base.js deleted file mode 100644 index 8838e5addd..0000000000 --- a/packages/ui.old/base.js +++ /dev/null @@ -1,244 +0,0 @@ -// @export UI -UI = { - nextGuid: 1 -}; - -var isComponentType = function (x) { - return (typeof x === 'function') && - ((x === Component) || - (x.prototype instanceof Component)); -}; - -var MAKING_PROTO = {}; // unique sentinel object - -var constrImpl = function (ths, args, type) { - if (args[0] === MAKING_PROTO) - return ths; - - if (! (ths instanceof type)) - // invoked without `new` - return new type(args[0], args[1]); - - // invoked as `new Foo(...)` - if (! type._superSealed) - type._superSealed = "instantiated"; - - var options = args[0]; - - // support `(dataFunc[, options])` args - var dataFunc = null; - if (typeof options === 'function') { - dataFunc = options; - options = args[1]; - } - - var specialOptions = false; - - if (options) { - for (var k in options) { - if (type._extendHooks[k]) { - specialOptions = true; - break; - } - } - } - - if (specialOptions) { - // create a subtype - return type.extend(options).create(dataFunc); - } else { - // don't create a subtype (faster) - if (options) - _extend(ths, options); - if (dataFunc) - ths.data = dataFunc; - } - - ths.guid = UI.nextGuid++; - ths.constructed(); - - return ths; -}; - -Component = function Component() { - return constrImpl(this, arguments, Component); -}; - -_extend = function (tgt, src) { - for (var k in src) - if (src.hasOwnProperty(k)) - tgt[k] = src[k]; - return tgt; -}; - -var setSuperType = function (subType, superType) { - var oldProto = subType.prototype; - - subType.prototype = new superType(MAKING_PROTO); - - // Make the 'constructor' property of components behave - // the way you'd think it should in OO, i.e. - // `Foo.create().constructor === Foo`. - // Make a non-enumerable property in browsers that allow - // it, which is all except IE <9. IE 8 has an - // Object.defineProperty but it doesn't work. - try { - Object.defineProperty(subType.prototype, - 'constructor', - { value: subType }); - } catch (e) { - subType.prototype.constructor = subType; - } - - // Inherit static properties from parent. - _extend(subType, superType); - - // Record the (new) superType for our future use. - subType.superType = superType; - - // restore old properties on proto (from previous includes - // or extends) - _extend(subType.prototype, oldProto); - - subType.create = Component.create; -}; - -var chainCallback = function (cb, cbName) { - var prevCb = ( - this.prototype.hasOwnProperty(cbName) ? - this.prototype[cbName] : - this.superType && this.superType.prototype[cbName]); - - this.prototype[cbName] = function (/*args*/) { - prevCb && prevCb.apply(this, arguments); - cb.apply(this, arguments); - }; -}; - -_extend(Component, { - typeName: "Component", - _extendHooks: { - extend: function (newSuper) { - var type = this; - if (! isComponentType(newSuper)) - throw new Error("'extend' option must be a Component type"); - - if (newSuper !== type.superType) { - if (type.superType !== Component) - throw new Error("Can only set Component supertype once"); - - if (type._superSealed) - throw new Error("Can't set Component supertype after " + type._superSealed); - - setSuperType(type, newSuper); - } - }, - extendHooks: function (hooks) { - this._extendHooks = - _extend(_extend({}, this._extendHooks), hooks); - }, - // make typeName count as a special option for when `create` - // checks for special options, even though it's not - // implemented here (but by `extend`) - typeName: function () {}, - constructed: 'chain' - }, - toString: function () { - return this.typeName || '(Component type)'; - }, - // must be exported for absolute access from `extend` - _constrImpl: constrImpl, - create: function (dataFunc, options) { - return new this(dataFunc, options); - }, - include: function (options) { - var type = this; - - // Note: We avoid calling `delete options.foo` even if it's - // convenient so that we don't mutate the `options` object, - // which might be used more than once. - - if ((!options) || typeof options !== 'object') - throw new Error("Options object required in 'include'"); - - // handle 'extend' first - if ('extend' in options) - type._extendHooks.extend(options.extend, 'extend'); - - for (var optKey in options) { - if (optKey === 'extend') - continue; - // Don't put typeName on the proto; it goes on the type constructor. - // When we're called from `extend`, typeName has already been - // taken care of. When we're called directly, we silently drop it. - if (optKey === 'typeName') - continue; - - var optValue = options[optKey]; - - var hook = type._extendHooks[optKey]; - if (hook) { - if (hook === 'chain') - hook = chainCallback; - // Note that it's ok for the hook to recursively - // invoke `this.include`. - hook.call(type, optValue, optKey); - if (! type._superSealed) - type._superSealed = optKey; - } else { - type.prototype[optKey] = optValue; - } - } - - return type; - }, - extend: function (options) { - var superType = this; - - // Note: We avoid calling `delete options.foo` even if it's - // convenient so that we don't mutate the `options` object, - // which might be used more than once. - - if (! superType._superSealed) - superType._superSealed = "extended"; - - var typeName = this.typeName; - if (options && options.typeName) { - typeName = String(options.typeName).replace( - /^[^a-zA-Z_]|[^a-zA-Z_0-9]/g, '') || typeName; - } - - var newType = Function( - "return function " + typeName + "() { " + - "return Package.ui.UI.Component._constrImpl(this, " + - "arguments, " + typeName + "); };")(); - - setSuperType(newType, superType); - newType.typeName = typeName; - newType._superSealed = null; - - if (options) - newType.include(options); - - return newType; - }, - define: function (props) { - if ((!props) || typeof props !== 'object') - throw new Error("Props object required in Component.define"); - - _extend(this, props); - - if (! this._superSealed) - this._superSealed = 'calling define()'; - }, - isType: isComponentType -}); - -Component.include({ - constructed: function () {}, - data: function () { - return this.parent ? this.parent.data() : null; - } -}); - -UI.Component = Component; \ No newline at end of file diff --git a/packages/ui.old/components.js b/packages/ui.old/components.js deleted file mode 100644 index 5981f4bf22..0000000000 --- a/packages/ui.old/components.js +++ /dev/null @@ -1,92 +0,0 @@ - -// All `` tags in HTML files are compiled to extend -// Body. If you put helpers and events on Body, they all -// inherit them. -UI.Body = Component.extend({isRoot: true}); - -UI.Text = Component.extend({ - typeName: 'Text', - _encodeEntities: UI.encodeSpecialEntities, - _stringify: function (x) { - return String(x == null ? '' : x); - }, - render: function (buf) { - var data = this.data(); - buf(this._encodeEntities(this._stringify(data))); - } -}); - -UI.HTML = Component.extend({ - typeName: 'HTML', - _stringify: function (x) { - return String(x == null ? '' : x); - }, - render: function (buf) { - var data = this.data(); - buf(this._stringify(data)); - } -}); - -UI.If = Component.extend({ - typeName: 'If', - init: function () { - // here we implement the idea that the one positional arg to - // a component becomes its data by default, but components - // like `#if` don't want it to be the data context - // seen by the content so they can change it. - // the implementation will change (but not the idea) - // if Geoff's proposal for extend and args is implemented. - // It's also possible the right thing to do is - // to have `arg` and `data` be separate. - this.condition = this.data; - this.data = this.parent.data; - }, - render: function (buf) { - var self = this; - var condition = Deps.isolateValue(function () { - return !! self.condition(); - }); - buf(condition ? self.content() : self.elseContent()); - } -}); - -UI.Unless = Component.extend({ - typeName: 'Unless', - init: function () { - // see comment in `If` - this.condition = this.data; - this.data = this.parent.data; - }, - render: function (buf) { - var self = this; - var condition = Deps.isolateValue(function () { - return ! self.condition(); - }); - buf(condition ? self.content() : self.elseContent()); - } -}); - -UI.Counter = Component.extend({ - typeName: "Counter", - fields: { - count: 0 - }, - increment: function () { - this.set('count', this.count() + 1); - }, - render: function (buf) { - var self = this; - - buf("
", - new UI.Text(function () { - return self.count(); - }), - "
"); - }, - built: function () { - var self = this; - self.$("div").on('click', function (evt) { - self.increment(); - }); - } -}); \ No newline at end of file diff --git a/packages/ui.old/dom.js b/packages/ui.old/dom.js deleted file mode 100644 index 043821045c..0000000000 --- a/packages/ui.old/dom.js +++ /dev/null @@ -1,768 +0,0 @@ - -var emptyCommentProp = 'meteor-ui-empty'; -var createEmptyComment = function (beforeNode) { - var x = document.createComment("empty"); - x[emptyCommentProp] = true; - return x; -}; - -// Returns 0 if the nodes are the same or either one contains the other; -// otherwise, -1 if a comes before b, or else 1 if b comes before a in -// document order. -// Requires: `a` and `b` are element nodes in the same document tree. -var compareElementIndex = function (a, b) { - // See http://ejohn.org/blog/comparing-document-position/ - if (a === b) - return 0; - if (a.compareDocumentPosition) { - var n = a.compareDocumentPosition(b); - return ((n & 0x18) ? 0 : ((n & 0x4) ? -1 : 1)); - } else { - // Only old IE is known to not have compareDocumentPosition (though Safari - // originally lacked it). Thankfully, IE gives us a way of comparing elements - // via the "sourceIndex" property. - if (a.contains(b) || b.contains(a)) - return 0; - return (a.sourceIndex < b.sourceIndex ? -1 : 1); - } -}; - -// Returns true if element a contains node b and is not node b. -var elementContains = function (a, b) { - if (a.nodeType !== 1) /* ELEMENT */ - return false; - if (a === b) - return false; - - if (a.compareDocumentPosition) { - return a.compareDocumentPosition(b) & 0x10; - } else { - // Should be only old IE and maybe other old browsers here. - // Modern Safari has both functions but seems to get contains() wrong. - // IE can't handle b being a text node. We work around this - // by doing a direct parent test now. - b = b.parentNode; - if (! (b && b.nodeType === 1)) /* ELEMENT */ - return false; - if (a === b) - return true; - - return a.contains(b); - } -}; - -var insertNodesBefore = function (nodes, parentNode, beforeNode) { - if (beforeNode) { - $(nodes).insertBefore(beforeNode); - } else { - $(nodes).appendTo(parentNode); - } -}; - -var makeSafeDiv = function () { - // create a DIV in a DocumentFragment, where the DocumentFragment - // is created by jQuery, which uses tricks to create a "safe" - // fragment for HTML5 tags in IE <9. - var div = document.createElement("DIV"); - var frag = $.buildFragment([div], document); - return div; -}; - -Component.include({ - start: null, - end: null, - - firstNode: function () { - this._requireBuilt(); - return this.start instanceof Component ? - this.start.firstNode() : this.start; - }, - - lastNode: function () { - this._requireBuilt(); - return this.end instanceof Component ? - this.end.lastNode() : this.end; - }, - - parentNode: function () { - return this.firstNode().parentNode; - }, - - isAttached: false, - - // DIV holding offscreen content (if component is BUILT and not attached). - // It's a DIV rather than a fragment so that jQuery can run against it. - _offscreen: null, - - // Unlike Component constructor, caller is not allowed to skip `dataFunc` - // and pass `options` as the first argument. However, `dataFunc` - // may be falsy and `options` is optional. - // - // The idea is that a Component type makes a perfect value - // for `content`, but you can also write your own function - // that constructs a Component. This function can return null. - content: function (dataFunc, options) { return null; }, - elseContent: function (dataFunc, options) { return null; }, - - render: function (buf) { - buf(this.content()); - }, - - _populate: function (div) { - var self = this; - - var buf = makeRenderBuffer(self); - self.render(buf); - - var html = buf.getHtml(); - - $(div).append(html); - - // returns info object with {start, end} - return buf.wireUpDOM(div); - }, - - build: function () { - var self = this; - - self._requireNotDestroyed(); - if (self.stage === Component.BUILT) - throw new Error("Component already built"); - if (self.stage !== Component.ADDED) - throw new Error("Component must be added to a parent (or made a root) before building"); - - self._rebuilder = self.autorun(function (c) { - // record set of children that existed before, - // or null (for efficiency) - var oldChildren = null; - for (var k in self.children) - (oldChildren || (oldChildren = {}))[k] = true; - - if (c.firstRun) { - var div = makeSafeDiv(); - // capture reactivity: - var info = self._populate(div); - - if (! div.firstChild) - div.appendChild(createEmptyComment()); - - self._offscreen = div; - self.start = info.start || div.firstChild; - self.end = info.end || div.lastChild; - } else { - // capture reactivity: - self._rebuild(c.builtChildren); - } - - var newChildren = null; - for (var k in self.children) - if (! (oldChildren && oldChildren[k])) - (newChildren || (newChildren = {}))[k] = self.children[k]; - - // `builtChildren` is actually children *added* during build - c.builtChildren = newChildren; - - // don't capture dependencies, but provide a - // parent autorun (so that any autoruns created - // from a built callback are stopped on rebuild) - var x = Deps.autorun(function (c) { - if (c.firstRun) - self._built(); - }); - Deps.onInvalidate(function () { - x.stop(); - }); - }); - }, - - // Don't call this directly. It implements the re-run of the - // build autorun, so it assumes it's already inside the appropriate - // reactive computation. - // - // `builtChildren` is a map of children that were added during - // the previous build (as opposed to at some other time, such as - // earlier from an `init` callback). - _rebuild: function (builtChildren) { - var self = this; - - self._assertStage(Component.BUILT); - - // Should work whether this component is detached or attached! - // In other words, it may reside in an offscreen element. - - var firstNode = self.firstNode(); - var lastNode = self.lastNode(); - var parentNode = lastNode.parentNode; - var nextNode = lastNode.nextSibling || null; - var prevNode = firstNode.previousSibling || null; - - // for efficiency, do a quick check to see if we've *ever* - // had children or if we are still using the prototype's - // empty object. - if (self.children !== UI.Component.prototype.children) { - Deps.nonreactive(function () { - // kill children from last render, and also any - // attached children - var children = self.children; - for (var k in children) { - var child = children[k]; - if (builtChildren && builtChildren[k]) { - // destroy first, then remove - // (which doesn't affect DOM, which we will - // remove all at once) - child.destroy(); - self.remove(child); - } else if (child.isAttached) { - // detach the child; we don't have a good way - // of keeping this from affecting the DOM - child.detach(); - } - } - }); - } - - var oldNodes = []; - // must be careful as call to `detach` above may have - // must with firstNode or lastNode - for (var n = prevNode ? prevNode.nextSibling : - parentNode.firstChild; - n && n !== nextNode; - n = n.nextSibling) - oldNodes.push(n); - - $(oldNodes).remove(); - - var div = makeSafeDiv(); - // set `self.start` to null so that calls to `attach` from - // `_populate` don't try to do start/end pointer logic. - self.start = self.end = null; - var info = self._populate(div); - if (! div.firstChild) - div.appendChild(createEmptyComment()); - - self.start = info.start || div.firstChild; - self.end = info.end || div.lastChild; - insertNodesBefore(div.childNodes, parentNode, nextNode); - }, - - // # component.attach(parentNode, [beforeNode]) - // - // Requires `component` be parented (or a root) and not attached. - // Builds it - // if necessary, then inserts it into the DOM at the specified - // location. - // - // If you want to move a Component in the DOM, detach it first - // and then attach it somewhere else. - attach: function (parentNode, beforeNode) { - var self = this; - - self._requireNotDestroyed(); - - if (self.stage === Component.INITIAL) - throw new Error("Component to attach must have a parent (or be a root)"); - - if (self.stage === Component.ADDED) // not built - self.build(); - - self._assertStage(Component.BUILT); - - if (self.isAttached) - throw new Error("Component already attached; must be detached first"); - - if ((! parentNode) || ! parentNode.nodeType) - throw new Error("first argument to attach must be a Node"); - if (beforeNode && ! beforeNode.nodeType) - throw new Error("second argument to attach must be a Node" + - " if given"); - - insertNodesBefore(self._offscreen.childNodes, - parentNode, beforeNode); - - self._offscreen = null; - self.isAttached = true; - - var parent = self.parent; - // We could be a root (and have no parent). Parent could - // theoretically be destroyed, or not yet built (if we - // are currently building). - // - // We use a falsy `parent.start` as a cue that this is a - // rebuild, another case where we skip the start/end adjustment - // logic. - // - // `attach` is special in that it is used during building - // and rebuilding; it is not required that the parent is - // completely built. - if (parent && parent.stage === Component.BUILT && - parent.start) { - if (parent.isEmpty()) { - var comment = parent.start; - parent.start = parent.end = self; - comment.parentNode.removeChild(comment); - } else { - if (parent.firstNode() === self.lastNode().nextSibling) - parent.start = self; - if (parent.lastNode() === self.firstNode().previousSibling) - parent.end = self; - } - } - - self.attached(); - }, - - // # component.detach() - // - // Component must be built and attached. Removes this component's - // DOM and puts it into an offscreen storage. Updates the parent's - // `start` and `end` and populates it with a comment if it becomes - // empty. - detach: function (_forDestruction) { - var self = this; - - self._requireBuilt(); - if (! self.isAttached) - throw new Error("Component not attached"); - - var parent = self.parent; - var A = self.firstNode(); - var B = self.lastNode(); - - // We could be a root (and have no parent). Parent could - // theoretically be destroyed, or not yet built. - if (parent && parent.stage === Component.BUILT) { - // Do some magic to update the - // firstNode and lastNode. The main issue is we need to - // know if the new firstNode or lastNode is part of a - // child component or not, because if it is, we need to - // set `start` or `end` to the component rather than the - // node. Since we don't have any pointers from the DOM - // and can't make any assumptions about the structure of - // the component, we have to do a search over our children. - // Repeatedly detaching the first or last of O(N) top-level - // components is asymptotically bad -- O(n^2). - // - // Components that manage large numbers of top-level components - // should override _findStartComponent and _findEndComponent. - if (parent.start === self) { - if (parent.end === self) { - // we're emptying the parent; populate it with a - // comment in an appropriate place (adjacent to - // the not-yet-extracted DOM) and set pointers. - var comment = createEmptyComment(); - A.parentNode.insertBefore(comment, A); - parent.start = parent.end = comment; - } else { - // Removing component at the beginning of parent. - // - // Figure out if the following top-level node is the - // first node of a Component. - var newFirstNode = B.nextSibling; - parent.start = parent._findStartComponent(newFirstNode); - if (! (parent.start && parent.start.firstNode() === newFirstNode)) - parent.start = newFirstNode; - } - } else if (parent.end === self) { - // Removing component at the end of parent. - // - // Figure out if the previous top-level node is the - // last node of a Component. - var newLastNode = A.previousSibling; - parent.end = parent._findEndComponent(newLastNode); - if (! (parent.end && parent.end.lastNode() === newLastNode)) - parent.end = newLastNode; - } - } - - var nodes = []; - for (var n = A; n !== B; n = n.nextSibling) - nodes.push(n); - nodes.push(B); - - if (_forDestruction) { - $(nodes).remove(); - } else { - // Move nodes into an offscreen div, preserving - // any event handlers and data associated with the nodes. - var div = makeSafeDiv(); - $(div).append(nodes); - - self._offscreen = div; - self.isAttached = false; - - self.detached(); - } - }, - - isEmpty: function () { - this._requireBuilt(); - - var start = this.start; - return start === this.end && ! (start instanceof Component) && - start.nodeType === 8 && start[emptyCommentProp] === true; - }, - - attached: function () {}, - detached: function () {}, - - extendHooks: { - attached: 'chain', - detached: 'chain', - attributeHandlers: function (handlers) { - this._attributeHandlers = - _extend(_extend({}, this._attributeHandlers), handlers); - } - }, - - destroyed: function () { - // clean up any data associated with offscreen nodes - if (this._offscreen) - $.cleanData(this._offscreen.childNodes); - - // stop all computations (rebuilding and comp.autorun) - var comps = this._computations; - if (comps) - for (var i = 0; i < comps.length; i++) - comps[i].stop(); - }, - - - // # component.append(childOrDom) - // - // childOrDom is a Component, or node, or HTML string, - // or array of elements (various things a la jQuery). - // - // Given `child`: It must be a child of this component or addable - // as one. Builds it if necessary. Attaches it at the end of - // this component. Updates `start` and `end` of this component. - - append: function (childOrDom) { - this.insertAfter(childOrDom, this.lastNode()); - }, - - prepend: function (childOrDom) { - this.insertBefore(childOrDom, this.firstNode()); - }, - - // # component.insertBefore(childOrDom, before, parentNode) - // - // `before` is a Component or node. parentNode is only used - // if `before` is null. It defaults to the Component's - // parentNode. - // - // See append. - - insertBefore: function (childOrDom, before, parentNode) { - var self = this; - - self._requireBuilt(); - - if (before instanceof Component) { - before = before.firstNode(); - } else if (! before) { - if ((! parentNode) || (parentNode === self.parentNode())) { - before = self.lastNode().nextSibling; - parentNode = parentNode || self.parentNode(); - } - } - parentNode = parentNode || before.parentNode; - - if (childOrDom instanceof Component) { - var child = childOrDom; - - child._requireNotDestroyed(); - - if (child.stage === Component.INITIAL) { - self.add(child); - } else if (child.parent !== self) { - throw new Error("Can only append/prepend/insert" + - " a child (or a component addable as one)"); - } - - child.attach(parentNode, before); - } else { - var nodes; - if (typeof childOrDom === 'string') { - nodes = $.parseHTML(childOrDom) || []; - } else if (childOrDom.nodeType) { - nodes = [childOrDom]; - } else if (typeof childOrDom.length === 'number' && - typeof childOrDom === 'object') { - nodes = Array.prototype.slice.call(childOrDom); - } else { - throw new Error( - "Expected HTML, DOM node, array, or Component, found " + - childOrDom); - } - - if (nodes.length) { - insertNodesBefore(nodes, parentNode, before); - - if (self.isEmpty()) { - var comment = self.start; - comment.parentNode.removeChild(comment); - self.start = nodes[0]; - self.end = nodes[nodes.length - 1]; - } else if (before === self.firstNode()) { - self.start = nodes[0]; - } else if (nodes[0].previousSibling === self.lastNode()) { - self.end = nodes[nodes.length - 1]; - } - } - } - }, - - insertAfter: function (childOrDom, after, parentNode) { - var self = this; - - if (after instanceof Component) { - after = after.lastNode(); - } else if (! after) { - if ((! parentNode) || (parentNode === self.parentNode())) { - after = self.firstNode().previousSibling; - parentNode = parentNode || self.parentNode(); - } - } - parentNode = parentNode || after.parentNode; - - this.insertBefore(childOrDom, after.nextSibling, parentNode); - }, - - containsElement: function (elem) { - if (elem.nodeType !== 1) - throw new Error("containsElement requires an Element node"); - - var self = this; - self._requireBuilt(); - - var firstNode = self.firstNode(); - var prevNode = firstNode.previousSibling; - var nextNode = self.lastNode().nextSibling; - - // element must not be "above" this component - if (elementContains(elem, firstNode)) - return false; - // element must not be "at or before" prevNode - if (prevNode && compareElementIndex(prevNode, elem) >= 0) - return false; - // element must not be "at or after" nextNode - if (nextNode && compareElementIndex(elem, nextNode) >= 0) - return false; - return true; - }, - - // Take element `elem` and find the innermost component containing - // it which is either this component or a descendent of this component. - findByElement: function (elem) { - if (elem.nodeType !== 1) - throw new Error("findByElement requires an Element node"); - - var self = this; - self._requireBuilt(); - - if (! self.containsElement(elem)) - return null; - - var children = self.children; - // XXX linear-time scan through all children, - // running DOM comparison methods that may themselves - // be O(N). Not sure what the constants are. - for (var k in children) { - var child = children[k]; - if (child.stage === Component.BUILT && - child.isAttached) { - var found = child.findByElement(elem); - if (found) - return found; - } - } - - return self; - }, - - $: function (selector) { - var self = this; - - self._requireBuilt(); - - var firstNode = self.firstNode(); - var parentNode = firstNode.parentNode; - var prevNode = firstNode.previousSibling; - var nextNode = self.lastNode().nextSibling; - - // Don't assume `results` has jQuery API; a plain array - // should do just as well. However, if we do have a jQuery - // array, we want to end up with one also. - var results = $(selector, self.parentNode()); - - // Function that selects only elements that are actually in this - // Component, out of elements that are descendents of the Component's - // parentNode in the DOM (but may be, or descend from, siblings of - // this Component's top-level nodes that aren't between `start` and - // `end` inclusive). - var filterFunc = function (elem) { - // handle jQuery's arguments to filter, where the node - // is in `this` and the index is the first argument. - if (typeof elem === 'number') - elem = this; - - if (prevNode && compareElementIndex(prevNode, elem) >= 0) - return false; - if (nextNode && compareElementIndex(elem, nextNode) >= 0) - return false; - return true; - }; - - if (! results.filter) { - // not a jQuery array, and not a browser with - // Array.prototype.filter (e.g. IE <9) - var newResults = []; - for (var i = 0; i < results.length; i++) { - var x = results[i]; - if (filterFunc(x)) - newResults.push(x); - } - results = newResults; - } else { - // `results.filter` is either jQuery's or ECMAScript's `filter` - results = results.filter(filterFunc); - } - - return results; - }, - - autorun: function (compFunc) { - var self = this; - - self._requireNotDestroyed(); - - // XXX so many nested functions... Deps.nonreactive here - // feels heavyweight, but we don't want building a child - // while building a parent to mean that when the parent - // rebuilds, the child automatically does. - var c = Deps.nonreactive(function () { - return Deps.autorun(compFunc); - }); - - self._computations = self._computations || []; - self._computations.push(c); - - return c; - }, - - replaceChild: function (oldChild, newChild) { - var self = this; - - self._requireBuilt(); - oldChild._requireBuilt(); - if (! oldChild.isAttached) - throw new Error("Child to replace must be attached"); - - var lastNode = oldChild.lastNode(); - var parentNode = lastNode.parentNode; - var nextNode = lastNode.nextSibling; - - oldChild.remove(); - self.insertBefore(newChild, nextNode, parentNode); - }, - - swapChild: function (oldChild, newChild) { - var self = this; - - self._requireBuilt(); - oldChild._requireBuilt(); - if (! oldChild.isAttached) - throw new Error("Child to swap out must be attached"); - - var lastNode = oldChild.lastNode(); - var parentNode = lastNode.parentNode; - var nextNode = lastNode.nextSibling; - - oldChild.detach(); - self.insertBefore(newChild, nextNode, parentNode); - }, - - built: function () { - var self = this; - var cbs = self._builtCallbacks; - if (cbs) { - for (var i = 0, N = cbs.length; i < N; i++) - cbs[i](self); - self._builtCallbacks.length = 0; - } - }, - - _onNextBuilt: function (cb) { - var self = this; - var cbs = self._builtCallbacks; - if (! cbs) - cbs = self._builtCallbacks = []; - cbs.push(cb); - }, - - // Return a child whose firstNode() may be `firstNode`. - // If such a child exists, it must be found by this function. - // If no such child exists, this function may return null - // or a wrong guess at a child. Subclasses that know, - // for example, the earliest child component in the DOM - // at all times can supply that as a guess. - _findStartComponent: function (firstNode) { - var children = this.children; - // linear-time scan until found - for (var k in children) - if (children[k].firstNode() === firstNode) - return children[k]; - return null; - }, - - _findEndComponent: function (lastNode) { - var children = parent.children; - // linear-time scan until found - for (var k in children) - if (children[k].lastNode() === lastNode) - return children[k]; - return null; - } - - - - // If Component is ever emptied, it gets an empty comment node. - // This case is treated specially and the comment is removed - // if you then, say, append a node or component. However, - // the developer doing advanced things needs to be aware of - // this case or they may be surprised there is a node there - // that they didn't put there, e.g. if they call remove() on - // the last component and then start inserting DOM nodes - // manually. - - // You are free to manipulate the DOM of your component, excluding - // the regions that belong to child components, though if you do it - // using jQuery or any other means besides the methods here - // (attach, detach, append, prepend, insert), you are responsible - // for ensuring that `start` and `end` point to the first and last - // *node or Component* at the top level of the component's DOM, - // and that the component does not become empty. -}); - -Component.include({ - attributeHandlers: { - 'class': AttributeHandler.extend({ - stringifyValue: function (value) { - if (typeof value === 'string') - return value; - else if (typeof value.length === 'number') { - return Array.prototype.join.call(value, ' '); - } else { - return String(value); - } - } - }) - } -}); - -// Next up: -// -// - Spacebars compiler -// - event maps -// - preview HTML -// - Each for cursors, and for arrays diff --git a/packages/ui.old/each.js b/packages/ui.old/each.js deleted file mode 100644 index e436b6c60c..0000000000 --- a/packages/ui.old/each.js +++ /dev/null @@ -1,256 +0,0 @@ - -// `id` arguments to this class MUST be non-empty strings -UI.List = Component.extend({ - typeName: 'List', - _items: null, // OrderedDict of id -> Component - _else: null, // Component - constructed: function () { - this._items = new OrderedDict; - }, - addItemBefore: function (id, compType, data, beforeId) { - var self = this; - - var comp = compType(function () { - this.dataDep.depend(); - return this._data; - }, { - _data: data, - dataDep: new Deps.Dependency - }); - self._items.putBefore(id, comp, beforeId); - - if (self.stage === Component.BUILT) { - if (self._else) { - self._else.remove(); - self._else = null; - } - - self.insertBefore( - comp, beforeId ? self._items.get(beforeId) : null); - } - }, - removeItem: function (id) { - var comp = this._items.remove(id); - - if (this.stage === Component.BUILT) { - comp.remove(); - if (this._items.empty()) { - this._else = this.elseContent(); - if (this._else) - this.append(this._else); - } - } - }, - moveItemBefore: function (id, beforeId) { - var comp = this._items.get(id); - this._items.moveBefore(id, beforeId); - - if (this.stage === Component.BUILT) { - comp.detach(); - this.insertBefore( - comp, beforeId ? this._items.get(beforeId) : null); - } - }, - getItem: function (id) { - return this._items.get(id) || null; - }, - setItemData: function (id, newData) { - var comp = this.getItem(id); - if (! comp) - throw new Error("No such item: " + id); - // Do a `===` check even though it's weak - if (newData !== comp._data) { - comp._data = newData; - comp.dataDep.changed(); - } - }, - numItems: function () { - return this._items.size(); - }, - render: function (buf) { - // This component reactively rebuilds when any dependencies - // here are invalidated. - // - // The "item" methods cannot be called from here; they assume - // they are not operating during the build, but either - // before or after it. - - var self = this; - if (self._items.empty()) { - buf(self._else = self.elseContent()); - } else { - self._items.forEach(function (comp) { - buf(comp); - }); - } - }, - // Optimize the calculation of the new `.start` and `.end` - // after removing child components at the start or end. - _findStartComponent: function () { - return this._items.firstValue(); - }, - _findEndComponent: function () { - return this._items.lastValue(); - }, - // Replace the data in this list with a different dataset, - // reusing components with matching ids, moving them if - // necessary. - // The caller supplies a sequence of (id, data) : (String, any) - // pairs using the `add` method of the returned object, and then - // calls `end`. - beginReplace: function () { - var self = this; - - var items = self._items; - var ptr = items.first(); - var seenIds = {}; - var counter = 1; - // uniquify IDs by adding a few random characters and - // a counter. - var rand = Random.id().slice(0,4); - return { - // here only, id may be null - add: function (id, compType, data) { - var origId = id; - while ((! id) || seenIds.hasOwnProperty(id)) - id = (origId || '') + rand + (counter++); - seenIds[id] = true; - - // Now we know `id` is unique among new items, - // but it may match an old item at or after - // the location of `ptr`. - // - // We use the strategy of moving an existing component - // into the appropriate place if one exists, otherwise - // inserting one. This is efficient if, say, a new document - // is inserted at the top or bottom, removed from the bottom, - // or moved to the top. It's inefficient if a document is - // removed from the top or moved to the bottom, because we - // will perform `N-1` "moves". - // - // In summary, we don't generate efficient moves the way - // least-common-subsequence would, be we do reuse existing - // components and move them to the right place. - if (ptr === id) { - // XXX we don't deal the case where compType is different - // from the original compType. - self.setItemData(id, data); - ptr = items.next(ptr); - } else if (items.has(id)) { - self.moveItemBefore(id, ptr); - self.setItemData(id, data); - } else { - self.addItemBefore(id, compType, data, ptr); - } - }, - end: function () { - // delete everything at or after ptr - while (ptr) { - var next = items.next(ptr); - self.removeItem(ptr); - ptr = next; - } - } - }; - } -}); - -UI.Each = Component.extend({ - typeName: 'Each', - List: UI.List, - _oldData: null, - init: function () { - var self = this; - self._list = self.List({ - elseContent: function (/**/) { - return self.elseContent.apply(self, arguments); - } - }); - // add outside of the rebuild cycle - self.add(self._list); - }, - _getId: function (value) { // override this - if (value == null) { - return null; - } else if (value._id == null) { - if (typeof value === 'object') - // value is some object without `_id`. oh well. - return null; - else - // value is a string or number, say - return String(value); - } else { - if (typeof value._id === 'object') - return null; - else - return String(value._id); - } - }, - render: function (buf) { - var self = this; - var list = self._list; - - var newData = self.data(); - // Do a `===` check even though it's weak - // XXX no, don't, because of the case where we - // are given the same array but it has been - // mutated, like test-in-browser does. - // But if we did some "is it the same" check - // it would go here. - if (true || newData !== self._oldData) { - self._oldData = newData; - - var replacer = list.beginReplace(); - - if (! newData) { - // nothing to do - } else if (typeof newData.length === 'number' && - typeof newData.splice === 'function') { - // looks like an array - var array = newData; - - for (var i=0, N=array.length; i', - ADDED: '', - BUILT: '', - DESTROYED: '' -}); - -Component.include({ - stage: Component.INITIAL, - - // use this internally, not to produce error messages for - // developers - _assertStage: function (stage) { - if (this.stage !== stage) - throw new Error("Need " + stage + " Component, found " + - this.stage + " Component."); - }, - - // use this to produce error messages - _requireNotDestroyed: function () { - if (this.stage === Component.DESTROYED) - throw new Error("Component has been destroyed; can't perform this operation"); - }, - - _requireBuilt: function () { - this._requireNotDestroyed(); - if (this.stage !== Component.BUILT) - throw new Error("Component must be built into DOM to use this function"); - }, - - _added: function () { - this._assertStage(Component.INITIAL); - this.stage = Component.ADDED; - this.init(); - }, - - _built: function () { - this.stage = Component.BUILT; - this.built(); - }, - - destroy: function () { - if (this.stage === Component.DESTROYED) - return; - - this.stage = Component.DESTROYED; - - this.destroyed(); - }, - - init: function () {}, - built: function () {}, - destroyed: function () {}, - - extendHooks: { - init: 'chain', - built: 'chain', - destroyed: 'chain' - } -}); diff --git a/packages/ui.old/lookup.js b/packages/ui.old/lookup.js deleted file mode 100644 index 1f9e970e33..0000000000 --- a/packages/ui.old/lookup.js +++ /dev/null @@ -1,61 +0,0 @@ - -var global = (function () { return this; })(); - -var findComponentWithProp = function (id, comp) { - while (comp) { - if (id in comp) - return comp; - comp = comp.parent; - } - return null; -}; - -Component.include({ - lookup: function (id) { - var self = this; - - var result = null; - var thisToBind = null; - - // XXX figure out what this should really do, - // and how custom component classes should - // hook into this behavior. - - var cmp; - if (! id) { - result = self.data(); - } else if ((cmp = findComponentWithProp(id, self))) { - result = cmp[id]; - thisToBind = cmp; - } else if (id === 'if') { - result = UI.If; - } else if (id === 'each') { - result = UI.Each; - } else if (id === 'unless') { - result = UI.Unless; - } else if (id === 'with') { - result = Component; - } else if (/^[A-Z]/.test(id) && (id in global)) { - // Only look for a global identifier if `id` is - // capitalized. This avoids have `{{name}}` mean - // `window.name`. - result = global[id]; - thisToBind = self.data(); - } else { - // check `data()` last, because it establishes - // a dependency. - var data = self.data(); - if (data != null) { - thisToBind = data; - result = data[id]; - } - } - - if (thisToBind && - typeof result === 'function' && - ! Component.isType(result)) - return _.bind(result, thisToBind); - - return result; - } -}); diff --git a/packages/ui.old/notes.txt b/packages/ui.old/notes.txt deleted file mode 100644 index f4ba80da81..0000000000 --- a/packages/ui.old/notes.txt +++ /dev/null @@ -1,81 +0,0 @@ - - -TO FIX: -- get rid of `include`? - - no `new` means include - - maybe eliminate `create` - - just need good error messages if you forget `new` - - Problem: Pretty hard to detect if you forget `new`. - `Template.foo` used to be a pure function; now it - modifies classes? -- how to let helpers access data in `this`? -- support primitive data arg? -- `#if` shouldn't set data, got confused there... -- eliminate makeRoot? - -- MAYBE TEMPLATES ARE NOT COMPONENTS AFTER ALL - - Templates have helpers with data in `this` - - Template.foo(...) works but FooComponent(...) doesn't - - -TODO: -- event maps - - -- create/extend: ok to put primitives on proto in create? - -==================== - - - - - - -TextSearchComponent = Component.extend({ - SubView: FooView, - myStuff: {}, - init: function () { - this.x = new this.subView({}); - }, - helpers: { - function () { - - } - } -}); - -Template.foo.helpers({ - prettyName: -}); -Template.foo.events({ - prettyName: -}); - - - - -Template.foo.include({ - extend: FooComponent -}); - - -.count() - -new Foo(function () {}) - -{{#each foo}} - - - - -built: function () { - this.append(new TextBox); - this.append( -} - diff --git a/packages/ui.old/old/component.js b/packages/ui.old/old/component.js deleted file mode 100644 index 4cce5bf514..0000000000 --- a/packages/ui.old/old/component.js +++ /dev/null @@ -1,772 +0,0 @@ -// @export UI -UI = { - isComponentClass: function (value) { - return (typeof value === 'function') && - ((value === Component) || - (value.prototype instanceof Component)); - } -}; - -var constructorsLocked = true; - -var global = (function () { return this; })(); - -// @export Component -Component = function (args) { - if (! (this instanceof Component)) { - // without `new`, `Component(...)` is an alias for - // `Component.augment(...)`. This code controls just - // the base class, but derived classes have the same logic. - return Component.augment.apply(Component, arguments); - } - - if (constructorsLocked) - throw new Error("To create a Component, " + - "use ComponentClass.create(...)"); - constructorsLocked = true; - - this.stage = Component.UNADDED; - - this._uniqueIdCounter = 1; - - // UNINITED Components get these: - this._args = args || {}; - this._argDeps = {}; - - // INITED Components get these: - this.key = ''; - this.parent = null; - this.children = {}; - - // BUILT Components get these: - this._start = null; // first Component or Node - this._end = null; // last Component or Node - this.isAttached = false; - this._detachedContent = null; // DocumentFragment - - this._buildUpdater = null; - this._childUpdaters = {}; - this.elements = {}; - - // element annotations, defined during build - this.annotations = {}; - - this.constructed(); -}; - -// life stages of a Component -_.extend(Component, { - UNADDED: ['UNADDED'], - ADDED: ['ADDED'], - BUILT: ['BUILT'], - DESTROYED: ['DESTROYED'] -}); - -// Fills in for _start and _end on a temporary basis. -var EMPTY = ['EMPTY']; - -_.extend(Component.prototype, { - _requireStage: function (stage) { - if (this.stage !== stage) - throw new Error("Need " + stage + " Component, found " + - this.stage + " Component."); - }, - _added: function (key, parent) { - this._requireStage(Component.UNADDED); - this.key = key; - this.parent = parent; - this.stage = Component.ADDED; - this.init(); - }, - build: function () { - var self = this; - self._requireStage(Component.ADDED); - self._buildUpdater = - Deps.autorun(function (c) { - var isRebuild = (self.stage === Component.BUILT); - var oldFirstNode, oldLastNode; - if (isRebuild) { - oldFirstNode = self.firstNode(); - oldLastNode = self.lastNode(); - Deps.nonreactive(function () { - for (var k in self.children) { - if (self.children.hasOwnProperty(k)) { - var child = self.children[k]; - child.destroy(); - self.removeChild(child.key); - } - } - }); - self.elements = {}; - self.stage = Component.ADDED; - } - var buf = new RenderBuffer(self); - self.render(buf); - var buildResult = buf.build(); - if (isRebuild) { - var parentNode = oldFirstNode.parentNode; - var beforeNode = oldLastNode.nextSibling; - DomUtils.extractRange(oldFirstNode, oldLastNode); - parentNode.insertBefore(buildResult.fragment, - beforeNode || null); - } else { - self._detachedContent = buildResult.fragment; - } - self._start = buildResult.start; - self._end = buildResult.end; - - self.stage = Component.BUILT; - Deps.nonreactive(function () { - if (c.firstRun) { - self.built(); - } else { - self.rebuilt(); - } - }); - }); - }, - destroy: function () { - // Leaves the DOM and component hierarchy in place - - if (this.stage === Component.DESTROYED) - return; - - var oldStage = this.stage; - this.stage = Component.DESTROYED; - - if (oldStage === Component.UNADDED) - return; - - if (this._buildUpdater) - this._buildUpdater.stop(); - - for (var k in this._childUpdaters) { - if (this._childUpdaters.hasOwnProperty(k)) { - this._childUpdaters[k].stop(); - delete this._childUpdaters[k]; - } - } - - // maybe GC sooner - this._start = null; - this._end = null; - - this.destroyed(); - - var children = this.children; - for (var k in children) - if (children.hasOwnProperty(k)) - children[k].destroy(); - }, - attach: function (parentNode, beforeNode) { - var self = this; - if (self.stage === Component.ADDED) // not built - self.build(); - - var parent = self.parent; - - self._requireStage(Component.BUILT); - if (self.isAttached) - throw new Error("Component already attached"); - - if ((! parentNode) || ! parentNode.nodeType) - throw new Error("first argument of attach must be a Node"); - if (beforeNode && ! beforeNode.nodeType) - throw new Error("second argument of attach must be a Node" + - " if given"); - - var frag = self._detachedContent; - - if (DomUtils.wrapFragmentForContainer(frag, parentNode)) - self.setBounds(frag.firstChild, frag.lastChild); - - parentNode.insertBefore(frag, beforeNode || null); - self._detachedContent = null; - - self.isAttached = true; - - if (parent && parent.stage === Component.BUILT) { - if (parent._start === EMPTY) { - parent.setBounds(self); - } else { - if (parent.firstNode() === self.lastNode().nextSibling) - parent.setStart(self); - if (parent.lastNode() === self.firstNode().previousSibling) - parent.setEnd(self); - } - } - - self.attached(); - }, - detach: function (_allowTransientEmpty) { - var self = this; - var parent = self.parent; - - if (parent) - parent._requireStage(Component.BUILT); - self._requireStage(Component.BUILT); - if (! self.isAttached) - throw new Error("Component not attached"); - - if (parent) { - if (parent._start === self) { - if (parent._end === self) { - if (_allowTransientEmpty) - parent._start = parent._end = EMPTY; - else - throw new Error("Can't detach entire contents of " + - "Component; use swapInChild instead"); - } else { - var newFirstNode = self.lastNode().nextSibling; - var foundComp = null; - for (var k in parent.children) { - if (parent.children.hasOwnProperty(k) && - parent.children[k].firstNode() === newFirstNode) { - foundComp = parent.children[k]; - break; - } - } - parent.setStart(foundComp || newFirstNode); - } - } else if (parent._end === self) { - var newLastNode = self.firstNode().previousSibling; - var foundComp = null; - for (var k in parent.children) { - if (parent.children.hasOwnProperty(k) && - parent.children[k].lastNode() === newLastNode) { - foundComp = parent.children[k]; - break; - } - } - parent.setEnd(foundComp || newLastNode); - } - } - - self._detachedContent = document.createDocumentFragment(); - - DomUtils.extractRange(self.firstNode(), self.lastNode(), - self._detachedContent); - - self.isAttached = false; - - self.detached(); - }, - swapInChild: function (toAttach, toDetach) { - var parentNode = toDetach.parentNode(); - var beforeNode = toDetach.lastNode().nextSibling; - toDetach.detach(true); - toAttach.attach(parentNode, beforeNode); - }, - getPreviewHtml: function () { - this._requireStage(Component.ADDED); - var buf = new RenderBuffer(this, { preview: true }); - this.render(buf); - return buf.getFullHtml(); - } -}); - -// Once the Component is built, if the Component implementation -// modifies the DOM composition of the Component, it must specify -// the new bounds using some combination of these. -_.extend(Component.prototype, { - setStart: function (start) { - this._requireStage(Component.BUILT); - - if (! ((start instanceof Component && - start.stage === Component.BUILT) || - (start && start.nodeType))) - throw new Error("start must be a built Component or a Node"); - - this._start = start; - }, - setEnd: function (end) { - this._requireStage(Component.BUILT); - - if (! ((end instanceof Component && - end.stage === Component.BUILT) || - (end && end.nodeType))) - throw new Error("end must be a built Component or a Node"); - - this._end = end; - }, - setBounds: function (start, end) { - end = end || start; - this.setStart(start); - this.setEnd(end); - }, - firstNode: function () { - this._requireStage(Component.BUILT); - return this._start instanceof Component ? - this._start.firstNode() : this._start; - }, - lastNode: function () { - this._requireStage(Component.BUILT); - return this._end instanceof Component ? - this._end.lastNode() : this._end; - }, - parentNode: function () { - return this.firstNode().parentNode; - }, - findOne: function (selector) { - return DomUtils.findClipped( - this.parentNode(), selector, - this.firstNode(), this.lastNode()); - }, - findAll: function (selector) { - return DomUtils.findAllClipped( - this.parentNode(), selector, - this.firstNode(), this.lastNode()); - } -}); - -_.extend(Component.prototype, { - getArg: function (argName) { - var dep = (this._argDeps.hasOwnProperty(argName) ? - this._argDeps[argName] : - (this._argDeps[argName] = new Deps.Dependency)); - dep.depend(); - return this._args[argName]; - }, - getData: function () { - var self = this; - // look for data arg, maybe in parent. stop as - // soon as we find a non-null value. - var comp = self; - var data = self.getArg('data'); - // `== null` means null or undefined - while (data == null && comp.parent) { - comp = comp.parent; - data = comp.getArg('data'); - } - if (data == null) - data = null; - return data; - }, - update: function (args) { - var oldArgs = this._args; - this._args = args; - - var argDeps = this._argDeps; - - for (var k in args) { - if (args.hasOwnProperty(k) && - argDeps.hasOwnProperty(k) && - ! EJSON.equals(args[k], oldArgs[k])) { - argDeps[k].changed(); - delete oldArgs[k]; - } - } - for (var k in oldArgs) { - if (oldArgs.hasOwnProperty(k) && - argDeps.hasOwnProperty(k)) { - argDeps[k].changed(); - } - } - - this.updated(args, oldArgs); - } -}); - -_.extend(Component.prototype, { - hasChild: function (key) { - return this.children.hasOwnProperty(key); - }, - addChild: function (key, childComponentOrFunc, - attachParentNode, - attachBeforeNode) { - if ((key instanceof Component) || - ((typeof key) === 'function')) { - // omitted key arg - childComponentOrFunc = key; - key = null; - } - - // omitted key, generate unique child key - if (key === null || typeof key === 'undefined') - key = "__child#" + (this._uniqueIdCounter++) + "__"; - key = String(key); - - var self = this; - if (self.stage === Component.DESTROYED) - throw new Error("parent Component already destroyed"); - if (self.stage === Component.UNADDED) - throw new Error("parent Component is unadded"); - - if (self.hasChild(key)) - throw new Error("Already have a child with key: " + key); - - var childComponent; - if (typeof childComponentOrFunc === 'function') { - var func = childComponentOrFunc; - this._childUpdaters[key] = - Deps.autorun(function (c) { - if (c.firstRun) { - childComponent = func(); - return; - } - var oldChild = self.children[key]; - if ((! (oldChild instanceof Component)) || - oldChild.stage === Component.DESTROYED) { - // child shouldn't be missing, but may be - // destroyed - c.stop(); - return; - } - var newChild = func(); - if (! (newChild instanceof Component)) - throw new Error("not a Component: " + newChild); - if (oldChild.constructor === newChild.constructor) { - oldChild.update(newChild._args); - } else { - self.replaceChild(key, newChild); - } - }); - } else { - childComponent = childComponentOrFunc; - } - - if (! (childComponent instanceof Component)) - throw new Error("not a Component: " + childComponent); - - childComponent._requireStage(Component.UNADDED); - - self.children[key] = childComponent; - - childComponent._added(key, self); - - if (attachParentNode) { - if (self.stage !== Component.BUILT) - throw new Error("Attaching new child requires built " + - "parent Component"); - childComponent.attach(attachParentNode, attachBeforeNode); - } - - return childComponent; - }, - removeChild: function (key, _allowTransientEmpty) { - // note: must work if child is destroyed - - key = String(key); - - if (this.stage === Component.DESTROYED) - throw new Error("parent Component already destroyed"); - if (this.stage === Component.UNADDED) - throw new Error("parent Component is unadded"); - - if (! this.hasChild(key)) - throw new Error("No such child component: " + key); - - var childComponent = this.children[key]; - if (childComponent.stage === Component.BUILT && - childComponent.isAttached) - childComponent.detach(_allowTransientEmpty); - - delete this.children[key]; - - if (this._childUpdaters[key]) { - this._childUpdaters[key].stop(); - delete this._childUpdaters[key]; - } - - childComponent.parent = null; - - childComponent.destroy(); - }, - replaceChild: function (key, newChild, newKey) { - if (this.stage === Component.DESTROYED) - throw new Error("parent Component already destroyed"); - if (this.stage === Component.UNADDED) - throw new Error("parent Component is unadded"); - - if (! this.hasChild(key)) - throw new Error("No such child component: " + key); - - if (! (newChild instanceof Component)) - throw new Error("Component required"); - - if ((typeof newKey) !== 'string') - newKey = key; - - var oldChild = this.children[key]; - - if (newKey === key && - oldChild.constructor === newChild.constructor) { - oldChild.update(newChild._args); - } else if (this.stage !== Component.BUILT || - oldChild.stage !== Component.BUILT || - ! oldChild.isAttached) { - this.removeChild(key); - this.addChild(newKey, newChild); - } else { - // swap attached child - var parentNode = oldChild.parentNode(); - var beforeNode = oldChild.lastNode().nextSibling; - this.removeChild(key, true); - this.addChild(newKey, newChild, parentNode, beforeNode); - } - }, - addAnnotation: function (elementKey, annotation) { - annotation.component = this; - - if (! this.annotations[elementKey]) - this.annotations[elementKey] = []; - this.annotations[elementKey].push(annotation); - }, - registerElement: function (elementKey, element) { - this.elements[elementKey] = element; - if (this.annotations[elementKey]) { - _.each(this.annotations[elementKey], function (ann) { - ann.element = element; - ann.wired && ann.wired(); - }); - } - } -}); - -_.extend(Component.prototype, { - render: function (buf) { - var content = this.getArg('content'); - if (content) - buf.component(content.create(), {key: 'content'}); - } -}); - -var allCallbacks = { - constructed: function () {}, - init: function () {}, - updated: function (args, oldArgs) {}, - destroyed: function () {}, - attached: function () {}, - detached: function () {}, - built: function () {}, - rebuilt: function () {} -}; - -_.extend(Component.prototype, allCallbacks); - -_.extend(Component.prototype, { - lookup: function (id) { - var self = this; - - var result = null; - var thisToBind = null; - - // XXX figure out what this should really do, - // and how custom component classes should - // hook into this behavior. - - if (! id) { - result = self.getData(); - } else if (id in self) { - result = self[id]; - thisToBind = self; - } else if (id === 'if') { - result = If; - } else if (id === 'each') { - result = Each; - } else if (id === 'unless') { - result = Unless; - } else if (id === 'with') { - result = Component; - } else if (id in global) { - result = global[id]; - thisToBind = self.getData(); - } else if ((result = self.getArg(id)) != null) { - // (`!= null` means not `null` or `undefined`) - thisToBind = self; - } else { - var data = self.getData(); - if (data !== null) { - thisToBind = data; - result = data[id]; - } - } - - if (thisToBind && - typeof result === 'function' && - ! UI.isComponentClass(result)) - return _.bind(result, thisToBind); - - return result; - }, - dispatch: function (event) { - var self = this; - - if (event.name in self) { - self[event.name].call(self, event); - } else if (self.parent) { - self.parent.dispatch(event); - } - } -}); - -// Require ComponentClass.create(...) instead of -// new ComponentClass(...) because a factory method gives -// us more flexibility, and there should be one way to -// make a component. The `new` syntax is awkward if -// the component class is calculated by a complex expression -// (like a reactive getter). -Component.create = function (args) { - constructorsLocked = false; - var comp; - if (this === Component) { - comp = new Component(args); - } else { - comp = new this; - Component.call(comp, args); - } - return comp; -}; - - -var setSuperClass = function (subClass, superClass) { - // Establish a prototype link from newClass.prototype to - // superClass.prototype. This is similar to making - // newClass.prototype a `new superClass` but bypasses - // the constructor. - - var oldProto = subClass.prototype; - - var fakeSuperClass = function () {}; - fakeSuperClass.prototype = superClass.prototype; - subClass.prototype = new fakeSuperClass; - - // Inherit class (static) properties from parent. - _.extend(subClass, superClass); - - // Record the (new) superClass for our future use. - subClass.superClass = superClass; - - // restore old properties on proto (from previous augments - // or extends) - for (var k in oldProto) - if (oldProto.hasOwnProperty(k)) - subClass.prototype[k] = oldProto[k]; - - // For browsers that don't support it, fill in `obj.constructor`. - subClass.prototype.constructor = subClass; - - subClass.create = Component.create; -}; - -Component.augment = function (options) { - var cls = this; - - if (! options) - throw new Error("Options object required to augment object"); - - if ('extend' in options) { - var newSuper = options.extend; - if (! UI.isComponentClass(newSuper)) - throw new Error("'extend' option must be a Component class"); - - if (cls.superClass !== Component) - throw new Error("Can only set superclass once, on generic Component"); - - if (newSuper !== cls.superClass) { - setSuperClass(cls, newSuper); - } - delete options.extend; - } - - _.each(options, function (propValue, propKey) { - // (it's important that this loop body is a closure, - // because local variables are closed over by nested - // closures and those variables have different values - // each time through the loop) - if (allCallbacks.hasOwnProperty(propKey)) { - // Property is on our list of callbacks. - // Callbacks chain with previous callbacks - // and super's callback. - if (cls.prototype.hasOwnProperty(propKey)) { - // not the first time this callback has been defined - // on this class! Chain with previous, not super. - var prevFunction = cls.prototype[propKey]; - cls.prototype[propKey] = function (/*arguments*/) { - prevFunction.apply(this, arguments); - propValue.apply(this, arguments); - }; - } else { - // First time this callback has been defined on this - // class. Chain with super. - cls.prototype[propKey] = function (/*arguments*/) { - if (cls.superClass) - cls.superClass.prototype[propKey].apply(this, arguments); - propValue.apply(this, arguments); - }; - } - } else { - // normal, non-callback method or other property - cls.prototype[propKey] = propValue; - } - }); - - return cls; -}; - -Component.extend = function (options) { - var superClass = this; - // all constructors just call the base constructor - var newClass = function CustomComponent(/*arguments*/) { - if (! (this instanceof newClass)) { - // without `new`, `MyComp(...)` is an alias for - // `MyComp.augment(...)`. - return newClass.augment.apply(newClass, arguments); - } - - if (constructorsLocked) - throw new Error("To create a Component, " + - "use ComponentClass.create(...)"); - // (Component.create kicks off construction) - }; - - setSuperClass(newClass, superClass); - - if (options) - newClass.augment(options); - - return newClass; -}; - -// @export TextComponent -TextComponent = Component.extend({ - render: function (buf) { - buf.text(this.getArg('text')); - } -}); - -// @export RawHtmlComponent -RawHtmlComponent = Component.extend({ - render: function (buf) { - buf.rawHtml(this.getArg('html')); - } -}); - -// A RootComponent is the root of its Component tree in terms -// of parent/child relationships. It's the only kind of -// component that can function without actually being added -// to another component first. - -// @export RootComponent -RootComponent = Component.extend({ - constructed: function () { - // skip the UNADDED phase completely - this.stage = Component.ADDED; - - this._uid = Random.id(); - - // this would normally be called upon "add" - this.init(); - }, - attached: function () { - RootComponent._attachedInstances[this._uid] = this; - }, - detached: function () { - delete RootComponent._attachedInstances[this._uid]; - }, - destroyed: function () { - delete RootComponent._attachedInstances[this._uid]; - } -}); - -RootComponent._attachedInstances = {}; \ No newline at end of file diff --git a/packages/ui.old/old/library.js b/packages/ui.old/old/library.js deleted file mode 100644 index 796192dd10..0000000000 --- a/packages/ui.old/old/library.js +++ /dev/null @@ -1,205 +0,0 @@ -// @export If -If = Component.extend({ - render: function (buf) { - if (this.getArg('data')) { - if (this.getArg('content')) - buf.component(this.getArg('content').create()); - } else if (this.getArg('elseContent')) { - buf.component(this.getArg('elseContent').create()); - } - } -}); - -// @export Unless -Unless = Component.extend({ - render: function (buf) { - if (! this.getArg('data')) { - if (this.getArg('content')) - buf.component(this.getArg('content').create()); - } else if (this.getArg('elseContent')) { - buf.component(this.getArg('elseContent').create()); - } - } -}); - -// @export Each -Each = Component.extend({ - - // XXX what is init() good for if render lets you reactively - // depend on args, but init doesn't? (you can access them - // but your code only ever runs once) - - render: function (buf) { - var self = this; - // XXX support arrays too. - // For now, we assume the data is a database cursor. - var cursor = self.getArg('data'); - - // OrderedDict from id string or object (which is - // stringified internally by the dict) to untransformed - // document. - self.items = new OrderedDict(idStringify); - var items = self.items; - - // Templates should have access to data and methods added by the - // transformer, but observeChanges doesn't transform, so we have to do - // it here. - // - // NOTE: this is a little bit of an abstraction violation. Ideally, - // the only thing we should know about Minimongo is the contract of - // observeChanges. In theory, we could allow anything that implements - // observeChanges to be passed to us. - var transformedDoc = function (doc) { - if (cursor.getTransform && cursor.getTransform()) - return cursor.getTransform()(EJSON.clone(doc)); - return doc; - }; - - // because we're in render(), rebuild or destroy will - // stop this handle. - self.cursorHandle = cursor.observeChanges({ - addedBefore: function (id, item, beforeId) { - var doc = EJSON.clone(item); - doc._id = id; - items.putBefore(id, doc, beforeId); - - if (self.stage === Component.BUILT) { - var tdoc = transformedDoc(doc); - self.itemAddedBefore(id, tdoc, beforeId); - } - }, - removed: function (id) { - items.remove(id); - - if (self.stage === Component.BUILT) - self.itemRemoved(id); - }, - movedBefore: function (id, beforeId) { - items.moveBefore(id, beforeId); - - if (self.stage === Component.BUILT) - self.itemMovedBefore(id, beforeId); - }, - changed: function (id, fields) { - var doc = items.get(id); - if (! doc) - throw new Error("Unknown id for changed: " + idStringify(id)); - applyChanges(doc, fields); - - if (self.stage === Component.BUILT) { - var tdoc = transformedDoc(doc); - self.itemChanged(id, tdoc); - } - } - }); - - if (items.empty()) { - buf.component(function () { - return (self.getArg('elseContent') || Component).create( - { data: self.getArg('data') }); - }, { key: 'elseContent' }); - } else { - items.forEach(function (doc, id) { - var tdoc = transformedDoc(doc); - - buf.component(function () { - return self.getArg('content').create({ data: tdoc }); - }, { key: self._itemChildId(id) }); - }); - } - }, - - _itemChildId: function (id) { - return 'item:' + idStringify(id); - }, - itemAddedBefore: function (id, doc, beforeId) { - var self = this; - if (self.stage !== Component.BUILT) - throw new Error("Component must be built"); - - var childId = self._itemChildId(id); - var comp = self.getArg('content').create({data: doc}); - - if (self.items.size() === 1) { - // was empty - self.replaceChild('elseContent', comp, childId); - } else { - var beforeNode = - (beforeId ? - self.children[self._itemChildId(beforeId)].firstNode() : - (self.lastNode().nextSibling || null)); - var parentNode = self.parentNode(); - - self.addChild(childId, comp, parentNode, beforeNode); - } - }, - itemRemoved: function (id) { - var self = this; - if (self.stage !== Component.BUILT) - throw new Error("Component must be built"); - - var childId = self._itemChildId(id); - if (self.items.size() === 0) { - // made empty - var elseClass = self.getArg('elseContent') || Component; - var comp = elseClass.create({data: self.getArg('data')}); - self.replaceChild(childId, comp, 'elseContent'); - } else { - self.removeChild(childId); - } - }, - itemMovedBefore: function (id, beforeId) { - var self = this; - if (self.stage !== Component.BUILT) - throw new Error("Component must be built"); - - if (self.items.size() === 1) - return; // move is meaningless anyway - - var comp = self.children[self._itemChildId(id)]; - - var beforeNode = - (beforeId ? - self.children[self._itemChildId(beforeId)].firstNode() : - (self.lastNode().nextSibling || null)); - var parentNode = self.parentNode(); - - comp.detach(); - comp.attach(parentNode, beforeNode); - }, - itemChanged: function (id, doc) { - var self = this; - if (self.stage !== Component.BUILT) - throw new Error("Component must be built"); - - self.children[self._itemChildId(id)].update({data: doc}); - } -}); - -// Function equal to LocalCollection._idStringify, or the identity -// function if we don't have LiveData. Converts item keys (i.e. DDP -// keys) to strings for storage in an OrderedDict. -var idStringify; - -// XXX not clear if this is the right way to do a weak dependency -// now, post-linker -if (typeof LocalCollection !== 'undefined') { - idStringify = function (id) { - if (id === null) - return id; - else - return LocalCollection._idStringify(id); - }; -} else { - idStringify = function (id) { return id; }; -} - -// XXX duplicated code from minimongo.js. -var applyChanges = function (doc, changeFields) { - _.each(changeFields, function (value, key) { - if (value === undefined) - delete doc[key]; - else - doc[key] = value; - }); -}; diff --git a/packages/ui.old/old/renderbuffer.js b/packages/ui.old/old/renderbuffer.js deleted file mode 100644 index 8056b60713..0000000000 --- a/packages/ui.old/old/renderbuffer.js +++ /dev/null @@ -1,330 +0,0 @@ -// RenderBuffer is a friend class of Component that provides the -// API for implementations of comp.render(buf) and knows how to -// buffer HTML and then optionally wire it up as reactive DOM. -// -// Each Component creates its own instance of RenderBuffer during -// render (i.e. build or server-side HTML generation). - -// @export RenderBuffer -RenderBuffer = function (component, options) { - this._component = component; - if (! (component instanceof Component)) - throw new Error("Component required as first argument"); - - this._htmlBuf = []; - - this._isPreview = !! (options && options.preview); - - this._builderId = Random.id(); - this._nextNum = 1; - this._elementNextNums = {}; - - this._childrenToAttach = []; // comment string -> component -}; - -var TAG_NAME_REGEX = /^[a-zA-Z0-9]+$/; -var ATTRIBUTE_NAME_REGEX = /^[^\s"'>/=/]+$/; -var ESCAPED_CHARS_UNQUOTED_REGEX = /[&<>]/g; -var ESCAPED_CHARS_QUOTED_REGEX = /[&<>"]/g; - -var escapeMap = { - "<": "<", - ">": ">", - "&": "&", - '"': """ -}; -var escapeOne = function(c) { - return escapeMap[c]; -}; - -var encodeEntities = function (text, isQuoted) { - // All HTML entities in templates are decoded by the template parser - // and given to RenderBuffer as Unicode. We then re-encode some - // characters into entities here, but not most characters. If - // you're trying to use entities to send ASCII representations of - // non-ASCII characters to the client, you'll need a different - // policy here. - return text.replace(isQuoted ? ESCAPED_CHARS_QUOTED_REGEX : - ESCAPED_CHARS_UNQUOTED_REGEX, escapeOne); -}; - -var updateDOMAttribute = function (component, elemKey, attrName, - newValue, oldValue) { - // XXX Do smart stuff here, like treat "class" attribute - // specially, and set JS properties instead of HTML attributes - // when appropriate. Don't get too crazy, though. Fancy - // manipulation of DOM elements can be done programmatically - // instead. - // - // Allow Components to hook into (i.e. replace) this update - // logic for attributes of their choice? - var elem = component.elements[elemKey]; - elem.setAttribute(attrName, newValue); -}; - - -_.extend(RenderBuffer.prototype, { - _encodeEntities: encodeEntities, - // XXX implement dynamicAttrs option, - // takes [[k, v], ...], does something fancy to parse out - // name=value dynamically. For example, `` - // leads to `{ dynamicAttrs: [[attrs(), ''] }`, and each time - // `attrs()` is evaluated, it is tokenized for attribute - // assignments. - openTag: function (tagName, attrs, options) { - var self = this; - - if ((typeof tagName) !== 'string' || - ! TAG_NAME_REGEX.test(tagName)) - throw new Error("Illegal HTML tag name: " + tagName); - - attrs = attrs || {}; - options = options || {}; - - var isElementReactive = false; - // Any reactive updaters here will close over this variable, - // which we set to something non-null as soon as we are - // sure we need to generate a key. - var elementKey = (options.key || null); - var requireElementKey = function () { - if (! elementKey) { - if (! self._elementNextNums[tagName]) - self._elementNextNums[tagName] = 1; - elementKey = tagName + - (self._elementNextNums[tagName]++); - } - }; - - var buf = self._htmlBuf; - buf.push('<', tagName); - _.each(attrs, function (attrValue, attrName) { - if ((typeof attrName) !== 'string' || - ! ATTRIBUTE_NAME_REGEX.test(attrName)) - throw new Error("Illegal HTML attribute name: " + attrName); - - buf.push(' ', attrName, '="'); - var initialValue; - if (typeof attrValue === 'function') { - var func = attrValue; - // we assume we've been called from some Component build, - // so this autorun will be stopped when the Component - // is rebuilt or destroyed. - isElementReactive = true; - Deps.autorun(function (c) { - if (c.firstRun) { - var newValue = attrValue(); - initialValue = newValue; - c.oldValue = newValue; - if (self._isPreview) - c.stop(); - } else { - var newValue = attrValue(); - var comp = self._component; - if (comp && comp.stage === Component.BUILT) { - // (elementKey has been set by now) - var elem = elementKey && comp.elements[elementKey]; - if (elem) { - updateDOMAttribute(comp, elementKey, attrName, - newValue, c.oldValue); - } - } - c.oldValue = newValue; - } - }); - } else { - initialValue = attrValue; - } - buf.push(self._encodeEntities(initialValue, true)); - buf.push('"'); - }); - - if (options.annotations) { - _.each(options.annotations, function (ann) { - if (ann.type !== 'emit') - return; - - isElementReactive = true; - - if (! self.isPreview) { - requireElementKey(); - - self._component.addAnnotation( - elementKey, { - name: 'emit', - component: self._component, - element: null, - // XXX should probably have setup/teardown here, - // and maybe include reactive attributes under - // the same mechanism. Maybe it's an autorun. - wired: function () { - var self = this; - _.each(ann.data, function (v, k) { - if (self.element.addEventListener) { - self.element.addEventListener(k, function (event) { - event.name = v; - self.component.dispatch(event); - }); - } else { - // XXX IE - } - }); - } - }); - } - - }); - } - - if (isElementReactive && ! self._isPreview) { - requireElementKey(); - - buf.push(' data-meteorui-id="' + - self._encodeEntities(elementKey, true) + '"'); - } - - if (options.selfClose) - buf.push('/'); - buf.push('>'); - }, - closeTag: function (tagName) { - if ((typeof tagName) !== 'string' || - ! TAG_NAME_REGEX.test(tagName)) - throw new Error("Illegal HTML tag name: " + tagName); - this._htmlBuf.push(''); - }, - text: function (stringOrFunction) { - if (typeof stringOrFunction === 'function') { - var func = stringOrFunction; - this.component(function () { - return TextComponent.create({text: func()}); - }); - } else { - if (typeof stringOrFunction !== 'string') - throw new Error("string required"); - var text = stringOrFunction; - this._htmlBuf.push(this._encodeEntities(text)); - } - }, - rawHtml: function (stringOrFunction) { - if (typeof stringOrFunction === 'function') { - var func = stringOrFunction; - this.component(function () { - return RawHtmlComponent.create({html: func()}); - }); - } else { - if (typeof stringOrFunction !== 'string') - throw new Error("string required"); - var html = stringOrFunction; - this._htmlBuf.push(html); - } - }, - component: function (componentOrFunction, options) { - var self = this; - - if (! ((componentOrFunction instanceof Component) || - (typeof componentOrFunction === 'function'))) - throw new Error("Component or function required"); - - var childKey = (options && options.key || null); - - var childComp = self._component.addChild( - childKey, componentOrFunction); - - if (self._isPreview) { - self._htmlBuf.push( - childComp.getPreviewHtml()); - } else { - var commentString = self.builderId + '_' + - (self._nextNum++); - self._htmlBuf.push(''); - - self._childrenToAttach[commentString] = childComp; - } - }, - comment: function (stringOrFunction) { - // XXX making comments reactively update seems - // right, for completeness; consider doing that. - - var self = this; - - var content; - if (typeof stringOrFunction === 'function') { - var func = stringOrFunction; - content = func(); - } else { - if (typeof stringOrFunction !== 'string') - throw new Error("string required"); - content = stringOrFunction; - } - - // comments can't have "--" in them in HTML. - // just strip those so that we don't run into trouble. - content = content.replace(/--/g, ''); - self._htmlBuf.push(''); - }, - doctype: function (name, options) { - var buf = this._htmlBuf; - buf.push(''); - }, - build: function () { - var self = this; - - if (self._isPreview) - throw new Error("Can't build preview HTML as DOM"); - - var html = self._htmlBuf.join(''); - var frag = DomUtils.htmlToFragment(html); - if (! frag.firstChild) - frag.appendChild(document.createComment("empty")); - - var components = self._childrenToAttach; - var start = frag.firstChild; - var end = frag.lastChild; - - // wireUpDOM = replace comments with Components and register - // keyed elements - var wireUpDOM = function (parent) { - var n = parent.firstChild; - while (n) { - var next = n.nextSibling; - if (n.nodeType === 8) { // COMMENT - var comp = components[n.nodeValue]; - if (comp) { - if (parent === frag) { - if (n === frag.firstChild) - start = comp; - if (n === frag.lastChild) - end = comp; - } - comp.attach(parent, n); - parent.removeChild(n); - } - } else if (n.nodeType === 1) { // ELEMENT - var elemKey = n.getAttribute('data-meteorui-id'); - if (elemKey) - self._component.registerElement(elemKey, n); - - // recurse through DOM - wireUpDOM(n); - } - n = next; - } - }; - - wireUpDOM(frag); - - return { - fragment: frag, - start: start, - end: end - }; - }, - getFullHtml: function () { - if (! this._isPreview) - throw new Error("Can only get full HTML when previewing"); - - return this._htmlBuf.join(''); - } -}); diff --git a/packages/ui.old/package.js b/packages/ui.old/package.js deleted file mode 100644 index 684ec1d9ca..0000000000 --- a/packages/ui.old/package.js +++ /dev/null @@ -1,31 +0,0 @@ -Package.describe({ - summary: "Meteor UI Components framework" -}); - -Package.on_use(function (api) { - api.use('deps'); - api.use('random'); - api.use('domutils'); - api.use('underscore'); - api.use('ejson'); - api.use('ordered-dict'); - - api.add_files(['base.js', - 'lifecycle.js', - 'tree.js', - 'attrs.js', 'render.js', 'dom.js', - 'forms.js', - 'each.js', - 'components.js', - 'lookup.js']); -}); - -Package.on_test(function (api) { - api.use('tinytest'); - api.use('ui'); - api.use(['test-helpers', 'domutils'], 'client'); - -// api.add_files([ -// 'component_tests.js' -// ], 'client'); -}); diff --git a/packages/ui.old/render.js b/packages/ui.old/render.js deleted file mode 100644 index 01eb1b2658..0000000000 --- a/packages/ui.old/render.js +++ /dev/null @@ -1,257 +0,0 @@ - -var ESCAPED_CHARS_UNQUOTED_REGEX = /[&<>]/g; -var ESCAPED_CHARS_QUOTED_REGEX = /[&<>"]/g; - -var escapeMap = { - "<": "<", - ">": ">", - "&": "&", - '"': """ -}; -var escapeOne = function(c) { - return escapeMap[c]; -}; - -UI.encodeSpecialEntities = function (text, isQuoted) { - // Encode Unicode characters to HTML entities. - // - // This implementation just encodes the characters that otherwise - // wouldn't parse (like `<`) and passes the rest through. You'd - // need to do something different if you care about HTML entities as - // a way to embed special characters in ASCII. - return text.replace(isQuoted ? ESCAPED_CHARS_QUOTED_REGEX : - ESCAPED_CHARS_UNQUOTED_REGEX, escapeOne); -}; - - -var GT_OR_QUOTE = /[>'"]/; - -makeRenderBuffer = function (component, options) { - var isPreview = !! options && options.preview; - - var strs = []; - var componentsToAttach = null; // {} - var randomString = null; // Random.id() - var commentUid = 1; - var elementUid = 1; - // Problem: In the template ``, how do - // we make foo and bar insert some HTML in the stream that - // will allow us to find the element later? Since we don't - // tokenize the HTML here, we can't even be sure whether - // they are in the same tag. We can't emit a duplicate - // extra attribute. We can emit different attributes, - // but if every attr tag emits a different attribute, it - // won't be efficient to find them. - // - // Solution: Emit different attributes, data-meteorui-id1 - // and data-meteorui-id2, not knowing if they are on the - // same element or not. Reset the number, which is - // `curDataAttrNumber`, if we can be absolutely sure a tag - // has ended. To detect if a tag has definitely ended, - // we set `greaterThanEndsTag` to true after an attr tag, - // and set it to false if we see a quote character. If we - // a greater-than (`>`) between the attrs and the next quote - // character, we know the tag has ended and we can reset - // `curDataAttrNumber` to 1. When we look for these - // attributes, we look for attribute names with numbers - // between 1 and `maxDataAttrNumber` inclusive. - var curDataAttrNumber = 1; - var maxDataAttrNumber = 0; - var dataAttrs = null; // []; names of all HTML attributes used - var greaterThanEndsTag = false; - - var attrManagersToWire = null; // {} - - var push = function (/*stringsToPush*/) { - for (var i = 0, N = arguments.length; - greaterThanEndsTag && i < N; - i++) { - // find first greater-than or quote - var match = arguments[i].match(GT_OR_QUOTE); - if (match) { - if (match[0] == '>') - curDataAttrNumber = 1; - // if it's a quote, missed our chance to - // reset the count. either way, stop looking. - greaterThanEndsTag = false; - } - } - strs.push.apply(strs, arguments); - }; - - var handle = function (arg) { - if (arg == null) { - return; - } else if (typeof arg === 'string') { - // "HTML" - push(arg); - } else if (arg instanceof Component) { - // Component - randomString = randomString || Random.id(); - var commentString = randomString + '_' + (commentUid++); - push(''); - componentsToAttach = componentsToAttach || {}; - componentsToAttach[commentString] = arg; - } else if (arg.type) { - // `{type: componentTypeOrFunction, args: object}` - if (Component.isType(arg.type)) { - handle(arg.type.create(arg.args)); - } else if (typeof arg.type === 'function') { - var curType; - component.autorun(function (c) { - // capture dependencies of this line: - var type = arg.type(); - if (c.firstRun) { - curType = type; - } else if (component.stage !== Component.BUILT || - ! component.hasChild(curChild)) { - c.stop(); - } else if (type !== curType) { - var oldChild = curChild; - curType = type; - // don't capture any dependencies here - Deps.nonreactive(function () { - curChild = curType.create(arg.args); - component.replaceChild(oldChild, curChild); - }); - } - }); - var curChild = curType.create(arg.args); - handle(curChild); - } else { - throw new Error("Expected 'type' to be Component or function"); - } - } else if (arg.attrs) { - // `{attrs: functionOrDictionary }` - // attrs object inserts zero or more `name="value"` items - // into the HTML, and can reactively update them later. - // You can have multiple attrs objects in a tag, but they - // can't specify any of the same attributes (i.e. if `{{foo}}` - // and `{{bar}}` in the same tag declare a same-named attribute, - // they won't cooperate). - var elemId = null; - - var manager = new AttributeManager(component, arg.attrs); - - if (manager.isReactive()) { - var elemId = elementUid++; - // don't call the `push` helper, go around it - strs.push(' data-meteorui-id', curDataAttrNumber, - '="', elemId, '" '); - if (curDataAttrNumber > maxDataAttrNumber) { - if (! dataAttrs) { - dataAttrs = []; - attrManagersToWire = {}; - } - dataAttrs[curDataAttrNumber-1] = - 'data-meteorui-id' + curDataAttrNumber; - maxDataAttrNumber = curDataAttrNumber; - } - curDataAttrNumber++; - greaterThanEndsTag = true; - - attrManagersToWire[elemId] = manager; - } - - // don't call the `push` helper, go around it - strs.push(' ', manager.getInitialHTML(), ' '); - - } else { - throw new Error("Expected HTML string, Component, component spec or attrs spec, found: " + arg); - } - }; - - var buf = function (/*args*/) { - for (var i = 0; i < arguments.length; i++) - handle(arguments[i]); - }; - - buf.getHtml = function () { - return strs.join(''); - }; - - buf.wireUpDOM = function (root) { - var start = root.firstChild; - var end = root.lastChild; - - // walk div and replace comments with Components - - var recurse = function (parent) { - var n = parent.firstChild; - while (n) { - var next = n.nextSibling; - if (n.nodeType === 8) { // COMMENT - if (componentsToAttach) { - var comp = componentsToAttach[n.nodeValue]; - if (comp) { - if (parent === root) { - if (n === root.firstChild) - start = comp; - if (n === root.lastChild) - end = comp; - } - if (comp.stage === Component.INITIAL) { - component.add(comp); - } else if (comp.parent !== component) { - throw new Error("Component used in render must be a child " + - "(or addable as one)"); - } - comp.attach(parent, n); - parent.removeChild(n); - delete componentsToAttach[n.nodeValue]; - } - } - } else if (n.nodeType === 1) { // ELEMENT - if (attrManagersToWire) { - // detect elements with reactive attributes - for (var i = 0; i < maxDataAttrNumber; i++) { - var attrName = dataAttrs[i]; - var elemId = n.getAttribute(attrName); - if (elemId) { - var mgr = attrManagersToWire[elemId]; - if (mgr) { - mgr.wire(n, component); - // note: this callback will be called inside - // the build autorun, so its internal - // autorun will be stopped on rebuild - component._onNextBuilt((function (mgr) { - return function () { mgr.start(); }; - })(mgr)); - } - n.removeAttribute(attrName); - } - } - } - - // recurse through DOM - recurse(n); - } - n = next; - } - }; - - if (componentsToAttach || attrManagersToWire) - recurse(root); - - // We should have attached all specified components, but - // if the comments we generated somehow didn't turn into - // comments (due to bad HTML) we won't have found them, - // in which case we clean them up here just to be safe. - if (componentsToAttach) - for (var k in componentsToAttach) - componentsToAttach[k].destroy(); - - // aid GC - componentsToAttach = null; - attrManagersToWire = null; - - return { - // start and end will both be null if div is empty - start: start, - end: end - }; - - }; - - return buf; -}; diff --git a/packages/ui.old/tree.js b/packages/ui.old/tree.js deleted file mode 100644 index 176a7b261e..0000000000 --- a/packages/ui.old/tree.js +++ /dev/null @@ -1,158 +0,0 @@ - -// this is a shared object that lives on prototypes; -// don't ever mutate it! -var EMPTY_OBJECT = {}; - -Component.include({ - parent: null, - - // We declare data structures on the prototype for - // efficiency, but it's dangerous to put mutable objects - // on the prototype because we have to remember never to - // modify them in place. In general you should initialize - // data structures by assigning them to `this` from the `init` - // callback. - // - // public, externally read-only. - children: EMPTY_OBJECT, - - // # component.add(child) - // - // Adds `child` to this component in the parent/child - // hierarchy. - // - // Components must be assembled from "top to bottom." Each - // component must either be added as a child of another, - // or made a root using `component.makeRoot()`, before - // it can receive its own children. This ensures that - // every component already knows its parent when it is - // initialized. A component's parent is permanent; the - // component cannot be removed or reparented without - // destroying it. - // - // The child is not attached to the DOM unless a further call - // to `child.attach` is made specifying where to put the child - // in the DOM. The methods - // `component.append(child)`, `prepend`, and `insert` can also - // be used to add the child to the DOM, and in addition they - // will call `add` if the child is not already added. - // - // Requires `component` is not destroyed. - add: function (child) { - var self = this; - - if (self.stage === Component.DESTROYED) - throw new Error("Can't add child to a DESTROYED component"); - if (self.stage === Component.INITIAL) - throw new Error("Parent component must be added or made a root before a child can be added to it"); - - var guid = child.guid; - - if (self.children[guid]) - throw new Error("Child already added to this component!"); - if (child.stage === Component.DESTROYED) - throw new Error("Can't add DESTROYED child component"); - else if (child.stage !== Component.INITIAL) { - if (! child.parent) - throw new Error("Can't add a root component"); - throw new Error("Child already added to another component"); - } - - // instantiate a new dictionary to hold children rather - // than mutating the proto's empty object - if (self.children === EMPTY_OBJECT) - self.children = {}; - - self.children[guid] = child; - - child.parent = self; - child._added(); - }, - - // # component.remove([child]) - // - // Removes `child` from this component's list of children, - // removes its nodes from the DOM (if it is built and attached), - // and destroys it. If no child is given, removes `component` - // itself from its parent. - // - // If you want to just remove a component from the DOM but not - // remove it as a child or destroy it, use `child.detach()`. - // - // If `child` is already destroyed, its DOM is left untouched. - // Components with destroyed children still attached are - // presumed to be in the process of being destroyed or rebuilt. - // - // Requires `component` is not destroyed. - // - // Updates `start` and `end` and populates the component with - // a comment if it becomes empty. - remove: function (child) { - var self = this; - - self._requireNotDestroyed(); - - if (! child) { - // Support `()` form of args; remove self. - // Can't `remove()` if we are a root or haven't been parented. - if (self.stage === Component.INITIAL || ! self.parent) - throw new Error("Component to remove must have a parent"); - self.parent.remove(self); - return; - } - - // Don't make any requirements of the child's stage, - // though if it's actually a child, it can't be INTIAL. - // It may be DESTROYED. - - // Note that child is not removed from the DOM if it is already - // destroyed. This is used when a Component is rebuilt -- the - // children are first destroyed, then removed as children, then - // removed from the DOM wholesale in one operation. - if (child.stage === Component.BUILT && - child.isAttached) { - - child.detach(true); // _forDestruction = true - } - - var guid = child.guid; - if (! self.children[guid]) - throw new Error("Child not found (id " + guid + ")"); - - delete self.children[guid]; - // (don't delete child.parent pointer, could be useful - // in destroyed callback?) - - child.destroy(); - }, - - makeRoot: function () { - var self = this; - self._requireNotDestroyed(); - if (self.stage !== Component.INITIAL) - throw new Error("Component already added or made a root"); - - self._added(); - }, - - hasChild: function (comp) { - this._requireNotDestroyed(); - - return this.children[comp.guid] === comp; - }, - - extendHooks: { - isRoot: function (value) { - if (value) - this.include({ - constructed: function () { - this.makeRoot(); } }); - } - }, - - destroyed: function () { - // recursively destroy children as well - for (var k in this.children) - this.children[k].destroy(); - } -}); \ No newline at end of file diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index 4eb625d9b5..3626fd52e0 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -141,7 +141,6 @@ _extend(AttributeHandler.prototype, { } }); -// @export AttributeHandler AttributeHandler.extend = function (options) { var curType = this; var subType = function AttributeHandlerSubtype(/*arguments*/) { diff --git a/packages/ui/base.js b/packages/ui/base.js index 1175806242..22ba844640 100644 --- a/packages/ui/base.js +++ b/packages/ui/base.js @@ -8,7 +8,6 @@ _extend = function (tgt, src) { return tgt; }; -// @export UI UI = { nextGuid: 2, // Component is 1! diff --git a/packages/ui/components.js b/packages/ui/components.js index d694571f15..e29c9b6dc6 100644 --- a/packages/ui/components.js +++ b/packages/ui/components.js @@ -61,7 +61,6 @@ UI.Unless = Component.extend({ }); // for the demo..... -// @export FadeyIf FadeyIf = Component.extend({ typeName: 'FadeyIf', animationDuration: 1000, @@ -141,7 +140,6 @@ FadeyIf = Component.extend({ } }); -// @export Checkbox Checkbox = UI.makeTemplate(Component.extend({ typeName: 'Checkbox', init: function () { @@ -200,4 +198,4 @@ UI.Counter = Component.extend({ }); } }); - */ \ No newline at end of file + */ diff --git a/packages/ui/each.js b/packages/ui/each.js index 950772923a..e8412f0a1d 100644 --- a/packages/ui/each.js +++ b/packages/ui/each.js @@ -276,7 +276,7 @@ UI.Each = Component.extend({ cursor.observe({ _no_indices: true, addedAt: function (doc, i, beforeId) { - var id = Meteor.idStringify(doc._id); + var id = LocalCollection._idStringify(doc._id); var data = doc; var dep = new Deps.Dependency; @@ -300,19 +300,19 @@ UI.Each = Component.extend({ comp.isAttached = true; if (beforeId) - beforeId = Meteor.idStringify(beforeId); + beforeId = LocalCollection._idStringify(beforeId); range.add(id, r, beforeId); }, removed: function (doc) { - range.remove(Meteor.idStringify(doc._id)); + range.remove(LocalCollection._idStringify(doc._id)); }, movedTo: function (doc, i, j, beforeId) { range.moveBefore( - Meteor.idStringify(doc._id), - beforeId && Meteor.idStringify(beforeId)); + LocalCollection._idStringify(doc._id), + beforeId && LocalCollection._idStringify(beforeId)); }, changed: function (newDoc) { - range.get(Meteor.idStringify(newDoc._id)).component.data.$set(newDoc); + range.get(LocalCollection._idStringify(newDoc._id)).component.data.$set(newDoc); } }); } diff --git a/packages/ui/package.js b/packages/ui/package.js index 9c7e8d4221..0e80000341 100644 --- a/packages/ui/package.js +++ b/packages/ui/package.js @@ -3,11 +3,13 @@ Package.describe({ }); Package.on_use(function (api) { + api.export(['AttributeHandler', 'UI', 'FadeyIf', 'Checkbox']); api.use('deps'); api.use('random'); api.use('ejson'); api.use('underscore'); // very slight api.use('ordered-dict'); + api.use('minimongo'); // for idStringify api.add_files(['base.js', 'attrs.js', diff --git a/packages/underscore/package.js b/packages/underscore/package.js index dc6aa9ef6b..c070df7437 100644 --- a/packages/underscore/package.js +++ b/packages/underscore/package.js @@ -2,10 +2,8 @@ Package.describe({ summary: "Collection of small helper functions: _.map, _.each, ..." }); -Package.on_use(function (api, where) { - where = where || ['client', 'server']; - - // Like all package, we have an implicit depedency on the 'meteor' +Package.on_use(function (api) { + // Like all packages, we have an implicit depedency on the 'meteor' // package, which provides such things as the *.js file handler. Use // an undocumented API to allow 'meteor' to after us even though we // depend on it. This is necessary since 'meteor' depends on us. One @@ -18,9 +16,9 @@ Package.on_use(function (api, where) { // remove unordered dependency support, though I think it's worth keeping // around for now to keep the possibility of dependency // configuration alive in the codebase. - api.use('meteor', where, {unordered: true}); + api.use('meteor', {unordered: true}); - api.exportSymbol('_', where); + api.export('_'); - api.add_files('underscore.js', where); + api.add_files(['pre.js', 'underscore.js', 'post.js']); }); diff --git a/packages/underscore/post.js b/packages/underscore/post.js new file mode 100644 index 0000000000..0dc94565ab --- /dev/null +++ b/packages/underscore/post.js @@ -0,0 +1,3 @@ +// This exports object was created in pre.js. Now copy the `_` object from it +// into the package-scope variable `_`, which will get exported. +_ = exports._; diff --git a/packages/underscore/pre.js b/packages/underscore/pre.js new file mode 100644 index 0000000000..f5cc70801b --- /dev/null +++ b/packages/underscore/pre.js @@ -0,0 +1,3 @@ +// Define an object named exports. This will cause underscore.js to put `_` as a +// field on it, instead of in the global namespace. See also post.js. +exports = {}; diff --git a/packages/underscore/underscore.js b/packages/underscore/underscore.js index a12f0d96cf..7d4ee27c7d 100644 --- a/packages/underscore/underscore.js +++ b/packages/underscore/underscore.js @@ -1,6 +1,6 @@ -// Underscore.js 1.4.4 +// Underscore.js 1.5.1 // http://underscorejs.org -// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function() { @@ -21,11 +21,12 @@ var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. - var push = ArrayProto.push, - slice = ArrayProto.slice, - concat = ArrayProto.concat, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. @@ -64,7 +65,7 @@ } // Current version. - _.VERSION = '1.4.4'; + _.VERSION = '1.5.1'; // Collection Functions // -------------------- @@ -96,7 +97,7 @@ if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { - results[results.length] = iterator.call(context, value, index, list); + results.push(iterator.call(context, value, index, list)); }); return results; }; @@ -171,7 +172,7 @@ if (obj == null) return results; if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; + if (iterator.call(context, value, index, list)) results.push(value); }); return results; }; @@ -238,7 +239,7 @@ // Convenience version of a common use case of `filter`: selecting only objects // containing specific `key:value` pairs. _.where = function(obj, attrs, first) { - if (_.isEmpty(attrs)) return first ? null : []; + if (_.isEmpty(attrs)) return first ? void 0 : []; return _[first ? 'find' : 'filter'](obj, function(value) { for (var key in attrs) { if (attrs[key] !== value[key]) return false; @@ -255,7 +256,7 @@ // Return the maximum element or (element-based computation). // Can't optimize arrays of integers longer than 65,535 elements. - // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { return Math.max.apply(Math, obj); @@ -264,7 +265,7 @@ var result = {computed : -Infinity, value: -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; - computed >= result.computed && (result = {value : value, computed : computed}); + computed > result.computed && (result = {value : value, computed : computed}); }); return result.value; }; @@ -324,7 +325,7 @@ // An internal function used for aggregate "group by" operations. var group = function(obj, value, context, behavior) { var result = {}; - var iterator = lookupIterator(value || _.identity); + var iterator = lookupIterator(value == null ? _.identity : value); each(obj, function(value, index) { var key = iterator.call(context, value, index, obj); behavior(result, key, value); @@ -363,7 +364,7 @@ return low; }; - // Safely convert anything iterable into a real, live array. + // Safely create a real, live array from anything iterable. _.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); @@ -422,8 +423,11 @@ // Internal implementation of a recursive `flatten` function. var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } each(input, function(value) { - if (_.isArray(value)) { + if (_.isArray(value) || _.isArguments(value)) { shallow ? push.apply(output, value) : flatten(value, shallow, output); } else { output.push(value); @@ -466,7 +470,7 @@ // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { - return _.uniq(concat.apply(ArrayProto, arguments)); + return _.uniq(_.flatten(arguments, true)); }; // Produce an array that contains every item shared between all the @@ -490,11 +494,10 @@ // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); + var length = _.max(_.pluck(arguments, "length").concat(0)); var results = new Array(length); for (var i = 0; i < length; i++) { - results[i] = _.pluck(args, "" + i); + results[i] = _.pluck(arguments, '' + i); } return results; }; @@ -574,14 +577,25 @@ // Function (ahem) Functions // ------------------ + // Reusable constructor function for prototype setting. + var ctor = function(){}; + // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // available. _.bind = function(func, context) { - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - var args = slice.call(arguments, 2); - return function() { - return func.apply(context, args.concat(slice.call(arguments))); + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; }; }; @@ -598,7 +612,7 @@ // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); - if (funcs.length === 0) funcs = _.functions(obj); + if (funcs.length === 0) throw new Error("bindAll must be passed function names"); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; @@ -627,17 +641,23 @@ }; // Returns a function, that, when invoked, will only be triggered at most once - // during a given window of time. - _.throttle = function(func, wait) { - var context, args, timeout, result; + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; var previous = 0; + options || (options = {}); var later = function() { - previous = new Date; + previous = options.leading === false ? 0 : new Date; timeout = null; result = func.apply(context, args); }; return function() { var now = new Date; + if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; @@ -646,7 +666,7 @@ timeout = null; previous = now; result = func.apply(context, args); - } else if (!timeout) { + } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; @@ -658,7 +678,8 @@ // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. _.debounce = function(func, wait, immediate) { - var timeout, result; + var result; + var timeout = null; return function() { var context = this, args = arguments; var later = function() { @@ -712,7 +733,6 @@ // Returns a function that will only be executed after being called N times. _.after = function(times, func) { - if (times <= 0) return func(); return function() { if (--times < 1) { return func.apply(this, arguments); @@ -728,7 +748,7 @@ _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + for (var key in obj) if (_.has(obj, key)) keys.push(key); return keys; }; @@ -800,7 +820,7 @@ each(slice.call(arguments, 1), function(source) { if (source) { for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; + if (obj[prop] === void 0) obj[prop] = source[prop]; } } }); @@ -824,7 +844,7 @@ // Internal recursive comparison function for `isEqual`. var eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; @@ -866,6 +886,13 @@ // unique nested structures. if (aStack[length] == a) return bStack[length] == b; } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } // Add the first object to the stack of traversed objects. aStack.push(a); bStack.push(b); @@ -882,13 +909,6 @@ } } } else { - // Objects with different constructors are not equivalent, but `Object`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && - _.isFunction(bCtor) && (bCtor instanceof bCtor))) { - return false; - } // Deep compare objects. for (var key in a) { if (_.has(a, key)) { @@ -1012,7 +1032,7 @@ // Run a function **n** times. _.times = function(n, iterator, context) { - var accum = Array(n); + var accum = Array(Math.max(0, n)); for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); return accum; }; @@ -1055,10 +1075,10 @@ }; }); - // If the value of the named property is a function then invoke it; - // otherwise, return it. + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. _.result = function(object, property) { - if (object == null) return null; + if (object == null) return void 0; var value = object[property]; return _.isFunction(value) ? value.call(object) : value; }; diff --git a/packages/universal-events/events-ie.js b/packages/universal-events/events-ie.js index cb8d4503ed..d785c9e697 100644 --- a/packages/universal-events/events-ie.js +++ b/packages/universal-events/events-ie.js @@ -89,7 +89,7 @@ _.extend(UniversalEventListener._impl.ie.prototype, { props = ['onpropertychange']; props.push('oncellchange'); } else if (prop === 'onsubmit') - props.push(node, 'ondatasetcomplete'); + props.push('ondatasetcomplete'); for(var i = 0; i < props.length; i++) node[props[i]] = this.curriedHandler; diff --git a/packages/universal-events/listener.js b/packages/universal-events/listener.js index 0218f8f3a5..b6863d9d72 100644 --- a/packages/universal-events/listener.js +++ b/packages/universal-events/listener.js @@ -160,8 +160,6 @@ var typeCounts = {}; // in browsers that don't require it. In other words, when the flag // is set, modern browsers will require the same API calls as IE <= // 8. This is only used for tests and is private for now. -// -// @export UniversalEventListener UniversalEventListener = function (handler, _checkIECompliance) { this.handler = handler; this.types = {}; // map from event type name to 'true' diff --git a/packages/universal-events/package.js b/packages/universal-events/package.js index 24df93ba5a..55585255e7 100644 --- a/packages/universal-events/package.js +++ b/packages/universal-events/package.js @@ -5,7 +5,7 @@ Package.describe({ Package.on_use(function (api) { api.use(['underscore'], 'client'); - + api.export('UniversalEventListener', 'client'); api.add_files(['listener.js', 'events-w3c.js', 'events-ie.js'], 'client'); diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 351762533a..e504c6b3d5 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -8,7 +8,7 @@ Npm.depends({connect: "2.7.10", useragent: "2.0.1"}); Package.on_use(function (api) { - // XXX: Refactor so as not to have to use ctl-helper api.use(['logging', 'underscore', 'routepolicy'], 'server'); + api.export(['WebApp', 'main', 'WebAppInternals'], 'server'); api.add_files('webapp_server.js', 'server'); }); diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 9180e36e78..651cd66f95 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -12,8 +12,8 @@ var optimist = Npm.require('optimist'); var useragent = Npm.require('useragent'); var send = Npm.require('send'); -// @export WebApp WebApp = {}; +WebAppInternals = {}; var findGalaxy = _.once(function () { if (!('GALAXY' in process.env)) { @@ -23,7 +23,7 @@ var findGalaxy = _.once(function () { process.exit(1); } - return Meteor.connect(process.env['GALAXY']); + return DDP.connect(process.env['GALAXY']); }); // Keepalives so that when the outer server dies unceremoniously and @@ -180,7 +180,9 @@ var runWebAppServer = function () { throw new Error("Unsupported format for client assets: " + JSON.stringify(clientJson.format)); - // XXX change all this config to something more reasonable + // XXX change all this config to something more reasonable. + // and move it out of webapp into a different package so you don't + // have weird things like mongo-livedata weak-dep'ing on webapp var deployConfig = process.env.METEOR_DEPLOY_CONFIG ? JSON.parse(process.env.METEOR_DEPLOY_CONFIG) : {}; @@ -195,6 +197,9 @@ var runWebAppServer = function () { if (process.env.PORT && !_.has(deployConfig.boot.bind, 'localPort')) { deployConfig.boot.bind.localPort = parseInt(process.env.PORT); } + if (process.env.BIND_IP && !_.has(deployConfig.boot.bind, 'localIp')) { + deployConfig.boot.bind.localIp = process.env.BIND_IP; + } copyEnvVarToDeployConfig(deployConfig, "MONGO_URL", "mongo-livedata", "url"); // webserver @@ -419,7 +424,6 @@ var runWebAppServer = function () { // Let the rest of the packages (and Meteor.startup hooks) insert connect // middlewares and update __meteor_runtime_config__, then keep going to set up // actually serving HTML. - // @export main main = function (argv) { argv = optimist(argv).boolean('keepalive').argv; @@ -436,12 +440,14 @@ var runWebAppServer = function () { // only start listening after all the startup code has run. var bind = deployConfig.boot.bind; - httpServer.listen(bind.localPort || 0, Meteor.bindEnvironment(function() { + var localPort = bind.localPort || 0; + var localIp = bind.localIp || '0.0.0.0'; + httpServer.listen(localPort, localIp, Meteor.bindEnvironment(function() { if (argv.keepalive || true) console.log("LISTENING"); // must match run.js var port = httpServer.address().port; if (bind.viaProxy && bind.viaProxy.proxyEndpoint) { - Meteor._bindToProxy(bind.viaProxy); + WebAppInternals.bindToProxy(bind.viaProxy); } else if (bind.viaProxy) { // bind via the proxy, but we'll have to find it ourselves via // ultraworld. @@ -453,7 +459,7 @@ var runWebAppServer = function () { var doBinding = function (proxyService) { if (proxyService.providers.proxy) { Log("Attempting to bind to proxy at " + proxyService.providers.proxy); - Meteor._bindToProxy(_.extend({ + WebAppInternals.bindToProxy(_.extend({ proxyEndpoint: proxyService.providers.proxy }, bind.viaProxy)); } @@ -478,8 +484,7 @@ var runWebAppServer = function () { }; }; -Meteor._bindToProxy = function (proxyConfig) { - +WebAppInternals.bindToProxy = function (proxyConfig) { var securePort = proxyConfig.securePort || 4433; var insecurePort = proxyConfig.insecurePort || 8080; var bindPathPrefix = proxyConfig.bindPathPrefix || ""; @@ -510,8 +515,8 @@ Meteor._bindToProxy = function (proxyConfig) { }; // This is run after packages are loaded (in main) so we can use - // Meteor.connect. - var proxy = Meteor.connect(proxyConfig.proxyEndpoint); + // DDP.connect. + var proxy = DDP.connect(proxyConfig.proxyEndpoint); var route = process.env.ROUTE; var host = route.split(":")[0]; var port = +route.split(":")[1]; diff --git a/packages/weibo/package.js b/packages/weibo/package.js index 4857ad3086..9ea42e1875 100644 --- a/packages/weibo/package.js +++ b/packages/weibo/package.js @@ -8,16 +8,17 @@ Package.describe({ Package.on_use(function(api) { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.use('http', ['client', 'server']); + api.use('http', ['server']); api.use('templating', 'client'); api.use('random', 'client'); api.use('service-configuration', ['client', 'server']); + api.export('Weibo'); + api.add_files( ['weibo_configure.html', 'weibo_configure.js'], 'client'); - api.add_files('weibo_common.js', ['client', 'server']); api.add_files('weibo_server.js', 'server'); api.add_files('weibo_client.js', 'client'); }); diff --git a/packages/weibo/weibo_client.js b/packages/weibo/weibo_client.js index f70eb831c6..f5ab30c787 100644 --- a/packages/weibo/weibo_client.js +++ b/packages/weibo/weibo_client.js @@ -1,3 +1,5 @@ +Weibo = {}; + // Request Weibo credentials for the user // @param options {optional} // @param credentialRequestCompleteCallback {Function} Callback function to call on diff --git a/packages/weibo/weibo_common.js b/packages/weibo/weibo_common.js deleted file mode 100644 index fab7ca0854..0000000000 --- a/packages/weibo/weibo_common.js +++ /dev/null @@ -1,2 +0,0 @@ -// @export Weibo -Weibo = {}; diff --git a/packages/weibo/weibo_server.js b/packages/weibo/weibo_server.js index 4c6437899f..c5e11cd475 100644 --- a/packages/weibo/weibo_server.js +++ b/packages/weibo/weibo_server.js @@ -1,3 +1,5 @@ +Weibo = {}; + Oauth.registerService('weibo', 2, null, function(query) { var response = getTokenResponse(query); @@ -35,7 +37,7 @@ var getTokenResponse = function (query) { var response; try { - response = Meteor.http.post( + response = HTTP.post( "https://api.weibo.com/oauth2/access_token", {params: { code: query.code, client_id: config.clientId, @@ -60,7 +62,7 @@ var getTokenResponse = function (query) { var getIdentity = function (accessToken, userId) { try { - return Meteor.http.get( + return HTTP.get( "https://api.weibo.com/2/users/show.json", {params: {access_token: accessToken, uid: userId}}).data; } catch (err) { @@ -70,4 +72,4 @@ var getIdentity = function (accessToken, userId) { Weibo.retrieveCredential = function(credentialToken) { return Oauth.retrieveCredential(credentialToken); -}; \ No newline at end of file +}; diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index 41f18ade38..1442c35362 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,5 +1,4 @@ -=> Meteor 0.6.4 released: New OAuth packages and recommended updates. See - https://github.com/meteor/meteor/blob/devel/History.md for details. +=> Meteor 0.6.4.1: security fix for upstream MongoDB BSON library. This is being downloaded in the background. Update your project - to Meteor 0.6.4 by running 'meteor update'. + to Meteor 0.6.4.1 by running 'meteor update'. diff --git a/scripts/admin/build-package-tarballs.sh b/scripts/admin/build-package-tarballs.sh index 0a912965f3..39c5b4a490 100755 --- a/scripts/admin/build-package-tarballs.sh +++ b/scripts/admin/build-package-tarballs.sh @@ -16,7 +16,7 @@ set -u # cd to top level dir cd `dirname $0` cd ../.. -TOPDIR=$(pwd) +export TOPDIR=$(pwd) OUTDIR="$TOPDIR/dist/packages" mkdir -p $OUTDIR @@ -39,11 +39,6 @@ if [ -e "$TOPDIR/.package_manifest_chunk" ]; then rm "$TOPDIR/.package_manifest_chunk" fi -# The "build version" of the current tools. A change to this should result in a -# change to all package versions; this will only be incremented when it's -# important for all packages to be rebuilt, not on every tools release. -BUILD_VERSION=$(./meteor --build-version) - FIRST_RUN=true # keep track to place commas correctly cd packages for PACKAGE in * @@ -53,7 +48,7 @@ do echo "," >> "$TOPDIR/.package_manifest_chunk" fi - PACKAGE_VERSION=$(cat <(echo "$BUILD_VERSION") <(git ls-tree HEAD $PACKAGE) | shasum | cut -f 1 -d " ") # shasum's output looks like: 'SHA -' + PACKAGE_VERSION=$(perl -pe 's/\Q$ENV{TOPDIR}\E//g; s/os\..*\.json/os.json/g' $PACKAGE/.build/buildinfo.json | shasum | cut -c 1-10) echo "$PACKAGE version $PACKAGE_VERSION" ROOTDIR="$PACKAGE-${PACKAGE_VERSION}-${PLATFORM}" TARBALL="$OUTDIR/$PACKAGE-${PACKAGE_VERSION}-${PLATFORM}.tar.gz" diff --git a/scripts/admin/build-release.sh b/scripts/admin/build-release.sh index 1bab7907b9..3f0ce4c95b 100755 --- a/scripts/admin/build-release.sh +++ b/scripts/admin/build-release.sh @@ -40,13 +40,14 @@ MANIFEST_PACKAGE_CHUNK=$(cat "$TOPDIR/.package_manifest_chunk") rm "$TOPDIR/.tools_version" rm "$TOPDIR/.package_manifest_chunk" -cat > "$OUTDIR/release.json" < "$OUTDIR/release.json-$PLATFORM" < assets on server - var bundlePath; - if (relPath.match(/^packages\//)) { - var dir = path.dirname(relPath); - var base = path.basename(relPath, ".js"); - bundlePath = path.join('static', dir, base); - } else { - bundlePath = path.join('static', 'app'); - } - self.staticDirectory = new StaticDirectory({ - sourcePath: staticSourceDirectory, - bundlePath: bundlePath - }); - }, - // Set a source map for this File. sourceMap is given as a string. setSourceMap: function (sourceMap, root) { var self = this; @@ -396,6 +365,13 @@ _.extend(File.prototype, { throw new Error("sourceMap must be given as a string"); self.sourceMap = sourceMap; self.sourceMapRoot = root; + }, + + // note: this assets object may be shared among multiple files! + setAssets: function (assets) { + var self = this; + if (!_.isEmpty(assets)) + self.assets = assets; } }); @@ -414,7 +390,7 @@ var Target = function (options) { // Package library to use for resolving package dependenices. self.library = options.library; - // Something like "browser.w3c" or "native" or "native.osx.x86_64" + // Something like "browser.w3c" or "os" or "os.osx.x86_64" self.arch = options.arch; // All of the Slices that are to go into this target, in the order @@ -425,9 +401,11 @@ var Target = function (options) { // the order given. self.js = []; - // Files and paths used by this target, in the format used by - // watch.Watcher. - self.dependencyInfo = {files: {}, directories: {}}; + // On-disk dependencies of this target. + self.watchSet = new watch.WatchSet(); + + // Map from package name to package directory of all packages used. + self.pluginProviderPackageDirs = {}; // node_modules directories that we need to copy into the target (or // otherwise make available at runtime.) A map from an absolute path @@ -437,7 +415,7 @@ var Target = function (options) { // Static assets to include in the bundle. List of File. // For browser targets, these are served over HTTP. - self.static = []; + self.asset = []; }; _.extend(Target.prototype, { @@ -452,9 +430,6 @@ _.extend(Target.prototype, { // per _determineLoadOrder // - test: packages to test (Package or 'foo'), per _determineLoadOrder // - minify: true to minify - // - assetDirs: array of asset directories to add - // object with keys 'rootDir', 'exclude', 'assetPathPrefix', - // 'setUrl', 'setTargetPath', all per addAssetDir. // - addCacheBusters: if true, make all files cacheable by adding // unique query strings to their URLs. unlikely to be of much use // on server targets. @@ -472,17 +447,15 @@ _.extend(Target.prototype, { // Minify, if requested if (options.minify) { - self.minifyJs(); + var minifiers = unipackage.load({ + library: self.library, + packages: ['minifiers'] + }).minifiers; + self.minifyJs(minifiers); if (self.minifyCss) // XXX a bit of a hack - self.minifyCss(); + self.minifyCss(minifiers); } - // Process asset directories (eg, '/public') - // XXX this should probably be part of the appDir reader - _.each(options.assetDirs || [], function (ad) { - self.addAssetDir(ad); - }); - if (options.addCacheBusters) { // Make client-side CSS and JS assets cacheable forever, by // adding a query string with a cache-busting hash. @@ -602,15 +575,44 @@ _.extend(Target.prototype, { var self = this; var isBrowser = archinfo.matches(self.arch, "browser"); - var isNative = archinfo.matches(self.arch, "native"); + var isOs = archinfo.matches(self.arch, "os"); // Copy their resources into the bundle in order _.each(self.slices, function (slice) { var isApp = ! slice.pkg.name; // Emit the resources - _.each(slice.getResources(self.arch), function (resource) { - if (_.contains(["js", "css", "static"], resource.type)) { + var resources = slice.getResources(self.arch); + + // First, find all the assets, so that we can associate them with each js + // resource (for os slices). + var sliceAssets = {}; + _.each(resources, function (resource) { + if (resource.type !== "asset") + return; + + var f = new File({data: resource.data, cacheable: false}); + + var relPath = isOs + ? path.join("assets", resource.servePath) + : stripLeadingSlash(resource.servePath); + f.setTargetPathFromRelPath(relPath); + + if (isBrowser) + f.setUrlFromRelPath(resource.servePath); + else { + sliceAssets[resource.path] = resource.data; + } + + self.asset.push(f); + }); + + // Now look for the other kinds of resources. + _.each(resources, function (resource) { + if (resource.type === "asset") + return; // already handled + + if (_.contains(["js", "css"], resource.type)) { if (resource.type === "css" && ! isBrowser) // XXX might be nice to throw an error here, but then we'd // have to make it so that packages.js ignores css files @@ -620,41 +622,36 @@ _.extend(Target.prototype, { // meteor.js? return; - var f = new File({ - data: resource.data, - cacheable: false - }); + var f = new File({data: resource.data, cacheable: false}); - var relPath; - if (resource.type === "static" && isNative) - relPath = path.join("static", resource.servePath); - else { - relPath = stripLeadingSlash(resource.servePath); - } + var relPath = stripLeadingSlash(resource.servePath); f.setTargetPathFromRelPath(relPath); if (isBrowser) { f.setUrlFromRelPath(resource.servePath); - } else if (isNative) { - if (resource.type === "js") - f.setStaticDirectory(relPath, resource.staticDirectory); } - if (isNative && resource.type === "js" && ! isApp && - slice.nodeModulesPath) { - var nmd = self.nodeModulesDirectories[slice.nodeModulesPath]; - if (! nmd) { - nmd = new NodeModulesDirectory({ - sourcePath: slice.nodeModulesPath, - // It's important that this path end with - // node_modules. Otherwise, if two modules in this package - // depend on each other, they won't be able to find each other! - preferredBundlePath: path.join('npm', slice.pkg.name, - slice.sliceName, 'node_modules') - }); - self.nodeModulesDirectories[slice.nodeModulesPath] = nmd; + if (resource.type === "js" && isOs) { + // Hack, but otherwise we'll end up putting app assets on this file. + if (resource.servePath !== "/packages/global-imports.js") + f.setAssets(sliceAssets); + + if (! isApp && slice.nodeModulesPath) { + var nmd = self.nodeModulesDirectories[slice.nodeModulesPath]; + if (! nmd) { + nmd = new NodeModulesDirectory({ + sourcePath: slice.nodeModulesPath, + // It's important that this path end with + // node_modules. Otherwise, if two modules in this package + // depend on each other, they won't be able to find each + // other! + preferredBundlePath: path.join( + 'npm', slice.pkg.name, slice.sliceName, 'node_modules') + }); + self.nodeModulesDirectories[slice.nodeModulesPath] = nmd; + } + f.nodeModulesDirectory = nmd; } - f.nodeModulesDirectory = nmd; } if (resource.type === "js" && resource.sourceMap) { @@ -675,26 +672,25 @@ _.extend(Target.prototype, { throw new Error("Unknown type " + resource.type); }); - // Depend on the source files that produced these - // resources. (Since the dependencyInfo.directories should be - // disjoint, it should be OK to merge them this way.) - _.extend(self.dependencyInfo.files, - slice.dependencyInfo.files); - _.extend(self.dependencyInfo.directories, - slice.dependencyInfo.directories); + // Depend on the source files that produced these resources. + self.watchSet.merge(slice.watchSet); + // Remember the library resolution of all packages used in these + // resources. + // XXX assumes that this merges cleanly + _.extend(self.pluginProviderPackageDirs, + slice.pkg.pluginProviderPackageDirs) }); }, // Minify the JS in this target - minifyJs: function () { + minifyJs: function (minifiers) { var self = this; var allJs = _.map(self.js, function (file) { return file.contents('utf8'); }).join('\n;\n'); - var uglify = require('uglify-js'); - allJs = uglify.minify(allJs, { + allJs = minifiers.UglifyJSMinify(allJs, { fromString: true, compress: {drop_debugger: false} }).code; @@ -712,77 +708,26 @@ _.extend(Target.prototype, { }); }, - // Return all dependency info for this target, in the format - // expected by watch.Watcher. - getDependencyInfo: function () { + // Return the WatchSet for this target's dependency info. + getWatchSet: function () { var self = this; - return self.dependencyInfo; + return self.watchSet; + }, + + getPluginProviderPackageDirs: function () { + var self = this; + return self.pluginProviderPackageDirs; }, // Return the most inclusive architecture with which this target is // compatible. For example, if we set out to build a - // 'native.linux.x86_64' version of this target (by passing that as + // 'os.linux.x86_64' version of this target (by passing that as // the 'arch' argument to the constructor), but ended up not // including anything that was specific to Linux, the return value - // would be 'native'. + // would be 'os'. mostCompatibleArch: function () { var self = this; return archinfo.leastSpecificDescription(_.pluck(self.slices, 'arch')); - }, - - // assetDir has properties rootDir, exclude, assetPathPrefix, setUrl, - // and setTargetPath. (All but rootDir are optional.) - // Add all of the files in a directory `rootDir` (and its - // subdirectories) as static assets. `rootDir` should be an absolute - // path. If provided, exclude is an - // array of filename regexps to exclude. If provided, assetPathPrefix is a - // prefix to use when computing the path for each file. - addAssetDir: function (assetDir) { - var self = this; - var rootDir = assetDir.rootDir; - var exclude = assetDir.exclude; - var assetPathPrefix = assetDir.assetPathPrefix; - var setUrl = assetDir.setUrl; - var setTargetPath = assetDir.setTargetPath; - exclude = exclude || []; - - self.dependencyInfo.directories[rootDir] = { - include: [/.?/], - exclude: exclude - }; - - var walk = function (dir, assetPathPrefix) { - _.each(fs.readdirSync(dir), function (item) { - // Skip excluded files - var matchesAnExclude = _.any(exclude, function (pattern) { - return item.match(pattern); - }); - if (matchesAnExclude) - return; - - var absPath = path.resolve(dir, item); - var assetPath = path.join(assetPathPrefix, item); - if (fs.statSync(absPath).isDirectory()) { - walk(absPath, assetPath); - return; - } - - var f = new File({ sourcePath: absPath }); - if (setUrl) - f.setUrlFromRelPath(assetPath); - // XXX why is this separate from _emitResources ? - // XXX fix up server static resources - var relPath = assetDir.useSubDirectory - ? path.join('static', 'app', assetPath) - : assetPath; - if (setTargetPath) - f.setTargetPathFromRelPath(relPath); - self.dependencyInfo.files[absPath] = f.hash(); - self.static.push(f); - }); - }; - - walk(rootDir, assetPathPrefix || ''); } }); @@ -808,16 +753,15 @@ var ClientTarget = function (options) { inherits(ClientTarget, Target); _.extend(ClientTarget.prototype, { - // Minify the JS in this target - minifyCss: function () { + // Minify the CSS in this target + minifyCss: function (minifiers) { var self = this; var allCss = _.map(self.css, function (file) { return file.contents('utf8'); }).join('\n'); - var cleanCSS = require('clean-css'); - allCss = cleanCSS.process(allCss); + allCss = minifiers.CleanCSSProcess(allCss); self.css = [new File({ data: new Buffer(allCss, 'utf8') })]; self.css[0].setUrlToHash(".css"); @@ -827,8 +771,7 @@ _.extend(ClientTarget.prototype, { var self = this; var templatePath = path.join(__dirname, "app.html.in"); - var template = fs.readFileSync(templatePath); - self.dependencyInfo.files[templatePath] = Builder.sha1(template); + var template = watch.readAndWatchFile(self.watchSet, templatePath); var f = require('handlebars').compile(template.toString()); return new Buffer(f({ @@ -851,7 +794,7 @@ _.extend(ClientTarget.prototype, { // Helper to iterate over all resources that we serve over HTTP. var eachResource = function (f) { - _.each(["js", "css", "static"], function (type) { + _.each(["js", "css", "asset"], function (type) { _.each(self[type], function (file) { f(file, type); }); @@ -988,7 +931,7 @@ _.extend(JsImage.prototype, { // XXX This is mostly duplicated from server/boot.js, as is Npm.require // below. Some way to avoid this? - var getAsset = function (staticDirectory, assetPath, encoding, callback) { + var getAsset = function (assets, assetPath, encoding, callback) { var fut; if (! callback) { if (! Fiber.current) @@ -1003,10 +946,14 @@ _.extend(JsImage.prototype, { result = new Uint8Array(result); callback(err, result); }; - var filePath = path.join(staticDirectory, assetPath); - if (filePath.indexOf("..") !== -1) - throw new Error(".. is not allowed in asset paths."); - fs.readFile(filePath, encoding, _callback); + + if (!assets || !_.has(assets, assetPath)) { + _.callback(new Error("Unknown asset: " + assetPath)); + } else { + var buffer = assets[assetPath]; + var result = encoding ? buffer.toString(encoding) : buffer; + _callback(null, result); + } if (fut) return fut.wait(); }; @@ -1047,12 +994,10 @@ _.extend(JsImage.prototype, { }, Assets: { getText: function (assetPath, callback) { - return getAsset(item.staticDirectory.sourcePath, - assetPath, "utf8", callback); + return getAsset(item.assets, assetPath, "utf8", callback); }, getBinary: function (assetPath, callback) { - return getAsset(item.staticDirectory.sourcePath, - assetPath, undefined, callback); + return getAsset(item.assets, assetPath, undefined, callback); } } }, bindings || {}); @@ -1099,6 +1044,10 @@ _.extend(JsImage.prototype, { })); }); + // If multiple load files share the same asset, only write one copy of + // each. (eg, for app assets.) + var assetFilesBySha = {}; + // JavaScript sources var load = []; _.each(self.jsToLoad, function (item) { @@ -1111,9 +1060,7 @@ _.extend(JsImage.prototype, { var loadItem = { path: loadPath, node_modules: item.nodeModulesDirectory ? - item.nodeModulesDirectory.preferredBundlePath : undefined, - staticDirectory: item.staticDirectory ? - item.staticDirectory.bundlePath : undefined + item.nodeModulesDirectory.preferredBundlePath : undefined }; if (item.sourceMap) { @@ -1126,6 +1073,33 @@ _.extend(JsImage.prototype, { loadItem.sourceMapRoot = item.sourceMapRoot; } + if (!_.isEmpty(item.assets)) { + // For package code, static assets go inside a directory inside + // assets/packages specific to this package. Application assets (e.g. those + // inside private/) go in assets/app/. + // XXX same hack as setTargetPathFromRelPath + var assetBundlePath; + if (item.targetPath.match(/^packages\//)) { + var dir = path.dirname(item.targetPath); + var base = path.basename(item.targetPath, ".js"); + assetBundlePath = path.join('assets', dir, base); + } else { + assetBundlePath = path.join('assets', 'app'); + } + + loadItem.assets = {}; + _.each(item.assets, function (data, relPath) { + var sha = Builder.sha1(data); + if (_.has(assetFilesBySha, sha)) { + loadItem.assets[relPath] = assetFilesBySha[sha]; + } else { + loadItem.assets[relPath] = assetFilesBySha[sha] = + builder.writeToGeneratedFilename( + path.join(assetBundlePath, relPath), { data: data }); + } + }); + } + load.push(loadItem); }); @@ -1137,8 +1111,7 @@ _.extend(JsImage.prototype, { _.each(nodeModulesDirectories, function (nmd) { builder.copyDirectory({ from: nmd.sourcePath, - to: nmd.preferredBundlePath, - depend: false + to: nmd.preferredBundlePath }); }); @@ -1186,11 +1159,9 @@ JsImage.readFromDisk = function (controlFilePath) { var loadItem = { targetPath: item.path, source: fs.readFileSync(path.join(dir, item.path)), - nodeModulesDirectory: nmd, - staticDirectory: new StaticDirectory({ - sourcePath: item.staticDirectory - }) + nodeModulesDirectory: nmd }; + if (item.sourceMap) { // XXX this is the same code as initFromUnipackage rejectBadPath(item.sourceMap); @@ -1198,6 +1169,14 @@ JsImage.readFromDisk = function (controlFilePath) { path.join(dir, item.sourceMap), 'utf8'); loadItem.sourceMapRoot = item.sourceMapRoot; } + + if (!_.isEmpty(item.assets)) { + loadItem.assets = {}; + _.each(item.assets, function (filename, relPath) { + loadItem.assets[relPath] = fs.readFileSync(path.join(dir, filename)); + }); + } + ret.jsToLoad.push(loadItem); }); @@ -1208,7 +1187,7 @@ var JsImageTarget = function (options) { var self = this; Target.apply(this, arguments); - if (! archinfo.matches(self.arch, "native")) + if (! archinfo.matches(self.arch, "os")) // Conceivably we could support targeting the browser as long as // no native node modules were used. No use case for that though. throw new Error("JsImageTarget targeting something unusual?"); @@ -1226,7 +1205,7 @@ _.extend(JsImageTarget.prototype, { targetPath: file.targetPath, source: file.contents().toString('utf8'), nodeModulesDirectory: file.nodeModulesDirectory, - staticDirectory: file.staticDirectory, + assets: file.assets, sourceMap: file.sourceMap, sourceMapRoot: file.sourceMapRoot }); @@ -1253,7 +1232,7 @@ var ServerTarget = function (options) { self.releaseStamp = options.releaseStamp; self.library = options.library; - if (! archinfo.matches(self.arch, "native")) + if (! archinfo.matches(self.arch, "os")) throw new Error("ServerTarget targeting something that isn't a server?"); }; @@ -1300,7 +1279,8 @@ _.extend(ServerTarget.prototype, { if (! options.omitDependencyKit) builder.reserve("node_modules", { directory: true }); - // Linked JavaScript image + // Linked JavaScript image (including static assets, assuming that there are + // any JS files at all) var imageControlFile = self.toJsImage().write(builder); // Server bootstrap @@ -1309,9 +1289,9 @@ _.extend(ServerTarget.prototype, { // Script that fetches the dev_bundle and runs the server bootstrap var archToPlatform = { - 'native.linux.x86_32': 'Linux_i686', - 'native.linux.x86_64': 'Linux_x86_64', - 'native.osx.x86_64': 'Darwin_x86_64' + 'os.linux.x86_32': 'Linux_i686', + 'os.linux.x86_64': 'Linux_x86_64', + 'os.osx.x86_64': 'Darwin_x86_64' }; var arch = archinfo.host(); var platform = archToPlatform[arch]; @@ -1346,16 +1326,10 @@ _.extend(ServerTarget.prototype, { builder.copyDirectory({ from: path.join(files.get_dev_bundle(), 'lib', 'node_modules'), to: 'node_modules', - ignore: ignoreFiles, - depend: false + ignore: ignoreFiles }); } - // Static assets - _.each(self.static, function (file) { - writeFile(file, builder); - }); - return scriptName; } }); @@ -1382,8 +1356,8 @@ var writeFile = function (file, builder) { // path of a directory that should be created to contain the generated // site archive. // -// Returns dependencyInfo (in the format expected by watch.Watcher) -// for all files and directories that ultimately went into the bundle. +// Returns a watch.WatchSet for all files and directories that ultimately went +// into the bundle. // // options: // - nodeModulesMode: "skip", "symlink", "copy" @@ -1468,10 +1442,11 @@ var writeSiteArchive = function (targets, outputPath, options) { builder.write('main.js', { data: stub }); builder.write('README', { data: new Buffer( -"This is a Meteor application bundle. It has only one dependency,\n" + -"node.js (with the 'fibers' package). To run the application:\n" + +"This is a Meteor application bundle. It has only one dependency:\n" + +"Node.js 0.8 (with the 'fibers' package). The current release of Meteor\n" + +"has been tested with Node 0.8.24. To run the application:\n" + "\n" + -" $ npm install fibers@1.0.0\n" + +" $ npm install fibers@1.0.1\n" + " $ export MONGO_URL='mongodb://user:password@host:port/databasename'\n" + " $ export ROOT_URL='http://example.com'\n" + " $ export MAIL_URL='smtp://user:password@mailhost:port/'\n" + @@ -1488,25 +1463,17 @@ var writeSiteArchive = function (targets, outputPath, options) { // Control file builder.writeJson('star.json', json); - // Merge the dependencyInfo of everything that went into the - // bundle. A naive merge like this doesn't work in general but - // should work in this case. - var fileDeps = {}, directoryDeps = {}; + // Merge the WatchSet of everything that went into the bundle. + var watchSet = new watch.WatchSet(); var dependencySources = [builder].concat(_.values(targets)); _.each(dependencySources, function (s) { - var info = s.getDependencyInfo(); - _.extend(fileDeps, info.files); - _.extend(directoryDeps, info.directories); + watchSet.merge(s.getWatchSet()); }); // We did it! builder.complete(); - return { - files: fileDeps, - directories: directoryDeps - }; - + return watchSet; } catch (e) { builder.abort(); throw e; @@ -1526,10 +1493,9 @@ var writeSiteArchive = function (targets, outputPath, options) { * * Returns an object with keys: * - errors: A buildmessage.MessageSet, or falsy if bundling succeeded. - * - dependencyInfo: Information about files and paths that were + * - watchSet: Information about files and paths that were * inputs into the bundle and that we may wish to monitor for - * changes when developing interactively. It has two keys, 'files' - * and 'directories', in the format expected by watch.Watcher. + * changes when developing interactively, as a watch.WatchSet. * * On failure ('errors' is truthy), no bundle will be output (in fact, * outputPath will have been removed if it existed.) @@ -1576,56 +1542,30 @@ exports.bundle = function (appDir, outputPath, options) { " " + options.releaseStamp : ""); var success = false; - var dependencyInfo = { files: {}, directories: {} }; + var watchSet = new watch.WatchSet(); var messages = buildmessage.capture({ title: "building the application" }, function () { var targets = {}; var controlProgram = null; - var getValidAssetDirs = function (dirNames, assetDirDefaults) { - var assetDirs = []; - assetDirDefaults = assetDirDefaults || {}; - if (appDir) { - if (files.is_app_dir(appDir)) { /* XXX what is this checking? */ - _.each(dirNames, function (dirName) { - var assetDir = path.join(appDir, dirName); - var assetDirObj = _.extend({ rootDir: assetDir }, assetDirDefaults); - if (fs.existsSync(assetDir)) - assetDirs.push(assetDirObj); - }); - } - } - return assetDirs; - }; - - var makeClientTarget = function (app, appDir, assetDirs) { + var makeClientTarget = function (app) { var client = new ClientTarget({ library: library, arch: "browser" }); - // Scan /public if the client has it - // XXX this should probably be part of the appDir reader - assetDirs = assetDirs || []; - var clientAssetDirs = getValidAssetDirs(assetDirs, { - exclude: ignoreFiles, - setUrl: true, - setTargetPath: true - }); - client.make({ packages: [app], test: options.testPackages || [], minify: options.minify, - assetDirs: clientAssetDirs, addCacheBusters: true }); return client; }; - var makeBlankClientTarget = function (app) { + var makeBlankClientTarget = function () { var client = new ClientTarget({ library: library, arch: "browser" @@ -1639,16 +1579,7 @@ exports.bundle = function (appDir, outputPath, options) { return client; }; - var makeServerTarget = function (app, clientTarget, assetDirs) { - assetDirs = assetDirs || []; - var serverAssetDirs = getValidAssetDirs(assetDirs, { - exclude: ignoreFiles, - // We need to set the target path when the asset dir is added, - // because the target path comes from the asset's path. - setTargetPath: true, - // XXX this is a hack, re-assess how the subdirs are named - useSubDirectory: true - }); + var makeServerTarget = function (app, clientTarget) { var targetOptions = { library: library, arch: archinfo.host(), @@ -1662,98 +1593,111 @@ exports.bundle = function (appDir, outputPath, options) { server.make({ packages: [app], test: options.testPackages || [], - minify: false, - assetDirs: serverAssetDirs + minify: false }); return server; }; - var includeDefaultTargets = true; - if (fs.existsSync(path.join(appDir, 'no-default-targets'))) - includeDefaultTargets = false; + // Include default targets, unless there's a no-default-targets file in the + // top level of the app. (This is a very hacky interface which will + // change. Note, eg, that .meteor/packages is confusingly ignored in this + // case.) + + var includeDefaultTargets = watch.readAndWatchFile( + watchSet, path.join(appDir, 'no-default-targets')) === null; if (includeDefaultTargets) { // Create a Package object that represents the app var app = library.getForApp(appDir, ignoreFiles); // Client - var client = makeClientTarget(app, appDir, ['public']); + var client = makeClientTarget(app); targets.client = client; // Server - var server = makeServerTarget(app, client, ['private']); + var server = makeServerTarget(app, client); targets.server = server; } // Pick up any additional targets in /programs - // Step 1: scan for targets and make a list + // Step 1: scan for targets and make a list. We will reload if you create a + // new subdir in 'programs', or create 'programs' itself. var programsDir = path.join(appDir, 'programs'); var programs = []; - if (fs.existsSync(programsDir)) { - _.each(fs.readdirSync(programsDir), function (item) { - if (item.match(/^\./)) - return; // ignore dotfiles - var itemPath = path.join(programsDir, item); + var programsSubdirs = watch.readAndWatchDirectory(watchSet, { + absPath: programsDir, + include: [/\/$/], + exclude: [/^\./] + }); - if (! fs.statSync(itemPath).isDirectory()) - return; // ignore non-directories + _.each(programsSubdirs, function (item) { + // Remove trailing slash. + item = item.substr(0, item.length - 1); - if (item in targets) { - buildmessage.error("duplicate programs named '" + item + "'"); - // Recover by ignoring this program - return; + if (_.has(targets, item)) { + buildmessage.error("duplicate programs named '" + item + "'"); + // Recover by ignoring this program + return; + } + targets[item] = true; // will be overwritten with actual target later + + // Read attributes.json, if it exists + var attrsJsonAbsPath = path.join(programsDir, item, 'attributes.json'); + var attrsJsonRelPath = path.join('programs', item, 'attributes.json'); + var attrsJsonContents = watch.readAndWatchFile( + watchSet, attrsJsonAbsPath); + + var attrsJson = {}; + if (attrsJsonContents !== null) { + try { + attrsJson = JSON.parse(attrsJsonContents); + } catch (e) { + if (! (e instanceof SyntaxError)) + throw e; + buildmessage.error(e.message, { file: attrsJsonRelPath }); + // recover by ignoring attributes.json } + } - // Read attributes.json, if it exists - var attrsJsonPath = path.join(itemPath, 'attributes.json'); - var attrsJsonRelPath = path.join('programs', item, 'attributes.json'); - var attrsJson = {}; - if (fs.existsSync(attrsJsonPath)) { - try { - attrsJson = JSON.parse(fs.readFileSync(attrsJsonPath)); - } catch (e) { - if (! (e instanceof SyntaxError)) - throw e; - buildmessage.error(e.message, { file: attrsJsonRelPath }); - // recover by ignoring attributes.json - } - } - - var isControlProgram = !! attrsJson.isControlProgram; - if (isControlProgram) { - if (controlProgram !== null) { - buildmessage.error( + var isControlProgram = !! attrsJson.isControlProgram; + if (isControlProgram) { + if (controlProgram !== null) { + buildmessage.error( "there can be only one control program ('" + controlProgram + - "' is also marked as the control program)", - { file: attrsJsonRelPath }); - // recover by ignoring that it wants to be the control - // program - } else { - controlProgram = item; - } + "' is also marked as the control program)", + { file: attrsJsonRelPath }); + // recover by ignoring that it wants to be the control + // program + } else { + controlProgram = item; } + } - // Add to list - programs.push({ - type: attrsJson.type || "server", - name: item, - path: itemPath, - client: attrsJson.client, - attrsJsonRelPath: attrsJsonRelPath - }); + // Add to list + programs.push({ + type: attrsJson.type || "server", + name: item, + path: path.join(programsDir, item), + client: attrsJson.client, + attrsJsonRelPath: attrsJsonRelPath }); - } + }); if (! controlProgram) { - var target = makeServerTarget("ctl"); - targets["ctl"] = target; - controlProgram = "ctl"; + if (_.has(targets, 'ctl')) { + buildmessage.error( + "A program named ctl exists but no program has isControlProgram set"); + // recover by not making a control program + } else { + var target = makeServerTarget("ctl"); + targets["ctl"] = target; + controlProgram = "ctl"; + } } - // Step 2: sort the list so that programs are built first (because - // when we build the servers we need to be able to reference the - // clients) + // Step 2: sort the list so that client programs are built first (because + // when we build the servers we need to be able to reference the clients) programs.sort(function (a, b) { a = (a.type === "client") ? 0 : 1; b = (b.type === "client") ? 0 : 1; @@ -1778,6 +1722,8 @@ exports.bundle = function (appDir, outputPath, options) { if (! blankClientTarget) { clientTarget = blankClientTarget = targets._blank = makeBlankClientTarget(); + } else { + clientTarget = blankClientTarget; } } else { clientTarget = targets[p.client]; @@ -1795,10 +1741,7 @@ exports.bundle = function (appDir, outputPath, options) { target = makeServerTarget(p.name, clientTarget); break; case "client": - // We pass null for appDir because we are a - // package.js-driven directory and don't want to scan a - // /public directory for assets. - target = makeClientTarget(p.name, null); + target = makeClientTarget(p.name); break; default: buildmessage.error( @@ -1816,12 +1759,16 @@ exports.bundle = function (appDir, outputPath, options) { if (! (controlProgram in targets)) controlProgram = undefined; + // Make sure notice when somebody adds a package to the app packages dir + // that may override a warehouse package. + library.watchLocalPackageDirs(watchSet); + // Write to disk - dependencyInfo = writeSiteArchive(targets, outputPath, { + watchSet.merge(writeSiteArchive(targets, outputPath, { nodeModulesMode: options.nodeModulesMode, builtBy: builtBy, controlProgram: controlProgram - }); + })); success = true; }); @@ -1831,8 +1778,8 @@ exports.bundle = function (appDir, outputPath, options) { return { errors: success ? false : messages, - dependencyInfo: dependencyInfo - } ; + watchSet: watchSet + }; }; // Make a JsImage object (a complete, linked, ready-to-go JavaScript @@ -1842,7 +1789,7 @@ exports.bundle = function (appDir, outputPath, options) { // // Returns an object with keys: // - image: The created JsImage object. -// - dependencyInfo: Source file dependency info (see bundle().) +// - watchSet: Source file WatchSet (see bundle().) // // XXX return an 'errors' key for symmetry with bundle(), rather than // letting exceptions escape? @@ -1896,7 +1843,8 @@ exports.buildJsImage = function (options) { return { image: target.toJsImage(), - dependencyInfo: target.getDependencyInfo() + watchSet: target.getWatchSet(), + pluginProviderPackageDirs: target.getPluginProviderPackageDirs() }; }; diff --git a/tools/deploy-galaxy.js b/tools/deploy-galaxy.js index 6ff3b0ccb0..e147361712 100644 --- a/tools/deploy-galaxy.js +++ b/tools/deploy-galaxy.js @@ -9,36 +9,39 @@ var request = require('request'); var _ = require('underscore'); // a bit of a hack -var _meteor; -var getMeteor = function (context) { - if (! _meteor) { - _meteor = unipackage.load({ - library: context.library, - packages: [ 'livedata', 'mongo-livedata' ], - release: context.releaseVersion - }).meteor.Meteor; +var getPackage = _.once(function (context) { + return unipackage.load({ + library: context.library, + packages: [ 'meteor', 'livedata', 'mongo-livedata' ], + release: context.releaseVersion + }); +}); + +var getGalaxy = _.once(function (context) { + var Package = getPackage(context); + if (!context.galaxy) { + process.stderr.write("Could not find a deploy endpoint. " + + "You can set the GALAXY environment variable, " + + "or configure your site's DNS to resolve to " + + "your Galaxy's proxy.\n"); + process.exit(1); } - return _meteor; -}; - -var _galaxy; -var getGalaxy = function (context) { - if (! _galaxy) { - var Meteor = getMeteor(context); - if (!context.galaxy) { - process.stderr.write("Could not find a deploy endpoint. " + - "You can set the GALAXY environment variable, " + - "or configure your site's DNS to resolve to " + - "your Galaxy's proxy.\n"); + var galaxy = Package.livedata.DDP.connect(context.galaxy.url); + var timeout = Package.meteor.Meteor.setTimeout(function () { + if (galaxy.status().status !== "connected") { + process.stderr.write("Could not connect to galaxy " + context.galaxy.url + + ": " + galaxy.status().status + '\n'); process.exit(1); } - - _galaxy = Meteor.connect(context.galaxy.url); - } - - return _galaxy; -}; + }, 10*1000); + var close = galaxy.close; + galaxy.close = function (/*arguments*/) { + Package.meteor.Meteor.clearTimeout(timeout); + close.apply(galaxy, arguments); + }; + return galaxy; +}); var exitWithError = function (error, messages) { @@ -87,7 +90,14 @@ exports.discoverGalaxy = function (app) { // At some point we may want to send a version in the request so that galaxy // can respond differently to different versions of meteor. - request({ url: url, json: true }, function (err, resp, body) { + request({ + url: url, + json: true, + strictSSL: true, + // We don't want to be confused by, eg, a non-Galaxy-hosted site which + // redirects to a Galaxy-hosted site. + followRedirect: false + }, function (err, resp, body) { if (! err && resp.statusCode === 200 && body && @@ -102,8 +112,11 @@ exports.discoverGalaxy = function (app) { return fut.wait(); }; -exports.deleteApp = function (context) { - throw new Error("Not implemented"); +exports.deleteApp = function (app, context) { + var galaxy = getGalaxy(context); + galaxy.call("destroyApp", app); + galaxy.close(); + process.stdout.write("Deleted.\n"); }; // options: @@ -118,7 +131,7 @@ exports.deleteApp = function (context) { // in --star mode. exports.deploy = function (options) { var galaxy = getGalaxy(options.context); - var Meteor = getMeteor(options.context); + var Package = getPackage(options.context); var tmpdir = files.mkdtemp('deploy'); var buildDir = path.join(tmpdir, 'build'); @@ -157,10 +170,14 @@ exports.deploy = function (options) { var appConfig = { METEOR_SETTINGS: options.settings }; + + if (options.admin) + appConfig.admin = true; + try { galaxy.call('createApp', options.app, appConfig); } catch (e) { - if (e instanceof Meteor.Error && e.error === 'already-exists') { + if (e instanceof Package.meteor.Meteor.Error && e.error === 'already-exists') { // Cool, it already exists. No problem. Just set the settings if they were // passed. We explicitly check for undefined because we want to allow you // to unset settings by passing an empty file. @@ -238,7 +255,8 @@ exports.logs = function (options) { } var lastLogId = null; - var logReader = getMeteor(options.context).connect(logReaderURL); + var logReader = + getPackage(options.context).livedata.DDP.connect(logReaderURL); var Log = unipackage.load({ library: options.context.library, packages: [ 'logging' ], diff --git a/tools/deploy.js b/tools/deploy.js index cb86f9219a..90e00d0811 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -189,7 +189,13 @@ var temporaryMongoUrl = function (url) { if (password) opts.password = password; meteor_rpc('mongo', 'GET', - parsed_url.hostname, opts, urlFut.resolver()); + parsed_url.hostname, opts, function (err, body) { + if (err) { + process.stderr.write(body + "\n"); + process.exit(1); + } + urlFut.return(body); + }); var mongoUrl = urlFut.wait(); return mongoUrl; }; diff --git a/tools/files.js b/tools/files.js index a815fc0b20..935051ad60 100644 --- a/tools/files.js +++ b/tools/files.js @@ -39,6 +39,20 @@ var files = exports; _.extend(exports, { // A sort comparator to order files into load order. sort: function (a, b) { + // XXX HUGE HACK -- + // push html (template) files ahead of everything else. this is + // important because the user wants to be able to say + // Template.foo.events = { ... } + // + // maybe all of the templates should go in one file? packages should + // probably have a way to request this treatment (load order dependency + // tags?) .. who knows. + var ishtml_a = path.extname(a) === '.html'; + var ishtml_b = path.extname(b) === '.html'; + if (ishtml_a !== ishtml_b) { + return (ishtml_a ? -1 : 1); + } + // main.* loaded last var ismain_a = (path.basename(a).indexOf('main.') === 0); var ismain_b = (path.basename(b).indexOf('main.') === 0); @@ -47,8 +61,10 @@ _.extend(exports, { } // /lib/ loaded first - var islib_a = (a.indexOf(path.sep + 'lib' + path.sep) !== -1); - var islib_b = (b.indexOf(path.sep + 'lib' + path.sep) !== -1); + var islib_a = (a.indexOf(path.sep + 'lib' + path.sep) !== -1 || + a.indexOf('lib' + path.sep) === 0); + var islib_b = (b.indexOf(path.sep + 'lib' + path.sep) !== -1 || + b.indexOf('lib' + path.sep) === 0); if (islib_a !== islib_b) { return (islib_a ? -1 : 1); } @@ -64,84 +80,6 @@ _.extend(exports, { return (a < b ? -1 : 1); }, - // Returns true if this is a file we should maybe care about (stat it, - // descend if it is a directory, etc). - pre_filter: function (filename) { - if (!filename) { return false; } - // no . files - var base = path.basename(filename); - if (base && base[0] === '.') { return false; } - - // XXX - // first, we only want to exclude APP_ROOT/public, not some deeper public - // second, we don't really like this at all - // third, we don't update the app now if anything here changes - if (base === 'public' || base === 'private') { return false; } - - return true; - }, - - // Returns true if this is a file we should monitor. Iterate over - // all the interesting files, applying 'func' to each file - // path. 'extensions' is an array of extensions to include, without - // leading dots (eg ['html', 'js']) - file_list_async: function (filepath, extensions, func) { - if (!files.pre_filter(filepath)) { return; } - fs.stat(filepath, function(err, stats) { - if (err) { - // XXX! - return; - } - - if (stats.isDirectory()) { - fs.readdir(filepath, function(err, fileNames) { - if(err) { - // XXX! - return; - } - - _.each(fileNames, function (fileName) { - files.file_list_async(path.join(filepath, fileName), - extensions, func); - }); - }); - } else if (files.findExtension(extensions, filepath)) { - func(filepath); - } - }); - }, - - file_list_sync: function (filepath, extensions) { - var ret = []; - if (!files.pre_filter(filepath)) { return ret; } - var stats = fs.statSync(filepath); - if (stats.isDirectory()) { - var fileNames = fs.readdirSync(filepath); - _.each(fileNames, function (fileName) { - ret = ret.concat(files.file_list_sync( - path.join(filepath, fileName), extensions)); - }); - } else if (files.findExtension(extensions, filepath)) { - ret.push(filepath); - } - - return ret; - }, - - // given a list of extensions (no leading dots) and a path, return - // the file extension provided in the list. If it doesn't find it, - // return null. - findExtension: function (extensions, filepath) { - var len = filepath.length; - for (var i = 0; i < extensions.length; ++i) { - var ext = "." + extensions[i]; - if (filepath.indexOf(ext, len - ext.length) !== -1){ - return ext; - } - } - return null; - }, - // given a path, returns true if it is a meteor application (has a // .meteor directory with a 'packages' file). false otherwise. is_app_dir: function (filepath) { @@ -161,14 +99,6 @@ _.extend(exports, { } }, - // given a path, returns true if it is a meteor package (is a - // directory with a 'packages.js' file). false otherwise. - // - // Note that a directory can be both a package _and_ an application. - is_package_dir: function (filepath) { - return fs.existsSync(path.join(filepath, 'package.js')); - }, - // given a predicate function and a starting path, traverse upwards // from the path until we find a path that satisfies the predicate. // diff --git a/tools/library.js b/tools/library.js index 040cbf645e..fc9c039c55 100644 --- a/tools/library.js +++ b/tools/library.js @@ -1,6 +1,7 @@ var path = require('path'); var _ = require('underscore'); var files = require('./files.js'); +var watch = require('./watch.js'); var packages = require('./packages.js'); var warehouse = require('./warehouse.js'); var bundler = require('./bundler.js'); @@ -34,8 +35,13 @@ var Library = function (options) { return stats.isDirectory(); }); - self.loadedPackages = {}; self.overrides = {}; // package name to package directory + + // both map from package name to: + // - pkg: cached Package object + // - packageDir: directory from which it was loaded + self.softReloadCache = {}; + self.loadedPackages = {}; }; _.extend(Library.prototype, { @@ -46,7 +52,7 @@ _.extend(Library.prototype, { // two overrides for the same packageName. override: function (packageName, packageDir) { var self = this; - if (packageName in self.overrides) + if (_.has(self.overrides, packageName)) throw new Error("Duplicate override for package '" + packageName + "'"); self.overrides[packageName] = path.resolve(packageDir); }, @@ -54,18 +60,76 @@ _.extend(Library.prototype, { // Undo an override previously set up with override(). removeOverride: function (packageName) { var self = this; - if (!(packageName in self.overrides)) + if (!_.has(self.overrides, packageName)) throw new Error("No override present for package '" + packageName + "'"); delete self.loadedPackages[packageName]; delete self.overrides[packageName]; + delete self.softReloadCache[packageName]; }, - // Force reload of all packages. See description at get(). - refresh: function () { + // Force reload of changed packages. See description at get(). + // + // If soft is false, the default, the cache is totally flushed and + // all packages are reloaded unconditionally. + // + // If soft is true, then built packages without dependency info (such as those + // from the warehouse) aren't reloaded (there's no way to rebuild them, after + // all), and if we loaded a built package with dependency info, we won't + // reload it if the dependency info says that its source files are still up to + // date. The ideas is that assuming the user is "following the rules", this + // will correctly reload any changed packages while in most cases avoiding + // nearly all reloading. + refresh: function (soft) { var self = this; + soft = soft || false; + + self.softReloadCache = soft ? self.loadedPackages : {}; self.loadedPackages = {}; }, + // Given a package name as a string, returns the absolute path to the package + // directory (which is the *source* tree in the source-with-built-unipackage + // case, not the .build directory), or null if not found. + // + // Does NOT load the package or make any recursive calls, so can safely be + // called from Package initialization code. Intended primarily for comparison + // to the packageDirForBuildInfo field on a Package object; also used + // internally to implement 'get'. + findPackageDirectory: function (name) { + var self = this; + + // Packages cached from previous calls + if (_.has(self.loadedPackages, name)) { + return self.loadedPackages[name].packageDir; + } + + // If there's an override for this package, use that without + // looking at any other options. + if (_.has(self.overrides, name)) + return self.overrides[name]; + + 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'))) + return packageDir; + } + + // Try the Meteor distribution, if we have one. + var version = self.releaseManifest && self.releaseManifest.packages[name]; + if (version) { + packageDir = path.join(warehouse.getWarehouseDir(), + 'packages', name, version); + if (! fs.existsSync(packageDir)) + throw new Error("Package missing from warehouse: " + name + + " version " + version); + return packageDir; + } + + // Nope! + return null; + }, + // Given a package name as a string, retrieve a Package object. If // throwOnError is true, the default, throw an error if the package // can't be found. (If false is passed for throwOnError, then return @@ -83,42 +147,17 @@ _.extend(Library.prototype, { // refresh(). get: function (name, throwOnError) { var self = this; - var packageDir; - var fromWarehouse = false; // Passed a Package? if (name instanceof packages.Package) return name; // Packages cached from previous calls - if (name in self.loadedPackages) - return self.loadedPackages[name]; - - // If there's an override for this package, use that without - // looking at any other options. - if (name in self.overrides) - packageDir = self.overrides[name]; - - // Try localPackageDirs - if (! packageDir) { - for (var i = 0; i < self.localPackageDirs.length; ++i) { - var packageDir = path.join(self.localPackageDirs[i], name); - if (fs.existsSync(path.join(packageDir, 'package.js'))) - break; - packageDir = null; - } + if (_.has(self.loadedPackages, name)) { + return self.loadedPackages[name].pkg; } - // Try the Meteor distribution, if we have one. - var version = self.releaseManifest && self.releaseManifest.packages[name]; - if (! packageDir && version) { - var packageDir = path.join(warehouse.getWarehouseDir(), - 'packages', name, version); - if (! fs.existsSync(packageDir)) - throw new Error("Package missing from warehouse: " + name + - " version " + version); - fromWarehouse = true; - } + var packageDir = self.findPackageDirectory(name); if (! packageDir) { if (throwOnError === false) @@ -130,35 +169,41 @@ _.extend(Library.prototype, { return pkg; } + // See if we can reuse a package that we have cached from before + // the last soft refresh. + if (_.has(self.softReloadCache, name)) { + var entry = self.softReloadCache[name]; + + // Either we will decide that the cache is invalid, or we will "upgrade" + // this entry into loadedPackages. Either way, it's not needed in + // softReloadCache any more. + delete self.softReloadCache[name]; + + if (entry.packageDir === packageDir && entry.pkg.checkUpToDate()) { + // Cache hit + self.loadedPackages[name] = entry; + return entry.pkg; + } + } + // Load package from disk - var pkg = new packages.Package(self); + var pkg = new packages.Package(self, packageDir); if (fs.existsSync(path.join(packageDir, 'unipackage.json'))) { // It's an already-built package pkg.initFromUnipackage(name, packageDir); - self.loadedPackages[name] = pkg; + self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir}; } else { - // It's a source tree + // It's a source tree. Does it have a built unipackage inside it? var buildDir = path.join(packageDir, '.build'); if (fs.existsSync(buildDir) && pkg.initFromUnipackage(name, buildDir, - { onlyIfUpToDate: ! fromWarehouse, + { onlyIfUpToDate: true, buildOfPath: packageDir })) { // We already had a build and it was up to date. - self.loadedPackages[name] = pkg; + self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir}; } else { - // Either we didn't have a build, or it was out of date (and - // as a transitional matter until the only thing the warehouse - // contains is unipackages, we don't do an up-to-date check on - // warehouse packages, for efficiency.) Build the package. - // - // As a temporary, transitional optimization, assume that any - // source trees in the warehouse have already had their npm - // dependencies fetched. The 0.6.0 installer does this - // (rather, it downloads packages that already have their npm - // dependencies inside of them), and during the transitional - // period where we still have source trees in the warehouse - // AND the unipackage format can't handle packages with - // extensions, this will reduce startup time. + // Either we didn't have a build, or it was out of date. Build the + // package. buildmessage.enterJob({ title: "building package `" + name + "`", rootPath: packageDir @@ -172,9 +217,8 @@ _.extend(Library.prototype, { // forever. (build() needs the dependencies because it needs // to look at the handlers registered by any plugins in the // packages that we use.) - pkg.initFromPackageDir(name, packageDir, - { skipNpmUpdate: fromWarehouse }); - self.loadedPackages[name] = pkg; + pkg.initFromPackageDir(name, packageDir); + self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir}; pkg.build(); if (! buildmessage.jobHasMessages() && // ensure no errors! @@ -226,6 +270,24 @@ _.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. + watchLocalPackageDirs: function (watchSet) { + var self = this; + _.each(self.localPackageDirs, function (packageDir) { + var packages = watch.readAndWatchDirectory(watchSet, { + absPath: packageDir, + include: [/\/$/] + }); + _.each(packages, function (p) { + watch.readAndWatchFile(watchSet, + path.join(packageDir, p, 'package.js')); + // XXX unipackage.json too? + }); + }); + }, + // Get all packages available. Returns a map from the package name // to a Package object. // @@ -299,11 +361,8 @@ _.extend(Library.prototype, { }); }); - _.each(self.releaseManifest || {}, function (name, version) { - var packageDir = path.join(warehouse.getWarehouseDir(), - 'packages', name, version); - all[packageDir] = name; - }); + // We *DON'T* look in the warehouse here, because warehouse packages are + // prebuilt. // Delete any that are source packages with builds. var count = 0; @@ -352,9 +411,10 @@ _.extend(exports, { formatList: function (pkgs) { var longest = ''; _.each(pkgs, function (pkg) { - if (pkg.name.length > longest.length) + if (!pkg.metadata.internal && pkg.name.length > longest.length) longest = pkg.name; }); + var pad = longest.replace(/./g, ' '); // it'd be nice to read the actual terminal width, but I tried // several methods and none of them work (COLUMNS isn't set in diff --git a/tools/linker.js b/tools/linker.js index b6eaf7eab7..8392d28877 100644 --- a/tools/linker.js +++ b/tools/linker.js @@ -14,8 +14,8 @@ var packageDot = function (name) { // Module /////////////////////////////////////////////////////////////////////////////// -// options include name, imports, forceExport, useGlobalNamespace, -// combinedServePath, importStubServePath, and noExports, all of which have the +// options include name, imports, exports, useGlobalNamespace, +// combinedServePath, and importStubServePath, all of which have the // same meaning as they do when passed to import(). var Module = function (options) { var self = this; @@ -27,11 +27,10 @@ var Module = function (options) { self.files = []; // options - self.forceExport = options.forceExport || []; + self.declaredExports = options.declaredExports; self.useGlobalNamespace = options.useGlobalNamespace; self.combinedServePath = options.combinedServePath; self.importStubServePath = options.importStubServePath; - self.noExports = !!options.noExports; self.jsAnalyze = options.jsAnalyze; }; @@ -60,52 +59,42 @@ _.extend(Module.prototype, { return _.max(maxInFile); }, - runLinkerFileTransforms: function (exports) { - var self = this; - _.each(self.files, function (f) { - f.runLinkerFileTransform(exports); - }); - }, - // Figure out which vars need to be specifically put in the module // scope. - // - // XXX We used to subtract 'import roots' out of this (defined as - // the first part of each imported symbol) but two-phase link - // complicates this. We should really go back to doing it, though, - // because otherwise the output looks ugly and it's harder to skim - // and see what your globals are. Probably this means we need to - // move the emission of the Package-scope Variables section (but not - // the actual static analysis) to the final phase. - computeModuleScopeVars: function (exports) { + computeAssignedVariables: function () { var self = this; if (!self.jsAnalyze) { // We don't have access to static analysis, probably because we *are* the - // js-analyze package. Let's do a stupid heuristic: any exports that have - // no dots are module scope vars. (This works for - // js-analyze.JSAnalyze...) - return _.filter(exports, function (e) { - return e.indexOf('.') === -1; - }); + // js-analyze package. Let's do a stupid heuristic: the exports are + // the only module scope vars. (This works for js-analyze.JSAnalyze...) + return self.declaredExports; } // Find all global references in any files - var globalReferences = []; + var assignedVariables = []; _.each(self.files, function (file) { - globalReferences = globalReferences.concat(file.computeGlobalReferences()); + assignedVariables = assignedVariables.concat( + file.computeAssignedVariables()); }); - globalReferences = _.uniq(globalReferences); + assignedVariables = _.uniq(assignedVariables); - return _.isEmpty(globalReferences) ? undefined : globalReferences; + return _.isEmpty(assignedVariables) ? undefined : assignedVariables; }, // Output is a list of objects with keys 'source', 'servePath', 'sourceMap', // 'sourcePath' - getPrelinkedFiles: function (moduleExports) { + getPrelinkedFiles: function () { var self = this; - if (! self.files.length) + // If there are no files *and* we are a no-exports-at-all slice (eg a test + // slice), then generate no prelink output. + // + // If there are no files, but we are a use slice (and thus + // self.declaredExports is an actual, albeit potentially empty, list), we + // DON'T want to take this path: we want to return an empty prelink file, so + // that at link time we end up at least setting `Package.foo = {}`. + if (_.isEmpty(self.files) && !self.declaredExports) return []; // If we don't want to create a separate scope for this module, @@ -113,8 +102,7 @@ _.extend(Module.prototype, { // preserving the line numbers. if (self.useGlobalNamespace) { return _.map(self.files, function (file) { - var node = file.getPrelinkedOutput({ preserveLineNumbers: true, - exports: moduleExports }); + var node = file.getPrelinkedOutput({ preserveLineNumbers: true }); var results = node.toStringWithSourceMap({ file: file.servePath }); // results has 'code' and 'map' attributes @@ -146,8 +134,7 @@ _.extend(Module.prototype, { _.each(self.files, function (file) { if (!_.isEmpty(chunks)) chunks.push("\n\n\n\n\n\n"); - chunks.push(file.getPrelinkedOutput({ sourceWidth: sourceWidth, - exports: moduleExports })); + chunks.push(file.getPrelinkedOutput({ sourceWidth: sourceWidth })); }); var node = new sourcemap.SourceNode(null, null, null, chunks); @@ -160,30 +147,17 @@ _.extend(Module.prototype, { servePath: self.combinedServePath, sourceMap: results.map.toString() }]; - }, - - // Return our exports as a list of string - getExports: function () { - var self = this; - - if (self.noExports) - return []; - - var exports = {}; - _.each(self.files, function (file) { - _.extend(exports, file.exports); - }); - - return _.union(_.keys(exports), self.forceExport); } }); // Given 'symbolMap' like {Foo: 's1', 'Bar.Baz': 's2', 'Bar.Quux.A': 's3', 'Bar.Quux.B': 's4'} // return something like // {Foo: 's1', Bar: {Baz: 's2', Quux: {A: 's3', B: 's4'}}} -var buildSymbolTree = function (symbolMap, f) { - // XXX XXX detect and report conflicts, like one file exporting - // Foo and another file exporting Foo.Bar +// +// If the value of a symbol in symbolMap is set null, then we just +// ensure that its parents exist. For example, {'A.B.C': null} means +// to make sure that symbol tree contains at least {A: {B: {}}}. +var buildSymbolTree = function (symbolMap) { var ret = {}; _.each(symbolMap, function (value, symbol) { @@ -196,7 +170,9 @@ var buildSymbolTree = function (symbolMap, f) { walk[part] = {}; walk = walk[part]; }); - walk[lastPart] = value; + + if (value) + walk[lastPart] = value; }); return ret; @@ -210,6 +186,9 @@ var writeSymbolTree = function (symbolTree, indent) { if (typeof node === "string") { return node; } + if (_.keys(node).length === 0) { + return '{}'; + } var spacing = new Array(indent + 1).join(' '); // XXX prettyprint! return "{\n" + @@ -239,13 +218,6 @@ var File = function (inputFile, module) { // package or app.) Used for source maps, error messages.. self.sourcePath = inputFile.sourcePath; - // A function which transforms the source code once all exports are - // known. (eg, for CoffeeScript.) - self.linkerFileTransform = - inputFile.linkerFileTransform || function (sourceWithMap, exports) { - return sourceWithMap; - }; - // If true, don't wrap this individual file in a closure. self.bare = !!inputFile.bare; @@ -254,16 +226,6 @@ var File = function (inputFile, module) { // The Module containing this file. self.module = module; - - // symbols mentioned in @export, @require, @provide, or @weak - // directives. each is a map from the symbol (given as a string) to - // true. (only @export is actually implemented) - self.exports = {}; - self.requires = {}; - self.provides = {}; - self.weaks = {}; - - self._scanForComments(); }; _.extend(File.prototype, { @@ -271,7 +233,7 @@ _.extend(File.prototype, { // example: if the code references 'Foo.bar.baz' and 'Quux', and // neither are declared in a scope enclosing the point where they're // referenced, then globalReferences would include ["Foo", "Quux"]. - computeGlobalReferences: function () { + computeAssignedVariables: function () { var self = this; var jsAnalyze = self.module.jsAnalyze; @@ -325,21 +287,11 @@ _.extend(File.prototype, { return require('path').basename(self.sourcePath); }, - runLinkerFileTransform: function (exports) { - var self = this; - var sourceAndMap = self.linkerFileTransform( - {source: self.source, sourceMap: self.sourceMap}, - exports); - self.source = sourceAndMap.source; - self.sourceMap = sourceAndMap.sourceMap; - }, - // Options: // - preserveLineNumbers: if true, decorate minimally so that line // numbers don't change between input and output. In this case, // sourceWidth is ignored. // - sourceWidth: width in columns to use for the source code - // - exports: the module's exports // // Returns a SourceNode. getPrelinkedOutput: function (options) { @@ -455,65 +407,6 @@ _.extend(File.prototype, { node.setSourceContent(self._pathForSourceMap(), self.source); return node; - }, - - // If "line" contains nothing but a comment (of either syntax), return the - // body of the comment with leading and trailing spaces trimmed (possibly the - // empty string). Otherwise return null. (We need to support both comment - // syntaxes because the CoffeeScript compiler only emits /**/ comments.) - _getSingleLineCommentBody: function (line) { - var self = this; - var match = /^\s*\/\/(.+)$/.exec(line); - if (match) { - return match[1].trim(); - } - match = /^\s*\/\*(.+)\*\/\s*$/.exec(line); - // Make sure we don't get tricked by lines like - // /* Comment */ var myRegexp = /x*/ - if (match && match[1].indexOf('*/') === -1) - return match[1].trim(); - return null; - }, - - // Scan for @export, etc. - _scanForComments: function () { - var self = this; - var lines = self.source.split("\n"); - - _.each(lines, function (line) { - var commentBody = self._getSingleLineCommentBody(line); - if (!commentBody) - return; - - // XXX overly permissive. should detect errors - var match = /^@(export|require|provide|weak)(\s+.*)$/.exec(commentBody); - if (match) { - var what = match[1]; - var symbols = _.map(match[2].split(/,/), function (s) { - return s.trim(); - }); - - var badSymbols = _.reject(symbols, function (s) { - // XXX should be unicode-friendlier - return s.match(/^([_$a-zA-Z][_$a-zA-Z0-9]*)(\.[_$a-zA-Z][_$a-zA-Z0-9]*)*$/); - }); - if (!_.isEmpty(badSymbols)) { - buildmessage.error("bad symbols for @" + what + ": " + - JSON.stringify(badSymbols), - { file: self.sourcePath }); - // recover by ignoring - } else if (self.module.noExports && what === "export") { - buildmessage.error("@export not allowed in this slice", - { file: self.sourcePath }); - // recover by ignoring - } else { - _.each(symbols, function (s) { - if (s.length) - self[what + "s"][s] = true; - }); - } - } - }); } }); @@ -568,13 +461,9 @@ var bannerPadding = function (bannerWidth) { // look good) // - sourcePath: path to use in error messages // - sourceMap: an optional source map (as string) for the input file -// - linkerFileTransform: if given, this function will be called -// when the module is being linked with the source of the file -// and an array of the exports of the module; the file's source will -// be replaced by what the function returns. // -// forceExport: an array of symbols (as dotted strings) to force the -// module to export, even if it wouldn't otherwise +// declaredExports: an array of symbols that the module exports. null if our +// slice isn't allowed to have exports. Symbols are {name,testOnly} pairs. // // useGlobalNamespace: make the top level namespace be the same as the // global namespace, so that symbols are accessible from the @@ -588,9 +477,6 @@ var bannerPadding = function (bannerWidth) { // containing import setup code for the global environment. this is // the servePath to use for it. // -// noExports: if true, the module does not export anything (even an empty -// Package.foo object). eg, for test slices. -// // jsAnalyze: if possible, the JSAnalyze object from the js-analyze // package. (This is not possible if we are currently linking the main slice of // the js-analyze package!) @@ -599,20 +485,15 @@ var bannerPadding = function (bannerWidth) { // - files: is an array of output files in the same format as inputFiles // - EXCEPT THAT, for now, sourcePath is omitted and is replaced with // sourceMap (a string) (XXX) -// - exports: the exports, as a list of string ('Foo', 'Thing.Stuff', etc) +// - assignedPackageVariables: an array of variables assigned to without +// being declared var prelink = function (options) { - if (options.noExports && options.forceExport && - ! _.isEmpty(options.forceExport)) { - throw new Error("Can't force exports if there are no exports!"); - }; - var module = new Module({ name: options.name, - forceExport: options.forceExport, + declaredExports: options.declaredExports, useGlobalNamespace: options.useGlobalNamespace, importStubServePath: options.importStubServePath, combinedServePath: options.combinedServePath, - noExports: !!options.noExports, jsAnalyze: options.jsAnalyze }); @@ -620,23 +501,15 @@ var prelink = function (options) { module.addFile(inputFile); }); - // 1) Figure out what this entire module exports. - // 2) Run the linkerFileTransforms, which depend on the exports. (This is, eg, - // CoffeeScript arranging to not close over the exports.) - // 3) Do static analysis to compute module-scoped variables; this has to be - // done based on the *output* of the transforms. Error recovery from the - // static analysis mutates the sources, so this has to be done before - // concatenation. - // 4) Finally, concatenate. - var exports = module.getExports(); - module.runLinkerFileTransforms(exports); - var packageScopeVariables = module.computeModuleScopeVars(exports); - var files = module.getPrelinkedFiles(exports); + // Do static analysis to compute module-scoped variables. Error recovery from + // the static analysis mutates the sources, so this has to be done before + // concatenation. + var assignedVariables = module.computeAssignedVariables(); + var files = module.getPrelinkedFiles(); return { files: files, - exports: exports, - packageScopeVariables: packageScopeVariables + assignedVariables: assignedVariables }; }; @@ -662,6 +535,13 @@ var SOURCE_MAP_INSTRUCTIONS_COMMENT = banner([ // 'Foo', "Foo.bar", etc) to the module from which it should be // imported (which must load before us at runtime) // +// noExports: if true, don't generate an exports section (don't even create +// `Package.name`). +// +// packageVariables: package-scope variables, some of which may be exports. +// a list of {name, export} objects; any non-falsy value for "export" means +// to export it. +// // useGlobalNamespace: must be the same value that was passed to link() // // prelinkFiles: the 'files' output from prelink() @@ -683,30 +563,44 @@ var link = function (options) { var header = getHeader({ imports: options.imports, - packageScopeVariables: options.packageScopeVariables + packageVariables: options.packageVariables }); + + var exported; + if (!options.noExports) { + exported = _.pluck(_.filter(options.packageVariables, function (v) { + return v.export; + }), 'name'); + } + var footer = getFooter({ - exports: options.exports, + exported: exported, name: options.name }); var ret = []; _.each(options.prelinkFiles, function (file) { if (file.sourceMap) { - var chunks = [header]; if (options.includeSourceMapInstructions) - chunks.push("\n" + SOURCE_MAP_INSTRUCTIONS_COMMENT + "\n\n"); - chunks.push(sourcemap.SourceNode.fromStringWithSourceMap( - file.source, new sourcemap.SourceMapConsumer(file.sourceMap))); - chunks.push(footer); - var node = new sourcemap.SourceNode(null, null, null, chunks); - var results = node.toStringWithSourceMap({ - file: file.servePath - }); + header = SOURCE_MAP_INSTRUCTIONS_COMMENT + "\n\n" + header; + + // Bias the source map by the length of the header without + // (fully) parsing and re-serializing it. (We used to do this + // with the source-map library, but it was incredibly slow, + // accounting for over half of bundling time.) It would be nice + // if we could use "index maps" for this (the 'sections' key), + // as that would let us avoid even JSON-parsing the source map, + // but that doesn't seem to be supported by Firefox yet. + if (header.charAt(header.length - 1) !== "\n") + header += "\n"; // make sure it's a whole number of lines + var headerLines = header.split('\n').length - 1; + var sourceMapJson = JSON.parse(file.sourceMap); + sourceMapJson.mappings = (new Array(headerLines + 1).join(';')) + + sourceMapJson.mappings; ret.push({ - source: results.code, + source: header + file.source + footer, servePath: file.servePath, - sourceMap: results.map.toString() + sourceMap: JSON.stringify(sourceMapJson) }); } else { ret.push({ @@ -722,11 +616,11 @@ var link = function (options) { var getHeader = function (options) { var chunks = []; chunks.push("(function () {\n\n" ); - chunks.push(getImportCode(options.imports, "/* Imports */\n")); - if (options.packageScopeVariables - && !_.isEmpty(options.packageScopeVariables)) { + chunks.push(getImportCode(options.imports, "/* Imports */\n", false)); + if (!_.isEmpty(options.packageVariables)) { chunks.push("/* Package-scope variables */\n"); - chunks.push("var " + options.packageScopeVariables.join(', ') + ";\n\n"); + chunks.push("var " + _.pluck(options.packageVariables, 'name').join(', ') + + ";\n\n"); } return chunks.join(''); }; @@ -737,46 +631,47 @@ var getImportCode = function (imports, header, omitvar) { if (_.isEmpty(imports)) return ""; + // Imports var scratch = {}; _.each(imports, function (name, symbol) { scratch[symbol] = packageDot(name) + "." + symbol; }); var tree = buildSymbolTree(scratch); + // Generate output var buf = header; _.each(tree, function (node, key) { buf += (omitvar ? "" : "var " ) + key + " = " + writeSymbolTree(node) + ";\n"; }); - - // XXX need to remove newlines, whitespace, in line number preserving mode buf += "\n"; + return buf; }; var getFooter = function (options) { var chunks = []; - if (options.name && options.exports && !_.isEmpty(options.exports)) { - chunks.push("/* Exports */\n"); + if (options.name && options.exported) { + chunks.push("\n\n/* Exports */\n"); chunks.push("if (typeof Package === 'undefined') Package = {};\n"); chunks.push(packageDot(options.name), " = "); // Even if there are no exports, we need to define Package.foo, because the // existence of Package.foo is how another package (eg, one that weakly // depends on foo) can tell if foo is loaded. - if (_.isEmpty(options.exports)) { + if (_.isEmpty(options.exported)) { chunks.push("{};\n"); } else { - // Given exports like Foo, Bar.Baz, Bar.Quux.A, and Bar.Quux.B, - // construct an expression like - // {Foo: Foo, Bar: {Baz: Bar.Baz, Quux: {A: Bar.Quux.A, B: Bar.Quux.B}}} + // A slightly overkill way to print out a properly indented version of + // {Foo: Foo, Bar: Bar, Quux: Quux}. (This was less overkill back when + // you could export dotted symbols.) var scratch = {}; - _.each(options.exports, function (symbol) { + _.each(options.exported, function (symbol) { scratch[symbol] = symbol; }); var exportTree = buildSymbolTree(scratch); - chunks.push(writeSymbolTree(exportTree, 0)); + chunks.push(writeSymbolTree(exportTree)); chunks.push(";\n"); } } diff --git a/tools/meteor.js b/tools/meteor.js index 4739812230..df708d31df 100644 --- a/tools/meteor.js +++ b/tools/meteor.js @@ -162,6 +162,17 @@ Fiber(function () { return tunnel; }; + var qualifySitename = function (site) { + // Append .meteor.com if we don't have a domain name. In the future, we + // probably want this to be configurable via a client-side preference of + // some kind. + if (site.indexOf(".") === -1) + site = site + ".meteor.com"; + while (site[site.length - 1] === ".") + site = site.substring(0, site.length - 1); + return site; + }; + var prepareForGalaxy = function (site, context, sshIdentity) { if (! deployGalaxy) deployGalaxy = require('./deploy-galaxy.js'); @@ -178,19 +189,20 @@ Fiber(function () { // ssh-identity argument is used to set it up. // 3. Runs the command, and kills the tunnel, if any, when it finishes. var galaxyCommand = function (cmd) { - return function (argv) { - if (argv._[1]) { - var tunnel = prepareForGalaxy(argv._[1], context, argv["ssh-identity"]); + return function (argv, showUsage) { + if (argv._[0]) { + argv._[0] = qualifySitename(argv._[0]); + var tunnel = prepareForGalaxy(argv._[0], context, argv["ssh-identity"]); var result; try { - result = cmd(argv); + result = cmd(argv, showUsage); } finally { if (tunnel) killTunnel(tunnel); } return result; } else { - return cmd(argv); + return cmd(argv, showUsage); } }; }; @@ -321,12 +333,26 @@ Fiber(function () { process.exit(1); }; - var runCommand = function (cmd, argv) { - var cmdRunner = findCommand(cmd); - if (cmdRunner.argumentParser) - cmdRunner.func(cmdRunner.argumentParser(argv)); - else - cmdRunner.func(argv); + var runCommand = function (cmd, showHelp) { + var cmdRunner = findCommand(cmd || 'run'); + // Reparse args. + var opt = require('optimist')(process.argv.slice(2)); + cmdRunner.argumentParser(opt); + var showUsage = function () { + process.stdout.write(opt.help()); + process.exit(1); + }; + if (showHelp) { + showUsage(); + } else { + // Remove the command name from argv._. Note that argv is a getter, so we + // actually have to save it into a new variable if we want to mutate its + // internals. + var argv = opt.argv; + if (cmd && cmd === argv._[0]) + argv._.shift(); + cmdRunner.func(argv, showUsage); + } }; // XXX when the pass unexpected argument or unrecognized flags, print @@ -335,42 +361,33 @@ Fiber(function () { Commands.push({ name: "run", help: "[default] Run this project in local development mode", - argumentParser: function (argv) { + argumentParser: function (opt) { // reparse args - // This help logic should probably move to run.js eventually - var opt = require('optimist') - .alias('port', 'p').default('port', 3000) - .describe('port', 'Port to listen on. NOTE: Also uses port N+1 and N+2.') - .boolean('production') - .describe('production', 'Run in production mode. Minify and bundle CSS and JS files.') - .describe('settings', 'Set optional data for Meteor.settings on the server') - .describe('release', 'Specify the release of Meteor to use') - .describe('program', 'The program in the app to run (Advanced)') - // #Once - // With --once, meteor does not re-run the project if it crashes and - // does not monitor for file changes. Intentionally undocumented: - // intended for automated testing (eg, cli-test.sh), not end-user - // use. - .boolean('once') - .usage( - "Usage: meteor run [options]\n" + - "\n" + - "Searches upward from the current directory for the root directory of a\n" + - "Meteor project, then runs that project in local development\n" + - "mode. You can use the application by pointing your web browser at\n" + - "localhost:3000. No internet connection is required.\n" + - "\n" + - "Whenever you change any of the application's source files, the changes\n" + - "are automatically detected and applied to the running application.\n" + - "\n" + - "The application's database persists between runs. It's stored under\n" + - "the .meteor directory in the root of the project.\n"); - - if (argv.help) { - process.stdout.write(opt.help()); - process.exit(1); - } - return opt.argv; + opt.alias('port', 'p').default('port', 3000) + .describe('port', 'Port to listen on. NOTE: Also uses port N+1 and N+2.') + .boolean('production') + .describe('production', 'Run in production mode. Minify and bundle CSS and JS files.') + .describe('settings', 'Set optional data for Meteor.settings on the server') + .describe('release', 'Specify the release of Meteor to use') + .describe('program', 'The program in the app to run (Advanced)') + // #Once + // With --once, meteor does not re-run the project if it crashes and does + // not monitor for file changes. Intentionally undocumented: intended for + // automated testing (eg, cli-test.sh), not end-user use. + .boolean('once') + .usage( + "Usage: meteor run [options]\n" + + "\n" + + "Searches upward from the current directory for the root directory of a\n" + + "Meteor project, then runs that project in local development\n" + + "mode. You can use the application by pointing your web browser at\n" + + "localhost:3000. No internet connection is required.\n" + + "\n" + + "Whenever you change any of the application's source files, the changes\n" + + "are automatically detected and applied to the running application.\n" + + "\n" + + "The application's database persists between runs. It's stored under\n" + + "the .meteor directory in the root of the project.\n"); }, func: function (argv) { requireDirInApp("run"); @@ -388,14 +405,30 @@ Fiber(function () { Commands.push({ name: "galaxy", help: "Interact with your galaxy server", + // Remove this once Galaxy support is official. + hidden: true, + argumentParser: function (opt) { + opt.usage( + "Usage: meteor galaxy configure \n" + + "\n" + + "Allows you to interact with a Galaxy server.\n"); + }, func: function (argv) { - var cmd = argv._.splice(0, 1)[0]; + var cmd = argv._.shift(); switch (cmd) { case "configure": // We don't use galaxyCommand here because we want the tunnel to stay // open (galaxyCommand closes the tunnel as soon as the command finishes // running). The tunnel will be cleaned up when the process exits. - prepareForGalaxy(null, context, argv["ssh-identity"]); + if (argv._[0]) + argv._[0] = qualifySitename(argv._[0]); + prepareForGalaxy(argv._[0], context, argv["ssh-identity"]); + if (! context.galaxy) { + process.stdout.write( + "You must provide a galaxy to configure (by setting the GALAXY environment variable " + + "or providing a sitename (meteor galaxy configure ).\n"); + process.exit(1); + } console.log("Visit http://localhost:" + context.galaxy.port + "/panel to configure your galaxy"); break; default: @@ -404,58 +437,33 @@ Fiber(function () { } }); - Commands.push({ - name: "help", - func: function (argv) { - if (!argv._.length || argv.help) - usage(); - var cmd = argv._.splice(0,1)[0]; - argv.help = true; - runCommand(cmd, argv); - } - }); - Commands.push({ name: "create", help: "Create a new project", - argumentParser: function (argv) { - // reparse args - var opt = require('optimist') - .describe('example', 'Example template to use.') - .boolean('list') - .describe('list', 'Show list of available examples.') - .usage( - "Usage: meteor create [--release ] \n" + - " meteor create [--release ] --example []\n" + - " meteor create --list\n" + - "\n" + - "Make a subdirectory named and create a new Meteor project\n" + - "there. You can also pass an absolute or relative path.\n" + - "\n" + - "The project will use the release of Meteor specified with the --release\n" + - "option, or the latest available version if the option is not specified.\n" + - "\n" + - "You can pass --example to start off with a copy of one of the Meteor\n" + - "sample applications. Use --list to see the available examples."); - - var new_argv = opt.argv; - + argumentParser: function (opt) { + opt.describe('example', 'Example template to use.') + .boolean('list') + .describe('list', 'Show list of available examples.') + .usage( + "Usage: meteor create [--release ] \n" + + " meteor create [--release ] --example []\n" + + " meteor create --list\n" + + "\n" + + "Make a subdirectory named and create a new Meteor project\n" + + "there. You can also pass an absolute or relative path.\n" + + "\n" + + "The project will use the release of Meteor specified with the --release\n" + + "option, or the latest available version if the option is not specified.\n" + + "\n" + + "You can pass --example to start off with a copy of one of the Meteor\n" + + "sample applications. Use --list to see the available examples."); + }, + func: function (argv, showUsage) { var appPath; if (argv._.length === 1) appPath = argv._[0]; else if (argv._.length === 0 && argv.example) appPath = argv.example; - if (appPath) { - new_argv.appPath = appPath; - } else if (argv.help) { - process.stdout.write(opt.help()); - process.exit(1); - } - - return new_argv; - }, - func: function (argv) { - var appPath = argv.appPath; var example_dir = path.join(__dirname, '..', 'examples'); var examples = _.reject(fs.readdirSync(example_dir), function (e) { @@ -472,6 +480,10 @@ Fiber(function () { process.exit(1); }; + if (!appPath) { + showUsage(); + } + if (fs.existsSync(appPath)) { process.stderr.write(appPath + ": Already exists\n"); process.exit(1); @@ -532,20 +544,14 @@ Fiber(function () { Commands.push({ name: "update", help: "Upgrade this project to the latest version of Meteor", - argumentParser: function (argv) { - // reparse args - var opt = require('optimist').usage( - "Usage: meteor update [--release ]\n" + - "\n" + - "Sets the version of Meteor to use with the current project. If a\n" + - "release is specified with --release, set the project to use that\n" + - "version. Otherwise download and use the latest release of Meteor."); - - if (argv.help) { - process.stdout.write(opt.help()); - process.exit(1); - } - return opt.argv; + argumentParser: function (opt) { + opt.boolean('dont-fetch-latest') + .usage( + "Usage: meteor update [--release ]\n" + + "\n" + + "Sets the version of Meteor to use with the current project. If a\n" + + "release is specified with --release, set the project to use that\n" + + "version. Otherwise download and use the latest release of Meteor."); }, func: function (argv) { // refuse to update if we're in a git checkout. @@ -610,15 +616,9 @@ Fiber(function () { return; } - // Otherwise, we have to upgrade the app too. - - // Write the release to .meteor/release if it's changed (or if this is a - // pre-engine app). + // Otherwise, we have to upgrade the app too, if the release changed. var appRelease = project.getMeteorReleaseVersion(context.appDir); - if (appRelease === null || appRelease !== context.releaseVersion) { - project.writeMeteorReleaseVersion(context.appDir, - context.releaseVersion); - } else { + if (appRelease !== null && appRelease === context.releaseVersion) { if (triedToGloballyUpdateButFailed) { console.log( "This project is already at Meteor %s, the latest release\n" + @@ -632,9 +632,26 @@ Fiber(function () { return; } + // Write the release to .meteor/release. + project.writeMeteorReleaseVersion(context.appDir, + context.releaseVersion); + + // 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); + // We can only run upgrades from pinned apps. + if (oldManifest) { + var upgraders = _.difference(context.releaseManifest.upgraders || [], + oldManifest.upgraders || []); + _.each(upgraders, function (upgrader) { + require("./upgraders.js").runUpgrader(upgrader, context.appDir); + }); + } + // This is the right spot to do any other changes we need to the app in - // order to update it for the new release (new metadata file formats, - // etc, or maybe even updating renamed APIs). + // order to update it for the new release . // XXX add app packages to .meteor/packages here for linker upgrade! console.log("%s: updated to Meteor %s.", path.basename(context.appDir), context.releaseVersion); @@ -647,19 +664,43 @@ Fiber(function () { } }); + Commands.push({ + name: "run-upgrader", + help: "Execute a specific upgrader by name. Intended for testing.", + hidden: true, + argumentParser: function (opt) { + opt .usage( + "Usage: meteor run-upgrader \n" + + "\n" + + "Runs a specific upgrader on the current app. This is for testing\n" + + "internal functionality of Meteor."); + }, + func: function (argv, showUsage) { + if (argv._.length !== 1) + showUsage(); + + requireDirInApp("run-upgrader"); + + var upgraders = require("./upgraders.js"); + console.log("%s: running upgrader %s.", + path.basename(context.appDir), argv._[0]); + upgraders.runUpgrader(argv._[0], context.appDir); + } + }); + Commands.push({ name: "add", help: "Add a package to this project", - func: function (argv) { - if (argv.help || !argv._.length) { - process.stdout.write( - "Usage: meteor add [package] [package..]\n" + + argumentParser: function (opt) { + opt.usage("Usage: meteor add [package] [package..]\n" + "\n" + "Adds packages to your Meteor project. You can add multiple\n" + "packages with one command. For a list of the available packages, see\n" + "'meteor list'.\n"); - process.exit(1); - } + }, + func: function (argv, showUsage) { + if (_.isEmpty(argv._)) + showUsage(); requireDirInApp('add'); var all = context.library.list(); @@ -685,16 +726,16 @@ Fiber(function () { Commands.push({ name: "remove", help: "Remove a package from this project", - func: function (argv) { - if (argv.help || !argv._.length) { - process.stdout.write( - "Usage: meteor remove [package] [package..]\n" + - "\n" + - "Removes a package previously added to your Meteor project. For a\n" + - "list of the packages that your application is currently using, see\n" + - "'meteor list --using'.\n"); - process.exit(1); - } + argumentParser: function (opt) { + opt.usage("Usage: meteor remove [package] [package..]\n" + + "\n" + + "Removes a package previously added to your Meteor project. For a\n" + + "list of the packages that your application is currently using, see\n" + + "'meteor list --using'.\n"); + }, + func: function (argv, showUsage) { + if (_.isEmpty(argv._)) + showUsage(); requireDirInApp('remove'); var using = {}; @@ -716,18 +757,16 @@ Fiber(function () { Commands.push({ name: "list", help: "List available packages", + argumentParser: function (opt) { + opt.boolean("using") + .usage("Usage: meteor list [--using]\n" + + "\n" + + "Without arguments, lists all available Meteor packages. To add one\n" + + "of these packages to your project, see 'meteor add'.\n" + + "\n" + + "With --using, list the packages that you have added to your project.\n"); + }, func: function (argv) { - if (argv.help) { - process.stdout.write( - "Usage: meteor list [--using]\n" + - "\n" + - "Without arguments, lists all available Meteor packages. To add one\n" + - "of these packages to your project, see 'meteor add'.\n" + - "\n" + - "With --using, list the packages that you have added to your project.\n"); - process.exit(1); - } - if (argv.using) { requireDirInApp('list --using'); var using = project.get_packages(context.appDir); @@ -762,27 +801,19 @@ Fiber(function () { Commands.push({ name: "bundle", help: "Pack this project up into a tarball", - func: function (argv) { - var usage = function () { - process.stdout.write( - "Usage: meteor bundle \n" + - "\n" + - "Package this project up for deployment. The output is a tarball that\n" + - "includes everything necessary to run the application. See README in\n" + - "the tarball for details.\n"); - process.exit(1); - }; - if (argv.help) - usage(); - - // re-parse the args to this command - // XXX clean up this whole file :) - argv = require("optimist") - .boolean('for-deploy').argv; - argv._.shift(); // pull off the word "bundle" - - if (argv._.length != 1) - usage(); + argumentParser: function (opt) { + opt.boolean('for-deploy') + .boolean('debug') + .describe('debug', "bundle in debug mode (don't minify, etc)") + .usage("Usage: meteor bundle \n" + + "\n" + + "Package this project up for deployment. The output is a tarball that\n" + + "includes everything necessary to run the application. See README in\n" + + "the tarball for details.\n"); + }, + func: function (argv, showUsage) { + if (argv._.length !== 1) + showUsage(); // XXX if they pass a file that doesn't end in .tar.gz or .tgz, // add the former for them @@ -802,7 +833,7 @@ Fiber(function () { var bundler = require(path.join(__dirname, 'bundler.js')); var bundleResult = bundler.bundle(context.appDir, bundle_path, { nodeModulesMode: argv['for-deploy'] ? 'skip' : 'copy', - minify: true, // XXX allow --debug + minify: !argv.debug, releaseStamp: context.releaseVersion, library: context.library }); @@ -825,47 +856,37 @@ Fiber(function () { Commands.push({ name: "mongo", help: "Connect to the Mongo database for the specified site", - argumentParser: function (argv) { - var opt = require('optimist') - .boolean('url') - .boolean('U') - .alias('url', 'U') - .describe('url', 'return a Mongo database URL') - .usage( - "Usage: meteor mongo [--url] [site]\n" + - "\n" + - "Opens a Mongo shell to view or manipulate collections.\n" + - "\n" + - "If site is specified, this is the hosted Mongo database for the deployed\n" + - "Meteor site.\n" + - "\n" + - "If no site is specified, this is the current project's local development\n" + - "database. In this case, the current working directory must be a\n" + - "Meteor project directory, and the Meteor application must already be\n" + - "running.\n" + - "\n" + - "Instead of opening a shell, specifying --url (-U) will return a URL\n" + - "suitable for an external program to connect to the database. For remote\n" + - "databases on deployed applications, the URL is valid for one minute.\n" - ); - - if (argv.help) { - process.stdout.write(opt.help()); - process.exit(1); - } - - if (opt.argv._.length !== 1 && opt.argv._.length !== 2) { - // usage - process.stdout.write(opt.help()); - process.exit(1); - } - - return opt.argv; + argumentParser: function (opt) { + opt.boolean('url') + .boolean('U') + .alias('url', 'U') + .describe('url', 'return a Mongo database URL') + .usage( + "Usage: meteor mongo [--url] [site]\n" + + "\n" + + "Opens a Mongo shell to view or manipulate collections.\n" + + "\n" + + "If site is specified, this is the hosted Mongo database for the deployed\n" + + "Meteor site.\n" + + "\n" + + "If no site is specified, this is the current project's local development\n" + + "database. In this case, the current working directory must be a\n" + + "Meteor project directory, and the Meteor application must already be\n" + + "running.\n" + + "\n" + + "Instead of opening a shell, specifying --url (-U) will return a URL\n" + + "suitable for an external program to connect to the database. For remote\n" + + "databases on deployed applications, the URL is valid for one minute.\n" + ); }, - func: galaxyCommand(function (argv) { + + func: galaxyCommand(function (argv, showUsage) { + if (argv._.length > 1) + showUsage(); + var mongoUrl; - if (argv._.length === 1) { + if (argv._.length === 0) { // localhost mode var fut = new Future(); find_mongo_port("mongo", function (mongod_port) { @@ -882,8 +903,8 @@ Fiber(function () { }); mongoUrl = fut.wait(); - } else if (argv._.length === 2) { - var site = argv._[1]; + } else { + var site = argv._[0]; // remote mode if (context.galaxy) { mongoUrl = deployGalaxy.temporaryMongoUrl({ @@ -906,63 +927,64 @@ Fiber(function () { Commands.push({ name: "deploy", help: "Deploy this project to Meteor", - argumentParser: function (argv) { - var opt = require('optimist') - .alias('password', 'P') - .boolean('password') - .boolean('P') - .describe('password', 'set a password for this deployment') - .alias('delete', 'D') - .boolean('delete') - .boolean('D') - .describe('delete', "permanently delete this deployment") - .boolean('debug') - .describe('debug', 'deploy in debug mode (don\'t minify, etc)') - .describe('settings', 'set optional data for Meteor.settings') - .alias('ssh-identity', 'i') - .describe('ssh-identity', 'Selects a file from which the identity (private key) is read. See ssh(1) for details.') - .describe('star', 'a star (tarball) to deploy instead of the current meteor app') - .usage( - "Usage: meteor deploy [--password] [--settings settings.json] [--debug] [--delete]\n" + - "\n" + - "Deploys the project in your current directory to Meteor's servers.\n" + - "\n" + - "You can deploy to any available name under 'meteor.com'\n" + - "without any additional configuration, for example,\n" + - "'myapp.meteor.com'. If you deploy to a custom domain, such as\n" + - "'myapp.mydomain.com', then you'll also need to configure your domain's\n" + - "DNS records. See the Meteor docs for details.\n" + - "\n" + - "The --settings flag can be used to pass deploy-specific information to\n" + - "the application. It will be available at runtime in Meteor.settings, but only\n" + - "on the server. If the object contains a key named 'public', then\n" + - "Meteor.settings.public will also be available on the client. The argument\n" + - "is the name of a file containing the JSON data to use. The settings will\n" + - "persist across deployments until you again specify a settings file. To\n" + - "unset Meteor.settings, pass an empty settings file.\n" + - "\n" + - "The --delete flag permanently removes a deployed application, including\n" + - "all of its stored data.\n" + - "\n" + - "The --password flag sets an administrative password for the domain. Once\n" + - "set, any subsequent 'deploy', 'logs', or 'mongo' command will prompt for\n" + - "the password. You can change the password with a second 'deploy' command." - ); - - var new_argv = opt.argv; - - if (argv.help || new_argv._.length != 2) { - process.stdout.write(opt.help()); - process.exit(1); - } - return new_argv; + argumentParser: function (opt) { + opt.alias('password', 'P') + .boolean('password') + .boolean('P') + .describe('password', 'set a password for this deployment') + .alias('delete', 'D') + .boolean('delete') + .boolean('D') + .describe('delete', "permanently delete this deployment") + .boolean('debug') + .describe('debug', 'deploy in debug mode (don\'t minify, etc)') + .describe('settings', 'set optional data for Meteor.settings') + .alias('ssh-identity', 'i') + .describe('ssh-identity', 'Selects a file from which the identity (private key) is read. See ssh(1) for details.') + .describe('star', 'a star (tarball) to deploy instead of the current meteor app') + .boolean('admin') + // Shouldn't be documented until the Galaxy release + //.describe('admin', 'Marks the application as an admin app, it will be available in Galaxy admin interface.') + .usage( + "Usage: meteor deploy [--password] [--settings settings.json] [--debug] [--delete]\n" + + "\n" + + "Deploys the project in your current directory to Meteor's servers.\n" + + "\n" + + "You can deploy to any available name under 'meteor.com'\n" + + "without any additional configuration, for example,\n" + + "'myapp.meteor.com'. If you deploy to a custom domain, such as\n" + + "'myapp.mydomain.com', then you'll also need to configure your domain's\n" + + "DNS records. See the Meteor docs for details.\n" + + "\n" + + "The --settings flag can be used to pass deploy-specific information to\n" + + "the application. It will be available at runtime in Meteor.settings, but only\n" + + "on the server. If the object contains a key named 'public', then\n" + + "Meteor.settings.public will also be available on the client. The argument\n" + + "is the name of a file containing the JSON data to use. The settings will\n" + + "persist across deployments until you again specify a settings file. To\n" + + "unset Meteor.settings, pass an empty settings file.\n" + + "\n" + + "The --delete flag permanently removes a deployed application, including\n" + + "all of its stored data.\n" + + "\n" + + "The --password flag sets an administrative password for the domain. Once\n" + + "set, any subsequent 'deploy', 'logs', or 'mongo' command will prompt for\n" + + "the password. You can change the password with a second 'deploy' command.\n" + // Shouldn't be documented until the Galaxy release + //"\n" + + //"The --admin flag marks application as administrative to Galaxy interface.\n" + + //"Application's web-interface will be accessible from admin's panel only.\n" + ); }, - func: galaxyCommand(function (argv) { - var site = argv._[1]; + func: galaxyCommand(function (argv, showUsage) { + if (argv._.length !== 1) + showUsage(); + + var site = argv._[0]; if (argv.delete) { if (context.galaxy) - deployGalaxy.deleteApp(context); + deployGalaxy.deleteApp(site, context); else deploy.delete_app(site); } else { @@ -991,7 +1013,8 @@ Fiber(function () { minify: !argv.debug, releaseStamp: context.releaseVersion, library: context.library - } + }, + admin: argv.admin }); } else { deploy.deployCmd({ @@ -1014,35 +1037,24 @@ Fiber(function () { Commands.push({ name: "logs", help: "Show logs for specified site", - argumentParser: function (argv) { - return require('optimist').boolean('f').argv; + argumentParser: function (opt) { + opt.boolean('f') + // XXX once Galaxy is released, document -f + .usage("Usage: meteor logs \n" + + "\n" + + "Retrieves the server logs for the requested site.\n"); }, - func: function (argv) { - var site = argv._[1]; + func: function (argv, showUsage) { + if (argv._.length !== 1) + showUsage(); + // We don't use galaxyCommand here because we want the tunnel to stay // open (galaxyCommand closes the tunnel as soon as the command finishes // running). The tunnel will be cleaned up when the process exits. + var site = qualifySitename(argv._[0]); var tunnel = prepareForGalaxy(site, context, argv["ssh-identity"]); var useGalaxy = !!context.galaxy; - if (argv.help || argv._.length !== 2) { - if (useGalaxy) { - process.stdout.write( - "Usage: meteor logs [-f] \n" + - "\n" + - "Retrieves the server logs for the requested site.\n" + - "\n" + - "Pass -f to see new logs as they come in.\n"); - } else { - process.stdout.write( - "Usage: meteor logs \n" + - "\n" + - "Retrieves the server logs for the requested site.\n"); - } - - process.exit(1); - } - if (useGalaxy) { var streaming = !!argv.f; deployGalaxy.logs({ @@ -1061,15 +1073,14 @@ Fiber(function () { Commands.push({ name: "reset", help: "Reset the project state. Erases the local database.", + argumentParser: function (opt) { + opt.usage("Usage: meteor reset\n" + + "\n" + + "Reset the current project to a fresh state. Removes all local\n" + + "data and kills any running meteor development servers.\n"); + }, func: function (argv) { - if (argv.help) { - process.stdout.write( - "Usage: meteor reset\n" + - "\n" + - "Reset the current project to a fresh state. Removes all local\n" + - "data and kills any running meteor development servers.\n"); - process.exit(1); - } else if (!_.isEmpty(argv._)) { + if (!_.isEmpty(argv._)) { process.stdout.write("meteor reset only affects the locally stored database.\n\n" + "To reset a deployed application use\nmeteor deploy --delete appname\n" + "followed by\nmeteor deploy appname\n"); @@ -1097,51 +1108,38 @@ Fiber(function () { Commands.push({ name: "test-packages", help: "Test one or more packages", - argumentParser: function (argv) { - // reparse args + argumentParser: function (opt) { // This help logic should probably move to run.js eventually - var opt = require('optimist') - .alias('port', 'p').default('port', 3000) - .describe('port', 'Port to listen on. NOTE: Also uses port N+1 and N+2.') - .describe('deploy', 'Optionally, specify a domain to deploy to, rather than running locally.') - .boolean('production') - .describe('production', 'Run in production mode. Minify and bundle CSS and JS files.') - .boolean('once') // See #Once - .describe('settings', 'Set optional data for Meteor.settings on the server') - .usage( - "Usage: meteor test-packages [--release ] [options] [package...]\n" + - "\n" + - "Runs unit tests for one or more packages. The results are shown in\n" + - "a browser dashboard that updates whenever a relevant source file is\n" + - "modified.\n" + - "\n" + - "Packages may be specified by name or by path. If a package argument\n" + - "contains a '/', it is loaded from a directory of that name; otherwise,\n" + - "the package name is resolved according to the usual package search\n" + - "algorithm ('packages' subdirectory of the current app, $PACKAGE_DIRS\n" + - "directories, and core packages in that order). You can test any number\n" + - "of packages simultaneously. If you don't specify any package names\n" + - "then all available packages will be tested.\n" + - "\n" + - "Open the test dashboard in your browser to run the tests and see the\n" + - "results. By default the URL is localhost:3000 but that can be changed\n" + - "with --port. Alternatively, you can deploy the tests onto the 'meteor\n" + - "deploy' server by using --deploy. This gives you a public URL that you\n" + - "can use in conjunction with a service like Browserling or BrowserStack\n" + - "to try the tests against many different browser versions."); - - if (argv.help) { - process.stdout.write(opt.help()); - process.exit(1); - } - return opt.argv; + opt .alias('port', 'p').default('port', 3000) + .describe('port', 'Port to listen on. NOTE: Also uses port N+1 and N+2.') + .describe('deploy', 'Optionally, specify a domain to deploy to, rather than running locally.') + .boolean('production') + .describe('production', 'Run in production mode. Minify and bundle CSS and JS files.') + .boolean('once') // See #Once + .describe('settings', 'Set optional data for Meteor.settings on the server') + .usage( + "Usage: meteor test-packages [--release ] [options] [package...]\n" + + "\n" + + "Runs unit tests for one or more packages. The results are shown in\n" + + "a browser dashboard that updates whenever a relevant source file is\n" + + "modified.\n" + + "\n" + + "Packages may be specified by name or by path. If a package argument\n" + + "contains a '/', it is loaded from a directory of that name; otherwise,\n" + + "the package name is resolved according to the usual package search\n" + + "algorithm ('packages' subdirectory of the current app, $PACKAGE_DIRS\n" + + "directories, and core packages in that order). You can test any number\n" + + "of packages simultaneously. If you don't specify any package names\n" + + "then all available packages will be tested.\n" + + "\n" + + "Open the test dashboard in your browser to run the tests and see the\n" + + "results. By default the URL is localhost:3000 but that can be changed\n" + + "with --port. Alternatively, you can deploy the tests onto the 'meteor\n" + + "deploy' server by using --deploy. This gives you a public URL that you\n" + + "can use in conjunction with a service like Browserling or BrowserStack\n" + + "to try the tests against many different browser versions."); }, func: function (argv) { - // remove 'test-packages'. - // XXX we need to fix up this argv stuff once and for all to provide a - // real interface to commands that isn't terrible. - argv._.shift(); - var testPackages; if (_.isEmpty(argv._)) { // XXX The call to list() here is unfortunate, because list() @@ -1211,21 +1209,21 @@ Fiber(function () { name: "rebuild-all", help: "Rebuild all packages", hidden: true, - func: function (argv) { - if (argv.help || argv._.length !== 0) { - process.stdout.write( -"Usage: meteor rebuild-all\n" + -"\n" + -"Rebuild all source packages in the library. This includes packages found\n" + -"through the PACKAGE_DIRS environment variable, local packages in the \n" + -"current application, and packages in the warehouse (but only those in the\n" + -"currently effective Meteor release.) It doesn't include any packages for\n" + -"which we don't have the source.\n" + -"\n" + -"You should never need to use this command. It is intended for use while\n" + -"debugging the Meteor packaging tools themselves.\n"); - process.exit(1); - } + argumentParser: function (opt) { + opt.usage("Usage: meteor rebuild-all\n" + + "\n" + + "Rebuild all source packages in the library. This includes packages found\n" + + "through the PACKAGE_DIRS environment variable, local packages in the \n" + + "current application, and packages in the warehouse (but only those in the\n" + + "currently effective Meteor release.) It doesn't include any packages for\n" + + "which we don't have the source.\n" + + "\n" + + "You should never need to use this command. It is intended for use while\n" + + "debugging the Meteor packaging tools themselves.\n"); + }, + func: function (argv, showUsage) { + if (argv._.length !== 0) + showUsage(); if (context.appDir) { // The library doesn't know about other programs in your app. Let's blow @@ -1262,11 +1260,15 @@ Fiber(function () { name: "run-command", help: "Build and run a command-line tool", hidden: true, + argumentParser: function (opt) { + // This command does things manually. See below. + }, func: function (argv) { // At this point options such as --help have already been parsed // out.. that's no good. We'll have to go back tho the original // process.argv and parse it ourselves. - argv = process.argv.slice(3); + argv = process.argv; + argv = argv.slice(argv.indexOf("run-command") + 1); if (! argv.length || argv[0] === "--help") { process.stdout.write( "Usage: meteor run-command [arguments..]\n" + @@ -1383,10 +1385,10 @@ Fiber(function () { process.exit(0); }; - // Implements --build-version. - var printBuildVersion = function () { + // Implements --built-by + var printBuiltBy = function () { var packages = require('./packages.js'); - console.log(packages.BUILD_VERSION); + console.log(packages.BUILT_BY); process.exit(0); }; @@ -1418,7 +1420,7 @@ Fiber(function () { .boolean("h") .boolean("help") .boolean("version") - .boolean("build-version") + .boolean("built-by") .boolean("arch") .boolean("debug") .alias("i", "ssh-identity"); @@ -1440,13 +1442,13 @@ Fiber(function () { return; } - if (argv.help) { - argv._.splice(0, 0, "help"); - delete argv.help; + if (argv._[0] === "help") { + argv._.shift(); + argv.help = true; } - if (argv['build-version']) { - printBuildVersion(); + if (argv['built-by']) { + printBuiltBy(); return; } @@ -1460,14 +1462,17 @@ Fiber(function () { return; } - var cmd = 'run'; + var cmd = null; if (argv._.length) - cmd = argv._.splice(0,1)[0]; + cmd = argv._[0]; if (PROFILE_REQUIRE) require('./profile-require.js').printReport(); - runCommand(cmd, argv); + if (argv.help && (!cmd || cmd === "help")) + usage(); + + runCommand(cmd, argv.help); }; main(); diff --git a/tools/packages.js b/tools/packages.js index e4491e2808..7ee8dc248a 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -20,54 +20,18 @@ var sourcemap = require('source-map'); // unipackage/slice changes, but this version (which is build-tool-specific) can // change when the the contents (not structure) of the built output changes. So // eg, if we improve the linker's static analysis, this should be bumped. -exports.BUILD_VERSION = 'meteor/2'; +// +// You should also update this whenever you update any of the packages used +// directly by the unipackage creation process (eg js-analyze) since they do not +// end up as watched dependencies. (At least for now, packages only used in +// target creation (eg minifiers and dev-bundle-fetcher) don't require you to +// update BUILT_BY, though you will need to quit and rerun "meteor run".) +exports.BUILT_BY = 'meteor/8'; -// Find all files under `rootPath` that have an extension in -// `extensions` (an array of extensions without leading dot), and -// return them as a list of paths relative to rootPath. Ignore files -// that match a regexp in the ignoreFiles array, if given. As a -// special case (ugh), push all html files to the head of the list. -var scanForSources = function (rootPath, extensions, ignoreFiles) { - var self = this; - - // find everything in tree, sorted depth-first alphabetically. - var fileList = files.file_list_sync(rootPath, extensions); - fileList = _.reject(fileList, function (file) { - return _.any(ignoreFiles || [], function (pattern) { - return file.match(pattern); - }); - }); - fileList.sort(files.sort); - - // XXX HUGE HACK -- - // push html (template) files ahead of everything else. this is - // important because the user wants to be able to say - // Template.foo.events = { ... } - // - // maybe all of the templates should go in one file? packages - // should probably have a way to request this treatment (load - // order dependency tags?) .. who knows. - var htmls = []; - _.each(fileList, function (filename) { - if (path.extname(filename) === '.html') { - htmls.push(filename); - fileList = _.reject(fileList, function (f) { return f === filename;}); - } - }); - fileList = htmls.concat(fileList); - - // now make everything relative to rootPath - var prefix = rootPath; - if (prefix[prefix.length - 1] !== path.sep) - prefix += path.sep; - - return fileList.map(function (abs) { - if (path.relative(prefix, abs).match(/\.\./)) - // XXX audit to make sure it works in all possible symlink - // scenarios - throw new Error("internal error: source file outside of parent?"); - return abs.substr(prefix.length); - }); +// Like Perl's quotemeta: quotes all regexp metacharacters. See +// https://github.com/substack/quotemeta/blob/master/index.js +var quotemeta = function (str) { + return String(str).replace(/(\W)/g, '\\$1'); }; var rejectBadPath = function (p) { @@ -86,12 +50,11 @@ var rejectBadPath = function (p) { // - uses // - implies // - getSourcesFunc -// - forceExport -// - dependencyInfo +// - exports +// - watchSet // - nodeModulesPath -// - noExports // -// Do not include the source files in dependencyInfo. They will be +// Do not include the source files in watchSet. They will be // added at compile time when the sources are actually read. var Slice = function (pkg, options) { var self = this; @@ -151,53 +114,53 @@ var Slice = function (pkg, options) { // local plugins in this package) to compute this. self.getSourcesFunc = options.getSourcesFunc || null; - // Symbols that this slice should export even if @export directives - // don't appear in the source code. List of symbols (as strings.) - // Empty if loaded from unipackage. - self.forceExport = options.forceExport || []; + // True if this slice is not permitted to have any exports, and in fact should + // not even define `Package.name` (ie, test slices). + self.noExports = options.noExports || false; + + // Symbols that this slice should export. List of symbols (as strings). Null + // on built packages (see packageVariables instead), or in packages where + // noExports is set. + self.declaredExports = options.declaredExports || null; // Files and directories that we want to monitor for changes in - // development mode, such as source files and package.js, in the - // format accepted by watch.Watcher. - self.dependencyInfo = options.dependencyInfo || - { files: {}, directories: {} }; + // development mode, such as source files and package.js, as a watch.WatchSet. + self.watchSet = options.watchSet || new watch.WatchSet(); // Has this slice been compiled? self.isBuilt = false; - // All symbols exported from the JavaScript code in this - // package. Array of string symbol (eg "Foo", "Bar.baz".) Set only - // when isBuilt is true. - self.exports = null; - - // Are we allowed to have exports? (eg, test slices don't export.) - self.noExports = !!options.noExports; - - // Prelink output. 'prelinkFiles' is the partially linked JavaScript code (an + // Prelink output. + // + // 'prelinkFiles' is the partially linked JavaScript code (an // array of objects with keys 'source' and 'servePath', both strings -- see - // prelink() in linker.js) 'packageScopeVariables' are are variables that are - // syntactically globals in our input files and which we capture with a - // package-scope closure. Both of these are inputs into the final link phase, - // which inserts the final JavaScript resources into 'resources'. Set only - // when isBuilt is true. + // prelink() in linker.js) + // + // 'packageVariables' are are variables that are syntactically globals in our + // input files and which we capture with a package-scope closure. A list of + // objects with keys 'name' (required) and 'export' (true, 'tests', or falsy). + // + // Both of these are saved into slices on disk, and are inputs into the final + // link phase, which inserts the final JavaScript resources into + // 'resources'. Set only when isBuilt is true. self.prelinkFiles = null; - self.packageScopeVariables = null; + self.packageVariables = null; // All of the data provided for eventual inclusion in the bundle, // other than JavaScript that still needs to be fed through the // final link stage. A list of objects with these keys: // - // type: "js", "css", "head", "body", "static" + // type: "js", "css", "head", "body", "asset" // // data: The contents of this resource, as a Buffer. For example, // for "head", the data to insert in ; for "js", the // JavaScript source code (which may be subject to further - // processing such as minification); for "static", the contents of a + // processing such as minification); for "asset", the contents of a // static resource such as an image. // // servePath: The (absolute) path at which the resource would prefer // to be served. Interpretation varies by type. For example, always - // honored for "static", ignored for "head" and "body", sometimes + // honored for "asset", ignored for "head" and "body", sometimes // honored for CSS but ignored if we are concatenating. // // sourceMap: Allowed only for "js". If present, a string. @@ -209,10 +172,6 @@ var Slice = function (pkg, options) { // resolve Npm.require() calls in this slice. null if this slice // does not have a node_modules. self.nodeModulesPath = options.nodeModulesPath; - - // Absolute path to the location on disk where Assets API calls will search in - // this slice. - self.staticDirectory = options.staticDirectory; }; _.extend(Slice.prototype, { @@ -220,7 +179,7 @@ _.extend(Slice.prototype, { // through the appropriate handlers and run the prelink phase on any // resulting JavaScript. Also add all provided source files to the // package dependencies. Sets fields such as dependencies, exports, - // prelinkFiles, packageScopeVariables, and resources. + // prelinkFiles, packageVariables, and resources. build: function () { var self = this; var isApp = ! self.pkg.name; @@ -251,14 +210,26 @@ _.extend(Slice.prototype, { self[field] = scrubbed; }); + var addAsset = function (contents, relPath) { + // XXX hack + if (!self.pkg.name) + relPath = relPath.replace(/^(private|public)\//, ''); + + resources.push({ + type: "asset", + data: contents, + path: relPath, + servePath: path.join(self.pkg.serveRoot, relPath) + }); + }; + _.each(self.getSourcesFunc(), function (source) { var relPath = source.relPath; var fileOptions = _.clone(source.fileOptions) || {}; var absPath = path.resolve(self.pkg.sourceRoot, relPath); var ext = path.extname(relPath).substr(1); - var handler = self._getSourceHandler(ext); - var contents = fs.readFileSync(absPath); - self.dependencyInfo.files[absPath] = Builder.sha1(contents); + var handler = !fileOptions.isAsset && self._getSourceHandler(ext); + var contents = watch.readAndWatchFile(self.watchSet, absPath); if (! handler) { // If we don't have an extension handler, serve this file as a @@ -266,11 +237,7 @@ _.extend(Slice.prototype, { // // XXX This is pretty confusing, especially if you've // accidentally forgotten a plugin -- revisit? - resources.push({ - type: "static", - data: contents, - servePath: path.join(self.pkg.serveRoot, relPath) - }); + addAsset(contents, relPath); return; } @@ -299,6 +266,9 @@ _.extend(Slice.prototype, { // - fileOptions: any options passed to "api.add_files"; for // use by the plugin. The built-in "js" plugin uses the "bare" // option for files that shouldn't be wrapped in a closure. + // - declaredExports: An array of symbols exported by this slice, or null + // if it may not export any symbols (eg, test slices). This is used by + // CoffeeScript to ensure that it doesn't close over those symbols, eg. // - read(n): read from the input file. If n is given it should // be an integer, and you will receive the next n bytes of the // file as a Buffer. If n is omitted you get the rest of the @@ -329,7 +299,7 @@ _.extend(Slice.prototype, { // in the module. // - addAsset({ path: "my/image.png", data: Buffer }) // Add a file to serve as-is over HTTP (browser targets) or - // to include as-is in the bundle (native targets). + // to include as-is in the bundle (os targets). // This time `data` is a Buffer rather than a string. For // browser targets, it will be served at the exact path you // request (concatenated with rootOutputPath). For server @@ -345,7 +315,7 @@ _.extend(Slice.prototype, { // // XXX for now, these handlers must only generate portable code // (code that isn't dependent on the arch, other than 'browser' - // vs 'native') -- they can look at the arch that is provided + // vs 'os') -- they can look at the arch that is provided // but they can't rely on the running on that particular arch // (in the end, an arch-specific slice will be emitted only if // there are native node modules.) Obviously this should @@ -390,6 +360,7 @@ _.extend(Slice.prototype, { rootOutputPath: self.pkg.serveRoot, arch: self.arch, fileOptions: fileOptions, + declaredExports: _.pluck(self.declaredExports, 'name'), read: function (n) { if (n === undefined || readOffset + n > contents.length) n = contents.length - readOffset; @@ -433,7 +404,6 @@ _.extend(Slice.prototype, { source: options.data, sourcePath: options.sourcePath, servePath: path.join(self.pkg.serveRoot, options.path), - linkerFileTransform: options.linkerFileTransform, bare: !!options.bare, sourceMap: options.sourceMap }); @@ -441,11 +411,7 @@ _.extend(Slice.prototype, { addAsset: function (options) { if (! (options.data instanceof Buffer)) throw new Error("'data' option to addAsset must be a Buffer"); - resources.push({ - type: "static", - data: options.data, - servePath: path.join(self.pkg.serveRoot, options.path) - }); + addAsset(options.data, options.path); }, error: function (options) { buildmessage.error(options.message || ("error building " + relPath), { @@ -488,30 +454,50 @@ _.extend(Slice.prototype, { "/packages/" + self.pkg.name + (self.sliceName === "main" ? "" : ("." + self.sliceName)) + ".js", name: self.pkg.name || null, - forceExport: self.forceExport, - noExports: self.noExports, + declaredExports: _.pluck(self.declaredExports, 'name'), jsAnalyze: jsAnalyze }); - // Add dependencies on the source code to any plugins that we - // could have used (we need to depend even on plugins that we - // didn't use, because if they were changed they might become - // relevant to us) - // - // XXX I guess they're probably properly disjoint since plugins - // probably include only file dependencies? Anyway it would be a - // strange situation if plugin source directories overlapped with - // other parts of your app + // Add dependencies on the source code to any plugins that we could have + // used. We need to depend even on plugins that we didn't use, because if + // they were changed they might become relevant to us. This means that we + // end up depending on every source file contributing to all plugins in the + // packages we use (including source files from other packages that the + // plugin program itself uses), as well as the package.js file from every + // package we directly use (since changing the package.js may add or remove + // a plugin). _.each(self._activePluginPackages(), function (otherPkg) { - _.extend(self.dependencyInfo.files, - otherPkg.pluginDependencyInfo.files); - _.extend(self.dependencyInfo.directories, - otherPkg.pluginDependencyInfo.directories); + self.watchSet.merge(otherPkg.pluginWatchSet); + // XXX this assumes this is not overwriting something different + self.pkg.pluginProviderPackageDirs[otherPkg.name] = + otherPkg.packageDirectoryForBuildInfo; }); self.prelinkFiles = results.files; - self.exports = results.exports; - self.packageScopeVariables = results.packageScopeVariables; + + self.packageVariables = []; + var packageVariableNames = {}; + _.each(self.declaredExports, function (symbol) { + if (_.has(packageVariableNames, symbol.name)) + return; + self.packageVariables.push({ + name: symbol.name, + export: symbol.testOnly? "tests" : true + }); + packageVariableNames[symbol.name] = true; + }); + _.each(results.assignedVariables, function (name) { + if (_.has(packageVariableNames, name)) + return; + self.packageVariables.push({ + name: name + }); + packageVariableNames[name] = true; + }); + // Forget about the *declared* exports; what matters is packageVariables + // now. + self.declaredExports = null; + self.resources = resources; self.isBuilt = true; }, @@ -553,8 +539,11 @@ _.extend(Slice.prototype, { bundleArch, {skipWeak: true, skipUnordered: true}, function (otherSlice) { if (! otherSlice.isBuilt) throw new Error("dependency wasn't built?"); - _.each(otherSlice.exports, function (symbol) { - imports[symbol] = otherSlice.pkg.name; + _.each(otherSlice.packageVariables, function (symbol) { + // Slightly hacky implementation of test-only exports. + if (symbol.export === true || + (symbol.export === "tests" && self.sliceName === "tests")) + imports[symbol.name] = otherSlice.pkg.name; }); }); @@ -566,8 +555,8 @@ _.extend(Slice.prototype, { // XXX report an error if there is a package called global-imports importStubServePath: isApp && '/packages/global-imports.js', prelinkFiles: self.prelinkFiles, - exports: self.exports, - packageScopeVariables: self.packageScopeVariables, + noExports: self.noExports, + packageVariables: self.packageVariables, includeSourceMapInstructions: archinfo.matches(self.arch, "browser"), name: self.pkg.name || null }); @@ -578,7 +567,6 @@ _.extend(Slice.prototype, { type: "js", data: new Buffer(file.source, 'utf8'), // XXX encoding servePath: file.servePath, - staticDirectory: self.staticDirectory, sourceMap: file.sourceMap }; }); @@ -681,6 +669,7 @@ _.extend(Slice.prototype, { path: compileStep.inputPath, sourcePath: compileStep.inputPath, // XXX eventually get rid of backward-compatibility "raw" name + // XXX COMPAT WITH 0.6.4 bare: compileStep.fileOptions.bare || compileStep.fileOptions.raw }); } @@ -743,7 +732,7 @@ _.extend(Slice.prototype, { // (find better names, though.) var nextPackageId = 1; -var Package = function (library) { +var Package = function (library, packageDirectoryForBuildInfo) { var self = this; // A unique ID (guaranteed to not be reused in this process -- if @@ -771,6 +760,14 @@ var Package = function (library) { // it's still nice to get it right.) null if loaded from unipackage. self.serveRoot = null; + // The package's directory. This is used only by other packages that use this + // package in their buildinfo.json (to detect that they need to be rebuilt if + // the library's resolution of the package name changes); it is not used to + // read files or anything else. Notably, it should be the same if a package is + // read from a source tree or read from the .build unipackage inside that + // source tree. + self.packageDirectoryForBuildInfo = packageDirectoryForBuildInfo; + // Package library that should be used to resolve this package's // dependencies self.library = library; @@ -798,22 +795,31 @@ var Package = function (library) { self.testSlices = {}; // The information necessary to build the plugins in this - // package. Map from plugin name to object with keys 'name', 'us', + // package. Map from plugin name to object with keys 'name', 'use', // 'sources', and 'npmDependencies'. self.pluginInfo = {}; - // Plugins in this package. Map from plugin name to - // bundler.Plugin. Present only when isBuilt is true. + // Plugins in this package. Map from plugin name to JsImage. Present only when + // pluginsBuilt is true. self.plugins = {}; - // Dependencies for any plugins in this package. Present only when - // isBuilt is true. - // XXX Refactor so that slice and plugin dependencies are handled by - // the same mechanism. - self.pluginDependencyInfo = { files: {}, directories: {} }; + // A WatchSet for the full transitive dependencies for all plugins in this + // package, as well as this package's package.js. If any of these dependencies + // change, our plugins need to be rebuilt... but also, any package that + // directly uses this package needs to be rebuilt in case the change to + // plugins affected compilation. + // + // Complete only when pluginsBuilt is true. + self.pluginWatchSet = new watch.WatchSet(); - // True if plugins have been initialized (if - // _ensurePluginsInitialized has been called) + // Map from package name to packageDirectoryForBuildInfo of packages that are + // directly used by this package. We use this to figure out that we need to + // rebuild if the resolution of the package changes (eg, an app package is + // added that overshadows a warehouse package, or the release changes). + self.pluginProviderPackageDirs = {}; + + // True if plugins have been initialized (if _ensurePluginsInitialized has + // been called) self._pluginsInitialized = false; // Source file handlers registered by plugins. Map from extension @@ -825,7 +831,7 @@ var Package = function (library) { // means that doesn't create it in a build state to start with) you // will need to call build() before you can use it. We break down // the two phases of the build process, plugin building and - // building, into two flags. + // slice building, into two flags. self.pluginsBuilt = false; self.slicesBuilt = false; }; @@ -841,7 +847,7 @@ _.extend(Package.prototype, { // Return the slice of the package to use for a given slice name // (eg, 'main' or 'test') and target architecture (eg, - // 'native.linux.x86_64' or 'browser'), or throw an exception if + // 'os.linux.x86_64' or 'browser'), or throw an exception if // that packages can't be loaded under these circumstances. getSingleSlice: function (name, arch) { var self = this; @@ -997,16 +1003,15 @@ _.extend(Package.prototype, { info.name)) }); - if (buildResult.dependencyInfo) { - // Merge plugin dependencies - // XXX is naive merge sufficient here? should be, because - // plugins can't (for now) contain directory dependencies? - _.extend(self.pluginDependencyInfo.files, - buildResult.dependencyInfo.files); - _.extend(self.pluginDependencyInfo.directories, - buildResult.dependencyInfo.directories); - } + // Add this plugin's dependencies to our "plugin dependency" WatchSet. + self.pluginWatchSet.merge(buildResult.watchSet); + // Remember the library resolution of all packages used by the plugin. + // XXX assumes that this merges cleanly + _.extend(self.pluginProviderPackageDirs, + buildResult.pluginProviderPackageDirs); + + // Register the built plugin's code. self.plugins[info.name] = buildResult.image; }); }); @@ -1074,7 +1079,7 @@ _.extend(Package.prototype, { return source; }); - var arch = isPortable ? "native" : archinfo.host(); + var arch = isPortable ? "os" : archinfo.host(); var slice = new Slice(self, { name: options.sliceName, arch: arch, @@ -1082,25 +1087,17 @@ _.extend(Package.prototype, { return { spec: spec }; }), getSourcesFunc: function () { return sources; }, - nodeModulesPath: nodeModulesPath, - staticDirectory: options.sourceRoot + nodeModulesPath: nodeModulesPath }); self.slices.push(slice); - self.defaultSlices = {'native': [options.sliceName]}; + self.defaultSlices = {'os': [options.sliceName]}; }, // Initialize a package from a legacy-style (package.js) package // directory. This function does not retrieve the package's // dependencies from the library, and on return, the package will be // in an unbuilt state. - // - // options: - // - skipNpmUpdate: if true, don't refresh .npm/package/node_modules (for - // packages that use Npm.depend). Only use this when you are - // certain that .npm/package/node_modules was previously created by some - // other means, and you're certain that the package's Npm.depend - // instructions haven't changed since then. initFromPackageDir: function (name, dir, options) { var self = this; var isPortable = true; @@ -1119,6 +1116,12 @@ _.extend(Package.prototype, { var code = fs.readFileSync(packageJsPath); var packageJsHash = Builder.sha1(code); + // Any package that depends on us needs to be rebuilt if our package.js file + // changes, because a change to package.js might add or remove a plugin, + // which could change a file from being handled by extension vs treated as + // an asset. + self.pluginWatchSet.addFile(packageJsPath, packageJsHash); + // == 'Package' object visible in package.js == var Package = { // Set package metadata. Options: @@ -1153,6 +1156,7 @@ _.extend(Package.prototype, { roleHandlers.test = f; }, + // XXX COMPAT WITH 0.6.4 // extension doesn't contain a dot register_extension: function (extension, callback) { if (_.has(self.legacyExtensionHandlers, extension)) { @@ -1400,9 +1404,8 @@ _.extend(Package.prototype, { var sources = {use: {client: [], server: []}, test: {client: [], server: []}}; - // symbols force-exported - var forceExport = {use: {client: [], server: []}, - test: {client: [], server: []}}; + // symbols exported + var exports = {client: [], server: []}; // packages used and implied (keys are 'spec', 'unordered', and 'weak'). an // "implied" package is a package that will be used by a slice which uses @@ -1429,6 +1432,33 @@ _.extend(Package.prototype, { // one. #OldStylePackageSupport _.each(["use", "test"], function (role) { if (roleHandlers[role]) { + var toArray = function (x) { + if (x instanceof Array) + return x; + return x ? [x] : []; + }; + + var allWheres = ['client', 'server']; + var toWhereArray = function (where) { + if (!(where instanceof Array)) { + where = where ? [where] : allWheres; + } + where = _.uniq(where); + var realWhere = _.intersection(where, allWheres); + if (realWhere.length !== where.length) { + var badWheres = _.difference(where, allWheres); + // avoid using _.each so as to not add more frames to skip + for (var i = 0; i < badWheres.length; ++i) { + buildmessage.error( + "Invalid 'where' argument: '" + badWheres[i] + "'", + // skip toWhereArray in addition to the actual API function + {useMyCaller: 1}); + }; + // recover by using the real ones only + } + return realWhere; + }; + var api = { // Called when this package wants to make another package be // used. Can also take literal package objects, if you have @@ -1461,13 +1491,15 @@ _.extend(Package.prototype, { // flag is not tracked per-environment or per-role; this may // change.) use: function (names, where, options) { + // Support `api.use(package, {weak: true})` without where. + if (_.isObject(where) && !_.isArray(where) && !options) { + options = where; + where = null; + } options = options || {}; - if (!(names instanceof Array)) - names = names ? [names] : []; - - if (!(where instanceof Array)) - where = where ? [where] : ["client", "server"]; + names = toArray(names); + where = toWhereArray(where); // A normal dependency creates an ordering constraint and a "if I'm // used, use that" constraint. Unordered dependencies lack the @@ -1506,11 +1538,8 @@ _.extend(Package.prototype, { return; } - if (!(names instanceof Array)) - names = names ? [names] : []; - - if (!(where instanceof Array)) - where = where ? [where] : ["client", "server"]; + names = toArray(names); + where = toWhereArray(where); _.each(names, function (name) { _.each(where, function (w) { @@ -1527,11 +1556,8 @@ _.extend(Package.prototype, { // be processed according to its extension (eg, *.coffee // files will be compiled to JavaScript.) add_files: function (paths, where, fileOptions) { - if (!(paths instanceof Array)) - paths = paths ? [paths] : []; - - if (!(where instanceof Array)) - where = where ? [where] : ["client", "server"]; + paths = toArray(paths); + where = toWhereArray(where); _.each(paths, function (path) { _.each(where, function (w) { @@ -1543,32 +1569,43 @@ _.extend(Package.prototype, { }); }, - // Force the export of a symbol from this package. An - // alternative to using @export directives. Possibly helpful - // when you don't want to modify the source code of a third - // party library. + // Export symbols from this package. // - // @param symbols String (eg "Foo", "Foo.bar") or array of String + // @param symbols String (eg "Foo") or array of String // @param where 'client', 'server', or an array of those - exportSymbol: function (symbols, where) { + // @param options 'testOnly', boolean. + export: function (symbols, where, options) { if (role === "test") { buildmessage.error("You cannot export symbols from a test.", { useMyCaller: true }); // recover by ignoring return; } - if (!(symbols instanceof Array)) - symbols = symbols ? [symbols] : []; + // Support `api.export("FooTest", {testOnly: true})` without + // where. + if (_.isObject(where) && !_.isArray(where) && !options) { + options = where; + where = null; + } + options = options || {}; - if (!(where instanceof Array)) - where = where ? [where] : []; + symbols = toArray(symbols); + where = toWhereArray(where); _.each(symbols, function (symbol) { + // XXX be unicode-friendlier + if (!symbol.match(/^([_$a-zA-Z][_$a-zA-Z0-9]*)$/)) { + buildmessage.error("Bad exported symbol: " + symbol, + { useMyCaller: true }); + // recover by ignoring + return; + } _.each(where, function (w) { - forceExport[role][w].push(symbol); + exports[w].push({name: symbol, testOnly: !!options.testOnly}); }); }); }, + // XXX COMPAT WITH 0.6.4 error: function () { // I would try to support this but I don't even know what // its signature was supposed to be anymore @@ -1577,6 +1614,7 @@ _.extend(Package.prototype, { { useMyCaller: true }); // recover by ignoring }, + // XXX COMPAT WITH 0.6.4 registered_extensions: function () { buildmessage.error( "api.registered_extensions() is no longer supported", @@ -1615,40 +1653,34 @@ _.extend(Package.prototype, { // XXX maybe there should be separate NPM dirs for use vs test? var packageNpmDir = path.resolve(path.join(self.sourceRoot, '.npm', 'package')); - var npmOk = true; - if (! options.skipNpmUpdate) { - // If this package was previously built with pre-linker versions, it may - // have files directly inside `.npm` instead of nested inside - // `.npm/package`. Clean them up if they are there. - var preLinkerFiles = [ - 'npm-shrinkwrap.json', 'README', '.gitignore', 'node_modules']; - _.each(preLinkerFiles, function (f) { - files.rm_recursive(path.join(self.sourceRoot, '.npm', f)); - }); - - // go through a specialized npm dependencies update process, - // ensuring we don't get new versions of any - // (sub)dependencies. this process also runs mostly safely - // multiple times in parallel (which could happen if you have - // two apps running locally using the same package) - // We run this even if we have no dependencies, because we might - // need to delete dependencies we used to have. - npmOk = meteorNpm.updateDependencies(name, packageNpmDir, - npmDependencies); - } + // If this package was previously built with pre-linker versions, it may + // have files directly inside `.npm` instead of nested inside + // `.npm/package`. Clean them up if they are there. + var preLinkerFiles = [ + 'npm-shrinkwrap.json', 'README', '.gitignore', 'node_modules']; + _.each(preLinkerFiles, function (f) { + files.rm_recursive(path.join(self.sourceRoot, '.npm', f)); + }); + // go through a specialized npm dependencies update process, + // ensuring we don't get new versions of any + // (sub)dependencies. this process also runs mostly safely + // multiple times in parallel (which could happen if you have + // two apps running locally using the same package) + // We run this even if we have no dependencies, because we might + // need to delete dependencies we used to have. var nodeModulesPath = null; - if (npmOk) { + if (meteorNpm.updateDependencies(name, packageNpmDir, npmDependencies)) { nodeModulesPath = path.join(packageNpmDir, 'node_modules'); if (! meteorNpm.dependenciesArePortable(packageNpmDir)) isPortable = false; } // Create slices - var nativeArch = isPortable ? "native" : archinfo.host(); + var osArch = isPortable ? "os" : archinfo.host(); _.each(["use", "test"], function (role) { - _.each(["browser", nativeArch], function (arch) { + _.each(["browser", osArch], function (arch) { var where = (arch === "browser") ? "client" : "server"; // Everything depends on the package 'meteor', which sets up @@ -1670,10 +1702,12 @@ _.extend(Package.prototype, { uses[role][where].unshift({ spec: "meteor" }); } - // We need to create a separate (non ===) copy of - // dependencyInfo for each slice. - var dependencyInfo = { files: {}, directories: {} }; - dependencyInfo.files[packageJsPath] = packageJsHash; + // Each slice has its own separate WatchSet. This is so that, eg, a test + // slice's dependencies doesn't end up getting merged into the + // pluginWatchSet of a package that uses it: only the use slice's + // dependencies need to go there! + var watchSet = new watch.WatchSet(); + watchSet.addFile(packageJsPath, packageJsHash); self.slices.push(new Slice(self, { name: ({ use: "main", test: "tests" })[role], @@ -1681,21 +1715,17 @@ _.extend(Package.prototype, { uses: uses[role][where], implies: role === "use" && implies[where] || undefined, getSourcesFunc: function () { return sources[role][where]; }, - forceExport: forceExport[role][where], - dependencyInfo: dependencyInfo, - nodeModulesPath: arch === nativeArch && nodeModulesPath || undefined, - staticDirectory: self.sourceRoot, - // test slices don't get used by other packages, so they have nothing - // to export. (And notably, they should NOT stomp on the Package.foo - // object defined by their corresponding use slice.) - noExports: role === "test" + noExports: role === "test", + declaredExports: role === "use" ? exports[where] : null, + watchSet: watchSet, + nodeModulesPath: arch === osArch && nodeModulesPath || undefined })); }); }); // Default slices - self.defaultSlices = { browser: ['main'], 'native': ['main'] }; - self.testSlices = { browser: ['tests'], 'native': ['tests'] }; + self.defaultSlices = { browser: ['main'], 'os': ['main'] }; + self.testSlices = { browser: ['tests'], 'os': ['tests'] }; }, // Initialize a package from a legacy-style application directory @@ -1711,16 +1741,8 @@ _.extend(Package.prototype, { _.each(["client", "server"], function (sliceName) { // Determine used packages - var names = _.union( - // standard client packages for the classic meteor stack. - // XXX remove and make everyone explicitly declare all dependencies - ['meteor', 'webapp', 'logging', 'deps', 'session', - 'livedata', 'mongo-livedata', 'ui', 'spacebars', - 'templating', 'startup', - 'past', 'check'], - project.get_packages(appDir)); - - var arch = sliceName === "server" ? "native" : "browser"; + var names = project.get_packages(appDir); + var arch = sliceName === "server" ? "os" : "browser"; // Create slice var slice = new Slice(self, { @@ -1733,82 +1755,77 @@ _.extend(Package.prototype, { self.slices.push(slice); // Watch control files for changes - // XXX this read has a race with the actual read that is used + // XXX this read has a race with the actual reads that are used _.each([path.join(appDir, '.meteor', 'packages'), - path.join(appDir, '.meteor', 'releases')], function (p) { - if (fs.existsSync(p)) { - slice.dependencyInfo.files[p] = - Builder.sha1(fs.readFileSync(p)); - } + path.join(appDir, '.meteor', 'release')], function (p) { + watch.readAndWatchFile(slice.watchSet, p); }); // Determine source files slice.getSourcesFunc = function () { - var allSources = scanForSources( - self.sourceRoot, slice.registeredExtensions(), - ignoreFiles || []); + var sourceInclude = _.map(slice.registeredExtensions(), function (ext) { + return new RegExp('\\.' + quotemeta(ext) + '$'); + }); + var sourceExclude = [/^\./].concat(ignoreFiles); - var withoutAppPackages = _.reject(allSources, function (sourcePath) { - // Skip files that are in app packages; they'll get watched if they - // are actually listed in the .meteor/packages file. (Directories - // named "packages" lower in the tree are OK.) - return sourcePath.match(/^packages\//); + // Wrapper around watch.readAndWatchDirectory which takes in and returns + // sourceRoot-relative directories. + var readAndWatchDirectory = function (relDir, filters) { + filters = filters || {}; + var absPath = path.join(self.sourceRoot, relDir); + var contents = watch.readAndWatchDirectory(slice.watchSet, { + absPath: absPath, + include: filters.include, + exclude: filters.exclude + }); + return _.map(contents, function (x) { + return path.join(relDir, x); + }); + }; + + // Read top-level source files. + var sources = readAndWatchDirectory('', { + include: sourceInclude, + exclude: sourceExclude }); - var otherSliceName = (sliceName === "server") ? "client" : "server"; - var withoutOtherSlice = - _.reject(withoutAppPackages, function (sourcePath) { - return (path.sep + sourcePath + path.sep).indexOf( - path.sep + otherSliceName + path.sep) !== -1; - }); + var otherSliceRegExp = + (sliceName === "server" ? /^client\/$/ : /^server\/$/); - var tests = false; /* for now */ - var withoutOtherRole = - _.reject(withoutOtherSlice, function (sourcePath) { - var isTest = - ((path.sep + sourcePath + path.sep).indexOf( - path.sep + 'tests' + path.sep) !== -1); - return isTest !== (!!tests); - }); + // Read top-level subdirectories. Ignore subdirectories that have + // special handling. + var sourceDirectories = readAndWatchDirectory('', { + include: [/\/$/], + exclude: [/^packages\/$/, /^programs\/$/, /^tests\/$/, + /^public\/$/, /^private\/$/, + otherSliceRegExp].concat(sourceExclude) + }); - var withoutOtherPrograms = - _.reject(withoutOtherRole, function (sourcePath) { - return !! sourcePath.match(/^programs\//); - }); + // XXX avoid infinite recursion with bad symlinks + while (!_.isEmpty(sourceDirectories)) { + var dir = sourceDirectories.shift(); + // remove trailing slash + dir = dir.substr(0, dir.length - 1); - // XXX Add directory dependencies to slice at the time that - // getSourcesFunc is called. This is kind of a hack but it'll - // do for the moment. + // Find source files in this directory. + Array.prototype.push.apply(sources, readAndWatchDirectory(dir, { + include: sourceInclude, + exclude: sourceExclude + })); - // XXX nothing here monitors for the no-default-targets file + // Find sub-sourceDirectories. Note that we DON'T need to ignore the + // directory names that are only special at the top level. + Array.prototype.push.apply(sourceDirectories, readAndWatchDirectory(dir, { + include: [/\/$/], + exclude: [/^tests\/$/, otherSliceRegExp].concat(sourceExclude) + })); + } - // Directories to monitor for new files - var appIgnores = _.clone(ignoreFiles); - slice.dependencyInfo.directories[appDir] = { - include: _.map(slice.registeredExtensions(), function (ext) { - return new RegExp('\\.' + ext + "$"); - }), - // XXX This excludes watching under *ANY* packages or programs - // directory, but we should really only care about top-level ones. - // But watcher doesn't let you do that. - exclude: ignoreFiles.concat([/^packages$/, /^programs$/, - /^tests$/]) - }; - - // Inside the programs directory, only look for new program (which we - // can detect by the appearance of a package.js file.) Other than that, - // programs explicitly call out the files they use. - slice.dependencyInfo.directories[path.resolve(appDir, 'programs')] = { - include: [ /^package\.js$/ ], - exclude: ignoreFiles - }; - - // Exclude .meteor/local and everything under it. - slice.dependencyInfo.directories[ - path.resolve(appDir, '.meteor', 'local')] = { exclude: [/.?/] }; + // We've found all the source files. Sort them! + sources.sort(files.sort); // Convert into relPath/fileOptions objects. - return _.map(withoutOtherPrograms, function (relPath) { + sources = _.map(sources, function (relPath) { var sourceObj = {relPath: relPath}; // Special case: on the client, JavaScript files in a @@ -1821,10 +1838,52 @@ _.extend(Package.prototype, { } return sourceObj; }); + + // Now look for assets for this slice. + var assetDir = sliceName === "client" ? "public" : "private"; + var assetDirs = readAndWatchDirectory('', { + 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)); + + while (!_.isEmpty(assetDirs)) { + dir = assetDirs.shift(); + // remove trailing slash + dir = dir.substr(0, dir.length - 1); + + // Find asset files in this directory. + var assetsAndSubdirs = readAndWatchDirectory(dir, { + include: [/.?/], + // we DO look under dot directories here + exclude: ignoreFiles + }); + + _.each(assetsAndSubdirs, function (item) { + if (item[item.length - 1] === '/') { + // Recurse on this directory. + assetDirs.push(item); + } else { + // This file is an asset. + sources.push({ + relPath: item, + fileOptions: { + isAsset: true + } + }); + } + }); + } + } + + return sources; }; }); - self.defaultSlices = { browser: ['client'], 'native': ['server'] }; + self.defaultSlices = { browser: ['client'], 'os': ['server'] }; }, // Initialize a package from a prebuilt Unipackage on disk. On @@ -1861,23 +1920,31 @@ _.extend(Package.prototype, { // XXX should comprehensively sanitize (eg, typecheck) everything // read from json files - // Read the dependency info (if present), and make the strings - // back into regexps - var dependencies = buildInfoJson.dependencies || - { files: {}, directories: {} }; - _.each(dependencies.directories, function (d) { - _.each(["include", "exclude"], function (k) { - d[k] = _.map(d[k], function (s) { - return new RegExp(s); - }); - }); + // Read the watch sets for each slice; keep them separate (for passing to + // the Slice constructor below) as well as merging them into one big + // WatchSet. + var mergedWatchSet = new watch.WatchSet(); + var sliceWatchSets = {}; + _.each(buildInfoJson.sliceDependencies, function (watchSetJSON, sliceTag) { + var watchSet = watch.WatchSet.fromJSON(watchSetJSON); + mergedWatchSet.merge(watchSet); + sliceWatchSets[sliceTag] = watchSet; }); + // We do NOT put this (or anything!) onto self until we've passed the + // onlyIfUpToDate check. + var pluginWatchSet = watch.WatchSet.fromJSON( + buildInfoJson.pluginDependencies); + // This might be redundant (since pluginWatchSet was probably merged into + // each slice watchSet when it was built) but shouldn't hurt. + mergedWatchSet.merge(pluginWatchSet); + var pluginProviderPackageDirs = buildInfoJson.pluginProviderPackages || {}; + // If we're supposed to check the dependencies, go ahead and do so if (options.onlyIfUpToDate) { // Do we think we'll generate different contents than the tool that built // this package? - if (buildInfoJson.builtBy !== exports.BUILD_VERSION) + if (buildInfoJson.builtBy !== exports.BUILT_BY) return false; if (options.buildOfPath && @@ -1891,17 +1958,7 @@ _.extend(Package.prototype, { return false; } - var isUpToDate = true; - var watcher = new watch.Watcher({ - files: dependencies.files, - directories: dependencies.directories, - onChange: function () { - isUpToDate = false; - } - }); - watcher.stop(); - - if (! isUpToDate) + if (! self.checkUpToDate(mergedWatchSet, pluginProviderPackageDirs)) return false; } @@ -1912,6 +1969,8 @@ _.extend(Package.prototype, { }; self.defaultSlices = mainJson.defaultSlices; self.testSlices = mainJson.testSlices; + self.pluginWatchSet = pluginWatchSet; + self.pluginProviderPackageDirs = pluginProviderPackageDirs; _.each(mainJson.plugins, function (pluginMeta) { rejectBadPath(pluginMeta.path); @@ -1956,14 +2015,10 @@ _.extend(Package.prototype, { nodeModulesPath = path.join(sliceBasePath, sliceJson.node_modules); } - var staticDirectory = null; - if (sliceJson.staticDirectory) - staticDirectory = path.join(sliceBasePath, sliceJson.staticDirectory); - var slice = new Slice(self, { name: sliceMeta.name, arch: sliceMeta.arch, - dependencyInfo: dependencies, + watchSet: sliceWatchSets[sliceMeta.path], nodeModulesPath: nodeModulesPath, uses: _.map(sliceJson.uses, function (u) { return { @@ -1976,29 +2031,34 @@ _.extend(Package.prototype, { return { spec: u['package'] + (u.slice ? "." + u.slice : "") }; - }), - staticDirectory: staticDirectory + }) }); slice.isBuilt = true; - slice.exports = sliceJson.exports || []; - slice.packageScopeVariables = sliceJson.packageScopeVariables || []; + slice.noExports = !!sliceJson.noExports; + slice.packageVariables = sliceJson.packageVariables || []; slice.prelinkFiles = []; slice.resources = []; _.each(sliceJson.resources, function (resource) { rejectBadPath(resource.file); - var fd = fs.openSync(path.join(sliceBasePath, resource.file), "r"); - try { - var data = new Buffer(resource.length); - var count = fs.readSync( - fd, data, 0, resource.length, resource.offset); - } finally { - fs.closeSync(fd); + var data = new Buffer(resource.length); + // Read the data from disk, if it is non-empty. Avoid doing IO for empty + // files, because (a) unnecessary and (b) fs.readSync with length 0 + // throws instead of acting like POSIX read: + // https://github.com/joyent/node/issues/5685 + if (resource.length > 0) { + var fd = fs.openSync(path.join(sliceBasePath, resource.file), "r"); + try { + var count = fs.readSync( + fd, data, 0, resource.length, resource.offset); + } finally { + fs.closeSync(fd); + } + if (count !== resource.length) + throw new Error("couldn't read entire resource"); } - if (count !== resource.length) - throw new Error("couldn't read entire resource"); if (resource.type === "prelink") { var prelinkFile = { @@ -2011,12 +2071,13 @@ _.extend(Package.prototype, { path.join(sliceBasePath, resource.sourceMap), 'utf8'); } slice.prelinkFiles.push(prelinkFile); - } else if (_.contains(["head", "body", "css", "js", "static"], + } else if (_.contains(["head", "body", "css", "js", "asset"], resource.type)) { slice.resources.push({ type: resource.type, data: data, - servePath: resource.servePath || undefined + servePath: resource.servePath || undefined, + path: resource.path || undefined }); } else throw new Error("bad resource type in unipackage: " + @@ -2030,6 +2091,42 @@ _.extend(Package.prototype, { return true; }, + // Try to check if this package is up-to-date (that is, whether its source + // files have been modified.) True if we have dependency info and it says that + // the package is up-to-date. False if a source file has changed. + // + // The arguments _watchSet and _pluginProviderPackageDirs are used when + // reading from disk when there are no slices yet; don't pass them from + // outside this file. + checkUpToDate: function (_watchSet, _pluginProviderPackageDirs) { + var self = this; + + if (!_watchSet) { + // This call was on an already-fully-loaded Package and we just want to + // see if it's changed. So we have some watchSets inside ourselves. + _watchSet = new watch.WatchSet(); + _watchSet.merge(self.pluginWatchSet); + _.each(self.slices, function (slice) { + _watchSet.merge(slice.watchSet); + }); + } + if (!_pluginProviderPackageDirs) { + _pluginProviderPackageDirs = self.pluginProviderPackageDirs; + } + + // Are all of the packages we directly use (which can provide plugins which + // affect compilation) resolving to the same directory? (eg, have we updated + // our release version to something with a new version of a package?) + var packageResolutionsSame = _.all( + _pluginProviderPackageDirs, function (packageDir, name) { + return self.library.findPackageDirectory(name) === packageDir; + }); + if (!packageResolutionsSame) + return false; + + return watch.isUpToDate(_watchSet); + }, + // True if this package can be saved as a unipackage canBeSavedAsUnipackage: function () { var self = this; @@ -2044,11 +2141,15 @@ _.extend(Package.prototype, { // then modified. saveAsUnipackage: function (outputPath, options) { var self = this; - var builder = new Builder({ outputPath: outputPath }); + + if (!self.pluginsBuilt || !self.slicesBuilt) + throw new Error("Unbuilt packages cannot be saved"); if (! self.canBeSavedAsUnipackage()) throw new Error("This package can not yet be saved as a unipackage"); + var builder = new Builder({ outputPath: outputPath }); + try { var mainJson = { @@ -2061,9 +2162,16 @@ _.extend(Package.prototype, { plugins: [] }; + // Note: The contents of buildInfoJson (with the root directory of the + // Meteor checkout naively deleted) gets its SHA taken to determine the + // built package's warehouse version. So it should not contain + // platform-dependent data and should contain all sources of change to the + // unipackage's output. See scripts/admin/build-package-tarballs.sh. var buildInfoJson = { - builtBy: exports.BUILD_VERSION, - dependencies: { files: {}, directories: {} }, + builtBy: exports.BUILT_BY, + sliceDependencies: { }, + pluginDependencies: self.pluginWatchSet.toJSON(), + pluginProviderPackages: self.pluginProviderPackageDirs, source: options.buildOfPath || undefined }; @@ -2112,18 +2220,16 @@ _.extend(Package.prototype, { path: sliceJsonFile }); - // Merge slice dependencies - // XXX is naive merge sufficient here? - _.extend(buildInfoJson.dependencies.files, - slice.dependencyInfo.files); - _.extend(buildInfoJson.dependencies.directories, - slice.dependencyInfo.directories); + // Save slice dependencies. Keyed by the json path rather than thinking + // too hard about how to encode pair (name, arch). + buildInfoJson.sliceDependencies[sliceJsonFile] = + slice.watchSet.toJSON(); // Construct slice metadata var sliceJson = { format: "unipackage-slice-pre1", - exports: slice.exports, - packageScopeVariables: slice.packageScopeVariables, + noExports: slice.noExports || undefined, + packageVariables: slice.packageVariables, uses: _.map(slice.uses, function (u) { var specParts = u.spec.split('.'); if (specParts.length > 2) @@ -2146,8 +2252,7 @@ _.extend(Package.prototype, { }; })), node_modules: slice.nodeModulesPath ? 'npm/node_modules' : undefined, - resources: [], - staticDirectory: path.join(sliceDir, self.serveRoot) + resources: [] }; // Output 'head', 'body' resources nicely @@ -2191,7 +2296,8 @@ _.extend(Package.prototype, { { data: resource.data }), length: resource.data.length, offset: 0, - servePath: resource.servePath || undefined + servePath: resource.servePath || undefined, + path: resource.path || undefined }); }); @@ -2223,8 +2329,7 @@ _.extend(Package.prototype, { if (slice.nodeModulesPath) { builder.copyDirectory({ from: slice.nodeModulesPath, - to: 'npm/node_modules', - depend: false + to: 'npm/node_modules' }); } @@ -2245,16 +2350,6 @@ _.extend(Package.prototype, { }); }); - // Prep dependencies for serialization by turning regexps into - // strings - _.each(buildInfoJson.dependencies.directories, function (d) { - _.each(["include", "exclude"], function (k) { - d[k] = _.map(d[k], function (r) { - return r.source; - }); - }); - }); - builder.writeJson("unipackage.json", mainJson); builder.writeJson("buildinfo.json", buildInfoJson); builder.complete(); diff --git a/tools/project.js b/tools/project.js index d077ef0fa7..b3809f887e 100644 --- a/tools/project.js +++ b/tools/project.js @@ -79,6 +79,8 @@ _.extend(exports, { // detail: if the file starts with a comment, try to keep a single // blank line after the comment (unless the user removes it) var current = project.get_packages(app_dir); + if (_.contains(current, name)) + return; if (!current.length && lines.length) lines.push(''); lines.push(name); diff --git a/tools/run.js b/tools/run.js index 00c4553a9e..6982cb9f2a 100644 --- a/tools/run.js +++ b/tools/run.js @@ -405,6 +405,29 @@ exports.run = function (context, options) { ("mongodb://127.0.0.1:" + mongoPort + "/meteor"); var firstRun = true; + // node-http-proxy doesn't properly handle errors if it has a problem writing + // to the proxy target. While we try to not proxy requests when we don't think + // the target is listening, there are race conditions here, and in any case + // those attempts don't take effect for pre-existing websocket connections. + // Error handling in node-http-proxy is really convoluted and will change with + // their ongoing Node 0.10.x compatible rewrite, so rather than trying to + // debug and send pull request now, we'll wait for them to finish their + // rewrite. In the meantime, ignore two common exceptions that we sometimes + // see instead of crashing. + // + // See https://github.com/meteor/meteor/issues/513 + // + // That bug is about "meteor deploy"s use of http-proxy, but it also affects + // our use here; see + // https://groups.google.com/d/msg/meteor-core/JgbnfKEa5lA/FJHZtJftfSsJ + // + // XXX remove this once we've upgraded and fixed http-proxy + process.on('uncaughtException', function (e) { + if (e && (e.errno === 'EPIPE' || e.message === "This socket is closed.")) + return; + throw e; + }); + var serverHandle; var watcher; @@ -448,7 +471,10 @@ exports.run = function (context, options) { library: context.library }; - var startWatching = function (dependencyInfo) { + var startWatching = function (watchSet) { + if (process.env.METEOR_DEBUG_WATCHSET) + console.log(JSON.stringify(watchSet, null, 2)); + if (!Status.shouldRestart) return; @@ -456,13 +482,12 @@ exports.run = function (context, options) { watcher.stop(); watcher = new watch.Watcher({ - files: dependencyInfo.files, - directories: dependencyInfo.directories, + watchSet: watchSet, onChange: function () { if (Status.crashing) logToClients({'system': "=> Modified -- restarting."}); Status.reset(); - context.library.refresh(); // pick up changes to packages + context.library.refresh(true); // pick up changes to packages restartServer(); } }); @@ -473,8 +498,14 @@ exports.run = function (context, options) { var restartServer = inFiber(function () { Status.running = false; Status.listening = false; - if (serverHandle) + if (watcher) { + watcher.stop(); + watcher = null; + } + if (serverHandle) { killServer(serverHandle); + serverHandle = null; + } // If the user did not specify a --release on the command line, and // simultaneously runs `meteor update` during this run, just exit and let @@ -501,12 +532,12 @@ exports.run = function (context, options) { // Bundle up the app var bundleResult = bundler.bundle(context.appDir, bundlePath, bundleOpts); - var dependencyInfo = bundleResult.dependencyInfo; + var watchSet = bundleResult.watchSet; if (bundleResult.errors) { logToClients({stdout: "=> Errors prevented startup:\n\n" + bundleResult.errors.formatMessages() + "\n"}); Status.hardCrashed("has errors"); - startWatching(dependencyInfo); + startWatching(watchSet); return; } @@ -523,33 +554,9 @@ exports.run = function (context, options) { Builder.sha1(fs.readFileSync(options.settingsFile, "utf8")); // Reload if the setting file changes - dependencyInfo.files[path.resolve(options.settingsFile)] = - settingsHash; + watchSet.addFile(path.resolve(options.settingsFile), settingsHash); } - // If using a warehouse, don't do dependency monitoring on any of - // the files that are in the warehouse. You should not be editing - // those files directly. - if (files.usesWarehouse()) { - var warehouseDir = path.resolve(warehouse.getWarehouseDir()); - var filterKeys = function (obj) { - _.each(_.keys(obj), function (k) { - k = path.resolve(k); - if (warehouseDir.length <= k.length && - k.substr(0, warehouseDir.length) === warehouseDir) - delete obj[k]; - }); - }; - filterKeys(dependencyInfo.files); - filterKeys(dependencyInfo.directories); - } - - // Start watching for changes for files. There's no hurry to call - // this, since dependencyInfo contains a snapshot of the state of - // the world at the time of bundling, in the form of hashes and - // lists of matching files in each directory. - startWatching(dependencyInfo); - // Start the server Status.running = true; @@ -600,6 +607,12 @@ exports.run = function (context, options) { settings: settings, program: options.program }); + + // Start watching for changes for files. There's no hurry to call + // this, since watchSet contains a snapshot of the state of + // the world at the time of bundling, in the form of hashes and + // lists of matching files in each directory. + startWatching(watchSet); }); var mongoErrorCount = 0; diff --git a/tools/server/boot.js b/tools/server/boot.js index 57fa52fd90..f2381f273f 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -25,7 +25,7 @@ __meteor_bootstrap__ = { startup_hooks: [], serverDir: serverDir, configJson: configJson }; -__meteor_runtime_config__ = { meteorRelease: configJson.release }; +__meteor_runtime_config__ = { meteorRelease: configJson.meteorRelease }; // connect (and some other NPM modules) use $NODE_ENV to make some decisions; @@ -108,25 +108,31 @@ Fiber(function () { } } }; - var staticDirectory = path.resolve(serverDir, fileInfo.staticDirectory); var getAsset = function (assetPath, encoding, callback) { var fut; if (! callback) { fut = new Future(); callback = fut.resolver(); } - var _callback = Meteor.bindEnvironment(function (err, result) { + // This assumes that we've already loaded the meteor package, so meteor + // itself (and weird special cases like js-analyze) can't call + // Assets.get*. (We could change this function so that it doesn't call + // bindEnvironment if you don't pass a callback if we need to.) + var _callback = Package.meteor.Meteor.bindEnvironment(function (err, result) { if (result && ! encoding) // Sadly, this copies in Node 0.10. result = new Uint8Array(result); callback(err, result); }, function (e) { - Meteor._debug("Exception in callback of getAsset", e.stack); + console.log("Exception in callback of getAsset", e.stack); }); - var filePath = path.join(staticDirectory, assetPath); - if (filePath.indexOf("..") !== -1) - throw new Error(".. is not allowed in asset paths."); - fs.readFile(filePath, encoding, _callback); + + if (!fileInfo.assets || !_.has(fileInfo.assets, assetPath)) { + _callback(new Error("Unknown asset: " + assetPath)); + } else { + var filePath = path.join(serverDir, fileInfo.assets[assetPath]); + fs.readFile(filePath, encoding, _callback); + } if (fut) return fut.wait(); }; diff --git a/tools/skel/.meteor/packages b/tools/skel/.meteor/packages index 2ca3c152a4..240f048420 100644 --- a/tools/skel/.meteor/packages +++ b/tools/skel/.meteor/packages @@ -3,6 +3,7 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. +standard-app-packages autopublish insecure preserve-inputs diff --git a/tools/test-runner-app/.meteor/packages b/tools/test-runner-app/.meteor/packages index 69f1bac0f6..006a126776 100644 --- a/tools/test-runner-app/.meteor/packages +++ b/tools/test-runner-app/.meteor/packages @@ -1,2 +1,4 @@ -# This file intentionally left blank. (A driver package will be added by "meteor -# test-packages".) +# In addition to the standard app packages, a driver package will be added by +# "meteor test-packages". + +standard-app-packages diff --git a/tools/tests/app-with-package/.meteor/packages b/tools/tests/app-with-package/.meteor/packages index 8eca235559..da83992817 100644 --- a/tools/tests/app-with-package/.meteor/packages +++ b/tools/tests/app-with-package/.meteor/packages @@ -1 +1,2 @@ -test-package \ No newline at end of file +test-package +standard-app-packages diff --git a/tools/tests/app-with-private/.meteor/packages b/tools/tests/app-with-private/.meteor/packages index 2670629f0c..ab08350a86 100644 --- a/tools/tests/app-with-private/.meteor/packages +++ b/tools/tests/app-with-private/.meteor/packages @@ -7,3 +7,4 @@ autopublish insecure preserve-inputs test-package +standard-app-packages diff --git a/tools/tests/app-with-private/packages/test-package/package.js b/tools/tests/app-with-private/packages/test-package/package.js index 6ee9466b04..e273422703 100644 --- a/tools/tests/app-with-private/packages/test-package/package.js +++ b/tools/tests/app-with-private/packages/test-package/package.js @@ -5,5 +5,6 @@ Package._transitional_registerBuildPlugin({ }); Package.on_use(function (api) { + api.export('TestAsset', 'server'); api.add_files(['test-package.js', 'test-package.txt', 'test.notregistered'], 'server'); }); diff --git a/tools/tests/app-with-private/packages/test-package/test-package.js b/tools/tests/app-with-private/packages/test-package/test-package.js index 77c1461c19..9ad080938a 100644 --- a/tools/tests/app-with-private/packages/test-package/test-package.js +++ b/tools/tests/app-with-private/packages/test-package/test-package.js @@ -1,4 +1,3 @@ -// @export TestAsset TestAsset = {}; if (Meteor.isServer) { diff --git a/tools/tests/app-with-public/.meteor/packages b/tools/tests/app-with-public/.meteor/packages index 2ca3c152a4..4cd1dcb2d5 100644 --- a/tools/tests/app-with-public/.meteor/packages +++ b/tools/tests/app-with-public/.meteor/packages @@ -6,3 +6,4 @@ autopublish insecure preserve-inputs +standard-app-packages diff --git a/tools/tests/empty-app/.meteor/packages b/tools/tests/empty-app/.meteor/packages index 9c8d080e06..25c1e7842a 100644 --- a/tools/tests/empty-app/.meteor/packages +++ b/tools/tests/empty-app/.meteor/packages @@ -1 +1,3 @@ -# no packages \ No newline at end of file +# no packages + +standard-app-packages diff --git a/tools/tests/test_bundler_assets.js b/tools/tests/test_bundler_assets.js index c99865df4a..e3771bf089 100644 --- a/tools/tests/test_bundler_assets.js +++ b/tools/tests/test_bundler_assets.js @@ -57,28 +57,25 @@ assert.doesNotThrow(function () { "program.json") ) ); - var staticDir; + var testTxtPath; + var nestedTxtPath; var packageTxtPath; var unregisteredExtensionPath; _.each(serverManifest.load, function (item) { if (item.path === "packages/test-package.js") { - packageTxtPath = path.join(tmpOutputDir, - "programs", "server", - item.staticDirectory, "test-package.txt"); - unregisteredExtensionPath = path.join(tmpOutputDir, - "programs", "server", - item.staticDirectory, - "test.notregistered"); + packageTxtPath = path.join( + tmpOutputDir, "programs", "server", item.assets['test-package.txt']); + unregisteredExtensionPath = path.join( + tmpOutputDir, "programs", "server", item.assets["test.notregistered"]); } if (item.path === "app/test.js") { - staticDir = path.join(tmpOutputDir, - "programs", "server", - item.staticDirectory); + testTxtPath = path.join( + tmpOutputDir, "programs", "server", item.assets['test.txt']); + nestedTxtPath = path.join( + tmpOutputDir, "programs", "server", item.assets["nested/test.txt"]); } }); // check that the files are where the manifest says they are - var testTxtPath = path.join(staticDir, "test.txt"); - var nestedTxtPath = path.join(staticDir, "nested", "test.txt"); assert.strictEqual(result.errors, false, result.errors && result.errors[0]); assert(fs.existsSync(testTxtPath)); assert(fs.existsSync(nestedTxtPath)); diff --git a/tools/tests/test_watch.js b/tools/tests/test_watch.js index 19f042d2b3..b30d6dd8c4 100644 --- a/tools/tests/test_watch.js +++ b/tools/tests/test_watch.js @@ -41,10 +41,11 @@ var go = function (options) { } fired = false; - var files = {}; + var watchSet = new watch.WatchSet(); + _.each(options.files, function (value, file) { file = path.join(tmp, file); - if (typeof value !== "string") { + if (value !== null && typeof value !== "string") { if (fs.existsSync(file)) { var hash = crypto.createHash('sha1'); hash.update(fs.readFileSync(file)); @@ -53,18 +54,23 @@ var go = function (options) { value = 'dummyhash'; } } - files[file] = value; + watchSet.addFile(file, value); }); - var directories = {}; - _.each(options.directories, function (options, dir) { - dir = path.join(tmp, dir); - directories[dir] = options; + _.each(options.directories, function (dir) { + // don't mutate options.directories, since we may reuse it with a no-arg + // go() call + var realDir = { + absPath: path.join(tmp, dir.absPath), + include: dir.include, + exclude: dir.exclude + }; + realDir.contents = dir.contents || watch.readDirectory(realDir); + watchSet.addDirectory(realDir); }); theWatcher = new watch.Watcher({ - files: files, - directories: directories, + watchSet: watchSet, onChange: function () { fired = true; if (firedFuture) @@ -97,11 +103,7 @@ var waitForTopOfSecond = function () { if (msPastSecond < 100) { return; } - var f = new Future; - setTimeout(function () { - f.return(); - }, 25); - f.wait(); + delay(25); } }; @@ -139,15 +141,30 @@ Fiber(function () { files: { '/aa/b': true, '/aa/c': true } }); assert(fires()); // look like /aa/c was removed + go({ + files: { '/aa/b': true, '/aa/c': null } + }); + assert(!fires()); // assert that /aa/c doesn't exist console.log("... directories"); go({ files: {'/aa/b': true }, - directories: {'/aa': { - include: [/yes/, /maybe/, /aa/], - exclude: [/not/, /never/] - }} + directories: [ + {absPath: '/aa', + include: [/yes/, /maybe/, /aa/], + exclude: [/not/, /never/], + contents: [] + }, + {absPath: '/bb', + include: [/.?/], + contents: [] + } + ] }); + assert(fires()); // because /bb doesn't exist + touchDir('/bb'); + go(); + assert(!fires()); touchFile('/aa/c'); assert(!fires()); touchFile('/aa/maybe-not'); @@ -158,9 +175,10 @@ Fiber(function () { assert(!fires()); touchFile('/aa/yes-for-sure'); assert(fires()); + go(); touchFile('/aa/nope'); - assert(fires()); // because yes-for-sure isn't in the file list + assert(fires()); // because yes-for-sure isn't in 'contents' remove('/aa/yes-for-sure'); go(); assert(!fires()); @@ -169,11 +187,18 @@ Fiber(function () { go(); assert(fires()); // maybe-this-time is still there go({ - files: {'/aa/b': true, '/aa/maybe-this-time': true }, - directories: {'/aa': { - include: [/yes/, /maybe/, /aa/], - exclude: [/not/, /never/] - }} + files: {'/aa/b': true}, + directories: [ + {absPath: '/aa', + include: [/yes/, /maybe/, /aa/], + exclude: [/not/, /never/], + contents: ['maybe-this-time'] + }, + {absPath: '/bb', + include: [/.?/], + contents: [] + } + ] }); go(); assert(!fires()); // maybe-this-time is now in the expected file list @@ -183,84 +208,62 @@ Fiber(function () { remove('/aa/maybe-this-time'); go(); assert(fires()); // maybe-this-time is missing - - console.log("... recursive directories"); - touchFile('/aa/b'); + touchFile('/aa/maybe-this-time'); + touchDir('/aa/yes-i-said-yes-i-will-yes'); go({ - files: {'/aa/b': true }, - directories: {'/aa': { - include: [/yes/, /maybe/, /aa/], - exclude: [/not/, /never/] - }} - }); - touchDir('/aa/yess'); - assert(!fires()); - remove('/aa/yess'); - assert(!fires()); - touchFile('/aa/yess/kitten'); - assert(!fires()); - touchFile('/aa/yess/maybe'); - assert(fires()); - remove('/aa/yess'); - go(); - touchFile('/aa/whatever/kitten'); - assert(!fires()); - touchFile('/aa/whatever/maybe'); - assert(fires()); - - remove('/aa/whatever'); - go(); - touchDir('/aa/i/love/subdirectories'); - assert(!fires()); - touchFile('/aa/i/love/subdirectories/yessir'); - assert(fires()); - remove('/aa/i/love/subdirectories/yessir'); - go(); - touchFile('/aa/i/love/subdirectories/every/day'); - assert(!fires()); - remove('/aa/i/love/subdirectories'); - assert(!fires()); - touchFile('/aa/i/love/not/nothing/yes'); - assert(!fires()); - touchFile('/aa/i/love/not/nothing/maybe/yes'); - assert(!fires()); - touchFile('/aa/i/love/maybe'); - assert(fires()); - remove('/aa/i'); - remove('/aa/whatever'); - - remove('/aa'); - touchFile('/aa/b'); - console.log("... nested directories"); - go({ - files: {'/aa/b': true }, - directories: { - '/aa': { - include: [/yes/, /maybe/, /aa/], - exclude: [/not/, /never/] - }, - '/aa/x': { - include: [/kitten/], - exclude: [/puppy/] + directories: [ + {absPath: '/aa', + include: [/yes/, /maybe/, /aa/], + exclude: [/not/, /never/], + contents: ['maybe-this-time'] } - } + ] + }); + assert(fires()); // yes-i-said-yes-i-will-yes/ is missing + go({ + directories: [ + {absPath: '/aa', + include: [/yes/, /maybe/, /aa/], + exclude: [/not/, /never/], + contents: ['maybe-this-time', 'yes-i-said-yes-i-will-yes'] + } + ] + }); + assert(fires()); // yes-i-said-yes-i-will-yes is a dir, not a file + go({ + directories: [ + {absPath: '/aa', + include: [/yes/, /maybe/, /aa/], + exclude: [/not/, /never/], + contents: ['maybe-this-time', 'yes-i-said-yes-i-will-yes/'] + } + ] }); - touchFile('/aa/kitten'); assert(!fires()); - touchFile('/aa/maybe.puppy'); - assert(fires()); - remove('/aa/maybe.puppy'); - go(); - touchFile('/aa/x/kitten'); - assert(fires()); - remove('/aa/x/kitten'); - go(); - touchFile('/aa/x/yes'); + // same directory, different filters + go({ + directories: [ + // dirs + {absPath: '/aa', + include: [/\/$/], + contents: ['yes-i-said-yes-i-will-yes/'] + }, + // files + {absPath: '/aa', + include: [/.?/], + exclude: [/\/$/], + contents: ['b', 'c', 'maybe-not', 'maybe-this-time', 'never', + 'never-yes', 'nope'] + } + ] + }); assert(!fires()); - touchFile('/aa/x/kitten.not'); + touchFile('/aa/bla'); assert(fires()); - remove('/aa'); + // nb: these are supposed to verify that the "wait a second and try again" + // logic works, but I couldn't get them to fail even when I turned that logic + // off. console.log("... rapid changes to file"); touchFile('/aa/x'); waitForTopOfSecond(); @@ -268,52 +271,24 @@ Fiber(function () { files: {'/aa/x': true }}); touchFile('/aa/x'); assert(fires(2000)); + go({ - directories: { - '/aa': { - include: [/yes/, /maybe/, /aa/], - exclude: [/not/, /never/] + directories: [ + {absPath: '/aa', + include: [/yes/, /maybe/, /aa/], + exclude: [/not/, /never/] } - } + ] }); + assert(!fires()); + waitForTopOfSecond(); - touchFile('/aa/thing1/whatever'); - delay(100); - touchFile('/aa/thing2/yes'); + touchFile('/aa/wtf'); + delay(600); + touchFile('/aa/yes-indeed'); assert(fires(2000)); remove('/aa'); - console.log("... rapid changes to directory"); - touchDir('/aa'); - waitForTopOfSecond(); - go({ - directories: {'/aa': { - include: [/yes/, /maybe/, /aa/], - exclude: [/not/, /never/] - }} - }); - touchFile('/aa/x/yes'); - assert(fires(2000)); - remove('/aa/x'); - - waitForTopOfSecond(); - go(); - delay(600); - touchFile('/aa/x/not'); - delay(600); - touchFile('/aa/x/yes'); - assert(fires(2000)); - remove('/aa/x'); - - touchDir('/aa/x'); - go(); - delay(2000); - waitForTopOfSecond(); - touchFile('/aa/x/no'); - delay(600); - touchFile('/aa/x/yes'); - assert(fires(2000)); - console.log("Watcher test passed"); theWatcher.stop(); diff --git a/tools/unipackage.js b/tools/unipackage.js index 7f45e029fc..ff4d9e5786 100644 --- a/tools/unipackage.js +++ b/tools/unipackage.js @@ -7,9 +7,18 @@ var bundler = require('./bundler.js'); // tools (such as 'meteor'.) The requested packages will be loaded // together will all of their dependencies, and each time you call // this function you load another, distinct copy of all of the -// packages. The return value is an object that maps package name to -// package exports (that is, it is the Package object from inside the -// sandbox created for the newly loaded packages.) +// packages (except see note about caching below.) The return value is +// an object that maps package name to package exports (that is, it is +// the Package object from inside the sandbox created for the newly +// loaded packages.) +// +// Caching: There is a simple cache. If you call this function with +// exactly the same library, release, and packages, we will attempt to +// return the memoized return value from the previous load (rather +// than creating a whole new copy of the packages in memory.) The +// caching logic is not particularly sophisticated. For example, +// whenever you call load() with a different library the cache is +// flushed. // // Options: // - library: The Library to use to retrieve packages and their @@ -22,19 +31,35 @@ var bundler = require('./bundler.js'); // environment) // // Example usage: -// var Meteor = require('./unipackage.js').load({ +// var DDP = require('./unipackage.js').load({ // library: context.library, // packages: ['livedata'], // release: context.releaseVersion -// }).meteor.Meteor; -// var reverse = Meteor.connect('reverse.meteor.com'); +// }).livedata.DDP; +// var reverse = DDP.connect('reverse.meteor.com'); // console.log(reverse.call('reverse', 'hello world')); +var cacheLibrary = null; +var cacheRelease = null; +var cache = null; // map from package names (joined with ',') to return value + var load = function (options) { options = options || {}; if (! (options.library instanceof library.Library)) throw new Error("unipackage.load requires a library"); + // Check the cache first + if (cacheLibrary !== options.library || + cacheRelease !== options.release) { + cacheLibrary = options.library; + cacheRelease = options.release; + cache = {}; + } + var cacheKey = (options.packages || []).join(','); + if (_.has(cache, cacheKey)) { + return cache[cacheKey]; + } + // Set up a minimal server-like environment (omitting the parts that // are specific to the HTTP server.) Kind of a hack. I suspect this // will get refactored before too long. Note that @@ -55,6 +80,9 @@ var load = function (options) { // Run any user startup hooks. _.each(env.__meteor_bootstrap__.startup_hooks, function (x) { x(); }); + // Save to cache + cache[cacheKey] = ret; + return ret; }; diff --git a/tools/upgraders.js b/tools/upgraders.js new file mode 100644 index 0000000000..f26d3b23fe --- /dev/null +++ b/tools/upgraders.js @@ -0,0 +1,70 @@ +var _ = require('underscore'); +var fs = require('fs'); +var path = require('path'); +var project = require('./project.js'); + +// This file implements "upgraders" --- functions which upgrade a Meteor app to +// a new version. Each upgrader has a name (registered in upgradersByName). +// +// You can test upgraders by running "meteor run-upgrader myupgradername". +// +// Upgraders are run automatically by "meteor update" by comparing the +// "upgraders" field of the release JSON file. Add upgraders to the JSON blob in +// scripts/admin/build-release.sh. Upgraders are run in the order found in that +// file, after removing all upgraders in the pre-update release +// manifest. Basically, once an upgrader is added to the list it should stay +// there forever (or at least until we no longer are interested in allowing +// updates from that release). + + +// This upgrader implements two changes made in 0.6.5 as part of the Linker +// project. +// +// First, linker changed how "app packages" (packages in the "packages" +// directory in an app) are treated. Before 0.6.5, all app packages were +// implicitly "use"d by the app. This meant there was no way to have an app +// package that was intended only to be "use" by the test slices of other app +// packages. In 0.6.5, you have to explicitly "meteor add" app packages to +// .meteor/packages in order for them to be used by your app. This upgrader +// adds all existing packages found in the packages/ directory to +// .meteor/packages. (If you had such test helpers, you can remove them +// afterwards.) +// +// Second, linker changed how the standard set of packages used by apps is +// included. Instead of being hard-coded into initFromAppDir, the standard +// packages are "implied" by the new "standard-app-packages" package, which is +// explicitly listed in .meteor/packages. So we need to add +// "standard-app-packages" to .meteor/packages when upgrading. +var addAppPackagesAndStandardAppPackages = function (appDir) { + project.add_package(appDir, 'standard-app-packages'); + + var appPackageDir = path.join(appDir, 'packages'); + try { + var appPackages = fs.readdirSync(appPackageDir); + } catch (e) { + if (!(e && e.code === 'ENOENT')) + throw e; + } + + _.each(appPackages, function (p) { + // We can ignore empty directories, etc. Packages have to have a + // package.js. (In 0.6.5, they can also be built packages with + // unipackage.json... but that surely is irrelevant for this upgrade.) + if (fs.existsSync(path.join(appPackageDir, p, 'package.js'))) + project.add_package(appDir, p); + }); +}; + + +var upgradersByName = { + "app-packages": addAppPackagesAndStandardAppPackages +}; + +exports.runUpgrader = function (upgraderName, appDir) { + // This should only be called from the hidden run-upgrader command or by + // "meteor update" with an upgrader from one of our releases, so it's OK if + // error handling is just an exception. + if (! _.has(upgradersByName, upgraderName)) + throw new Error("Unknown upgrader: " + upgraderName); + upgradersByName[upgraderName](appDir); +}; diff --git a/tools/watch.js b/tools/watch.js index 2af72c0a09..d4b05bdb37 100644 --- a/tools/watch.js +++ b/tools/watch.js @@ -6,86 +6,260 @@ var _ = require('underscore'); // the files change, call a user-provided callback. (If you want a // second callback, you'll need to create a second Watcher.) // +// You describe the structure you want to watch in a WatchSet; you then create a +// Watcher to watch it. Watcher does not mutate WatchSet, so you can create +// several Watchers from the same WatchSet. WatchSet can be easily converted to +// and from JSON for serialization. +// // You can set up two kinds of watches, file and directory watches. // -// In a file watch, you provide an absolute path to a file and a SHA1 -// (encoded as hex) of the contents of that file. If the file ever -// changes so that its contents no longer match that SHA1, the -// callback triggers. +// In a file watch, you provide an absolute path to a file and a SHA1 (encoded +// as hex) of the contents of that file. If the file ever changes so that its +// contents no longer match that SHA1, the callback triggers. You can also +// provide `null` for the SHA1, which means the file should not exist. // -// In a directory watch, you provide an absolute path to a directory -// and two lists of regular expressions specifying the files to -// include or exclude. If there is ever a file in the directory or its -// children that matches the criteria set up by the regular -// expressions, but that IS NOT present as a file watch, then the -// callback triggers. +// In a directory watch, you provide an absolute path to a directory, +// two lists of regular expressions specifying the entries to +// include and exclude, and an array of which entries to expect // -// For directory watches, the regular expressions work as follows. You -// provide two arrays of regular expressions, an include list and an -// exclude list. A file in the directory matches if it matches at -// least one regular expression in the include list, and doesn't match -// any regular expressions in the exclude list. Subdirectories are -// included recursively, as long as their names do not match any -// regular expression in the exclude list. +// For directory watches, the regular expressions work as follows. You provide +// two arrays of regular expressions, an include list and an exclude list. An +// entry in the directory matches if it matches at least one regular expression +// in the include list, and doesn't match any regular expressions in the exclude +// list. The string that is matched against the regular expression ends with a +// '/' if the entry is directory. There is NO IMPLICIT RECURSION here: a +// directory watch ONLY watches the immediate children of the directory! If you +// want a recursive watch, you need to do the recursive walk while building the +// WatchSet and add a bunch of separate directory watches. // -// When multiple directory watches are set up, say on a directory A -// and its subdirectory B, the most specific watch takes precedence in -// each directory. So only B's include/exclude lists will be checked -// in B. +// There can be multiple directory watches on the same directory. There is no +// relationship between the files found in directory watches and the files +// watched by file watches; they are parallel mechanisms. // -// Regular expressions are checked only against individual path -// components (the actual name of the file or the subdirectory), not -// against the entire path. +// Regular expressions are checked only against individual path components (the +// actual name of the file or the subdirectory) plus the trailing '/' for +// directories, not against the entire path. // // You can call stop() to stop watching and tear down the // watcher. Calling stop() guarantees that you will not receive a // callback (if you have not already.) Calling stop() is unnecessary // if you've received a callback. // -// A limitation of the current implementation is that if you set up a -// directory watch on a directory A, and A does not exist at the time -// the Watcher is created but is then created later, then A will not -// be monitored. (Of course, this limitation only applies to the roots -// of the directory watches. If A exists at the time the watch is -// created, and a subdirectory B is later created, it will be properly -// detected. Likewise if A exists and is then deleted it will be -// detected.) -// -// To do a "one-shot" (to see if any files have been modified, -// compared to the dependencies, at a particular point in time, just -// create a Watcher and see if your onChange function was called -// before the Watcher constructor changed. (Then call stop() as -// usual.) -// -// XXX This should be reengineered so that dependency information from -// multiple sources can be easily merged in a generic way. Possibly in -// this new model subdirectories would be allowed in include/exclude -// patterns, and multiple directory rules would be OR'd rather than -// taking the most specific rule. -// -// Options may include -// - files: see self.files comment below -// - directories: see self.directories comment below -// - onChange: the function to call when the first change is detected. -// received one argument, the absolute path to a changed or removed -// file (potentially not the only one that changed or was removed) +// To do a "one-shot" (to see if any files have been modified, compared to the +// dependencies, at a particular point in time), use the isUpToDate function. // +// XXX Symlinks are currently treated transparently: we treat them as the thing +// they point to (ie, as a directory if they point to a directory, as +// nonexistent if they point to something nonexist, etc). Not sure if this is +// correct. + + + +var WatchSet = function () { + var self = this; + + // Set this to true if any Watcher built on this WatchSet must immediately + // fire (eg, if this WatchSet was given two different sha1 for the same file). + self.alwaysFire = false; + + // Map from the absolute path to a file, to a sha1 hash, or null if the file + // should not exist. A Watcher created from this set fires when the file + // changes from that sha, or is deleted (if non-null) or created (if null). + self.files = {}; + + // Array of object with keys: + // - 'absPath': absolute path to a directory + // - 'include': array of RegExps + // - 'exclude': array of RegExps + // - 'contents': array of strings, or null if the directory should not exist + // + // This represents the assertion that 'absPath' is a directory and that + // 'contents' is its immediate contents, as filtered by the regular + // expressions. Entries in 'contents' are file and subdirectory names; + // directory names end with '/'. 'contents' is sorted. An entry is in + // 'contents' if its value (including the slash, for directories) matches at + // least one regular expression in 'include' and no regular expressions in + // 'exclude'. + // + // There is no recursion here: files contained in subdirectories never appear. + // + // A directory may have multiple entries (presumably with different + // include/exclude filters). + self.directories = []; +}; + +_.extend(WatchSet.prototype, { + addFile: function (filePath, hash) { + var self = this; + // No need to update if this is in always-fire mode already. + if (self.alwaysFire) + return; + if (_.has(self.files, filePath)) { + // Redundant? + if (self.files[filePath] === hash) + return; + // Nope, inconsistent. + self.alwaysFire = true; + return; + } + self.files[filePath] = hash; + }, + + // Takes options absPath, include, exclude, and contents, as described + // above. contents does not need to be pre-sorted. + addDirectory: function (options) { + var self = this; + if (self.alwaysFire) + return; + if (_.isEmpty(options.include)) + return; + var contents = _.clone(options.contents); + if (contents) + contents.sort(); + + self.directories.push({ + absPath: options.absPath, + include: options.include, + exclude: options.exclude, + contents: contents + }); + }, + + // Merges another WatchSet into this one. This one will now fire if either + // WatchSet would have fired. + merge: function (other) { + var self = this; + if (self.alwaysFire) + return; + if (other.alwaysFire) { + self.alwaysFire = true; + return; + } + _.each(other.files, function (hash, name) { + self.addFile(name, hash); + }); + _.each(other.directories, function (dir) { + // XXX this doesn't deep-clone the directory, but I think these objects + // are never mutated + self.directories.push(dir); + }); + }, + + toJSON: function () { + var self = this; + if (self.alwaysFire) + return {alwaysFire: true}; + var ret = {files: self.files}; + + var reToJSON = function (r) { + var options = ''; + if (r.ignoreCase) + options += 'i'; + if (r.multiline) + options += 'm'; + if (r.global) + options += 'g'; + if (options) + return {$regex: r.source, $options: options}; + return r.source; + }; + + ret.directories = _.map(self.directories, function (d) { + return { + absPath: d.absPath, + include: _.map(d.include, reToJSON), + exclude: _.map(d.exclude, reToJSON), + contents: d.contents + }; + }); + + return ret; + } +}); + +WatchSet.fromJSON = function (json) { + var set = new WatchSet; + + if (!json) + return set; + + if (json.alwaysFire) { + set.alwaysFire = true; + return set; + } + + set.files = _.clone(json.files); + + var reFromJSON = function (j) { + if (j.$regex) + return new RegExp(j.$regex, j.$options); + return new RegExp(j); + }; + + set.directories = _.map(json.directories, function (d) { + return { + absPath: d.absPath, + include: _.map(d.include, reFromJSON), + exclude: _.map(d.exclude, reFromJSON), + contents: d.contents + }; + }); + + return set; +}; + +var readDirectory = function (options) { + // Read the directory. + try { + var contents = fs.readdirSync(options.absPath); + } catch (e) { + // If the path is not a directory, return null; let other errors through. + if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) + return null; + throw e; + } + + // Add slashes to the end of directories. + var contentsWithSlashes = []; + _.each(contents, function (entry) { + try { + // We do stat instead of lstat here, so that we treat symlinks to + // directories just like directories themselves. + // 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; + } + // XXX if we're on windows, I guess it's possible for files to end with '/'. + if (stats.isDirectory()) + entry += '/'; + contentsWithSlashes.push(entry); + }); + + // Filter based on regexps. + var filtered = _.filter(contentsWithSlashes, function (entry) { + return _.any(options.include, function (re) { + return re.test(entry); + }) && !_.any(options.exclude, function (re) { + return re.test(entry); + }); + }); + + // Sort it! + filtered.sort(); + return filtered; +}; + +// All fields are private. var Watcher = function (options) { var self = this; - // Map from the absolute path to a file, to a sha1 hash. Fire when - // the file changes from that sha. - self.files = options.files || {}; - - // Map from an absolute path to a directory, to an object with keys - // 'include' and 'exclude', both lists of regular expressions. Fire - // when a file is added to that directory whose name matches at - // least one regular expression in 'include' and no regular - // expressions in 'exclude'. Subdirectories are included - // recursively, but not subdirectories that match 'exclude'. The - // most specific rule wins, so you can change the parameters in - // effect in subdirectories simply by specifying additional rules. - self.directories = options.directories || {}; + // The set to watch. + self.watchSet = options.watchSet; + if (! self.watchSet) + throw new Error("watchSet option is required"); // Function to call when a change is detected according to one of // the above. @@ -93,77 +267,135 @@ var Watcher = function (options) { if (! self.onChange) throw new Error("onChange option is required"); - // self.directories in a different form. It's an array of objects, - // each with keys 'dir', 'include', 'options', where path is - // guaranteed to not contain a trailing slash (unless it is the root - // directory) and the objects are sorted from longest path to - // shortest (that is, most specific rule to least specific.) - self.rules = _.map(self.directories, function (options, dir) { - return { - dir: path.resolve(dir), - include: options.include || [], - exclude: options.exclude || [] - }; - }); - self.rules = self.rules.sort(function (a, b) { - return a.dir.length < b.dir.length ? 1 : -1; - }); - self.stopped = false; + self.justCheckOnce = !!options._justCheckOnce; + self.fileWatches = []; // array of paths - self.directoryWatches = {}; // map from path to watch object + self.directoryWatches = []; // array of watch object // We track all of the currently active timers so that we can cancel // them at stop() time. This stops the process from hanging at - // shutdown until all of the timers have fired.. An alternate + // shutdown until all of the timers have fired. An alternate // approach would be to use the unref() timer handle method present // in modern node. var nextTimerId = 1; self.timers = {}; // map from arbitrary number (nextTimerId) to timer handle + // Were we given an inconsistent WatchSet? Fire now and be done with it. + if (self.watchSet.alwaysFire) { + self._fire(); + return; + } + self._startFileWatches(); - _.each(self.rules, function (rule) { - self._watchDirectory(rule.dir); - }); + self._startDirectoryWatches(); }; _.extend(Watcher.prototype, { - _checkFileChanged: function (absPath) { + _fireIfFileChanged: function (absPath) { var self = this; - if (! fs.existsSync(absPath)) + if (self.stopped) return true; - var crypto = require('crypto'); - var hasher = crypto.createHash('sha1'); - hasher.update(fs.readFileSync(absPath)); - var hash = hasher.digest('hex'); + var oldHash = self.watchSet.files[absPath]; - return (self.files[absPath] !== hash); + if (oldHash === undefined) + throw new Error("Checking unknown file " + absPath); + + var contents = readFile(absPath); + + if (contents === null) { + // File does not exist (or is a directory). + // Is this what we expected? + if (oldHash === null) + return false; + // Nope, not what we expected. + self._fire(); + return true; + } + + // File exists! Is that what we expected? + if (oldHash === null) { + self._fire(); + return true; + } + + var newHash = sha1(contents); + + // Unchanged? + if (newHash === oldHash) + return false; + + self._fire(); + return true; + }, + + _fireIfDirectoryChanged: function (info, isDoubleCheck) { + var self = this; + + if (self.stopped) + return true; + + var newContents = exports.readDirectory({ + absPath: info.absPath, + include: info.include, + exclude: info.exclude + }); + + // If the directory has changed (including being deleted or created). + if (!_.isEqual(info.contents, newContents)) { + self._fire(); + return true; + } + + if (!isDoubleCheck && !self.justCheckOnce) { + // Whenever a directory changes, scan it soon as we notice, + // but then scan it again one secord later just to make sure + // that we haven't missed any changes. See commentary at + // #WorkAroundLowPrecisionMtimes + // XXX not sure why this uses a different strategy than files + var timerId = self.nextTimerId++; + self.timers[timerId] = setTimeout(function () { + delete self.timers[timerId]; + if (! self.stopped) + self._fireIfDirectoryChanged(info, true); + }, 1000); + } + + return false; }, _startFileWatches: function () { var self = this; // Set up a watch for each file - _.each(self.files, function (hash, absPath) { + _.each(self.watchSet.files, function (hash, absPath) { + if (self.stopped) + return; + + // Check for the case where by the time we created the watch, + // the file had already changed from the sha we were provided. + if (self._fireIfFileChanged(absPath)) + return; + + if (self.justCheckOnce) + return; + // Intentionally not using fs.watch since it doesn't play well with // vim (https://github.com/joyent/node/issues/3172) // Note that we poll very frequently (500 ms) fs.watchFile(absPath, {interval: 500}, function () { // Fire only if the contents of the file actually changed (eg, // maybe just its atime changed) - if (self._checkFileChanged(absPath)) - self._fire(absPath); + self._fireIfFileChanged(absPath); }); self.fileWatches.push(absPath); - - // Check for the case where by the time we created the watch, - // the file had already changed from the sha we were provided. - if (self._checkFileChanged(absPath)) - self._fire(absPath); }); + if (self.stopped || self.justCheckOnce) + return; + // One second later, check the files again, because fs.watchFile // is actually implemented by polling the file's mtime, and some // filesystems (OSX HFS+) only keep mtimes to a resolution of one @@ -174,147 +406,57 @@ _.extend(Watcher.prototype, { var timerId = self.nextTimerId++; self.timers[timerId] = setTimeout(function () { delete self.timers[timerId]; - if (self.stopped) - return; - - _.each(self.files, function (hash, absPath) { - if (self._checkFileChanged(absPath)) - self._fire(absPath); + _.each(self.watchSet.files, function (hash, absPath) { + self._fireIfFileChanged(absPath); }); }, 1000); }, - // Pass true for `include` to include everything (and process only - // excludes) - _matches: function (filename, include, exclude) { + _startDirectoryWatches: function () { var self = this; - if (include === true) - include = [/.?/]; - for (var i = 0; i < include.length; i++) - if (include[i].test(filename)) - break; - if (i === include.length) { - return false; // didn't match any includes - } - - for (var i = 0; i < exclude.length; i++) { - if (exclude[i].test(filename)) { - return false; // matched an exclude - } - } - - // Matched an include and didn't match any excludes - return true; - }, - - _watchDirectory: function (absPath) { - var self = this; - - if (absPath in self.directoryWatches) - // Already being taken care of - return; - - // Determine the options that apply to this directory by finding - // the most specific rule. - absPath = path.resolve(absPath); // ensure no trailing slash - for (var i = 0; i < self.rules.length; i++) { - var rule = self.rules[i]; - if (absPath.length >= rule.dir.length && - absPath.substr(0, rule.dir.length) === rule.dir) - break; // found a match - rule = null; - } - if (! rule) - // Huh, doesn't appear that we're supposed to be watching this - // directory. - return; - - var contents = []; - var scanDirectory = function (isDoubleCheck) { + // Set up a watch for each directory + _.each(self.watchSet.directories, function (info) { if (self.stopped) return; - if (! fs.existsSync(absPath)) { - // Directory was removed. Stop watching. - var watch = self.directoryWatches[absPath]; - watch && watch.close(); - delete self.directoryWatches[absPath]; + // Check for the case where by the time we created the watch, the + // directory has already changed. + if (self._fireIfDirectoryChanged(info)) return; - } - // Find previously unknown files and subdirectories. (We don't - // care about removed subdirectories because the logic - // immediately above handles them, and we don't care about - // removed files because the ones we care about will already - // have file watches on them.) - var newContents = fs.readdirSync(absPath); - var added = _.difference(newContents, contents); - contents = newContents; + if (self.stopped || self.justCheckOnce) + return; - // Look at each newly added item - _.each(added, function (addedItem) { - var addedPath = path.join(absPath, addedItem); - - // Is it a directory? - try { - var stats = fs.lstatSync(addedPath); - } catch (e) { - // Can't be found? That's weird. Ignore. + // fs.watchFile doesn't work for directories (as tested on ubuntu) + // Notice that we poll very frequently (500 ms) + try { + self.directoryWatches.push( + fs.watch(info.absPath, {interval: 500}, function () { + self._fireIfDirectoryChanged(info); + }) + ); + } catch (e) { + // Can happen if the directory doesn't exist, in which case we should + // fire if it should be there. + if (e && e.code === "ENOENT") { + if (info.contents !== null) + self._fire(); return; } - var isDirectory = stats.isDirectory(); - - // Does it match the rule? - if (! self._matches(addedItem, - isDirectory ? true : rule.include, - rule.exclude)) - return; // No - - if (! isDirectory) { - if (! (addedPath in self.files)) - // Found a newly added file that we care about. - self._fire(absPath); - } else { - // Found a subdirectory that we care to monitor. - self._watchDirectory(addedPath); - } - }); - - if (! isDoubleCheck) { - // Whenever a directory changes, scan it soon as we notice, - // but then scan it again one secord later just to make sure - // that we haven't missed any changes. See commentary at - // #WorkAroundLowPrecisionMtimes - var timerId = self.nextTimerId++; - self.timers[timerId] = setTimeout(function () { - delete self.timers[timerId]; - if (! self.stopped) - scanDirectory(true); - }, 1000); + throw e; } - }; - - // fs.watchFile doesn't work for directories (as tested on ubuntu) - // Notice that we poll very frequently (500 ms) - try { - self.directoryWatches[absPath] = - fs.watch(absPath, {interval: 500}, scanDirectory); - scanDirectory(); - } catch (e) { - // Can happen if the directory doesn't exist, say because a - // nonexistent path was included in self.directories - } + }); }, - _fire: function (changedFile) { + _fire: function () { var self = this; if (self.stopped) return; self.stop(); - self.onChange(changedFile); + self.onChange(); }, stop: function () { @@ -325,6 +467,7 @@ _.extend(Watcher.prototype, { _.each(self.timers, function (timer, id) { clearTimeout(timer); }); + self.timers = {}; // Clean up file watches _.each(self.fileWatches, function (absPath) { @@ -336,8 +479,65 @@ _.extend(Watcher.prototype, { _.each(self.directoryWatches, function (watch) { watch.close(); }); - self.directoryWatches = {}; + self.directoryWatches = []; } }); -exports.Watcher = Watcher; +// Given a WatchSet, returns true if it currently describes the state of the +// disk. +var isUpToDate = function (watchSet) { + var upToDate = true; + var watcher = new Watcher({ + watchSet: watchSet, + onChange: function () { + upToDate = false; + }, + // internal flag which prevents us from starting watches and timers that + // we're about to cancel anyway + _justCheckOnce: true + }); + watcher.stop(); + return upToDate; +}; + +// Options should have absPath/include/exclude. +var readAndWatchDirectory = function (watchSet, options) { + var contents = readDirectory(options); + watchSet.addDirectory(_.extend({contents: contents}, options)); + return contents; +}; + +var readAndWatchFile = function (watchSet, absPath) { + var contents = readFile(absPath); + var hash = contents === null ? null : sha1(contents); + watchSet.addFile(absPath, hash); + return contents; +}; + +var readFile = function (absPath) { + try { + return fs.readFileSync(absPath); + } catch (e) { + // Rethrow most errors. + if (!e || (e.code !== 'ENOENT' && e.code !== 'EISDIR')) + throw e; + // File does not exist (or is a directory). + return null; + } +}; + +var sha1 = function (contents) { + var crypto = require('crypto'); + var hash = crypto.createHash('sha1'); + hash.update(contents); + return hash.digest('hex'); +}; + +_.extend(exports, { + WatchSet: WatchSet, + Watcher: Watcher, + readDirectory: readDirectory, + isUpToDate: isUpToDate, + readAndWatchDirectory: readAndWatchDirectory, + readAndWatchFile: readAndWatchFile +});