Merge branch 'release-0.5.3'

This commit is contained in:
David Glasser
2013-01-07 12:24:03 -08:00
121 changed files with 3551 additions and 3663 deletions

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@
\#*\#
.\#*
.idea
*.iml
*.sublime-project
*.sublime-workspace

View File

@@ -1,6 +1,81 @@
## vNEXT
## v0.5.3
* Add `--settings` argument to `meteor deploy` and `meteor run`. This
allows you to specify deployment-specific information made available
to server code in the variable `Meteor.settings`.
* Support unlimited open tabs in a single browser. Work around the
browser per-hostname connection limit by using randomized hostnames
for deployed apps. #131
* minimongo improvements:
* Allow observing cursors with `skip` or `limit`. #528
* Allow sorting on `dotted.sub.keys`. #533
* Allow querying specific array elements (`foo.1.bar`).
* `$and`, `$or`, and `$nor` no longer accept empty arrays (for consistency
with Mongo)
* Re-rendering a template with Spark no longer reverts changes made by
users to a `preserve`d form element. Instead, the newly rendered value
is only applied if it is different from the previously rendered value.
Additionally, <INPUT> elements with type other than TEXT can now have
reactive values (eg, the labels on submit buttons can now be
reactive). #510 #514 #523 #537 #558
* Support JavaScript RegExp objects in selectors in Collection write
methods on the client, eg `myCollection.remove({foo: /bar/})`. #346
* `meteor` command-line improvements:
* Improve error message when mongod fails to start.
* The `NODE_OPTIONS` environment variable can be used to pass command-line
flags to node (eg, `--debug` or `--debug-brk` to enable the debugger).
* Die with error if an app name is mistakenly passed to `meteor reset`.
* Add support for "offline" access tokens with Google login. #464 #525
* Don't remove `serviceData` fields from previous logins when logging in
with an external service.
* Improve `OAuth1Binding` to allow making authenticated API calls to
OAuth1 providers (eg Twitter). #539
* New login providers automatically work with `{{loginButtons}}` without
needing to edit the `accounts-ui-unstyled` package. #572
* Use `Content-Type: application/json` by default when sending JSON data
with `Meteor.http`.
* Improvements to `jsparse`: hex literals, keywords as property names, ES5 line
continuations, trailing commas in object literals, line numbers in error
messages, decimal literals starting with `.`, regex character classes with
slashes.
* Spark improvements:
* Improve rendering of <SELECT> elements on IE. #496
* Don't lose nested data contexts in IE9/10 after two seconds. #458
* Don't print a stack trace if DOM nodes are manually removed
from the document without calling `Spark.finalize`. #392
* Always use the `autoReconnect` flag when connecting to Mongo. #425
* Fix server-side `observe` with no `added` callback. #589
* Fix re-sending method calls on reconnect. #538
* Remove deprecated `/sockjs` URL support from `Meteor.connect`.
* Avoid losing a few bits of randomness in UUID v4 creation. #519
* Update clean-css package from 0.8.2 to 0.8.3, fixing minification of `0%`
values in `hsl` colors. #515
Patches contributed by GitHub users Ed-von-Schleck, egtann, jwulf, lvbreda,
martin-naumann, meawoppl, nwmartin, timhaines, and zealoushacker.
## v0.5.2
* Fix 0.5.1 regression: Cursor `observe` works during server startup. #507

View File

@@ -1,9 +1,19 @@
#!/bin/bash
# NOTE: by default this tests the installed meteor, not the one in your
# working copy.
# NOTE: by default this tests the working copy, not the installed meteor.
# To test the installed meteor, pass in --global
METEOR=/usr/local/bin/meteor
cd `dirname $0`
METEOR=`pwd`/../meteor
if [ -z "$NODE" ]; then
NODE=`pwd`/node.sh
fi
#If this ever takes more options, use getopt
if [ "$1" == "--global" ]; then
METEOR=meteor
fi
DIR=`mktemp -d -t meteor-cli-test-XXXXXXXX`
trap 'echo FAILED ; rm -rf "$DIR" >/dev/null 2>&1' EXIT
@@ -71,7 +81,8 @@ echo "... run"
MONGOMARK='--bind_ip 127.0.0.1 --smallfiles --port 9102'
# kill any old test meteor
# there is probably a better way to do this, but it is at least portable across macos and linux
ps ax | grep -e 'meteor.js -p 9100' | grep -v grep | awk '{print $1}' | xargs kill
# (the || true is needed on linux, whose xargs will invoke kill even with no args)
ps ax | grep -e 'meteor.js -p 9100' | grep -v grep | awk '{print $1}' | xargs kill || true
! $METEOR mongo > /dev/null 2>&1
$METEOR reset > /dev/null 2>&1
@@ -83,7 +94,7 @@ PORT=9100
$METEOR -p $PORT > /dev/null 2>&1 &
METEOR_PID=$!
sleep 1 # XXX XXX lame
sleep 2 # XXX XXX lame
test -d .meteor/local/db
ps ax | grep -e "$MONGOMARK" | grep -v grep > /dev/null
@@ -105,16 +116,51 @@ echo "... rerun"
$METEOR -p $PORT > /dev/null 2>&1 &
METEOR_PID=$!
sleep 1 # XXX XXX lame
sleep 2 # XXX XXX lame
ps ax | grep -e "$MONGOMARK" | grep -v grep > /dev/null
curl -s "http://localhost:$PORT" > /dev/null
kill $METEOR_PID
ps ax | grep -e "$MONGOMARK" | grep -v grep | awk '{print $1}' | xargs kill
sleep 10 # XXX XXX lame. have to wait for inner app to die via keepalive!
ps ax | grep -e "$MONGOMARK" | grep -v grep | awk '{print $1}' | xargs kill || true
sleep 2 # need to make sure these kills take effect
echo "... mongo message"
# Run a server on the same port as mongod, so that mongod fails to start up. Rig
# it so that a single connection will cause it to exit.
$NODE -e 'require("net").createServer(function(){process.exit(0)}).listen('$PORT'+2, "127.0.0.1")' &
sleep 1
$METEOR -p $PORT > error.txt || true
grep 'port was closed' error.txt > /dev/null
# Kill the server by connecting to it.
$NODE -e 'require("net").connect({host:"127.0.0.1",port:'$PORT'+2},function(){process.exit(0);})'
echo "... settings"
cat > settings.json <<EOF
{ "foo" : "bar",
"baz" : "quux"
}
EOF
cat > settings.js <<EOF
if (Meteor.isServer) {
Meteor.startup(function () {
if (!Meteor.settings) process.exit(1);
if (Meteor.settings.foo !== "bar") process.exit(1);
process.exit(0);
});
}
EOF
$METEOR -p $PORT --settings='settings.json' --once > /dev/null
# XXX more tests here!

