Merge branch 'devel' into oplog-limits-buffered

Conflicts:
	packages/mongo-livedata/oplog_tests.js
This commit is contained in:
David Glasser
2014-02-26 15:55:39 -08:00
76 changed files with 6838 additions and 5096 deletions

View File

@@ -15,11 +15,13 @@ GITHUB: codeinthehole <david.winterbottom@gmail.com>
GITHUB: dandv <ddascalescu+github@gmail.com>
GITHUB: DenisGorbachev <Denis.Gorbachev@faster-than-wind.ru>
GITHUB: emgee3 <hello@gravitronic.com>
GITHUB: EOT <eot@gmx.at>
GITHUB: FooBarWidget <honglilai@gmail.com>
GITHUB: jacott <geoffjacobsen@gmail.com>
GITHUB: Maxhodges <Max@whiterabbitpress.com>
GITHUB: meawoppl <meawoppl@gmail.com>
GITHUB: mitar <mitar.git@tnode.com>
GITHUB: mitar <mitar.github@tnode.com>
GITHUB: mizzao <mizzao@gmail.com>
GITHUB: mquandalle <maxime.quandalle@gmail.com>
GITHUB: nathan-muir <ndmuir@gmail.com>
@@ -28,6 +30,14 @@ GITHUB: ryw <ry@rywalker.com>
GITHUB: rzymek <rzymek@gmail.com>
GITHUB: sdarnell <stephen@darnell.plus.com>
GITHUB: timhaines <tmhaines@gmail.com>
GITHUB: jfhamlin <jfhamlin@gmail.com>
GITHUB: marcandre <github@marc-andre.ca>
GITHUB: michaelbishop <michael@michaelsplace.net>
GITHUB: OyoKooN <nathan@sxnlabs.com>
GITHUB: paulswartz <paulswartz@gmail.com>
GITHUB: rdickert <robert.dickert@gmail.com>
GITHUB: icellan <icellan@icellan.com>
GITHUB: yeputons <egor.suvorov@gmail.com>
METEOR: avital <avital@thewe.net>
METEOR: debergalis <matt@meteor.com>
@@ -40,3 +50,5 @@ METEOR: karayu <lele.yu@gmail.com>
METEOR: n1mmy <nim@meteor.com>
METEOR: sixolet <naomi@meteor.com>
METEOR: Slava <slava@meteor.com>
METEOR: stubailo <sashko@mit.edu>
METEOR: ekatek <ekate@meteor.com>

View File

@@ -1,85 +1,57 @@
## v.NEXT
* Meteor developer accounts
- `accounts-meteor-developer` package for OAuth support
- managing deployed apps with developer accounts instead of site
passwords.
- New commands: 'meteor authorized', 'meteor claim', 'meteor logout',
'meteor whoami'
## v0.7.1.1
* oplog improvements
- support all operators except $where and $near. still not used for
limit and skip
- more optimizations to avoid needless data fetches from MongoDB
- fix "Cannot call method 'has' of null" error #1767
* Integrate with Meteor developer accounts, a new way of managing your
meteor.com deployed sites. When you use `meteor deploy`, you will be
prompted to create a developer account.
- Once you've created a developer account, you can log in and out
from the command line with `meteor login` and `meteor logout`.
- You can claim legacy sites with `meteor claim`. This command will
prompt you for your site password if you are claiming a
password-protected site; after claiming it, you will not need to
enter the site password again.
- You can add or remove authorized users, and view the list of
authorized users, for a site with `meteor authorized`.
- You can view your current username with `meteor whoami`.
- This release also includes the `accounts-meteor-developer` package
for building Meteor apps that allow users to log in with their own
developer accounts.
* Minimongo improvements
- support $comment
- support 'obj' name in $where
- $regexp matches actual regexps properly
- better support for $nin, $ne, $not
- support using { $in: [/foo/, /bar/] }. #1707
- support {$exists: false}
- better type-checking for selectors
- support {x: {$elemMatch: {$gt: 5}}}
- match mongo's behavior better when there are arrays in the document
- support $near with sort
- implement updates with { $set: { 'a.$.b': 5 } }
- {$type: 4} queries
- optimize `remove({})` when observers are paused
- make update-by-id constant time
* Improve the oplog tailing implementation for getting real-time database
updates from MongoDB.
- Add support for all operators except `$where` and `$near`. Limit and
skip are not supported yet.
- Add optimizations to avoid needless data fetches from MongoDB.
- Fix an error ("Cannot call method 'has' of null") in an oplog
callback. #1767
* Add `clientAddress` and `httpHeaders` to `this.connection` in method
calls and publish functions.
* Add and improve support for minimongo operators.
- Support `$comment`.
- Support `obj` name in `$where`.
- `$regexp` matches actual regexps properly.
- Improve support for `$nin`, `$ne`, `$not`.
- Support using `{ $in: [/foo/, /bar/] }`. #1707
- Support `{$exists: false}`.
- Improve type-checking for selectors.
- Support `{x: {$elemMatch: {$gt: 5}}}`.
- Match Mongo's behavior better when there are arrays in the document.
- Support `$near` with sort.
- Implement updates with `{ $set: { 'a.$.b': 5 } }`.
- Support `{$type: 4}` queries.
- Optimize `remove({})` when observers are paused.
- Make update-by-id constant time.
- Allow `{$set: {'x._id': 1}}`. #1794
* Hash login tokens before storing them in the database. Legacy unhashed
tokens are upgraded to hashed tokens in the database as they are used
in logins.
* Cursors with a field specifier containing `{_id: 0}` can no longer be used
with `observeChanges` or `observe`. This includes the implicit calls to these
functions that are done when returning a cursor from a publish function or
using `{{#each}}`.
* Transform functions must return objects and may not change the `_id` field
(though they may leave it out)
* XXX sourcemaps support for stylesheets, including less sourcemaps
* XXX css linting (breaks on errors)
* XXX css preprocessing to concatenate files correctly (pulls @imports to the
beginning)
* XXX supports `.import.less` and `.import.styl` to prevent Meteor processing
stylesheets. `.lessimport` is deprecated
* Patch Underscore to not treat plain objects (`x.constructor === Object`)
with numeric `length` fields as arrays. Among other things, this allows you
to use documents with numeric `length` fields with Mongo. #594 #1737
* Fix races when calling login and/or logoutOtherClients from multiple
tabs. #1616
* Upgrade `jquery-waypoints` package from 1.1.7 to 2.0.3. (Contains
backward-incompatible changes).
* Add `frame-src` to `browser-policy-content` and account for
cross-browser CSP disparities.
* Upgrade CoffeeScript from 1.6.3 to 1.7.1.
* Make sure that `api.add_files('foo.coffee', {bare: true})` works when
adding CoffeeScript files. #1668
* `force-ssl`: don't require SSL during `meteor run` in IPv6
environments. #1751
* Upgraded dependencies:
- node from 0.10.22 to 0.10.25 (removed workaround from 0.7.0 -- now
support 0.10.25+)
- Upgrade jQuery from 1.8.2 to 1.10.2.
XXX see http://jquery.com/upgrade-guide/1.9/ for incompatibilities
XXX consider taking 1.11 instead, which was released this week
- source-map from 0.3.30 to 0.3.32 #1782
- websocket-driver from 0.3.1 to 0.3.2
* Upgraded dependencies
- node: 0.10.25 (from 0.10.22). The workaround for specific Node
versions from 0.7.0 is now removed; 0.10.25+ is supported.
- jquery: 1.11.0 (from 1.8.2). See
http://jquery.com/upgrade-guide/1.9/ for upgrade instructions.
- jquery-waypoints: 2.0.4 (from 1.1.7). Contains
backwards-incompatible changes.
- source-map: 0.3.2 (from 0.3.30) #1782
- websocket-driver: 0.3.2 (from 0.3.1)
- http-proxy: 1.0.2 (from a pre-release fork of 1.0)
- semver: 2.2.1 (from 2.1.0)
- request: 2.33.0 (from 2.27.0)
@@ -90,73 +62,138 @@
- source-map-support: 0.2.5 (from 0.2.3)
- mongo: 2.4.9 (from 2.4.8)
- openssl in mongo: 1.0.1f (from 1.0.1e)
- kexec from 0.1.1 to 0.2.0
- drop shell-quote from dev bundle
- XXX upgraded `less` from 1.3.3 to 1.6.1
- XXX upgraded `stylus` from 0.37.0 to 0.42.2 and `nib` from `1.0.0` to `1.0.2`
- kexec: 0.2.0 (from 0.1.1)
- less: 1.6.1 (from 1.3.3)
- stylus: 0.42.2 (from 0.37.0)
- nib: 1.0.2 (from 1.0.0)
- coffeescript: 1.7.1 (from 1.6.3)
* Types added with `EJSON.addType` now have default `clone` and `equals`
implementations. #1745
* CSS preprocessing and sourcemaps:
- Add sourcemap support for CSS stylesheet preprocessors. Use
sourcemaps for stylesheets compiled with LESS.
- Improve CSS minification to deal with `@import` statements correctly.
- Lint CSS files for invalid `@` directives.
- Change the recommended suffix for imported LESS files from
`.lessimport` to `.import.less`. Add `.import.styl` to allow
`stylus` imports. `.lessimport` continues to work but is deprecated.
* Allow cursors on named local collections to be returned from arrays in publish
functions. #1820
* Add `clientAddress` and `httpHeaders` to `this.connection` in method
calls and publish functions.
* Don't lose permissions (eg, executable bit) on npm files. #1808
* Detect if CSS failed to load and refresh.
* Don't crash with an empty programs/foo directory or one without a
package.js.
* Do a better job of handling shrinkwrap files when a npm module depends
on something that isn't a semver. #1684
* Fix failures updating npm dependencies when a node_modules directory exists
above the project directory. #1761
* In email package, print a message in dev mode when email is not sent. #1196
* Meteor accounts logins (or anything else using the `localstorage` package) no
longer persist in IE7.
* Fix `accounts-password` login with private-browsing Safari (and
generally, the use of the `localstorage` package). #1291
* New `retry` package.
* Pass through `update` and `remove` return values for validated
operations. #1759
* Hash login tokens before storing them in the database. Legacy unhashed
tokens are upgraded to hashed tokens in the database as they are used
in login requests.
* Change default accounts-ui styling and add more CSS classes.
* Don't leak sockets on error in dev-mode proxy.
* Refactor command-line tool. Add test harness and better tests. Run
`meteor self-test --help` for info on running the tools test suite.
* XXX Make springboard actually exec
* Speed up application re-build in development mode by re-using file
hash computation between file change watching code and application
build code..
* Speed up build by re-using file hashes.
* Fix issues with documents containing a key named `length` with a
numeric value. Underscore treated these as arrays instead of objects,
leading to exceptions when . Patch Underscore to not treat plain
objects (`x.constructor === Object`) with numeric `length` fields as
arrays. #594 #1737
* Include oauth_verifier as a header rather than a parameter in
`oauth1` package. #1825
* Deprecate `Accounts.loginServiceConfiguration` in favor of
`ServiceConfiguration.configurations`, exported by the
`service-configuration` package. `Accounts.loginServiceConfiguration`
is maintained for backwards-compatibility, but it is defined in a
`Meteor.startup` block and so cannot be used from top-level code.
* Refactor command-line tool. Add test harness and better tests.
* Cursors with a field specifier containing `{_id: 0}` can no longer be
used with `observeChanges` or `observe`. This includes the implicit
calls to these functions that are done when returning a cursor from a
publish function or using `{{#each}}`.
* Add `Accounts.connection` for using Meteor accounts packages with a
non-default DDP connection.
* Transform functions must return objects and may not change the `_id`
field, though they may leave it out.
* Fix order of setting login token and setting user id on a connection.
* Remove broken IE7 support from the `localstorage` package. Meteor
accounts logins no longer persist in IE7.
* Fix the `localstorage` package when used with Safari in private
browsing mode. This fixes a problem with login token storage and
account login. #1291
* Types added with `EJSON.addType` now have default `clone` and `equals`
implementations. Users may still specify `clone` or `equals` functions
to override the default behavior. #1745
* Add `frame-src` to `browser-policy-content` and account for
cross-browser CSP disparities.
* Deprecate `Oauth.initiateLogin` in favor of `Oauth.showPopup`.
* Add `WebApp.rawConnectHandlers` for adding connect handlers that run
before any other Meteor handlers, except `connect.compress()`. Raw
connect handlers see the URL's full path (even if ROOT_URL contains a
non-empty path) and they run before static assets are served.
* Add `Accounts.connection` to allow using Meteor accounts packages with
a non-default DDP connection.
* Detect and reload if minified CSS files fail to load at startup. This
prevents the application from running unstyled if the page load occurs
while the server is switching versions.
* Allow Npm.depends to specify any http or https URL containing a full
40-hex-digit SHA. #1686
* Add `retry` package for connection retry with exponential backoff.
* Pass `update` and `remove` return values correctly when using
collections validated with `allow` and `deny` rules. #1759
* If you're using Deps on the server, computations and invalidation
functions are not allowed to yield.
functions are not allowed to yield. Throw an error instead of behaving
unpredictably.
* Fix namespacing in coffeescript files added to a package with the
`bare: true` option. #1668
* Fix races when calling login and/or logoutOtherClients from multiple
tabs. #1616
* Include oauth_verifier as a header rather than a parameter in
the `oauth1` package. #1825
* Fix `force-ssl` to allow local development with `meteor run` in IPv6
environments. #1751`
* Allow cursors on named local collections to be returned from a publish
function in an array. #1820
* Fix build failure caused by a directory in `programs/` without a
package.js file.
* Do a better job of handling shrinkwrap files when an npm module
depends on something that isn't a semver. #1684
* Fix failures updating npm dependencies when a node_modules directory
exists above the project directory. #1761
* Preserve permissions (eg, executable bit) on npm files. #1808
* SockJS tweak to support relative base URLs.
* Oauth.initiateLogin is deprecated in favor of Oauth.showPopup.
* Don't leak sockets on error in dev-mode proxy.
* User-supplied connect handlers now see the URL's full path, even if
ROOT_URL contains a non-empty path.
* Clone arguments to `added` and `changed` methods in publish
functions. This allows callers to reuse objects and prevents already
published data from changing after the fact. #1750
* Don't cache direct references to the fields arguments to the subscription
`added` and `changed` methods. #1750
* Ensure springboarding to a different meteor tools version always uses
`exec` to run the old version. This simplifies process management for
wrapper scripts.
Patches contributed by GitHub users DenisGorbachev, EOT, OyoKooN, awwx,
dandv, icellan, jfhamlin, marcandre, michaelbishop, mitar, mizzao,
mquandalle, paulswartz, rdickert, rzymek, timhaines, and yeputons.
## v0.7.0.1
@@ -267,8 +304,6 @@ apply the patch and will instead disable websockets.
* Increase the maximum size spiderable will return for a page from 200kB
to 5MB.
* New 'facts' package publishes internal statistics about Meteor.
* Upgraded dependencies:
* SockJS server from 0.3.7 to 0.3.8, including new faye-websocket module.
* Node from 0.10.21 to 0.10.22

View File

@@ -71,6 +71,13 @@ handlebars: https://github.com/wycats/handlebars.js/
Copyright (C) 2011 by Yehuda Katz
----------
clean-css: https://github.com/GoalSmashers/clean-css
----------
Copyright (c) 2011 GoalSmashers.com
----------
progress: https://github.com/visionmedia/node-progress
qs: https://github.com/visionmedia/node-querystring
@@ -285,6 +292,7 @@ deep-equal: https://github.com/substack/node-deep-equal
editor: https://github.com/substack/node-editor
minimist: https://github.com/substack/minimist
quotemeta: https://github.com/substack/quotemeta
text-table: https://github.com/substack/text-table
----------
Copyright 2010, 2011, 2012, 2013 James Halliday (mail@substack.net)
@@ -587,7 +595,7 @@ Copyright 2009 Google Inc.
----------
request: https://github.com/mikeal/request
aws-sign: https://github.com/mikeal/aws-sign
aws-sign2: https://github.com/mikeal/aws-sign
cookie-jar: https://github.com/mikeal/cookie-jar
forever-agent: https://github.com/mikeal/forever-agent
oauth-sign: https://github.com/mikeal/oauth-sign
@@ -646,6 +654,35 @@ Diff Match and Patch: http://code.google.com/p/google-diff-match-patch/
Copyright 2006 Google Inc.
----------
ansicolors: https://github.com/thlorenz/ansicolors
ansistyles: https://github.com/thlorenz/ansistyles
----------
Copyright 2013 Thorsten Lorenz.
----------
columnify: https://github.com/timoxley/columnify
----------
Copyright Tim Oxley
----------
eventemitter3: https://github.com/3rd-Eden/EventEmitter3
----------
Copyright Arnout Kazemier
----------
punycode: https://github.com/bestiejs/punycode.js
----------
Copyright Mathias Bynens <http://mathiasbynens.be/>
============
BSD Licenses
@@ -1346,6 +1383,25 @@ required for its use.
Other
=====
----------
heapdump: https://github.com/bnoordhuis/node-heapdump
----------
Copyright (c) 2012, Ben Noordhuis <info@bnoordhuis.nl>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
----------
mimelib-noiconv: https://github.com/andris9/mimelib
mailcomposer: https://github.com/andris9/mailcomposer
@@ -1396,6 +1452,7 @@ By Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)
----------
opener: https://github.com/domenic/opener
path-is-inside: https://github.com/domenic/path-is-inside
----------
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
@@ -1444,6 +1501,91 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
----------
tough-cookie: https://github.com/goinstant/tough-cookie
----------
Copyright GoInstant, Inc. and other contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
The following exceptions apply:
===
`pubSufTest()` of generate-pubsuffix.js is in the public domain.
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
===
`public-suffix.txt` was obtained from
<https://mxr.mozilla.org/mozilla-central/source/netwerk/dns/effective_tld_names.dat?raw=1>
via <http://publicsuffix.org>.
That file contains the usual Mozilla triple-license, for which this project uses it
under the terms of the MPL 1.1:
// ***** BEGIN LICENSE BLOCK *****
// Version: MPL 1.1/GPL 2.0/LGPL 2.1
//
// The contents of this file are subject to the Mozilla Public License Version
// 1.1 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
// http://www.mozilla.org/MPL/
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// The Original Code is the Public Suffix List.
//
// The Initial Developer of the Original Code is
// Jo Hermans <jo.hermans@gmail.com>.
// Portions created by the Initial Developer are Copyright (C) 2007
// the Initial Developer. All Rights Reserved.
//
// Contributor(s):
// Ruben Arakelyan <ruben@rubenarakelyan.com>
// Gervase Markham <gerv@gerv.net>
// Pamela Greene <pamg.bugs@gmail.com>
// David Triendl <david@triendl.name>
// Jothan Frakes <jothan@gmail.com>
// The kind representatives of many TLD registries
//
// Alternatively, the contents of this file may be used under the terms of
// either the GNU General Public License Version 2 or later (the "GPL"), or
// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
// in which case the provisions of the GPL or the LGPL are applicable instead
// of those above. If you wish to allow use of your version of this file only
// under the terms of either the GPL or the LGPL, and not to allow others to
// use your version of this file under the terms of the MPL, indicate your
// decision by deleting the provisions above and replace them with the notice
// and other provisions required by the GPL or the LGPL. If you do not delete
// the provisions above, a recipient may use your version of this file under
// the terms of any one of the MPL, the GPL or the LGPL.
//
// ***** END LICENSE BLOCK *****
----------
MongoDB: http://www.mongodb.org/
----------

