From c2649982797bf448fa4d1837e70c503d4b003875 Mon Sep 17 00:00:00 2001 From: Joshua Wulf Date: Sun, 21 Oct 2012 00:08:00 -0400 Subject: [PATCH 001/183] added --debug and --debug_brk options to meteor run --- app/meteor/meteor.js | 7 ++++++- app/meteor/run.js | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index 57de848134..a1b1499556 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -88,6 +88,10 @@ Commands.push({ .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.') + .boolean('debug') + .describe('debug', 'Run in debug mode for node-inspector') + .boolean('debug_brk') + .describe('debug_brk', 'Run in debug mode and break on first line') .usage( "Usage: meteor run [options]\n" + "\n" + @@ -111,7 +115,8 @@ Commands.push({ } var app_dir = path.resolve(require_project("run", true)); // app or package - var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true }; + var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true, + debug: new_argv.debug, debug_brk: new_argv.debug_brk}; require('./run.js').run(app_dir, bundle_opts, new_argv.port); } }); diff --git a/app/meteor/run.js b/app/meteor/run.js index 180b2f4f59..9e50ab1f05 100644 --- a/app/meteor/run.js +++ b/app/meteor/run.js @@ -15,6 +15,7 @@ var _ = require('../lib/third/underscore.js'); ////////// Globals ////////// +var debug, debug_brk; // list of log objects from the child process. var server_log = []; @@ -178,10 +179,24 @@ var start_server = function (bundle_path, outer_port, inner_port, mongo_url, env.MONGO_URL = mongo_url; env.ROOT_URL = env.ROOT_URL || ('http://localhost:' + outer_port); - var proc = spawn(process.execPath, + //spawn inner server, with debug enabled if requested + if (debug_brk){ console.log('Debug break on first line'); + var proc = spawn(process.execPath, + ['--debug-brk', path.join(bundle_path, 'main.js'), '--keepalive'], + {env: env}); + } else { + if (debug) { var proc = spawn(process.execPath, + ['--debug', path.join(bundle_path, 'main.js'), '--keepalive'], + {env: env}); + } else { + var proc = spawn(process.execPath, [path.join(bundle_path, 'main.js'), '--keepalive'], {env: env}); + } } + + + // XXX deal with test server logging differently?! proc.stdout.setEncoding('utf8'); @@ -450,6 +465,8 @@ var start_update_checks = function () { // can't continue. If you change this, remember to call // watcher.destroy() as appropriate. exports.run = function (app_dir, bundle_opts, port) { + debug = bundle_opts.debug; + debug_brk = bundle_opts.debug_brk; var outer_port = port || 3000; var inner_port = outer_port + 1; var mongo_port = outer_port + 2; From 1852834ef7915e2f95411a75aa60dc65643133b3 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 15 Nov 2012 23:53:45 -0800 Subject: [PATCH 002/183] Note that our problem with http-proxy 0.8.3 is fixed in 0.8.5. We've already built a dev bundle for this round though, so not worth actually upgrading yet. --- admin/generate-dev-bundle.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh index 5977224392..f7ce755ab9 100755 --- a/admin/generate-dev-bundle.sh +++ b/admin/generate-dev-bundle.sh @@ -98,8 +98,7 @@ npm install stream-buffers@0.2.3 npm install keypress@0.1.0 npm install sockjs@0.3.4 -# 0.8.4 contains a regression w/ maxSockets support. it is fixed on -# master, and will hopefully be in 0.8.5. +# 0.8.4 contains a regression w/ maxSockets support. it is fixed in 0.8.5. npm install http-proxy@0.8.3 # pinned at older version. 0.1.16+ uses mimelib, not mimelib-noiconv # which make the dev bundle much bigger. We need a better solution. From 0af26570c3c5c313550d14161d62093134b09326 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 14 Nov 2012 20:41:12 -0800 Subject: [PATCH 003/183] accounts-ui fixes related to the introduction of {{loggingIn}} - Correctly position certain elements - Reduce flicker/things moving around in various cases - Force dropdown to open in case there is a message to display --- .../accounts-ui-viewer/accounts-ui-viewer.js | 12 ++++--- .../accounts-ui-unstyled/login_buttons.html | 34 +++++++++++++++++-- .../accounts-ui-unstyled/login_buttons.js | 15 ++++++-- .../login_buttons_dialogs.js | 4 +-- .../login_buttons_dropdown.html | 22 +++++++----- .../login_buttons_dropdown.js | 16 ++++----- .../login_buttons_session.js | 27 +++++++++++++-- .../login_buttons_single.html | 7 ++++ .../login_buttons_single.js | 2 +- packages/accounts-ui/login_buttons.less | 17 ++++++++-- 10 files changed, 122 insertions(+), 34 deletions(-) diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js index 98815d8f31..28a86e9d74 100644 --- a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js @@ -114,7 +114,7 @@ if (Meteor.isClient) { return settings.alignRight ? 'right' : 'left'; }; - var fakeLogin = function () { + var fakeLogin = function (callback) { Accounts.createUser( {username: Meteor.uuid(), password: "password", @@ -130,6 +130,7 @@ if (Meteor.isClient) { if (! Session.get('settings').hasPasswords) Meteor.users.update(Meteor.userId(), { $unset: { username: 1 }}); + callback(); }); }; @@ -169,17 +170,18 @@ if (Meteor.isClient) { Accounts.loginServiceConfiguration.remove({service: service}); } else if (this.key === "messages") { if (this.value === "error") { - Accounts._loginButtonsSession.set('errorMessage', 'An error occurred! Gee golly gosh.'); + Accounts._loginButtonsSession.errorMessage('An error occurred! Gee golly gosh.'); } else if (this.value === "info") { - Accounts._loginButtonsSession.set('infoMessage', 'Here is some information that is crucial.'); + Accounts._loginButtonsSession.infoMessage('Here is some information that is crucial.'); } else if (this.value === "clear") { Accounts._loginButtonsSession.resetMessages(); } } else if (this.key === "sign") { if (this.value === 'in') { // create a random new user - Accounts._loginButtonsSession.closeDropdown(); - fakeLogin(); + fakeLogin(function () { + Accounts._loginButtonsSession.closeDropdown(); + }); } else if (this.value === 'out') { Meteor.logout(); } diff --git a/packages/accounts-ui-unstyled/login_buttons.html b/packages/accounts-ui-unstyled/login_buttons.html index bf0da6d013..08d2dd3a46 100644 --- a/packages/accounts-ui-unstyled/login_buttons.html +++ b/packages/accounts-ui-unstyled/login_buttons.html @@ -7,7 +7,21 @@ + + diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 7a15ed2b1b..b2c94bd6d4 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -87,6 +87,15 @@ }; + // + // loginButtonsLoggingInPadding template + // + + Template._loginButtonsLoggingInPadding.dropdown = function () { + return Accounts._loginButtons.dropdown(); + }; + + // // helpers // @@ -136,7 +145,7 @@ if (username.length >= 3) { return true; } else { - loginButtonsSession.set('errorMessage', "Username must be at least 3 characters long"); + loginButtonsSession.errorMessage("Username must be at least 3 characters long"); return false; } }; @@ -147,7 +156,7 @@ if (email.indexOf('@') !== -1) { return true; } else { - loginButtonsSession.set('errorMessage', "Invalid email"); + loginButtonsSession.errorMessage("Invalid email"); return false; } }; @@ -155,7 +164,7 @@ if (password.length >= 6) { return true; } else { - loginButtonsSession.set('errorMessage', "Password must be at least 6 characters long"); + 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 fadce1d17e..8252258538 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dialogs.js +++ b/packages/accounts-ui-unstyled/login_buttons_dialogs.js @@ -61,7 +61,7 @@ loginButtonsSession.get('resetPasswordToken'), newPassword, function (error) { if (error) { - loginButtonsSession.set('errorMessage', error.reason || "Unknown error"); + loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.set('resetPasswordToken', null); Accounts._enableAutoLogin(); @@ -102,7 +102,7 @@ loginButtonsSession.get('enrollAccountToken'), password, function (error) { if (error) { - loginButtonsSession.set('errorMessage', error.reason || "Unknown error"); + loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.set('enrollAccountToken', null); Accounts._enableAutoLogin(); diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.html b/packages/accounts-ui-unstyled/login_buttons_dropdown.html index 2104840787..578021083c 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.html +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.html @@ -43,20 +43,24 @@ + + + diff --git a/packages/accounts-ui-unstyled/login_buttons_single.js b/packages/accounts-ui-unstyled/login_buttons_single.js index d7aafb3c5e..4c33ab5146 100644 --- a/packages/accounts-ui-unstyled/login_buttons_single.js +++ b/packages/accounts-ui-unstyled/login_buttons_single.js @@ -14,7 +14,7 @@ } else if (err instanceof Accounts.ConfigError) { loginButtonsSession.configureService(serviceName); } else { - loginButtonsSession.set('errorMessage', err.reason || "Unknown error"); + loginButtonsSession.errorMessage(err.reason || "Unknown error"); } }; diff --git a/packages/accounts-ui/login_buttons.less b/packages/accounts-ui/login_buttons.less index 4b2476d708..e6053c83e1 100644 --- a/packages/accounts-ui/login_buttons.less +++ b/packages/accounts-ui/login_buttons.less @@ -91,7 +91,7 @@ #login-buttons { .display-inline-block(); - margin-right: 0.01px; // Fixes display on IE8: http://www.compsoft.co.uk/Blog/2009/11/inline-block-not-quite-inline-blocking.html + margin-right: 0.2px; // Fixes display on IE8: http://www.compsoft.co.uk/Blog/2009/11/inline-block-not-quite-inline-blocking.html // This seems to keep the height of the line from // being sensitive to the presence of the unicode down arrow, @@ -183,6 +183,11 @@ } #login-buttons { + .login-buttons-padding { + display: inline-block; + width: 30px; + } + .login-display-name { margin-right: 4px; } .configure-button { @@ -215,9 +220,17 @@ position: relative; padding-bottom: 8px; } - #login-dropdown-list .loading { + + .login-text-and-button .loading, .login-link-and-dropdown-list .loading { + display: inline-block; + } + &.login-buttons-dropdown-align-left #login-dropdown-list .loading { float: right; } + &.login-buttons-dropdown-align-right #login-dropdown-list .loading { + float: left; + } + .login-close-text-clear { clear: both; } From 637692db31e6537304eb814e6d5e5457dd74daca Mon Sep 17 00:00:00 2001 From: David Glasser Date: Sat, 17 Nov 2012 17:46:57 -0800 Subject: [PATCH 004/183] Add filename and line number to test failures using a v8 stack trace API. This works for server tests (Node uses v8) or for client tests in Chrome. Uses a simple heuristic to guess which line in the stack trace is most likely to be the actual high-level assertion. Adjustments to this heuristic and implementations for other browsers are welcome :) --- packages/tinytest/tinytest.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/tinytest/tinytest.js b/packages/tinytest/tinytest.js index 894fa6cbea..90010a35c9 100644 --- a/packages/tinytest/tinytest.js +++ b/packages/tinytest/tinytest.js @@ -50,6 +50,28 @@ _.extend(TestCaseResults.prototype, { if (self.stop_at_offset) self.stop_at_offset--; + // Get filename and line number of failure if we're using v8 (Chrome or + // Node). + if (Error.captureStackTrace) { + var savedPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = function(_, stack){ return stack; }; + var err = new Error; + Error.captureStackTrace(err); + var stack = err.stack; + Error.prepareStackTrace = savedPrepareStackTrace; + console.log(stack); + for (var i = stack.length - 1; i >= 0; --i) { + var frame = stack[i]; + // Heuristic: use the OUTERMOST line which is in a _test.js or _tests.js + // file (this is less likely to be a test helper function). + if (frame.getFileName().match(/_tests?\.js/)) { + doc.filename = frame.getFileName(); + doc.line = frame.getLineNumber(); + break; + } + } + } + self.onEvent({ type: (self.expecting_failure ? "expected_fail" : "fail"), details: doc, From 5b066c77a0b2922320e6ce26b9e14491f8f440fe Mon Sep 17 00:00:00 2001 From: David Glasser Date: Sat, 17 Nov 2012 17:53:00 -0800 Subject: [PATCH 005/183] Remove stray debugging log from previous commit. --- packages/tinytest/tinytest.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tinytest/tinytest.js b/packages/tinytest/tinytest.js index 90010a35c9..9474a49ad4 100644 --- a/packages/tinytest/tinytest.js +++ b/packages/tinytest/tinytest.js @@ -59,7 +59,6 @@ _.extend(TestCaseResults.prototype, { Error.captureStackTrace(err); var stack = err.stack; Error.prepareStackTrace = savedPrepareStackTrace; - console.log(stack); for (var i = stack.length - 1; i >= 0; --i) { var frame = stack[i]; // Heuristic: use the OUTERMOST line which is in a _test.js or _tests.js From 1da3ef978a73ade7900876f0d8b7e95766ba8478 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 19 Nov 2012 19:34:26 -0800 Subject: [PATCH 006/183] accounts-ui: don't display message twice --- .../login_buttons_session.js | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/accounts-ui-unstyled/login_buttons_session.js b/packages/accounts-ui-unstyled/login_buttons_session.js index df15d86f13..d542d254a7 100644 --- a/packages/accounts-ui-unstyled/login_buttons_session.js +++ b/packages/accounts-ui-unstyled/login_buttons_session.js @@ -11,6 +11,7 @@ 'errorMessage', 'infoMessage', + // dialogs with messages (info and error) 'resetPasswordToken', 'enrollAccountToken', 'justVerifiedEmail', @@ -58,17 +59,33 @@ infoMessage: function(message) { this._set("errorMessage", null); this._set("infoMessage", message); - this.set("dropdownVisible", true); // See #OpenDropdownForMessage + this.ensureMessageVisible(); }, errorMessage: function(message) { this._set("errorMessage", message); this._set("infoMessage", null); + this.ensureMessageVisible(); + }, - // #OpenDropdownForMessage - // for the case that you're taking some action in the dropdown, and then you - // get an error or message. notably has no effect in the single button case. - this.set("dropdownVisible", true); + // is there a visible dialog that shows messages (info and error) + isMessageDialogVisible: function () { + return this.get('resetPasswordToken') || + this.get('enrollAccountToken') || + this.get('justVerifiedEmail'); + }, + + // ensure that somethings displaying a message (info or error) is + // visible. if a dialog with messages is open, do nothing; + // otherwise open the dropdown. + // + // notably this doesn't matter when only displaying a single login + // button since then we have an explicit message dialog + // (_loginButtonsMessageDialog), and dropdownVisible is ignored in + // this case. + ensureMessageVisible: function () { + if (!this.isMessageDialogVisible()) + this.set("dropdownVisible", true); }, resetMessages: function () { From 974f0be4a47aff38410e266deaf62ddda12b8e17 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 19 Nov 2012 19:57:57 -0800 Subject: [PATCH 007/183] Expand failed tests by default. --- packages/test-in-browser/driver.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index 550ca5f834..8710f2e62b 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -294,6 +294,11 @@ var reportResults = function(results) { test.events = out; } + // Expand a failed test (but only set this if the user hasn't clicked on the + // test name yet). + if (_testStatus(test) === "failed" && test.expanded === undefined) + test.expanded = true; + _.defer(_throttled_update); }; From dcdd6da47fd2d093ba585e667bb3473c02b27bfb Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 19 Nov 2012 20:28:41 -0800 Subject: [PATCH 008/183] accounts-ui: show messages in all dropdowns --- packages/accounts-ui-unstyled/login_buttons_dropdown.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.html b/packages/accounts-ui-unstyled/login_buttons_dropdown.html index 578021083c..2f85950873 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.html +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.html @@ -36,6 +36,8 @@ + + {{> _loginButtonsMessages}} From f4573dbb1b68accf345c0badada7f83cba00dba3 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 19 Nov 2012 20:34:48 -0800 Subject: [PATCH 009/183] Add a sleep function (useful for introducing latency in manual debugging). --- packages/meteor/fiber_helpers.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/meteor/fiber_helpers.js b/packages/meteor/fiber_helpers.js index a708eb3bc7..680b3e5545 100644 --- a/packages/meteor/fiber_helpers.js +++ b/packages/meteor/fiber_helpers.js @@ -141,4 +141,15 @@ _.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() { + fiber.run(); + }, ms); + Fiber.yield(); +}; + + })(); From 35df034a7d28e6d7d38628cb12d6a37d5f4e64e3 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 20 Nov 2012 15:14:58 -0800 Subject: [PATCH 010/183] Add IntelliJ IDEA and SublimeEdit extensions to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0bac695bd7..c6a3fe5a87 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ \#*\# .\#* .idea +*.iml +*.sublime-project +*.sublime-workspace From 8d9420d25584c4c125ca596dd5faa8700c8f461a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 20 Nov 2012 15:16:47 -0800 Subject: [PATCH 011/183] Rename local variables (to camel case) --- packages/accounts-google/google_client.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 75d1a3da2f..c01801c45a 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -15,21 +15,20 @@ var state = Meteor.uuid(); // always need this to get user id from google. - var required_scope = ['https://www.googleapis.com/auth/userinfo.profile']; + var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile']; var scope = ['https://www.googleapis.com/auth/userinfo.email']; if (options && options.requestPermissions) scope = options.requestPermissions; - scope = _.union(scope, required_scope); - var flat_scope = _.map(scope, encodeURIComponent).join('+'); - // Might be good to have a way to set access_type=offline. Need to // both set it here and store the refresh token on the server. + scope = _.union(scope, requiredScope); + var flatScope = _.map(scope, encodeURIComponent).join('+'); var loginUrl = 'https://accounts.google.com/o/oauth2/auth' + '?response_type=code' + '&client_id=' + config.clientId + - '&scope=' + flat_scope + + '&scope=' + flatScope + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + '&state=' + state; From 9b8eb41f0476d7355673844cd55dd6cbaea67a3a Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 20 Nov 2012 19:31:29 -0800 Subject: [PATCH 012/183] Add comment about how remembering passwords doesn't work. --- .../login_buttons_dropdown.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.html b/packages/accounts-ui-unstyled/login_buttons_dropdown.html index 2f85950873..f7d4a16f61 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.html +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.html @@ -156,6 +156,26 @@ + + + From da4699288da28fd9af524804ff4c52a212735469 Mon Sep 17 00:00:00 2001 From: Geoff Schmidt Date: Wed, 12 Dec 2012 12:17:53 -0800 Subject: [PATCH 094/183] tweaks to Email.send example --- docs/client/api.html | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index f98c3989d4..48ad532098 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2452,26 +2452,34 @@ If `MAIL_URL` is not set (eg, when running your application locally), You must provide the `from` option and at least one of `to`, `cc`, and `bcc`; all other options are optional. -Example server method for sending email: +`Email.send` only works on the server. Here is an example of how a +client could use a server method call to send an email. (In an actual +application, you'd need to be careful to limit the emails that a +client could send, to prevent your server from being used as a relay +by spammers.) + // In your server code: define a method that the client can call Meteor.methods({ - sendEmail: function (toEmail, fromEmail, subject) { + sendEmail: function (to, from, subject, text) { + // Let other method calls from the same client start running, + // without waiting for the email sending to complete. this.unblock(); - Email.send({ - to: toEmail, - from: fromEmail, - subject: subject - }); + + Email.send({ + to: to, + from: from, + subject: subject, + text: text + }); } }); -Example asynchronous email method call: - + // In your client code: asynchronously send an email Meteor.call('sendEmail', - 'test_to@test.com', - 'test_from@test.com', - 'Subject line', - function (error, result) { return error ? error : result; }); + 'alice@example.com', + 'bob@example.com', + 'Hello from Meteor!', + 'This is a test of Email.send.'); {{/better_markdown}} From 06a3e237305292b7a796dbddc8f0eceec8b59c04 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 11 Dec 2012 17:27:37 -0800 Subject: [PATCH 095/183] The rest of the docs for Meteor.settings --- docs/client/api.html | 2 ++ docs/client/api.js | 11 +++++++++++ docs/client/commandline.html | 15 +++++++-------- docs/client/docs.js | 3 ++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 48ad532098..aeee5f0f76 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -30,6 +30,8 @@ put on the screen. {{> api_box absoluteUrl}} +{{> api_box settings}} +

Publish and subscribe

These functions control how Meteor servers publish sets of records and diff --git a/docs/client/api.js b/docs/client/api.js index 88d2ccefd6..2b4d85c4fb 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -54,6 +54,17 @@ Template.api.absoluteUrl = { ] }; +Template.api.settings = { + id: "meteor_settings", + name: "Meteor.settings", + locus: "Server", + descr: ["`Meteor.settings` contains any deployment-specific options that were " + + "provided using the `--settings` option for `meteor run` or `meteor deploy`. " + + "If you provide the `--settings` option, `Meteor.settings` will be the " + + "JSON object in the file you specify. Otherwise, `Meteor.settings` will " + + "be an empty object."] +}; + Template.api.publish = { id: "meteor_publish", name: "Meteor.publish(name, func)", diff --git a/docs/client/commandline.html b/docs/client/commandline.html index 979e9197f8..5f8ab1f0b9 100644 --- a/docs/client/commandline.html +++ b/docs/client/commandline.html @@ -92,15 +92,14 @@ address of origin.meteor.com. -To add deploy-specific information to your application, use the `--settings` -option. This will set the variable `Meteor.settings` in your application, but -only on the server. The `--settings` option takes an argument: the name of a -file containing JSON data to put into `Meteor.settings`. +You can add information specific to a particular deployment of your application +by using the `--settings` option. The argument to `--settings` is a file +containing any JSON string. The object in your settings file will appear on the +server side of your application in [`Meteor.settings`](#meteor_settings). -The settings you pass will persist on your deployed application across -invocations of `meteor deploy` until you again pass the `--settings` option with -different contents in your settings file. To unset `Meteor.settings`, pass an -empty settings file. +Settings are persistent. When you redeploy your app, the old value will be +preserved unless you explicitly pass new settings using the `--settings` option. +To unset `Meteor.settings`, pass an empty settings file.

meteor logs site

diff --git a/docs/client/docs.js b/docs/client/docs.js index e01f20a1d8..3413bfa00e 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -94,7 +94,8 @@ var toc = [ "Meteor.isClient", "Meteor.isServer", "Meteor.startup", - "Meteor.absoluteUrl" + "Meteor.absoluteUrl", + "Meteor.settings" ], "Publish and subscribe", [ From 7724c7ef7f1f5580e10fcd20722b9b16228d20cd Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 11 Dec 2012 17:58:42 -0800 Subject: [PATCH 096/183] Changed from a fake enum thing to just strings for debug opts Taking Geoff's advice. Also adjusting the docstrings in the usage for --debug and --debug-brk --- app/meteor/meteor.js | 10 +++++----- app/meteor/run.js | 12 +++--------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index 1ce6074292..c2aeb819ac 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -118,9 +118,9 @@ Commands.push({ .boolean('production') .describe('production', 'Run in production mode. Minify and bundle CSS and JS files.') .boolean('debug') - .describe('debug', 'Run in debug mode for node-inspector') + .describe('debug', 'Pass --debug to node.js to enable node-inspector debugging.') .boolean('debug-brk') - .describe('debug-brk', 'Run in debug mode and break on first line') + .describe('debug-brk', 'Pass --debug-brk to node.js to enable debugging and break on the first line.') .describe('settings', 'Set optional data for Meteor.settings on the server') .boolean('once') .usage( @@ -151,9 +151,9 @@ Commands.push({ var app_dir = path.resolve(require_project("run", true)); // app or package var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true }; - var debugStatus = runner.DebugStatus.OFF; - if (new_argv['debug']) debugStatus = runner.DebugStatus.DEBUG; - if (new_argv['debug-brk']) debugStatus = runner.DebugStatus.BREAK; + var debugStatus = "OFF"; + if (new_argv['debug']) debugStatus = "DEBUG"; + if (new_argv['debug-brk']) debugStatus = "BREAK"; runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, settings, debugStatus); } }); diff --git a/app/meteor/run.js b/app/meteor/run.js index 1f6762af36..be493d1b62 100644 --- a/app/meteor/run.js +++ b/app/meteor/run.js @@ -212,7 +212,7 @@ var log_to_clients = function (msg) { var start_server = function (options) { // environment options = _.extend({runOnce: false, - debugStatus: exports.DebugStatus.OFF + debugStatus: "OFF" }, options); if (options.runOnce) { @@ -229,9 +229,9 @@ var start_server = function (options) { var dbg = options.debugStatus; var nodeOptions = []; - if (dbg === exports.DebugStatus.DEBUG) + if (dbg === "DEBUG") nodeOptions.push('--debug'); - if (dbg === exports.DebugStatus.BREAK) { + if (dbg === "BREAK") { console.log('Debug will break on the first line'); nodeOptions.push('--debug-brk'); } @@ -508,12 +508,6 @@ var start_update_checks = function () { // XXX leave a pidfile and check if we are already running -exports.DebugStatus = { - OFF : "OFF", - DEBUG : "DEBUG", - BREAK : "BREAK" -}; - // This function never returns and will call process.exit() if it // can't continue. If you change this, remember to call // watcher.destroy() as appropriate. From 69459b8c704c237c7f00f66cdde145230e0f393a Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Wed, 12 Dec 2012 11:48:44 -0800 Subject: [PATCH 097/183] Add pre to the whitspace formatting of code --- docs/client/docs.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/client/docs.css b/docs/client/docs.css index 4f98beac79..b4c72506e2 100644 --- a/docs/client/docs.css +++ b/docs/client/docs.css @@ -43,6 +43,7 @@ pre { code { font-family: monospace; font-size: 1.1em; + white-space: pre; } ul { From cd250f094509fa99f707bb35dc612aa800156ff4 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 12 Dec 2012 12:54:48 -0800 Subject: [PATCH 098/183] tinytest: drop results for a run when requested by client, not onComplete. Should be useful for a non-browser-based test runner. --- packages/tinytest/tinytest_client.js | 4 +++- packages/tinytest/tinytest_server.js | 14 +++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/tinytest/tinytest_client.js b/packages/tinytest/tinytest_client.js index a53885f73b..5c07a4a487 100644 --- a/packages/tinytest/tinytest_client.js +++ b/packages/tinytest/tinytest_client.js @@ -35,13 +35,15 @@ Meteor._runTestsEverywhere = function (onReport, onComplete) { } }); - Meteor.subscribe(Meteor._ServerTestResultsSubscription, runId); + var handle = Meteor.subscribe(Meteor._ServerTestResultsSubscription, runId); Meteor.call('tinytest/run', runId, function (error, result) { if (error) // XXX better report error throw new Error("Test server returned an error"); remoteComplete = true; + handle.stop(); + Meteor.call('tinytest/clearResults', runId); maybeDone(); }); }; diff --git a/packages/tinytest/tinytest_server.js b/packages/tinytest/tinytest_server.js index 2c9636e29f..b7d274448f 100644 --- a/packages/tinytest/tinytest_server.js +++ b/packages/tinytest/tinytest_server.js @@ -48,17 +48,21 @@ }; var onComplete = function() { - _.each(handlesForRun[runId], function (handle) { - handle.stop(); - }); - delete handlesForRun[runId]; - delete reportsForRun[runId]; future.ret(); }; Meteor._runTests(onReport, onComplete); future.wait(); + }, + 'tinytest/clearResults': function (runId) { + _.each(handlesForRun[runId], function (handle) { + // XXX this doesn't actually notify the client that it has been + // unsubscribed. + handle.stop(); + }); + delete handlesForRun[runId]; + delete reportsForRun[runId]; } }); }()); From 033fd00a58b7b428392d3f643d40a86d6b18c02e Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Wed, 12 Dec 2012 16:06:21 -0800 Subject: [PATCH 099/183] add npm "tar" package for use by engine --- admin/generate-dev-bundle.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh index 8f60154601..4c259aa71f 100755 --- a/admin/generate-dev-bundle.sh +++ b/admin/generate-dev-bundle.sh @@ -98,6 +98,7 @@ npm install keypress@0.1.0 npm install sockjs@0.3.4 npm install http-proxy@0.8.5 npm install underscore@1.4.2 +npm install tar@0.1.14 # progress 0.1.0 has a regression where it opens stdin and thus does not # allow the node process to exit cleanly. See From 6c9cd218a91cd5dbf2aa4a14fb9a4617b75c6ae6 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Wed, 12 Dec 2012 16:06:31 -0800 Subject: [PATCH 100/183] bump dev_bundle version to 0.2.11 --- admin/generate-dev-bundle.sh | 2 +- meteor | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh index 4c259aa71f..9400830014 100755 --- a/admin/generate-dev-bundle.sh +++ b/admin/generate-dev-bundle.sh @@ -3,7 +3,7 @@ set -e set -u -BUNDLE_VERSION=0.2.10 +BUNDLE_VERSION=0.2.11 UNAME=$(uname) ARCH=$(uname -m) diff --git a/meteor b/meteor index 7082e70515..958a8cc51c 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.2.10 +BUNDLE_VERSION=0.2.11 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. From d85497a275cf98d27544cc2bb7846d02fa72e9f4 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Wed, 12 Dec 2012 16:16:02 -0800 Subject: [PATCH 101/183] Fix capturing the twitter identity information --- packages/accounts-twitter/twitter_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 4008224b3d..0a19516159 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,7 +1,7 @@ (function () { Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { - var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json'); + var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json').data; return { serviceData: { From 6fee95fc348772ac75482d7a31673434882bf7b2 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 12 Dec 2012 22:01:09 -0800 Subject: [PATCH 102/183] make static bigdata app to stress page load (merge box) --- .../scenarios/{bigdata.json => bigdata-static.json} | 5 ++--- .../benchmark/scenarios/bigdata-updates.json | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) rename examples/unfinished/benchmark/scenarios/{bigdata.json => bigdata-static.json} (54%) create mode 100644 examples/unfinished/benchmark/scenarios/bigdata-updates.json diff --git a/examples/unfinished/benchmark/scenarios/bigdata.json b/examples/unfinished/benchmark/scenarios/bigdata-static.json similarity index 54% rename from examples/unfinished/benchmark/scenarios/bigdata.json rename to examples/unfinished/benchmark/scenarios/bigdata-static.json index 93f10fe834..8523c45360 100644 --- a/examples/unfinished/benchmark/scenarios/bigdata.json +++ b/examples/unfinished/benchmark/scenarios/bigdata-static.json @@ -3,8 +3,7 @@ "numCollections": 1, "numBuckets": 1, "initialDocuments": 1024, - "updatesPerSecond": 1, - "documentSize": 10240, - "documentNumFields": 16 + "documentSize": 2048, + "documentNumFields": 64 } } diff --git a/examples/unfinished/benchmark/scenarios/bigdata-updates.json b/examples/unfinished/benchmark/scenarios/bigdata-updates.json new file mode 100644 index 0000000000..19e64db5d8 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/bigdata-updates.json @@ -0,0 +1,10 @@ +{ + "params": { + "numCollections": 1, + "numBuckets": 1, + "initialDocuments": 1024, + "updatesPerSecond": 0.2, + "documentSize": 1024, + "documentNumFields": 32 + } +} From 1070a682fd651ffd35a13b8193de899c8ec61f1b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 13 Dec 2012 17:49:09 -0800 Subject: [PATCH 103/183] Rename soma.jpeg to soma.png, because that's what it is. --- examples/parties/client/parties.css | 2 +- examples/parties/public/{soma.jpeg => soma.png} | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/parties/public/{soma.jpeg => soma.png} (100%) diff --git a/examples/parties/client/parties.css b/examples/parties/client/parties.css index 19b3cb58ef..e51bfc6fb5 100644 --- a/examples/parties/client/parties.css +++ b/examples/parties/client/parties.css @@ -44,7 +44,7 @@ input.chosen { .map { position: relative; - background-image: url('/soma.jpeg'); + background-image: url('/soma.png'); background-position: -20px -20px; width: 500px; height: 500px; diff --git a/examples/parties/public/soma.jpeg b/examples/parties/public/soma.png similarity index 100% rename from examples/parties/public/soma.jpeg rename to examples/parties/public/soma.png From 836c80ef67f4b334474e3726c08674d60bcd7a9c Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 14 Dec 2012 09:34:33 -0800 Subject: [PATCH 104/183] Add comment to History.md --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 0aca974501..599af4cf28 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,9 @@ ## vNEXT +* `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response, + rather than just the data. Also, introduce `OAuth1Binding.post`. #534 + ## v0.5.2 * Fix 0.5.1 regression: Cursor `observe` works during server startup. #507 From 02c092e140adca82ce587edf092f1767665d2a0b Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 14 Dec 2012 09:43:50 -0800 Subject: [PATCH 105/183] Improvement to last commit changing History.md --- History.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index 599af4cf28..3b6b9a38f9 100644 --- a/History.md +++ b/History.md @@ -1,8 +1,9 @@ ## vNEXT -* `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response, - rather than just the data. Also, introduce `OAuth1Binding.post`. #534 +* `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response + (including headers and statusCode), rather than just the data. Also, + introduce `OAuth1Binding.post`. #534 ## v0.5.2 From 7fb8f22a64a6e081ae47d887f88ba3772ca89172 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 14 Dec 2012 09:46:48 -0800 Subject: [PATCH 106/183] More History.md fixes --- History.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index 3b6b9a38f9..9ae3054864 100644 --- a/History.md +++ b/History.md @@ -1,9 +1,12 @@ ## vNEXT -* `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response - (including headers and statusCode), rather than just the data. Also, - introduce `OAuth1Binding.post`. #534 +* `OAuth1Binding` improvements: #539 + * `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response + (including headers and statusCode), rather than just the data. + * Introduce `OAuth1Binding.post`. + * `OAuth1Binding.get`, `OAuth1Binding.call` and `OAuth1Binding.post` now take + a `params` argument. This facilitates making calls to the Twitter API. ## v0.5.2 From 3d870954ca5b63ab0037c1ca41085cab579bcd0e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 14 Dec 2012 15:13:15 -0800 Subject: [PATCH 107/183] Fix queries and updates with JS RegExp objects with //i and //m flags. Follow-up to 28a136d7 continuing to address issue #346. --- packages/mongo-livedata/collection.js | 12 +++++++++++- packages/mongo-livedata/mongo_livedata_tests.js | 10 ++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 20d9d8c8a9..073e689ab6 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -178,8 +178,18 @@ Meteor.Collection._rewriteSelector = function (selector) { var ret = {}; _.each(selector, function (value, key) { - if (value instanceof RegExp) + 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; + } else ret[key] = value; }); diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index d1606b077f..a2f4e28f47 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -544,21 +544,23 @@ testAsyncMulti('mongo-livedata - rewrite selector', [ var doc = coll.findOne(docId); test.isTrue(doc); - test.equal(doc.name, "foobar"); + test.equal(doc.name, "f\noobar"); test.equal(doc.value, 43); }); - coll.insert({name: 'foobar', value: 42}, expect(function (err1, id) { + coll.insert({name: 'f\noobar', value: 42}, expect(function (err1, id) { test.isFalse(err1); test.isTrue(id); docId = id; var doc = coll.findOne(docId); test.isTrue(doc); - test.equal(doc.name, "foobar"); + test.equal(doc.name, "f\noobar"); test.equal(doc.value, 42); - coll.update({name: /o+b/}, {$inc: {value: 1}}, updateCallback); + // Ensure that "i" and "m" flags are respected by making /B/ match b and + // /^/ match at the newline. + coll.update({name: /^o+B/im}, {$inc: {value: 1}}, updateCallback); })); } ]); From f68a8a34316724d2afc4fac4e333358bba2e968f Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 10:06:48 -0800 Subject: [PATCH 108/183] Add a XXX caveat to #346 fix. --- packages/mongo-livedata/collection.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 073e689ab6..a719675820 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -179,6 +179,8 @@ Meteor.Collection._rewriteSelector = function (selector) { var ret = {}; _.each(selector, function (value, key) { if (value instanceof RegExp) { + // XXX should also do this translation at lower levels (eg if the outer + // level is $and/$or/$nor, or if there's an $elemMatch) ret[key] = {$regex: value.source}; var regexOptions = ''; // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options From aee54f3ac057c176c0ade7842a88db564a95e4b4 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 11 Dec 2012 22:30:28 -0800 Subject: [PATCH 109/183] Add skip and limit support to observe. --- packages/minimongo/minimongo.js | 33 ++++++++++++++++++++++++--- packages/minimongo/minimongo_tests.js | 19 +++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 91477e54df..34cbc22f9a 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -173,7 +173,6 @@ LocalCollection.LiveResultsSet = function () {}; // initial results delivered through added callback // XXX maybe callbacks should take a list of objects, to expose transactions? // XXX maybe support field limiting (to limit what you're notified on) -// XXX maybe support limit/skip _.extend(LocalCollection.Cursor.prototype, { observe: function (options) { @@ -187,8 +186,8 @@ _.extend(LocalCollection.Cursor.prototype, { _observeInternal: function (ordered, options) { var self = this; - if (self.skip || self.limit) - throw new Error("cannot observe queries with skip or limit"); + if (!ordered && (self.skip || self.limit)) + throw new Error("must use ordered observe with skip or limit"); var qid = self.collection.next_qid++; @@ -463,8 +462,16 @@ LocalCollection._deepcopy = function (v) { // XXX the sorted-query logic below is laughably inefficient. we'll // need to come up with a better datastructure for this. +// +// XXX the logic for observing with a skip or a limit is even more +// laughably inefficient. we recompute the whole results every time! LocalCollection._insertInResults = function (query, doc) { + if (query.cursor.skip || query.cursor.limit) { + LocalCollection._recomputeResults(query); + return; + } + if (query.ordered) { if (!query.sort_f) { query.added(LocalCollection._deepcopy(doc), query.results.length); @@ -481,6 +488,11 @@ LocalCollection._insertInResults = function (query, doc) { }; LocalCollection._removeFromResults = function (query, doc) { + if (query.cursor.skip || query.cursor.limit) { + LocalCollection._recomputeResults(query); + return; + } + if (query.ordered) { var i = LocalCollection._findInOrderedResults(query, doc); query.removed(doc, i); @@ -493,6 +505,11 @@ LocalCollection._removeFromResults = function (query, doc) { }; LocalCollection._updateInResults = function (query, doc, old_doc) { + if (query.cursor.skip || query.cursor.limit) { + LocalCollection._recomputeResults(query); + return; + } + if (doc._id !== old_doc._id) throw new Error("Can't change a doc's _id while updating"); @@ -517,6 +534,16 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { query.moved(LocalCollection._deepcopy(doc), orig_idx, new_idx); }; +LocalCollection._recomputeResults = function (query, doc) { + var old_results = query.results; + query.results = query.cursor._getRawObjects(query.ordered); + + if (!query.paused) + LocalCollection._diffQuery( + query.ordered, old_results, query.results, query, true); +}; + + LocalCollection._findInOrderedResults = function (query, doc) { if (!query.ordered) throw new Error("Can't call _findInOrderedResults on unordered query"); diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index fdfe5b3c54..e9de875f7c 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1203,6 +1203,25 @@ Tinytest.add("minimongo - observe", function (test) { c.insert({a:100}); test.equal(operations.shift(), ['added', {a:100}, 0]); handle.stop(); + + // test skip and limit. + c.remove({}); + handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2}).observe(cbs); + test.equal(operations.shift(), undefined); + c.insert({a:1}); + test.equal(operations.shift(), undefined); + c.insert({a:2}); + test.equal(operations.shift(), ['added', {a:2}, 0]); + c.insert({a:3}); + test.equal(operations.shift(), ['added', {a:3}, 1]); + c.insert({a:4}); + test.equal(operations.shift(), undefined); + id = c.findOne({a:2})._id; + c.update({a:1}, {a:5}); + test.equal(operations.shift(), ['removed', id, 0, {a:2}]); + test.equal(operations.shift(), ['added', {a:4}, 1]); + + handle.stop(); }); Tinytest.add("minimongo - diff", function (test) { From 0faaa07c777e59f9bad7d65ed3b2b4021db2c039 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 14 Dec 2012 18:52:27 -0800 Subject: [PATCH 110/183] Feedback from review. --- packages/minimongo/minimongo.js | 15 +++++++++++---- packages/minimongo/minimongo_tests.js | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 34cbc22f9a..fa32a06623 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -94,10 +94,17 @@ LocalCollection.prototype.findOne = function (selector, options) { if (arguments.length === 0) selector = {}; - // XXX disable limit here so that we can observe findOne() cursor, - // as required by markAsReactive. - // options = options || {}; - // options.limit = 1; + // NOTE: by setting limit 1 here, we end up using very inefficient + // code that recomputes the whole query on each update. The upside is + // that when you reactively depend on a findOne you only get + // invalidated when the found object changes, not any object in the + // collection. Most findOne will be by id, which has a fast path, so + // this might not be a big deal. In most cases, invalidation causes + // the called to re-query anyway, so this should be a net performance + // improvement. + options = options || {}; + options.limit = 1; + return this.find(selector, options).fetch()[0]; }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index e9de875f7c..f0affd485b 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1217,7 +1217,9 @@ Tinytest.add("minimongo - observe", function (test) { c.insert({a:4}); test.equal(operations.shift(), undefined); id = c.findOne({a:2})._id; - c.update({a:1}, {a:5}); + c.update({a:1}, {a:0}); + test.equal(operations.shift(), undefined); + c.update({a:0}, {a:5}); test.equal(operations.shift(), ['removed', id, 0, {a:2}]); test.equal(operations.shift(), ['added', {a:4}, 1]); From 2d9e8a8fd62a79069af0dfa1bf7ffb925a80ea17 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 11 Dec 2012 22:33:37 -0800 Subject: [PATCH 111/183] Move some tests around. No functional changes. --- packages/minimongo/minimongo_tests.js | 222 ++++++++++++++------------ 1 file changed, 116 insertions(+), 106 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index f0affd485b..777b8a73ec 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -59,123 +59,81 @@ var log_callbacks = function (operations) { }; // XXX test shared structure in all MM entrypoints +Tinytest.add("minimongo - basics", function (test) { + var c = new LocalCollection(); -_.each(['observe', '_observeUnordered'], function (observeMethod) { - Tinytest.add("minimongo - basics (" + observeMethod + ")", function (test) { - var c = new LocalCollection(); + c.insert({type: "kitten", name: "fluffy"}); + c.insert({type: "kitten", name: "snookums"}); + c.insert({type: "cryptographer", name: "alice"}); + c.insert({type: "cryptographer", name: "bob"}); + c.insert({type: "cryptographer", name: "cara"}); + test.equal(c.find().count(), 5); + test.equal(c.find({type: "kitten"}).count(), 2); + test.equal(c.find({type: "cryptographer"}).count(), 3); + test.length(c.find({type: "kitten"}).fetch(), 2); + test.length(c.find({type: "cryptographer"}).fetch(), 3); - c.insert({type: "kitten", name: "fluffy"}); - c.insert({type: "kitten", name: "snookums"}); - c.insert({type: "cryptographer", name: "alice"}); - c.insert({type: "cryptographer", name: "bob"}); - c.insert({type: "cryptographer", name: "cara"}); - test.equal(c.find().count(), 5); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 3); + c.remove({name: "cara"}); + test.equal(c.find().count(), 4); + test.equal(c.find({type: "kitten"}).count(), 2); + test.equal(c.find({type: "cryptographer"}).count(), 2); + test.length(c.find({type: "kitten"}).fetch(), 2); + test.length(c.find({type: "cryptographer"}).fetch(), 2); - c.remove({name: "cara"}); - test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 2); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 2); + c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); + test.equal(c.find().count(), 4); + test.equal(c.find({type: "kitten"}).count(), 1); + test.equal(c.find({type: "cryptographer"}).count(), 3); + test.length(c.find({type: "kitten"}).fetch(), 1); + test.length(c.find({type: "cryptographer"}).fetch(), 3); - c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); - test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 1); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 1); - test.length(c.find({type: "cryptographer"}).fetch(), 3); + c.remove(null); + c.remove(false); + c.remove(undefined); + test.equal(c.find().count(), 4); - c.remove(null); - c.remove(false); - c.remove(undefined); - test.equal(c.find().count(), 4); + c.remove({_id: null}); + c.remove({_id: false}); + c.remove({_id: undefined}); + c.remove(); + test.equal(c.find().count(), 4); - c.remove({_id: null}); - c.remove({_id: false}); - c.remove({_id: undefined}); - c.remove(); - test.equal(c.find().count(), 4); + c.remove({}); + test.equal(c.find().count(), 0); - c.remove({}); - test.equal(c.find().count(), 0); + c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); + c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); + c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); - c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); - c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); - c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + test.equal(c.find({tags: "flower"}).count(), 1); + test.equal(c.find({tags: "fruit"}).count(), 2); + test.equal(c.find({tags: "red"}).count(), 3); + test.length(c.find({tags: "flower"}).fetch(), 1); + test.length(c.find({tags: "fruit"}).fetch(), 2); + test.length(c.find({tags: "red"}).fetch(), 3); - test.equal(c.find({tags: "flower"}).count(), 1); - test.equal(c.find({tags: "fruit"}).count(), 2); - test.equal(c.find({tags: "red"}).count(), 3); - test.length(c.find({tags: "flower"}).fetch(), 1); - test.length(c.find({tags: "fruit"}).fetch(), 2); - test.length(c.find({tags: "red"}).fetch(), 3); + test.equal(c.findOne(1).name, "strawberry"); + test.equal(c.findOne(2).name, "apple"); + test.equal(c.findOne(3).name, "rose"); + test.equal(c.findOne(4), undefined); + test.equal(c.findOne("abc"), undefined); + test.equal(c.findOne(undefined), undefined); - test.equal(c.findOne(1).name, "strawberry"); - test.equal(c.findOne(2).name, "apple"); - test.equal(c.findOne(3).name, "rose"); - test.equal(c.findOne(4), undefined); - test.equal(c.findOne("abc"), undefined); - test.equal(c.findOne(undefined), undefined); + test.equal(c.find(1).count(), 1); + test.equal(c.find(4).count(), 0); + test.equal(c.find("abc").count(), 0); + test.equal(c.find(undefined).count(), 0); + test.equal(c.find().count(), 3); - test.equal(c.find(1).count(), 1); - test.equal(c.find(4).count(), 0); - test.equal(c.find("abc").count(), 0); - test.equal(c.find(undefined).count(), 0); - test.equal(c.find().count(), 3); + // Regression test for #455. + c.insert({foo: {bar: 'baz'}}); + test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); + test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); - // Regression test for #455. - c.insert({foo: {bar: 'baz'}}); - test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); - test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); - - // Duplicate ID. - test.throws(function () { c.insert({_id: 1, name: "bla"}); }); - test.equal(c.find({_id: 1}).count(), 1); - test.equal(c.findOne(1).name, "strawberry"); - - var ev = ""; - var makecb = function (tag) { - return { - added: function (doc) { ev += "a" + tag + doc._id + "_"; }, - changed: function (doc) { ev += "c" + tag + doc._id + "_"; }, - removed: function (doc) { ev += "r" + tag + doc._id + "_"; } - }; - }; - var expect = function (x) { - test.equal(ev, x); - ev = ""; - }; - // This should work equally well for ordered and unordered observations - // (because the callbacks don't look at indices and there's no 'moved' - // callback). - var handle = c.find({tags: "flower"})[observeMethod](makecb('a')); - expect("aa3_"); - c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); - expect("ra3_"); - c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); - expect("aa3_"); - c.update({name: "rose"}, {$set: {food: false}}); - expect("ca3_"); - c.remove({}); - expect("ra3_"); - c.insert({_id: 4, name: "daisy", tags: ["flower"]}); - expect("aa4_"); - handle.stop(); - // After calling stop, no more callbacks are called. - c.insert({_id: 5, name: "iris", tags: ["flower"]}); - expect(""); - - // Test that observing a lookup by ID works. - handle = c.find(4)[observeMethod](makecb('b')); - expect('ab4_'); - c.update(4, {$set: {eek: 5}}); - expect('cb4_'); - handle.stop(); - }); + // Duplicate ID. + test.throws(function () { c.insert({_id: 1, name: "bla"}); }); + test.equal(c.find({_id: 1}).count(), 1); + test.equal(c.findOne(1).name, "strawberry"); }); Tinytest.add("minimongo - cursors", function (test) { @@ -1159,7 +1117,7 @@ Tinytest.add("minimongo - modify", function (test) { // XXX test update() (selecting docs, multi, upsert..) -Tinytest.add("minimongo - observe", function (test) { +Tinytest.add("minimongo - observe ordered", function (test) { var operations = []; var cbs = log_callbacks(operations); var handle; @@ -1226,6 +1184,58 @@ Tinytest.add("minimongo - observe", function (test) { handle.stop(); }); +_.each(['observe', '_observeUnordered'], function (observeMethod) { + Tinytest.add("minimongo - observe (" + observeMethod + ")", function (test) { + var c = new LocalCollection(); + + var ev = ""; + var makecb = function (tag) { + return { + added: function (doc) { ev += "a" + tag + doc._id + "_"; }, + changed: function (doc) { ev += "c" + tag + doc._id + "_"; }, + removed: function (doc) { ev += "r" + tag + doc._id + "_"; } + }; + }; + var expect = function (x) { + test.equal(ev, x); + ev = ""; + }; + + c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); + c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); + c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + + // This should work equally well for ordered and unordered observations + // (because the callbacks don't look at indices and there's no 'moved' + // callback). + var handle = c.find({tags: "flower"})[observeMethod](makecb('a')); + expect("aa3_"); + c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); + expect("ra3_"); + c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); + expect("aa3_"); + c.update({name: "rose"}, {$set: {food: false}}); + expect("ca3_"); + c.remove({}); + expect("ra3_"); + c.insert({_id: 4, name: "daisy", tags: ["flower"]}); + expect("aa4_"); + handle.stop(); + // After calling stop, no more callbacks are called. + c.insert({_id: 5, name: "iris", tags: ["flower"]}); + expect(""); + + // Test that observing a lookup by ID works. + handle = c.find(4)[observeMethod](makecb('b')); + expect('ab4_'); + c.update(4, {$set: {eek: 5}}); + expect('cb4_'); + handle.stop(); + }); +}); + + + Tinytest.add("minimongo - diff", function (test) { // test correctness From 33fa24e912ba61796aade4432644286741016cf4 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 14 Dec 2012 19:50:32 -0800 Subject: [PATCH 112/183] Make find(1) and find({_id: 1}) return the same thing. --- packages/minimongo/minimongo.js | 10 ++++++++-- packages/minimongo/minimongo_tests.js | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index fa32a06623..366f2d478c 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -70,9 +70,9 @@ LocalCollection.Cursor = function (collection, selector, options) { } else { this.selector_f = LocalCollection._compileSelector(selector); this.sort_f = options.sort ? LocalCollection._compileSort(options.sort) : null; - this.skip = options.skip; - this.limit = options.limit; } + this.skip = options.skip; + this.limit = options.limit; // db_objects is a list of the objects that match the cursor. (It's always a // list, never an object: LocalCollection.Cursor is always ordered.) @@ -260,6 +260,12 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered) { // fast path for single ID value if (self.selector_id) { + // If you have non-zero skip and ask for a single id, you get + // nothing. This is so it matches the behavior of the '{_id: foo}' + // path. + if (self.skip) + return results; + if (_.has(self.collection.docs, self.selector_id)) { var selectedDoc = self.collection.docs[self.selector_id]; if (ordered) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 777b8a73ec..b3713eb25c 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -124,6 +124,8 @@ Tinytest.add("minimongo - basics", function (test) { test.equal(c.find("abc").count(), 0); test.equal(c.find(undefined).count(), 0); test.equal(c.find().count(), 3); + test.equal(c.find(1, {skip: 1}).count(), 0); + test.equal(c.find({_id: 1}, {skip: 1}).count(), 0); // Regression test for #455. c.insert({foo: {bar: 'baz'}}); From ac81438f74ceb5f801c76c55441c8813a03a5948 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Sat, 15 Dec 2012 17:32:25 -0800 Subject: [PATCH 113/183] Refinements to limit/skip observe support: - Decide that queries need to be recomputed in _modifyAndNotify directly instead of in _removeFromResults and friends; this is because the choice of which _fooFromResults function to invoke might not actually be correct in the presence of skip and limit. (The previous code still worked because all three of those functions had the same code for the skip/limit case.) - If update or remove affects more than one doc for a query, only recompute it once at the end instead of once per doc. --- packages/minimongo/minimongo.js | 72 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 366f2d478c..f84aad0674 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -204,7 +204,8 @@ _.extend(LocalCollection.Cursor.prototype, { sort_f: ordered && self.sort_f, results_snapshot: null, ordered: ordered, - cursor: this + cursor: this, + needsRecompute: false }; query.results = self._getRawObjects(ordered); if (self.collection.paused) @@ -349,9 +350,15 @@ LocalCollection.prototype.insert = function (doc) { // trigger live queries that match for (var qid in self.queries) { var query = self.queries[qid]; - if (query.selector_f(doc)) - LocalCollection._insertInResults(query, doc); + if (query.selector_f(doc)) { + if (query.cursor.skip || query.cursor.limit) + query.needsRecompute = true; + else + LocalCollection._insertInResults(query, doc); + } } + + self._recomputeNecessaryQueries(); }; LocalCollection.prototype.remove = function (selector) { @@ -377,8 +384,12 @@ LocalCollection.prototype.remove = function (selector) { var removeId = remove[i]; var removeDoc = self.docs[removeId]; _.each(self.queries, function (query) { - if (query.selector_f(removeDoc)) - queryRemove.push([query, removeDoc]); + if (query.selector_f(removeDoc)) { + if (query.cursor.skip || query.cursor.limit) + query.needsRecompute = true; + else + queryRemove.push([query, removeDoc]); + } }); self._saveOriginal(removeId, removeDoc); delete self.docs[removeId]; @@ -388,6 +399,8 @@ LocalCollection.prototype.remove = function (selector) { for (var i = 0; i < queryRemove.length; i++) { LocalCollection._removeFromResults(queryRemove[i][0], queryRemove[i][1]); } + + self._recomputeNecessaryQueries(); }; // XXX atomicity: if multi is true, and one modification fails, do @@ -405,7 +418,7 @@ LocalCollection.prototype.update = function (selector, mod, options) { self._saveOriginal(id, doc); self._modifyAndNotify(doc, mod); if (!options.multi) - return; + break; any = true; } } @@ -420,6 +433,8 @@ LocalCollection.prototype.update = function (selector, mod, options) { self.insert(insert); } } + + self._recomputeNecessaryQueries(); }; LocalCollection.prototype._modifyAndNotify = function (doc, mod) { @@ -443,12 +458,24 @@ LocalCollection.prototype._modifyAndNotify = function (doc, mod) { query = self.queries[qid]; var before = matched_before[qid]; var after = query.selector_f(doc); - if (before && !after) + + if (query.cursor.skip || query.cursor.limit) { + // We need to recompute any query where the doc may have been in the + // cursor's window either before or after the update. (Note that if skip + // or limit is set, "before" and "after" being true do not necessarily + // mean that the document is in the cursor's output after skip/limit is + // applied... but if they are false, then the document definitely is NOT + // in the output. So it's safe to skip recompute if neither before or + // after are true.) + if (before || after) + query.needsRecompute = true; + } else if (before && !after) { LocalCollection._removeFromResults(query, doc); - else if (!before && after) + } else if (!before && after) { LocalCollection._insertInResults(query, doc); - else if (before && after) + } else if (before && after) { LocalCollection._updateInResults(query, doc, old_doc); + } } }; @@ -480,11 +507,6 @@ LocalCollection._deepcopy = function (v) { // laughably inefficient. we recompute the whole results every time! LocalCollection._insertInResults = function (query, doc) { - if (query.cursor.skip || query.cursor.limit) { - LocalCollection._recomputeResults(query); - return; - } - if (query.ordered) { if (!query.sort_f) { query.added(LocalCollection._deepcopy(doc), query.results.length); @@ -501,11 +523,6 @@ LocalCollection._insertInResults = function (query, doc) { }; LocalCollection._removeFromResults = function (query, doc) { - if (query.cursor.skip || query.cursor.limit) { - LocalCollection._recomputeResults(query); - return; - } - if (query.ordered) { var i = LocalCollection._findInOrderedResults(query, doc); query.removed(doc, i); @@ -518,11 +535,6 @@ LocalCollection._removeFromResults = function (query, doc) { }; LocalCollection._updateInResults = function (query, doc, old_doc) { - if (query.cursor.skip || query.cursor.limit) { - LocalCollection._recomputeResults(query); - return; - } - if (doc._id !== old_doc._id) throw new Error("Can't change a doc's _id while updating"); @@ -547,7 +559,17 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { query.moved(LocalCollection._deepcopy(doc), orig_idx, new_idx); }; -LocalCollection._recomputeResults = function (query, doc) { +LocalCollection.prototype._recomputeNecessaryQueries = function () { + var self = this; + _.each(self.queries, function (query) { + if (query.needsRecompute) { + LocalCollection._recomputeResults(query); + query.needsRecompute = false; + } + }); +}; + +LocalCollection._recomputeResults = function (query) { var old_results = query.results; query.results = query.cursor._getRawObjects(query.ordered); From e32502c20c48b602bc437853f69787d0b51c6276 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 09:53:21 -0800 Subject: [PATCH 114/183] Add a fast path to limited unsorted unskipped queries. --- packages/minimongo/minimongo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index f84aad0674..0e328253c0 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -286,6 +286,10 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered) { else results[id] = doc; } + // Fast path for limited unsorted queries. + if (self.limit && !self.skip && !self.sort_f && + results.length === self.limit) + return results; } if (!ordered) From bd94569d863bf2832807187ea8fa0d45ae289e95 Mon Sep 17 00:00:00 2001 From: lvbreda Date: Thu, 1 Nov 2012 11:20:56 +0100 Subject: [PATCH 115/183] Added sorting functionality for 'a.b' --- packages/minimongo/sort.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js index 6d1d8077fe..9cd73e3327 100644 --- a/packages/minimongo/sort.js +++ b/packages/minimongo/sort.js @@ -44,14 +44,18 @@ LocalCollection._compileSort = function (spec) { var _func; var code = "_func = (function(c){return function(a,b){var x;"; for (var i = 0; i < keys.length; i++) { - if (i !== 0) - code += "if(x!==0)return x;"; - code += "x=" + (asc[i] ? "" : "-") + - "c(a[" + JSON.stringify(keys[i]) + "],b[" + - JSON.stringify(keys[i]) + "]);"; + var splittedKeys = keys[i].split("."); + var keyString = ""; + for(o = 0;o Date: Wed, 12 Dec 2012 11:59:21 -0800 Subject: [PATCH 116/183] Test for sub-key sort. Doesn't pass yet. --- packages/minimongo/minimongo_tests.js | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index b3713eb25c..16331319ae 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -875,6 +875,40 @@ Tinytest.add("minimongo - sort", function (test) { {a: 47, b: 1, _id: "47_1"}]); }); +Tinytest.add("minimongo - subkey sort", function (test) { + var c = new LocalCollection(); + + // normal case + c.insert({a: {b: 1}}); + c.insert({a: {b: 2}}); + c.insert({a: {b: 3}}); + test.equal( + _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + [{b: 3}, {b: 2}, {b: 1}]); + + // isn't an object + c.insert({a: 1}); + test.equal( + _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + [{b: 3}, {b: 2}, {b: 1}, 1]); + + // complex object + c.insert({a: {b: {c: 1}}}); + test.equal( + _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1]); + + // no such top level prop + c.insert({c: 1}); + test.equal( + _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1, undefined]); + + // no such mid level prop. just test that it doesn't throw. + c.find({}, {sort: {'a.nope.c': -1}}).fetch(); +}); + + Tinytest.add("minimongo - modify", function (test) { var modify = function (doc, mod, result) { var copy = LocalCollection._deepcopy(doc); From 4c78d3dc5116d4fbf1f146019ef425e6f2f1a50a Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 12 Dec 2012 12:00:38 -0800 Subject: [PATCH 117/183] Make sub-key sort check for undefined before dereferencing. This way it doesn't throw when given an object that doesn't contain all the keys being searched on. --- packages/minimongo/sort.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js index 9cd73e3327..53114153dc 100644 --- a/packages/minimongo/sort.js +++ b/packages/minimongo/sort.js @@ -11,8 +11,6 @@ // first object comes first in order, 1 if the second object comes // first, or 0 if neither object comes before the other. -// XXX sort does not yet support subkeys ('a.b') .. fix that! - LocalCollection._compileSort = function (spec) { var keys = []; var asc = []; @@ -44,16 +42,22 @@ LocalCollection._compileSort = function (spec) { var _func; var code = "_func = (function(c){return function(a,b){var x;"; for (var i = 0; i < keys.length; i++) { - var splittedKeys = keys[i].split("."); - var keyString = ""; - for(o = 0;o Date: Fri, 14 Dec 2012 20:05:22 -0800 Subject: [PATCH 118/183] Feedback from review. --- packages/minimongo/minimongo_tests.js | 8 ++++---- packages/minimongo/sort.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 16331319ae..ec3cf13f4a 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -879,8 +879,8 @@ Tinytest.add("minimongo - subkey sort", function (test) { var c = new LocalCollection(); // normal case - c.insert({a: {b: 1}}); c.insert({a: {b: 2}}); + c.insert({a: {b: 1}}); c.insert({a: {b: 3}}); test.equal( _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), @@ -889,8 +889,8 @@ Tinytest.add("minimongo - subkey sort", function (test) { // isn't an object c.insert({a: 1}); test.equal( - _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), - [{b: 3}, {b: 2}, {b: 1}, 1]); + _.pluck(c.find({}, {sort: {'a.b': 1}}).fetch(), 'a'), + [1, {b: 1}, {b: 2}, {b: 3}]); // complex object c.insert({a: {b: {c: 1}}}); @@ -905,7 +905,7 @@ Tinytest.add("minimongo - subkey sort", function (test) { [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1, undefined]); // no such mid level prop. just test that it doesn't throw. - c.find({}, {sort: {'a.nope.c': -1}}).fetch(); + test.equal(c.find({}, {sort: {'a.nope.c': -1}}).count(), 6); }); diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js index 53114153dc..94420cf237 100644 --- a/packages/minimongo/sort.js +++ b/packages/minimongo/sort.js @@ -53,7 +53,7 @@ LocalCollection._compileSort = function (spec) { aCode += '&&a' + keyString; bCode += '&&b' + keyString; } - if (i !== 0){ + if (i !== 0) { code += "if(x!==0)return x;"; } code += "x=" + (asc[i] ? "" : "-") + From fcfc7dd4c1ef807d947674b8d5aa7ddcaba4e322 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 17 Dec 2012 19:56:54 -0800 Subject: [PATCH 119/183] add bug-fixed isaacs/fstream and have tar use it --- admin/generate-dev-bundle.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh index 9400830014..1e2f046db6 100755 --- a/admin/generate-dev-bundle.sh +++ b/admin/generate-dev-bundle.sh @@ -109,6 +109,13 @@ npm install progress@0.0.5 # which make the dev bundle much bigger. We need a better solution. npm install mailcomposer@0.1.15 +# Use our version of fstream with a bug fixed. Also have tar use it. +# See https://github.com/isaacs/fstream/pull/11 . +npm install https://github.com/meteor/fstream/tarball/91c56e7 +cd tar/node_modules +npm install https://github.com/meteor/fstream/tarball/91c56e7 +cd ../.. + # If you update the version of fibers in the dev bundle, also update the "npm # install" command in docs/client/concepts.html. npm install fibers@0.6.9 From 7f55a7e928095ee60b022b67ed72ab0a3cecf1e0 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 17 Dec 2012 19:57:26 -0800 Subject: [PATCH 120/183] bump dev-bundle version --- admin/generate-dev-bundle.sh | 2 +- meteor | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/generate-dev-bundle.sh b/admin/generate-dev-bundle.sh index 1e2f046db6..e3a1e2ad14 100755 --- a/admin/generate-dev-bundle.sh +++ b/admin/generate-dev-bundle.sh @@ -3,7 +3,7 @@ set -e set -u -BUNDLE_VERSION=0.2.11 +BUNDLE_VERSION=0.2.12 UNAME=$(uname) ARCH=$(uname -m) diff --git a/meteor b/meteor index 958a8cc51c..6ff0939e5d 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.2.11 +BUNDLE_VERSION=0.2.12 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. From 2d2aaa80823c14ec1e7d02bad4ef26cdd9d71809 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 21:03:56 -0800 Subject: [PATCH 121/183] minimongo: Implement ordinal indexing: {'foo.1.bar': 42} --- packages/minimongo/minimongo_tests.js | 5 ++++- packages/minimongo/selector.js | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index ec3cf13f4a..92ce8e69f4 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -795,9 +795,12 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({$where: "_.isArray(this.a)"}, {a: []}); nomatch({$where: "_.isArray(this.a)"}, {a: 1}); + match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({"dogs.1.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + // XXX still needs tests: // - $elemMatch - // - people.2.name // - non-scalar arguments to $gt, $lt, etc }); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 5d1dfe7b99..e462b5e611 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -291,8 +291,6 @@ LocalCollection._selectorIsId = function (selector) { return (typeof selector === "string") || (typeof selector === "number"); }; -// XXX implement ordinal indexing: 'people.2.name' - // Given an arbitrary Mongo-style query selector, return an expression // that evaluates to true if the document in 'doc' matches the // selector, else false. @@ -396,19 +394,31 @@ LocalCollection._exprForKeypathPredicate = function (keypath, value, literals) { // drilling down through the dotted parts var ret = ''; var innermost = true; + var lastPartWasNumber = false; while (keyparts.length) { var part = keyparts.pop(); + var thisPartIsNumber = false; + if (/^\d+/.test(part)) { + part = +part; + thisPartIsNumber = true; + } var formal = keyparts.length ? "x" : "doc"; if (innermost) { ret = '(function(x){return ' + predcode + ';})(' + formal + '&&' + formal + '[' + JSON.stringify(part) + '])'; innermost = false; + } else if (lastPartWasNumber) { + // The last part was an array index, so if we find an array here we + // shouldn't search it! + ret = '(function(x){return ' + ret + ';})(' + formal + '&&' + formal + '[' + + JSON.stringify(part) + '])'; } else { - // for all but the innermost level of a dotted expression, - // if the runtime type is an array, search it + // If the runtime type is an array, search it, unless we're already at the + // innermost bit, or if the next part is a number (ie, an array index). ret = 'f._matches(' + formal + '&&' + formal + '[' + JSON.stringify(part) + '], function(x){return ' + ret + ';})'; } + lastPartWasNumber = thisPartIsNumber; } return ret; From e9913d2055ec450236701ac48161c38d801412ae Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 23:39:06 -0800 Subject: [PATCH 122/183] Fix to skip/limit observe: update would never produce 'changed' calls. Also remove dead upsert code. --- packages/minimongo/minimongo.js | 86 +++++++++++++-------------- packages/minimongo/minimongo_tests.js | 2 + 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 0e328253c0..35dddf1a1f 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -204,8 +204,7 @@ _.extend(LocalCollection.Cursor.prototype, { sort_f: ordered && self.sort_f, results_snapshot: null, ordered: ordered, - cursor: this, - needsRecompute: false + cursor: this }; query.results = self._getRawObjects(ordered); if (self.collection.paused) @@ -351,24 +350,30 @@ LocalCollection.prototype.insert = function (doc) { self._saveOriginal(doc._id, undefined); self.docs[doc._id] = doc; + var queriesToRecompute = []; + // trigger live queries that match for (var qid in self.queries) { var query = self.queries[qid]; if (query.selector_f(doc)) { if (query.cursor.skip || query.cursor.limit) - query.needsRecompute = true; + queriesToRecompute.push(query); else LocalCollection._insertInResults(query, doc); } } - self._recomputeNecessaryQueries(); + _.each(queriesToRecompute, function (query) { + LocalCollection._recomputeResults(query); + }); }; LocalCollection.prototype.remove = function (selector) { var self = this; var remove = []; + var queriesToRecompute = []; + // Avoid O(n) for "remove a single doc by ID". if (LocalCollection._selectorIsId(selector)) { if (_.has(self.docs, selector)) @@ -390,7 +395,7 @@ LocalCollection.prototype.remove = function (selector) { _.each(self.queries, function (query) { if (query.selector_f(removeDoc)) { if (query.cursor.skip || query.cursor.limit) - query.needsRecompute = true; + queriesToRecompute.push(query); else queryRemove.push([query, removeDoc]); } @@ -403,8 +408,9 @@ LocalCollection.prototype.remove = function (selector) { for (var i = 0; i < queryRemove.length; i++) { LocalCollection._removeFromResults(queryRemove[i][0], queryRemove[i][1]); } - - self._recomputeNecessaryQueries(); + _.each(queriesToRecompute, function (query) { + LocalCollection._recomputeResults(query); + }); }; // XXX atomicity: if multi is true, and one modification fails, do @@ -415,33 +421,34 @@ LocalCollection.prototype.update = function (selector, mod, options) { var self = this; var any = false; var selector_f = LocalCollection._compileSelector(selector); + + var qidToOriginalResults = {}; + _.each(self.queries, function (query, qid) { + if ((query.cursor.skip || query.cursor.limit) && !query.paused) + qidToOriginalResults[qid] = LocalCollection._deepcopy(query.results); + }); + var recomputeQids = {}; + for (var id in self.docs) { var doc = self.docs[id]; if (selector_f(doc)) { // XXX Should we save the original even if mod ends up being a no-op? self._saveOriginal(id, doc); - self._modifyAndNotify(doc, mod); + self._modifyAndNotify(doc, mod, recomputeQids); if (!options.multi) break; any = true; } } - if (options.upsert) { - throw Error("upsert not yet implemented"); - if (!any) { - // XXX is this actually right? don't we have to resolve/delete $-ops or - // something like that? - var insert = LocalCollection._deepcopy(selector); - LocalCollection._modify(insert, mod); - self.insert(insert); - } - } - - self._recomputeNecessaryQueries(); + _.each(recomputeQids, function (dummy, qid) { + LocalCollection._recomputeResults(self.queries[qid], + qidToOriginalResults[qid]); + }); }; -LocalCollection.prototype._modifyAndNotify = function (doc, mod) { +LocalCollection.prototype._modifyAndNotify = function ( + doc, mod, recomputeQids) { var self = this; var matched_before = {}; @@ -450,6 +457,8 @@ LocalCollection.prototype._modifyAndNotify = function (doc, mod) { if (query.ordered) { matched_before[qid] = query.selector_f(doc); } else { + // Because we don't support skip or limit (yet) in unordered queries, we + // can just do a direct lookup. matched_before[qid] = _.has(query.results, doc._id); } } @@ -464,15 +473,7 @@ LocalCollection.prototype._modifyAndNotify = function (doc, mod) { var after = query.selector_f(doc); if (query.cursor.skip || query.cursor.limit) { - // We need to recompute any query where the doc may have been in the - // cursor's window either before or after the update. (Note that if skip - // or limit is set, "before" and "after" being true do not necessarily - // mean that the document is in the cursor's output after skip/limit is - // applied... but if they are false, then the document definitely is NOT - // in the output. So it's safe to skip recompute if neither before or - // after are true.) - if (before || after) - query.needsRecompute = true; + recomputeQids[qid] = true; } else if (before && !after) { LocalCollection._removeFromResults(query, doc); } else if (!before && after) { @@ -563,23 +564,22 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { query.moved(LocalCollection._deepcopy(doc), orig_idx, new_idx); }; -LocalCollection.prototype._recomputeNecessaryQueries = function () { - var self = this; - _.each(self.queries, function (query) { - if (query.needsRecompute) { - LocalCollection._recomputeResults(query); - query.needsRecompute = false; - } - }); -}; - -LocalCollection._recomputeResults = function (query) { - var old_results = query.results; +// Recomputes the results of a query and runs observe callbacks for the +// difference between the previous results and the current results (unless +// paused). Used for skip/limit queries. +// +// When this is used by insert or remove, it can just use query.results for the +// old results (and there's no need to pass in oldResults), because these +// operations don't mutate the documents in the collection. Update needs to pass +// in an oldResults which was deep-copied before the modifier was applied. +LocalCollection._recomputeResults = function (query, oldResults) { + if (!oldResults) + oldResults = query.results; query.results = query.cursor._getRawObjects(query.ordered); if (!query.paused) LocalCollection._diffQuery( - query.ordered, old_results, query.results, query, true); + query.ordered, oldResults, query.results, query, true); }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 92ce8e69f4..706907c6e9 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1219,6 +1219,8 @@ Tinytest.add("minimongo - observe ordered", function (test) { c.update({a:0}, {a:5}); test.equal(operations.shift(), ['removed', id, 0, {a:2}]); test.equal(operations.shift(), ['added', {a:4}, 1]); + c.update({a:3}, {a:3.5}); + test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); handle.stop(); }); From 8ad174eeadb5c74a362e4092afb36dfb663e2f5d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 18 Dec 2012 10:19:53 -0800 Subject: [PATCH 123/183] Feedback from review of skip/limit fix: - Keep upsert as an error. - Keep if (before||after) check. - Add some comments. --- packages/minimongo/minimongo.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 35dddf1a1f..6677e7819f 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -416,12 +416,19 @@ LocalCollection.prototype.remove = function (selector) { // XXX atomicity: if multi is true, and one modification fails, do // we rollback the whole operation, or what? LocalCollection.prototype.update = function (selector, mod, options) { + var self = this; if (!options) options = {}; - var self = this; - var any = false; + if (options.upsert) + throw new Error("upsert not yet implemented"); + var selector_f = LocalCollection._compileSelector(selector); + // Save the original results of any query that we might need to + // _recomputeResults on, because _modifyAndNotify will mutate the objects in + // it. (We don't need to save the original results of paused queries because + // they already have a results_snapshot and we won't be diffing in + // _recomputeResults.) var qidToOriginalResults = {}; _.each(self.queries, function (query, qid) { if ((query.cursor.skip || query.cursor.limit) && !query.paused) @@ -437,7 +444,6 @@ LocalCollection.prototype.update = function (selector, mod, options) { self._modifyAndNotify(doc, mod, recomputeQids); if (!options.multi) break; - any = true; } } @@ -473,7 +479,15 @@ LocalCollection.prototype._modifyAndNotify = function ( var after = query.selector_f(doc); if (query.cursor.skip || query.cursor.limit) { - recomputeQids[qid] = true; + // We need to recompute any query where the doc may have been in the + // cursor's window either before or after the update. (Note that if skip + // or limit is set, "before" and "after" being true do not necessarily + // mean that the document is in the cursor's output after skip/limit is + // applied... but if they are false, then the document definitely is NOT + // in the output. So it's safe to skip recompute if neither before or + // after are true.) + if (before || after) + recomputeQids[qid] = true; } else if (before && !after) { LocalCollection._removeFromResults(query, doc); } else if (!before && after) { From e0532012f6c4c6cc1b3dac8e667cb77fbf6e6ea0 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 18 Dec 2012 14:47:27 -0800 Subject: [PATCH 124/183] Remove --debug option, replace with general NODE_OPTIONS env var --- app/meteor/meteor.js | 12 ++--------- app/meteor/run.js | 41 ++++++++++++++++++------------------ docs/client/commandline.html | 3 +++ 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index c2aeb819ac..7f0a29d5d9 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -117,10 +117,6 @@ Commands.push({ .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.') - .boolean('debug') - .describe('debug', 'Pass --debug to node.js to enable node-inspector debugging.') - .boolean('debug-brk') - .describe('debug-brk', 'Pass --debug-brk to node.js to enable debugging and break on the first line.') .describe('settings', 'Set optional data for Meteor.settings on the server') .boolean('once') .usage( @@ -151,10 +147,7 @@ Commands.push({ var app_dir = path.resolve(require_project("run", true)); // app or package var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true }; - var debugStatus = "OFF"; - if (new_argv['debug']) debugStatus = "DEBUG"; - if (new_argv['debug-brk']) debugStatus = "BREAK"; - runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, settings, debugStatus); + runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, settings); } }); @@ -573,7 +566,7 @@ Commands.push({ "the password. You can change the password with a second 'deploy' command." ); - new_argv = opt.argv; + var new_argv = opt.argv; if (argv.help || new_argv._.length != 2) { process.stdout.write(opt.help()); @@ -682,4 +675,3 @@ var main = function() { }; main(); - diff --git a/app/meteor/run.js b/app/meteor/run.js index be493d1b62..530aed4a52 100644 --- a/app/meteor/run.js +++ b/app/meteor/run.js @@ -74,8 +74,17 @@ var Status = { } }; +// Parse out s as if it were a bash command line. +var bashParse = function (s) { + if (s.search("\"") !== -1 || s.search("'") !== -1) { + throw new Error("Meteor cannot currently handle quoted NODE_OPTIONS"); + } + return _.without(s.split(/\s+/), ''); +}; - +var getNodeOptionsFromEnvironment = function () { + return bashParse(process.env.NODE_OPTIONS || ""); +}; // List of queued requests. Each item in the list is a function to run // when the inner app is ready to receive connections. @@ -203,18 +212,17 @@ var log_to_clients = function (msg) { // mongoURL // onExit // [onListen] -// [debugStatus] -// +// [nodeOptions] // [runOnce]: boolean; default false; if true doesn't ever try to restart, and // forwards server exit code. // [settings] var start_server = function (options) { // environment - options = _.extend({runOnce: false, - debugStatus: "OFF" - }, - options); + options = _.extend({ + runOnce: false, + nodeOptions: [] + }, options); if (options.runOnce) { Status.shouldRestart = false; } @@ -226,22 +234,15 @@ var start_server = function (options) { env.PORT = options.innerPort; env.MONGO_URL = options.mongoURL; env.ROOT_URL = env.ROOT_URL || ('http://localhost:' + options.outerPort); - - var dbg = options.debugStatus; - var nodeOptions = []; - if (dbg === "DEBUG") - nodeOptions.push('--debug'); - if (dbg === "BREAK") { - console.log('Debug will break on the first line'); - nodeOptions.push('--debug-brk'); - } - if (options.settings) env.METEOR_SETTINGS = options.settings; + var nodeOptions = _.clone(options.nodeOptions); + nodeOptions.push(path.join(options.bundlePath, 'main.js')); + nodeOptions.push('--keepalive'); var proc = spawn(process.execPath, - nodeOptions.concat([path.join(options.bundlePath, 'main.js'), '--keepalive']), + nodeOptions, {env: env}); // XXX deal with test server logging differently?! @@ -511,7 +512,7 @@ var start_update_checks = function () { // This function never returns and will call process.exit() if it // can't continue. If you change this, remember to call // watcher.destroy() as appropriate. -exports.run = function (app_dir, bundle_opts, port, once, settings, dbg) { +exports.run = function (app_dir, bundle_opts, port, once, settings) { var outer_port = port || 3000; var inner_port = outer_port + 1; var mongo_port = outer_port + 2; @@ -631,7 +632,7 @@ exports.run = function (app_dir, bundle_opts, port, once, settings, dbg) { _.each(request_queue, function (f) { f(); }); request_queue = []; }, - debugStatus: dbg, + nodeOptions: getNodeOptionsFromEnvironment(), runOnce: once, settings: settings }); diff --git a/docs/client/commandline.html b/docs/client/commandline.html index 5f8ab1f0b9..619e3f0472 100644 --- a/docs/client/commandline.html +++ b/docs/client/commandline.html @@ -29,6 +29,9 @@ required. This is the default command. Simply running `meteor` is the same as `meteor run`. +To pass additional options to Node.js use the `NODE_OPTIONS` environment variable. +For example: `NODE_OPTIONS='--debug'` or `NODE_OPTIONS='--debug-brk'` + Run `meteor help run` to see the full list of options. From 326b316d3657d9341a977152f01e02c1fcf795ba Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 18 Dec 2012 16:00:07 -0800 Subject: [PATCH 125/183] Mark messages as unsent when we reconnect. Sent means 'sent on this connection' not 'ever sent'. Fixes #538. --- packages/livedata/livedata_connection.js | 6 +++ .../livedata/livedata_connection_tests.js | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index ad2aae5e04..dd0644a2ef 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -214,6 +214,12 @@ Meteor._LivedataConnection = function (url, options) { self._outstandingMethodBlocks.shift(); } + // Mark all messages as unsent, they have not yet been sent on this + // connection. + _.each(self._methodInvokers, function (m) { + m.sentMessage = false; + }); + // If an `onReconnect` handler is set, call it first. Go through // some hoops to ensure that methods that are called from within // `onReconnect` get executed _before_ ones that were originally diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 127b273c83..86c0cd3967 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1051,6 +1051,49 @@ 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 conn = newConnection(stream); + startAndConnect(test, stream); + + // setup method + conn.methods({do_something: function (x) {}}); + + conn.onReconnect = function() { + conn.apply('do_something', ['login'], {wait: true}); + }; + + conn.apply('do_something', ['one']); + + // initial connect + stream.sent = []; + stream.reset(); + testGotMessage( + test, stream, {msg: 'connect', session: conn._lastSessionId}); + + // Test that we sent just the login message. + var loginId = testGotMessage( + test, stream, {msg: 'method', method: 'do_something', + params: ['login'], id: '*'}); + + // we connect. + stream.receive({msg: 'connected', session: Meteor.uuid()}); + test.length(stream.sent, 0); + + // login got result (but not yet data) + stream.receive({msg: 'result', id: loginId, result: 'foo'}); + test.length(stream.sent, 0); + + // login got data. now we send next method. + stream.receive({msg: 'data', methods: [loginId]}); + + testGotMessage( + test, stream, {msg: 'method', method: 'do_something', + params: ['one'], id: '*'}); +}); + + + Tinytest.add("livedata stub - reconnect double wait method", function (test) { var stream = new Meteor._StubStream; var conn = newConnection(stream); From 27382c62eecd72665194374bff28859f94e7ec98 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Thu, 13 Dec 2012 11:33:36 -0800 Subject: [PATCH 126/183] meteor.js: wrap in Fiber, use npm underscore Conflicts: app/meteor/meteor.js --- app/meteor/meteor.js | 1280 +++++++++++++++++++++--------------------- 1 file changed, 642 insertions(+), 638 deletions(-) diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index 7f0a29d5d9..d1a0b0004d 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -1,677 +1,681 @@ -var path = require('path'); -var files = require(path.join(__dirname, '..', 'lib', 'files.js')); -var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js')); -var deploy = require(path.join(__dirname, 'deploy')); -var fs = require("fs"); -var runner = require(path.join(__dirname, 'run.js')); +var Fiber = require('fibers'); +Fiber(function () { -// This code is duplicated in app/server/server.js. -var MIN_NODE_VERSION = 'v0.8.11'; -if (require('semver').lt(process.version, MIN_NODE_VERSION)) { - process.stderr.write( - 'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n'); - process.exit(1); -} + var path = require('path'); + var files = require(path.join(__dirname, '..', 'lib', 'files.js')); + var _ = require('underscore'); + var deploy = require(path.join(__dirname, 'deploy')); + var fs = require("fs"); + var runner = require(path.join(__dirname, 'run.js')); -var usage = function() { - process.stdout.write( -"Usage: meteor [--version] [--help] []\n" + -"\n" + -"With no arguments, 'meteor' runs the project in the current\n" + -"directory in local development mode. You can run it from the root\n" + -"directory of the project or from any subdirectory.\n" + -"\n" + -"Use 'meteor create ' to create a new Meteor project.\n" + -"\n" + -"Commands:\n"); - _.each(Commands, function (cmd) { - if (cmd.help) { - var name = cmd.name + " ".substr(cmd.name.length); - process.stdout.write(" " + name + cmd.help + "\n"); - } - }); - process.stdout.write("\n"); -process.stdout.write( -"See 'meteor help ' for details on a command.\n"); - process.exit(1); -}; - -var require_project = function (cmd, accept_package) { - var app_dir = files.find_upwards(files.is_app_dir); - if (app_dir) - return app_dir; - - var package_dir = files.find_upwards(function (p) { - return files.is_package_dir(p) || files.is_package_collection_dir(p); - }); - if (package_dir) { - if (accept_package) - return package_dir; - - process.stdout.write(cmd + ": Only works on applications, not packages\n"); + // This code is duplicated in app/server/server.js. + var MIN_NODE_VERSION = 'v0.8.11'; + if (require('semver').lt(process.version, MIN_NODE_VERSION)) { + process.stderr.write( + 'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n'); process.exit(1); } - // This is where you end up if you type 'meteor' with no - // args. Be gentle to the noobs.. - process.stdout.write( -cmd + ": You're not in a Meteor project directory.\n" + -"\n" + -"To create a new Meteor project:\n" + -" meteor create \n" + -"For example:\n" + -" meteor create myapp\n" + -"\n" + -"For more help, see 'meteor --help'.\n"); - process.exit(1); -}; - -var find_mongo_port = function (cmd, callback) { - var app_dir = require_project(cmd); - var mongo_runner = require(path.join(__dirname, '..', 'lib', 'mongo_runner.js')); - mongo_runner.find_mongo_port(app_dir, callback); -}; - -Commands = []; - -var findCommand = function (name) { - for (var i = 0; i < Commands.length; i++) - if (Commands[i].name === name) - return Commands[i]; - process.stdout.write("'" + name + "' is not a Meteor command. See " + - "'meteor --help'.\n"); - process.exit(1); -}; - -var getSettings = function (filename) { - var str; - try { - str = fs.readFileSync(filename, "utf8"); - } catch (e) { - throw new Error("Could not find settings file " + filename); - } - if (str.length > 0x10000) { - throw new Error("Settings file must be less than 64 KB long"); - } - // Ensure that the string is parseable in JSON, but there's - // no reason to use the object value of it yet. - if (str.match(/\S/)) { - JSON.parse(str); - return str; - } else { - return ""; - } -}; - -// XXX when the pass unexpected argument or unrecognized flags, print -// an error and fail out - -Commands.push({ - name: "run", - help: "[default] Run this project in local development mode", - func: function (argv) { - // 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') - .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"); - - var new_argv = opt.argv; - var settings = ""; - - if (argv.help) { - process.stdout.write(opt.help()); - process.exit(1); - } - if (new_argv.settings) { - settings = getSettings(new_argv.settings); - } - - var app_dir = path.resolve(require_project("run", true)); // app or package - - var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true }; - runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, settings); - } -}); - -Commands.push({ - name: "help", - func: function (argv) { - if (!argv._.length || argv.help) - usage(); - var cmd = argv._.splice(0,1)[0]; - argv.help = true; - findCommand(cmd).func(argv); - } -}); - -Commands.push({ - name: "create", - help: "Create a new project", - func: 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 \n" + -" meteor create --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" + -"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; - var appname; - - var example_dir = path.join(__dirname, '..', '..', 'examples'); - var examples = _.reject(fs.readdirSync(example_dir), function (e) { - return (e === 'unfinished' || e === 'other'); + var usage = function() { + process.stdout.write( + "Usage: meteor [--version] [--help] []\n" + + "\n" + + "With no arguments, 'meteor' runs the project in the current\n" + + "directory in local development mode. You can run it from the root\n" + + "directory of the project or from any subdirectory.\n" + + "\n" + + "Use 'meteor create ' to create a new Meteor project.\n" + + "\n" + + "Commands:\n"); + _.each(Commands, function (cmd) { + if (cmd.help) { + var name = cmd.name + " ".substr(cmd.name.length); + process.stdout.write(" " + name + cmd.help + "\n"); + } }); + process.stdout.write("\n"); + process.stdout.write( + "See 'meteor help ' for details on a command.\n"); + process.exit(1); + }; - if (argv._.length === 1) { - appname = argv._[0]; - } else if (argv._.length === 0 && new_argv.example) { - appname = new_argv.example; - } + var require_project = function (cmd, accept_package) { + var app_dir = files.find_upwards(files.is_app_dir); + if (app_dir) + return app_dir; - if (new_argv['list']) { - process.stdout.write("Available examples:\n"); - _.each(examples, function (e) { - process.stdout.write(" " + e + "\n"); - }); - process.stdout.write("\n" + -"Create a project from an example with 'meteor create --example '.\n") - process.exit(1); - }; + var package_dir = files.find_upwards(function (p) { + return files.is_package_dir(p) || files.is_package_collection_dir(p); + }); + if (package_dir) { + if (accept_package) + return package_dir; - if (argv.help || !appname) { - process.stdout.write(opt.help()); + process.stdout.write(cmd + ": Only works on applications, not packages\n"); process.exit(1); } - if (fs.existsSync(appname)) { - process.stderr.write(appname + ": Already exists\n"); - process.exit(1); + // This is where you end up if you type 'meteor' with no + // args. Be gentle to the noobs.. + process.stdout.write( + cmd + ": You're not in a Meteor project directory.\n" + + "\n" + + "To create a new Meteor project:\n" + + " meteor create \n" + + "For example:\n" + + " meteor create myapp\n" + + "\n" + + "For more help, see 'meteor --help'.\n"); + process.exit(1); + }; + + var find_mongo_port = function (cmd, callback) { + var app_dir = require_project(cmd); + var mongo_runner = require(path.join(__dirname, '..', 'lib', 'mongo_runner.js')); + mongo_runner.find_mongo_port(app_dir, callback); + }; + + Commands = []; + + var findCommand = function (name) { + for (var i = 0; i < Commands.length; i++) + if (Commands[i].name === name) + return Commands[i]; + process.stdout.write("'" + name + "' is not a Meteor command. See " + + "'meteor --help'.\n"); + process.exit(1); + }; + + var getSettings = function (filename) { + var str; + try { + str = fs.readFileSync(filename, "utf8"); + } catch (e) { + throw new Error("Could not find settings file " + filename); } - - if (files.find_app_dir(appname)) { - process.stderr.write( -"You can't create a Meteor project inside another Meteor project.\n"); - process.exit(1); + if (str.length > 0x10000) { + throw new Error("Settings file must be less than 64 KB long"); } + // Ensure that the string is parseable in JSON, but there's + // no reason to use the object value of it yet. + if (str.match(/\S/)) { + JSON.parse(str); + return str; + } else { + return ""; + } + }; - var transform = function (x) { - return x.replace(/~name~/g, path.basename(appname)); - }; + // XXX when the pass unexpected argument or unrecognized flags, print + // an error and fail out - if (new_argv.example) { - if (examples.indexOf(new_argv.example) === -1) { - process.stderr.write(new_argv.example + ": no such example\n\n"); - process.stderr.write("List available applications with 'meteor create --list'.\n"); + Commands.push({ + name: "run", + help: "[default] Run this project in local development mode", + func: function (argv) { + // 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') + .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"); + + var new_argv = opt.argv; + var settings = ""; + + if (argv.help) { + process.stdout.write(opt.help()); process.exit(1); + } + if (new_argv.settings) { + settings = getSettings(new_argv.settings); + } + + var app_dir = path.resolve(require_project("run", true)); // app or package + + var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true }; + runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, settings); + } + }); + + Commands.push({ + name: "help", + func: function (argv) { + if (!argv._.length || argv.help) + usage(); + var cmd = argv._.splice(0,1)[0]; + argv.help = true; + findCommand(cmd).func(argv); + } + }); + + Commands.push({ + name: "create", + help: "Create a new project", + func: 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 \n" + + " meteor create --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" + + "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; + var appname; + + var example_dir = path.join(__dirname, '..', '..', 'examples'); + var examples = _.reject(fs.readdirSync(example_dir), function (e) { + return (e === 'unfinished' || e === 'other'); + }); + + if (argv._.length === 1) { + appname = argv._[0]; + } else if (argv._.length === 0 && new_argv.example) { + appname = new_argv.example; + } + + if (new_argv['list']) { + process.stdout.write("Available examples:\n"); + _.each(examples, function (e) { + process.stdout.write(" " + e + "\n"); + }); + process.stdout.write("\n" + + "Create a project from an example with 'meteor create --example '.\n") + process.exit(1); + }; + + if (argv.help || !appname) { + process.stdout.write(opt.help()); + process.exit(1); + } + + if (fs.existsSync(appname)) { + process.stderr.write(appname + ": Already exists\n"); + process.exit(1); + } + + if (files.find_app_dir(appname)) { + process.stderr.write( + "You can't create a Meteor project inside another Meteor project.\n"); + process.exit(1); + } + + var transform = function (x) { + return x.replace(/~name~/g, path.basename(appname)); + }; + + if (new_argv.example) { + if (examples.indexOf(new_argv.example) === -1) { + process.stderr.write(new_argv.example + ": no such example\n\n"); + process.stderr.write("List available applications with 'meteor create --list'.\n"); + process.exit(1); + } else { + files.cp_r(path.join(example_dir, new_argv.example), appname, { + ignore: [/^local$/] + }); + } } else { - files.cp_r(path.join(example_dir, new_argv.example), appname, { + files.cp_r(path.join(__dirname, 'skel'), appname, { + transform_filename: function (f) { + return transform(f); + }, + transform_contents: function (contents, f) { + if ((/(\.html|\.js|\.css)/).test(f)) + return new Buffer(transform(contents.toString())); + else + return contents; + }, ignore: [/^local$/] }); } - } else { - files.cp_r(path.join(__dirname, 'skel'), appname, { - transform_filename: function (f) { - return transform(f); - }, - transform_contents: function (contents, f) { - if ((/(\.html|\.js|\.css)/).test(f)) - return new Buffer(transform(contents.toString())); - else - return contents; - }, - ignore: [/^local$/] - }); + + process.stderr.write(appname + ": created"); + if (new_argv.example && + new_argv.example !== appname) + process.stderr.write(" (from '" + new_argv.example + "' template)"); + process.stderr.write(".\n\n"); + + process.stderr.write( + "To run your new app:\n" + + " cd " + appname + "\n" + + " meteor\n"); } + }); - process.stderr.write(appname + ": created"); - if (new_argv.example && - new_argv.example !== appname) - process.stderr.write(" (from '" + new_argv.example + "' template)"); - process.stderr.write(".\n\n"); - - process.stderr.write( -"To run your new app:\n" + -" cd " + appname + "\n" + -" meteor\n"); - } -}); - -Commands.push({ - name: "update", - help: "Upgrade to the latest version of Meteor", - func: function (argv) { - if (argv.help) { - process.stdout.write( -"Usage: meteor update\n" + -"\n" + -"Checks to see if a new version of Meteor is available, and if so,\n" + -"downloads and installs it. You must be connected to the internet.\n"); - process.exit(1); - } - - require(path.join(__dirname, 'update.js')); - } -}); - -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" + -"\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); - } - - var app_dir = require_project('add'); - var packages = require(path.join(__dirname, '..', 'lib', 'packages.js')); - var project = require(path.join(__dirname, '..', 'lib', 'project.js')); - var all = packages.list(); - var using = {}; - _.each(project.get_packages(app_dir), function (name) { - using[name] = true; - }); - - _.each(argv._, function (name) { - if (!(name in all)) { - process.stderr.write(name + ": no such package\n"); - } else if (name in using) { - process.stderr.write(name + ": already using\n"); - } else { - project.add_package(app_dir, name); - var note = all[name].metadata.summary || ''; - process.stderr.write(name + ": " + note + "\n"); - } - }); - } -}); - -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); - } - - var app_dir = require_project('remove'); - var packages = require(path.join(__dirname, '..', 'lib', 'packages.js')); - var project = require(path.join(__dirname, '..', 'lib', 'project.js')); - var using = {}; - _.each(project.get_packages(app_dir), function (name) { - using[name] = true; - }); - - _.each(argv._, function (name) { - if (!(name in using)) { - process.stderr.write(name + ": not in project\n"); - } else { - project.remove_package(app_dir, name); - process.stderr.write(name + ": removed\n"); - } - }); - } -}); - -Commands.push({ - name: "list", - help: "List available packages", - 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) { - var app_dir = require_project('list --using'); - var using = require(path.join(__dirname, '..', 'lib', 'project.js')).get_packages(app_dir); - - if (using.length) { - _.each(using, function (name) { - process.stdout.write(name + "\n"); - }); - } else { - process.stderr.write( -"This project doesn't use any packages yet. To add some packages:\n" + -" meteor add ...\n" + -"\n" + -"To see available packages:\n" + -" meteor list\n"); - } - return; - } - - var list = require(path.join(__dirname, '..', 'lib', 'packages.js')).list(); - var names = _.keys(list); - names.sort(); - var pkgs = []; - _.each(names, function (name) { - pkgs.push(list[name]); - }); - process.stdout.write("\n" + - require(path.join(__dirname, '..', 'lib', 'packages.js')).format_list(pkgs) + - "\n"); - } -}); - -Commands.push({ - name: "bundle", - help: "Pack this project up into a tarball", - func: function (argv) { - if (argv.help || argv._.length != 1) { - 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); - } - - // XXX if they pass a file that doesn't end in .tar.gz or .tgz, - // add the former for them - - // XXX output, to stderr, the name of the file written to (for - // human comfort, especially since we might change the name) - - // XXX name the root directory in the bundle based on the basename - // of the file, not a constant 'bundle' (a bit obnoxious for - // machines, but worth it for humans) - - var app_dir = path.resolve(require_project("bundle")); - var build_dir = path.join(app_dir, '.meteor', 'local', 'build_tar'); - var bundle_path = path.join(build_dir, 'bundle'); - var output_path = path.resolve(argv._[0]); // get absolute path - - var bundler = require(path.join(__dirname, '..', 'lib', 'bundler.js')); - var errors = bundler.bundle(app_dir, bundle_path); - if (errors) { - process.stdout.write("Errors prevented bundling:\n"); - _.each(errors, function (e) { - process.stdout.write(e + "\n"); - }); - files.rm_recursive(build_dir); - process.exit(1); - } - - var cp = require('child_process'); - cp.execFile('/usr/bin/env', - ['tar', 'czf', output_path, 'bundle'], - {cwd: build_dir}, - function (error, stdout, stderr) { - if (error !== null) { - console.log(JSON.stringify(error)); - process.stderr.write("couldn't run tar\n"); - } else { - process.stdout.write(stdout); - process.stderr.write(stderr); - } - files.rm_recursive(build_dir); - }); - } -}); - -Commands.push({ - name: "mongo", - help: "Connect to the Mongo database for the specified site", - func: 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); - } - - var new_argv = opt.argv; - - if (new_argv._.length === 1) { - // localhost mode - find_mongo_port("mongo", function (mongod_port) { - if (!mongod_port) { - process.stdout.write( -"mongo: Meteor isn't running.\n" + -"\n" + -"This command only works while Meteor is running your application\n" + -"locally. Start your application first.\n"); - process.exit(1); - } - - var mongo_url = "mongodb://127.0.0.1:" + mongod_port + "/meteor"; - - if (new_argv.url) - console.log(mongo_url); - else - deploy.run_mongo_shell(mongo_url); - }); - - } else if (new_argv._.length === 2) { - // remote mode - deploy.mongo(new_argv._[1], new_argv.url); - - } else { - // usage - process.stdout.write(opt.help()); - process.exit(1); - } - } -}); - -Commands.push({ - name: "deploy", - help: "Deploy this project to Meteor", - func: 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)') - .boolean('tests') - .describe('settings', 'set optional data for Meteor.settings on the server') -// .describe('tests', 'deploy the tests instead of the actual application') - .usage( - // XXX document --tests in the future, once we publicly - // support tests -"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. The argument is the name of a file containing the JSON data\n" + -"to use. The settings will persist across deployments until you again specify\n" + -"a settings file. To 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); - } - - if (new_argv.delete) { - deploy.delete_app(new_argv._[1]); - } else { - var settings = undefined; - if (new_argv.settings) - settings = getSettings(new_argv.settings); - // accept packages iff we're deploying tests - var project_dir = path.resolve(require_project("bundle", new_argv.tests)); - deploy.deploy_app(new_argv._[1], project_dir, new_argv.debug, - new_argv.tests, new_argv.password, settings); - } - } -}); - -Commands.push({ - name: "logs", - help: "Show logs for specified site", - func: function (argv) { - if (argv.help || argv._.length < 1 || argv._.length > 2) { - process.stdout.write( -"Usage: meteor logs \n" + -"\n" + -"Retrieves the server logs for the requested site.\n"); - process.exit(1); - } - - deploy.logs(argv._[0]); - } -}); - -Commands.push({ - name: "reset", - help: "Reset the project state. Erases the local database.", - 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); - } - - var app_dir = path.resolve(require_project("reset")); - - find_mongo_port("reset", function (mongod_port) { - if (mongod_port) { + Commands.push({ + name: "update", + help: "Upgrade to the latest version of Meteor", + func: function (argv) { + if (argv.help) { process.stdout.write( -"reset: Meteor is running.\n" + -"\n" + -"This command does not work while Meteor is running your application.\n" + -"Exit the running meteor development server.\n"); + "Usage: meteor update\n" + + "\n" + + "Checks to see if a new version of Meteor is available, and if so,\n" + + "downloads and installs it. You must be connected to the internet.\n"); process.exit(1); } - var local_dir = path.join(app_dir, '.meteor', 'local'); - files.rm_recursive(local_dir); + require(path.join(__dirname, 'update.js')); + } + }); - process.stdout.write("Project reset.\n"); - }); - } -}); + 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" + + "\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); + } + + var app_dir = require_project('add'); + var packages = require(path.join(__dirname, '..', 'lib', 'packages.js')); + var project = require(path.join(__dirname, '..', 'lib', 'project.js')); + var all = packages.list(); + var using = {}; + _.each(project.get_packages(app_dir), function (name) { + using[name] = true; + }); + + _.each(argv._, function (name) { + if (!(name in all)) { + process.stderr.write(name + ": no such package\n"); + } else if (name in using) { + process.stderr.write(name + ": already using\n"); + } else { + project.add_package(app_dir, name); + var note = all[name].metadata.summary || ''; + process.stderr.write(name + ": " + note + "\n"); + } + }); + } + }); + + 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); + } + + var app_dir = require_project('remove'); + var packages = require(path.join(__dirname, '..', 'lib', 'packages.js')); + var project = require(path.join(__dirname, '..', 'lib', 'project.js')); + var using = {}; + _.each(project.get_packages(app_dir), function (name) { + using[name] = true; + }); + + _.each(argv._, function (name) { + if (!(name in using)) { + process.stderr.write(name + ": not in project\n"); + } else { + project.remove_package(app_dir, name); + process.stderr.write(name + ": removed\n"); + } + }); + } + }); + + Commands.push({ + name: "list", + help: "List available packages", + 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) { + var app_dir = require_project('list --using'); + var using = require(path.join(__dirname, '..', 'lib', 'project.js')).get_packages(app_dir); + + if (using.length) { + _.each(using, function (name) { + process.stdout.write(name + "\n"); + }); + } else { + process.stderr.write( + "This project doesn't use any packages yet. To add some packages:\n" + + " meteor add ...\n" + + "\n" + + "To see available packages:\n" + + " meteor list\n"); + } + return; + } + + var list = require(path.join(__dirname, '..', 'lib', 'packages.js')).list(); + var names = _.keys(list); + names.sort(); + var pkgs = []; + _.each(names, function (name) { + pkgs.push(list[name]); + }); + process.stdout.write("\n" + + require(path.join(__dirname, '..', 'lib', 'packages.js')).format_list(pkgs) + + "\n"); + } + }); + + Commands.push({ + name: "bundle", + help: "Pack this project up into a tarball", + func: function (argv) { + if (argv.help || argv._.length != 1) { + 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); + } + + // XXX if they pass a file that doesn't end in .tar.gz or .tgz, + // add the former for them + + // XXX output, to stderr, the name of the file written to (for + // human comfort, especially since we might change the name) + + // XXX name the root directory in the bundle based on the basename + // of the file, not a constant 'bundle' (a bit obnoxious for + // machines, but worth it for humans) + + var app_dir = path.resolve(require_project("bundle")); + var build_dir = path.join(app_dir, '.meteor', 'local', 'build_tar'); + var bundle_path = path.join(build_dir, 'bundle'); + var output_path = path.resolve(argv._[0]); // get absolute path + + var bundler = require(path.join(__dirname, '..', 'lib', 'bundler.js')); + var errors = bundler.bundle(app_dir, bundle_path); + if (errors) { + process.stdout.write("Errors prevented bundling:\n"); + _.each(errors, function (e) { + process.stdout.write(e + "\n"); + }); + files.rm_recursive(build_dir); + process.exit(1); + } + + var cp = require('child_process'); + cp.execFile('/usr/bin/env', + ['tar', 'czf', output_path, 'bundle'], + {cwd: build_dir}, + function (error, stdout, stderr) { + if (error !== null) { + console.log(JSON.stringify(error)); + process.stderr.write("couldn't run tar\n"); + } else { + process.stdout.write(stdout); + process.stderr.write(stderr); + } + files.rm_recursive(build_dir); + }); + } + }); + + Commands.push({ + name: "mongo", + help: "Connect to the Mongo database for the specified site", + func: 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); + } + + var new_argv = opt.argv; + + if (new_argv._.length === 1) { + // localhost mode + find_mongo_port("mongo", function (mongod_port) { + if (!mongod_port) { + process.stdout.write( + "mongo: Meteor isn't running.\n" + + "\n" + + "This command only works while Meteor is running your application\n" + + "locally. Start your application first.\n"); + process.exit(1); + } + + var mongo_url = "mongodb://127.0.0.1:" + mongod_port + "/meteor"; + + if (new_argv.url) + console.log(mongo_url); + else + deploy.run_mongo_shell(mongo_url); + }); + + } else if (new_argv._.length === 2) { + // remote mode + deploy.mongo(new_argv._[1], new_argv.url); + + } else { + // usage + process.stdout.write(opt.help()); + process.exit(1); + } + } + }); + + Commands.push({ + name: "deploy", + help: "Deploy this project to Meteor", + func: 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)') + .boolean('tests') + .describe('settings', 'set optional data for Meteor.settings on the server') + // .describe('tests', 'deploy the tests instead of the actual application') + .usage( + // XXX document --tests in the future, once we publicly + // support tests + "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. The argument is the name of a file containing the JSON data\n" + + "to use. The settings will persist across deployments until you again specify\n" + + "a settings file. To 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); + } + + if (new_argv.delete) { + deploy.delete_app(new_argv._[1]); + } else { + var settings = undefined; + if (new_argv.settings) + settings = getSettings(new_argv.settings); + // accept packages iff we're deploying tests + var project_dir = path.resolve(require_project("bundle", new_argv.tests)); + deploy.deploy_app(new_argv._[1], project_dir, new_argv.debug, + new_argv.tests, new_argv.password, settings); + } + } + }); + + Commands.push({ + name: "logs", + help: "Show logs for specified site", + func: function (argv) { + if (argv.help || argv._.length < 1 || argv._.length > 2) { + process.stdout.write( + "Usage: meteor logs \n" + + "\n" + + "Retrieves the server logs for the requested site.\n"); + process.exit(1); + } + + deploy.logs(argv._[0]); + } + }); + + Commands.push({ + name: "reset", + help: "Reset the project state. Erases the local database.", + 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); + } + + var app_dir = path.resolve(require_project("reset")); + + find_mongo_port("reset", function (mongod_port) { + if (mongod_port) { + process.stdout.write( + "reset: Meteor is running.\n" + + "\n" + + "This command does not work while Meteor is running your application.\n" + + "Exit the running meteor development server.\n"); + process.exit(1); + } + + var local_dir = path.join(app_dir, '.meteor', 'local'); + files.rm_recursive(local_dir); + + process.stdout.write("Project reset.\n"); + }); + } + }); -var main = function() { - var optimist = require('optimist') - .alias("h", "help") - .boolean("h") - .boolean("help") - .boolean("version") - .boolean("debug"); + var main = function() { + var optimist = require('optimist') + .alias("h", "help") + .boolean("h") + .boolean("help") + .boolean("version") + .boolean("debug"); - var argv = optimist.argv; + var argv = optimist.argv; - if (argv.help) { - argv._.splice(0, 0, "help"); - delete argv.help; - } + if (argv.help) { + argv._.splice(0, 0, "help"); + delete argv.help; + } - if (argv.version) { - var updater = require(path.join(__dirname, '..', 'lib', 'updater.js')); - var sha = updater.git_sha(); + if (argv.version) { + var updater = require(path.join(__dirname, '..', 'lib', 'updater.js')); + var sha = updater.git_sha(); - process.stdout.write("Meteor version " + updater.CURRENT_VERSION); + process.stdout.write("Meteor version " + updater.CURRENT_VERSION); - if (files.in_checkout()) - process.stdout.write(" (git checkout)"); - else if (sha) - process.stdout.write(" (" + sha.substr(0, 10) + ")"); + if (files.in_checkout()) + process.stdout.write(" (git checkout)"); + else if (sha) + process.stdout.write(" (" + sha.substr(0, 10) + ")"); - process.stdout.write("\n"); - process.exit(0); - } + process.stdout.write("\n"); + process.exit(0); + } - var cmd = 'run'; - if (argv._.length) - cmd = argv._.splice(0,1)[0]; + var cmd = 'run'; + if (argv._.length) + cmd = argv._.splice(0,1)[0]; - findCommand(cmd).func(argv); -}; + findCommand(cmd).func(argv); + }; -main(); + main(); +}).run(); From a744cbd9e82486a184af895fde192a8de5b057b1 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Wed, 19 Dec 2012 14:19:14 -0800 Subject: [PATCH 127/183] Print a warning when you try to reset a deployed app, regarding how to actually dwym --- app/meteor/meteor.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index d1a0b0004d..df284fa153 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -617,6 +617,11 @@ Fiber(function () { "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._)) { + 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"); + process.exit(1); } var app_dir = path.resolve(require_project("reset")); From 821a2370821335ef6b772231e07c4131d3cd7b93 Mon Sep 17 00:00:00 2001 From: Matt DeBergalis Date: Wed, 19 Dec 2012 17:23:56 -0800 Subject: [PATCH 128/183] madewith banner tweaks. Don't show ??? and don't propagate click event if app isn't found. --- packages/madewith/madewith.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/madewith/madewith.js b/packages/madewith/madewith.js index 4b2c48844e..01dc7b44cf 100644 --- a/packages/madewith/madewith.js +++ b/packages/madewith/madewith.js @@ -19,7 +19,7 @@ Template.madewith.vote_count = function() { var app = apps.findOne(); - return app ? app.vote_count : '???'; + return app ? app.vote_count : ''; }; Template.madewith.shortname = function () { @@ -29,13 +29,13 @@ Template.madewith.events({ 'click .madewith_upvote': function(event) { var app = apps.findOne(); - if (app) { + if (app) server.call('vote', hostname); - // stop these so you don't click through the link to go to the - // app. - event.stopPropagation(); - event.preventDefault(); - } + + // stop these so you don't click through the link to go to the + // app. + event.stopPropagation(); + event.preventDefault(); } }); })(); \ No newline at end of file From 13074d58b9ca5a1851f49c25ec754afa7ca7b9d4 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 21 Dec 2012 11:20:01 -0800 Subject: [PATCH 129/183] Remove two copies of underscore.js from Meteor, using NPM package instead. Follow-up to 27382c6. A step towards addressing issue #302. We still have one copy in the underscore smart package, for serving to clients. --- app/lib/bundler.js | 2 +- app/lib/files.js | 2 +- app/lib/mongo_exit_codes.js | 2 +- app/lib/mongo_runner.js | 2 +- app/lib/packages.js | 2 +- app/lib/project.js | 2 +- app/lib/third/underscore.js | 1200 ----------------------------------- app/meteor/deploy.js | 2 +- app/meteor/post-upgrade.js | 2 +- app/meteor/run.js | 2 +- app/meteor/update.js | 2 +- app/server/server.js | 4 +- app/server/underscore.js | 1200 ----------------------------------- 13 files changed, 11 insertions(+), 2413 deletions(-) delete mode 100644 app/lib/third/underscore.js delete mode 100644 app/server/underscore.js diff --git a/app/lib/bundler.js b/app/lib/bundler.js index f92f126f6b..045077f004 100644 --- a/app/lib/bundler.js +++ b/app/lib/bundler.js @@ -31,7 +31,7 @@ var crypto = require('crypto'); var fs = require('fs'); var uglify = require('uglify-js'); var cleanCSS = require('clean-css'); -var _ = require(path.join(__dirname, 'third', 'underscore.js')); +var _ = require('underscore'); // files to ignore when bundling. node has no globs, so use regexps var ignore_files = [ diff --git a/app/lib/files.js b/app/lib/files.js index 902f6c078f..cfb555307e 100644 --- a/app/lib/files.js +++ b/app/lib/files.js @@ -1,6 +1,6 @@ var fs = require("fs"); var path = require('path'); -var _ = require(path.join(__dirname, 'third', 'underscore.js')); +var _ = require('underscore'); var files = module.exports = { // A sort comparator to order files into load order. diff --git a/app/lib/mongo_exit_codes.js b/app/lib/mongo_exit_codes.js index e8f71be11b..4e5dfe2e79 100644 --- a/app/lib/mongo_exit_codes.js +++ b/app/lib/mongo_exit_codes.js @@ -6,7 +6,7 @@ var path = require("path"); -var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js')); +var _ = require('underscore'); exports.Codes = { 0 : { code: 0, diff --git a/app/lib/mongo_runner.js b/app/lib/mongo_runner.js index cb9359bbbb..fec13e4d85 100644 --- a/app/lib/mongo_runner.js +++ b/app/lib/mongo_runner.js @@ -4,7 +4,7 @@ var spawn = require('child_process').spawn; var files = require(path.join(__dirname, '..', 'lib', 'files.js')); -var _ = require(path.join('..', 'lib', 'third', 'underscore.js')); +var _ = require('underscore'); /** Internal. diff --git a/app/lib/packages.js b/app/lib/packages.js index 58d559e896..3a0c3a6a62 100644 --- a/app/lib/packages.js +++ b/app/lib/packages.js @@ -1,5 +1,5 @@ var path = require('path'); -var _ = require(path.join(__dirname, 'third', 'underscore.js')); +var _ = require('underscore'); var files = require(path.join(__dirname, 'files.js')); var fs = require('fs'); diff --git a/app/lib/project.js b/app/lib/project.js index a8905c86e6..fdf7353b8b 100644 --- a/app/lib/project.js +++ b/app/lib/project.js @@ -1,6 +1,6 @@ var fs = require('fs'); var path = require('path'); -var _ = require(path.join(__dirname, 'third', 'underscore.js')); +var _ = require('underscore'); var project = module.exports = { diff --git a/app/lib/third/underscore.js b/app/lib/third/underscore.js deleted file mode 100644 index 1ebe2671b9..0000000000 --- a/app/lib/third/underscore.js +++ /dev/null @@ -1,1200 +0,0 @@ -// Underscore.js 1.4.2 -// http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore may be freely distributed under the MIT license. - -(function() { - - // Baseline setup - // -------------- - - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - // Save the previous value of the `_` variable. - var previousUnderscore = root._; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - // Save bytes in the minified (but not gzipped) version: - 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, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = function(obj) { - if (obj instanceof _) return obj; - if (!(this instanceof _)) return new _(obj); - this._wrapped = obj; - }; - - // Export the Underscore object for **Node.js**, with - // backwards-compatibility for the old `require()` API. If we're in - // the browser, add `_` as a global object via a string identifier, - // for Closure Compiler "advanced" mode. - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = _; - } - exports._ = _; - } else { - root['_'] = _; - } - - // Current version. - _.VERSION = '1.4.2'; - - // Collection Functions - // -------------------- - - // The cornerstone, an `each` implementation, aka `forEach`. - // Handles objects with the built-in `forEach`, arrays, and raw objects. - // Delegates to **ECMAScript 5**'s native `forEach` if available. - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - // Return the results of applying the iterator to each element. - // Delegates to **ECMAScript 5**'s native `map` if available. - _.map = _.collect = function(obj, iterator, context) { - var results = []; - 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); - }); - return results; - }; - - // **Reduce** builds up a single result from a list of values, aka `inject`, - // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. - _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduce && obj.reduce === nativeReduce) { - if (context) iterator = _.bind(iterator, context); - return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); - } - each(obj, function(value, index, list) { - if (!initial) { - memo = value; - initial = true; - } else { - memo = iterator.call(context, memo, value, index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // The right-associative version of reduce, also known as `foldr`. - // Delegates to **ECMAScript 5**'s native `reduceRight` if available. - _.reduceRight = _.foldr = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { - if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); - } - var length = obj.length; - if (length !== +length) { - var keys = _.keys(obj); - length = keys.length; - } - each(obj, function(value, index, list) { - index = keys ? keys[--length] : --length; - if (!initial) { - memo = obj[index]; - initial = true; - } else { - memo = iterator.call(context, memo, obj[index], index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // Return the first value which passes a truth test. Aliased as `detect`. - _.find = _.detect = function(obj, iterator, context) { - var result; - any(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) { - result = value; - return true; - } - }); - return result; - }; - - // Return all the elements that pass a truth test. - // Delegates to **ECMAScript 5**'s native `filter` if available. - // Aliased as `select`. - _.filter = _.select = function(obj, iterator, context) { - var results = []; - 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; - }); - return results; - }; - - // Return all the elements for which a truth test fails. - _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Determine whether all of the elements match a truth test. - // Delegates to **ECMAScript 5**'s native `every` if available. - // Aliased as `all`. - _.every = _.all = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = true; - if (obj == null) return result; - if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); - each(obj, function(value, index, list) { - if (!(result = result && iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if at least one element in the object matches a truth test. - // Delegates to **ECMAScript 5**'s native `some` if available. - // Aliased as `any`. - var any = _.some = _.any = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = false; - if (obj == null) return result; - if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); - each(obj, function(value, index, list) { - if (result || (result = iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if the array or object contains a given value (using `===`). - // Aliased as `include`. - _.contains = _.include = function(obj, target) { - var found = false; - if (obj == null) return found; - if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { - return value === target; - }); - return found; - }; - - // Invoke a method (with arguments) on every item in a collection. - _.invoke = function(obj, method) { - var args = slice.call(arguments, 2); - return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); - }); - }; - - // Convenience version of a common use case of `map`: fetching a property. - _.pluck = function(obj, key) { - return _.map(obj, function(value){ return value[key]; }); - }; - - // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { - for (var key in attrs) { - if (attrs[key] !== value[key]) return false; - } - return true; - }); - }; - - // 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 - _.max = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.max.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -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}); - }); - return result.value; - }; - - // Return the minimum element (or element-based computation). - _.min = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.min.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : 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}); - }); - return result.value; - }; - - // Shuffle an array. - _.shuffle = function(obj) { - var rand; - var index = 0; - var shuffled = []; - each(obj, function(value) { - rand = _.random(index++); - shuffled[index - 1] = shuffled[rand]; - shuffled[rand] = value; - }); - return shuffled; - }; - - // An internal function to generate lookup iterators. - var lookupIterator = function(value) { - return _.isFunction(value) ? value : function(obj){ return obj[value]; }; - }; - - // Sort the object's values by a criterion produced by an iterator. - _.sortBy = function(obj, value, context) { - var iterator = lookupIterator(value); - return _.pluck(_.map(obj, function(value, index, list) { - return { - value : value, - index : index, - criteria : iterator.call(context, value, index, list) - }; - }).sort(function(left, right) { - var a = left.criteria; - var b = right.criteria; - if (a !== b) { - if (a > b || a === void 0) return 1; - if (a < b || b === void 0) return -1; - } - return left.index < right.index ? -1 : 1; - }), 'value'); - }; - - // An internal function used for aggregate "group by" operations. - var group = function(obj, value, context, behavior) { - var result = {}; - var iterator = lookupIterator(value); - each(obj, function(value, index) { - var key = iterator.call(context, value, index, obj); - behavior(result, key, value); - }); - return result; - }; - - // Groups the object's values by a criterion. Pass either a string attribute - // to group by, or a function that returns the criterion. - _.groupBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - (_.has(result, key) ? result[key] : (result[key] = [])).push(value); - }); - }; - - // Counts instances of an object that group by a certain criterion. Pass - // either a string attribute to count by, or a function that returns the - // criterion. - _.countBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - if (!_.has(result, key)) result[key] = 0; - result[key]++; - }); - }; - - // Use a comparator function to figure out the smallest index at which - // an object should be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iterator, context) { - iterator = iterator == null ? _.identity : lookupIterator(iterator); - var value = iterator.call(context, obj); - var low = 0, high = array.length; - while (low < high) { - var mid = (low + high) >>> 1; - iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; - } - return low; - }; - - // Safely convert anything iterable into a real, live array. - _.toArray = function(obj) { - if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); - return _.values(obj); - }; - - // Return the number of elements in an object. - _.size = function(obj) { - return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; - }; - - // Array Functions - // --------------- - - // Get the first element of an array. Passing **n** will return the first N - // values in the array. Aliased as `head` and `take`. The **guard** check - // allows it to work with `_.map`. - _.first = _.head = _.take = function(array, n, guard) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; - }; - - // Returns everything but the last entry of the array. Especially useful on - // the arguments object. Passing **n** will return all the values in - // the array, excluding the last N. The **guard** check allows it to work with - // `_.map`. - _.initial = function(array, n, guard) { - return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); - }; - - // Get the last element of an array. Passing **n** will return the last N - // values in the array. The **guard** check allows it to work with `_.map`. - _.last = function(array, n, guard) { - if ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { - return array[array.length - 1]; - } - }; - - // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. - // Especially useful on the arguments object. Passing an **n** will return - // the rest N values in the array. The **guard** - // check allows it to work with `_.map`. - _.rest = _.tail = _.drop = function(array, n, guard) { - return slice.call(array, (n == null) || guard ? 1 : n); - }; - - // Trim out all falsy values from an array. - _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); - }; - - // Internal implementation of a recursive `flatten` function. - var flatten = function(input, shallow, output) { - each(input, function(value) { - if (_.isArray(value)) { - shallow ? push.apply(output, value) : flatten(value, shallow, output); - } else { - output.push(value); - } - }); - return output; - }; - - // Return a completely flattened version of an array. - _.flatten = function(array, shallow) { - return flatten(array, shallow, []); - }; - - // Return a version of the array that does not contain the specified value(s). - _.without = function(array) { - return _.difference(array, slice.call(arguments, 1)); - }; - - // Produce a duplicate-free version of the array. If the array has already - // been sorted, you have the option of using a faster algorithm. - // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted, iterator, context) { - var initial = iterator ? _.map(array, iterator, context) : array; - var results = []; - var seen = []; - each(initial, function(value, index) { - if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { - seen.push(value); - results.push(array[index]); - } - }); - return results; - }; - - // 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)); - }; - - // Produce an array that contains every item shared between all the - // passed-in arrays. - _.intersection = function(array) { - var rest = slice.call(arguments, 1); - return _.filter(_.uniq(array), function(item) { - return _.every(rest, function(other) { - return _.indexOf(other, item) >= 0; - }); - }); - }; - - // Take the difference between one array and a number of other arrays. - // Only the elements present in just the first array will remain. - _.difference = function(array) { - var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); - return _.filter(array, function(value){ return !_.contains(rest, value); }); - }; - - // 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 results = new Array(length); - for (var i = 0; i < length; i++) { - results[i] = _.pluck(args, "" + i); - } - return results; - }; - - // Converts lists into objects. Pass either a single array of `[key, value]` - // pairs, or two parallel arrays of the same length -- one of keys, and one of - // the corresponding values. - _.object = function(list, values) { - var result = {}; - for (var i = 0, l = list.length; i < l; i++) { - if (values) { - result[list[i]] = values[i]; - } else { - result[list[i][0]] = list[i][1]; - } - } - return result; - }; - - // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), - // we need this function. Return the position of the first occurrence of an - // item in an array, or -1 if the item is not included in the array. - // Delegates to **ECMAScript 5**'s native `indexOf` if available. - // If the array is large and already in sort order, pass `true` - // for **isSorted** to use binary search. - _.indexOf = function(array, item, isSorted) { - if (array == null) return -1; - var i = 0, l = array.length; - if (isSorted) { - if (typeof isSorted == 'number') { - i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); - } else { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; - } - } - if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); - for (; i < l; i++) if (array[i] === item) return i; - return -1; - }; - - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. - _.lastIndexOf = function(array, item, from) { - if (array == null) return -1; - var hasIndex = from != null; - if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { - return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); - } - var i = (hasIndex ? from : array.length); - while (i--) if (array[i] === item) return i; - return -1; - }; - - // Generate an integer Array containing an arithmetic progression. A port of - // the native Python `range()` function. See - // [the Python documentation](http://docs.python.org/library/functions.html#range). - _.range = function(start, stop, step) { - if (arguments.length <= 1) { - stop = start || 0; - start = 0; - } - step = arguments[2] || 1; - - var len = Math.max(Math.ceil((stop - start) / step), 0); - var idx = 0; - var range = new Array(len); - - while(idx < len) { - range[idx++] = start; - start += step; - } - - return range; - }; - - // 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). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && 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; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; - }; - }; - - // Bind all of an object's methods to that object. Useful for ensuring that - // 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); - each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); - return obj; - }; - - // Memoize an expensive function by storing its results. - _.memoize = function(func, hasher) { - var memo = {}; - hasher || (hasher = _.identity); - return function() { - var key = hasher.apply(this, arguments); - return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); - }; - }; - - // Delays a function for the given number of milliseconds, and then calls - // it with the arguments supplied. - _.delay = function(func, wait) { - var args = slice.call(arguments, 2); - return setTimeout(function(){ return func.apply(null, args); }, wait); - }; - - // Defers a function, scheduling it to run after the current call stack has - // cleared. - _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); - }; - - // 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, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); - return function() { - context = this; args = arguments; - var later = function() { - timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; - result = func.apply(context, args); - } - whenDone(); - return result; - }; - }; - - // Returns a function, that, as long as it continues to be invoked, will not - // be triggered. The function will be called after it stops being called for - // 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; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if (!immediate) result = func.apply(context, args); - }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) result = func.apply(context, args); - return result; - }; - }; - - // Returns a function that will be executed at most one time, no matter how - // often you call it. Useful for lazy initialization. - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - memo = func.apply(this, arguments); - func = null; - return memo; - }; - }; - - // Returns the first function passed as an argument to the second, - // allowing you to adjust arguments, run code before and after, and - // conditionally execute the original function. - _.wrap = function(func, wrapper) { - return function() { - var args = [func]; - push.apply(args, arguments); - return wrapper.apply(this, args); - }; - }; - - // Returns a function that is the composition of a list of functions, each - // consuming the return value of the function that follows. - _.compose = function() { - var funcs = arguments; - return function() { - var args = arguments; - for (var i = funcs.length - 1; i >= 0; i--) { - args = [funcs[i].apply(this, args)]; - } - return args[0]; - }; - }; - - // 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); - } - }; - }; - - // Object Functions - // ---------------- - - // Retrieve the names of an object's properties. - // Delegates to **ECMAScript 5**'s native `Object.keys` - _.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; - return keys; - }; - - // Retrieve the values of an object's properties. - _.values = function(obj) { - var values = []; - for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); - return values; - }; - - // Convert an object into a list of `[key, value]` pairs. - _.pairs = function(obj) { - var pairs = []; - for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); - return pairs; - }; - - // Invert the keys and values of an object. The values must be serializable. - _.invert = function(obj) { - var result = {}; - for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; - return result; - }; - - // Return a sorted list of the function names available on the object. - // Aliased as `methods` - _.functions = _.methods = function(obj) { - var names = []; - for (var key in obj) { - if (_.isFunction(obj[key])) names.push(key); - } - return names.sort(); - }; - - // Extend a given object with all the properties in passed-in object(s). - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Return a copy of the object only containing the whitelisted properties. - _.pick = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - each(keys, function(key) { - if (key in obj) copy[key] = obj[key]; - }); - return copy; - }; - - // Return a copy of the object without the blacklisted properties. - _.omit = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - for (var key in obj) { - if (!_.contains(keys, key)) copy[key] = obj[key]; - } - return copy; - }; - - // Fill in a given object with default properties. - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Create a (shallow-cloned) duplicate of an object. - _.clone = function(obj) { - if (!_.isObject(obj)) return obj; - return _.isArray(obj) ? obj.slice() : _.extend({}, obj); - }; - - // Invokes interceptor with the obj, and then returns obj. - // The primary purpose of this method is to "tap into" a method chain, in - // order to perform operations on intermediate results within the chain. - _.tap = function(obj, interceptor) { - interceptor(obj); - return obj; - }; - - // 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. - 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; - // Unwrap any wrapped objects. - if (a instanceof _) a = a._wrapped; - if (b instanceof _) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className != toString.call(b)) return false; - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - return +a == +b; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') return false; - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] == a) return bStack[length] == b; - } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); - var size = 0, result = true; - // Recursively compare objects and arrays. - if (className == '[object Array]') { - // Compare array lengths to determine if a deep comparison is necessary. - size = a.length; - result = size == b.length; - if (result) { - // Deep compare the contents, ignoring non-numeric properties. - while (size--) { - if (!(result = eq(a[size], b[size], aStack, bStack))) break; - } - } - } 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)) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; - } - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (_.has(b, key) && !(size--)) break; - } - result = !size; - } - } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); - return result; - }; - - // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - return eq(a, b, [], []); - }; - - // Is a given array, string, or object empty? - // An "empty" object has no enumerable own-properties. - _.isEmpty = function(obj) { - if (obj == null) return true; - if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; - for (var key in obj) if (_.has(obj, key)) return false; - return true; - }; - - // Is a given value a DOM element? - _.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); - }; - - // Is a given value an array? - // Delegates to ECMA5's native Array.isArray - _.isArray = nativeIsArray || function(obj) { - return toString.call(obj) == '[object Array]'; - }; - - // Is a given variable an object? - _.isObject = function(obj) { - return obj === Object(obj); - }; - - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { - _['is' + name] = function(obj) { - return toString.call(obj) == '[object ' + name + ']'; - }; - }); - - // Define a fallback version of the method in browsers (ahem, IE), where - // there isn't any inspectable "Arguments" type. - if (!_.isArguments(arguments)) { - _.isArguments = function(obj) { - return !!(obj && _.has(obj, 'callee')); - }; - } - - // Optimize `isFunction` if appropriate. - if (typeof (/./) !== 'function') { - _.isFunction = function(obj) { - return typeof obj === 'function'; - }; - } - - // Is a given object a finite number? - _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); - }; - - // Is the given value `NaN`? (NaN is the only number which does not equal itself). - _.isNaN = function(obj) { - return _.isNumber(obj) && obj != +obj; - }; - - // Is a given value a boolean? - _.isBoolean = function(obj) { - return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; - }; - - // Is a given value equal to null? - _.isNull = function(obj) { - return obj === null; - }; - - // Is a given variable undefined? - _.isUndefined = function(obj) { - return obj === void 0; - }; - - // Shortcut function for checking if an object has a given property directly - // on itself (in other words, not on a prototype). - _.has = function(obj, key) { - return hasOwnProperty.call(obj, key); - }; - - // Utility Functions - // ----------------- - - // Run Underscore.js in *noConflict* mode, returning the `_` variable to its - // previous owner. Returns a reference to the Underscore object. - _.noConflict = function() { - root._ = previousUnderscore; - return this; - }; - - // Keep the identity function around for default iterators. - _.identity = function(value) { - return value; - }; - - // Run a function **n** times. - _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); - }; - - // Return a random integer between min and max (inclusive). - _.random = function(min, max) { - if (max == null) { - max = min; - min = 0; - } - return min + (0 | Math.random() * (max - min + 1)); - }; - - // List of HTML entities for escaping. - var entityMap = { - escape: { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - } - }; - entityMap.unescape = _.invert(entityMap.escape); - - // Regexes containing the keys and values listed immediately above. - var entityRegexes = { - escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), - unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') - }; - - // Functions for escaping and unescaping strings to/from HTML interpolation. - _.each(['escape', 'unescape'], function(method) { - _[method] = function(string) { - if (string == null) return ''; - return ('' + string).replace(entityRegexes[method], function(match) { - return entityMap[method][match]; - }); - }; - }); - - // If the value of the named property is a function then invoke it; - // otherwise, return it. - _.result = function(object, property) { - if (object == null) return null; - var value = object[property]; - return _.isFunction(value) ? value.call(object) : value; - }; - - // Add your own custom functions to the Underscore object. - _.mixin = function(obj) { - each(_.functions(obj), function(name){ - var func = _[name] = obj[name]; - _.prototype[name] = function() { - var args = [this._wrapped]; - push.apply(args, arguments); - return result.call(this, func.apply(_, args)); - }; - }); - }; - - // Generate a unique integer id (unique within the entire client session). - // Useful for temporary DOM ids. - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = idCounter++; - return prefix ? prefix + id : id; - }; - - // By default, Underscore uses ERB-style template delimiters, change the - // following template settings to use alternative delimiters. - _.templateSettings = { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g - }; - - // When customizing `templateSettings`, if you don't want to define an - // interpolation, evaluation or escaping regex, we need one that is - // guaranteed not to match. - var noMatch = /(.)^/; - - // Certain characters need to be escaped so that they can be put into a - // string literal. - var escapes = { - "'": "'", - '\\': '\\', - '\r': 'r', - '\n': 'n', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; - - // JavaScript micro-templating, similar to John Resig's implementation. - // Underscore templating handles arbitrary delimiters, preserves whitespace, - // and correctly escapes quotes within interpolated code. - _.template = function(text, data, settings) { - settings = _.defaults({}, settings, _.templateSettings); - - // Combine delimiters into one regular expression via alternation. - var matcher = new RegExp([ - (settings.escape || noMatch).source, - (settings.interpolate || noMatch).source, - (settings.evaluate || noMatch).source - ].join('|') + '|$', 'g'); - - // Compile the template source, escaping string literals appropriately. - var index = 0; - var source = "__p+='"; - text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { - source += text.slice(index, offset) - .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; - index = offset + match.length; - }); - source += "';\n"; - - // If a variable is not specified, place data values in local scope. - if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; - - source = "var __t,__p='',__j=Array.prototype.join," + - "print=function(){__p+=__j.call(arguments,'');};\n" + - source + "return __p;\n"; - - try { - var render = new Function(settings.variable || 'obj', '_', source); - } catch (e) { - e.source = source; - throw e; - } - - if (data) return render(data, _); - var template = function(data) { - return render.call(this, data, _); - }; - - // Provide the compiled function source as a convenience for precompilation. - template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; - - return template; - }; - - // Add a "chain" function, which will delegate to the wrapper. - _.chain = function(obj) { - return _(obj).chain(); - }; - - // OOP - // --------------- - // If Underscore is called as a function, it returns a wrapped object that - // can be used OO-style. This wrapper holds altered versions of all the - // underscore functions. Wrapped objects may be chained. - - // Helper function to continue chaining intermediate results. - var result = function(obj) { - return this._chain ? _(obj).chain() : obj; - }; - - // Add all of the Underscore functions to the wrapper object. - _.mixin(_); - - // Add all mutator Array functions to the wrapper. - each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - var obj = this._wrapped; - method.apply(obj, arguments); - if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; - return result.call(this, obj); - }; - }); - - // Add all accessor Array functions to the wrapper. - each(['concat', 'join', 'slice'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - return result.call(this, method.apply(this._wrapped, arguments)); - }; - }); - - _.extend(_.prototype, { - - // Start chaining a wrapped Underscore object. - chain: function() { - this._chain = true; - return this; - }, - - // Extracts the result from a wrapped and chained object. - value: function() { - return this._wrapped; - } - - }); - -}).call(this); diff --git a/app/meteor/deploy.js b/app/meteor/deploy.js index 3dea097fc5..5a9166b7b3 100644 --- a/app/meteor/deploy.js +++ b/app/meteor/deploy.js @@ -10,7 +10,7 @@ var request = require('request'); var qs = require('querystring'); var path = require('path'); var files = require(path.join(__dirname, '..', 'lib', 'files.js')); -var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js')); +var _ = require('underscore'); var keypress = require('keypress'); var child_process = require('child_process'); diff --git a/app/meteor/post-upgrade.js b/app/meteor/post-upgrade.js index 625d07cc6a..3153a4f449 100644 --- a/app/meteor/post-upgrade.js +++ b/app/meteor/post-upgrade.js @@ -8,7 +8,7 @@ try { var path = require('path'); var files = require(path.join(__dirname, "..", "lib", "files.js")); - var _ = require(path.join(__dirname, "..", "lib", "third", "underscore.js")); + var _ = require('underscore'); var topDir = files.get_dev_bundle(); var changelogPath = path.join(topDir, 'History.md'); diff --git a/app/meteor/run.js b/app/meteor/run.js index 530aed4a52..a8f4b19949 100644 --- a/app/meteor/run.js +++ b/app/meteor/run.js @@ -12,7 +12,7 @@ var bundler = require(path.join(__dirname, '..', 'lib', 'bundler.js')); var mongo_runner = require(path.join(__dirname, '..', 'lib', 'mongo_runner.js')); var mongoExitCodes = require(path.join(__dirname, '..', 'lib', 'mongo_exit_codes.js')); -var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js')); +var _ = require('underscore'); ////////// Globals ////////// //XXX: Refactor to not have globals anymore? diff --git a/app/meteor/update.js b/app/meteor/update.js index 8371dfc97e..f2dffb7e8f 100644 --- a/app/meteor/update.js +++ b/app/meteor/update.js @@ -10,7 +10,7 @@ var ProgressBar = require('progress'); var updater = require(path.join(__dirname, "..", "lib", "updater.js")); var files = require(path.join(__dirname, "..", "lib", "files.js")); -var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js')); +var _ = require('underscore'); // refuse to update if we're in a git checkout. if (files.in_checkout()) { diff --git a/app/server/server.js b/app/server/server.js index da9ccc80a5..78121555c5 100644 --- a/app/server/server.js +++ b/app/server/server.js @@ -12,9 +12,7 @@ var mime = require('mime'); var handlebars = require('handlebars'); var useragent = require('useragent'); -// this is a copy of underscore that will be shipped just for use by -// this file, server.js. -var _ = require(path.join(__dirname, 'underscore.js')); +var _ = require('underscore'); // This code is duplicated in app/server/server.js. var MIN_NODE_VERSION = 'v0.8.11'; diff --git a/app/server/underscore.js b/app/server/underscore.js deleted file mode 100644 index 1ebe2671b9..0000000000 --- a/app/server/underscore.js +++ /dev/null @@ -1,1200 +0,0 @@ -// Underscore.js 1.4.2 -// http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore may be freely distributed under the MIT license. - -(function() { - - // Baseline setup - // -------------- - - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - // Save the previous value of the `_` variable. - var previousUnderscore = root._; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - // Save bytes in the minified (but not gzipped) version: - 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, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = function(obj) { - if (obj instanceof _) return obj; - if (!(this instanceof _)) return new _(obj); - this._wrapped = obj; - }; - - // Export the Underscore object for **Node.js**, with - // backwards-compatibility for the old `require()` API. If we're in - // the browser, add `_` as a global object via a string identifier, - // for Closure Compiler "advanced" mode. - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = _; - } - exports._ = _; - } else { - root['_'] = _; - } - - // Current version. - _.VERSION = '1.4.2'; - - // Collection Functions - // -------------------- - - // The cornerstone, an `each` implementation, aka `forEach`. - // Handles objects with the built-in `forEach`, arrays, and raw objects. - // Delegates to **ECMAScript 5**'s native `forEach` if available. - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - // Return the results of applying the iterator to each element. - // Delegates to **ECMAScript 5**'s native `map` if available. - _.map = _.collect = function(obj, iterator, context) { - var results = []; - 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); - }); - return results; - }; - - // **Reduce** builds up a single result from a list of values, aka `inject`, - // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. - _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduce && obj.reduce === nativeReduce) { - if (context) iterator = _.bind(iterator, context); - return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); - } - each(obj, function(value, index, list) { - if (!initial) { - memo = value; - initial = true; - } else { - memo = iterator.call(context, memo, value, index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // The right-associative version of reduce, also known as `foldr`. - // Delegates to **ECMAScript 5**'s native `reduceRight` if available. - _.reduceRight = _.foldr = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { - if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); - } - var length = obj.length; - if (length !== +length) { - var keys = _.keys(obj); - length = keys.length; - } - each(obj, function(value, index, list) { - index = keys ? keys[--length] : --length; - if (!initial) { - memo = obj[index]; - initial = true; - } else { - memo = iterator.call(context, memo, obj[index], index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // Return the first value which passes a truth test. Aliased as `detect`. - _.find = _.detect = function(obj, iterator, context) { - var result; - any(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) { - result = value; - return true; - } - }); - return result; - }; - - // Return all the elements that pass a truth test. - // Delegates to **ECMAScript 5**'s native `filter` if available. - // Aliased as `select`. - _.filter = _.select = function(obj, iterator, context) { - var results = []; - 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; - }); - return results; - }; - - // Return all the elements for which a truth test fails. - _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Determine whether all of the elements match a truth test. - // Delegates to **ECMAScript 5**'s native `every` if available. - // Aliased as `all`. - _.every = _.all = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = true; - if (obj == null) return result; - if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); - each(obj, function(value, index, list) { - if (!(result = result && iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if at least one element in the object matches a truth test. - // Delegates to **ECMAScript 5**'s native `some` if available. - // Aliased as `any`. - var any = _.some = _.any = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = false; - if (obj == null) return result; - if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); - each(obj, function(value, index, list) { - if (result || (result = iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if the array or object contains a given value (using `===`). - // Aliased as `include`. - _.contains = _.include = function(obj, target) { - var found = false; - if (obj == null) return found; - if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { - return value === target; - }); - return found; - }; - - // Invoke a method (with arguments) on every item in a collection. - _.invoke = function(obj, method) { - var args = slice.call(arguments, 2); - return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); - }); - }; - - // Convenience version of a common use case of `map`: fetching a property. - _.pluck = function(obj, key) { - return _.map(obj, function(value){ return value[key]; }); - }; - - // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { - for (var key in attrs) { - if (attrs[key] !== value[key]) return false; - } - return true; - }); - }; - - // 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 - _.max = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.max.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -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}); - }); - return result.value; - }; - - // Return the minimum element (or element-based computation). - _.min = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.min.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : 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}); - }); - return result.value; - }; - - // Shuffle an array. - _.shuffle = function(obj) { - var rand; - var index = 0; - var shuffled = []; - each(obj, function(value) { - rand = _.random(index++); - shuffled[index - 1] = shuffled[rand]; - shuffled[rand] = value; - }); - return shuffled; - }; - - // An internal function to generate lookup iterators. - var lookupIterator = function(value) { - return _.isFunction(value) ? value : function(obj){ return obj[value]; }; - }; - - // Sort the object's values by a criterion produced by an iterator. - _.sortBy = function(obj, value, context) { - var iterator = lookupIterator(value); - return _.pluck(_.map(obj, function(value, index, list) { - return { - value : value, - index : index, - criteria : iterator.call(context, value, index, list) - }; - }).sort(function(left, right) { - var a = left.criteria; - var b = right.criteria; - if (a !== b) { - if (a > b || a === void 0) return 1; - if (a < b || b === void 0) return -1; - } - return left.index < right.index ? -1 : 1; - }), 'value'); - }; - - // An internal function used for aggregate "group by" operations. - var group = function(obj, value, context, behavior) { - var result = {}; - var iterator = lookupIterator(value); - each(obj, function(value, index) { - var key = iterator.call(context, value, index, obj); - behavior(result, key, value); - }); - return result; - }; - - // Groups the object's values by a criterion. Pass either a string attribute - // to group by, or a function that returns the criterion. - _.groupBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - (_.has(result, key) ? result[key] : (result[key] = [])).push(value); - }); - }; - - // Counts instances of an object that group by a certain criterion. Pass - // either a string attribute to count by, or a function that returns the - // criterion. - _.countBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - if (!_.has(result, key)) result[key] = 0; - result[key]++; - }); - }; - - // Use a comparator function to figure out the smallest index at which - // an object should be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iterator, context) { - iterator = iterator == null ? _.identity : lookupIterator(iterator); - var value = iterator.call(context, obj); - var low = 0, high = array.length; - while (low < high) { - var mid = (low + high) >>> 1; - iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; - } - return low; - }; - - // Safely convert anything iterable into a real, live array. - _.toArray = function(obj) { - if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); - return _.values(obj); - }; - - // Return the number of elements in an object. - _.size = function(obj) { - return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; - }; - - // Array Functions - // --------------- - - // Get the first element of an array. Passing **n** will return the first N - // values in the array. Aliased as `head` and `take`. The **guard** check - // allows it to work with `_.map`. - _.first = _.head = _.take = function(array, n, guard) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; - }; - - // Returns everything but the last entry of the array. Especially useful on - // the arguments object. Passing **n** will return all the values in - // the array, excluding the last N. The **guard** check allows it to work with - // `_.map`. - _.initial = function(array, n, guard) { - return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); - }; - - // Get the last element of an array. Passing **n** will return the last N - // values in the array. The **guard** check allows it to work with `_.map`. - _.last = function(array, n, guard) { - if ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { - return array[array.length - 1]; - } - }; - - // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. - // Especially useful on the arguments object. Passing an **n** will return - // the rest N values in the array. The **guard** - // check allows it to work with `_.map`. - _.rest = _.tail = _.drop = function(array, n, guard) { - return slice.call(array, (n == null) || guard ? 1 : n); - }; - - // Trim out all falsy values from an array. - _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); - }; - - // Internal implementation of a recursive `flatten` function. - var flatten = function(input, shallow, output) { - each(input, function(value) { - if (_.isArray(value)) { - shallow ? push.apply(output, value) : flatten(value, shallow, output); - } else { - output.push(value); - } - }); - return output; - }; - - // Return a completely flattened version of an array. - _.flatten = function(array, shallow) { - return flatten(array, shallow, []); - }; - - // Return a version of the array that does not contain the specified value(s). - _.without = function(array) { - return _.difference(array, slice.call(arguments, 1)); - }; - - // Produce a duplicate-free version of the array. If the array has already - // been sorted, you have the option of using a faster algorithm. - // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted, iterator, context) { - var initial = iterator ? _.map(array, iterator, context) : array; - var results = []; - var seen = []; - each(initial, function(value, index) { - if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { - seen.push(value); - results.push(array[index]); - } - }); - return results; - }; - - // 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)); - }; - - // Produce an array that contains every item shared between all the - // passed-in arrays. - _.intersection = function(array) { - var rest = slice.call(arguments, 1); - return _.filter(_.uniq(array), function(item) { - return _.every(rest, function(other) { - return _.indexOf(other, item) >= 0; - }); - }); - }; - - // Take the difference between one array and a number of other arrays. - // Only the elements present in just the first array will remain. - _.difference = function(array) { - var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); - return _.filter(array, function(value){ return !_.contains(rest, value); }); - }; - - // 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 results = new Array(length); - for (var i = 0; i < length; i++) { - results[i] = _.pluck(args, "" + i); - } - return results; - }; - - // Converts lists into objects. Pass either a single array of `[key, value]` - // pairs, or two parallel arrays of the same length -- one of keys, and one of - // the corresponding values. - _.object = function(list, values) { - var result = {}; - for (var i = 0, l = list.length; i < l; i++) { - if (values) { - result[list[i]] = values[i]; - } else { - result[list[i][0]] = list[i][1]; - } - } - return result; - }; - - // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), - // we need this function. Return the position of the first occurrence of an - // item in an array, or -1 if the item is not included in the array. - // Delegates to **ECMAScript 5**'s native `indexOf` if available. - // If the array is large and already in sort order, pass `true` - // for **isSorted** to use binary search. - _.indexOf = function(array, item, isSorted) { - if (array == null) return -1; - var i = 0, l = array.length; - if (isSorted) { - if (typeof isSorted == 'number') { - i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); - } else { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; - } - } - if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); - for (; i < l; i++) if (array[i] === item) return i; - return -1; - }; - - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. - _.lastIndexOf = function(array, item, from) { - if (array == null) return -1; - var hasIndex = from != null; - if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { - return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); - } - var i = (hasIndex ? from : array.length); - while (i--) if (array[i] === item) return i; - return -1; - }; - - // Generate an integer Array containing an arithmetic progression. A port of - // the native Python `range()` function. See - // [the Python documentation](http://docs.python.org/library/functions.html#range). - _.range = function(start, stop, step) { - if (arguments.length <= 1) { - stop = start || 0; - start = 0; - } - step = arguments[2] || 1; - - var len = Math.max(Math.ceil((stop - start) / step), 0); - var idx = 0; - var range = new Array(len); - - while(idx < len) { - range[idx++] = start; - start += step; - } - - return range; - }; - - // 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). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && 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; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; - }; - }; - - // Bind all of an object's methods to that object. Useful for ensuring that - // 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); - each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); - return obj; - }; - - // Memoize an expensive function by storing its results. - _.memoize = function(func, hasher) { - var memo = {}; - hasher || (hasher = _.identity); - return function() { - var key = hasher.apply(this, arguments); - return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); - }; - }; - - // Delays a function for the given number of milliseconds, and then calls - // it with the arguments supplied. - _.delay = function(func, wait) { - var args = slice.call(arguments, 2); - return setTimeout(function(){ return func.apply(null, args); }, wait); - }; - - // Defers a function, scheduling it to run after the current call stack has - // cleared. - _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); - }; - - // 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, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); - return function() { - context = this; args = arguments; - var later = function() { - timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; - result = func.apply(context, args); - } - whenDone(); - return result; - }; - }; - - // Returns a function, that, as long as it continues to be invoked, will not - // be triggered. The function will be called after it stops being called for - // 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; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if (!immediate) result = func.apply(context, args); - }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) result = func.apply(context, args); - return result; - }; - }; - - // Returns a function that will be executed at most one time, no matter how - // often you call it. Useful for lazy initialization. - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - memo = func.apply(this, arguments); - func = null; - return memo; - }; - }; - - // Returns the first function passed as an argument to the second, - // allowing you to adjust arguments, run code before and after, and - // conditionally execute the original function. - _.wrap = function(func, wrapper) { - return function() { - var args = [func]; - push.apply(args, arguments); - return wrapper.apply(this, args); - }; - }; - - // Returns a function that is the composition of a list of functions, each - // consuming the return value of the function that follows. - _.compose = function() { - var funcs = arguments; - return function() { - var args = arguments; - for (var i = funcs.length - 1; i >= 0; i--) { - args = [funcs[i].apply(this, args)]; - } - return args[0]; - }; - }; - - // 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); - } - }; - }; - - // Object Functions - // ---------------- - - // Retrieve the names of an object's properties. - // Delegates to **ECMAScript 5**'s native `Object.keys` - _.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; - return keys; - }; - - // Retrieve the values of an object's properties. - _.values = function(obj) { - var values = []; - for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); - return values; - }; - - // Convert an object into a list of `[key, value]` pairs. - _.pairs = function(obj) { - var pairs = []; - for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); - return pairs; - }; - - // Invert the keys and values of an object. The values must be serializable. - _.invert = function(obj) { - var result = {}; - for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; - return result; - }; - - // Return a sorted list of the function names available on the object. - // Aliased as `methods` - _.functions = _.methods = function(obj) { - var names = []; - for (var key in obj) { - if (_.isFunction(obj[key])) names.push(key); - } - return names.sort(); - }; - - // Extend a given object with all the properties in passed-in object(s). - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Return a copy of the object only containing the whitelisted properties. - _.pick = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - each(keys, function(key) { - if (key in obj) copy[key] = obj[key]; - }); - return copy; - }; - - // Return a copy of the object without the blacklisted properties. - _.omit = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - for (var key in obj) { - if (!_.contains(keys, key)) copy[key] = obj[key]; - } - return copy; - }; - - // Fill in a given object with default properties. - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Create a (shallow-cloned) duplicate of an object. - _.clone = function(obj) { - if (!_.isObject(obj)) return obj; - return _.isArray(obj) ? obj.slice() : _.extend({}, obj); - }; - - // Invokes interceptor with the obj, and then returns obj. - // The primary purpose of this method is to "tap into" a method chain, in - // order to perform operations on intermediate results within the chain. - _.tap = function(obj, interceptor) { - interceptor(obj); - return obj; - }; - - // 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. - 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; - // Unwrap any wrapped objects. - if (a instanceof _) a = a._wrapped; - if (b instanceof _) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className != toString.call(b)) return false; - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - return +a == +b; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') return false; - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] == a) return bStack[length] == b; - } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); - var size = 0, result = true; - // Recursively compare objects and arrays. - if (className == '[object Array]') { - // Compare array lengths to determine if a deep comparison is necessary. - size = a.length; - result = size == b.length; - if (result) { - // Deep compare the contents, ignoring non-numeric properties. - while (size--) { - if (!(result = eq(a[size], b[size], aStack, bStack))) break; - } - } - } 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)) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; - } - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (_.has(b, key) && !(size--)) break; - } - result = !size; - } - } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); - return result; - }; - - // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - return eq(a, b, [], []); - }; - - // Is a given array, string, or object empty? - // An "empty" object has no enumerable own-properties. - _.isEmpty = function(obj) { - if (obj == null) return true; - if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; - for (var key in obj) if (_.has(obj, key)) return false; - return true; - }; - - // Is a given value a DOM element? - _.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); - }; - - // Is a given value an array? - // Delegates to ECMA5's native Array.isArray - _.isArray = nativeIsArray || function(obj) { - return toString.call(obj) == '[object Array]'; - }; - - // Is a given variable an object? - _.isObject = function(obj) { - return obj === Object(obj); - }; - - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { - _['is' + name] = function(obj) { - return toString.call(obj) == '[object ' + name + ']'; - }; - }); - - // Define a fallback version of the method in browsers (ahem, IE), where - // there isn't any inspectable "Arguments" type. - if (!_.isArguments(arguments)) { - _.isArguments = function(obj) { - return !!(obj && _.has(obj, 'callee')); - }; - } - - // Optimize `isFunction` if appropriate. - if (typeof (/./) !== 'function') { - _.isFunction = function(obj) { - return typeof obj === 'function'; - }; - } - - // Is a given object a finite number? - _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); - }; - - // Is the given value `NaN`? (NaN is the only number which does not equal itself). - _.isNaN = function(obj) { - return _.isNumber(obj) && obj != +obj; - }; - - // Is a given value a boolean? - _.isBoolean = function(obj) { - return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; - }; - - // Is a given value equal to null? - _.isNull = function(obj) { - return obj === null; - }; - - // Is a given variable undefined? - _.isUndefined = function(obj) { - return obj === void 0; - }; - - // Shortcut function for checking if an object has a given property directly - // on itself (in other words, not on a prototype). - _.has = function(obj, key) { - return hasOwnProperty.call(obj, key); - }; - - // Utility Functions - // ----------------- - - // Run Underscore.js in *noConflict* mode, returning the `_` variable to its - // previous owner. Returns a reference to the Underscore object. - _.noConflict = function() { - root._ = previousUnderscore; - return this; - }; - - // Keep the identity function around for default iterators. - _.identity = function(value) { - return value; - }; - - // Run a function **n** times. - _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); - }; - - // Return a random integer between min and max (inclusive). - _.random = function(min, max) { - if (max == null) { - max = min; - min = 0; - } - return min + (0 | Math.random() * (max - min + 1)); - }; - - // List of HTML entities for escaping. - var entityMap = { - escape: { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - } - }; - entityMap.unescape = _.invert(entityMap.escape); - - // Regexes containing the keys and values listed immediately above. - var entityRegexes = { - escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), - unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') - }; - - // Functions for escaping and unescaping strings to/from HTML interpolation. - _.each(['escape', 'unescape'], function(method) { - _[method] = function(string) { - if (string == null) return ''; - return ('' + string).replace(entityRegexes[method], function(match) { - return entityMap[method][match]; - }); - }; - }); - - // If the value of the named property is a function then invoke it; - // otherwise, return it. - _.result = function(object, property) { - if (object == null) return null; - var value = object[property]; - return _.isFunction(value) ? value.call(object) : value; - }; - - // Add your own custom functions to the Underscore object. - _.mixin = function(obj) { - each(_.functions(obj), function(name){ - var func = _[name] = obj[name]; - _.prototype[name] = function() { - var args = [this._wrapped]; - push.apply(args, arguments); - return result.call(this, func.apply(_, args)); - }; - }); - }; - - // Generate a unique integer id (unique within the entire client session). - // Useful for temporary DOM ids. - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = idCounter++; - return prefix ? prefix + id : id; - }; - - // By default, Underscore uses ERB-style template delimiters, change the - // following template settings to use alternative delimiters. - _.templateSettings = { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g - }; - - // When customizing `templateSettings`, if you don't want to define an - // interpolation, evaluation or escaping regex, we need one that is - // guaranteed not to match. - var noMatch = /(.)^/; - - // Certain characters need to be escaped so that they can be put into a - // string literal. - var escapes = { - "'": "'", - '\\': '\\', - '\r': 'r', - '\n': 'n', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; - - // JavaScript micro-templating, similar to John Resig's implementation. - // Underscore templating handles arbitrary delimiters, preserves whitespace, - // and correctly escapes quotes within interpolated code. - _.template = function(text, data, settings) { - settings = _.defaults({}, settings, _.templateSettings); - - // Combine delimiters into one regular expression via alternation. - var matcher = new RegExp([ - (settings.escape || noMatch).source, - (settings.interpolate || noMatch).source, - (settings.evaluate || noMatch).source - ].join('|') + '|$', 'g'); - - // Compile the template source, escaping string literals appropriately. - var index = 0; - var source = "__p+='"; - text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { - source += text.slice(index, offset) - .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; - index = offset + match.length; - }); - source += "';\n"; - - // If a variable is not specified, place data values in local scope. - if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; - - source = "var __t,__p='',__j=Array.prototype.join," + - "print=function(){__p+=__j.call(arguments,'');};\n" + - source + "return __p;\n"; - - try { - var render = new Function(settings.variable || 'obj', '_', source); - } catch (e) { - e.source = source; - throw e; - } - - if (data) return render(data, _); - var template = function(data) { - return render.call(this, data, _); - }; - - // Provide the compiled function source as a convenience for precompilation. - template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; - - return template; - }; - - // Add a "chain" function, which will delegate to the wrapper. - _.chain = function(obj) { - return _(obj).chain(); - }; - - // OOP - // --------------- - // If Underscore is called as a function, it returns a wrapped object that - // can be used OO-style. This wrapper holds altered versions of all the - // underscore functions. Wrapped objects may be chained. - - // Helper function to continue chaining intermediate results. - var result = function(obj) { - return this._chain ? _(obj).chain() : obj; - }; - - // Add all of the Underscore functions to the wrapper object. - _.mixin(_); - - // Add all mutator Array functions to the wrapper. - each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - var obj = this._wrapped; - method.apply(obj, arguments); - if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; - return result.call(this, obj); - }; - }); - - // Add all accessor Array functions to the wrapper. - each(['concat', 'join', 'slice'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - return result.call(this, method.apply(this._wrapped, arguments)); - }; - }); - - _.extend(_.prototype, { - - // Start chaining a wrapped Underscore object. - chain: function() { - this._chain = true; - return this; - }, - - // Extracts the result from a wrapped and chained object. - value: function() { - return this._wrapped; - } - - }); - -}).call(this); From 321581dc8ae903f7e1c4cded2e1870195114cd5e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Sun, 23 Dec 2012 12:34:59 -0800 Subject: [PATCH 130/183] Fix increment-version script for bundled underscore --- admin/increment-version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/increment-version.js b/admin/increment-version.js index 4c961b1039..13267dc61b 100644 --- a/admin/increment-version.js +++ b/admin/increment-version.js @@ -7,7 +7,7 @@ var semver = require('semver'); var optimist = require('optimist'); var updater = require(path.join(__dirname, '..', 'app', 'lib', 'updater.js')); -var _ = require(path.join(__dirname, '..', 'app', 'lib', 'third', 'underscore.js')); +var _ = require('underscore'); // What files to update. Relative to project root. var UPDATE_FILES = [path.join('app', 'lib', 'updater.js'), From cf4190e9c0edf692fb2b0cd16ffeb6e36351c42b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Sun, 23 Dec 2012 12:36:05 -0800 Subject: [PATCH 131/183] Increment version to 0.5.3. --- History.md | 4 ++++ admin/debian/changelog | 2 +- admin/install-s3.sh | 2 +- admin/manifest.json | 6 +++--- admin/meteor.spec | 2 +- app/lib/updater.js | 2 +- app/meteor/post-upgrade.js | 2 +- docs/client/docs.html | 2 +- docs/client/docs.js | 2 +- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/History.md b/History.md index 9ae3054864..9232e415e0 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,10 @@ ## vNEXT +## v0.5.3 + +TODO: Finish 0.5.3 section + * `OAuth1Binding` improvements: #539 * `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response (including headers and statusCode), rather than just the data. diff --git a/admin/debian/changelog b/admin/debian/changelog index 6b85fb6ca3..b8b7317e6b 100644 --- a/admin/debian/changelog +++ b/admin/debian/changelog @@ -1,4 +1,4 @@ -meteor (0.5.2-1) unstable; urgency=low +meteor (0.5.3-1) unstable; urgency=low * Automated debian build. diff --git a/admin/install-s3.sh b/admin/install-s3.sh index eb3ccc3ee8..91fbc35085 100755 --- a/admin/install-s3.sh +++ b/admin/install-s3.sh @@ -5,7 +5,7 @@ ## example. URLBASE="https://d3sqy0vbqsdhku.cloudfront.net" -VERSION="0.5.2" +VERSION="0.5.3" PKGVERSION="${VERSION}-1" UNAME=`uname` diff --git a/admin/manifest.json b/admin/manifest.json index 79fb8a130c..d0f3e2f9fc 100644 --- a/admin/manifest.json +++ b/admin/manifest.json @@ -1,6 +1,6 @@ { - "version": "0.5.2", - "deb_version": "0.5.2-1", - "rpm_version": "0.5.2-1", + "version": "0.5.3", + "deb_version": "0.5.3-1", + "rpm_version": "0.5.3-1", "urlbase": "https://d3sqy0vbqsdhku.cloudfront.net" } diff --git a/admin/meteor.spec b/admin/meteor.spec index 25fe47ef1e..9388c6198e 100644 --- a/admin/meteor.spec +++ b/admin/meteor.spec @@ -5,7 +5,7 @@ Summary: Meteor platform and JavaScript application server Vendor: Meteor Name: meteor -Version: 0.5.2 +Version: 0.5.3 Release: 1 License: MIT Group: Networking/WWW diff --git a/app/lib/updater.js b/app/lib/updater.js index 43589b7c66..af6936c6eb 100644 --- a/app/lib/updater.js +++ b/app/lib/updater.js @@ -1,4 +1,4 @@ -exports.CURRENT_VERSION = "0.5.2"; +exports.CURRENT_VERSION = "0.5.3"; var fs = require("fs"); var http = require("http"); diff --git a/app/meteor/post-upgrade.js b/app/meteor/post-upgrade.js index 3153a4f449..036dca5d17 100644 --- a/app/meteor/post-upgrade.js +++ b/app/meteor/post-upgrade.js @@ -2,7 +2,7 @@ try { // XXX can't get this from updater.js because in 0.3.7 and before the // updater didn't have the right NODE_PATH set. At some point we can // remove this and just use updater.CURRENT_VERSION. - var VERSION = "0.5.2"; + var VERSION = "0.5.3"; var fs = require('fs'); var path = require('path'); diff --git a/docs/client/docs.html b/docs/client/docs.html index d03097a480..e4c114fff2 100644 --- a/docs/client/docs.html +++ b/docs/client/docs.html @@ -11,7 +11,7 @@
-

Meteor 0.5.2

+

Meteor 0.5.3

{{> introduction }} {{> concepts }} {{> api }} diff --git a/docs/client/docs.js b/docs/client/docs.js index 3413bfa00e..a32fbac4d7 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -1,4 +1,4 @@ -METEOR_VERSION = "0.5.2"; +METEOR_VERSION = "0.5.3"; Meteor.startup(function () { // XXX this is broken by the new multi-page layout. Also, it was From 41520a3df86d47b6bbc581dba0abb37c931bf80d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 24 Dec 2012 11:06:49 -0800 Subject: [PATCH 132/183] Start on History.md for 0.5.3. --- History.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/History.md b/History.md index 9232e415e0..308186a2ea 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,37 @@ TODO: Finish 0.5.3 section +* A new `--settings` argument to `meteor deploy` and `meteor run` allows you to + specify a file containing a JSON object which will be made available to server + code in the variable `Meteor.settings`. + +* Deployed apps now use a randomized hostname for their long-polling + connections, allowing users to use apps in an arbitrary number of simultaneous + tabs without hitting browser per-hostname connection limits. #131 + +* In Spark, when a template is re-rendered, elements that are preserved (using + `Template.foo.preserve` or the `preserve-input` package) and which have + user-controllable "values" (eg, form inputs) now preserve the value set by the + user, unless the server's newly rendered value is different from the + previously rendered value. That is, re-rendering a template no longer reverts + changes to form elements made by users. Additionally, elements with + type other than TEXT can now have reactive values (eg, the labels on submit + buttons can now be reactive). #510 #514 #523 #537 #558 + +* Improve rendering of " onto unchecked input works. This test was extracted from a fix for this specific issue #478 which accidentally never got merged; 442b86eebd ended up fixing this issue with a slightly different change than the unmerged fix. --- packages/spark/patch_tests.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/spark/patch_tests.js b/packages/spark/patch_tests.js index 50975bd4f7..4d2d698ae8 100644 --- a/packages/spark/patch_tests.js +++ b/packages/spark/patch_tests.js @@ -132,7 +132,9 @@ Tinytest.add("spark - patch - copyAttributes", function(test) { buf.push('<', tagName); _.each(kv, function(v,k) { allAttrNames[k] = true; - buf.push(' ', k, '="', v, '"'); + buf.push(' ', k); + if (v !== 'NO_VALUE') + buf.push('="', v, '"'); }); buf.push('>'); var nodeHtml = buf.join(''); @@ -160,18 +162,20 @@ Tinytest.add("spark - patch - copyAttributes", function(test) { check: function() { _.each(lastAttrs, function(v,k) { var actualAttr; + var expectedAttr = v || ""; if (k === "style") { actualAttr = node.style.cssText; } else if (k === "class") { actualAttr = node.className; } else if (k === "checked") { actualAttr = String(node.getAttribute(k) || ""); + if (expectedAttr === "NO_VALUE") + expectedAttr = "checked"; if (actualAttr === "true") actualAttr = "checked"; // save IE's butt } else { actualAttr = String(node.getAttribute(k) || ""); } - var expectedAttr = v || ""; test.equal(actualAttr, expectedAttr, k); }); }, @@ -228,6 +232,12 @@ Tinytest.add("spark - patch - copyAttributes", function(test) { c.copy({type:'checkbox', name:'foo', checked:'checked'}); c.check(); test.equal(c.node().checked, true); + c.copy({type:'checkbox', name:'foo'}); + c.check(); + test.equal(c.node().checked, false); + c.copy({type:'checkbox', name:'foo', checked:'NO_VALUE'}); + c.check(); + test.equal(c.node().checked, true); c.copy({type:'checkbox', name:'bar'}); test.expect_fail(); // changing "name" on a form control won't take in IE From 06ddf733998f98dfb82a3cbdf69e0773e69e41ce Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 24 Dec 2012 11:29:03 -0800 Subject: [PATCH 134/183] Finish first draft of 0.5.3 release notes. --- History.md | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/History.md b/History.md index 308186a2ea..a9bc0cd8b6 100644 --- a/History.md +++ b/History.md @@ -3,8 +3,6 @@ ## v0.5.3 -TODO: Finish 0.5.3 section - * A new `--settings` argument to `meteor deploy` and `meteor run` allows you to specify a file containing a JSON object which will be made available to server code in the variable `Meteor.settings`. @@ -22,12 +20,20 @@ TODO: Finish 0.5.3 section type other than TEXT can now have reactive values (eg, the labels on submit buttons can now be reactive). #510 #514 #523 #537 #558 -* Improve rendering of elements on IE. #496 + * Don't lose nested data contexts in IE9/10 after two seconds. #458 + * Always use the `autoReconnect` flag when connecting to Mongo. #425 * When logging in with Google, allow apps to request an "offline" token. #464 @@ -36,6 +42,15 @@ TODO: Finish 0.5.3 section overwrite `serviceData` fields that were set on a previous login and not set on this login. (For example, the refresh token from Google "offline" login.) +* `meteor` command-line improvements: + * Improve error message when mongod fails to start. + * The `NODE_OPTIONS` environment variable can be used to pass command-line + flags to node (eg, `--debug` or `--debug-brk` to enable the debugger). + * A new flag `meteor run --once` does not re-run the project if it crashes or + monitor for file changes; intended for automated testing (eg + `admin/cli-test.sh`). + * Die with error if an app name is mistakenly passed to `meteor reset`. + * `OAuth1Binding` improvements: #539 * `OAuth1Binding.get` and `OAuth1Binding.call` now return the full response (including headers and statusCode), rather than just the data. @@ -43,13 +58,19 @@ TODO: Finish 0.5.3 section * `OAuth1Binding.get`, `OAuth1Binding.call` and `OAuth1Binding.post` now take a `params` argument. This facilitates making calls to the Twitter API. -* A new flag `meteor run --once` does not re-run the project if it crashes or - monitor for file changes; intended for automated testing (eg - `admin/cli-test.sh`). +* Fix re-sending method calls on reconnect. #538 + +* `Meteor.connect` now supports URLs of the form + `ddp+sockjs://host-***.name/sockjs` and `ddpi+sockjs://host-***.name/sockjs` + to connect with DDP over HTTPS and HTTP respectively. The asterisks are + substituted with random digits. This syntax may change in a future version of + Meteor. While the trailing `/sockjs` is required in these URLs, it is no + longer supported in scheme-less URLs passed to `Meteor.connect`. * Improvements to `jsparse`: hex literals, keywords as property names, ES5 line continuations, trailing commas in object literals, line numbers in error - messages + messages, decimal literals starting with `.`, regex character classes with + slashes. * Implement the UUID v4 spec correctly (instead of losing a few bits of randomness). @@ -57,12 +78,8 @@ TODO: Finish 0.5.3 section * Update clean-css package from 0.8.2 to 0.8.3, fixing minification of `0%` values in `hsl` colors. #515 -* The underscore library is now in the dev bundle instead of in three different - places inside the source tree. (It's still in one place in the source tree, to - be served to clients.) - -Patches contributed by GitHub users Ed-von-Schleck, jwulf, meawoppl, and -nwmartin. +Patches contributed by GitHub users Ed-von-Schleck, egtann, jwulf, lvbreda, +meawoppl, nwmartin, and timhaines. ## v0.5.2 From 47d5abb7d8a654879c6d47158568f47e1b1b9e80 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 24 Dec 2012 11:31:33 -0800 Subject: [PATCH 135/183] Remove some no-longer-true caveats about Minimongo from docs. --- docs/client/api.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index aeee5f0f76..33c0455324 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -528,11 +528,9 @@ In this release, Minimongo has some limitations: * `$elemMatch` is not supported in selectors. * `$pull` in modifiers can only accept certain kinds of selectors. -* In selectors, dot notation and ordinal indexing may not work correctly. +* In selectors, dot notation may not work correctly. * `$` to denote the matched array position is not supported in modifier. -* Sort does not support subkeys (you can sort on `a`, -but not `a.b`). * `findAndModify`, upsert, aggregate functions, and map/reduce aren't supported. * The supported types are String, Number, Boolean, Array, From 79405a495d4acaffc56042f8f7e15b5b34b6171f Mon Sep 17 00:00:00 2001 From: Martin Naumann Date: Fri, 14 Dec 2012 22:41:06 +0100 Subject: [PATCH 136/183] Adding expiresAt property to Google login data for accounts-google. fixes #525 --- packages/accounts-google/google_server.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 4cfc9da432..ed863e95de 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -3,29 +3,31 @@ Accounts.oauth.registerService('google', 2, function(query) { var response = getTokens(query); - var accessToken = response.access_token; + var accessToken = response.accessToken; var identity = getIdentity(accessToken); var serviceData = { id: identity.id, accessToken: accessToken, - email: identity.email + email: identity.email, + expiresAt: (+new Date) + 1000 * parseInt(response.expiresIn, 10) }; // only set the token in serviceData if it's there. this ensures // that we don't lose old ones (since we only get this on the first // log in attempt) - if (response.refresh_token) - serviceData.refreshToken = response.refresh_token; + if (response.refreshToken) + serviceData.refreshToken = response.refreshToken; return { - serviceData: serviceData, options: {profile: {name: identity.name}} }; }); - // returns an object containing access_token, and if this is the first - // authorization request also refresh_token + // returns an object containing: + // - accessToken + // - expiresIn: lifetime of token in seconds + // - refreshToken, if this is the first authorization request var getTokens = function (query) { var config = Accounts.loginServiceConfiguration.findOne({service: 'google'}); if (!config) @@ -44,7 +46,12 @@ throw result.error; if (result.data.error) // if the http response was a json object with an error attribute throw result.data; - return result.data; + + return { + accessToken: result.data.access_token, + refreshToken: result.data.refresh_token, + expiresIn: result.data.expires_in + }; }; var getIdentity = function (accessToken) { From 1e09d4ee8d29b2f2dc53a29c723bbc94dab3038b Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 26 Dec 2012 18:06:39 -0800 Subject: [PATCH 137/183] Some minor modifications to 79405a495d4acaffc56042f8f7e15b5b34b6171f --- packages/accounts-google/google_server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index ed863e95de..ee0b6be698 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -10,7 +10,7 @@ id: identity.id, accessToken: accessToken, email: identity.email, - expiresAt: (+new Date) + 1000 * parseInt(response.expiresIn, 10) + expiresAt: (+new Date) + (1000 * response.expiresIn) }; // only set the token in serviceData if it's there. this ensures @@ -20,6 +20,7 @@ serviceData.refreshToken = response.refreshToken; return { + serviceData: serviceData, options: {profile: {name: identity.name}} }; }); From 28b42e924eff420e4decfe3e6fdf54427d7830ac Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Sat, 22 Dec 2012 14:31:35 -0800 Subject: [PATCH 138/183] Removed linkedin package from this PR Login services should be defined from within their packages Corresponding login service assets (images and css should be in their respective packages not inside of accounts-ui-unstyled) Conflicts: packages/accounts-linkedin/linkedin_client.js packages/accounts-linkedin/package.js packages/accounts-ui-unstyled/login_buttons.js packages/accounts-ui-unstyled/login_buttons_images.css --- .../accounts-facebook/accounts_facebook.css | 3 +++ packages/accounts-facebook/facebook_client.js | 1 + packages/accounts-facebook/package.js | 2 +- packages/accounts-github/accounts_github.css | 3 +++ packages/accounts-github/github_client.js | 2 ++ packages/accounts-github/package.js | 2 +- packages/accounts-google/accounts_google.css | 3 +++ packages/accounts-google/google_client.js | 1 + packages/accounts-google/package.js | 2 +- .../accounts-twitter/accounts_twitter.css | 3 +++ packages/accounts-twitter/package.js | 2 +- packages/accounts-twitter/twitter_client.js | 1 + .../accounts-ui-unstyled/login_buttons.js | 4 +++- .../login_buttons_images.css | 21 ------------------- packages/accounts-ui-unstyled/package.js | 1 - packages/accounts-weibo/accounts_weibo.css | 3 +++ packages/accounts-weibo/package.js | 2 +- packages/accounts-weibo/weibo_client.js | 1 + 18 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 packages/accounts-facebook/accounts_facebook.css create mode 100644 packages/accounts-github/accounts_github.css create mode 100644 packages/accounts-google/accounts_google.css create mode 100644 packages/accounts-twitter/accounts_twitter.css delete mode 100644 packages/accounts-ui-unstyled/login_buttons_images.css create mode 100644 packages/accounts-weibo/accounts_weibo.css diff --git a/packages/accounts-facebook/accounts_facebook.css b/packages/accounts-facebook/accounts_facebook.css new file mode 100644 index 0000000000..0da77ab20f --- /dev/null +++ b/packages/accounts-facebook/accounts_facebook.css @@ -0,0 +1,3 @@ +#login-buttons-image-facebook { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC); +} diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 42fa7c630a..0af2a361d3 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -28,6 +28,7 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback); }; + Accounts._loginButtons.loginServices.push('facebook'); })(); diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 9b63589120..ac05560e8d 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['facebook_configure.html', 'facebook_configure.js'], + ['accounts_facebook.css', 'facebook_configure.html', 'facebook_configure.js'], 'client'); api.add_files('facebook_common.js', ['client', 'server']); diff --git a/packages/accounts-github/accounts_github.css b/packages/accounts-github/accounts_github.css new file mode 100644 index 0000000000..e1893da5ad --- /dev/null +++ b/packages/accounts-github/accounts_github.css @@ -0,0 +1,3 @@ +#login-buttons-image-github { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); +} diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js index c16db9680d..ccb0540d27 100644 --- a/packages/accounts-github/github_client.js +++ b/packages/accounts-github/github_client.js @@ -25,4 +25,6 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback, {width: 900, height: 450}); }; + + Accounts._loginButtons.loginServices.push('github'); }) (); diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index 99187fb043..b713681b48 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['github_configure.html', 'github_configure.js'], + ['accounts_github.css', 'github_configure.html', 'github_configure.js'], 'client'); api.add_files('github_common.js', ['client', 'server']); diff --git a/packages/accounts-google/accounts_google.css b/packages/accounts-google/accounts_google.css new file mode 100644 index 0000000000..b9a3ecb0eb --- /dev/null +++ b/packages/accounts-google/accounts_google.css @@ -0,0 +1,3 @@ +#login-buttons-image-google { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==); +} diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 4dffdebb17..aa4f7e3eaf 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -37,4 +37,5 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback); }; + Accounts._loginButtons.loginServices.push('google'); }) (); diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index e6484baadb..a95de93723 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['google_configure.html', 'google_configure.js'], + ['accounts_google.css', 'google_configure.html', 'google_configure.js'], 'client'); api.add_files('google_common.js', ['client', 'server']); diff --git a/packages/accounts-twitter/accounts_twitter.css b/packages/accounts-twitter/accounts_twitter.css new file mode 100644 index 0000000000..d3ee6cd8fa --- /dev/null +++ b/packages/accounts-twitter/accounts_twitter.css @@ -0,0 +1,3 @@ +#login-buttons-image-twitter { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=); +} diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index bae64bbd53..d7f01f14df 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['twitter_configure.html', 'twitter_configure.js'], + ['accounts_twitter.css', 'twitter_configure.html', 'twitter_configure.js'], 'client'); api.add_files('twitter_common.js', ['client', 'server']); diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 3eb979d57d..929557516f 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -31,4 +31,5 @@ Accounts.oauth.initiateLogin(state, url, callback); }; + Accounts._loginButtons.loginServices.push('twitter'); })(); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index b2c94bd6d4..3e48d166aa 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -115,12 +115,14 @@ return ''; }; + Accounts._loginButtons.loginServices = []; + Accounts._loginButtons.getLoginServices = function () { var ret = []; // make sure to put password last, since this is how it is styled // in the ui as well. _.each( - ['facebook', 'github', 'google', 'twitter', 'weibo', 'password'], + this.loginServices, function (service) { if (Accounts[service]) ret.push({name: service}); diff --git a/packages/accounts-ui-unstyled/login_buttons_images.css b/packages/accounts-ui-unstyled/login_buttons_images.css deleted file mode 100644 index 07e05215ba..0000000000 --- a/packages/accounts-ui-unstyled/login_buttons_images.css +++ /dev/null @@ -1,21 +0,0 @@ -/* These should be in their respective packages. https://app.asana.com/0/988582960612/1477837179813 */ - -#login-buttons-image-google { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==); -} - -#login-buttons-image-facebook { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC); -} - -#login-buttons-image-weibo { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=); -} - -#login-buttons-image-twitter { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=); -} - -#login-buttons-image-github { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); -} diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js index ef41ac3bf0..66c3997f80 100644 --- a/packages/accounts-ui-unstyled/package.js +++ b/packages/accounts-ui-unstyled/package.js @@ -8,7 +8,6 @@ Package.on_use(function (api) { api.add_files([ 'accounts_ui.js', - 'login_buttons_images.css', 'login_buttons.html', 'login_buttons_single.html', 'login_buttons_dropdown.html', diff --git a/packages/accounts-weibo/accounts_weibo.css b/packages/accounts-weibo/accounts_weibo.css new file mode 100644 index 0000000000..fe3188ef80 --- /dev/null +++ b/packages/accounts-weibo/accounts_weibo.css @@ -0,0 +1,3 @@ +#login-buttons-image-weibo { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=); +} diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index c178954131..49a9555085 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['weibo_configure.html', 'weibo_configure.js'], + ['accounts_weibo.css', 'weibo_configure.html', 'weibo_configure.js'], 'client'); api.add_files('weibo_common.js', ['client', 'server']); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 7f2aea042f..c26c207095 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -25,4 +25,5 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback); }; + Accounts._loginButtons.loginServices.push('weibo'); }) (); From 423fc1d71ec64b2330c2822caef756d508a30367 Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Tue, 25 Dec 2012 17:33:15 -0800 Subject: [PATCH 139/183] Tests for the functionality we want --- .../accounts-ui-unstyled/accounts_ui_tests.js | 1 - .../accounts_ui_unstyled_tests.js | 25 +++++++++++++++++++ packages/accounts-ui-unstyled/package.js | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js diff --git a/packages/accounts-ui-unstyled/accounts_ui_tests.js b/packages/accounts-ui-unstyled/accounts_ui_tests.js index e65989f5d8..bbb10de7b9 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_tests.js @@ -14,4 +14,3 @@ Tinytest.add('accounts-ui - config validates keys', function (test) { Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); }); }); - diff --git a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js new file mode 100644 index 0000000000..d3071018e7 --- /dev/null +++ b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js @@ -0,0 +1,25 @@ +Tinytest.add( + 'accounts-ui - getLoginServices retuns an array of service hashes', + function (test) { + // setup + var services; + Accounts._loginButtons.loginServices.push('password'); + services = Accounts._loginButtons.getLoginServices(); + + test.equal(_.first(services), {name: "password"}); + } +); + + +Tinytest.add( + 'accounts-ui - getLoginServices retuns unique services', + function (test) { + // setup + var services; + Accounts._loginButtons.loginServices.push('password'); + Accounts._loginButtons.loginServices.push('password'); + + services = Accounts._loginButtons.getLoginServices(); + test.length(services, 1); + } +); diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js index 66c3997f80..7d1742c819 100644 --- a/packages/accounts-ui-unstyled/package.js +++ b/packages/accounts-ui-unstyled/package.js @@ -25,4 +25,5 @@ Package.on_test(function (api) { api.use('accounts-ui-unstyled'); api.use('tinytest'); api.add_files('accounts_ui_tests.js', 'client'); + api.add_files('accounts_ui_unstyled_tests.js', 'client'); }); From bc242ccc863d71179773992c29e0702f7274edbb Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Tue, 25 Dec 2012 17:33:39 -0800 Subject: [PATCH 140/183] Accounts._loginButtons.getLoginServices returns unique services This may be improved by passing an iterator to _.uniq instead of doing this malarkey I did for the first run with JSON stringify and parse. ^_^ --- .../accounts-ui-unstyled/login_buttons.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 3e48d166aa..15ae106955 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -118,17 +118,17 @@ Accounts._loginButtons.loginServices = []; Accounts._loginButtons.getLoginServices = function () { - var ret = []; - // make sure to put password last, since this is how it is styled - // in the ui as well. - _.each( - this.loginServices, - function (service) { - if (Accounts[service]) - ret.push({name: service}); - }); - - return ret; + var that = this; + return _.map( + _.uniq( + _.map(that.loginServices, function(service) { + return JSON.stringify({name: service}); + }) + ), + function(service_str) { + return JSON.parse(service_str); + } + ); }; Accounts._loginButtons.hasPasswordService = function () { From 2c195c0b0400ee5ed0274283368e52c5c41c3a1c Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Tue, 25 Dec 2012 17:34:49 -0800 Subject: [PATCH 141/183] Add password to the available service buttons if it's included --- packages/accounts-password/password_client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 0f9f8e8211..da13791c9c 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -155,5 +155,6 @@ methodArguments: [token], userCallback: callback}); }; -})(); + Accounts._loginButtons.loginServices.push('password'); +})(); From a16dd5c1583d3b14ed1acbe14bb72d8e7f1f3408 Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Tue, 25 Dec 2012 18:20:22 -0800 Subject: [PATCH 142/183] Vastly simplify by removing the JSON parsing. Instead, just run _.uniq on the Accounts._loginButtons.loginServices. DUH. PRESTO! --- packages/accounts-ui-unstyled/login_buttons.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 15ae106955..8aa11c3e97 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -118,17 +118,10 @@ Accounts._loginButtons.loginServices = []; Accounts._loginButtons.getLoginServices = function () { - var that = this; - return _.map( - _.uniq( - _.map(that.loginServices, function(service) { - return JSON.stringify({name: service}); - }) - ), - function(service_str) { - return JSON.parse(service_str); - } - ); + var that = this, services = that.loginServices; + return _.map(_.uniq(services), function(service) { + return {name: service}; + }); }; Accounts._loginButtons.hasPasswordService = function () { From 15513fd96c0d6ee2795613c04c018b03fa267689 Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Wed, 26 Dec 2012 13:07:33 -0800 Subject: [PATCH 143/183] Variable declarations on one line; self instead of that. --- packages/accounts-ui-unstyled/login_buttons.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 8aa11c3e97..fb716141e5 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -118,7 +118,8 @@ Accounts._loginButtons.loginServices = []; Accounts._loginButtons.getLoginServices = function () { - var that = this, services = that.loginServices; + var self = this, + services = self.loginServices; return _.map(_.uniq(services), function(service) { return {name: service}; }); From 6356b73ef8bdab6df67d87d1c11c494b9a6d5260 Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Wed, 26 Dec 2012 13:10:03 -0800 Subject: [PATCH 144/183] Remove uniqueness check. Devs shouldn't be idiots :) --- .../accounts_ui_unstyled_tests.js | 14 -------------- packages/accounts-ui-unstyled/login_buttons.js | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js index d3071018e7..aef7f5c2b4 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js @@ -9,17 +9,3 @@ Tinytest.add( test.equal(_.first(services), {name: "password"}); } ); - - -Tinytest.add( - 'accounts-ui - getLoginServices retuns unique services', - function (test) { - // setup - var services; - Accounts._loginButtons.loginServices.push('password'); - Accounts._loginButtons.loginServices.push('password'); - - services = Accounts._loginButtons.getLoginServices(); - test.length(services, 1); - } -); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index fb716141e5..7442209f4f 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -120,7 +120,7 @@ Accounts._loginButtons.getLoginServices = function () { var self = this, services = self.loginServices; - return _.map(_.uniq(services), function(service) { + return _.map(services, function(service) { return {name: service}; }); }; From 09d5c94522ffbe65bb920edfae7055655910abba Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Wed, 26 Dec 2012 15:59:52 -0800 Subject: [PATCH 145/183] Make sure password is last --- .../accounts_ui_unstyled_tests.js | 32 +++++++++++++------ .../accounts-ui-unstyled/login_buttons.js | 12 ++++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js index aef7f5c2b4..6fd36a2c96 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js @@ -1,11 +1,23 @@ -Tinytest.add( - 'accounts-ui - getLoginServices retuns an array of service hashes', - function (test) { - // setup - var services; - Accounts._loginButtons.loginServices.push('password'); - services = Accounts._loginButtons.getLoginServices(); +(function(environment) { + // setup + Accounts._loginButtons.loginServices.push('password'); + environment.meteorServices = function() { + return Accounts._loginButtons.getLoginServices(); + }; + + Tinytest.add( + 'accounts-ui - getLoginServices retuns an array of service hashes', + function (test) { + test.equal(_.first(environment.meteorServices()), {name: "password"}); + } + ); + + Tinytest.add( + 'accounts-ui - getLoginServices should always return password last', + function (test) { + Accounts._loginButtons.loginServices.push('some_other_service'); + test.equal(_.last(environment.meteorServices()), {name: "password"}); + } + ); +})(Tinytest); - test.equal(_.first(services), {name: "password"}); - } -); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 7442209f4f..351043c10c 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -119,7 +119,17 @@ Accounts._loginButtons.getLoginServices = function () { var self = this, - services = self.loginServices; + services = self.loginServices, // memoize services array + passwordIndex = services.indexOf("password"), // memoize password idx. + lastServiceAt = services.length - 1; // memoize last service idx. + + // make sure to put password last, since this is how it is styled + // if we had found password, swap w last service + if (passwordIndex !== -1) { + services[lastServiceAt] = services[passwordIndex]; + services[passwordIndex] = services[lastServiceAt]; + } + return _.map(services, function(service) { return {name: service}; }); From 8c8fdc93cbc1d3d1c0d7db6606df446f9ed9ddf2 Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Wed, 26 Dec 2012 16:03:35 -0800 Subject: [PATCH 146/183] Oops, we should memoize the last service in case we need to swap --- packages/accounts-ui-unstyled/login_buttons.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 351043c10c..02a3ae4689 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -121,13 +121,14 @@ var self = this, services = self.loginServices, // memoize services array passwordIndex = services.indexOf("password"), // memoize password idx. - lastServiceAt = services.length - 1; // memoize last service idx. + lastServiceAt = services.length - 1, // memoize last service idx. + lastService = _.last(services); // in case we need to swap anything // make sure to put password last, since this is how it is styled // if we had found password, swap w last service if (passwordIndex !== -1) { services[lastServiceAt] = services[passwordIndex]; - services[passwordIndex] = services[lastServiceAt]; + services[passwordIndex] = lastService; } return _.map(services, function(service) { From d248f15a414f83d3c1eabc5e2ff6b5f6ec872f54 Mon Sep 17 00:00:00 2001 From: Alex Notov Date: Wed, 26 Dec 2012 16:08:34 -0800 Subject: [PATCH 147/183] Remove whitespace --- packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js index 6fd36a2c96..076129dc3f 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js @@ -20,4 +20,3 @@ } ); })(Tinytest); - From a0b497e9260f8f3aa6fc6f12ce5017c8dd9bc0b9 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 26 Dec 2012 21:15:37 -0800 Subject: [PATCH 148/183] Simplify and fix accounts-ui listing of login services --- packages/accounts-facebook/facebook_client.js | 2 - packages/accounts-github/github_client.js | 2 - packages/accounts-google/google_client.js | 2 - packages/accounts-password/password_client.js | 2 - packages/accounts-twitter/twitter_client.js | 2 - .../accounts-ui-unstyled/accounts_ui_tests.js | 6 ++ .../accounts_ui_unstyled_tests.js | 22 -------- .../accounts-ui-unstyled/login_buttons.js | 56 ++++++++++++++----- .../login_buttons_dropdown.js | 1 + packages/accounts-ui-unstyled/package.js | 1 - packages/accounts-weibo/weibo_client.js | 2 - 11 files changed, 48 insertions(+), 50 deletions(-) delete mode 100644 packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 0af2a361d3..caca4c0961 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -27,8 +27,6 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback); }; - - Accounts._loginButtons.loginServices.push('facebook'); })(); diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js index ccb0540d27..c16db9680d 100644 --- a/packages/accounts-github/github_client.js +++ b/packages/accounts-github/github_client.js @@ -25,6 +25,4 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback, {width: 900, height: 450}); }; - - Accounts._loginButtons.loginServices.push('github'); }) (); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index aa4f7e3eaf..bfcd83f605 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -36,6 +36,4 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback); }; - - Accounts._loginButtons.loginServices.push('google'); }) (); diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index da13791c9c..e49cddb737 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -155,6 +155,4 @@ methodArguments: [token], userCallback: callback}); }; - - Accounts._loginButtons.loginServices.push('password'); })(); diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 929557516f..a9fd891a17 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -30,6 +30,4 @@ Accounts.oauth.initiateLogin(state, url, callback); }; - - Accounts._loginButtons.loginServices.push('twitter'); })(); diff --git a/packages/accounts-ui-unstyled/accounts_ui_tests.js b/packages/accounts-ui-unstyled/accounts_ui_tests.js index bbb10de7b9..a8f4fc40f5 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_tests.js @@ -1,3 +1,9 @@ +// XXX Most of the testing of accounts-ui is done manually, across +// multiple browsers using examples/unfinished/accounts-ui-helper. We +// should *definitely* automate this, but Tinytest is generally not +// the right abstraction to use for this. + + // XXX it'd be cool to also test that the right thing happens if options // *are* validated, but Accouns.ui._options is global state which makes this hard // (impossible?) diff --git a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js b/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js deleted file mode 100644 index 076129dc3f..0000000000 --- a/packages/accounts-ui-unstyled/accounts_ui_unstyled_tests.js +++ /dev/null @@ -1,22 +0,0 @@ -(function(environment) { - // setup - Accounts._loginButtons.loginServices.push('password'); - environment.meteorServices = function() { - return Accounts._loginButtons.getLoginServices(); - }; - - Tinytest.add( - 'accounts-ui - getLoginServices retuns an array of service hashes', - function (test) { - test.equal(_.first(environment.meteorServices()), {name: "password"}); - } - ); - - Tinytest.add( - 'accounts-ui - getLoginServices should always return password last', - function (test) { - Accounts._loginButtons.loginServices.push('some_other_service'); - test.equal(_.last(environment.meteorServices()), {name: "password"}); - } - ); -})(Tinytest); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 02a3ae4689..415072bb8c 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -115,24 +115,50 @@ return ''; }; - Accounts._loginButtons.loginServices = []; - + // 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, - services = self.loginServices, // memoize services array - passwordIndex = services.indexOf("password"), // memoize password idx. - lastServiceAt = services.length - 1, // memoize last service idx. - lastService = _.last(services); // in case we need to swap anything + var self = this; + var services = []; - // make sure to put password last, since this is how it is styled - // if we had found password, swap w last service - if (passwordIndex !== -1) { - services[lastServiceAt] = services[passwordIndex]; - services[passwordIndex] = lastService; - } + // 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(); - return _.map(services, function(service) { - return {name: service}; + // 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}; }); }; diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.js b/packages/accounts-ui-unstyled/login_buttons_dropdown.js index cd3f56a448..5eee397499 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.js +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.js @@ -180,6 +180,7 @@ return Accounts._loginButtons.hasPasswordService(); }; + // return all login services, with password last Template._loginButtonsLoggedOutAllServices.services = function () { return Accounts._loginButtons.getLoginServices(); }; diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js index 7d1742c819..66c3997f80 100644 --- a/packages/accounts-ui-unstyled/package.js +++ b/packages/accounts-ui-unstyled/package.js @@ -25,5 +25,4 @@ Package.on_test(function (api) { api.use('accounts-ui-unstyled'); api.use('tinytest'); api.add_files('accounts_ui_tests.js', 'client'); - api.add_files('accounts_ui_unstyled_tests.js', 'client'); }); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index c26c207095..a4b5258383 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -24,6 +24,4 @@ Accounts.oauth.initiateLogin(state, loginUrl, callback); }; - - Accounts._loginButtons.loginServices.push('weibo'); }) (); From de90c755516d4ae5ec83f9755aede8be36609a43 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 26 Dec 2012 23:01:05 -0800 Subject: [PATCH 149/183] Rename css files in oauth login provider packages --- .../{accounts_facebook.css => facebook_login_button.css} | 0 packages/accounts-facebook/package.js | 2 +- .../{accounts_github.css => github_login_button.css} | 0 packages/accounts-github/package.js | 2 +- .../{accounts_google.css => google_login_button.css} | 0 packages/accounts-google/package.js | 2 +- packages/accounts-twitter/package.js | 2 +- .../{accounts_twitter.css => twitter_login_button.css} | 0 packages/accounts-weibo/package.js | 2 +- .../{accounts_weibo.css => weibo_login_button.css} | 0 10 files changed, 5 insertions(+), 5 deletions(-) rename packages/accounts-facebook/{accounts_facebook.css => facebook_login_button.css} (100%) rename packages/accounts-github/{accounts_github.css => github_login_button.css} (100%) rename packages/accounts-google/{accounts_google.css => google_login_button.css} (100%) rename packages/accounts-twitter/{accounts_twitter.css => twitter_login_button.css} (100%) rename packages/accounts-weibo/{accounts_weibo.css => weibo_login_button.css} (100%) diff --git a/packages/accounts-facebook/accounts_facebook.css b/packages/accounts-facebook/facebook_login_button.css similarity index 100% rename from packages/accounts-facebook/accounts_facebook.css rename to packages/accounts-facebook/facebook_login_button.css diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index ac05560e8d..a9fb73a8aa 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['accounts_facebook.css', 'facebook_configure.html', 'facebook_configure.js'], + ['facebook_login_button.css', 'facebook_configure.html', 'facebook_configure.js'], 'client'); api.add_files('facebook_common.js', ['client', 'server']); diff --git a/packages/accounts-github/accounts_github.css b/packages/accounts-github/github_login_button.css similarity index 100% rename from packages/accounts-github/accounts_github.css rename to packages/accounts-github/github_login_button.css diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index b713681b48..e1fc2f4eb8 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['accounts_github.css', 'github_configure.html', 'github_configure.js'], + ['github_login_button.css', 'github_configure.html', 'github_configure.js'], 'client'); api.add_files('github_common.js', ['client', 'server']); diff --git a/packages/accounts-google/accounts_google.css b/packages/accounts-google/google_login_button.css similarity index 100% rename from packages/accounts-google/accounts_google.css rename to packages/accounts-google/google_login_button.css diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index a95de93723..e8c799276f 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['accounts_google.css', 'google_configure.html', 'google_configure.js'], + ['google_login_button.css', 'google_configure.html', 'google_configure.js'], 'client'); api.add_files('google_common.js', ['client', 'server']); diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index d7f01f14df..eeac247033 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['accounts_twitter.css', 'twitter_configure.html', 'twitter_configure.js'], + ['twitter_login_button.css', 'twitter_configure.html', 'twitter_configure.js'], 'client'); api.add_files('twitter_common.js', ['client', 'server']); diff --git a/packages/accounts-twitter/accounts_twitter.css b/packages/accounts-twitter/twitter_login_button.css similarity index 100% rename from packages/accounts-twitter/accounts_twitter.css rename to packages/accounts-twitter/twitter_login_button.css diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index 49a9555085..ddb17ea9f7 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -9,7 +9,7 @@ Package.on_use(function(api) { api.use('templating', 'client'); api.add_files( - ['accounts_weibo.css', 'weibo_configure.html', 'weibo_configure.js'], + ['weibo_login_button.css', 'weibo_configure.html', 'weibo_configure.js'], 'client'); api.add_files('weibo_common.js', ['client', 'server']); diff --git a/packages/accounts-weibo/accounts_weibo.css b/packages/accounts-weibo/weibo_login_button.css similarity index 100% rename from packages/accounts-weibo/accounts_weibo.css rename to packages/accounts-weibo/weibo_login_button.css From 64253bc47044ed776639ae044772c0841a7bf483 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 26 Dec 2012 23:30:26 -0800 Subject: [PATCH 150/183] accounts-ui-viewer: use triple-stash for the inclusiong of checked="checked" to avoid escaping the quotes. --- examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html index 1470af65b9..87d4b8a881 100644 --- a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html @@ -7,7 +7,7 @@