View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Requires s3cmd to be installed and an appropriate ~/.s3cfg.
# Usage:
# admin/copy-release-from-jenkins.sh [--prod] BUILDNUMBER
# where BUILDNUMBER is the small integer Jenkins build number.
set -e
set -u
cd `dirname $0`
TARGET="s3://com.meteor.static/test/"
TEST=no
if [ $# -ge 1 -a $1 = '--prod' ]; then
shift
TARGET="s3://com.meteor.static/"
else
TEST=yes
fi
if [ $# -ne 1 ]; then
echo "usage: $0 [--prod] jenkins-build-number" 1>&2
exit 1
fi
DIRNAME=$(s3cmd ls s3://com.meteor.jenkins/ | perl -nle 'print $1 if m!/(release-.+--'$1'--.+)/!')
if [ -z "$DIRNAME" ]; then
echo "build not found" 1>&2
exit 1
fi
echo Found build $DIRNAME
# Check to make sure the proper number of each kind of file is there.
s3cmd ls s3://com.meteor.jenkins/$DIRNAME/ | \
perl -nle '++$RPM if /\.rpm/; ++$DEB if /\.deb/; ++$TAR if /\.tar\.gz/; ++$DIR if /DIR/; END { exit !($RPM == 2 && $DEB == 2 && $TAR == 3 && $DIR == 1) }'
echo Copying to $TARGET
s3cmd -P cp -r s3://com.meteor.jenkins/$DIRNAME/ $TARGET
if [ $TEST = 'yes' ]; then
echo Uploading modified install-s3.sh and manifest.json
OUTDIR=$(mktemp -dt meteor-crfj)
perl -pe 's!https://d3sqy0vbqsdhku.cloudfront.net!https://s3.amazonaws.com/com.meteor.static/test!g' install-s3.sh >$OUTDIR/install-s3.sh
perl -pe 's!https://d3sqy0vbqsdhku.cloudfront.net!https://s3.amazonaws.com/com.meteor.static/test!g' manifest.json >$OUTDIR/manifest.json
cd $OUTDIR
s3cmd -P put install-s3.sh s3://com.meteor.static/test/update/
s3cmd -P put manifest.json s3://com.meteor.static/test/update/
fi

View File

@@ -1,4 +1,4 @@
meteor (0.5.2-1) unstable; urgency=low
meteor (0.5.3-1) unstable; urgency=low
* Automated debian build.

11
admin/find-new-npm-versions.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
BASEDIR=`dirname $0`
cat $BASEDIR/generate-dev-bundle.sh | grep "npm install" | sed "s/npm install //" | sed "s/@.*//" | while read PACKAGE
do
CURRENT_VERSION=`cat $BASEDIR/generate-dev-bundle.sh | grep "npm install $PACKAGE" | sed "s/npm install //" | sed "s/.*@//"`
LATEST_VERSION=`$BASEDIR/../dev_bundle/bin/npm info $PACKAGE version 2> /dev/null`
if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]
then
echo "$PACKAGE -- current version: $CURRENT_VERSION, latest version: $LATEST_VERSION"
fi
done

View File

@@ -3,7 +3,7 @@
set -e
set -u
BUNDLE_VERSION=0.2.8
BUNDLE_VERSION=0.2.12
UNAME=$(uname)
ARCH=$(uname -m)
@@ -89,7 +89,7 @@ npm install semver@1.1.0
npm install handlebars@1.0.7
npm install mongodb@1.1.11
npm install uglify-js@1.3.4
npm install clean-css@0.8.2
npm install clean-css@0.8.3
npm install useragent@1.1.0
npm install request@2.12.0
npm install simplesmtp@0.1.25
@@ -97,6 +97,8 @@ npm install stream-buffers@0.2.3
npm install keypress@0.1.0
npm install sockjs@0.3.4
npm install http-proxy@0.8.5
npm install underscore@1.4.2
npm install tar@0.1.14
# progress 0.1.0 has a regression where it opens stdin and thus does not
# allow the node process to exit cleanly. See
@@ -107,6 +109,13 @@ npm install progress@0.0.5
# which make the dev bundle much bigger. We need a better solution.
npm install mailcomposer@0.1.15
# Use our version of fstream with a bug fixed. Also have tar use it.
# See https://github.com/isaacs/fstream/pull/11 .
npm install https://github.com/meteor/fstream/tarball/91c56e7
cd tar/node_modules
npm install https://github.com/meteor/fstream/tarball/91c56e7
cd ../..
# If you update the version of fibers in the dev bundle, also update the "npm
# install" command in docs/client/concepts.html.
npm install fibers@0.6.9

View File

@@ -7,7 +7,7 @@ var semver = require('semver');
var optimist = require('optimist');
var updater = require(path.join(__dirname, '..', 'app', 'lib', 'updater.js'));
var _ = require(path.join(__dirname, '..', 'app', 'lib', 'third', 'underscore.js'));
var _ = require('underscore');
// What files to update. Relative to project root.
var UPDATE_FILES = [path.join('app', 'lib', 'updater.js'),

View File

@@ -5,7 +5,7 @@
## example.
URLBASE="https://d3sqy0vbqsdhku.cloudfront.net"
VERSION="0.5.2"
VERSION="0.5.3"
PKGVERSION="${VERSION}-1"
UNAME=`uname`

View File

@@ -1,6 +1,6 @@
{
"version": "0.5.2",
"deb_version": "0.5.2-1",
"rpm_version": "0.5.2-1",
"version": "0.5.3",
"deb_version": "0.5.3-1",
"rpm_version": "0.5.3-1",
"urlbase": "https://d3sqy0vbqsdhku.cloudfront.net"
}

View File

@@ -5,7 +5,7 @@
Summary: Meteor platform and JavaScript application server
Vendor: Meteor
Name: meteor
Version: 0.5.2
Version: 0.5.3
Release: 1
License: MIT
Group: Networking/WWW

View File

@@ -20,6 +20,7 @@ if [ "$EMACS" == t ]; then
fi
"$TOPDIR/dev_bundle/bin/node" "$@"
EXITSTATUS=$?
# Node sets stdin to non-blocking, which causes Emacs shell to die after it
# exits. Work around this by setting stdin to blocking again.
@@ -27,3 +28,5 @@ if [ "$EMACS" == t ]; then
perl -MFcntl=F_GETFL,F_SETFL,O_NONBLOCK -e \
'fcntl(STDIN, F_SETFL, ~O_NONBLOCK & fcntl(STDIN, F_GETFL, 0))'
fi
exit $EXITSTATUS

View File

@@ -31,7 +31,7 @@ var crypto = require('crypto');
var fs = require('fs');
var uglify = require('uglify-js');
var cleanCSS = require('clean-css');
var _ = require(path.join(__dirname, 'third', 'underscore.js'));
var _ = require('underscore');
// files to ignore when bundling. node has no globs, so use regexps
var ignore_files = [
@@ -143,7 +143,7 @@ _.extend(PackageInstance.prototype, {
// should be the extension of the file without a leading dot.)
get_source_handler: function (extension) {
var self = this;
var candidates = []
var candidates = [];
if (extension in self.pkg.extensions)
candidates.push(self.pkg.extensions[extension]);
@@ -277,7 +277,7 @@ var Bundle = function () {
_.each(where, function (w) {
if (options.type === "js") {
if (!options.path)
throw new Error("Must specify path")
throw new Error("Must specify path");
if (w === "client" || w === "server") {
self.files[w][options.path] = data;
@@ -292,7 +292,7 @@ var Bundle = function () {
// that appear in the server directories in an app tree
return;
if (!options.path)
throw new Error("Must specify path")
throw new Error("Must specify path");
self.files.client[options.path] = data;
self.css.push(options.path);
} else if (options.type === "head" || options.type === "body") {
@@ -358,8 +358,8 @@ _.extend(Bundle.prototype, {
// XXX detect circular dependencies and print an error. (not sure
// what the current code will do)
if (pkg.on_use)
pkg.on_use(inst.api, where);
if (pkg.on_use_handler)
pkg.on_use_handler(inst.api, where);
},
include_tests: function (pkg) {
@@ -369,8 +369,8 @@ _.extend(Bundle.prototype, {
self.tests_included[pkg.id] = true;
var inst = self._get_instance(pkg);
if (inst.pkg.on_test)
inst.pkg.on_test(inst.api);
if (inst.pkg.on_test_handler)
inst.pkg.on_test_handler(inst.api);
},
// Minify the bundle

View File

@@ -1,6 +1,6 @@
var fs = require("fs");
var path = require('path');
var _ = require(path.join(__dirname, 'third', 'underscore.js'));
var _ = require('underscore');
var files = module.exports = {
// A sort comparator to order files into load order.

View File

@@ -0,0 +1,67 @@
// MongoDB exit codes. This replicates information in
// https://github.com/mongodb/docs/blob/master/source/reference/exit-codes.txt
// but in a javascript dictionary instead of just a text file.
// Explanations have been rewritten, not copied, for license reasons.
var path = require("path");
var _ = require('underscore');
exports.Codes = {
0 : { code: 0,
symbol: "EXIT_CLEAN",
longText: "MongoDB exited cleanly"
},
2 : { code: 2,
symbol: "EXIT_BADOPTIONS",
longText: "MongoDB was started with erroneous or incompatible command line options"
},
3 : { code: 3,
symbol: "EXIT_REPLICATION_ERROR",
longText: "There was an inconsistency between hostnames specified\n" +
"on the command line compared with hostnames stored in local.sources"
},
4 : { code: 4,
symbol: "EXIT_NEED_UPGRADE",
longText: "MongoDB needs to upgrade to use this database"
},
5 : { code: 5,
symbol: "EXIT_SHARDING_ERROR",
longText: "A moveChunk operation failed"
},
12 : { code: 12,
symbol: "EXIT_KILL",
longText: "The MongoDB process was killed, on Windows"
},
14 : { code: 14,
symbol: "EXIT_ABRUPT",
longText: "Unspecified unrecoverable error. Exit was not clean"
},
20 : { code: 20,
symbol: "EXIT_NTSERVICE_ERROR",
longText: "Error managing NT Service on Windows"
},
45 : { code: 45,
symbol: "EXIT_FS",
longText: "MongoDB cannot open or obtain a lock on a file"
},
47 : { code: 47,
symbol: "EXIT_CLOCK_SKEW",
longText: "MongoDB exited due to excess clock skew"
},
48 : { code: 48,
symbol: "EXIT_NET_ERROR",
longText: "MongoDB exited because its port was closed, or was already\n" +
"taken by a previous instance of MongoDB"
},
100 : { code: 100,
symbol: "EXIT_UNCAUGHT",
longText: "MongoDB had an unspecified uncaught exception.\n" +
"Check to make sure that MongoDB is able to write to its database directory."
}
};
_.each(exports.Codes, function (value) {
exports[value.symbol] = value;
});

View File

@@ -1,10 +1,10 @@
var fs = require("fs");
var path = require("path");
var spawn = require('child_process').spawn;
var child_process = require('child_process');
var files = require(path.join(__dirname, '..', 'lib', 'files.js'));
var _ = require(path.join('..', 'lib', 'third', 'underscore.js'));
var _ = require('underscore');
/** Internal.
@@ -15,37 +15,34 @@ var _ = require(path.join('..', 'lib', 'third', 'underscore.js'));
*/
var find_mongo_pids = function (app_dir, port, callback) {
// 'ps ax' should be standard across all MacOS and Linux.
var proc = spawn('ps', ['ax']);
var data = '';
proc.stdout.on('data', function (d) {
data += d;
});
child_process.exec('ps ax',
function (error, stdout, stderr) {
if (error) {
callback({reason: error});
} else if (stderr) {
callback({reason: 'ps produced stderr ' + stderr});
} else {
var pids = [];
proc.on('exit', function (code, signal) {
if (code === 0) {
var pids = [];
_.each(stdout.split('\n'), function (ps_line) {
// matches mongos we start.
var m = ps_line.match(/^\s*(\d+).+mongod .+--port (\d+) --dbpath (.+)(?:\/|\\)\.meteor(?:\/|\\)local(?:\/|\\)db\s*$/);
if (m && m.length === 4) {
var found_pid = parseInt(m[1]);
var found_port = parseInt(m[2]);
var found_path = m[3];
_.each(data.split('\n'), function (ps_line) {
// matches mongos we start.
var m = ps_line.match(/^\s*(\d+).+mongod .+--port (\d+) --dbpath (.+)(?:\/|\\)\.meteor(?:\/|\\)local(?:\/|\\)db\s*$/);
if (m && m.length === 4) {
var found_pid = parseInt(m[1]);
var found_port = parseInt(m[2]);
var found_path = m[3];
if ( (!port || port === found_port) &&
(!app_dir || app_dir === found_path)) {
pids.push({
pid: found_pid, port: found_port, app_dir: found_path});
if ( (!port || port === found_port) &&
(!app_dir || app_dir === found_path)) {
pids.push({
pid: found_pid, port: found_port, app_dir: found_path});
}
}
}
});
});
callback(null, pids);
} else {
callback({reason: 'ps exit code ' + code});
}
});
callback(null, pids);
}
});
};
@@ -131,17 +128,24 @@ var find_mongo_and_kill_it_dead = function (port, callback) {
};
exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callback) {
var handle = {stop: function (callback) { callback(); } };
launch_callback = launch_callback || function () {};
on_exit_callback = on_exit_callback || function () {};
// If we are passed an external mongo, assume it is launched and never
// exits. Matches code in run.js:exports.run.
// Since it is externally managed, asking it to actually stop would be
// impolite, so our stoppable handle is a noop
if (process.env.MONGO_URL) {
launch_callback();
return;
return handle;
}
var mongod_path = path.join(files.get_dev_bundle(), 'mongodb', 'bin', 'mongod');
var mongod_path = path.join(files.get_dev_bundle(),
'mongodb',
'bin',
'mongod');
// store data in app_dir
var data_path = path.join(app_dir, '.meteor', 'local', 'db');
@@ -155,21 +159,21 @@ exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callbac
return;
}
var proc = spawn(mongod_path, [
var proc = child_process.spawn(mongod_path, [
'--bind_ip', '127.0.0.1',
'--smallfiles',
'--port', port,
'--dbpath', data_path
]);
handle.stop = function (callback) {
var tries = 0;
var exited = false;
proc.removeListener('exit', on_exit_callback);
proc.kill('SIGINT');
callback && callback(err);
};
proc.on('exit', function (code, signal) {
on_exit_callback(code, signal);
});
// proc.stderr.setEncoding('utf8');
// proc.stderr.on('data', function (data) {
// process.stdout.write(data);
// });
proc.on('exit', on_exit_callback);
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', function (data) {
@@ -178,6 +182,5 @@ exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callbac
launch_callback();
});
});
return handle;
};

View File

@@ -1,5 +1,5 @@
var path = require('path');
var _ = require(path.join(__dirname, 'third', 'underscore.js'));
var _ = require('underscore');
var files = require(path.join(__dirname, 'files.js'));
var fs = require('fs');
@@ -36,14 +36,14 @@ var Package = function () {
// package metadata, from describe()
self.metadata = {};
self.on_use = null;
self.on_test = null;
self.on_use_handler = null;
self.on_test_handler = null;
// registered source file handlers
self.extensions = {};
// functions that can be called when the package is scanned
self.api = {
self.declarationFuncs = {
// keys
// - summary: for 'meteor list'
// - internal: if true, hide in list
@@ -60,15 +60,15 @@ var Package = function () {
},
on_use: function (f) {
if (self.on_use)
if (self.on_use_handler)
throw new Error("A package may have only one on_use handler");
self.on_use = f;
self.on_use_handler = f;
},
on_test: function (f) {
if (self.on_test)
if (self.on_test_handler)
throw new Error("A package may have only one on_test handler");
self.on_test = f;
self.on_test_handler = f;
},
register_extension: function (extension, callback) {
@@ -86,10 +86,10 @@ _.extend(Package.prototype, {
self.name = name;
self.source_root = files.get_package_dir(name);
self.serve_root = path.join(path.sep, 'packages', name);
if (!self.source_root)
throw new Error("The package named " + self.name + " does not exist.");
var fullpath = path.join(self.source_root, 'package.js');
var code = fs.readFileSync(fullpath).toString();
// \n is necessary in case final line is a //-comment
@@ -104,7 +104,7 @@ _.extend(Package.prototype, {
// 'templating' use this to load other code to run at
// bundle-time. and to pull in, eg, 'fs' and 'path' to access
// the file system
func(self.api, require);
func(self.declarationFuncs, require);
},
init_from_app_dir: function (app_dir, ignore_files) {
@@ -124,7 +124,7 @@ _.extend(Package.prototype, {
});
};
self.api.on_use(function (api) {
self.declarationFuncs.on_use(function (api) {
// -- Packages --
// standard client packages (for now), for the classic meteor
@@ -140,7 +140,7 @@ _.extend(Package.prototype, {
api.add_files(sources_except(api, "client"), "server");
});
self.api.on_test(function (api) {
self.declarationFuncs.on_test(function (api) {
api.use(self);
api.add_files(sources_except(api, "server", true), "client");
api.add_files(sources_except(api, "client", true), "server");
@@ -202,7 +202,7 @@ _.extend(Package.prototype, {
self.source_root = null;
self.serve_root = null;
self.api.on_test(function (api) {
self.declarationFuncs.on_test(function (api) {
_.each(fs.readdirSync(collection_dir), function (name) {
// only take things that are actually packages
if (files.is_package_dir(path.join(collection_dir, name)))
@@ -269,13 +269,13 @@ var packages = module.exports = {
// a package object.
list: function () {
var ret = {};
_.each(files.get_package_dirs(), function(dir) {
_.each(fs.readdirSync(dir), function (name) {
// skip .meteor directory
if (fs.existsSync(path.join(dir, name, 'package.js')))
ret[name] = packages.get(name);
});
});
})
return ret;

View File

@@ -1,6 +1,6 @@
var fs = require('fs');
var path = require('path');
var _ = require(path.join(__dirname, 'third', 'underscore.js'));
var _ = require('underscore');
var project = module.exports = {

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,8 @@
exports.CURRENT_VERSION = "0.5.2";
// During automated QA of the updater, modify this file to set testingUpdater to
// true. This will make it act as if it is at version 0.1.0 and use test URLs
// for update checks.
var testingUpdater = false;
exports.CURRENT_VERSION = testingUpdater ? "0.1.0" : "0.5.3";
var fs = require("fs");
var http = require("http");
@@ -8,11 +12,10 @@ var semver = require("semver");
var files = require(path.join(__dirname, 'files.js'));
var manifest_options = {
/* uncomment for testing
var manifest_options = testingUpdater ? {
host: 's3.amazonaws.com',
path: '/com.meteor.static/test/update/manifest.json'
*/
} : {
host: 'update.meteor.com',
path: '/manifest.json'
};

View File

@@ -10,7 +10,7 @@ var request = require('request');
var qs = require('querystring');
var path = require('path');
var files = require(path.join(__dirname, '..', 'lib', 'files.js'));
var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js'));
var _ = require('underscore');
var keypress = require('keypress');
var child_process = require('child_process');
@@ -33,10 +33,15 @@ if (process.env.EMACS == "t") {
// interactively prompt for here.
var meteor_rpc = function (rpc_name, method, site, query_params, callback) {
var url = "https://" + DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site;
var url;
if (DEPLOY_HOSTNAME.indexOf("http://") === 0)
url = DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site;
else
url = "https://" + DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site;
if (!_.isEmpty(query_params))
if (!_.isEmpty(query_params)) {
url += '?' + qs.stringify(query_params);
}
var r = request({method: method, url: url}, function (error, response, body) {
if (error || ((response.statusCode !== 200)
@@ -51,26 +56,39 @@ var meteor_rpc = function (rpc_name, method, site, query_params, callback) {
};
var deploy_app = function (url, app_dir, opt_debug, opt_tests,
opt_set_password) {
opt_set_password, settings) {
var parsed_url = parse_url(url);
// a bit contorted here to make sure we ask for the password before
// launching the slow bundle process.
with_password(parsed_url.hostname, function (password) {
var deployOptions = {
site: parsed_url.hostname,
appDir: app_dir,
debug: opt_debug,
tests: opt_tests,
password: password,
settings: settings
};
if (opt_set_password)
get_new_password(function (set_password) {
bundle_and_deploy(parsed_url.hostname, app_dir, opt_debug, opt_tests,
password, set_password);
deployOptions.setPassword = set_password;
bundle_and_deploy(deployOptions);
});
else
bundle_and_deploy(parsed_url.hostname, app_dir, opt_debug, opt_tests,
password);
bundle_and_deploy(deployOptions);
});
};
var bundle_and_deploy = function (site, app_dir, opt_debug, opt_tests,
password, set_password) {
var bundle_and_deploy = function (options) {
var site = options.site;
var app_dir = options.appDir;
var opt_debug = options.debug;
var opt_tests = options.tests;
var password = options.password;
var set_password = options.setPassword;
var settings = options.settings;
var build_dir = path.join(app_dir, '.meteor', 'local', 'build_tar');
var bundle_path = path.join(build_dir, 'bundle');
var bundle_opts = { skip_dev_bundle: true, no_minify: !!opt_debug,
@@ -90,14 +108,17 @@ var bundle_and_deploy = function (site, app_dir, opt_debug, opt_tests,
process.stdout.write('uploading ... ');
var opts = {};
if (password) opts.password = password;
if (set_password) opts.set_password = set_password;
var rpcOptions = {};
if (password) rpcOptions.password = password;
if (set_password) rpcOptions.set_password = set_password;
// When it hits the wire, all these opts will be URL-encoded.
if (settings !== undefined) rpcOptions.settings = settings;
var tar = child_process.spawn(
'tar', ['czf', '-', 'bundle'], {cwd: build_dir});
var rpc = meteor_rpc('deploy', 'POST', site, opts, function (err, body) {
var rpc = meteor_rpc('deploy', 'POST', site, rpcOptions, function (err, body) {
if (err) {
var errorMessage = (body || ("Connection error (" + err.message + ")"));
process.stderr.write("\nError deploying application: " + errorMessage + "\n");
@@ -105,15 +126,34 @@ var bundle_and_deploy = function (site, app_dir, opt_debug, opt_tests,
}
process.stdout.write('done.\n');
process.stdout.write('Now serving at ' + site + '\n');
var hostname = null;
var response = null;
try {
response = JSON.parse(body);
} catch (e) {
// ... leave null
}
if (response && response.url) {
var url = require('url').parse(response.url);
if (url && url.hostname)
hostname = url.hostname;
}
if (!hostname) {
process.stdout.write('Error receiving hostname from deploy server.\n');
process.exit(1);
}
process.stdout.write('Now serving at ' + hostname + '\n');
files.rm_recursive(build_dir);
if (!site.match('meteor.com')) {
if (hostname && !hostname.match(/meteor\.com$/)) {
var dns = require('dns');
dns.resolve(site, 'CNAME', function (err, cnames) {
dns.resolve(hostname, 'CNAME', function (err, cnames) {
if (err || cnames[0] !== 'origin.meteor.com') {
dns.resolve(site, 'A', function (err, addresses) {
dns.resolve(hostname, 'A', function (err, addresses) {
if (err || addresses[0] !== '107.22.210.133') {
process.stdout.write('-------------\n');
process.stdout.write("You've deployed to a custom domain.\n");
@@ -207,9 +247,6 @@ var parse_url = function (url) {
delete parsed.host; // we use hostname
if (parsed.hostname && !parsed.hostname.match(/\./))
parsed.hostname += '.meteor.com';
if (!parsed.hostname) {
process.stdout.write(
"Please specify a domain to connect to, such as www.example.com or\n" +

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,13 @@ try {
// XXX can't get this from updater.js because in 0.3.7 and before the
// updater didn't have the right NODE_PATH set. At some point we can
// remove this and just use updater.CURRENT_VERSION.
var VERSION = "0.5.2";
var VERSION = "0.5.3";
var fs = require('fs');
var path = require('path');
var files = require(path.join(__dirname, "..", "lib", "files.js"));
var _ = require(path.join(__dirname, "..", "lib", "third", "underscore.js"));
var _ = require('underscore');
var topDir = files.get_dev_bundle();
var changelogPath = path.join(topDir, 'History.md');

View File

@@ -10,10 +10,12 @@ var files = require(path.join(__dirname, '..', 'lib', 'files.js'));
var updater = require(path.join(__dirname, '..', 'lib', 'updater.js'));
var bundler = require(path.join(__dirname, '..', 'lib', 'bundler.js'));
var mongo_runner = require(path.join(__dirname, '..', 'lib', 'mongo_runner.js'));
var mongoExitCodes = require(path.join(__dirname, '..', 'lib', 'mongo_exit_codes.js'));
var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js'));
var _ = require('underscore');
////////// Globals //////////
//XXX: Refactor to not have globals anymore?
// list of log objects from the child process.
var server_log = [];
@@ -23,18 +25,42 @@ var Status = {
crashing: false, // does server crash whenever we start it?
listening: false, // do we expect the server to be listening now.
counter: 0, // how many crashes in rapid succession
code: 0, // exit code last returned
shouldRestart: true, // true if we should be restarting the server
shuttingDown: false, // true if we're on the way to shutting down the server
exitNow: function () {
var self = this;
log_to_clients({'exit': "Your application is exiting."});
self.shuttingDown = true;
self.mongoHandle && self.mongoHandle.stop(function (err) {
if (err)
process.stdout.write(err.reason + "\n");
process.exit(self.code);
});
},
reset: function () {
this.crashing = false;
this.counter = 0;
},
hard_crashed: function () {
var self = this;
if (!self.shouldRestart) {
self.exitNow();
return;
}
log_to_clients({'exit': "Your application is crashing. Waiting for file change."});
this.crashing = true;
},
soft_crashed: function () {
var self = this;
if (!self.shouldRestart) {
self.exitNow();
return;
}
if (this.counter === 0)
setTimeout(function () {
this.counter = 0;
@@ -48,6 +74,18 @@ var Status = {
}
};
// Parse out s as if it were a bash command line.
var bashParse = function (s) {
if (s.search("\"") !== -1 || s.search("'") !== -1) {
throw new Error("Meteor cannot currently handle quoted NODE_OPTIONS");
}
return _.without(s.split(/\s+/), '');
};
var getNodeOptionsFromEnvironment = function () {
return bashParse(process.env.NODE_OPTIONS || "");
};
// List of queued requests. Each item in the list is a function to run
// when the inner app is ready to receive connections.
var request_queue = [];
@@ -167,19 +205,48 @@ var log_to_clients = function (msg) {
};
////////// Launch server process //////////
// Takes options:
// bundlePath
// outerPort
// innerPort
// mongoURL
// onExit
// [onListen]
// [nodeOptions]
// [runOnce]: boolean; default false; if true doesn't ever try to restart, and
// forwards server exit code.
// [settingsFile]
var start_server = function (bundle_path, outer_port, inner_port, mongo_url,
on_exit_callback, on_listen_callback) {
var start_server = function (options) {
// environment
options = _.extend({
runOnce: false,
nodeOptions: []
}, options);
if (options.runOnce) {
Status.shouldRestart = false;
}
var env = {};
for (var k in process.env)
env[k] = process.env[k];
env.PORT = inner_port;
env.MONGO_URL = mongo_url;
env.ROOT_URL = env.ROOT_URL || ('http://localhost:' + outer_port);
env.PORT = options.innerPort;
env.MONGO_URL = options.mongoURL;
env.ROOT_URL = env.ROOT_URL || ('http://localhost:' + options.outerPort);
if (options.settingsFile) {
// Re-read the settings file each time we call start_server.
var settings = exports.getSettings(options.settingsFile);
if (settings)
env.METEOR_SETTINGS = settings;
}
var nodeOptions = _.clone(options.nodeOptions);
nodeOptions.push(path.join(options.bundlePath, 'main.js'));
nodeOptions.push('--keepalive');
var proc = spawn(process.execPath,
[path.join(bundle_path, 'main.js'), '--keepalive'],
nodeOptions,
{env: env});
// XXX deal with test server logging differently?!
@@ -192,7 +259,7 @@ var start_server = function (bundle_path, outer_port, inner_port, mongo_url,
// string must match server.js
data = data.replace(/^LISTENING\s*(?:\n|$)/m, '');
if (data.length != originalLength)
on_listen_callback && on_listen_callback();
options.onListen && options.onListen();
if (data)
log_to_clients({stdout: data});
});
@@ -209,7 +276,7 @@ var start_server = function (bundle_path, outer_port, inner_port, mongo_url,
log_to_clients({'exit': 'Exited with code: ' + code});
}
on_exit_callback();
options.onExit(code);
});
// this happens sometimes when we write a keepalive after the app is
@@ -245,8 +312,11 @@ var kill_server = function (handle) {
////////// Watching dependencies //////////
// deps is the data from dependencies.json in the bundle
// app_dir is the root of the app
// relativeFiles are any other files to watch, relative to the current
// directory (eg, the --settings file)
// on_change is only fired once
var DependencyWatcher = function (deps, app_dir, on_change) {
var DependencyWatcher = function (deps, app_dir, relativeFiles, on_change) {
var self = this;
self.app_dir = app_dir;
@@ -279,6 +349,10 @@ var DependencyWatcher = function (deps, app_dir, on_change) {
});
};
_.each(relativeFiles, function (file) {
self.specific_files[file] = true;
});
// Things that are never interesting.
self.exclude_patterns = _.map((deps.exclude || []), function (pattern) {
return new RegExp(pattern);
@@ -310,7 +384,7 @@ _.extend(DependencyWatcher.prototype, {
return false;
try {
var stats = fs.lstatSync(filepath)
var stats = fs.lstatSync(filepath);
} catch (e) {
// doesn't exist -- leave stats undefined
}
@@ -444,12 +518,34 @@ var start_update_checks = function () {
///////////////////////////////////////////////////////////////////////////////
// Also used by "meteor deploy" in meteor.js.
exports.getSettings = function (filename) {
var str;
try {
str = fs.readFileSync(filename, "utf8");
} catch (e) {
throw new Error("Could not find settings file " + filename);
}
if (str.length > 0x10000) {
throw new Error("Settings file must be less than 64 KB long");
}
// Ensure that the string is parseable in JSON, but there's
// no reason to use the object value of it yet.
if (str.match(/\S/)) {
JSON.parse(str);
return str;
} else {
return "";
}
};
// XXX leave a pidfile and check if we are already running
// This function never returns and will call process.exit() if it
// can't continue. If you change this, remember to call
// watcher.destroy() as appropriate.
exports.run = function (app_dir, bundle_opts, port) {
exports.run = function (app_dir, bundle_opts, port, once, settingsFile) {
var outer_port = port || 3000;
var inner_port = outer_port + 1;
var mongo_port = outer_port + 2;
@@ -462,6 +558,7 @@ exports.run = function (app_dir, bundle_opts, port) {
var test_mongo_url = "mongodb://127.0.0.1:" + mongo_port + "/meteor_test";
var test_bundle_opts;
if (files.is_app_dir(app_dir)) {
// If we're an app, make separate test_bundle_opts to trigger a
// separate runner.
@@ -483,11 +580,19 @@ exports.run = function (app_dir, bundle_opts, port) {
var watcher;
var start_watching = function () {
if (!Status.shouldRestart)
return;
if (deps_info) {
if (watcher)
watcher.destroy();
watcher = new DependencyWatcher(deps_info, app_dir, function () {
var relativeFiles;
if (settingsFile) {
relativeFiles = [settingsFile];
}
watcher = new DependencyWatcher(deps_info, app_dir, relativeFiles,
function () {
if (Status.crashing)
log_to_clients({'system': "=> Modified -- restarting."});
Status.reset();
@@ -546,21 +651,30 @@ exports.run = function (app_dir, bundle_opts, port) {
start_watching();
Status.running = true;
server_handle = start_server(
bundle_path, outer_port, inner_port, mongo_url,
function () {
server_handle = start_server({
bundlePath: bundle_path,
outerPort: outer_port,
innerPort: inner_port,
mongoURL: mongo_url,
onExit: function (code) {
// on server exit
Status.running = false;
Status.listening = false;
Status.code = code;
Status.soft_crashed();
if (!Status.crashing)
restart_server();
}, function () {
},
onListen: function () {
// on listen
Status.listening = true;
_.each(request_queue, function (f) { f(); });
request_queue = [];
});
},
nodeOptions: getNodeOptionsFromEnvironment(),
runOnce: once,
settingsFile: settingsFile
});
// launch test bundle and server if needed.
@@ -574,12 +688,16 @@ exports.run = function (app_dir, bundle_opts, port) {
});
files.rm_recursive(test_bundle_path);
} else {
test_server_handle = start_server(
test_bundle_path, test_port, test_mongo_url, function () {
test_server_handle = start_server({
bundlePath: test_bundle_path,
outerPort: test_port,
innerPort: test_port,
mongoURL: test_mongo_url,
onExit: function (code) {
// No restarting or crash loop prevention on the test server
// for now. We'll see how annoying it is.
log_to_clients({'system': "Test server crashed."});
});
}});
}
};
};
@@ -590,7 +708,7 @@ exports.run = function (app_dir, bundle_opts, port) {
var mongo_startup_print_timer;
var process_startup_printer;
var launch = function () {
mongo_runner.launch_mongo(
Status.mongoHandle = mongo_runner.launch_mongo(
app_dir,
mongo_port,
function () { // On Mongo startup complete
@@ -607,13 +725,22 @@ exports.run = function (app_dir, bundle_opts, port) {
restart_server();
},
function (code, signal) { // On Mongo dead
if (Status.shuttingDown) {
return;
}
console.log("Unexpected mongo exit code " + code + ". Restarting.");
// if mongo dies 3 times with less than 5 seconds between each,
// declare it failed and die.
mongo_err_count += 1;
if (mongo_err_count >= 3) {
console.log("Can't start mongod. Check for other processes listening on port " + mongo_port + " or other meteors running in the same project.");
var explanation = mongoExitCodes.Codes[code];
console.log("Can't start mongod\n");
if (explanation)
console.log(explanation.longText);
if (explanation === mongoExitCodes.EXIT_NET_ERROR)
console.log("\nCheck for other processes listening on port " + mongo_port +
"\nor other meteors running in the same project.");
process.exit(1);
}
if (mongo_err_timer)

View File

@@ -10,7 +10,7 @@ var ProgressBar = require('progress');
var updater = require(path.join(__dirname, "..", "lib", "updater.js"));
var files = require(path.join(__dirname, "..", "lib", "files.js"));
var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js'));
var _ = require('underscore');
// refuse to update if we're in a git checkout.
if (files.in_checkout()) {

View File

@@ -12,9 +12,7 @@ var mime = require('mime');
var handlebars = require('handlebars');
var useragent = require('useragent');
// this is a copy of underscore that will be shipped just for use by
// this file, server.js.
var _ = require(path.join(__dirname, 'underscore.js'));
var _ = require('underscore');
// This code is duplicated in app/server/server.js.
var MIN_NODE_VERSION = 'v0.8.11';

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,8 @@ put on the screen.
{{> api_box absoluteUrl}}
{{> api_box settings}}
<h2 id="publishandsubscribe"><span>Publish and subscribe</span></h2>
These functions control how Meteor servers publish sets of records and
@@ -526,11 +528,9 @@ In this release, Minimongo has some limitations:
* `$elemMatch` is not supported in selectors.
* `$pull` in modifiers can only accept certain kinds
of selectors.
* In selectors, dot notation and ordinal indexing may not work correctly.
* In selectors, dot notation may not work correctly.
* `$` to denote the matched array position is not
supported in modifier.
* Sort does not support subkeys (you can sort on `a`,
but not `a.b`).
* `findAndModify`, upsert, aggregate functions, and
map/reduce aren't supported.
* The supported types are String, Number, Boolean, Array,
@@ -1031,8 +1031,8 @@ it's up to you to be sure.
{{#api_box_inline fieldspecifiers}}
On the server, queries can specify a particular set of fields to include
or exclude from the result object. Minimongo ignores the field
specifier.
or exclude from the result object. (The field specifier is currently
ignored on the client.)
To exclude certain fields from the result objects, the field specifier
is a dictionary whose keys are field names and whose values are `0`.
@@ -1337,6 +1337,9 @@ Example:
facebook: ['user_likes'],
github: ['user', 'repo']
},
requestOfflineToken: {
google: true
},
passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL'
});
@@ -2449,6 +2452,35 @@ If `MAIL_URL` is not set (eg, when running your application locally),
You must provide the `from` option and at least one of `to`, `cc`, and `bcc`;
all other options are optional.
`Email.send` only works on the server. Here is an example of how a
client could use a server method call to send an email. (In an actual
application, you'd need to be careful to limit the emails that a
client could send, to prevent your server from being used as a relay
by spammers.)
// In your server code: define a method that the client can call
Meteor.methods({
sendEmail: function (to, from, subject, text) {
// Let other method calls from the same client start running,
// without waiting for the email sending to complete.
this.unblock();
Email.send({
to: to,
from: from,
subject: subject,
text: text
});
}
});
// In your client code: asynchronously send an email
Meteor.call('sendEmail',
'alice@example.com',
'bob@example.com',
'Hello from Meteor!',
'This is a test of Email.send.');
{{/better_markdown}}
</template>

View File

@@ -54,6 +54,17 @@ Template.api.absoluteUrl = {
]
};
Template.api.settings = {
id: "meteor_settings",
name: "Meteor.settings",
locus: "Server",
descr: ["`Meteor.settings` contains any deployment-specific options that were " +
"provided using the `--settings` option for `meteor run` or `meteor deploy`. " +
"If you provide the `--settings` option, `Meteor.settings` will be the " +
"JSON object in the file you specify. Otherwise, `Meteor.settings` will " +
"be an empty object."]
};
Template.api.publish = {
id: "meteor_publish",
name: "Meteor.publish(name, func)",
@@ -363,10 +374,10 @@ Template.api.find = {
{name: "fields",
type: "Object: field specifier",
type_link: "fieldspecifiers",
descr: "Dictionary of fields to return or exclude."},
descr: "(Server only) Dictionary of fields to return or exclude."},
{name: "reactive",
type: "Boolean",
descr: "Default `true`; pass `false` to disable reactivity"}
descr: "(Client only) Default `true`; pass `false` to disable reactivity"}
]
};
@@ -392,10 +403,10 @@ Template.api.findone = {
{name: "fields",
type: "Object: field specifier",
type_link: "fieldspecifiers",
descr: "Dictionary of fields to return or exclude."},
descr: "(Server only) Dictionary of fields to return or exclude."},
{name: "reactive",
type: "Boolean",
descr: "Default true; pass false to disable reactivity"}
descr: "(Client only) Default true; pass false to disable reactivity"}
]
};
@@ -791,6 +802,11 @@ Template.api.loginWithExternalService = {
name: "requestPermissions",
type: "Array of Strings",
descr: "A list of permissions to request from the user."
},
{
name: "requestOfflineToken",
type: "Boolean",
descr: "If true, asks the user for permission to act on their behalf when offline. This stores an additional offline token in the `services` field of the user document. Currently only supported with Google."
}
]
};
@@ -827,6 +843,11 @@ Template.api.accounts_ui_config = {
type: "Object",
descr: "Which [permissions](#requestpermissions) to request from the user for each external service."
},
{
name: "requestOfflineToken",
type: "Object",
descr: "To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details."
},
{
name: "passwordSignupFields",
type: "String",

View File

@@ -29,6 +29,9 @@ required.
This is the default command. Simply running `meteor` is the
same as `meteor run`.
To pass additional options to Node.js use the `NODE_OPTIONS` environment variable.
For example: `NODE_OPTIONS='--debug'` or `NODE_OPTIONS='--debug-brk'`
Run `meteor help run` to see the full list of options.
@@ -90,6 +93,17 @@ domain like myapp.com, you'll need a DNS A record, matching the IP
address of origin.meteor.com.
{{/warning}}
You can add information specific to a particular deployment of your application
by using the `--settings` option. The argument to `--settings` is a file
containing any JSON string. The object in your settings file will appear on the
server side of your application in [`Meteor.settings`](#meteor_settings).
Settings are persistent. When you redeploy your app, the old value will be
preserved unless you explicitly pass new settings using the `--settings` option.
To unset `Meteor.settings`, pass an empty settings file.
<h3 id="meteorlogs">meteor logs <i>site</i></h3>
Retrieves the server logs for the named Meteor application.

View File

@@ -43,6 +43,7 @@ pre {
code {
font-family: monospace;
font-size: 1.1em;
white-space: pre;
}
ul {

View File

@@ -11,7 +11,7 @@
</div>
<div id="main">
<div id="top"></div>
<h1 class="main-headline">Meteor 0.5.2</h1>
<h1 class="main-headline">Meteor 0.5.3</h1>
{{> introduction }}
{{> concepts }}
{{> api }}

View File

@@ -1,4 +1,4 @@
METEOR_VERSION = "0.5.2";
METEOR_VERSION = "0.5.3";
Meteor.startup(function () {
// XXX this is broken by the new multi-page layout. Also, it was
@@ -94,7 +94,8 @@ var toc = [
"Meteor.isClient",
"Meteor.isServer",
"Meteor.startup",
"Meteor.absoluteUrl"
"Meteor.absoluteUrl",
"Meteor.settings"
],
"Publish and subscribe", [

View File

@@ -44,7 +44,7 @@ input.chosen {
.map {
position: relative;
background-image: url('/soma.jpeg');
background-image: url('/soma.png');
background-position: -20px -20px;
width: 500px;
height: 500px;

View File

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 288 KiB

View File

@@ -7,7 +7,7 @@
</body>
<template name="radio">
<span class="radio"><input id="{{key}}:{{value}}" {{maybeChecked}} type="radio" name="{{key}}" value="{{value}}" />{{! no whitespace}}<label for="{{key}}:{{value}}">{{label}}</label></span>
<span class="radio"><input id="{{key}}:{{value}}" {{{maybeChecked}}} type="radio" name="{{key}}" value="{{value}}" />{{! no whitespace}}<label for="{{key}}:{{value}}">{{label}}</label></span>
</template>
<template name="button">

View File

@@ -1,58 +0,0 @@
// Parameters for simulation:
//
// Each document is randomly placed in a collection, with a random
// 'bucket' field. Clients sub to 1 bucket in each collection.
//
// - numCollections
// how many collections to spread the documents over
// - numBuckets
// number of buckets per collection.
//
// - initialDocuments: Inital documents added by the server. Probably
// not usefully combined with maxAgeSeconds
//
// - maxAgeSeconds: How long to leave documents in the database. This,
// combined with all the various rates, determines the steady state
// database size. In seconds. falsy to disable.
//
// Per-client action rates:
// - insertsPerSecond
// - updatesPerSecond
// - removesPerSecond
//
// - documentSize: bytes of randomness per document.
// // XXX make this a random distribution?
// - documentNumFields: how many fields of randomness per document.
//
// XXX also max documents? (count and remove N)
SCENARIOS = {
default: {
numCollections: 1,
numBuckets: 3,
initialDocuments: 1,
maxAgeSeconds: 60,
insertsPerSecond: 1,
updatesPerSecond: 1,
removesPerSecond: 0.1,
documentSize: 1024,
documentNumFields: 8
},
nodata: {
numCollections: 1,
numBuckets: 1,
initialDocuments: 0
},
bigdata: {
numCollections: 1,
numBuckets: 1,
initialDocuments: 1024,
updatesPerSecond: 1,
documentSize: 10240,
documentNumFields: 16
}
};

View File

@@ -2,14 +2,14 @@
// to change this. See 'benchmark-scenarios.js' for the list of
// scenarios.
var PARAMS = {};
if (Meteor.isServer) {
if (process.env.SCENARIO)
__meteor_runtime_config__.SCENARIO = process.env.SCENARIO;
else
__meteor_runtime_config__.SCENARIO = 'default';
if (!Meteor.settings.params)
throw new Error("Must set scenario with Meteor.settings");
__meteor_runtime_config__.PARAMS = PARAMS = Meteor.settings.params;
} else {
PARAMS = __meteor_runtime_config__.PARAMS;
}
var PARAMS = SCENARIOS[__meteor_runtime_config__.SCENARIO];
//////////////////////////////
// Helper Functions

View File

@@ -12,13 +12,15 @@ PROJDIR=`dirname $0`
cd "$PROJDIR"
PROJDIR=`pwd`
SCENARIO="${1:-default}"
# clean up from previous runs
# XXX this is gross!
pkill -f "$PROJDIR/.meteor/local/db" || true
../../../meteor reset || true
# start the benchmark app
../../../meteor --production --port 9000 &
../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port 9000 &
OUTER_PID=$!

View File

@@ -0,0 +1,27 @@
Parameters for simulation:
Each document is randomly placed in a collection, with a random
'bucket' field. Clients sub to 1 bucket in each collection.
- numCollections
how many collections to spread the documents over
- numBuckets
number of buckets per collection.
- initialDocuments: Inital documents added by the server. Probably
not usefully combined with maxAgeSeconds
- maxAgeSeconds: How long to leave documents in the database. This,
combined with all the various rates, determines the steady state
database size. In seconds. falsy to disable.
Per-client action rates:
- insertsPerSecond
- updatesPerSecond
- removesPerSecond
- documentSize: bytes of randomness per document.
// XXX make this a random distribution?
- documentNumFields: how many fields of randomness per document.
XXX also max documents? (count and remove N)

View File

@@ -0,0 +1,9 @@
{
"params": {
"numCollections": 1,
"numBuckets": 1,
"initialDocuments": 1024,
"documentSize": 2048,
"documentNumFields": 64
}
}

View File

@@ -0,0 +1,10 @@
{
"params": {
"numCollections": 1,
"numBuckets": 1,
"initialDocuments": 1024,
"updatesPerSecond": 0.2,
"documentSize": 1024,
"documentNumFields": 32
}
}

View File

@@ -0,0 +1,13 @@
{
"params": {
"numCollections": 1,
"numBuckets": 3,
"initialDocuments": 1,
"maxAgeSeconds": 60,
"insertsPerSecond": 1,
"updatesPerSecond": 1,
"removesPerSecond": 0.1,
"documentSize": 1024,
"documentNumFields": 8
}
}

View File

@@ -0,0 +1,7 @@
{
"params": {
"numCollections": 1,
"numBuckets": 1,
"initialDocuments": 0
}
}

View File

@@ -0,0 +1 @@
local

View File

@@ -0,0 +1,6 @@
# 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.
autopublish

View File

@@ -0,0 +1,42 @@
p, .topnotes .nodespec { margin: 0.8em; }
.topnotes {
background: #ffd;
margin-bottom: 2em;
margin-left: 1em;
margin-right: 1em;
border: 2px solid #ccc;
}
.str { font-weight: bold; }
.token { font-family: monospace; font-size: 110%; font-weight: bold; }
.token, .tokentype { background: #ddd; padding: 2px 5px; }
.tokentype { font-size: 85%; }
.ref { font-style: italic; }
.punc { font-size: 140%; }
.comma { color: #fff; }
.or { padding-left: 3px; padding-right: 3px; }
.nodespec {
font-size: 16px;
margin: 1em 0;
line-height: 20px;
}
.indent {
margin-left: 2em;
}
.explan {
margin-left: 20em;
margin-right: 2em;
border: 1px solid #ccc;
}
#page { max-width: 50em; margin: 0 auto; }
.spacer { text-align: center; font-size:50%; font-weight: bold; border-top: 1px solid #999;
margin-left: 6em; margin-right: 6em;
}
code { font-weight: bold; }

View File

@@ -0,0 +1,279 @@
<head>
<title>jsparse Docs</title>
</head>
<body>
<div id="page">
{{> page}}
</div>
</body>
<template name="page">
<div class="topnotes">
<p>This is the spec for the JavaScript syntax tree returned by <code>jsparse</code>.</p>
<p>It's notable that <em>every token</em> from the source code appears in order in this syntax tree, which
is a Concrete Syntax Tree (CST) rather than an abstract AST.</p>
{{spacer}}
<p>The tree consists of <strong>nodes</strong>
and <strong>tokens</strong>. A node consists of a name followed
by zero or more child nodes or tokens. A token consists of
one or more characters from the source code.</p>
<p>For example, the following tree, which might have come from parsing <code>1 + 2</code>,
contains nodes named <span class="str">"binary"</span>,
<span class="str">"number"</span>, and <span class="str">"number"</span>
and the tokens <span class="token">1</span>, <span class="token">+</span>,
and <span class="token">2</span>:</p>
<p>{{#nodespec}}["binary", ["number", `1`], `+`, ["number", `2`]]{{/nodespec}}</p>
{{spacer}}
<p>The following notation is used throughout this document to give schematic definitions of
the different types of nodes.</p>
<p>An <span class="ref">italicized</span> word refers to a list of one or more possible
nodes or tokens that could fill a certain spot. For example, the following is the
definition of the <span class="str">"binary"</span> node:</p>
<p>{{#nodespec}}["binary", expression, binaryOp, expression]{{/nodespec}}</p>
<p>The <span class="ref">expression</span> spot could be filled by one of over 20 types
of expression nodes, and if you look up <span class="ref">binaryOp</span> you'll see
it refers to any of a list of binary operator tokens.</p>
<p>A vertical bar (<span class="punc or">|</span>) separates alternatives, of which exactly
one must be present in the tree. An ellipsis (<span class="punc">...</span>) stands
for a sequence of <em>zero or more</em> of the preceding item. For example:</p>
{{#nodespec}}["program", (statement | functionDecl), ...]{{/nodespec}}
<p>This definition says that a program node contains zero or more items, each of which may be either a
<span class="ref">statement</span> or a <span class="ref">functionDecl</span>.</p>
<p>A question mark (<span class="punct">?</span>) indicates that the previous item
(or items enclosed in parentheses) may or may not present.</p>
<p>Token classes like <span class="tokentype">IDENTIFIER</span> match any token in the class. The <span class="tokentype">BOOLEAN</span>, <span class="tokentype">NUMBER</span>, <span class="tokentype">STRING</span>, and <span class="tokentype">REGEX</span> classes are all literals. An example <span class="tokentype">STRING</span> would be <span class="token">"hello"</span>.
{{spacer}}
<p>Some definitions with ellipses may seem strangely permissive, such as:</p>
{{#nodespec}}["varStmnt", `var`, (varDecl | `,`), ..., semi]{{/nodespec}}
<p>This seems to say that <code>var;</code> and <code>var,,x,</code> are valid statements.
The reason for this style of definition is to discourage reliance on exact comma
positions, which may occasionally vary between JavaScript implementations and versions.
For example, the 5th edition of ECMAScript allows a trailing comma in object literals whereas
the 3rd edition doesn't.</p>
<p>For most applications, the commas aren't that important, and you can read all the varDecls
by just skipping any commas you encounter.</p>
</div>
{{#nodespec}}program:{{/nodespec}}
<div class="indent">
{{#nodespec}}["program", (statement | functionDecl), ...]{{/nodespec}}
</div>
{{#nodespec}}statement:{{/nodespec}}
<div class="indent">
{{#nodespec}}["expressionStmnt", expression, semi]{{/nodespec}}
<div class="explan">
<p>Function calls and assignments are expressions.</p>
</div>
{{#nodespec}}["emptyStmnt", `;`]{{/nodespec}}
<div class="explan">
<p>Only an actual <span class="token">;</span> token can create an empty statement.</p>
</div>
{{#nodespec}}["blockStmnt", `{`, statement, ..., `}`]{{/nodespec}}
{{#nodespec}}["varStmnt", `var`, (varDecl | `,`), ..., semi]{{/nodespec}}
<div class="explan">
<p>You can assume at least one varDecl is present.</p>
</div>
{{#nodespec}}["ifStmnt", `if`, `(`, expression, `)`, statement, (`else`, statement)?]{{/nodespec}}
{{#nodespec}}["whileStmnt", `while`, `(`, expression, `)`, statement]{{/nodespec}}
{{#nodespec}}["doStmnt", `do`, statement, `while`, `(`, expression, `)`, semi]{{/nodespec}}
{{#nodespec}}["forStmnt", `for`, `(`, forSpec, `)`, statement]{{/nodespec}}
{{#nodespec}}["returnStmnt", `return`, (expression | nil), semi]{{/nodespec}}
{{#nodespec}}["continueStmnt", `continue`, (IDENTIFIER | nil), semi]{{/nodespec}}
{{#nodespec}}["breakStmnt", `break`, (IDENTIFIER | nil), semi]{{/nodespec}}
{{#nodespec}}["throwStmnt", `throw`, expression, semi]{{/nodespec}}
{{#nodespec}}["withStmnt", `with`, `(`, expression, `)`, statement]{{/nodespec}}
{{#nodespec}}["switchStmnt", `switch`, `(`, expression, `)`, `{`, (case | default), ..., `}`]{{/nodespec}}
<div class="explan">
<p>There's at most one default clause, but it can be anywhere in the list of cases and defaults.</p>
</div>
{{#nodespec}}["tryStmnt", `try`, statement, (catch | nil), (finally | nil)]{{/nodespec}}
<div class="explan">
<p>The statement is always a blockStmnt.</p>
</div>
{{#nodespec}}["labelStmnt", IDENTIFIER, `:`, statement]{{/nodespec}}
{{#nodespec}}["debuggerStmnt", `debugger`, semi]{{/nodespec}}
</div>
{{#nodespec}}functionDecl:{{/nodespec}}
<div class="indent">
{{#nodespec}}["functionDecl", `function`, IDENTIFIER, `(`, (IDENTIFIER | `,`), ..., `)`, `{`, (statement | functionDecl), ..., `}`]{{/nodespec}}
<div class="explan">
<p>Different from a functionExpr only in that the function name is required and not optional.</p>
</div>
</div>
{{#nodespec}}expression:{{/nodespec}}
<div class="indent">
{{#nodespec}}["this", `this`]{{/nodespec}}
{{#nodespec}}["null", `null`]{{/nodespec}}
{{#nodespec}}["number", NUMBER]{{/nodespec}}
{{#nodespec}}["boolean", BOOLEAN]{{/nodespec}}
{{#nodespec}}["regex", REGEX]{{/nodespec}}
{{#nodespec}}["string", STRING]{{/nodespec}}
{{#nodespec}}["identifier", IDENTIFIER]{{/nodespec}}
{{#nodespec}}["parens", `(`, expression, `)`]{{/nodespec}}
{{#nodespec}}["array", `[`, (expression | `,`), ..., `]`]{{/nodespec}}
<div class="explan">
<p>All commas are significant because of element elision, and any
combination of commas and expressions is possible.
The array <code>[,,,7,,8,,]</code> has 7 and 8 as
its 3rd and 5th elements.</p>
</div>
{{#nodespec}}["object", `{`, (prop | `,`), ..., `}`]{{/nodespec}}
{{#nodespec}}["functionExpr", `function`, (IDENTIFIER | nil), `(`, (IDENTIFIER | `,`), ..., `)`, `{`, (statement | functionDecl), ..., `}`]{{/nodespec}}
{{#nodespec}}["dot", expression, `.`, identifierName]{{/nodespec}}
{{#nodespec}}["bracket", expression, `[`, expression, `]`]{{/nodespec}}
{{#nodespec}}["call", expression, `(`, (expression | `,`), ..., `)`]{{/nodespec}}
{{#nodespec}}["new", `new`, expression]{{/nodespec}}
{{#nodespec}}["newcall", `new`, expression, `(`, (expression | `,`), ..., `)`]{{/nodespec}}
{{#nodespec}}["unary", unaryOp, expression]{{/nodespec}}
{{#nodespec}}["binary", expression, binaryOp, expression]{{/nodespec}}
{{#nodespec}}["postfix", expression, postfixOp]{{/nodespec}}
{{#nodespec}}["ternary", expression, `?`, expression, `:`, expression]{{/nodespec}}
{{#nodespec}}["assignment", expression, assignmentOp, expression]{{/nodespec}}
<div class="explan">
<p>There is no simple constraint on what the first expression can be.
A left-hand-side expression could be a simple identifier like <code>foo</code>,
or it could be any expression ending in a property access, such as <code>foo().bar[baz]</code>.
Parentheses are also allowed around the left-hand side, so surprisingly <code>((x)) = 3</code>
is completely legal and equivalent to <code>x = 3</code>. This is because
JavaScript evaluates the left-hand side all the way down to the final variable reference.</p>
</div>
{{#nodespec}}["comma", (expression | `,`), ...]{{/nodespec}}
<div class="explan"><p>You can assume there are at least two expressions, since there would have been no comma otherwise.</p></div>
</div>
{{#nodespec}}nil:{{/nodespec}}
<div class="indent">
{{#nodespec}}["nil"]{{/nodespec}}
<div class="explan"><p>Serves as a placeholder for optional parts of nodes.</p></div>
</div>
{{#nodespec}}semi:{{/nodespec}}
<div class="indent">
{{#nodespec}}[";"] | `;`{{/nodespec}}
<div class="explan">
<p>An optional semicolon at the end of a statement. Most semicolons in JavaScript can legally
be omitted if a line break follows. If the semicolon was omitted, a <span class="str">";"</span>
node takes its place.</p>
</div>
</div>
{{#nodespec}}prop:{{/nodespec}}
<div class="indent">
{{#nodespec}}["prop", propertyName, `:`, expression]{{/nodespec}}
</div>
{{#nodespec}}propertyName:{{/nodespec}}
<div class="indent">
{{#nodespec}}["idPropName", identifierName]{{/nodespec}}
{{#nodespec}}["strPropName", STRING]{{/nodespec}}
{{#nodespec}}["numPropName", NUMBER]{{/nodespec}}
<div class="explan">
<p>A property name in an object literal can be given as a bare identifier, a quoted string literal,
or a number literal. These nodes indicate which it is.</p>
</div>
</div>
{{#nodespec}}varDecl:{{/nodespec}}
<div class="indent">
{{#nodespec}}["varDecl", IDENTIFIER, (`=`, expression)?]{{/nodespec}}
</div>
{{#nodespec}}forSpec:{{/nodespec}}
<div class="indent">
{{#nodespec}}["forSpec", (expression | nil), `;`, (expression | nil), `;`, (expression | nil)]{{/nodespec}}
{{#nodespec}}["forVarSpec", `var`, (varDecl | `,`), ..., `;`, (expression | nil), `;`, (expression | nil)]{{/nodespec}}
{{#nodespec}}["forInSpec", expression, `in`, expression]{{/nodespec}}
{{#nodespec}}["forVarInSpec", `var`, varDecl, `in`, expression]{{/nodespec}}
<div class="explan">
<p>There are technically four types of for-loops in JavaScript.</p>
<p>These definitions suggest some
odd possibilities, and interestingly, they work. The form <code>for (x.foo in y)</code> will actually
set <code>x.foo</code> each time through the loop, and <code>for (var x = bar() in y)</code> will call
<code>bar()</code> and assign it first thing,
even if <code>x</code> is immediately overwritten before the first iteration of the loop.</p>
<p>The semicolons are mandatory, even at a line break.</p>
</div>
</div>
{{#nodespec}}case:{{/nodespec}}
<div class="indent">
{{#nodespec}}["case", `case`, expression, `:`, statement, ...]{{/nodespec}}
</div>
{{#nodespec}}default:{{/nodespec}}
<div class="indent">
{{#nodespec}}["default", `default`, `:`, statement, ...]{{/nodespec}}
</div>
{{#nodespec}}catch:{{/nodespec}}
<div class="indent">
{{#nodespec}}["catch", `catch`, `(`, IDENTIFIER, `)`, statement]{{/nodespec}}
<div class="explan">
<p>The statement is always a blockStmnt.</p>
</div>
</div>
{{#nodespec}}finally:{{/nodespec}}
<div class="indent">
{{#nodespec}}["finally", `finally`, statement]{{/nodespec}}
<div class="explan">
<p>The statement is always a blockStmnt.</p>
</div>
</div>
{{#nodespec}}identifierName:{{/nodespec}}
<div class="indent">
{{#nodespec}}(IDENTIFIER | KEYWORD | BOOLEAN | `null`){{/nodespec}}
<div class="explan">
<p>As of ECMAScript 5th edition, keywords and reserved words can be used in some places
where an identifier is expected. For example, <code>x.return()</code> or
<code>{true: 'yes'}</code>.</p>
</div>
</div>
{{#nodespec}}unaryOp:{{/nodespec}}
<div class="indent">
{{#nodespec}}(`delete` | `void` | `typeof` | `++` | `--` | `+` | `-` | `~` | `!`){{/nodespec}}
</div>
{{#nodespec}}binaryOp:{{/nodespec}}
<div class="indent">
{{#nodespec}}(`*` | `/` | `%` | `+` | `-` | `<<` | `>>` | `>>>` | `<` | `>` | `<=` | `>=` | `instanceof` | `in` | `==` | `!=` | `===` | `!==` | `&` | `^` | `|` | `&&` | `||`){{/nodespec}}
</div>
{{#nodespec}}postfixOp:{{/nodespec}}
<div class="indent">
{{#nodespec}}(`++` | `--`){{/nodespec}}
</div>
{{#nodespec}}assignmentOp:{{/nodespec}}
<div class="indent">
{{#nodespec}}(`=` | `*=` | `/=` | `%=` | `+=` | `-=` | `<<=` | `>>=` | `>>>=` | `&=` | `^=` | `|=`){{/nodespec}}
</div>
</template>

View File

@@ -0,0 +1,67 @@
if (Meteor.is_client) {
Template.page.nodespec = function (fn) {
var parts = [fn()];
var replaceParts = function(regex, replacementFunc) {
var newParts = [];
_.each(parts, function (part) {
if (typeof part !== 'string') {
newParts.push(part);
return;
}
regex.lastIndex = 0;
var charsTaken = 0;
var matchResult;
while ((matchResult = regex.exec(part))) {
var matchIndex = matchResult.index;
if (matchIndex > charsTaken)
newParts.push(part.substring(charsTaken, matchIndex));
charsTaken = regex.lastIndex;
var replacementParts = replacementFunc(matchResult);
newParts.push.apply(newParts, _.toArray(replacementParts));
}
if (charsTaken < part.length)
newParts.push(part.slice(charsTaken));
});
parts = newParts;
};
parts.unshift(['<div class="nodespec">']);
parts.push(['</div>']);
replaceParts(/".*?"/g, function (match) {
return [['<span class="str">', Handlebars._escape(match[0]), '</span>']];
});
replaceParts(/`(.*?)`/g, function (match) {
return [['<span class="token">', Handlebars._escape(match[1]), '</span>']];
});
replaceParts(/[A-Z]{3,}/g, function (match) {
return [['<span class="tokentype">', Handlebars._escape(match[0]), '</span>']];
});
replaceParts(/[a-z]\w*/g, function (match) {
return [['<span class="ref">', Handlebars._escape(match[0]), '</span>']];
});
replaceParts(/[\[\]()|.,*?]/g, function (match) {
return [['<span class="punc">'], match[0], ['</span>']];
});
replaceParts(/,/g, function (match) {
return [['<span class="comma">'], match[0], ['</span>']];
});
replaceParts(/\|/g, function (match) {
return [['<span class="or">'], match[0], ['</span>']];
});
var html = _.map(parts, function (part) {
if (typeof part === "string")
return Handlebars._escape(part);
return part.join('');
}).join('');
return new Handlebars.SafeString(html);
};
Template.page.spacer = function () {
return new Handlebars.SafeString('<div class="spacer">&nbsp;</div>');
};
}

View File

@@ -60,7 +60,7 @@ Meteor.setInterval(function () {
var idle_threshold = now - 70*1000; // 70 sec
var remove_threshold = now - 60*60*1000; // 1hr
Players.update({$lt: {last_keepalive: idle_threshold}},
Players.update({last_keepalive: {$lt: idle_threshold}},
{$set: {idle: true}});
// XXX need to deal with people coming back!

5
meteor
View File

@@ -1,6 +1,6 @@
#!/bin/bash
BUNDLE_VERSION=0.2.8
BUNDLE_VERSION=0.2.12
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.
@@ -58,6 +58,8 @@ function install_dev_bundle {
test -x "${TMPDIR}/bin/node" # bomb out if it didn't work, eg no net
fi
# Delete old dev bundle and rename the new one on top of it.
rm -rf "$SCRIPT_DIR/dev_bundle"
mv "$TMPDIR" "$SCRIPT_DIR/dev_bundle"
echo "Installed dependency kit v${BUNDLE_VERSION} in dev_bundle."
@@ -77,7 +79,6 @@ if [ -d "$SCRIPT_DIR/.git" ] || [ -f "$SCRIPT_DIR/.git" ]; then
elif [ ! -f "$SCRIPT_DIR/dev_bundle/.bundle_version.txt" ] ||
grep -qvx "$BUNDLE_VERSION" "$SCRIPT_DIR/dev_bundle/.bundle_version.txt" ; then
echo "Your dependency kit is out of date. I will download the new one."
rm -rf "$SCRIPT_DIR/dev_bundle"
install_dev_bundle
fi

View File

@@ -219,7 +219,10 @@
// the profile too
var stampedToken = Accounts._generateStampedLoginToken();
var setAttrs = {};
setAttrs["services." + serviceName] = serviceData;
_.each(serviceData, function(value, key) {
setAttrs["services." + serviceName + "." + key] = value;
});
// XXX Maybe we should re-use the selector above and notice if the update
// touches nothing?
Meteor.users.update(

View File

@@ -32,11 +32,13 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService', function (test)
test.equal(users[0].profile.foo, 1);
test.equal(users[0].profile.bar, undefined);
test.equal(users[0].services.facebook.llama, 50);
test.equal(users[0].services.facebook.monkey, undefined);
// make sure we *don't* lose values not passed this call to
// updateOrCreateUserFromExternalService
test.equal(users[0].services.facebook.monkey, 42);
// cleanup
Meteor.users.remove(uid1);
// users that have different service ids get different users
uid1 = Accounts.updateOrCreateUserFromExternalService(
'weibo', {id: weiboId1}, {profile: {foo: 1}}).id;

View File

@@ -27,7 +27,6 @@
Accounts.oauth.initiateLogin(state, loginUrl, callback);
};
})();

View File

@@ -0,0 +1,3 @@
#login-buttons-image-facebook {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC);
}

View File

@@ -9,7 +9,7 @@ Package.on_use(function(api) {
api.use('templating', 'client');
api.add_files(
['facebook_configure.html', 'facebook_configure.js'],
['facebook_login_button.css', 'facebook_configure.html', 'facebook_configure.js'],
'client');
api.add_files('facebook_common.js', ['client', 'server']);

View File

@@ -0,0 +1,3 @@
#login-buttons-image-github {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=);
}

View File

@@ -9,7 +9,7 @@ Package.on_use(function(api) {
api.use('templating', 'client');
api.add_files(
['github_configure.html', 'github_configure.js'],
['github_login_button.css', 'github_configure.html', 'github_configure.js'],
'client');
api.add_files('github_common.js', ['client', 'server']);

View File

@@ -15,25 +15,25 @@
var state = Meteor.uuid();
// always need this to get user id from google.
var required_scope = ['https://www.googleapis.com/auth/userinfo.profile'];
var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile'];
var scope = ['https://www.googleapis.com/auth/userinfo.email'];
if (options && options.requestPermissions)
scope = options.requestPermissions;
scope = _.union(scope, required_scope);
var flat_scope = _.map(scope, encodeURIComponent).join('+');
scope = _.union(scope, requiredScope);
var flatScope = _.map(scope, encodeURIComponent).join('+');
// Might be good to have a way to set access_type=offline. Need to
// both set it here and store the refresh token on the server.
// https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl
var accessType = options.requestOfflineToken ? 'offline' : 'online';
var loginUrl =
'https://accounts.google.com/o/oauth2/auth' +
'?response_type=code' +
'&client_id=' + config.clientId +
'&scope=' + flat_scope +
'&scope=' + flatScope +
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') +
'&state=' + state;
'&state=' + state +
'&access_type=' + accessType;
Accounts.oauth.initiateLogin(state, loginUrl, callback);
};
}) ();

View File

@@ -7,4 +7,4 @@ Template.configureLoginServiceDialogForGoogle.fields = function () {
{property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client secret'}
];
};
};

View File

@@ -0,0 +1,3 @@
#login-buttons-image-google {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==);
}

View File

@@ -2,20 +2,34 @@
Accounts.oauth.registerService('google', 2, function(query) {
var accessToken = getAccessToken(query);
var response = getTokens(query);
var accessToken = response.accessToken;
var identity = getIdentity(accessToken);
var serviceData = {
id: identity.id,
accessToken: accessToken,
email: identity.email,
expiresAt: (+new Date) + (1000 * response.expiresIn)
};
// only set the token in serviceData if it's there. this ensures
// that we don't lose old ones (since we only get this on the first
// log in attempt)
if (response.refreshToken)
serviceData.refreshToken = response.refreshToken;
return {
serviceData: {
id: identity.id,
accessToken: accessToken,
email: identity.email
},
serviceData: serviceData,
options: {profile: {name: identity.name}}
};
});
var getAccessToken = function (query) {
// returns an object containing:
// - accessToken
// - expiresIn: lifetime of token in seconds
// - refreshToken, if this is the first authorization request
var getTokens = function (query) {
var config = Accounts.loginServiceConfiguration.findOne({service: 'google'});
if (!config)
throw new Accounts.ConfigError("Service not configured");
@@ -33,7 +47,12 @@
throw result.error;
if (result.data.error) // if the http response was a json object with an error attribute
throw result.data;
return result.data.access_token;
return {
accessToken: result.data.access_token,
refreshToken: result.data.refresh_token,
expiresIn: result.data.expires_in
};
};
var getIdentity = function (accessToken) {

View File

@@ -9,7 +9,7 @@ Package.on_use(function(api) {
api.use('templating', 'client');
api.add_files(
['google_configure.html', 'google_configure.js'],
['google_login_button.css', 'google_configure.html', 'google_configure.js'],
'client');
api.add_files('google_common.js', ['client', 'server']);

View File

@@ -51,19 +51,27 @@ OAuth1Binding.prototype.prepareAccessToken = function(query) {
self.accessTokenSecret = tokens.oauth_token_secret;
};
OAuth1Binding.prototype.call = function(method, url) {
OAuth1Binding.prototype.call = function(method, url, params) {
var self = this;
var headers = self._buildHeader({
oauth_token: self.accessToken
});
var response = self._call(method, url, headers);
return response.data;
if(!params) {
params = {};
}
var response = self._call(method, url, headers, params);
return response;
};
OAuth1Binding.prototype.get = function(url) {
return this.call('GET', url);
OAuth1Binding.prototype.get = function(url, params) {
return this.call('GET', url, params);
};
OAuth1Binding.prototype.post = function(url, params) {
return this.call('POST', url, params);
};
OAuth1Binding.prototype._buildHeader = function(headers) {
@@ -77,9 +85,9 @@ OAuth1Binding.prototype._buildHeader = function(headers) {
}, headers);
};
OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, accessTokenSecret) {
OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, accessTokenSecret, params) {
var self = this;
var headers = self._encodeHeader(rawHeaders);
var headers = self._encodeHeader(_.extend(rawHeaders, params));
var parameters = _.map(headers, function(val, key) {
return key + '=' + val;
@@ -87,13 +95,13 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access
var signatureBase = [
method,
encodeURIComponent(url),
encodeURIComponent(parameters)
self._encodeString(url),
self._encodeString(parameters)
].join('&');
var signingKey = encodeURIComponent(self._secret) + '&';
var signingKey = self._encodeString(self._secret) + '&';
if (accessTokenSecret)
signingKey += encodeURIComponent(accessTokenSecret);
signingKey += self._encodeString(accessTokenSecret);
return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64');
};
@@ -102,7 +110,7 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) {
var self = this;
// Get the signature
headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret);
headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret, params);
// Make a authorization string according to oauth1 spec
var authString = self._getAuthHeaderString(headers);
@@ -117,6 +125,8 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) {
if (response.error) {
Meteor._debug('Error sending OAuth1 HTTP call', response.content, method, url, params, authString);
if (response.statusCode) response.error.statusCode = response.statusCode;
if (response.data) response.error.data = response.data;
throw response.error;
}
@@ -124,14 +134,20 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) {
};
OAuth1Binding.prototype._encodeHeader = function(header) {
var self = this;
return _.reduce(header, function(memo, val, key) {
memo[encodeURIComponent(key)] = encodeURIComponent(val);
memo[self._encodeString(key)] = self._encodeString(val);
return memo;
}, {});
};
OAuth1Binding.prototype._encodeString = function(str) {
return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
};
OAuth1Binding.prototype._getAuthHeaderString = function(headers) {
var self = this;
return 'OAuth ' + _.map(headers, function(val, key) {
return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"';
return self._encodeString(key) + '="' + self._encodeString(val) + '"';
}).sort().join(', ');
};

View File

@@ -156,4 +156,3 @@
userCallback: callback});
};
})();

View File

@@ -9,7 +9,7 @@ Package.on_use(function(api) {
api.use('templating', 'client');
api.add_files(
['twitter_configure.html', 'twitter_configure.js'],
['twitter_login_button.css', 'twitter_configure.html', 'twitter_configure.js'],
'client');
api.add_files('twitter_common.js', ['client', 'server']);

View File

@@ -30,5 +30,4 @@
Accounts.oauth.initiateLogin(state, url, callback);
};
})();

View File

@@ -0,0 +1,3 @@
#login-buttons-image-twitter {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=);
}

View File

@@ -1,7 +1,7 @@
(function () {
Accounts.oauth.registerService('twitter', 1, function(oauthBinding) {
var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json');
var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json').data;
return {
serviceData: {

View File

@@ -3,14 +3,15 @@ if (!Accounts.ui)
if (!Accounts.ui._options) {
Accounts.ui._options = {
requestPermissions: {}
requestPermissions: {},
requestOfflineToken: {}
};
}
Accounts.ui.config = function(options) {
// validate options keys
var VALID_KEYS = ['passwordSignupFields', 'requestPermissions'];
var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken'];
_.each(_.keys(options), function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Accounts.ui.config: Invalid key: " + key);
@@ -45,6 +46,20 @@ Accounts.ui.config = function(options) {
}
});
}
// deal with `requestOfflineToken`
if (options.requestOfflineToken) {
_.each(options.requestOfflineToken, function (value, service) {
if (service !== 'google')
throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment.");
if (Accounts.ui._options.requestOfflineToken[service]) {
throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service);
} else {
Accounts.ui._options.requestOfflineToken[service] = value;
}
});
}
};
Accounts.ui._passwordSignupFields = function () {

View File

@@ -1,3 +1,9 @@
// XXX Most of the testing of accounts-ui is done manually, across
// multiple browsers using examples/unfinished/accounts-ui-helper. We
// should *definitely* automate this, but Tinytest is generally not
// the right abstraction to use for this.
// XXX it'd be cool to also test that the right thing happens if options
// *are* validated, but Accouns.ui._options is global state which makes this hard
// (impossible?)
@@ -14,4 +20,3 @@ Tinytest.add('accounts-ui - config validates keys', function (test) {
Accounts.ui.config({requestPermissions: {facebook: "not an array"}});
});
});

View File

@@ -115,18 +115,51 @@
return '';
};
// returns an array of the login services used by this app. each
// element of the array is an object (eg {name: 'facebook'}), since
// that makes it useful in combination with handlebars {{#each}}.
//
// NOTE: It is very important to have this return password last
// because of the way we render the different providers in
// login_buttons_dropdown.html
Accounts._loginButtons.getLoginServices = function () {
var ret = [];
// make sure to put password last, since this is how it is styled
// in the ui as well.
_.each(
['facebook', 'github', 'google', 'twitter', 'weibo', 'password'],
function (service) {
if (Accounts[service])
ret.push({name: service});
});
var self = this;
var services = [];
return ret;
// find all methods of the form: `Meteor.loginWithFoo`, where
// `Foo` corresponds to a login service
//
// XXX we should consider having a client-side
// Accounts.oauth.registerService function which records the
// active services and encapsulates boilerplate code now found in
// files such as facebook_client.js. This would have the added
// benefit of allow us to unify facebook_{client,common,server}.js
// into one file, which would encourage people to build more login
// services packages.
_.each(_.keys(Meteor), function(methodName) {
var match;
if ((match = methodName.match(/^loginWith(.*)/))) {
var serviceName = match[1].toLowerCase();
// HACKETY HACK. needed to not match
// Meteor.loginWithToken. See XXX above.
if (Accounts[serviceName])
services.push(match[1].toLowerCase());
}
});
// Be equally kind to all login services. This also preserves
// backwards-compatibility. (But maybe order should be
// configurable?)
services.sort();
// ensure password is last
if (_.contains(services, 'password'))
services = _.without(services, 'password').concat(['password']);
return _.map(services, function(name) {
return {name: name};
});
};
Accounts._loginButtons.hasPasswordService = function () {

View File

@@ -162,6 +162,8 @@
var configuration = {
service: serviceName
};
// Fetch the value of each input field
_.each(configurationFields(), function(field) {
configuration[field.property] = document.getElementById(
'configure-login-service-dialog-' + field.property).value
@@ -229,7 +231,6 @@
return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled');
};
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
str = str == null ? '' : String(str);

View File

@@ -156,6 +156,26 @@
</div>
</template>
<!--
This strategy for login forms means that browsers' "Remember password"
functionality does not work. Different browsers have different
requirements for remembering passwords:
- Firefox: Must be an actual form (with a submit button), but you can
cancel the submit with onsubmit='return false'.
- Safari: Must be an actual form, and the form must actually be
submitted somewhere (though it can target a hidden iframe and go to a
bogus URL)
- Chrome: Must be an actual form, and the the form elements must be
present in the initial HTML, not added to the page with javascript. This
basically rules out using normal meteor templates.
https://gist.github.com/968927
-->
<template name="_loginButtonsFormField">
{{#if visible}}
<div id="login-{{fieldName}}-label-and-input">

View File

@@ -180,6 +180,7 @@
return Accounts._loginButtons.hasPasswordService();
};
// return all login services, with password last
Template._loginButtonsLoggedOutAllServices.services = function () {
return Accounts._loginButtons.getLoginServices();
};

View File

@@ -1,21 +0,0 @@
/* These should be in their respective packages. https://app.asana.com/0/988582960612/1477837179813 */
#login-buttons-image-google {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==);
}
#login-buttons-image-facebook {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC);
}
#login-buttons-image-weibo {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=);
}
#login-buttons-image-twitter {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=);
}
#login-buttons-image-github {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=);
}

View File

@@ -23,6 +23,8 @@
var options = {}; // use default scope unless specified
if (Accounts.ui._options.requestPermissions[serviceName])
options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName];
if (Accounts.ui._options.requestOfflineToken[serviceName])
options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName];
loginWithService(options, callback);
}

View File

@@ -8,7 +8,6 @@ Package.on_use(function (api) {
api.add_files([
'accounts_ui.js',
'login_buttons_images.css',
'login_buttons.html',
'login_buttons_single.html',
'login_buttons_dropdown.html',

View File

@@ -372,7 +372,7 @@
width: @configure-login-service-dialog-width;
margin-left: -(@configure-login-service-dialog-width
+ @meteor-accounts-base-padding) / 2;
margin-top: -180px; /* = approximately -height/2, though height can change */
margin-top: -220px; /* = approximately -height/2, though height can change */
table { width: 100%; }
input {

View File

@@ -4,7 +4,7 @@ Package.describe({
Package.on_use(function (api) {
api.use('accounts-ui-unstyled', 'client');
api.use('less', 'server');
api.use('less', 'client');
api.add_files(['login_buttons.less'], 'client');
});

View File

@@ -9,7 +9,7 @@ Package.on_use(function(api) {
api.use('templating', 'client');
api.add_files(
['weibo_configure.html', 'weibo_configure.js'],
['weibo_login_button.css', 'weibo_configure.html', 'weibo_configure.js'],
'client');
api.add_files('weibo_common.js', ['client', 'server']);

View File

@@ -24,5 +24,4 @@
Accounts.oauth.initiateLogin(state, loginUrl, callback);
};
}) ();

View File

@@ -0,0 +1,3 @@
#login-buttons-image-weibo {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=);
}

View File

@@ -43,6 +43,11 @@ DomUtils = {};
var testDiv = document.createElement("div");
testDiv.innerHTML = " <link/><table></table><select><!----></select>";
// Need to wrap in a div rather than directly creating SELECT to avoid
// *another* IE bug.
var testSelectDiv = document.createElement("div");
testSelectDiv.innerHTML = "<select><option selected>Foo</option></select>";
testSelectDiv.firstChild.setAttribute("name", "myname");
// Tests that, if true, indicate browser quirks present.
var quirks = {
@@ -55,10 +60,16 @@ DomUtils = {};
// IE loses some tags in some environments (requiring extra wrapper).
tagsLost: testDiv.getElementsByTagName("link").length === 0,
// IE <= 8 loses HTML comments in <select> and <option> tags.
// Assert that we have IE's mergeAttributes to use in our work-around.
commentsLost: ((! testDiv.getElementsByTagName("select")[0].firstChild)
&& testDiv.mergeAttributes)
// IE <= 9 loses HTML comments in <select> and <option> tags.
commentsLost: (! testDiv.getElementsByTagName("select")[0].firstChild),
selectValueMustBeFromAttribute: (testSelectDiv.firstChild.value !== "Foo"),
// In IE7, setAttribute('name', foo) doesn't show up in rendered HTML.
// (In FF3, outerHTML is undefined, but it doesn't have this quirk.)
mustSetNameInCreateElement: (
testSelectDiv.firstChild.outerHTML &&
testSelectDiv.firstChild.outerHTML.indexOf("myname") === -1)
};
// Set up map of wrappers for different nodes.
@@ -157,15 +168,41 @@ DomUtils = {};
// the DOM.
// Here we build an array of fake tags and iterate over that.
_.each(container.getElementsByTagName("ins"), function (ins) {
if (ins.getAttribute("domutilsrealtagname"))
if (ins.getAttribute("domutilsrealtagname")) {
fakeTags.push(ins);
}
});
_.each(fakeTags, function (fakeTag) {
var realTag = document.createElement(
fakeTag.getAttribute('domutilsrealtagname'));
var tagName = fakeTag.getAttribute('domutilsrealtagname');
if (quirks.mustSetNameInCreateElement &&
fakeTag.getAttribute('name')) {
// IE7 can't set 'name' with setAttribute, but it has this
// crazy syntax for setting it at create time.
// http://webbugtrack.blogspot.com/2007/10/bug-235-createelement-is-broken-in-ie.html
// http://msdn.microsoft.com/en-us/library/ms536389.aspx
tagName = "<" + tagName + " name='" +
_.escape(fakeTag.getAttribute('name')) + "'/>";
}
var realTag = document.createElement(tagName);
fakeTag.removeAttribute('domutilsrealtagname');
// copy all attributes
realTag.mergeAttributes(fakeTag, false);
// copy all attributes. for some reason mergeAttributes doesn't work
// here: eg, it doesn't copy SELECTED or VALUE. (Probably because
// these attributes would be expando on INS?)
var fakeAttrs = fakeTag.attributes;
for (var i = 0; i < fakeAttrs.length; ++i) {
var fakeAttr = fakeAttrs.item(i);
if (fakeAttr.specified) {
var name = fakeAttr.name.toLowerCase();
var value = String(fakeAttr.value);
// IE7 gets confused if you try to setAttribute('selected', ''),
// so be a little more explicit.
if (name === 'selected' && value === '')
value = 'selected';
realTag.setAttribute(name, value);
}
}
// move all children
while (fakeTag.firstChild)
realTag.appendChild(fakeTag.firstChild);
@@ -480,5 +517,44 @@ DomUtils = {};
return DomUtils.rangeToHtml(node, node);
};
// Sets the value of an element, portably across browsers. There's a special
// case for SELECT elements in IE.
DomUtils.setElementValue = function (node, value) {
// Try to assign the value.
node.value = value;
if (node.value === value || node.nodeName !== 'SELECT')
return;
// IE (all versions) appears to only let you assign SELECT values which
// match valid OPTION values... and moreover, the OPTION value must be
// explicitly given as an attribute, not just as the text. So we hunt for
// the OPTION and select it.
var options = DomUtils.findAll(node, 'option');
for (var i = 0; i < options.length; ++i) {
if (DomUtils.getElementValue(options[i]) === value) {
options[i].selected = true;
return;
}
}
};
// Gets the value of an element, portably across browsers. There's a special
// case for SELECT elements in IE.
DomUtils.getElementValue = function (node) {
if (!quirks.selectValueMustBeFromAttribute)
return node.value;
if (node.nodeName === 'OPTION') {
// Inspired by jQuery.valHooks.option.get.
var val = node.attributes.value;
return !val || val.specified ? node.value : node.text;
} else if (node.nodeName === 'SELECT') {
if (node.selectedIndex < 0)
return null;
return DomUtils.getElementValue(node.options[node.selectedIndex]);
} else {
return node.value;
}
};
})();

View File

@@ -1 +1,13 @@
// TESTS GO HERE
Tinytest.add("domutils - setElementValue", function (test) {
var div = OnscreenDiv();
div.node().appendChild(DomUtils.htmlToFragment(
("<select><option>Foo</option><option value='Bar'>Baz</option>" +
"<option selected value='Quux'>Quux</option></select>")));
var select = DomUtils.find(div.node(), 'select');
test.equal(DomUtils.getElementValue(select), "Quux");
_.each(["Foo", "Bar", "Quux"], function (value) {
DomUtils.setElementValue(select, value);
test.equal(DomUtils.getElementValue(select), value);
});
});

View File

@@ -20,9 +20,13 @@ Meteor.http = Meteor.http || {};
method = (method || "").toUpperCase();
var headers = {};
var content = options.content;
if (options.data)
if (options.data) {
content = JSON.stringify(options.data);
headers['Content-Type'] = 'application/json';
}
var params_for_url, params_for_body;
if (content || method === "GET" || method === "HEAD")
@@ -51,6 +55,8 @@ Meteor.http = Meteor.http || {};
content = Meteor.http._encodeParams(params_for_body);
}
_.extend(headers, options.headers || {});
////////// Callback wrapping //////////
// wrap callback to always return a result object, and always
@@ -84,9 +90,8 @@ Meteor.http = Meteor.http || {};
xhr.open(method, url, true, username, password);
if (options.headers)
for (var k in options.headers)
xhr.setRequestHeader(k, options.headers[k]);
for (var k in headers)
xhr.setRequestHeader(k, headers[k]);
// setup timeout

View File

@@ -8,11 +8,16 @@ Meteor.http = Meteor.http || {};
_.each(params, function(value, key) {
if (buf.length)
buf.push('&');
buf.push(encodeURIComponent(key), '=', encodeURIComponent(value));
buf.push(Meteor.http._encodeString(key), '=',
Meteor.http._encodeString(value));
});
return buf.join('').replace(/%20/g, '+');
};
Meteor.http._encodeString = function(str) {
return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
};
Meteor.http._buildUrl = function(before_qmark, from_qmark, opt_query, opt_params) {
var url_without_query = before_qmark;
var query = from_qmark ? from_qmark.slice(1) : null;

View File

@@ -27,9 +27,13 @@ Meteor.http = Meteor.http || {};
var url_parts = url_util.parse(url);
var headers = {};
var content = options.content;
if (options.data)
if (options.data) {
content = JSON.stringify(options.data);
headers['Content-Type'] = 'application/json';
}
var params_for_url, params_for_body;
@@ -42,9 +46,6 @@ Meteor.http = Meteor.http || {};
url_parts.protocol+"//"+url_parts.host+url_parts.pathname,
url_parts.search, options.query, params_for_url);
var headers = {};
if (options.auth) {
if (options.auth.indexOf(':') < 0)
throw new Error('auth option should be of the form "username:password"');

View File

@@ -222,6 +222,22 @@ testAsyncMulti("httpcall - methods", [
test.equal(result.statusCode, 200);
var data = result.data;
test.equal(data.body, {greeting: "Hello World!"});
// nb: some browsers include a charset here too.
test.matches(data.headers['content-type'], /^application\/json\b/);
}));
Meteor.http.call(
"POST", url_prefix()+"/data-test-explicit",
{ data: {greeting: "Hello World!"},
headers: {'Content-Type': 'text/stupid'} },
expect(function(error, result) {
test.isFalse(error);
test.isTrue(result);
test.equal(result.statusCode, 200);
var data = result.data;
test.equal(data.body, {greeting: "Hello World!"});
// nb: some browsers include a charset here too.
test.matches(data.headers['content-type'], /^text\/stupid\b/);
}));
}
]);
@@ -294,7 +310,6 @@ testAsyncMulti("httpcall - headers", [
testAsyncMulti("httpcall - params", [
function(test, expect) {
var do_test = function(method, url, params, opt_opts, expect_url, expect_body) {
var opts = {};
if (typeof opt_opts === "string") {
@@ -324,6 +339,8 @@ testAsyncMulti("httpcall - params", [
do_test("GET", "/", {foo:"bar", fruit:"apple"}, "/?foo=bar&fruit=apple", "");
do_test("POST", "/", {foo:"bar", fruit:"apple"}, "/", "foo=bar&fruit=apple");
do_test("POST", "/", {foo:"bar", fruit:"apple"}, "/", "foo=bar&fruit=apple");
do_test("GET", "/", {'foo!':"bang!"}, {}, "/?foo%21=bang%21", "");
do_test("POST", "/", {'foo!':"bang!"}, {}, "/", "foo%21=bang%21");
do_test("POST", "/", {foo:"bar", fruit:"apple"}, {
content: "stuff!"}, "/?foo=bar&fruit=apple", "stuff!");
do_test("POST", "/", {foo:"bar", greeting:"Hello World"}, {

View File

@@ -70,10 +70,10 @@ var rPunctuator = new RegExp(
.join('|'), 'g');
var rDivPunctuator = /\/=?/g;
// Section 7.8.3
var rHexLiteral = /0x[0-9a-fA-F]+$/g;
var rOctLiteral = /0[0-7]+/g; // deprecated
var rHexLiteral = /0[xX][0-9a-fA-F]+(?!\w)/g;
var rOctLiteral = /0[0-7]+(?!\w)/g; // deprecated
var rDecLiteral =
/(((0|[1-9][0-9]*)(\.[0-9]*)?)|\.[0-9]+)([Ee][+-]?[0-9]+)?/g;
/(((0|[1-9][0-9]*)(\.[0-9]*)?)|\.[0-9]+)([Ee][+-]?[0-9]+)?(?!\w)/g;
// Section 7.8.4
var rStringQuote = /["']/g;
// Match one or more characters besides quotes, backslashes, or line ends
@@ -81,14 +81,30 @@ var rStringMiddle = /(?=.)[^"'\\]+?((?!.)|(?=["'\\]))/g;
// Match one escape sequence, including the backslash.
var rEscapeSequence =
/\\(['"\\bfnrtv]|0(?![0-9])|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|(?=.)[^ux0-9])/g;
// Match one ES5 line continuation
var rLineContinuation =
/\\(\r\n|[\u000A\u000D\u2028\u2029])/g;
// Section 7.8.5
// Match one regex literal, including slashes, not including flags.
// XXX Add support for unescaped '/' in character class, allowed by 5th ed.
var rRegexLiteral = /\/(?![*\/])(\\.|(?=.)[^\\])+?\//g;
// Support unescaped '/' in character classes, per 5th ed.
// For example: `/[/]/` will match the string `"/"`.
//
// Explanation of regex:
// - Match `/` not followed by `/` or `*`
// - Match one or more of any of these:
// - Backslash followed by one non-newline
// - One non-newline, not `[` or `\` or `/`
// - A character class, beginning with `[` and ending with `]`.
// In the middle is zero or more of any of these:
// - Backslash followed by one non-newline
// - One non-newline, not `]` or `\`
// - Match closing `/`
var rRegexLiteral =
/\/(?![*\/])(\\.|(?=.)[^\[\/\\]|\[(\\.|(?=.)[^\]\\])*\])+\//g;
var rRegexFlags = /[a-zA-Z]*/g;
var rDecider =
/((?=.)\s)|(\/[\/\*]?)|([\][{}().;,<>=!+*%&|^~?:-])|(\d)|(["'])|(.)|([\S\s])/g;
/((?=.)\s)|(\/[\/\*]?)|([\][{}();,<>=!+*%&|^~?:-]|\.(?![0-9]))|([\d.])|(["'])|(.)|([\S\s])/g;
var keywordLookup = {
' break': 'KEYWORD',
@@ -376,7 +392,8 @@ JSLexer.prototype.next = function () {
run(rStringQuote);
var quote = match[0];
do {
run(rStringMiddle) || run(rEscapeSequence) || run(rStringQuote);
run(rStringMiddle) || run(rEscapeSequence) ||
run(rLineContinuation) || run(rStringQuote);
} while (match && match[0] !== quote);
if (! (match && match[0] === quote))
return lexeme('ERROR');
@@ -388,12 +405,21 @@ JSLexer.prototype.next = function () {
}
// dot (any non-line-terminator)
run(rIdentifierPrefix);
// Use non-short-circuiting OR, '|', to allow matching
// Use non-short-circuiting bitwise OR, '|', to always try
// both regexes in sequence, returning false only if neither
// matched.
while (run(rIdentifierMiddle) | run(rIdentifierPrefix)) {/*continue*/}
while ((!! run(rIdentifierMiddle)) |
(!! run(rIdentifierPrefix))) { /*continue*/ }
var word = code.substring(origPos, pos);
return lexeme(keywordLookup[' '+word] || 'IDENTIFIER');
};
JSLexer.prettyOffset = function (code, pos) {
var codeUpToPos = code.substring(0, pos);
var startOfLine = codeUpToPos.lastIndexOf('\n') + 1;
var indexInLine = pos - startOfLine; // 0-based
var lineNum = codeUpToPos.replace(/[^\n]+/g, '').length + 1; // 1-based
return "line " + lineNum + ", offset " + indexInLine;
};
})();

View File

@@ -70,7 +70,8 @@ JSParser.prototype.consumeNewToken = function () {
do {
lex = lexer.next();
if (lex.isError())
throw new Error("Bad token at position " + lex.startPos() +
throw new Error("Bad token at " +
JSLexer.prettyOffset(lexer.code, lex.startPos()) +
", text `" + lex.text() + "`");
else if (lex.type() === "NEWLINE")
self.isLineTerminatorHere = true;
@@ -89,7 +90,7 @@ JSParser.prototype.getParseError = function (expecting, found) {
if (this.oldToken)
msg += " after " + this.oldToken;
var pos = this.pos;
msg += " at position " + pos;
msg += " at " + JSLexer.prettyOffset(this.lexer.code, pos);
msg += ", found " + (found || this.newToken);
return new Error(msg);
};
@@ -233,21 +234,42 @@ JSParser.prototype.getSyntaxTree = function () {
list(token(',')))),
token(']')));
// "IdentifierName" in ES5 allows reserved words, like in a property access
// or a key of an object literal.
// Put IDENTIFIER last so it shows up in the error message.
var identifierName = or(tokenType('NULL'), tokenType('BOOLEAN'),
tokenType('KEYWORD'), tokenType('IDENTIFIER'));
var propertyName = expecting('propertyName', or(
node('idPropName', tokenType('IDENTIFIER')),
node('idPropName', identifierName),
node('numPropName', tokenType('NUMBER')),
node('strPropName', tokenType('STRING'))));
var nameColonValue = expecting(
'propertyName',
node('prop', seq(propertyName, token(':'), assignmentExpression)));
// Allow trailing comma in object literal, per ES5. Trailing comma
// must follow a `name:value`, that is, `{,}` is invalid.
//
// We can't just use a normal comma list(), because it will seize
// on the comma as a sign that the list continues. Instead,
// we specify a list of either ',' or nameColonValue, using positive
// and negative lookAheads to constrain the sequence. The grammar
// is ordered so that error messages will always say
// "Expected propertyName" or "Expected ," as appropriate, not
// "Expected ," when the look-ahead is negative or "Expected }".
var objectLiteral =
node('object',
seq(token('{'),
or(lookAheadToken('}'),
list(nameColonValue,
token(','))),
token('}')));
and(not(lookAheadToken(',')),
list(or(seq(token(','),
expecting('propertyName',
not(lookAheadToken(',')))),
seq(nameColonValue,
or(lookAheadToken('}'),
lookAheadToken(','))))))),
expecting('propertyName', token('}'))));
var functionMaybeNameRequired = booleanFlaggedParser(
function (nameRequired) {
@@ -281,7 +303,8 @@ JSParser.prototype.getSyntaxTree = function () {
objectLiteral,
functionExpression));
var dotEnding = seq(token('.'), tokenType('IDENTIFIER'));
var dotEnding = seq(token('.'), identifierName);
var bracketEnding = seq(token('['), expression, token(']'));
var callArgs = seq(token('('),
or(lookAheadToken(')'),

View File

@@ -141,7 +141,8 @@ var makeTester = function (test) {
// assert that a tokenization error occurred at '@'.
badToken: function (code) {
var constructMessage = function (pos, text) {
return "Bad token at position " + pos + ", text `" + text + "`";
var nicePos = JSLexer.prettyOffset(code, pos);
return "Bad token at " + nicePos + ", text `" + text + "`";
};
var pos = code.indexOf('`');
var text = code.match(/`(.*?)`/)[1];
@@ -176,7 +177,7 @@ var makeTester = function (test) {
badParse: function (code) {
var constructMessage = function (whatExpected, pos, found, after) {
return "Expected " + whatExpected + (after ? " after " + after : "") +
" at position " + pos + ", found " + found;
" at " + JSLexer.prettyOffset(code, pos) + ", found " + found;
};
var pos = code.indexOf('`');
@@ -200,7 +201,8 @@ var makeTester = function (test) {
var after = parser.oldToken;
found = (found || parser.newToken);
test.equal(error.message,
constructMessage(whatExpected, pos, found, after));
constructMessage(whatExpected, pos, found, after),
code);
}
}
};
@@ -240,6 +242,10 @@ Tinytest.add("jsparse - tokenization errors", function (test) {
var tester = makeTester(test);
tester.badToken("123`@`");
tester.badToken("thisIsATestOf = `'unterminated `\n strings'");
// make sure newlines aren't quietly included in regex literals
tester.badToken("var x = `/`a\nb/;");
tester.badToken("var x = `/`a\\\nb/;");
tester.badToken("var x = `/`a[\n]b/;");
});
Tinytest.add("jsparse - syntax forms", function (test) {
@@ -395,6 +401,20 @@ Tinytest.add("jsparse - syntax forms", function (test) {
["null + this - 3 + true",
"program(expressionStmnt(binary(binary(binary(null(null) + this(this)) - " +
"number(3)) + boolean(true)) ;()))"],
["+.5",
"program(expressionStmnt(unary(+ number(.5)) ;()))"],
["a1a1a",
"program(expressionStmnt(identifier(a1a1a) ;()))"],
["/abc/mig",
"program(expressionStmnt(regex(/abc/mig) ;()))"],
["/[]/",
"program(expressionStmnt(regex(/[]/) ;()))"],
["/[/]/",
"program(expressionStmnt(regex(/[/]/) ;()))"],
["/[[/]/",
"program(expressionStmnt(regex(/[[/]/) ;()))"],
["/.\\/[a//b]\\[\\][[\\d/]/",
"program(expressionStmnt(regex(/.\\/[a//b]\\[\\][[\\d/]/) ;()))"],
["a / /b/mgi / c",
"program(expressionStmnt(binary(binary(identifier(a) / " +
"regex(/b/mgi)) / identifier(c)) ;()))"],
@@ -549,7 +569,36 @@ Tinytest.add("jsparse - syntax forms", function (test) {
// comments don't interfere with parse
["if (true)\n//comment\nfoo();",
"program(ifStmnt(if `(` boolean(true) `)` " +
"expressionStmnt(call(identifier(foo) `(` `)`) ;)))"]
"expressionStmnt(call(identifier(foo) `(` `)`) ;)))"],
// bare keywords allowed in property access and object literal
["foo.return();",
"program(expressionStmnt(call(dot(identifier(foo) . return) `(` `)`) ;))"],
["foo.true();",
"program(expressionStmnt(call(dot(identifier(foo) . true) `(` `)`) ;))"],
["foo.null();",
"program(expressionStmnt(call(dot(identifier(foo) . null) `(` `)`) ;))"],
["({true:3})",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(true) : number(3)) }) `)`) ;()))"],
["({null:3})",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(null) : number(3)) }) `)`) ;()))"],
["({if:3})",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(if) : number(3)) }) `)`) ;()))"],
// ES5 line continuations in string literals
["var x = 'a\\\nb\\\nc';",
"program(varStmnt(var varDecl(x = string(`'a\\\nb\\\nc'`)) ;))"],
// ES5 trailing comma in object literal
["({});",
"program(expressionStmnt(parens(`(` object({ }) `)`) ;))"],
["({x:1});",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) }) `)`) ;))"],
["({x:1,});",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , }) `)`) ;))"],
["({x:1,y:2});",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , " +
"prop(idPropName(y) : number(2)) }) `)`) ;))"],
["({x:1,y:2,});",
"program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , " +
"prop(idPropName(y) : number(2)) , }) `)`) ;))"]
];
_.each(trials, function (tr) {
tester.goodParse(tr[0], tr[1]);
@@ -558,6 +607,8 @@ Tinytest.add("jsparse - syntax forms", function (test) {
Tinytest.add("jsparse - bad parses", function (test) {
var tester = makeTester(test);
// string between backticks is pulled out and becomes what's "expected"
// at that location, according to the correct error message
var trials = [
'{`statement`',
'if (`expression`)',
@@ -590,16 +641,29 @@ Tinytest.add("jsparse - bad parses", function (test) {
'foo: `statement`function foo() {}',
'[`expression`=',
'[,,`expression`=',
'({`propertyName`true:3})',
'({`propertyName`|:3})',
'({1:2,3`:`})',
'({1:2,`propertyName`',
'x.`IDENTIFIER`true',
'x.`IDENTIFIER`,',
'foo;`semicolon`:;',
'1;`statement`=',
'a+b`semicolon`=c;',
'for(1+1 `semicolon`in {});',
'`statement`=',
'for(;`expression`var;) {}'
'for(;`expression`var;) {}',
'({`propertyName`',
'({`propertyName`,})',
'({`propertyName`:})',
'({x`:`})',
'({x:1,`propertyName`',
'({x:1,`propertyName`,})',
'({x:1`,`',
'({x:1,`propertyName`,y:2})',
'({x:1,`propertyName`,})',
'({x:1,y:2`,`:',
'({x:1,y:2,`propertyName`',
'({x:1,y:2,`propertyName`:',
'({x:1,y:2,`propertyName`,})'
];
_.each(trials, function (tr) {
tester.badParse(tr);

View File

@@ -1,14 +1,14 @@
(function () {
// By default, try to connect back to the same endpoint as the page
// was served from.
var ddp_endpoint = '/';
if (typeof __meteor_runtime_config__ !== "undefined" &&
__meteor_runtime_config__.DEFAULT_DDP_ENDPOINT)
ddp_endpoint = __meteor_runtime_config__.DEFAULT_DDP_ENDPOINT;
var ddpUrl = '/';
if (typeof __meteor_runtime_config__ !== "undefined") {
if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL)
ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL;
}
_.extend(Meteor, {
default_connection: Meteor.connect(ddp_endpoint,
true /* restart_on_update */),
default_connection: Meteor.connect(ddpUrl, true /* restart_on_update */),
refresh: function (notification) {
}

View File

@@ -9,8 +9,8 @@ if (Meteor.isServer) {
// we can't do recursive Meteor.autosubscribe().
var captureSubs = null;
// @param url {String|Object} URL to Meteor app or sockjs endpoint (deprecated),
// or an object as a test hook (see code)
// @param url {String|Object} URL to Meteor app,
// or an object as a test hook (see code)
// Options:
// reloadOnUpdate: should we try to reload when the server says
// there's new code available?
@@ -30,9 +30,6 @@ Meteor._LivedataConnection = function (url, options) {
// as a test hook, allow passing a stream instead of a url.
if (typeof url === "object") {
self._stream = url;
// if we have two test streams, auto reload stuff will break because
// the url is used as a key for the migration data.
url = "/debug";
} else {
self._stream = new Meteor._Stream(url);
}
@@ -217,6 +214,12 @@ Meteor._LivedataConnection = function (url, options) {
self._outstandingMethodBlocks.shift();
}
// Mark all messages as unsent, they have not yet been sent on this
// connection.
_.each(self._methodInvokers, function (m) {
m.sentMessage = false;
});
// If an `onReconnect` handler is set, call it first. Go through
// some hoops to ensure that methods that are called from within
// `onReconnect` get executed _before_ ones that were originally
@@ -488,7 +491,7 @@ _.extend(Meteor._LivedataConnection.prototype, {
callback = Meteor.bindEnvironment(callback, function (e) {
// XXX improve error message (and how we report it)
Meteor._debug("Exception while delivering result of invoking '" +
name + "'", e.stack);
name + "'", e, e.stack);
});
}
@@ -1155,13 +1158,12 @@ _.extend(Meteor._LivedataConnection.prototype, {
});
_.extend(Meteor, {
// @param url {String} URL to Meteor app, or to sockjs endpoint (deprecated),
// @param url {String} URL to Meteor app,
// e.g.:
// "subdomain.meteor.com",
// "http://subdomain.meteor.com",
// "/",
// "http://subdomain.meteor.com/sockjs" (deprecated),
// "/sockjs" (deprecated)
// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs"
connect: function (url, _reloadOnUpdate) {
var ret = new Meteor._LivedataConnection(
url, {reloadOnUpdate: _reloadOnUpdate});

View File

@@ -1051,6 +1051,49 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with
]);
});
Tinytest.add("livedata connection - onReconnect with sent messages", function(test) {
var stream = new Meteor._StubStream();
var conn = newConnection(stream);
startAndConnect(test, stream);
// setup method
conn.methods({do_something: function (x) {}});
conn.onReconnect = function() {
conn.apply('do_something', ['login'], {wait: true});
};
conn.apply('do_something', ['one']);
// initial connect
stream.sent = [];
stream.reset();
testGotMessage(
test, stream, {msg: 'connect', session: conn._lastSessionId});
// Test that we sent just the login message.
var loginId = testGotMessage(
test, stream, {msg: 'method', method: 'do_something',
params: ['login'], id: '*'});
// we connect.
stream.receive({msg: 'connected', session: Meteor.uuid()});
test.length(stream.sent, 0);
// login got result (but not yet data)
stream.receive({msg: 'result', id: loginId, result: 'foo'});
test.length(stream.sent, 0);
// login got data. now we send next method.
stream.receive({msg: 'data', methods: [loginId]});
testGotMessage(
test, stream, {msg: 'method', method: 'do_something',
params: ['one'], id: '*'});
});
Tinytest.add("livedata stub - reconnect double wait method", function (test) {
var stream = new Meteor._StubStream;
var conn = newConnection(stream);

View File

@@ -1,5 +1,7 @@
if (process.env.DEFAULT_DDP_ENDPOINT)
__meteor_runtime_config__.DEFAULT_DDP_ENDPOINT = process.env.DEFAULT_DDP_ENDPOINT;
if (process.env.DDP_DEFAULT_CONNECTION_URL) {
__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL =
process.env.DDP_DEFAULT_CONNECTION_URL;
}
_.extend(Meteor, {

View File

@@ -15,8 +15,18 @@
try {
testElem.test = 123;
} catch (exception) { }
if (testElem.test !== 123)
return false;
return (testElem.test === 123);
// IE9 and 10 have a weird issue with multiple text nodes next to
// each other losing their expando attributes. Use the same
// workaround as IE8. Not sure how to test this as a feature, so use
// browser detection instead.
// See https://github.com/meteor/meteor/issues/458
if (document.documentMode)
return false;
return true;
})();
var wrapEndpoints = function (start, end) {

View File

@@ -19,7 +19,7 @@
Template.madewith.vote_count = function() {
var app = apps.findOne();
return app ? app.vote_count : '???';
return app ? app.vote_count : '';
};
Template.madewith.shortname = function () {
@@ -29,13 +29,13 @@
Template.madewith.events({
'click .madewith_upvote': function(event) {
var app = apps.findOne();
if (app) {
if (app)
server.call('vote', hostname);
// stop these so you don't click through the link to go to the
// app.
event.stopPropagation();
event.preventDefault();
}
// stop these so you don't click through the link to go to the
// app.
event.stopPropagation();
event.preventDefault();
}
});
})();

View File

@@ -22,7 +22,7 @@ Meteor._noYieldsAllowed = function (f) {
};
// js2-mode AST blows up when parsing 'future.return()', so alias.
Future.prototype.ret = Future.prototype.return;
Future.prototype.ret = Future.prototype['return'];
// Meteor._SynchronousQueue is a queue which runs task functions serially.
// Tasks are assumed to be synchronous: ie, it's assumed that they are
@@ -141,4 +141,15 @@ _.extend(Meteor._SynchronousQueue.prototype, {
}
});
// Sleep. Mostly used for debugging (eg, inserting latency into server
// methods).
Meteor._sleepForMs = function (ms) {
var fiber = Fiber.current;
setTimeout(function() {
fiber.run();
}, ms);
Fiber.yield();
};
})();

View File

@@ -2,3 +2,12 @@ Meteor = {
isClient: false,
isServer: true
};
try {
Meteor.settings = {};
if (process.env.METEOR_SETTINGS)
Meteor.settings = JSON.parse(process.env.METEOR_SETTINGS);
} catch (e) {
throw new Error("Settings are not valid JSON");
}

Some files were not shown because too many files have changed in this diff Show More