View File

@@ -1 +1 @@
sso-1
0.7.1.1

View File

@@ -1629,6 +1629,16 @@ This function is provided by the `accounts-password` package. See the
{{> api_box loginWithExternalService}}
Available functions are:
* `Meteor.loginWithMeteorDeveloperAccount`
* `Meteor.loginWithFacebook`
* `Meteor.loginWithGithub`
* `Meteor.loginWithGoogle`
* `Meteor.loginWithMeetup`
* `Meteor.loginWithTwitter`
* `Meteor.loginWithWeibo`
These functions initiate the login process with an external
service (eg: Facebook, Google, etc), using OAuth. When called they open a new pop-up
window that loads the provider's login page. Once the user has logged in
@@ -1659,14 +1669,15 @@ External login services typically require registering and configuring
your application before use. The easiest way to do this is with the
[`accounts-ui` package](#accountsui) which presents a step-by-step guide
to configuring each service. However, the data can be also be entered
manually in the `Accounts.loginServiceConfiguration` collection. For
example:
manually in the `ServiceConfiguration.configurations` collection, which
is exported by the `service-configuration` package. For example, after
running `meteor add service-configuration` in your app:
// first, remove configuration entry in case service is already configured
Accounts.loginServiceConfiguration.remove({
ServiceConfiguration.configurations.remove({
service: "weibo"
});
Accounts.loginServiceConfiguration.insert({
ServiceConfiguration.configurations.insert({
service: "weibo",
clientId: "1292962797",
secret: "75a730b58f5691de5522789070c319bc"

View File

@@ -154,21 +154,18 @@ other packages. However sometimes load order dependencies in your
application are unavoidable. The JavaScript and CSS files in an
application are loaded according to these rules:
* Files in directories named `lib` are loaded first.
* Files that match `main.*` are loaded after everything else.
* Files in subdirectories are loaded before files in parent
directories, so that files in the deepest subdirectory are loaded
first (after `lib`), and files in the root directory are loaded last
(other than `main.*`).
* Files in subdirectories are loaded before files in parent directories, so that
files in the deepest subdirectory are loaded first, and files in the root
directory are loaded last.
* Within a directory, files are loaded in alphabetical order by
filename.
These rules stack, so that within `lib`, for example, files are still
loaded in alphabetical order; and if there are multiple files named
`main.js`, the ones in subdirectories are loaded earlier.
* After sorting as described above, all files under directories named `lib` are
moved before everything else (preserving their order).
* Finally, all files that match `main.*` are moved after everything else
(preserving their order).
{{/better_markdown}}
</template>

View File

@@ -201,13 +201,7 @@ var toc = [
"Meteor.logout",
"Meteor.logoutOtherClients",
"Meteor.loginWithPassword",
{name: "Meteor.loginWithMeteorDeveloperAccount", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWithFacebook", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWithGithub", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWithGoogle", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWithMeetup", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWithTwitter", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWithWeibo", id: "meteor_loginwithexternalservice"},
{name: "Meteor.loginWith<Service>", id: "meteor_loginwithexternalservice"},
{type: "spacer"},
{name: "{{currentUser}}", id: "template_currentuser"},

View File

@@ -1,5 +1,5 @@
// While galaxy apps are on their own special meteor releases, override
// Meteor.release here.
if (Meteor.isClient) {
Meteor.release = Meteor.release ? "0.7.0.1" : undefined;
Meteor.release = Meteor.release ? "0.7.1.1" : undefined;
}

View File

@@ -1 +1 @@
0.7.0.1
0.7.1.1

View File

@@ -1 +1 @@
0.7.0.1
0.7.1.1

View File

@@ -1 +1 @@
0.7.0.1
0.7.1.1

View File

@@ -1 +1 @@
0.7.0.1
0.7.1.1

View File

@@ -1,6 +1,5 @@
Package.describe({
summary: "Login service for Meteor developer accounts",
internal: true // XXX for now
summary: "Login service for Meteor developer accounts"
});
Package.on_use(function (api) {

View File

@@ -43,7 +43,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () {
// XXX we should allow service packages to set their capitalized name
return 'GitHub';
else if (this.name === 'meteor-developer')
return 'a Meteor developer account';
return 'Meteor';
else
return capitalize(this.name);
};

View File

@@ -177,7 +177,8 @@ WebApp.connectHandlers.use(function(req, res, next) {
var sizeCheck = function() {
var totalSize = 0;
_.each(WebApp.clientProgram.manifest, function (resource) {
if (resource.cacheable && resource.where === 'client') {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url)) {
totalSize += resource.size;
}
});

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Automatically publish the entire database to all clients"
summary: "Publish the entire database to all clients"
});
// This package is empty; its presence is detected by livedata and

View File

@@ -1 +0,0 @@
.build*

View File

@@ -1,59 +0,0 @@
Cookies = {};
// Given the value of a Cookie header, returns a dictionary of cookie
// keys => values. If passed the empty string (or a string that is all
// whitespace), returns {}
//
// cookieString is the value of document.cookies or a Cookie header,
// for example "a=b; c=d".
Cookies.parse = function (cookieString) {
var cookies = {};
var cookieParts = cookieString.split(/\s*;\s*/);
_.each(cookieParts, function (part) {
var match = part.match(/^([^=]+)=(.*)/);
if (match)
// Browsers are not supposed to send multiple values for the
// same cookie, but if they do, do the easy thing, which is to
// take the last value seen.
cookies[match[1]] = match[2];
});
return cookies;
};
// Given a dictionary of cookie names and values, return a Cookie
// header (as parsed by Cookies.parse).
//
// No attempt is made to sanitize or quote characters in the cookie
// name or value. Behavior varies between browsers, between RFCs, and
// between browsers and RFCs. If you want to play it safe, good advice
// would be to limit cookie names to alphanumerics, dashes, and
// underscores, and limit cookie values to printable ASCII characters
// excluding quote, comma, semicolon, backspace, and whitespace.
Cookies.stringify = function (cookies) {
// RFC6265 says that valid characters in a cookie name are:
// token = <token, defined in [RFC2616], Section 2.2>
// RFC2616 is HTTP 1.1 and defines 'token' as:
// token = 1*<any CHAR except CTLs or separators>
// separators = "(" | ")" | "<" | ">" | "@"
// | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "="
// | "{" | "}" | SP | HT
//
// RFC6265 says that valid characters in a cookie value are:
// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs,
// ; whitespace DQUOTE, comma, semicolon,
// ; and backslash
//
// In practice, browsers (at least Chrome) permit a wider range of
// characters in cookie values (such as, in Chome, at least comma
// and double quote). So for now we're going to not worry about this
// and trust the user to know what characters their targeted
// browsers tolerate.
return _.map(cookies, function (value, key) {
return key + "=" + value;
}).join(";");
};

View File

@@ -1,55 +0,0 @@
// If the current origin (and path) has a (non-HttpOnly) cookie for
// 'name', return it. Otherwise return null.
Cookies.get = function (name) {
var cookies = Cookies.parse(document.cookie || '');
return _.has(cookies, name) ? cookies[name] : null;
};
// Set a cookie 'name'='value on the current origin. It is the
// caller's responsibility to ensure that 'name' and 'value' contain
// only characters that are legal in cookie names and values.
//
// Options may include:
//
// - path: Path prefix for which the cookie should be sent. If not
// specified, defaults to the current path of document.location.
//
// - domain: Domain for which the cookie should be sent. Default to
// the current domain (more precisely, the host part of the current
// document.location). Use ".mysite.com" to send to mysite.com and
// all of mysite's subdomains.
//
// - maxAge: How long the cookie should live (in seconds). If not
// provided, the cookie will expire at the end of the browser
// session.
//
// - secure: If true, provide this cookie only for secure (https)
// connections. If you call this from a page that was loaded over
// http, the cookie will be set but you won't be able to read it
// back unless the user reloads the page over https.
//
// To delete a cookie, set maxAge to zero, passing the same name,
// domain, and path.
Cookies.set = function (name, value, options) {
options = options || {};
var cmd = name + '=' + value;
if (_.has(options, 'path'))
cmd += ";path=" + options.path;
if (_.has(options, 'domain'))
cmd += ";domain=" + options.domain;
if (_.has(options, "maxAge")) {
// Not all browsers support 'max-age', but all support 'expires'.
var when = new Date((new Date).getTime() + options.maxAge * 1000);
cmd += ";expires=" + when.toUTCString();
}
if (_.has(options, "secure"))
cmd += ";secure";
// This does not set document.cookie. It causes the browser to
// behave as if it had received a Set-Cookie header with the value
// 'cmd'.
//
// "This is the worst interface I have ever seen in my life." -- Emily
document.cookie = cmd;
};

View File

@@ -1,14 +0,0 @@
Tinytest.add("cookies - browser set/get", function (test) {
var name = Random.id();
test.equal(Cookies.get(name), null);
Cookies.set(name, "hello");
test.equal(Cookies.get(name), "hello");
Cookies.set(name, "hello again");
test.equal(Cookies.get(name), "hello again");
Cookies.set(name, "stuff", { maxAge: 0 });
test.equal(Cookies.get(name), null);
Cookies.set(name, "kitten", { path: "/somewhere-else" });
test.equal(Cookies.get(name), null);
Cookies.set(name, "kitten", { path: "/somewhere-else", maxAge: 0 });
});

View File

@@ -1,12 +0,0 @@
Tinytest.add("cookies - parse and stringify", function (test) {
test.equal(Cookies.parse("a=b; c=d"), {a: "b", c: "d"});
test.equal(Cookies.parse("a=b;c=d"), {a: "b", c: "d"});
test.equal(Cookies.parse("a=b;c=d;e=12"), {a: "b", c: "d", e: "12"});
test.equal(Cookies.parse("a=b=c;d=e=f"), {a: "b=c", d: "e=f"});
// results depends on object key order being preserved, but it will
// probably work in all of our test hosts
test.equal(Cookies.stringify({a: "1"}), "a=1");
test.equal(Cookies.stringify({a: "1", b: "2"}), "a=1;b=2");
test.equal(Cookies.stringify({a: "a=1", b: "b=2"}), "a=a=1;b=b=2");
});

View File

@@ -1,20 +0,0 @@
Package.describe({
summary: "Parsing cookies",
internal: true
});
Package.on_use(function (api) {
api.use('underscore', ['client', 'server']);
api.export('Cookies', ['client', 'server']);
api.add_files('cookies.js', ['client', 'server']);
api.add_files('cookies_client.js', ['client']);
});
Package.on_test(function (api) {
api.use('cookies', ['client', 'server']);
api.use('tinytest', ['client', 'server']);
api.use('random', ['client']);
api.add_files('cookies_test.js', ['client', 'server']);
api.add_files('cookies_client_test.js', ['client']);
});

View File

@@ -22,6 +22,15 @@ var _debugFunc = function () {
function () {}));
};
var _throwOrLog = function (from, e) {
if (throwFirstError) {
throw e;
} else {
_debugFunc()("Exception from Deps " + from + " function:",
e.stack || e.message);
}
};
var nextId = 1;
// computations whose callbacks we should call at flush time
var pendingComputations = [];
@@ -34,6 +43,12 @@ var inFlush = false;
// Deps.nonreactive, which nullfies currentComputation even though
// an enclosing computation may still be running.
var inCompute = false;
// `true` if the `_throwFirstError` option was passed in to the call
// to Deps.flush that we are in. When set, throw rather than log the
// first error encountered while flushing. Before throwing the error,
// finish flushing (from a catch block), logging any subsequent
// errors.
var throwFirstError = false;
var afterFlushCallbacks = [];
@@ -159,20 +174,23 @@ _.extend(Deps.Computation.prototype, {
var self = this;
self._recomputing = true;
while (self.invalidated && ! self.stopped) {
try {
self._compute();
} catch (e) {
_debugFunc()("Exception from Deps recompute:", e.stack || e.message);
try {
while (self.invalidated && ! self.stopped) {
try {
self._compute();
} catch (e) {
_throwOrLog("recompute", e);
}
// If _compute() invalidated us, we run again immediately.
// A computation that invalidates itself indefinitely is an
// infinite loop, of course.
//
// We could put an iteration counter here and catch run-away
// loops.
}
// If _compute() invalidated us, we run again immediately.
// A computation that invalidates itself indefinitely is an
// infinite loop, of course.
//
// We could put an iteration counter here and catch run-away
// loops.
} finally {
self._recomputing = false;
}
self._recomputing = false;
}
});
@@ -227,7 +245,7 @@ _.extend(Deps.Dependency.prototype, {
_.extend(Deps, {
// http://docs.meteor.com/#deps_flush
flush: function () {
flush: function (_opts) {
// Nested flush could plausibly happen if, say, a flush causes
// DOM mutation, which causes a "blur" event, which runs an
// app event handler that calls Deps.flush. At the moment
@@ -244,32 +262,37 @@ _.extend(Deps, {
inFlush = true;
willFlush = true;
throwFirstError = !! (_opts && _opts._throwFirstError);
while (pendingComputations.length ||
afterFlushCallbacks.length) {
try {
while (pendingComputations.length ||
afterFlushCallbacks.length) {
// recompute all pending computations
var comps = pendingComputations;
pendingComputations = [];
// recompute all pending computations
while (pendingComputations.length) {
var comp = pendingComputations.shift();
comp._recompute();
}
for (var i = 0, comp; comp = comps[i]; i++)
comp._recompute();
if (afterFlushCallbacks.length) {
// call one afterFlush callback, which may
// invalidate more computations
var func = afterFlushCallbacks.shift();
try {
func();
} catch (e) {
_debugFunc()("Exception from Deps afterFlush function:",
e.stack || e.message);
if (afterFlushCallbacks.length) {
// call one afterFlush callback, which may
// invalidate more computations
var func = afterFlushCallbacks.shift();
try {
func();
} catch (e) {
_throwOrLog("afterFlush function", e);
}
}
}
} catch (e) {
inFlush = false; // needed before calling `Deps.flush()` again
Deps.flush({_throwFirstError: false}); // finish flushing
throw e;
} finally {
willFlush = false;
inFlush = false;
}
inFlush = false;
willFlush = false;
},
// http://docs.meteor.com/#deps_autorun

View File

@@ -371,4 +371,60 @@ Tinytest.add("deps - onInvalidate", function (test) {
c1.stop();
test.equal(buf, 'm');
Deps.flush();
});
});
Tinytest.add('deps - invalidate at flush time', function (test) {
// Test this sentence of the docs: Functions are guaranteed to be
// called at a time when there are no invalidated computations that
// need rerunning.
var buf = [];
Deps.afterFlush(function () {
buf.push('C');
});
// When c1 is invalidated, it invalidates c2, then stops.
var c1 = Deps.autorun(function (c) {
if (! c.firstRun) {
buf.push('A');
c2.invalidate();
c.stop();
}
});
var c2 = Deps.autorun(function (c) {
if (! c.firstRun) {
buf.push('B');
c.stop();
}
});
// Invalidate c1. If all goes well, the re-running of
// c2 should happen before the afterFlush.
c1.invalidate();
Deps.flush();
test.equal(buf.join(''), 'ABC');
});
Tinytest.add('deps - throwFirstError', function (test) {
var d = new Deps.Dependency;
Deps.autorun(function (c) {
d.depend();
if (!c.firstRun)
throw new Error("foo");
});
d.changed();
// doesn't throw; logs instead.
Meteor._suppress_log(1);
Deps.flush();
d.changed();
test.throws(function () {
Deps.flush({_throwFirstError: true});
}, /foo/);
});

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Require this application to use secure transport (HTTPS)"
summary: "Require this application to use HTTPS"
});
Package.on_use(function (api) {

View File

@@ -1,8 +1,9 @@
Package.describe({
summary: "Execute a function when the user scrolls past an element"
summary: "Run a function when the user scrolls past an element"
});
Package.on_use(function (api) {
api.use('jquery');
api.add_files('waypoints.js', 'client');
api.use('coffeescript');
api.add_files('waypoints.coffee', 'client');
});

View File

@@ -0,0 +1,692 @@
###
jQuery Waypoints - v2.0.4
Copyright (c) 2011-2014 Caleb Troughton
Dual licensed under the MIT license and GPL license.
https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt
###
((root, factory) ->
if typeof define is 'function' and define.amd
define 'waypoints', ['jquery'], ($) ->
factory $, root
else
factory root.jQuery, root
) this, ($, window) ->
$w = $ window
# Touch support feature test
isTouch = 'ontouchstart' in window
# Internal plugin-wide variables:
# - allWaypoints: A hash containing two hashes, one for vertical waypoints
# and one for horizontal waypoints. In each hash they value is a Waypoint
# instance and the key is that waypoint's unique ID.
# - contextCounter: A counter that is incremented with each instantiation
# of the Context class, used in its unique ID.
# - contexts: A hash of all contexts. The value of each entry is a Context
# instance and the key is that context's unique ID.
# - contextKey: The DOM element for each context keeps a reference to the
# context's unique ID in the jQuery .data() object. This is the key for
# that data entry.
# - resizeEvent: The namespaced resize event used by contexts.
# - scrollEvent: The namespaced scroll event used by contexts.
# - waypointCounter: A counter that is incremented with each instantiation
# of the Waypoint class, used in its unique ID.
# - waypointKey: The DOM element for each waypoint keeps a reference to an
# array of the unique IDs of all waypoints attached to that element. This
# array is kept in the jQuery .data() object, and this is the key for
# that entry.
# - wp: A variable shortcut for the waypoint method name on the $.fn object.
# Using this variable just helps with minification.
# - wps: A variable shortcut for the waypoints method name on the $ object.
# Using this variable just helps with minification.
allWaypoints =
horizontal: {}
vertical: {}
contextCounter = 1
contexts = {}
contextKey = 'waypoints-context-id'
resizeEvent = 'resize.waypoints'
scrollEvent = 'scroll.waypoints'
waypointCounter = 1
waypointKey = 'waypoints-waypoint-ids'
wp = 'waypoint'
wps = 'waypoints'
# Context: Represents a single scrolling element in which waypoints live.
# For most users there will only be one Context, the window, but users can
# use other scrollable elements as a context using the "context" option
# when creating waypoints.
# Properties:
# - $element: jQuery object containing the context element.
# - element: The raw HTMLNode of the context element.
# - didResize: A flag used in throttling the resize event.
# - didScroll: A flag used in throttling the scroll event.
# - id: A unique identifier for the context.
# - oldScroll: A hash containing...
# - x: The context's last known horizontal scroll value.
# - y: The context's last known vertical scroll value.
# - waypoints: A hash containing two hashes with all waypoints in the context.
# Entries are in the same style as the allWaypoints hashes:
# (key = waypoint.id, value = waypoint)
# - horizontal: A hash of all horizontal waypoints.
# - vertical: A hash of all vertical waypoints.
class Context
constructor: ($element) ->
@$element = $element
@element = $element[0]
@didResize = no
@didScroll = no
@id = 'context' + contextCounter++
@oldScroll =
x: $element.scrollLeft()
y: $element.scrollTop()
@waypoints =
horizontal: {}
vertical: {}
# We need to keep a reference to this Context instance on the DOM node
# so we can look it up later based on the node.
@element[contextKey] = @id
# To do that look up, we need to have this instance in the global hash.
contexts[@id] = this
# Run scroll checks on scroll, but throttle it for performance reasons.
$element.bind scrollEvent, =>
unless @didScroll or isTouch
@didScroll = yes
scrollHandler = =>
@doScroll()
@didScroll = no
window.setTimeout scrollHandler, $[wps].settings.scrollThrottle
# Run a refresh on resize, but throttle it for performance reasons.
$element.bind resizeEvent, =>
unless @didResize
@didResize = yes
resizeHandler = =>
$[wps] 'refresh'
@didResize = no
window.setTimeout resizeHandler, $[wps].settings.resizeThrottle
# doScroll()
# Looks at the new scroll values for the context, compares them to the old
# scroll values, and checks to see if any waypoints should be triggered
# by that change.
doScroll: ->
# We use some hashes with common values for each axis so that we can
# just iterate over it rather than write the whole thing twice for
# each axis.
axes =
horizontal:
newScroll: @$element.scrollLeft()
oldScroll: @oldScroll.x
forward: 'right'
backward: 'left'
vertical:
newScroll: @$element.scrollTop()
oldScroll: @oldScroll.y
forward: 'down'
backward: 'up'
# This is a small "hack" for iOS, needed because scrolls in mobile
# Safari that start or end with the URL bar showing will cause window
# height changes without firing a resize event.
if isTouch and (!axes.vertical.oldScroll or !axes.vertical.newScroll)
$[wps] 'refresh'
# For each axis, check to see if any waypoints have been crossed.
# Also determine the direction it's being crossed and sort/reverse all
# crossed waypoints accordingly. And, of course, trigger the waypoints.
$.each axes, (aKey, axis) =>
triggered = []
isForward = axis.newScroll > axis.oldScroll
direction = if isForward then axis.forward else axis.backward
$.each @waypoints[aKey], (wKey, waypoint) ->
if axis.oldScroll < waypoint.offset <= axis.newScroll
triggered.push waypoint
else if axis.newScroll < waypoint.offset <= axis.oldScroll
triggered.push waypoint
triggered.sort (a, b) -> a.offset - b.offset
triggered.reverse() unless isForward
$.each triggered, (i, waypoint) ->
if waypoint.options.continuous or i is triggered.length - 1
waypoint.trigger [direction]
# Now that we're done with the check, the new scroll values become
# the old scroll values for the next check.
@oldScroll =
x: axes.horizontal.newScroll
y: axes.vertical.newScroll
# refresh()
# Runs through all of the waypoints in the context and recalculates
# their offsets (the scroll value at which the waypoint is triggered.)
# If a change in offset also happens to cross the context's current
# scroll value, the waypoint will be triggered in the appropriate direction
# unless prevented by the "onlyOnScroll" waypoint option.
refresh: () ->
isWin = $.isWindow @element
cOffset = @$element.offset()
# Make sure we have the most up-to-date scroll values for our context.
@doScroll()
# Each axis recalculation needs to know some things:
# - contextOffset: The distance between the edge of the document and
# the context element.
# - contextScroll: The scroll value of the context. However, if the
# context is the window this needs to be 0 because this value only
# comes into play when used in adjustment calculations for non-window
# context waypoints.
# - contextDimension: Width or height of the context.
# - oldScroll: The scroll value of the context. Unlike "contextScroll",
# this is the same no matter the type of context, and is used when
# determining whether a newly added waypoint should immediately fire
# on its first offset calculation.
# - forward: Direction string passed to forward waypoint triggers.
# - backward: Direction string passed to backward waypoint triggers.
# - offsetProp: Key of the .offset() object for this axis.
axes =
horizontal:
contextOffset: if isWin then 0 else cOffset.left
contextScroll: if isWin then 0 else @oldScroll.x
contextDimension: @$element.width()
oldScroll: @oldScroll.x
forward: 'right'
backward: 'left'
offsetProp: 'left'
vertical:
contextOffset: if isWin then 0 else cOffset.top
contextScroll: if isWin then 0 else @oldScroll.y
contextDimension: if isWin then $[wps]('viewportHeight') else \
@$element.height()
oldScroll: @oldScroll.y
forward: 'down'
backward: 'up'
offsetProp: 'top'
# For each axis, run through the waypoints. Store the old offset.
# Recalculate the new offset. Check the difference against the context's
# current scroll value and trigger any crossed waypoints accordingly.
$.each axes, (aKey, axis) =>
$.each @waypoints[aKey], (i, waypoint) ->
adjustment = waypoint.options.offset
oldOffset = waypoint.offset
elementOffset = if $.isWindow waypoint.element then 0 else \
waypoint.$element.offset()[axis.offsetProp]
# The "offset" waypoint option (which we call "adjustment" here) can
# be a number, percentage string, keyword string (bottom-in-view),
# or a function. So we deal with all of these types here.
if $.isFunction adjustment
adjustment = adjustment.apply waypoint.element
else if typeof adjustment is 'string'
adjustment = parseFloat adjustment
if waypoint.options.offset.indexOf('%') > -1
adjustment = Math.ceil(axis.contextDimension * adjustment / 100)
# We've finally calculated all the crazy little adjustments that
# can come from using non-window contexts and the "offset" option.
# Store the damn thing.
waypoint.offset = elementOffset \
- axis.contextOffset \
+ axis.contextScroll \
- adjustment
# "onlyOnScroll" tells us to not even consider triggering waypoints
# during refresh, so we can eject early.
return if (waypoint.options.onlyOnScroll and oldOffset?) or \
!waypoint.enabled
# Case where the refresh causes a backward trigger.
if oldOffset isnt null and \
oldOffset < axis.oldScroll <= waypoint.offset
waypoint.trigger [axis.backward]
# Now the forward case.
else if oldOffset isnt null and \
oldOffset > axis.oldScroll >= waypoint.offset
waypoint.trigger [axis.forward]
# "oldOffset" values of null mean this is the first calculation of
# the waypoint's offset. It's a special time in a waypoint's life.
else if oldOffset is null and axis.oldScroll >= waypoint.offset
waypoint.trigger [axis.forward]
# checkEmpty()
# Looks at the waypoints hashes. If they are empty, the context removes
# itself from the global contexts hash.
checkEmpty: ->
if $.isEmptyObject(@waypoints.horizontal) and \
$.isEmptyObject(@waypoints.vertical)
@$element.unbind [resizeEvent, scrollEvent].join(' ')
delete contexts[@id]
# Waypoint: Represents a single callback function tied to an element. An
# element can have multiple waypoints with multiple offsets.
# Properties:
# - $element: jQuery object containing the waypoint element.
# - element: The raw HTMLNode of the waypoint element.
# - axis: 'horizontal' || 'vertical' - The axis on which this waypoint lives.
# - callback: The function that is fired when the waypoint is triggered.
# - context: A reference to the context this waypoint belongs to.
# - enabled: Boolean indicating whether this waypoint is enabled or not.
# Disabled waypoints are still returned in functions that aggregate
# waypoints, but do not fire their callbacks.
# - id: A unique identifier for the waypoint.
# - offset: The scroll offset at which the waypoint should trigger.
# - options: A hash containing the various waypoint options.
# See $.fn.waypoint.defaults for more information on those options.
class Waypoint
constructor: ($element, context, options) ->
options = $.extend {}, $.fn[wp].defaults, options
if options.offset is 'bottom-in-view'
options.offset = ->
contextHeight = $[wps] 'viewportHeight'
unless $.isWindow context.element
contextHeight = context.$element.height()
contextHeight - $(this).outerHeight()
@$element = $element
@element = $element[0]
@axis = if options.horizontal then 'horizontal' else 'vertical'
@callback = options.handler
@context = context
@enabled = options.enabled
@id = 'waypoints' + waypointCounter++
@offset = null
@options = options
# Add our new waypoint to its context.
context.waypoints[@axis][@id] = this
# Add it to the global hash.
allWaypoints[@axis][@id] = this
# Add the waypoint's id to the element's waypoint id list.
idList = @element[waypointKey] ? []
idList.push @id
@element[waypointKey] = idList
# trigger(array)
# Calls the waypoint's callback function, passing to it the arguments
# supplied in the "args" array.
trigger: (args) ->
return unless @enabled
if @callback?
@callback.apply @element, args
if @options.triggerOnce
@destroy()
# disable()
# Temporarily disables a waypoint from firing its callback.
disable: ->
@enabled = false
# enable()
# Breathe life back into the waypoint.
enable: ->
@context.refresh()
@enabled = true
# destroy()
# Kills the waypoint for good.
destroy: ->
delete allWaypoints[@axis][@id]
delete @context.waypoints[@axis][@id]
@context.checkEmpty()
# Waypoint.getWaypointsByElement(HTMLNode)
# Returns an array of all Waypoint instances attached to the "element"
# HTMLNode. Returns an empty array if there are no attached waypoints.
@getWaypointsByElement: (element) ->
ids = element[waypointKey]
return [] unless ids
all = $.extend {}, allWaypoints.horizontal, allWaypoints.vertical
$.map ids, (id) ->
all[id]
# These methods are available on the $.fn object by using the method
# name as the first argument to .waypoint. Ex: $('div').waypoint('destroy')
methods =
# init(function, object)
# Creates a new waypoint (and if needed, a new context) using the supplied
# callback function and options.
# The "f" function and the "options" object are both optional, but at least
# one must be supplied. So acceptable signatures are:
# - .waypoint(f)
# - .waypoint(options)
# - .waypoint(f, options)
# This "init" method should never need to be called explicity by the user.
# It is the default method that is delegated to when .waypoint is called
# with one of the above signatures.
# Ex: $('div').waypoint(function(direction) {
# // Do things
# }, { offset: '100%' });
init: (f, options) ->
options ?= {}
options.handler ?= f
@each ->
$this = $ this
contextElement = options.context ? $.fn[wp].defaults.context
unless $.isWindow contextElement
contextElement = $this.closest contextElement
contextElement = $ contextElement
context = contexts[contextElement[0][contextKey]]
context = new Context contextElement unless context
new Waypoint $this, context, options
$[wps] 'refresh'
this
# Disable, enable, and destroy all just delegate to the instance methods
# of the waypoints attached to the subject elements.
disable: -> methods._invoke.call this, 'disable'
enable: -> methods._invoke.call this, 'enable'
destroy: -> methods._invoke.call this, 'destroy'
# .waypoint('prev', string, string|HTMLNode|jQuery)
# Returns a jQuery object containing previous waypoint elements. This
# creates a new entry in the jQuery object stack just like jQuery's prev
# function. "axis" indicates the axis on which to traverse
# ('horizontal' | 'vertical') and "selector" indicates which context
# element to use. The defaults are 'vertical' and window respectively.
prev: (axis, selector) ->
methods._traverse.call this, axis, selector, (stack, index, waypoints) ->
stack.push waypoints[index-1] if index > 0
# .waypoint('next', string, string|HTMLNode|jQuery)
# Returns a jQuery object containing next waypoint elements. This
# creates a new entry in the jQuery object stack just like jQuery's next
# function. "axis" indicates the axis on which to traverse
# ('horizontal' | 'vertical') and "selector" indicates which context
# element to use. The defaults are 'vertical' and window respectively.
next: (axis, selector) ->
methods._traverse.call this, axis, selector, (stack, index, waypoints) ->
stack.push waypoints[index+1] if index < waypoints.length-1
# Internal: Aggregates waypoints on a given axis of a context, and applies
# a "push" callback for each element in the subject jQuery object. This
# callback builds the element array to push to the jQuery stack.
_traverse: (axis = 'vertical', selector = window, push) ->
waypoints = jQMethods.aggregate selector
stack = []
@each ->
index = $.inArray this, waypoints[axis]
push stack, index, waypoints[axis]
@pushStack stack
# Internal: Finds all waypoints on a given set of "$elements" and invokes
# "method" on each instance.
_invoke: (method) ->
this.each ->
waypoints = Waypoint.getWaypointsByElement this
$.each waypoints, (i, waypoint) ->
waypoint[method]()
true
this
# $.fn.waypoint. Let's just hook this guy up to our methods hash and
# add some trivial error reporting for bogus calls.
$.fn[wp] = (method, args...) ->
if methods[method]
methods[method].apply this, args
else if $.isFunction(method)
methods.init.apply this, arguments
else if $.isPlainObject(method)
methods.init.apply this, [null, method]
else if !method
$.error "jQuery Waypoints needs a callback function or handler option."
else
$.error "The #{method} method does not exist in jQuery Waypoints."
# The default options object for a waypoint.
# - context: string|HTMLNode|jQuery - The scrollable element that the
# waypoint acts within. The waypoint will look for the closest ancestor
# element that matches this selector or node.
# - continuous: Multiple waypoints may be triggered by a single scroll check.
# If you would like a waypoint to only trigger if it is the last waypoint
# in a scroll check, set this to false.
# - enabled: Should this waypoint start enabled (true) or disabled (false)?
# - handler: This option is not defined by default, but can be used as an
# alternate way to pass the waypoint callback function, rather than as
# the first argument to .waypoint.
# Ex: $('div').waypoint({
# handler: function(direction) { ... }
# });
# - horizontal: Set this to true if the waypoint is, well, horizontal.
# - offset: number|string|function - Determines how far from the top (or left
# if the waypoint is horizontal) of the context's viewport to trigger the
# waypoint. The default of 0 means that the waypoint is triggered when the
# top of the waypoint element hits the top of the window/context-element.
# An offset of 50 would mean the waypoint triggers when the top of the
# element is 50 pixels from the top of the window.
# A % string is translated into a percentage of the width/height of
# the context.
# If a function is passed, that function should return a number. The "this"
# keyword within this function will be set to the raw HTMLNode of the
# waypoint element.
# - triggerOnce: If true, the waypoint will destroy itself after
# first trigger.
$.fn[wp].defaults =
context: window
continuous: true
enabled: true
horizontal: false
offset: 0
triggerOnce: false
# These methods are available on the $ object by using the method name as
# the first argument to .waypoint. Ex: $.waypoints('refresh')
jQMethods =
# $.waypoints('refresh')
# Forces a refresh on all contexts, recalculating all waypoint offsets.
# This is done automatically on waypoint addition and during resize events,
# but if a user does something to change the DOM, CSS, or in some way
# change the layout of a page and its elements, they might need to call
# this method manually.
refresh: ->
$.each contexts, (i, context) -> context.refresh()
# $.waypoints('viewportHeight')
# A utility method that returns the window height, but takes into account
# inconsistencies that come with just using jQuery's .height() on iOS.
viewportHeight: ->
window.innerHeight ? $w.height()
# $.waypoints(['aggregate'], [contextSelector])
# Returns an object containing two HTMLNode arrays, one for each axis:
# {
# horizontal: [ HTMLNode... ]
# vertical: [ HTMLNode... ]
# }
# This is the default method used when calling $.waypoints(). If
# "contextSelector" is not supplied, it returns all waypoints. If
# "contextSelector" is supplied it only returns waypoints for that context.
# The array of waypoint elements is returned sorted by calculated offset,
# the order in which they would be triggered on the page.
aggregate: (contextSelector) ->
collection = allWaypoints
if contextSelector
collection = contexts[$(contextSelector)[0][contextKey]]?.waypoints
return [] unless collection
waypoints =
horizontal: []
vertical: []
$.each waypoints, (axis, arr) ->
$.each collection[axis], (key, waypoint) ->
arr.push waypoint
arr.sort (a, b) -> a.offset - b.offset
waypoints[axis] = $.map arr, (waypoint) -> waypoint.element
waypoints[axis] = $.unique waypoints[axis]
waypoints
# $.waypoints('above', [string|HTMLNode|jQuery])
# Returns all vertical waypoints that lie above the current scroll position
# of the context specified by "contextSelector". If no "contextSelector"
# is supplied, it defaults to the window.
above: (contextSelector = window) ->
jQMethods._filter contextSelector, 'vertical', (context, waypoint) ->
waypoint.offset <= context.oldScroll.y
# $.waypoints('below', [string|HTMLNode|jQuery])
# Returns all vertical waypoints that lie below the current scroll position
# of the context specified by "contextSelector". If no "contextSelector"
# is supplied, it defaults to the window.
below: (contextSelector = window) ->
jQMethods._filter contextSelector, 'vertical', (context, waypoint) ->
waypoint.offset > context.oldScroll.y
# $.waypoints('left', [string|HTMLNode|jQuery])
# Returns all horizontal waypoints left of the current scroll position
# of the context specified by "contextSelector". If no "contextSelector"
# is supplied, it defaults to the window.
left: (contextSelector = window) ->
jQMethods._filter contextSelector, 'horizontal', (context, waypoint) ->
waypoint.offset <= context.oldScroll.x
# $.waypoints('right', [string|HTMLNode|jQuery])
# Returns all horizontal waypoints right of the current scroll position
# of the context specified by "contextSelector". If no "contextSelector"
# is supplied, it defaults to the window.
right: (contextSelector = window) ->
jQMethods._filter contextSelector, 'horizontal', (context, waypoint) ->
waypoint.offset > context.oldScroll.x
# $.waypoints('enable/disable/destroy')
# These methods delegate to the enable/disable/destroy instance methods
# for all waypoints.
enable: -> jQMethods._invoke 'enable'
disable: -> jQMethods._invoke 'disable'
destroy: -> jQMethods._invoke 'destroy'
# $.waypoints('extendFn', string, function)
# Extends the $.fn.waypoint method object with a new method, "f". This
# just lets other modules piggyback on the .waypoint namespace.
extendFn: (methodName, f) ->
methods[methodName] = f
# Internal: Invokes "method" on all waypoints.
_invoke: (method) ->
waypoints = $.extend {}, allWaypoints.vertical, allWaypoints.horizontal
$.each waypoints, (key, waypoint) ->
waypoint[method]()
true
# Internal: Returns an array of all HTMLNodes for each waypoint that passes
# the "test" function. Only waypoints within the "selector" context on the
# "axis" axis are tested. As with .aggregate, the array is sorted by
# calculated offset (trigger order).
_filter: (selector, axis, test) ->
context = contexts[$(selector)[0][contextKey]]
return [] unless context
waypoints = []
$.each context.waypoints[axis], (i, waypoint) ->
waypoints.push waypoint if test context, waypoint
waypoints.sort (a, b) -> a.offset - b.offset
$.map waypoints, (waypoint) -> waypoint.element
# Hook up jQMethods to the $.waypoints namespace.
$[wps] = (method, args...) ->
if jQMethods[method]
jQMethods[method].apply null, args
else
jQMethods.aggregate.call null, method
# Plugin-wide settings:
# - resizeThrottle: For performance reasons, the refresh performed during
# resizes is throttled. This value is the rate-limit in milliseconds
# between resize refreshes. For more information on throttling, check out
# Ben Almans throttle / debounce plugin.
# http://benalman.com/projects/jquery-throttle-debounce-plugin/
# - scrollThrottle: For performance reasons, checking for any crossed
# waypoints during a scroll event is throttled. This value is the
# rate-limit in milliseconds between scroll checks. For more information
# on throttling, check out Ben Almans throttle / debounce plugin.
# http://benalman.com/projects/jquery-throttle-debounce-plugin/
$[wps].settings =
resizeThrottle: 100
scrollThrottle: 30
# Ensure a refresh on page load. Newly loaded images often shift layout.
$w.load -> $[wps] 'refresh'

View File

@@ -1,520 +0,0 @@
// Generated by CoffeeScript 1.6.2
/*
jQuery Waypoints - v2.0.3
Copyright (c) 2011-2013 Caleb Troughton
Dual licensed under the MIT license and GPL license.
https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt
*/
(function() {
var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__slice = [].slice;
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
return define('waypoints', ['jquery'], function($) {
return factory($, root);
});
} else {
return factory(root.jQuery, root);
}
})(this, function($, window) {
var $w, Context, Waypoint, allWaypoints, contextCounter, contextKey, contexts, isTouch, jQMethods, methods, resizeEvent, scrollEvent, waypointCounter, waypointKey, wp, wps;
$w = $(window);
isTouch = __indexOf.call(window, 'ontouchstart') >= 0;
allWaypoints = {
horizontal: {},
vertical: {}
};
contextCounter = 1;
contexts = {};
contextKey = 'waypoints-context-id';
resizeEvent = 'resize.waypoints';
scrollEvent = 'scroll.waypoints';
waypointCounter = 1;
waypointKey = 'waypoints-waypoint-ids';
wp = 'waypoint';
wps = 'waypoints';
Context = (function() {
function Context($element) {
var _this = this;
this.$element = $element;
this.element = $element[0];
this.didResize = false;
this.didScroll = false;
this.id = 'context' + contextCounter++;
this.oldScroll = {
x: $element.scrollLeft(),
y: $element.scrollTop()
};
this.waypoints = {
horizontal: {},
vertical: {}
};
$element.data(contextKey, this.id);
contexts[this.id] = this;
$element.bind(scrollEvent, function() {
var scrollHandler;
if (!(_this.didScroll || isTouch)) {
_this.didScroll = true;
scrollHandler = function() {
_this.doScroll();
return _this.didScroll = false;
};
return window.setTimeout(scrollHandler, $[wps].settings.scrollThrottle);
}
});
$element.bind(resizeEvent, function() {
var resizeHandler;
if (!_this.didResize) {
_this.didResize = true;
resizeHandler = function() {
$[wps]('refresh');
return _this.didResize = false;
};
return window.setTimeout(resizeHandler, $[wps].settings.resizeThrottle);
}
});
}
Context.prototype.doScroll = function() {
var axes,
_this = this;
axes = {
horizontal: {
newScroll: this.$element.scrollLeft(),
oldScroll: this.oldScroll.x,
forward: 'right',
backward: 'left'
},
vertical: {
newScroll: this.$element.scrollTop(),
oldScroll: this.oldScroll.y,
forward: 'down',
backward: 'up'
}
};
if (isTouch && (!axes.vertical.oldScroll || !axes.vertical.newScroll)) {
$[wps]('refresh');
}
$.each(axes, function(aKey, axis) {
var direction, isForward, triggered;
triggered = [];
isForward = axis.newScroll > axis.oldScroll;
direction = isForward ? axis.forward : axis.backward;
$.each(_this.waypoints[aKey], function(wKey, waypoint) {
var _ref, _ref1;
if ((axis.oldScroll < (_ref = waypoint.offset) && _ref <= axis.newScroll)) {
return triggered.push(waypoint);
} else if ((axis.newScroll < (_ref1 = waypoint.offset) && _ref1 <= axis.oldScroll)) {
return triggered.push(waypoint);
}
});
triggered.sort(function(a, b) {
return a.offset - b.offset;
});
if (!isForward) {
triggered.reverse();
}
return $.each(triggered, function(i, waypoint) {
if (waypoint.options.continuous || i === triggered.length - 1) {
return waypoint.trigger([direction]);
}
});
});
return this.oldScroll = {
x: axes.horizontal.newScroll,
y: axes.vertical.newScroll
};
};
Context.prototype.refresh = function() {
var axes, cOffset, isWin,
_this = this;
isWin = $.isWindow(this.element);
cOffset = this.$element.offset();
this.doScroll();
axes = {
horizontal: {
contextOffset: isWin ? 0 : cOffset.left,
contextScroll: isWin ? 0 : this.oldScroll.x,
contextDimension: this.$element.width(),
oldScroll: this.oldScroll.x,
forward: 'right',
backward: 'left',
offsetProp: 'left'
},
vertical: {
contextOffset: isWin ? 0 : cOffset.top,
contextScroll: isWin ? 0 : this.oldScroll.y,
contextDimension: isWin ? $[wps]('viewportHeight') : this.$element.height(),
oldScroll: this.oldScroll.y,
forward: 'down',
backward: 'up',
offsetProp: 'top'
}
};
return $.each(axes, function(aKey, axis) {
return $.each(_this.waypoints[aKey], function(i, waypoint) {
var adjustment, elementOffset, oldOffset, _ref, _ref1;
adjustment = waypoint.options.offset;
oldOffset = waypoint.offset;
elementOffset = $.isWindow(waypoint.element) ? 0 : waypoint.$element.offset()[axis.offsetProp];
if ($.isFunction(adjustment)) {
adjustment = adjustment.apply(waypoint.element);
} else if (typeof adjustment === 'string') {
adjustment = parseFloat(adjustment);
if (waypoint.options.offset.indexOf('%') > -1) {
adjustment = Math.ceil(axis.contextDimension * adjustment / 100);
}
}
waypoint.offset = elementOffset - axis.contextOffset + axis.contextScroll - adjustment;
if ((waypoint.options.onlyOnScroll && (oldOffset != null)) || !waypoint.enabled) {
return;
}
if (oldOffset !== null && (oldOffset < (_ref = axis.oldScroll) && _ref <= waypoint.offset)) {
return waypoint.trigger([axis.backward]);
} else if (oldOffset !== null && (oldOffset > (_ref1 = axis.oldScroll) && _ref1 >= waypoint.offset)) {
return waypoint.trigger([axis.forward]);
} else if (oldOffset === null && axis.oldScroll >= waypoint.offset) {
return waypoint.trigger([axis.forward]);
}
});
});
};
Context.prototype.checkEmpty = function() {
if ($.isEmptyObject(this.waypoints.horizontal) && $.isEmptyObject(this.waypoints.vertical)) {
this.$element.unbind([resizeEvent, scrollEvent].join(' '));
return delete contexts[this.id];
}
};
return Context;
})();
Waypoint = (function() {
function Waypoint($element, context, options) {
var idList, _ref;
options = $.extend({}, $.fn[wp].defaults, options);
if (options.offset === 'bottom-in-view') {
options.offset = function() {
var contextHeight;
contextHeight = $[wps]('viewportHeight');
if (!$.isWindow(context.element)) {
contextHeight = context.$element.height();
}
return contextHeight - $(this).outerHeight();
};
}
this.$element = $element;
this.element = $element[0];
this.axis = options.horizontal ? 'horizontal' : 'vertical';
this.callback = options.handler;
this.context = context;
this.enabled = options.enabled;
this.id = 'waypoints' + waypointCounter++;
this.offset = null;
this.options = options;
context.waypoints[this.axis][this.id] = this;
allWaypoints[this.axis][this.id] = this;
idList = (_ref = $element.data(waypointKey)) != null ? _ref : [];
idList.push(this.id);
$element.data(waypointKey, idList);
}
Waypoint.prototype.trigger = function(args) {
if (!this.enabled) {
return;
}
if (this.callback != null) {
this.callback.apply(this.element, args);
}
if (this.options.triggerOnce) {
return this.destroy();
}
};
Waypoint.prototype.disable = function() {
return this.enabled = false;
};
Waypoint.prototype.enable = function() {
this.context.refresh();
return this.enabled = true;
};
Waypoint.prototype.destroy = function() {
delete allWaypoints[this.axis][this.id];
delete this.context.waypoints[this.axis][this.id];
return this.context.checkEmpty();
};
Waypoint.getWaypointsByElement = function(element) {
var all, ids;
ids = $(element).data(waypointKey);
if (!ids) {
return [];
}
all = $.extend({}, allWaypoints.horizontal, allWaypoints.vertical);
return $.map(ids, function(id) {
return all[id];
});
};
return Waypoint;
})();
methods = {
init: function(f, options) {
var _ref;
if (options == null) {
options = {};
}
if ((_ref = options.handler) == null) {
options.handler = f;
}
this.each(function() {
var $this, context, contextElement, _ref1;
$this = $(this);
contextElement = (_ref1 = options.context) != null ? _ref1 : $.fn[wp].defaults.context;
if (!$.isWindow(contextElement)) {
contextElement = $this.closest(contextElement);
}
contextElement = $(contextElement);
context = contexts[contextElement.data(contextKey)];
if (!context) {
context = new Context(contextElement);
}
return new Waypoint($this, context, options);
});
$[wps]('refresh');
return this;
},
disable: function() {
return methods._invoke(this, 'disable');
},
enable: function() {
return methods._invoke(this, 'enable');
},
destroy: function() {
return methods._invoke(this, 'destroy');
},
prev: function(axis, selector) {
return methods._traverse.call(this, axis, selector, function(stack, index, waypoints) {
if (index > 0) {
return stack.push(waypoints[index - 1]);
}
});
},
next: function(axis, selector) {
return methods._traverse.call(this, axis, selector, function(stack, index, waypoints) {
if (index < waypoints.length - 1) {
return stack.push(waypoints[index + 1]);
}
});
},
_traverse: function(axis, selector, push) {
var stack, waypoints;
if (axis == null) {
axis = 'vertical';
}
if (selector == null) {
selector = window;
}
waypoints = jQMethods.aggregate(selector);
stack = [];
this.each(function() {
var index;
index = $.inArray(this, waypoints[axis]);
return push(stack, index, waypoints[axis]);
});
return this.pushStack(stack);
},
_invoke: function($elements, method) {
$elements.each(function() {
var waypoints;
waypoints = Waypoint.getWaypointsByElement(this);
return $.each(waypoints, function(i, waypoint) {
waypoint[method]();
return true;
});
});
return this;
}
};
$.fn[wp] = function() {
var args, method;
method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
if (methods[method]) {
return methods[method].apply(this, args);
} else if ($.isFunction(method)) {
return methods.init.apply(this, arguments);
} else if ($.isPlainObject(method)) {
return methods.init.apply(this, [null, method]);
} else if (!method) {
return $.error("jQuery Waypoints needs a callback function or handler option.");
} else {
return $.error("The " + method + " method does not exist in jQuery Waypoints.");
}
};
$.fn[wp].defaults = {
context: window,
continuous: true,
enabled: true,
horizontal: false,
offset: 0,
triggerOnce: false
};
jQMethods = {
refresh: function() {
return $.each(contexts, function(i, context) {
return context.refresh();
});
},
viewportHeight: function() {
var _ref;
return (_ref = window.innerHeight) != null ? _ref : $w.height();
},
aggregate: function(contextSelector) {
var collection, waypoints, _ref;
collection = allWaypoints;
if (contextSelector) {
collection = (_ref = contexts[$(contextSelector).data(contextKey)]) != null ? _ref.waypoints : void 0;
}
if (!collection) {
return [];
}
waypoints = {
horizontal: [],
vertical: []
};
$.each(waypoints, function(axis, arr) {
$.each(collection[axis], function(key, waypoint) {
return arr.push(waypoint);
});
arr.sort(function(a, b) {
return a.offset - b.offset;
});
waypoints[axis] = $.map(arr, function(waypoint) {
return waypoint.element;
});
return waypoints[axis] = $.unique(waypoints[axis]);
});
return waypoints;
},
above: function(contextSelector) {
if (contextSelector == null) {
contextSelector = window;
}
return jQMethods._filter(contextSelector, 'vertical', function(context, waypoint) {
return waypoint.offset <= context.oldScroll.y;
});
},
below: function(contextSelector) {
if (contextSelector == null) {
contextSelector = window;
}
return jQMethods._filter(contextSelector, 'vertical', function(context, waypoint) {
return waypoint.offset > context.oldScroll.y;
});
},
left: function(contextSelector) {
if (contextSelector == null) {
contextSelector = window;
}
return jQMethods._filter(contextSelector, 'horizontal', function(context, waypoint) {
return waypoint.offset <= context.oldScroll.x;
});
},
right: function(contextSelector) {
if (contextSelector == null) {
contextSelector = window;
}
return jQMethods._filter(contextSelector, 'horizontal', function(context, waypoint) {
return waypoint.offset > context.oldScroll.x;
});
},
enable: function() {
return jQMethods._invoke('enable');
},
disable: function() {
return jQMethods._invoke('disable');
},
destroy: function() {
return jQMethods._invoke('destroy');
},
extendFn: function(methodName, f) {
return methods[methodName] = f;
},
_invoke: function(method) {
var waypoints;
waypoints = $.extend({}, allWaypoints.vertical, allWaypoints.horizontal);
return $.each(waypoints, function(key, waypoint) {
waypoint[method]();
return true;
});
},
_filter: function(selector, axis, test) {
var context, waypoints;
context = contexts[$(selector).data(contextKey)];
if (!context) {
return [];
}
waypoints = [];
$.each(context.waypoints[axis], function(i, waypoint) {
if (test(context, waypoint)) {
return waypoints.push(waypoint);
}
});
waypoints.sort(function(a, b) {
return a.offset - b.offset;
});
return $.map(waypoints, function(waypoint) {
return waypoint.element;
});
}
};
$[wps] = function() {
var args, method;
method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
if (jQMethods[method]) {
return jQMethods[method].apply(null, args);
} else {
return jQMethods.aggregate.call(null, method);
}
};
$[wps].settings = {
resizeThrottle: 100,
scrollThrottle: 30
};
return $w.load(function() {
return $[wps]('refresh');
});
});
}).call(this);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
#id {}

View File

@@ -14,6 +14,6 @@ Package._transitional_registerBuildPlugin({
Package.on_test(function (api) {
api.use(['test-helpers', 'tinytest', 'less']);
api.use(['spark']);
api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.import.less'],
'client');
api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.import.less',
'less_tests_empty.less'], 'client');
});

View File

@@ -42,23 +42,25 @@ Plugin.registerSourceHandler("less", function (compileStep) {
return;
}
var cssFuture = new Future;
var sourceMap = null;
var css = ast.toCSS({
sourceMap: Boolean(true),
writeSourceMap: function (sourceMap) {
cssFuture.return(sourceMap);
sourceMap: true,
writeSourceMap: function (sm) {
sourceMap = JSON.parse(sm);
}
});
var sourceMap = JSON.parse(cssFuture.wait());
sourceMap.sources = [compileStep.inputPath];
sourceMap.sourcesContent = [source];
if (sourceMap) {
sourceMap.sources = [compileStep.inputPath];
sourceMap.sourcesContent = [source];
sourceMap = JSON.stringify(sourceMap);
}
compileStep.addStylesheet({
path: compileStep.inputPath + ".css",
data: css,
sourceMap: JSON.stringify(sourceMap)
sourceMap: sourceMap
});
});;

View File

@@ -1,5 +1,6 @@
Package.describe({
summary: "Meteor developer accounts OAuth flow"
summary: "Meteor developer accounts OAuth flow",
internal: true
});
Package.on_use(function (api) {

View File

@@ -1949,6 +1949,7 @@ Tinytest.add("minimongo - modify", function (test) {
modify({a: [1, 2]}, {$inc: {'a.3': 10}}, {a: [1, 2, null, 10]});
modify({a: {b: 2}}, {$inc: {'a.b': 10}}, {a: {b: 12}});
modify({a: {b: 2}}, {$inc: {'a.c': 10}}, {a: {b: 2, c: 10}});
exception({}, {$inc: {_id: 1}});
// $set
modify({a: 1, b: 2}, {$set: {a: 10}}, {a: 10, b: 2});
@@ -1960,6 +1961,9 @@ Tinytest.add("minimongo - modify", function (test) {
modify({a: [1], b: 2}, {$set: {'a.1': 9}}, {a: [1, 9], b: 2});
modify({a: [1], b: 2}, {$set: {'a.2': 9}}, {a: [1, null, 9], b: 2});
modify({a: {b: 1}}, {$set: {'a.c': 9}}, {a: {b: 1, c: 9}});
modify({}, {$set: {'x._id': 4}}, {x: {_id: 4}});
exception({}, {$set: {_id: 4}});
exception({_id: 4}, {$set: {_id: 4}}); // even not-changing _id is bad
// $unset
modify({}, {$unset: {a: 1}}, {});
@@ -1977,6 +1981,7 @@ Tinytest.add("minimongo - modify", function (test) {
modify({a: {b: 1}}, {$unset: {'a.b.c.d': 1}}, {a: {b: 1}});
modify({a: {b: 1}}, {$unset: {'a.x.c.d': 1}}, {a: {b: 1}});
modify({a: {b: {c: 1}}}, {$unset: {'a.b.c': 1}}, {a: {b: {}}});
exception({}, {$unset: {_id: 1}});
// $push
modify({}, {$push: {a: 1}}, {a: [1]});

View File

@@ -47,6 +47,9 @@ LocalCollection._modify = function (doc, mod, options) {
throw MinimongoError(
"Invalid mod field name, may not end in a period");
if (keypath === '_id')
throw MinimongoError("Mod on _id not allowed");
var keyparts = keypath.split('.');
var noCreate = _.has(NO_CREATE_MODIFIERS, op);
var forbidArray = (op === "$rename");
@@ -194,9 +197,6 @@ var MODIFIERS = {
e.setPropertyError = true;
throw e;
}
if (field === '_id' && !EJSON.equals(arg, target._id))
throw MinimongoError("Cannot change the _id of a document");
target[field] = EJSON.clone(arg);
},
$setOnInsert: function (target, field, arg) {

View File

@@ -48,11 +48,19 @@ Sorter = function (spec) {
// min/max.)
//
// XXX This is actually wrong! In fact, the whole attempt to compile sort
// functions independently of selectors is wrong. In MongoDB, if you have
// documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]},
// then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5).
// But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not match
// the selector, and 5 comes before 10).
// functions independently of selectors is wrong. In MongoDB, if you have
// documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]}, then
// C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). But
// C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not match
// the selector, and 5 comes before 10).
//
// The way this works is pretty subtle! For example, if the documents are
// instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and
// {_id: 'y', a: [{x: 5}, {x: 15}]}),
// then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and
// C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}})
// both follow this rule (y before x). ie, you do have to apply this
// through $elemMatch.
var reduceValue = function (branchValues, findMin) {
// Expand any leaf arrays that we find, and ignore those arrays themselves.
branchValues = expandArraysInBranches(branchValues, true);

View File

@@ -754,11 +754,15 @@ MongoConnection.prototype._createSynchronousCursor = function(
// ... and to keep querying the server indefinitely rather than just 5 times
// if there's no more data.
mongoOptions.numberOfRetries = -1;
// And if this cursor specifies a 'ts', then set the undocumented oplog
// replay flag, which does a special scan to find the first document
// (instead of creating an index on ts).
if (cursorDescription.selector.ts)
// And if this is on the oplog collection and the cursor specifies a 'ts',
// then set the undocumented oplog replay flag, which does a special scan to
// find the first document (instead of creating an index on ts). This is a
// very hard-coded Mongo flag which only works on the oplog collection and
// only works with the ts field.
if (cursorDescription.collectionName === OPLOG_COLLECTION &&
cursorDescription.selector.ts) {
mongoOptions.oplogReplay = true;
}
}
var dbCursor = collection.find(

View File

@@ -1,6 +1,6 @@
var Future = Npm.require('fibers/future');
var OPLOG_COLLECTION = 'oplog.rs';
OPLOG_COLLECTION = 'oplog.rs';
var REPLSET_COLLECTION = 'system.replset';
// Like Perl's quotemeta: quotes all regexp metacharacters. See

View File

@@ -1,9 +1,15 @@
var OplogCollection = new Meteor.Collection("oplog-" + Random.id());
Tinytest.add("mongo-livedata - oplog - cursorSupported", function (test) {
var oplogEnabled =
!!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle;
var supported = function (expected, selector, options) {
var cursor = OplogCollection.find(selector, options);
var handle = cursor.observeChanges({added: function () {}});
// If there's no oplog at all, we shouldn't ever use it.
if (!oplogEnabled)
expected = false;
test.equal(!!handle._multiplexer._observeDriver._usesOplog, expected);
handle.stop();
};

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Automatically preserve all form fields with a unique id"
summary: "Automatically preserve form fields with a unique id"
});
Package.on_use(function (api) {

View File

@@ -255,11 +255,9 @@ _.extend(TestCaseResults.prototype, {
else if (typeof s === "object")
pass = v in s;
else if (typeof s === "string")
for (var i = 0; i < s.length; i++)
if (s.charAt(i) === v) {
pass = true;
break;
}
if (s.indexOf(v) > -1) {
pass = true;
}
else
/* fail -- not something that contains other things */;
if (pass)

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Collection of small helper functions: _.map, _.each, ..."
summary: "Collection of small helpers: _.map, _.each, ..."
});
Package.on_use(function (api) {

View File

@@ -247,19 +247,13 @@ var runWebAppServer = function () {
// webserver
var app = connect();
// Parse the query string into res.query. Used by oauth_server, but it's
// generally pretty handy..
app.use(connect.query());
// Auto-compress any json, javascript, or text.
app.use(connect.compress());
// Packages and apps can add handlers to this via
// WebApp.connectHandlers. They are inserted before our default
// handler. If a path prefix is in use, they see the actual
// requested URL before the path prefix has been stripped off.
var packageAndAppHandlers = connect();
app.use(packageAndAppHandlers);
// Packages and apps can add handlers that run before any other Meteor
// handlers via WebApp.rawConnectHandlers.
var rawConnectHandlers = connect();
app.use(rawConnectHandlers);
// Strip off the path prefix, if it exists.
app.use(function (request, response, next) {
@@ -284,6 +278,10 @@ var runWebAppServer = function () {
}
});
// Parse the query string into res.query. Used by oauth_server, but it's
// generally pretty handy..
app.use(connect.query());
var getItemPathname = function (itemUrl) {
return decodeURIComponent(url.parse(itemUrl).pathname);
};
@@ -415,6 +413,11 @@ var runWebAppServer = function () {
.pipe(res);
});
// Packages and apps can add handlers to this via WebApp.connectHandlers.
// They are inserted before our default handler.
var packageAndAppHandlers = connect();
app.use(packageAndAppHandlers);
var suppressConnectErrors = false;
// connect knows it is an error handler because it has 4 arguments instead of
// 3. go figure. (It is not smart enough to find such a thing if it's hidden
@@ -520,6 +523,7 @@ var runWebAppServer = function () {
// start up app
_.extend(WebApp, {
connectHandlers: packageAndAppHandlers,
rawConnectHandlers: rawConnectHandlers,
httpServer: httpServer,
// metadata about the client program that we serve
clientProgram: {

View File

@@ -1,4 +1,7 @@
=> Meteor 0.7.0.1: Fix failure to initialize local MongoDB server.
=> Meteor 0.7.1.1: Extend oplog tailing driver to support most common
MongoDB queries. Introduce Meteor developer accounts, a new way of
managing your meteor.com deployed sites. When you use `meteor
deploy`, you will be prompted to create a developer account.
This release is being downloaded in the background. Update your
project to Meteor 0.7.0.1 by running 'meteor update'.
project to Meteor 0.7.1.1 by running 'meteor update'.

View File

@@ -78,6 +78,16 @@
{
"release": "0.7.0.1"
},
{
"release": "0.7.1"
},
{
"release": "0.7.1.1",
"packageNotices": {
"jquery": ["jquery has been upgraded to 1.11.0. See ",
"http://jquery.com/upgrade-guide/1.9/"]
}
},
{
"release": "NEXT"
}

View File

@@ -50,25 +50,41 @@ var withAccountsConnection = function (f) {
};
};
// Open a DDP connection to the accounts server, log in using the
// provided token, and ensure that the connection stays logged in across
// reconnects.
// Open a DDP connection to the accounts server and log in using the
// provided token. Returns the connection, or null if login fails.
//
// XXX if we reconnect we won't reauthenticate. Fix that before using
// this for long-lived connections.
var loggedInAccountsConnection = function (token) {
var connection = getLoadedPackages().livedata.DDP.connect(
config.getAuthDDPUrl()
);
var onReconnect = function () {
connection.apply(
'login',
[{ resume: token }],
{ wait: true },
function (err, result) {
if (err)
throw err;
}
);
};
onReconnect();
var fut = new Future;
connection.apply(
'login',
[{ resume: token }],
{ wait: true },
function (err, result) {
fut['return']({ err: err, result: result });
}
);
var outcome = fut.wait();
if (outcome.err) {
connection.close();
if (outcome.err.error === 403) {
// This is not an ideal value for the error code, but it means
// "server rejected our access token". For example, it expired
// or we revoked it from the web.
return null;
}
// Something else went wrong
throw outcome.err;
}
return connection;
};
@@ -189,26 +205,33 @@ var writeMeteorAccountsUsername = function (username) {
// Given an object 'data' in the format returned by readSessionData,
// modify it to make the user logged out.
var logOutAllSessions = function (data) {
_.each(data.sessions, function (session, domain) {
logOutSession(session);
});
};
// As logOutAllSessions, but for a session on a particular domain
// rather than all sessions.
var logOutSession = function (session) {
var crypto = require('crypto');
_.each(data.sessions, function (session, domain) {
delete session.username;
delete session.userId;
delete session.username;
delete session.userId;
delete session.registrationUrl;
if (_.has(session, 'token')) {
if (! (session.pendingRevoke instanceof Array))
session.pendingRevoke = [];
if (_.has(session, 'token')) {
if (! (session.pendingRevoke instanceof Array))
session.pendingRevoke = [];
// Delete the auth token itself, but save the tokenId, which
// is useless for authentication. The next time we're online,
// we'll send the tokenId to the server to revoke the token on
// the server side too.
if (typeof session.tokenId === "string")
session.pendingRevoke.push(session.tokenId);
delete session.token;
delete session.tokenId;
}
});
// Delete the auth token itself, but save the tokenId, which is
// useless for authentication. The next time we're online, we'll
// send the tokenId to the server to revoke the token on the
// server side too.
if (typeof session.tokenId === "string")
session.pendingRevoke.push(session.tokenId);
delete session.token;
delete session.tokenId;
}
};
// Given an object 'data' in the format returned by readSessionData,
@@ -222,37 +245,7 @@ var loggedIn = function (data) {
// the logged in user doesn't have a username.
var currentUsername = function (data) {
var sessionData = getSession(data, config.getAccountsDomain());
if (sessionData.username) {
return sessionData.username;
} else if (loggedIn(data) && sessionData.token) {
// If it looks like we are logged in but we don't yet have a
// username, then ask the server if we have one.
var username = null;
var fut = new Future();
var connection = loggedInAccountsConnection(sessionData.token);
connection.call('getUsername', function (err, result) {
if (err) {
// If anything went wrong, return null just as we would have if
// we hadn't bothered to ask the server.
fut['return'](null);
return;
}
fut['return'](result);
});
setTimeout(inFiber(function () {
fut['return'](null);
}), 5000);
username = fut.wait();
connection.close();
if (username) {
writeMeteorAccountsUsername(username);
}
return username;
} else {
return null;
}
return sessionData.username || null;
};
var removePendingRevoke = function (domain, tokenIds) {
@@ -584,13 +577,18 @@ var doInteractivePasswordLogin = function (options) {
return true;
};
// options are the same as for doInteractivePasswordLogin, exept without
// options are the same as for doInteractivePasswordLogin, except without
// username and email.
exports.doUsernamePasswordLogin = function (options) {
var username = utils.readLine({
prompt: "Username: ",
stream: process.stderr
});
var username;
do {
username = utils.readLine({
prompt: "Username: ",
stream: process.stderr
}).trim();
} while (username.length === 0);
return doInteractivePasswordLogin(_.extend({}, options, {
username: username
}));
@@ -685,9 +683,71 @@ exports.logoutCommand = function (options) {
process.stderr.write("Not logged in.\n");
};
exports.currentUsername = function () {
// If this is fully set up account (with a username and password), or
// if not logged in, do nothing. If it is an account without a
// username, contact the server and see if the user finished setting
// it up, and if so grab and save the username. But contact the server
// only once per run of the program. Options:
// - noLogout: boolean. Set to true if you don't want this function to
// log out the session if wehave an invalid credential (for example,
// if a caller wants to do its own error handling for invalid
// credentials). Defaults to false.
var alreadyPolledForRegistration = false;
exports.pollForRegistrationCompletion = function (options) {
if (alreadyPolledForRegistration)
return;
alreadyPolledForRegistration = true;
options = options || {};
var data = readSessionData();
return currentUsername(data);
var session = getSession(data, config.getAccountsDomain());
if (session.username || ! session.token)
return;
// We are logged in but we don't yet have a username. Ask the server
// if a username was chosen since we last checked.
var username = null;
var fut = new Future();
var connection = loggedInAccountsConnection(session.token);
if (! connection) {
// Server says our credential isn't any good anymore! Get rid of
// it. Note that, out of an abundance of caution, this also will
// enqueue the credential for invalidation (on a future run, we
// will try to explicitly revoke the credential ourselves).
if (! options.noLogout) {
logOutSession(session);
writeSessionData(data);
}
return;
}
connection.call('getUsername', function (err, result) {
if (fut.isResolved())
return;
if (err) {
// If anything went wrong, return null just as we would have if
// we hadn't bothered to ask the server.
fut['return'](null);
return;
}
fut['return'](result);
});
var timer = setTimeout(inFiber(function () {
if (! fut.isResolved()) {
fut['return'](null);
}
}), 5000);
username = fut.wait();
connection.close();
clearTimeout(timer);
if (username) {
writeMeteorAccountsUsername(username);
}
};
exports.registrationUrl = function () {
@@ -698,6 +758,7 @@ exports.registrationUrl = function () {
exports.whoAmICommand = function (options) {
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var data = readSessionData();
if (! loggedIn(data)) {
@@ -775,11 +836,23 @@ exports.registerOrLogIn = withAccountsConnection(function (connection) {
} else if (result.alreadyExisted && result.sentRegistrationEmail) {
process.stderr.write(
"\n" +
"That email address is already in use. We need to confirm that it belongs\n" +
"to you. Luckily this will only take a moment.\n" +
"\n" +
"Check your mail! We've sent you a link. Click it, pick a password,\n" +
"and then come back here to deploy your app.\n");
"You need to pick a password for your account so that you can log in.\n" +
"An email has been sent to you with the link.\n\n");
var animationFrame = 0;
var lastLinePrinted = "";
var timer = setInterval(function () {
var spinner = ['-', '\\', '|', '/'];
lastLinePrinted = "Waiting for you to register on the web... " +
spinner[animationFrame];
process.stderr.write(lastLinePrinted + "\r");
animationFrame = (animationFrame + 1) % spinner.length;
}, 200);
var stopSpinner = function () {
process.stderr.write(new Array(lastLinePrinted.length + 1).join(' ') +
"\r");
clearInterval(timer);
};
try {
var waitForRegistrationResult = connection.call(
@@ -787,17 +860,17 @@ exports.registerOrLogIn = withAccountsConnection(function (connection) {
email
);
} catch (e) {
stopSpinner();
if (! (e instanceof getLoadedPackages().meteor.Meteor.Error))
throw e;
process.stderr.write(
"\nWhen you've picked your password, run 'meteor login' and then you'll\n" +
"be good to go.\n");
"When you've picked your password, run 'meteor login' to log in.\n")
return false;
}
process.stderr.write("\nGreat! Nice to meet you, " +
waitForRegistrationResult.username +
"! Now log in with your new password.\n");
stopSpinner();
process.stderr.write("Username: " +
waitForRegistrationResult.username + "\n");
loginResult = doInteractivePasswordLogin({
username: waitForRegistrationResult.username,
retry: true,
@@ -821,6 +894,34 @@ exports.registerOrLogIn = withAccountsConnection(function (connection) {
}
});
// options: firstTime, leadingNewline
exports.maybePrintRegistrationLink = function (options) {
options = options || {};
auth.pollForRegistrationCompletion();
var data = readSessionData();
var session = getSession(data, config.getAccountsDomain());
if (session.userId && ! session.username && session.registrationUrl) {
if (options.leadingNewline)
process.stderr.write("\n");
if (! options.firstTime) {
// If they've already been prompted to set a password then this
// is more of a friendly reminder, so we word it slightly
// differently than the first time they're being shown a
// registration url.
process.stderr.write(
"You should set a password on your Meteor developer account. It takes\n" +
"about a minute at: " + session.registrationUrl + "\n\n");
} else {
process.stderr.write(
"You can set a password on your account or change your email address at:\n" +
session.registrationUrl + "\n\n");
}
}
};
exports.tryRevokeOldTokens = tryRevokeOldTokens;
exports.getSessionId = function (domain) {

View File

@@ -840,8 +840,16 @@ _.extend(ClientTarget.prototype, {
_.each(originals, function (file, name) {
if (! file.sourceMap)
return;
newMap.applySourceMap(
new sourcemap.SourceMapConsumer(file.sourceMap), name);
try {
newMap.applySourceMap(
new sourcemap.SourceMapConsumer(file.sourceMap), name);
} catch (err) {
// If we can't apply the source map, silently drop it.
//
// XXX This is here because there are some less files that
// produce source maps that throw when consumed. We should
// figure out exactly why and fix it, but this will do for now.
}
});
self.css[0].setSourceMap(JSON.stringify(newMap));

View File

@@ -6,6 +6,7 @@ var files = require('./files.js');
var deploy = require('./deploy.js');
var library = require('./library.js');
var buildmessage = require('./buildmessage.js');
var unipackage = require('./unipackage.js');
var project = require('./project.js');
var warehouse = require('./warehouse.js');
var auth = require('./auth.js');
@@ -677,6 +678,7 @@ main.registerCommand({
}
}, function (options) {
var mongoUrl;
var usedMeteorAccount = false;
if (options.args.length === 0) {
// localhost mode
@@ -707,6 +709,7 @@ main.registerCommand({
mongoUrl = deployGalaxy.temporaryMongoUrl(site);
} else {
mongoUrl = deploy.temporaryMongoUrl(site);
usedMeteorAccount = true;
}
if (! mongoUrl)
@@ -716,6 +719,8 @@ main.registerCommand({
if (options.url) {
console.log(mongoUrl);
} else {
if (usedMeteorAccount)
auth.maybePrintRegistrationLink();
process.stdin.pause();
var runMongo = require('./run-mongo.js');
runMongo.runMongoShell(mongoUrl);
@@ -772,7 +777,7 @@ main.registerCommand({
main.registerCommand({
name: 'deploy',
minArgs: 0,
minArgs: 1,
maxArgs: 1,
options: {
'delete': { type: Boolean, short: 'D' },
@@ -861,26 +866,16 @@ main.registerCommand({
});
}
var registrationUrl = auth.registrationUrl();
if (registrationUrl &&
deployResult === 0 &&
! auth.currentUsername()) {
process.stderr.write("\n");
if (loggedIn) {
if (deployResult === 0) {
auth.maybePrintRegistrationLink({
leadingNewline: true,
// If the user was already logged in at the beginning of the
// deploy, then they've already been prompted to set a password
// and this is more of a friendly reminder to set their password,
// so we word it slightly differently than the first time they're
// being shown a registration url.
process.stderr.write(
"You should set a password on your Meteor developer account. It takes\n" +
"about a minute at: " + registrationUrl + "\n\n");
} else {
process.stderr.write(
"You can set a password on your account or change your email address at:\n" +
registrationUrl + "\n\n");
}
// at least once before, so we use a slightly different message.
firstTime: ! loggedIn
});
}
return deployResult;
});
@@ -941,6 +936,7 @@ main.registerCommand({
}
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var site = qualifySitename(options.args[0]);
if (hostedWithGalaxy(site)) {
@@ -975,15 +971,14 @@ main.registerCommand({
maxArgs: 1
}, function (options) {
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var site = qualifySitename(options.args[0]);
if (! auth.isLoggedIn()) {
// XXX meteor.com/create-account or something should have a nice
// registration form
process.stderr.write(
"\nYou must be logged in to claim sites. Use 'meteor login' to log in.\n" +
"If you don't have a Meteor developer account yet, you can quickly\n" +
"create one at www.meteor.com.\n\n");
"You must be logged in to claim sites. Use 'meteor login' to log in.\n" +
"If you don't have a Meteor developer account yet, create one by clicking\n" +
"'Sign in' and then 'Create account' at www.meteor.com.\n\n");
return 1;
}
@@ -1133,6 +1128,66 @@ main.registerCommand({
});
///////////////////////////////////////////////////////////////////////////////
// run-command
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'run-command',
hidden: true,
minArgs: 1,
maxArgs: Infinity
}, function (options) {
var library = release.current.library;
if (! fs.existsSync(options.args[0]) ||
! fs.statSync(options.args[0]).isDirectory()) {
process.stderr.write(options.args[0] + ": not a directory\n");
return 1;
}
// Build and load the package
var world, packageName;
var messages = buildmessage.capture(
{ title: "building the program" }, function () {
// Make the directory visible as a package. Derive the last
// package name from the last component of the directory, and
// bail out if that creates a conflict.
var packageDir = path.resolve(options.args[0]);
packageName = path.basename(packageDir) + "-tool";
if (library.get(packageName, false)) {
buildmessage.error("'" + packageName +
"' conflicts with the name " +
"of a package in the library");
}
library.override(packageName, packageDir);
world = unipackage.load({
library: library,
packages: [ packageName ],
release: release.current.name
});
});
if (messages.hasMessages()) {
process.stderr.write(messages.formatMessages());
return 1;
}
if (typeof world[packageName].main !== "function") {
process.stderr.write("Package does not define a main() function.\n");
return 1;
}
var ret = world[packageName].main(options.args.slice(1));
// let exceptions propagate and get printed by node
if (ret === undefined)
ret = 0;
if (typeof ret !== "number")
ret = 1;
ret = +ret; // cast to integer
return ret;
});
///////////////////////////////////////////////////////////////////////////////
// login
///////////////////////////////////////////////////////////////////////////////
@@ -1141,10 +1196,14 @@ main.registerCommand({
name: 'login',
options: {
email: { type: String },
// Undocumented: get credentials on a specific Galaxy. Do we still
// need this?
galaxy: { type: String }
}
}, function (options) {
return auth.loginCommand(options);
return auth.loginCommand(_.extend({
overwriteExistingToken: true
}, options));
});
@@ -1191,12 +1250,13 @@ main.registerCommand({
main.registerCommand({
name: 'self-test',
minArgs: 0,
maxArgs: 1,
options: {
changed: { type: Boolean },
'force-online': { type: Boolean },
slow: { type: Boolean },
history: { type: Number },
tests: { type: String }
},
hidden: true
}, function (options) {
@@ -1214,13 +1274,13 @@ main.registerCommand({
}
var testRegexp = undefined;
if (options.tests) {
if (options.args.length) {
try {
testRegexp = new RegExp(options.tests);
testRegexp = new RegExp(options.args[0]);
} catch (e) {
if (!(e instanceof SyntaxError))
throw e;
process.stderr.write("Bad regular expression: " + options.tests + "\n");
process.stderr.write("Bad regular expression: " + options.args[0] + "\n");
return 1;
}
}

View File

@@ -11,6 +11,7 @@ var auth = require('./auth.js');
var release = require('./release.js');
var url = require('url');
var _ = require('underscore');
var buildmessage = require('./buildmessage.js');
// a bit of a hack
var getPackage = _.once(function () {
@@ -32,14 +33,14 @@ var handleError = function (error, galaxyName, messages) {
var Package = getPackage();
messages = messages || {};
if (e instanceof Package.meteor.Meteor.Error) {
if (error instanceof Package.meteor.Meteor.Error) {
var msg = messages[error.error];
if (msg)
process.stderr.write(msg + "\n");
else if (error.message)
process.stderr.write("Denied: " + error.message + "\n");
return 1;
} else if (e instanceof ConnectionTimeoutError) {
} else if (error instanceof ConnectionTimeoutError) {
// If we have an http/https URL for a galaxyName instead of a
// proper galaxyName (which is what the code in this file
// currently passes), strip off the scheme and trailing slash.
@@ -150,7 +151,7 @@ _.extend(ServiceConnection.prototype, {
}
});
self.subscribe.apply(self, args);
self.connection.subscribe.apply(self.connection, args);
return fut.wait();
},
@@ -414,7 +415,7 @@ exports.deploy = function (options) {
return 0;
} finally {
// Close the connection to Galaxy (otherwise Node will continue running).
conn.close();
conn && conn.close();
}
};

View File

@@ -147,10 +147,13 @@ var authedRpc = function (options) {
expectPayload: []
});
if (infoResult.statusCode === 403 && rpcOptions.promptIfAuthFails) {
if (infoResult.statusCode === 401 && rpcOptions.promptIfAuthFails) {
// Our authentication didn't validate, so prompt the user to log in
// again, and resend the RPC if the login succeeds.
var username = utils.readLine({ prompt: "Username: " });
var username = utils.readLine({
prompt: "Username: ",
stream: process.stderr
});
var loginOptions = {
username: username,
suppressErrorMessage: true
@@ -262,9 +265,9 @@ var printLegacyPasswordMessage = function (site) {
// authorized for, instruct them to get added via 'meteor authorized
// --add' or switch accounts.
var printUnauthorizedMessage = function () {
var username = auth.currentUsername();
var username = auth.loggedInUsername();
process.stderr.write(
"\nSorry, that site belongs to a different user.\n" +
"Sorry, that site belongs to a different user.\n" +
(username ? "You are currently logged in as " + username + ".\n" : "") +
"\nEither have the site owner use 'meteor authorized --add' to add you\n" +
"as an authorized developer for the site, or switch to an authorized\n" +
@@ -313,12 +316,27 @@ var bundleAndDeploy = function (options) {
if (! site)
return 1;
// We should give a username/password prompt if the user was logged in
// but the credentials are expired, unless the user is logged in but
// doesn't have a username (in which case they should hit the email
// prompt -- a user without a username shouldn't be given a username
// prompt). There's an edge case where things happen in the following
// order: user creates account, user sets username, credential expires
// or is revoked, user comes back to deploy again. In that case,
// they'll get an email prompt instead of a username prompt because
// the command-line tool didn't have time to learn about their
// username before the credential was expired.
auth.pollForRegistrationCompletion({
noLogout: true
});
var promptIfAuthFails = (auth.loggedInUsername() !== null);
// Check auth up front, rather than after the (potentially lengthy)
// bundling process.
var preflight = authedRpc({
site: site,
preflight: true,
promptIfAuthFails: true
promptIfAuthFails: promptIfAuthFails
});
if (preflight.errorMessage) {
@@ -341,7 +359,7 @@ var bundleAndDeploy = function (options) {
var buildDir = path.join(options.appDir, '.meteor', 'local', 'build_tar');
var bundlePath = path.join(buildDir, 'bundle');
process.stdout.write('Deploying to ' + site + '. Bundling...\n');
process.stdout.write('Deploying to ' + site + '. Bundling...\n');
var settings = null;
var messages = buildmessage.capture({
@@ -487,6 +505,7 @@ var checkAuthThenSendRpc = function (site, operation, what) {
return null;
}
} else { // User is logged in but not authorized for this app
process.stderr.write("\n");
printUnauthorizedMessage();
return null;
}
@@ -539,6 +558,7 @@ var logs = function (site) {
return 1;
} else {
process.stdout.write(result.message);
auth.maybePrintRegistrationLink({ leadingNewline: true });
return 0;
}
};
@@ -659,11 +679,12 @@ var claim = function (site) {
});
if (result.errorMessage) {
if (! auth.currentUsername() &&
auth.pollForRegistrationCompletion();
if (! auth.loggedInUsername() &&
auth.registrationUrl()) {
process.stderr.write(
"\nBefore you can claim existing sites, you need to set a password on\n" +
"your Meteor developer account. You can do that here in under a minute:\n\n" +
"You need to set a password on your Meteor developer account before\n" +
"you can claim sites. You can do that here in under a minute:\n\n" +
auth.registrationUrl() + "\n\n");
} else {
process.stderr.write("Couldn't claim site: " +

View File

@@ -478,9 +478,11 @@ files.run = function (command /*, arguments */) {
var child_process = require("child_process");
child_process.execFile(
command, args, {}, function (error, stdout, stderr) {
if (! (error === null || error.code === 0))
if (! (error === null || error.code === 0)) {
future.return(null);
future.return(stdout);
} else {
future.return(stdout);
}
});
return future.wait();
};

View File

@@ -144,8 +144,7 @@ Options:
Reset the project state. Erases the local database.
Usage: meteor reset
Reset the current project to a fresh state. Removes all local
data.
Reset the current project to a fresh state. Removes all local data.
>>> deploy
@@ -218,6 +217,32 @@ site into your account. If you had set a password on the site you will
be prompted for it one last time.
>>> login
Log in to your Meteor developer account
Usage: meteor login [--email]
Prompts for your username and password and logs you in to your Meteor
developer account. Pass --email to log in by email address rather than
by username.
>>> logout
Log out of your Meteor developer account
Usage: meteor logout
Log out of your Meteor developer account.
>>> whoami
Prints the username of your Meteor developer account
Usage: meteor whoami
Prints the username of the currently logged-in Meteor developer.
See 'meteor login' to log into or 'meteor logout' to log out of your
Meteor developer account.
>>> test-packages
Test one or more packages
Usage: meteor test-packages [--release <release>] [options] [package...]
@@ -264,6 +289,25 @@ You should never need to use this command. It is intended for use while
debugging the Meteor packaging tools themselves.
>>> run-command
Build and run a command-line tool
Usage: meteor run-command <package directory> [arguments..]
Builds the provided directory as a package, then loads the package and
calls the main() function inside the package. The function will receive
any remaining arguments. The exit status will be the return value of
main() (which is called inside a fiber).
The arguments will be parsed by Meteor's option parser, which means that
--release will be effective (but not passed to the command), and that it will be
an error to pass any unknown options. If you want to pass options to your tool,
place them after a '--' argument (which turns off option parsing for the rest of
the arguments).
This command is for temporary, internal use, until we have a more mature
system for building standalone command-line programs with Meteor.
>>> login
Log in to your Meteor account
Usage: meteor login [--email] [--galaxy <galaxy.example.com>]
@@ -272,30 +316,31 @@ Prompts for your username and password and logs you in to your
Meteor account. Pass --email to log in by email address instead of
username. Pass --galaxy to specify a galaxy to log in to.
Builds the provided directory as a package, then loads the package and
calls the main() function inside the package. The function will receive
any remaining arguments. The exit status will be the return value of
main() (which is called inside a fiber).
>>> logout
Log out of your Meteor account
Usage: meteor logout
The arguments will be parsed by Meteor's option parser, which means that
--release will be effective (but not passed to the command), and that it will be
an error to pass any unknown options. If you want to pass options to your tool,
place them after a '--' argument (which turns off option parsing for the rest of
the arguments).
Log out of your Meteor account.
>>> whoami
Prints the username of your Meteor developer account
Usage: meteor whoami
Prints the username of the currently logged-in Meteor developer.
See 'meteor login' to log into or 'meteor logout' to log out of of your
Meteor account.
This command is for temporary, internal use, until we have a more mature
system for building standalone command-line programs with Meteor.
>>> self-test
Run tests of the 'meteor' tool.
Usage: meteor self-test [--changed] [--slow] [--force-online] [--history n]
Usage: meteor self-test [pattern] [--changed] [--slow]
[--force-online] [--history n]
Runs internal tests. Exits with status 0 on success.
If 'pattern' is provided, it should be a regular expression. Only
tests that match the regular expression will be run.
Pass --changed to run only tests that have changed since they last
passed. This uses a really rough heuristic: A test has changed iff
there has been any change to the file in the 'selftests' subdirectory
@@ -342,4 +387,3 @@ Grant a permission on an official service
Usage: meteor admin grant [XXX]
Not yet implemented

View File

@@ -573,7 +573,19 @@ Fiber(function () {
// appRelease will be null if a super old project with no
// .meteor/release or 'none' if created by a checkout
appRelease = project.getMeteorReleaseVersion(appDir);
// This is what happens if the file exists and is empty. This really
// shouldn't happen unless the user did it manually.
if (appRelease === '') {
process.stderr.write(
"Problem! This project has a .meteor/release file which is empty.\n" +
"The file should either contain the release of Meteor that you want to use,\n" +
"or the word 'none' if you will only use the project with unreleased\n" +
"checkouts of Meteor. Please edit the .meteor/release file in the project\n" +
"and change it to a valid Meteor release or 'none'.\n");
process.exit(1);
}
}
if (! files.usesWarehouse()) {
// Running from a checkout
if (releaseOverride) {
@@ -909,7 +921,7 @@ commandName + ": You're not in a Meteor project directory.\n" +
}
if (command.requiresApp && release.current.isCheckout() &&
appRelease !== "none") {
appRelease && appRelease !== "none") {
// For commands that work with apps, if we have overridden the
// app's usual release by using a checkout, print a reminder banner.
process.stderr.write(

View File

@@ -336,7 +336,8 @@ _.extend(Slice.prototype, {
// - appendDocument({ section: "head", data: "my markup" })
// Browser targets only. Add markup to the "head" or "body"
// section of the document.
// - addStylesheet({ path: "my/stylesheet.css", data: "my css" })
// - addStylesheet({ path: "my/stylesheet.css", data: "my css",
// sourceMap: "stringified json sourcemap"})
// Browser targets only. Add a stylesheet to the
// document. 'path' is a requested URL for the stylesheet that
// may or may not ultimately be honored. (Meteor will add
@@ -453,7 +454,8 @@ _.extend(Slice.prototype, {
resources.push({
type: "css",
data: new Buffer(options.data, 'utf8'),
servePath: path.join(self.pkg.serveRoot, options.path)
servePath: path.join(self.pkg.serveRoot, options.path),
sourceMap: options.sourceMap
});
},
addJavaScript: function (options) {

View File

@@ -57,7 +57,8 @@ var meteorReleaseFilePath = function (appDir) {
// This will return "none" if the project is not pinned to a release
// (it was created by a checkout), or null for a legacy app with no
// .meteor/release file.
// .meteor/release file. It returns the empty string if the file exists
// but is empty.
project.getMeteorReleaseVersion = function (appDir) {
var releasePath = meteorReleaseFilePath(appDir);
try {
@@ -65,6 +66,9 @@ project.getMeteorReleaseVersion = function (appDir) {
} catch (e) {
return null;
}
// This should really never happen, and the caller will print a special error.
if (!lines.length)
return '';
return trimLine(lines[0]);
};

View File

@@ -477,6 +477,21 @@ _.extend(Sandbox.prototype, {
unlink: function (filename) {
var self = this;
fs.unlinkSync(path.join(self.cwd, filename));
},
// Return the current contents of .meteorsession in the sandbox.
readSessionFile: function () {
var self = this;
return fs.readFileSync(path.join(self.root, '.meteorsession'), 'utf8');
},
// Overwrite .meteorsession in the sandbox with 'contents'. You
// could use this in conjunction with readSessionFile to save and
// restore authentication states.
writeSessionFile: function (contents) {
var self = this;
return fs.writeFileSync(path.join(self.root, '.meteorsession'),
contents, 'utf8');
}
});
@@ -839,7 +854,7 @@ _.extend(Run.prototype, {
var net = require('net');
var lastStartTime = 0;
for (var attempts = 0; ! self.fakeMongoConnection && attempts < 20;
for (var attempts = 0; ! self.fakeMongoConnection && attempts < 50;
attempts ++) {
// Throttle attempts to one every 100ms
utils.sleepMs((lastStartTime + 100) - (+ new Date));
@@ -976,7 +991,7 @@ var tagDescriptions = {
// these last two are not actually test tags; they reflect the use of
// --changed and --tests
unchanged: 'unchanged since last pass',
misnamed: "don't match --tests argument"
'non-matching': "don't match specified pattern"
};
// options: onlyChanged, offline, includeSlowTests, historyLines, testRegexp
@@ -1015,7 +1030,7 @@ var runTests = function (options) {
tests = _.filter(tests, function (test) {
return options.testRegexp.test(test.name);
});
skipCounts.misnamed = lengthBeforeTestRegexp - tests.length;
skipCounts['non-matching'] = lengthBeforeTestRegexp - tests.length;
}
if (options.onlyChanged) {

View File

@@ -1,3 +1,8 @@
var _ = require('underscore');
var release = require('./release.js');
var unipackage = require('./unipackage.js');
var config = require('./config.js');
var randomString = function (charsCount) {
var chars = 'abcdefghijklmnopqrstuvwxyz';
var str = '';
@@ -7,6 +12,8 @@ var randomString = function (charsCount) {
return str;
};
exports.accountsCommandTimeoutSecs = 15;
exports.randomString = randomString;
var randomAppName = function () {
@@ -19,6 +26,16 @@ exports.randomUserEmail = function () {
return 'selftest-user-' + randomString(15) + '@guerrillamail.com';
};
var ensureLegacyReleaseDownloaded = function (sandbox) {
// Ensure we have 0.7.0.1 downloaded. This version didn't actually support
// --get-ready for a built release, but it's an easy way to verify we're
// actually running an old version.
var run = sandbox.run('--release', '0.7.0.1', '--get-ready');
run.waitSecs(75);
run.matchErr('only works in a checkout\n');
run.expectExit(1);
};
// Creates an app and deploys it with an old release. 'password' is
// optional. Returns the name of the deployed app.
exports.createAndDeployLegacyApp = function (sandbox, password) {
@@ -26,6 +43,8 @@ exports.createAndDeployLegacyApp = function (sandbox, password) {
sandbox.createApp(name, 'empty');
sandbox.cd(name);
ensureLegacyReleaseDownloaded(sandbox);
var runArgs = ['deploy', '--release', '0.7.0.1', name];
if (password)
runArgs.push('-P');
@@ -33,8 +52,7 @@ exports.createAndDeployLegacyApp = function (sandbox, password) {
var run = sandbox.run.apply(sandbox, runArgs);
if (password) {
// Give it time to download and install a new release, if necessary.
run.waitSecs(30);
run.waitSecs(10);
run.match('New Password:');
run.write(password + '\n');
run.match('New Password (again):');
@@ -49,6 +67,8 @@ exports.createAndDeployLegacyApp = function (sandbox, password) {
};
exports.cleanUpLegacyApp = function (sandbox, name, password) {
ensureLegacyReleaseDownloaded(sandbox);
var run = sandbox.run('deploy', '--release', '0.7.0.1', '-D', name);
if (password) {
run.waitSecs(10);
@@ -63,12 +83,22 @@ exports.cleanUpLegacyApp = function (sandbox, name, password) {
};
// Creates an app and deploys it. Assumes the sandbox is already logged
// in.
exports.createAndDeployApp = function (sandbox) {
var name = randomAppName();
sandbox.createApp(name, 'empty');
// in. Returns the name of the deployed app. Options:
// - settingsFile: a path to a settings file to deploy with
// - appName: app name to use; will be generated randomly if not
// provided
// - templateApp: the name of the template app to use. defaults to 'empty'
exports.createAndDeployApp = function (sandbox, options) {
options = options || {};
var name = options.appName || randomAppName();
sandbox.createApp(name, options.templateApp || 'empty');
sandbox.cd(name);
var run = sandbox.run('deploy', name);
var runArgs = ['deploy', name];
if (options.settingsFile) {
runArgs.push('--settings');
runArgs.push(options.settingsFile);
}
var run = sandbox.run.apply(sandbox, runArgs);
run.waitSecs(90);
run.match('Now serving at ' + name + '.meteor.com');
run.waitSecs(10);
@@ -86,7 +116,7 @@ exports.cleanUpApp = function (sandbox, name) {
exports.login = function (s, username, password) {
var run = s.run('login');
run.waitSecs(2);
run.waitSecs(15);
run.matchErr('Username:');
run.write(username + '\n');
run.matchErr('Password:');
@@ -102,3 +132,67 @@ exports.logout = function (s) {
run.matchErr('Logged out');
run.expectExit(0);
};
var registrationUrlRegexp =
/https:\/\/www\.meteor\.com\/setPassword\?([a-zA-Z0-9\+\/]+)/;
exports.registrationUrlRegexp = registrationUrlRegexp;
// In the sandbox `s`, create and deploy a new app with an unregistered
// email address. Returns the registration token from the printed URL in
// the deploy message.
exports.deployWithNewEmail = function (s, email, appName) {
s.createApp('deployapp', 'empty');
s.cd('deployapp');
var run = s.run('deploy', appName);
run.waitSecs(exports.accountsCommandTimeoutSecs);
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(90);
// Check that we got a prompt to set a password on meteor.com.
run.matchErr('set a password');
var urlMatch = run.matchErr(registrationUrlRegexp);
if (! urlMatch || ! urlMatch.length || ! urlMatch[1]) {
throw new Error("Missing registration token");
}
var token = urlMatch[1];
run.expectExit(0);
return token;
};
var getLoadedPackages = _.once(function () {
return unipackage.load({
library: release.current.library,
packages: ['meteor', 'livedata'],
release: release.current.name
});
});
var ddpConnect = function (url) {
var DDP = getLoadedPackages().livedata.DDP;
return DDP.connect(url);
};
exports.ddpConnect = ddpConnect;
// Given a registration token created by doing a deferred registration
// with `email`, makes a DDP connection to the accounts server and
// finishes the registration process.
exports.registerWithToken = function (token, username, password, email) {
// XXX It might make more sense to hard-code the DDP url to
// https://www.meteor.com, since that's who the sandboxes are talking
// to.
var accountsConn = ddpConnect(config.getAuthDDPUrl());
var registrationTokenInfo = accountsConn.call('registrationTokenInfo',
token);
var registrationCode = registrationTokenInfo.code;
accountsConn.call('register', {
username: username,
password: password,
emails: [email],
token: token,
code: registrationCode
});
accountsConn.close();
};

View File

@@ -0,0 +1 @@
local

View File

@@ -0,0 +1,4 @@
# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

View File

@@ -0,0 +1 @@
none

View File

@@ -0,0 +1,5 @@
// For testing meteor run-command.
main = function (argv) {
console.log("argv", argv);
return 17;
};

View File

@@ -0,0 +1,4 @@
Package.on_use(function (api) {
api.add_files('foo.js');
api.export('main', 'server');
});

View File

@@ -2,7 +2,7 @@ var selftest = require('../selftest.js');
var testUtils = require('../test-utils.js');
var Sandbox = selftest.Sandbox;
var commandTimeoutSecs = 15;
var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs;
var loggedInError = function(run) {
run.waitSecs(commandTimeoutSecs);
@@ -24,6 +24,9 @@ selftest.define("authorized", ['net', 'slow'], function () {
run.matchErr("You must be logged in for that.");
run.expectExit(1);
run = s.run("authorized");
run.matchErr("not enough arguments");
run = s.run("authorized", appName, "--remove", "bob");
run.waitSecs(commandTimeoutSecs);
run.matchErr("You must be logged in for that.");
@@ -116,3 +119,26 @@ selftest.define("authorized", ['net', 'slow'], function () {
testUtils.cleanUpApp(s, appName);
});
selftest.define('authorized - no username', ['net', 'slow'], function () {
var s = new Sandbox;
// We shouldn't be able to add authorized users before we set a
// username.
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
var run = s.run('authorized', appName, '--add', 'test');
run.waitSecs(commandTimeoutSecs);
run.matchErr('You must set a password on your account before ' +
'you can authorize other users');
run.expectExit(1);
// After we set a username, we should be able to authorize others.
testUtils.registerWithToken(token, username, 'testtest', email);
run = s.run('authorized', appName, '--add', 'test');
run.waitSecs(commandTimeoutSecs);
run.match(': added test');
run.expectExit(0);
testUtils.cleanUpApp(s, appName);
});

View File

@@ -1,8 +1,9 @@
var selftest = require('../selftest.js');
var testUtils = require('../test-utils.js');
var Sandbox = selftest.Sandbox;
var files = require('../files.js');
var commandTimeoutSecs = 15;
var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs;
var loggedInError = selftest.markStack(function(run) {
run.waitSecs(commandTimeoutSecs);
@@ -24,6 +25,11 @@ selftest.define("claim", ['net', 'slow'], function () {
var run = s.run('claim', testUtils.randomAppName(20));
loggedInError(run);
// Can't claim sites without specifying a site
run = s.run('claim');
run.matchErr('not enough arguments');
run.expectExit(1);
// Existing site.
run = s.run('claim', 'mother-test');
loggedInError(run);
@@ -46,6 +52,7 @@ selftest.define("claim", ['net', 'slow'], function () {
testUtils.login(s, "test", "testtest");
run = s.run('authorized', appName, '--add', 'testtest');
run.waitSecs(commandTimeoutSecs);
run.match('added');
run.expectExit(0);
testUtils.logout(s);
testUtils.login(s, "testtest", "testtest");
@@ -55,13 +62,18 @@ selftest.define("claim", ['net', 'slow'], function () {
testUtils.cleanUpApp(s, appName);
// Legacy sites.
var sLegacy = new Sandbox({
// Include a warehouse argument so that we can deploy apps with
// --release arguments.
warehouse: {
v1: { tools: 'tool1', latest: true }
}
});
var sLegacy;
if (files.inCheckout()) {
sLegacy = new Sandbox({
// Include a warehouse argument so that we can deploy apps with
// --release arguments.
warehouse: {
v1: { tools: 'tool1', latest: true }
}
});
} else {
sLegacy = new Sandbox;
}
// legacy w/pwd.
var pwd = testUtils.randomString(10);
@@ -99,3 +111,48 @@ selftest.define("claim", ['net', 'slow'], function () {
testUtils.cleanUpApp(s, legacyApp);
});
selftest.define('claim - no username', ['net', 'slow'], function () {
var s = new Sandbox;
var sandboxWithWarehouse;
if (files.inCheckout()) {
sandboxWithWarehouse = new Sandbox({
// Include a warehouse argument so that we can deploy apps with
// --release arguments.
warehouse: {
v1: { tools: 'tool1', latest: true }
}
});
} else {
sandboxWithWarehouse = new Sandbox;
}
// We shouldn't be able to claim sites before we set a username.
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
var legacyAppName = testUtils.createAndDeployLegacyApp(
sandboxWithWarehouse,
'test'
);
var run = s.run('claim', legacyAppName);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Password:');
run.write('test\n');
run.waitSecs(commandTimeoutSecs);
run.matchErr('You need to set a password');
run.matchErr(testUtils.registrationUrlRegexp);
run.expectExit(1);
// After we set a username, we should be able to claim sites.
testUtils.registerWithToken(token, username, 'testtest', email);
run = s.run('claim', legacyAppName);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Password: ');
run.write('test\n');
run.waitSecs(commandTimeoutSecs);
run.match('transferred to your account');
run.expectExit(0);
testUtils.cleanUpApp(s, appName);
testUtils.cleanUpApp(s, legacyAppName);
});

View File

@@ -1,22 +1,97 @@
var _ = require('underscore');
var selftest = require('../selftest.js');
var testUtils = require('../test-utils.js');
var files = require('../files.js');
var Sandbox = selftest.Sandbox;
var httpHelpers = require('../http-helpers.js');
var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs;
selftest.define('deploy - expired credentials', ['net', 'slow'], function () {
var s = new Sandbox;
// Create an account and then expire the login token before setting a
// username. On the next deploy, we should get an email prompt
// followed by a registration email, not a username prompt.
var email = testUtils.randomUserEmail();
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
var sessionFile = s.readSessionFile();
testUtils.logout(s);
s.writeSessionFile(sessionFile);
var run = s.run('deploy', appName);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Expired credential');
run.expectExit(1);
// Complete registration so that we can clean up our app.
var username = testUtils.randomString(10);
testUtils.registerWithToken(token, username,
'testtest', email);
testUtils.login(s, username, 'testtest');
testUtils.cleanUpApp(s, appName);
testUtils.logout(s);
// Create an account, set a username, expire the login token, and
// deploy again. We should get a username/password prompt.
email = testUtils.randomUserEmail();
appName = testUtils.randomAppName();
username = testUtils.randomString(10);
token = testUtils.deployWithNewEmail(s, email, appName);
testUtils.registerWithToken(token, username,
'testtest', email);
run = s.run('whoami');
run.waitSecs(commandTimeoutSecs);
run.read(username + '\n');
run.expectExit(0);
sessionFile = s.readSessionFile();
testUtils.logout(s);
s.writeSessionFile(sessionFile);
run = s.run('deploy', appName);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Username:');
run.write(username + '\n');
run.matchErr('Password:');
run.write('testtest' + '\n');
run.waitSecs(90);
run.expectExit(0);
testUtils.cleanUpApp(s, appName);
});
selftest.define('deploy - bad arguments', [], function () {
var s = new Sandbox;
// Deploy with no app name should fail
var run = s.run('deploy');
run.matchErr('not enough arguments');
run.expectExit(1);
// Deploy outside of an app directory
run = s.run('deploy', testUtils.randomAppName());
run.matchErr('not in a Meteor project directory');
run.expectExit(1);
});
selftest.define('deploy - logged in', ['net', 'slow'], function () {
// Create two sandboxes: one with a warehouse so that we can run
// --release, and one without a warehouse so that we run from the
// checkout or release that we started from.
// XXX Is having two sandboxes the only way to do this?
var sandboxWithWarehouse = new Sandbox({
// Include a warehouse arugment so that we can deploy apps with
// --release arguments.
warehouse: {
v1: { tools: 'tool1', latest: true }
}
});
var sandbox = new Sandbox;
var sandboxWithWarehouse;
if (files.inCheckout()) {
sandboxWithWarehouse = new Sandbox({
// Include a warehouse arugment so that we can deploy apps with
// --release arguments.
warehouse: {
v1: { tools: 'tool1', latest: true }
}
});
} else {
sandboxWithWarehouse = new Sandbox;
}
sandbox.createApp('deployapp', 'empty');
sandbox.cd('deployapp');
@@ -35,7 +110,7 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () {
run.expectExit(0);
// And we should have claimed the app by deploying to it.
run = sandbox.run('claim', noPasswordLegacyApp);
run.waitSecs(20);
run.waitSecs(commandTimeoutSecs);
run.matchErr('already belongs to you');
run.expectExit(1);
// Clean up
@@ -48,15 +123,15 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () {
);
// We shouldn't be able to deploy to this app without claiming it
run = sandbox.run('deploy', passwordLegacyApp);
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('meteor claim');
run.expectExit(1);
// If we claim it, we should be able to deploy to it.
run = sandbox.run('claim', passwordLegacyApp);
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Password:');
run.write('test\n');
run.waitSecs(10);
run.waitSecs(commandTimeoutSecs);
run.match('successfully transferred to your account');
run.expectExit(0);
run = sandbox.run('deploy', passwordLegacyApp);
@@ -75,7 +150,7 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () {
testUtils.logout(sandbox);
testUtils.login(sandbox, 'testtest', 'testtest');
run = sandbox.run('deploy', appName);
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('belongs to a different user');
run.expectExit(1);
@@ -89,9 +164,14 @@ selftest.define('deploy - logged in', ['net', 'slow'], function () {
selftest.define('deploy - logged out', ['net', 'slow'], function () {
var s = new Sandbox;
var sandboxWithWarehouse = new Sandbox({
warehouse: { v1: { tools: 'tool1', latest: true } }
});
var sandboxWithWarehouse;
if (files.inCheckout()) {
sandboxWithWarehouse = new Sandbox({
warehouse: { v1: { tools: 'tool1', latest: true } }
});
} else {
sandboxWithWarehouse = new Sandbox;
}
testUtils.login(s, 'test', 'testtest');
var appName = testUtils.createAndDeployApp(s);
@@ -100,10 +180,10 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () {
// Deploy when logged out. We should be prompted to log in and then
// the deploy should succeed.
var run = s.run('deploy', appName);
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Email:');
run.write('test@test.com\n');
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Password:');
run.write('testtest\n');
run.waitSecs(90);
@@ -119,10 +199,10 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () {
sandboxWithWarehouse
);
run = s.run('deploy', legacyNoPassword);
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Email:');
run.write('test@test.com\n');
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Password: ');
run.write('testtest\n');
run.waitSecs(90);
@@ -137,15 +217,15 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () {
'test'
);
run = s.run('deploy', legacyPassword);
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Email:');
// Log in with a username here to test that the email prompt also
// accepts emails. (We put an email in the email prompt above.)
run.write('test\n');
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Password:');
run.write('testtest\n');
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('meteor claim');
run.expectExit(1);
@@ -158,12 +238,12 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () {
appName = testUtils.randomAppName();
var email = testUtils.randomUserEmail();
run = s.run('deploy', appName);
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(90);
run.match('Now serving');
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.expectExit(0);
// Now that we've created an account with this email address, we
// should be logged in as it and should be able to delete it.
@@ -172,11 +252,11 @@ selftest.define('deploy - logged out', ['net', 'slow'], function () {
// Now that we've created a user, try to deploy a new app.
appName = testUtils.randomAppName();
run = s.run('deploy', appName);
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(5);
run.matchErr('already in use');
run.waitSecs(commandTimeoutSecs);
run.matchErr('pick a password');
run.matchErr('An email has been sent to you with the link');
run.stop();
});

View File

@@ -0,0 +1,82 @@
var _ = require('underscore');
var selftest = require('../selftest.js');
var testUtils = require('../test-utils.js');
var utils = require('../utils.js');
var Sandbox = selftest.Sandbox;
var httpHelpers = require('../http-helpers.js');
// Poll the given app looking for the correct settings. Throws an error
// if the settings aren't found after a timeout.
var checkForSettings = function (appName, settings, timeoutSecs) {
var timer = setTimeout(function () {
throw new Error('Expected settings not found on app ', appName);
}, timeoutSecs * 1000);
while (true) {
var result = httpHelpers.request('http://' + appName + '.meteor.com');
// XXX This is brittle; the test will break if we start formatting the
// __meteor_runtime_config__ JS differently. Ideally we'd do something
// like point a phantom at the deployed app and actually evaluate
// Meteor.settings.
var configRegexp = /__meteor_runtime_config__ = (.+);<\/script>/;
var configMatch = result.body.match(configRegexp);
if (configMatch && configMatch[1]) {
var stringifiedConfig = configMatch[1].trim();
var parsedConfig = JSON.parse(stringifiedConfig);
if (_.isEqual(parsedConfig.PUBLIC_SETTINGS, settings['public'])) {
clearTimeout(timer);
return;
}
}
}
};
selftest.define('deploy - with settings', ['net', 'slow'], function () {
var s = new Sandbox;
testUtils.login(s, 'test', 'testtest');
var settings = {
'public': { a: 'b' }
};
s.write('settings.json', JSON.stringify(settings));
// Deploy an app with settings and check that the public settings
// appear in the HTTP response body.
var appName = testUtils.createAndDeployApp(s, {
// Use standard-app instead of empty because we actually want
// standard-app-packages (including webapp) so that we can send a
// HTTP request to the app and get a response.
templateApp: 'standard-app',
// The path is ../settings.json instead of settings.json because
// createAndDeployApp creates a new app directory and cd's into it.
settingsFile: '../settings.json'
});
checkForSettings(appName, settings, 10);
// Re-deploy without settings and check that the settings still
// appear.
s.cd('..');
testUtils.createAndDeployApp(s, {
templateApp: 'standard-app',
appName: appName
});
// It takes a few seconds for the app to actually update, and we don't
// want to get a false positive in the meantime (i.e., if the settings
// disappear, we don't want to send our request before the app has
// updated and conclude that the settings are still there).
utils.sleepMs(5000);
checkForSettings(appName, settings, 10);
// Re-deploy with new settings and check that the settings get
// updated.
settings['public'].a = 'c';
s.cd('..');
s.write('settings.json', JSON.stringify(settings));
testUtils.createAndDeployApp(s, {
templateApp: 'standard-app',
settingsFile: '../settings.json',
appName: appName
});
checkForSettings(appName, settings, 10);
testUtils.cleanUpApp(s, appName);
});

View File

@@ -1,5 +1,8 @@
var selftest = require('../selftest.js');
var Sandbox = selftest.Sandbox;
var testUtils = require('../test-utils.js');
var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs;
selftest.define("login", ['net'], function () {
var s = new Sandbox;
@@ -11,12 +14,46 @@ selftest.define("login", ['net'], function () {
// Username and password prompts happen on stderr so that scripts can
// run commands that do login interactively and still save the command
// output with the login prompts appearing in it.
//
// Do this twice to confirm that the login command prints a prompt
// even if you are already logged in.
for (var i = 0; i < 2; i++) {
run = s.run("login");
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
run.write("testtest\n");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Logged in as test.");
run.expectExit(0);
}
// Leaving username blank, or getting the password wrong, doesn't
// reprompt. It also doesn't log you out.
run = s.run("login");
run.matchErr("Username:");
run.write("\n");
run.matchErr("Password:");
run.write("whatever\n");
run.waitSecs(commandTimeoutSecs);
run.matchErr("failed");
run.expectExit(1);
run = s.run("login");
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
run.write("whatever\n");
run.waitSecs(commandTimeoutSecs);
run.matchErr("failed");
run.expectExit(1);
run = s.run('login');
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
run.write("testtest\n");
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Logged in as test.");
run.expectExit(0);
@@ -28,12 +65,12 @@ selftest.define("login", ['net'], function () {
run.expectExit(0);
run = s.run("logout");
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Logged out");
run.expectExit(0);
run = s.run("logout");
run.waitSecs(1);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Not logged in");
run.expectExit(0);
@@ -47,7 +84,7 @@ selftest.define("login", ['net'], function () {
run.write("test\n");
run.matchErr("Password:");
run.write("badpassword\n");
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Login failed");
run.expectExit(1);
@@ -58,12 +95,12 @@ selftest.define("login", ['net'], function () {
run.write("TeSt\n");
run.matchErr("Password:");
run.write("testtest\n");
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Logged in as test.");
run.expectExit(0);
run = s.run("logout");
run.waitSecs(2);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Logged out");
run.expectExit(0);
@@ -74,7 +111,29 @@ selftest.define("login", ['net'], function () {
run.write("test\n");
run.matchErr("Password:");
run.write("TesTTesT\n");
run.waitSecs(5);
run.waitSecs(commandTimeoutSecs);
run.matchErr("Login failed");
run.expectExit(1);
});
selftest.define('whoami - no username', ['net', 'slow'], function () {
var s = new Sandbox;
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
var run = s.run('whoami');
run.waitSecs(commandTimeoutSecs);
run.matchErr('You haven\'t chosen your username yet');
run.matchErr(testUtils.registrationUrlRegexp);
run.expectExit(1);
testUtils.registerWithToken(token, username, 'test', email);
run = s.run('whoami');
run.waitSecs(commandTimeoutSecs);
run.read(username);
run.expectExit(0);
testUtils.cleanUpApp(s, appName);
});

View File

@@ -1,6 +1,7 @@
var _ = require('underscore');
var selftest = require('../selftest.js');
var Sandbox = selftest.Sandbox;
var testUtils = require('../test-utils.js');
// XXX need to make sure that mother doesn't clean up:
// 'legacy-password-app-for-selftest'
@@ -8,6 +9,8 @@ var Sandbox = selftest.Sandbox;
// 'app-for-selftest-not-test-owned'
// 'app-for-selftest-test-owned'
var commandTimeoutSecs = testUtils.accountsCommandTimeoutSecs;
// Run 'meteor logs' or 'meteor mongo' against an app. Options:
// - legacy: boolean
@@ -37,7 +40,7 @@ var logsOrMongoForApp = function (sandbox, command, appName, options) {
}
var run = sandbox.run.apply(sandbox, runArgs);
run.waitSecs(10);
run.waitSecs(commandTimeoutSecs);
var expectSuccess = selftest.markStack(function () {
run.match(matchString);
@@ -68,11 +71,26 @@ var logsOrMongoForApp = function (sandbox, command, appName, options) {
} else {
// If we are not logged in and this is not a legacy app, then we
// expect a login prompt.
//
// (If testReprompt is true, try getting reprompted as a result
// of entering no username or a bad password.)
if (options.testReprompt) {
run.matchErr('Username: ');
run.write("\n");
run.matchErr("Username:");
run.write(" \n");
}
run.matchErr('Username: ');
run.write((options.username || 'test') + '\n');
if (options.testReprompt) {
run.matchErr("Password:");
run.write("wrongpassword\n");
run.waitSecs(15);
run.matchErr("failed");
}
run.matchErr('Password: ');
run.write((options.password || 'testtest') + '\n');
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
if (options.authorized) {
expectSuccess();
} else {
@@ -90,18 +108,33 @@ _.each([false, true], function (loggedIn) {
['net'],
function () {
var s = new Sandbox;
var run;
if (loggedIn) {
var run = s.run('login');
run.waitSecs(2);
run = s.run('login');
run.waitSecs(commandTimeoutSecs);
run.matchErr('Username:');
run.write('test\n');
run.matchErr('Password:');
run.write('testtest\n');
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Logged in as test.');
run.expectExit(0);
}
// Running 'meteor logs' without an app name should fail.
if (command === 'logs') {
run = s.run(command);
run.matchErr('not enough arguments');
run.expectExit(1);
}
// Running 'meteor mongo' without an app name and not in an app
// dir should fail.
if (command === 'mongo') {
run = s.run('mongo');
run.matchErr('not in a Meteor project directory');
run.expectExit(1);
}
logsOrMongoForApp(s, command,
'legacy-no-password-app-for-selftest', {
legacy: true,
@@ -119,14 +152,15 @@ _.each([false, true], function (loggedIn) {
logsOrMongoForApp(s, command,
'app-for-selftest-not-test-owned', {
loggedIn: loggedIn,
authorized: false
authorized: false,
testReprompt: true
});
if (! loggedIn) {
// We logged in as a result of running the previous command,
// so log out again.
run = s.run('logout');
run.waitSecs(15);
run.waitSecs(commandTimeoutSecs);
run.matchErr('Logged out');
run.expectExit(0);
}

View File

@@ -25,11 +25,13 @@ selftest.define("npm", ["net"], function () {
run.tellMongo(MONGO_LISTENING);
if (i === 0) {
run.waitSecs(2);
// use match instead of read because on a built release we can
// also get an update message here.
run.read(
"npm-test: updating npm dependencies -- meteor-test-executable...\n");
}
run.waitSecs(15);
run.read("null; From shell script\n");
run.match("null; From shell script\n");
run.expectEnd();
run.expectExit(0);
});

View File

@@ -4,21 +4,20 @@ var testUtils = require('../test-utils.js');
var utils = require('../utils.js');
var Sandbox = selftest.Sandbox;
var httpHelpers = require('../http-helpers.js');
var release = require('../release.js');
var unipackage = require('../unipackage.js');
var config = require('../config.js');
var getLoadedPackages = _.once(function () {
return unipackage.load({
library: release.current.library,
packages: ['meteor', 'livedata'],
release: release.current.name
});
});
var ddpConnect = function (url) {
var DDP = getLoadedPackages().livedata.DDP;
return DDP.connect(url);
var expectInvalidToken = function (token) {
// Same XXX as testUtils.registerWithToken: should be hardcoded to
// https://www.meteor.com?
var accountsConn = testUtils.ddpConnect(config.getAuthDDPUrl());
var registrationTokenInfo = accountsConn.call('registrationTokenInfo',
token);
// We should not be able to get a registration code for an invalid
// token.
if (registrationTokenInfo.valid || registrationTokenInfo.code) {
throw new Error('Expected invalid token is valid!');
}
accountsConn.close();
};
// Polls a guerrillamail.com inbox every 3 seconds looking for an email
@@ -96,12 +95,61 @@ var waitForEmail = selftest.markStack(function (inbox, subjectRegExp,
return match;
});
selftest.define('deferred registration', ['net'], function () {
selftest.define('deferred registration - email registration token', ['net', 'slow'], function () {
var s = new Sandbox;
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var apiToken = testUtils.deployWithNewEmail(s, email, appName);
// Check that we got a registration email in our inbox.
var registrationEmail = waitForEmail(email, /Set a password/,
/set a password/, 60);
// Fish out the registration token and use to it to complete
// registration.
var token = testUtils.registrationUrlRegexp.exec(registrationEmail.bodyPage);
if (! token || ! token[1]) {
throw new Error("No registration token in email");
}
token = token[1];
testUtils.registerWithToken(token, username, 'testtest', email);
// Success! 'meteor whoami' should now know who we are.
run = s.run('whoami');
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.read(username + '\n');
run.expectExit(0);
// We should be able to log out and log back in with our new password.
testUtils.logout(s);
testUtils.login(s, username, 'testtest');
// And after logging out and logging back in, we should have
// authorization to delete our app.
testUtils.cleanUpApp(s, appName);
// All the tokens we got should now be invalid.
expectInvalidToken(token);
expectInvalidToken(apiToken);
// XXX Test that registration URLs get printed when they should
});
selftest.define('deferred registration revocation', ['net'], function () {
// Test that if we are logged in as a passwordless user, and our
// credential gets revoked, and we do something like 'meteor whoami'
// that polls to see if registration is complete, then we handle it
// gracefully.
var s = new Sandbox;
s.createApp('deployapp', 'empty');
s.cd('deployapp');
// Deploy an app with a new email address.
// Create a new deferred registration account. (Don't bother to wait
// for the deploy to go through.)
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
@@ -110,52 +158,114 @@ selftest.define('deferred registration', ['net'], function () {
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(90);
// Check that we got a prompt to set a password on meteor.com.
run.matchErr('set a password');
run.matchErr('https://www.meteor.com');
run.match('Deploying');
run.waitSecs(15); // because the bundler doesn't yield
run.stop();
// 'whoami' says that we don't have a password
run = s.run('whoami');
run.waitSecs(15);
run.matchErr('/setPassword?');
run.expectExit(1);
// Revoke the credential without updating .meteorsession.
var sessionState = s.readSessionFile();
run = s.run('logout');
run.waitSecs(15);
run.readErr("Logged out.\n");
run.expectEnd();
run.expectExit(0);
s.writeSessionFile(sessionState);
// Check that we got a registration email in our inbox.
var registrationEmail = waitForEmail(email, /Set a password/,
/set a password/, 60);
// Fish out the registration token and use to it to complete
// registration.
var token = /\/setPassword\?([a-zA-Z0-9\+\/]+)/.
exec(registrationEmail.bodyPage);
if (! token || ! token[1]) {
throw new Error("No registration token in email");
}
token = token[1];
// XXX It might make more sense to hard-code the DDP url to
// https://www.meteor.com, since that's who the sandboxes are talking
// to.
var accountsConn = ddpConnect(config.getAuthDDPUrl());
var registrationTokenInfo = accountsConn.call('registrationTokenInfo',
token);
var registrationCode = registrationTokenInfo.code;
accountsConn.call('register', {
username: username,
password: 'testtest',
emails: [email],
token: token,
code: registrationCode
});
accountsConn.close();
// Success! We should be able to log out and log back in with our new
// password.
testUtils.logout(s);
testUtils.login(s, username, 'testtest');
// And after logging out and logging back in, we should have
// authorization to delete our app.
testUtils.cleanUpApp(s, appName);
// XXX Test the api registration URLs
// XXX Test that registration URLs get printed when they should
// XXX Test registration while the tool is waiting on a DDP method to
// return (e.g. deploy and login with an existing username that
// doesn't have a password set yet)
// 'whoami' now says that we're not logged in. No errors are printed.
run = s.run('whoami');
run.waitSecs(15);
run.readErr("Not logged in. 'meteor login' to log in.\n");
run.expectEnd();
run.expectExit(1);
});
selftest.define(
'deferred registration - api registration token',
['net', 'slow'],
function () {
var s = new Sandbox;
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
testUtils.registerWithToken(token, username, 'testtest', email);
testUtils.logout(s);
testUtils.login(s, username, 'testtest');
testUtils.cleanUpApp(s, appName);
// All tokens we received should not be invalid.
expectInvalidToken(token);
var registrationEmail = waitForEmail(email, /Set a password/,
/set a password/, 60);
var emailToken = testUtils.registrationUrlRegexp.exec(
registrationEmail.bodyPage
);
if (! emailToken || ! emailToken[1]) {
throw new Error('No registration token in email');
}
expectInvalidToken(emailToken[1]);
}
);
selftest.define(
'deferred registration - register after logging out',
['net', 'slow'],
function () {
var s = new Sandbox;
var email = testUtils.randomUserEmail();
var username = testUtils.randomString(10);
var appName = testUtils.randomAppName();
var token = testUtils.deployWithNewEmail(s, email, appName);
testUtils.logout(s);
// If we deploy again with the same email address after logging out,
// we should get a message telling us to check our email and
// register, and the tool should obediently wait for us to do that
// before doing the deploy.
s.createApp('deployapp2', 'empty');
s.cd('deployapp2');
var run = s.run('deploy', appName);
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.matchErr('Email:');
run.write(email + '\n');
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.matchErr('pick a password');
run.matchErr('Waiting for you to register on the web...');
var registrationEmail = waitForEmail(
email,
/Set a password/,
/You previously created a Meteor developer account/,
60
);
token = testUtils.registrationUrlRegexp.exec(
registrationEmail.bodyPage
);
if (! token || ! token[1]) {
throw new Error('No registration token in email');
}
testUtils.registerWithToken(token[1], username, 'testtest', email);
run.waitSecs(testUtils.accountsCommandTimeoutSecs);
run.matchErr('Username: ' + username + '\n');
run.matchErr('Password: ');
run.write('testtest\n');
run.waitSecs(90);
run.match('Now serving at');
run.expectExit(0);
run = s.run('whoami');
run.read(username);
run.expectExit(0);
testUtils.cleanUpApp(s, appName);
}
);

View File

@@ -92,6 +92,12 @@ selftest.define("springboard", ['checkout'], function () {
run.read('v2\ntools2\n');
run.expectEnd();
run.expectExit(0);
// .meteor/release exists but is empty. You get an error.
s.write(".meteor/release", "\n");
run = s.run("list", "--using");
run.matchErr("release file which is empty");
run.expectExit(1);
});
});

View File

@@ -0,0 +1,18 @@
var selftest = require('../selftest.js');
var Sandbox = selftest.Sandbox;
selftest.define("run-command", function () {
var s = new Sandbox;
var run;
s.createApp("myapp", "with-command");
run = s.run("run-command", "myapp/packages/command");
run.read("argv []\n");
run.expectEnd();
run.expectExit(17);
run = s.run("run-command", "myapp/packages/command", "x", "--", "-f", "--bla");
run.read("argv [ 'x', '-f', '--bla' ]\n");
run.expectEnd();
run.expectExit(17);
});

View File

@@ -44,6 +44,7 @@ selftest.define("run", function () {
// Crashes
s.write("crash.js", "process.exit(42);");
run.waitSecs(5);
run.match("with code: 42");
run.waitSecs(5);
run.match("is crashing");
@@ -54,6 +55,7 @@ selftest.define("run", function () {
run.waitSecs(5);
run.match("restarted (x2)"); // see that restart counter reset
s.write("crash.js", "process.kill(process.pid, 'SIGKILL');");
run.waitSecs(5);
run.match("from signal: SIGKILL");
run.waitSecs(5);
run.match("is crashing");
@@ -81,6 +83,7 @@ selftest.define("run", function () {
" fs.writeFileSync(crashmark);\n" +
" process.exit(137);\n" +
"}\n");
run.waitSecs(5);
run.match("with code: 137");
run.match("restarted");
run.stop();
@@ -171,7 +174,7 @@ selftest.define("run --once", function () {
s.cd("onceapp");
s.set("RUN_ONCE_OUTCOME", "mongo");
run = s.run("--once");
run.waitSecs(5);
run.waitSecs(15);
run.expectExit(86);
});

View File

@@ -71,9 +71,9 @@ var check = function (showBanner) {
if (manifest.releases.stable.banner &&
warehouse.lastPrintedBannerRelease() !== manifestLatestRelease) {
if (showBanner) {
runLog.log();
runLog.log("");
runLog.log(manifest.releases.stable.banner);
runLog.log();
runLog.log("");
}
warehouse.writeLastPrintedBannerRelease(manifestLatestRelease);
} else {

View File

@@ -341,11 +341,24 @@ _.extend(warehouse, {
// (whether or not we just downloaded them). (Don't do this if we didn't
// print the installing message!)
if (newPieces && showInstalling) {
var unlinkIfExists = function (file) {
try {
fs.unlinkSync(file);
} catch (e) {
// If two processes populate the warehouse in parallel, the other
// process may have deleted the fresh file. That's OK!
if (e.code === "ENOENT")
return;
throw e;
}
};
if (newPieces.tools) {
fs.unlinkSync(warehouse.getToolsFreshFile(newPieces.tools.version));
unlinkIfExists(warehouse.getToolsFreshFile(newPieces.tools.version));
}
_.each(newPieces.packages, function (packageInfo, name) {
fs.unlinkSync(warehouse.getPackageFreshFile(name, packageInfo.version));
unlinkIfExists(
warehouse.getPackageFreshFile(name, packageInfo.version));
});
}