diff --git a/Contributing.md b/Contributing.md index c4db02a8a4..1eb6c36067 100644 --- a/Contributing.md +++ b/Contributing.md @@ -2,23 +2,24 @@ We are excited to have your help building Meteor — both the platform and the community behind it. Please read the project overview and guidelines for contributing bug reports and new code, or it might be hard for the community to help you with your issue or pull request. -

Project overview

+## Project overview Before we jump into detailed guidelines for opening and triaging issues and submitting pull requests, here is some information about how our project is structured and resources you should refer to as you start contributing. ### Ways to contribute There are many ways to contribute to the Meteor Project. Here’s a list of technical contributions with increasing levels of involvement and required knowledge of Meteor’s code and operations. -- [Reporting a bug](https://github.com/meteor/meteor/blob/devel/Contributing.md#reporting-a-bug-in-meteor) -- [Triaging issues](https://github.com/meteor/meteor/blob/devel/IssueTriage.md) -- [Contributing to documentation](https://github.com/meteor/docs/blob/master/Contributing.md) -- [Submitting pull requests](https://github.com/meteor/meteor/blob/devel/Contributing.md#making-changes-to-meteor-core) (See "Finding work" below) -- [Reviewing pull requests](https://github.com/meteor/meteor/blob/devel/Contributing.md#reviewer) -- [Maintaining a community package](https://github.com/meteor/meteor/blob/devel/Contributing.md#community-package-maintainer) +- [Reporting a bug](Contributing.md#reporting-a-bug-in-meteor) +- [Triaging issues](IssueTriage.md) +- [Contributing to documentation](https://github.com/meteor/docs/blob/master/Contributing.md) +- [Finding work](Contributing.md#finding-work) +- [Submitting pull requests](Contributing.md#making-changes-to-meteor-core) +- [Reviewing pull requests](Contributing.md#reviewer) +- [Maintaining a community package](Contributing.md#community-package-maintainer) -There are also several ways to contribute to the Meteor Project outside of GitHub, like organizing or speaking at [Meetups](https://www.meetup.com/topics/meteor/) and events and helping to moderate our [forums](https://forums.meteor.com/). Stay tuned for more documentation around non-code contributions. +There are also several ways to contribute to the Meteor Project outside of GitHub, like organizing or speaking at [Meetups](https://www.meetup.com/topics/meteor/) and events and helping to moderate our [forums](https://forums.meteor.com/). -If you can think of any changes to the project, [documentation](https://github.com/meteor/docs), or [guide](https://github.com/meteor/guide) that would improve the contributor experience, let us know by opening an issue! +If you can think of any changes to the project, [documentation](https://github.com/meteor/docs), or [guide](https://github.com/meteor/guide) that would improve the contributor experience, let us know by opening an issue in the correct repository! ### Finding work @@ -26,7 +27,7 @@ We curate specific issues that would make great pull requests for community cont Issues which *also* have the [`confirmed` label](https://github.com/meteor/meteor/issues?q=is%3Aissue%20is%3Aopen%20label%3Apull-requests-encouraged%20label%3Aconfirmed) are considered to have their details clear enough to begin working on. -Any issue which does not have the `confirmed` label still requires discussion on implementation details but input and positive commentary is welcome! Any pull-request opened on an issue which is not `confirmed` is still welcome, however the pull-request is more likely to be sent back for reworking than a `confirmed` issue. If in doubt about the best way to implement something, please create additional conversation on the issue. +Any issue which does not have the `confirmed` label still requires discussion on implementation details but input and positive commentary is welcome! Any pull request opened on an issue which is not `confirmed` is still welcome, however the pull-request is more likely to be sent back for reworking than a `confirmed` issue. If in doubt about the best way to implement something, please create additional conversation on the issue. ### Project roles @@ -72,7 +73,7 @@ Current Documentation Maintainers: #### Community Package Maintainer: -Community package maintainers are community members who maintain packages outside of Meteor core. This requires code to be extracted from meteor/meteor, and entails a high level of responsibility. For this reason, community maintainers generally (and currently) must first become an advanced contributor to Meteor core and have 4-5 non-trivial pull requests merged that went through the proper contribution workflow. At that point, core contributors may make the case for breaking out a particular core package, and assist in the technical process around doing so. +Community package maintainers are community members who maintain packages outside of Meteor core. This requires code to be extracted from meteor/meteor, and entails a high level of responsibility. For this reason, community maintainers generally (and currently) must first become an advanced contributor to Meteor core and have 4-5 non-trivial pull requests merged that went through the proper contribution work-flow. At that point, core contributors may make the case for breaking out a particular core package, and assist in the technical process around doing so. Current Community Package Maintainers: - [@mitar](https://github.com/mitar) for [Blaze](https://github.com/meteor/blaze) @@ -88,11 +89,12 @@ Current Community Manager: Right now, the best place to track the work being done on Meteor is to take a look at the latest release milestone [here](https://github.com/meteor/meteor/milestones). Also, the [Meteor Roadmap](Roadmap.md) contains high-level information on the current priorities of the project. -

Reporting a bug in Meteor

+## Reporting a bug in Meteor + We welcome clear bug reports. If you've found a bug in Meteor that isn't a security risk, please file a report in -[our issue tracker](https://github.com/meteor/meteor/issues). Before you file your issue, look to see if it has already been reported. If so, comment, up-vote or +1 the existing issue to show that it's affecting multiple people. +[our issue tracker](https://github.com/meteor/meteor/issues). Before you file your issue, **search** to see if it has already been reported. If so, up-vote (using GitHub reactions) or add additional helpful details to the existing issue to show that it's affecting multiple people. > There is a separate procedure for security-related issues. If the > issue you've found contains sensitive information or raises a security @@ -135,19 +137,18 @@ A reproduction recipe works like this: If you want to submit a pull request that fixes your bug, that's even better. We love getting bugfix pull requests. Just make sure they're -written to the MDG style guide and *come with tests*. Read further down +written with the [correct style](Development.md#code-style) and *come with tests*. Read further down for more details on proposing changes to core code. -

Feature requests

+## Feature requests -As of May 2016, we use GitHub to track feature requests. Feature request issues get the `feature` label, as well as a label +We use GitHub to track feature requests. Feature request issues get the `feature` label, as well as a label corresponding to the Meteor subproject that they are a part of. -Meteor is a big project with [many subprojects](https://github.com/meteor/meteor/tree/devel/packages). -Right now, the project doesn't have as many -[core developers (we're hiring!)](https://www.meteor.com/jobs/core-developer) -as subprojects, so we're not able to work on every single subproject every -month. We use our [roadmap](Roadmap.md) to communicate the high level features we're prioritizing over the near and medium term. +Meteor is a big project with [many sub-projects](https://github.com/meteor/meteor/tree/devel/packages). +There aren't as many [core developers (we're hiring!)](https://www.meteor.io/jobs/) +as there are sub-projects, so we're not able to work on every single sub-project every +month. We use our [roadmap](Roadmap.md) to communicate the high-level features we're currently prioritizing. Every additional feature adds a maintenance cost in addition to its value. This cost starts with the work of writing the feature or reviewing a community pull @@ -160,7 +161,7 @@ For these reasons, we strongly encourage features to be implemented as [Atmosphe Feature requests should be well specified and unambiguous to have the greatest chance of being worked on by a contributor. -Finally, you can show your support for features you would like by commenting with a +1 or up-voting the issue. +Finally, you can show your support for (or against!) features by using [GitHub reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) or by adding meaningful details which help the feature definition become more clear. Please do not comment with "+1" since it creates a lot of noise (e-mails, notifications, etc.). ## Triaging issues @@ -170,6 +171,10 @@ A great way to contribute to Meteor is by helping keep the issues in the reposit If you'd like to contribution to Meteor's documentation, head over to https://github.com/meteor/docs and create issues or pull requests there. +## Blaze + +Blaze lives in its [own repository](https://github.com/meteor/blaze/) with its own [issue tracker and feature prioritization](https://github.com/meteor/blaze/issues/) and is not tracked within Meteor core. + ## Making changes to Meteor core Eventually you may want to change something in a core Meteor package, or @@ -189,7 +194,7 @@ any change to a core package: 1. Nothing in Meteor should harm the experience of a new Meteor developer. That can be a difficult standard to reach, because we're - concerned here with the entire experience of developing and deploying + concerned with the entire experience of developing and deploying an application. For example, we work hard to make sure that the Meteor docs don't force new users to understand advanced concepts before they need them. And we think a great deal about making our APIs as @@ -206,8 +211,7 @@ any change to a core package: an expert then we'll probably prefer a different approach. We have found that writing software to meet both these standards at the -same time is hard but -incredibly rewarding. We hope you come to feel the same way. +same time is hard but incredibly rewarding. We hope you come to feel the same way. ### Understanding the core @@ -219,19 +223,18 @@ You'll have the best chance of getting a change into core if you can build conse Help drive discussion and advocate for your feature on the Github ticket (and perhaps the forums). The higher the demand for the feature and the greater the clarity of it's specification will determine the likelihood of a core contributor prioritizing your feature by flagging it with the `pull-requests-encouraged` label. -Split features up into smaller, logically separable chunks. It is unlikely that large and complicated PRs will be merged. +Split features up into smaller, logically separate chunks. It is unlikely that large and complicated PRs will be merged. Once your feature has been labelled with `pull-requests-encouraged`, leave a comment letting people know you're working on it and you can begin work on the code. ### Submitting pull requests -Once you've hammered out a good design go ahead and submit a pull request. If your PR isn't against a bug with the `confirmed` label or a feature request with the `pull-requests-encouraged` label, don't expect your PR to be merged unless it's a trivial and obvious fix (e.g documentation). When submitting a PR, please follow -these guidelines: +Once you've come up with a good design, go ahead and submit a pull request (PR). If your PR isn't against a bug with the `confirmed` label or a feature request with the `pull-requests-encouraged` label, don't expect your PR to be merged unless it's a trivial and obvious fix (e.g. documentation). When submitting a PR, please follow these guidelines: * Sign the [contributor's agreement](http://contribute.meteor.com/). * Base all your work off of the **devel** branch. The **devel** branch - is where active development happens. **We do not merge patches + is where active development happens. **We do not merge pull requests directly into master.** * Name your branch to match the feature/bug fix that you are @@ -251,12 +254,6 @@ these guidelines: ### Need help with your pull request? -Meteor now has groups defined to cover different areas of the codebase. If you need help getting acceptance on certain pull requests with an area of focus listed below, you can address the appropriate people in the pull request: +If you need help with a pull request, you should start by asking questions in the issue which it pertains to. If you feel that your pull request is almost ready or needs feedback which can only be demonstrated with code, go ahead and open a pull-request with as much progress as possible. By including a "[Work in Progress]" note in the subject, project contributors will know you need help! -* Meteor Data Team - This includes DDP, tracker, mongo, accounts, etc. You can mention @data in the PR. -* Blaze - This includes Spacebars, Blaze, etc. You can mention @view-layer in the PR. -* Build tools - This includes modules, build tool changes, etc. You can mention @platform in the PR. -* Mobile integration - This includes Cordova, React Native, etc. You can mention @mobile in the PR. -* Documentation - This includes the Guide, the Docs, and any supporting material. You can mention @guide in the PR. - -Including the people above is no guarantee that you will get a response, or ultimately that your pull request will be accepted. This section exists to give some minor guidance on internal Meteor Development Group team structures. +Submitting a pull request is no guarantee it will be accepted, but contributors will do their best to help move your pull request toward release. diff --git a/Development.md b/Development.md index fc36eea113..1138c53d79 100644 --- a/Development.md +++ b/Development.md @@ -88,6 +88,12 @@ $ ./scripts/generate-dev-bundle.sh This will generate a new tarball (`dev_bundle___.tar.gz`) in the root of the checkout. Assuming you bumped the `BUNDLE_VERSION`, the new version will be extracted automatically when you run `./meteor`. If you are rebuilding the same version (or didn't bump the version number), you should delete the existing `dev_bundle` directory to ensure the new tarball is extracted when you run `./meteor`. +### Submitting "Dev Bundle" Pull Requests + +It's important to note that while `dev_bundle` pull requests are accepted/reviewed, a new `dev_bundle` can only be published to MDG's Meteor infrastructure by an MDG staff member. This means that the build tool and package tests of submitted `dev_bundle` pull requests will always initially fail (since the new `dev_bundle` hasn't yet been built/published by MDG, which means it can't be downloaded by Meteor's continuous integration environment). + +Pull requests that contain `dev_bundle` changes will be noted by repo collaborators, and a request to have a new `dev_bundle` built/published will be forwarded to MDG. + ## Additional documentation The Meteor core is best documented within the code itself, however, many components also have a `README.md` in their respective directories. diff --git a/History.md b/History.md index f4318fd335..798d5cf1b8 100644 --- a/History.md +++ b/History.md @@ -6,6 +6,8 @@ ## v1.5, TBD +* Node has been upgraded to version 4.8.3. + * Running `meteor add dynamic-import` installs support for ECMAScript [dynamic `import(...)`](https://github.com/tc39/proposal-dynamic-import), a new language feature which allows for asynchronous module fetching @@ -15,6 +17,71 @@ information about how dynamic `import(...)` works in Meteor, and how to use it in your applications. +* The `meteor-babel` npm package has been upgraded to version 0.21.2, + enabling the latest Reify compiler and the transform-class-properties + plugin, among other improvements. + +* The `reify` npm package has been upgraded to version 0.11.0, fixing + [issue #8595](https://github.com/meteor/meteor/issues/8595) and + improving compilation and runtime performance. + +> Note: With this version of Reify, `import` declarations are compiled to + `module.watch(require(id), ...)` instead of `module.importSync(id, ...)` + or the older `module.import(id, ...)`. The behavior of the compiled code + should be the same as before, but the details seemed different enough to + warrant a note. + +* The `install` npm package has been upgraded to version 0.10.1. + +* If you're using the `standard-minifier-js` Meteor package, as most + Meteor developers do, it will now produce a detailed analysis of package + and module sizes within your production `.js` bundle whenever you run + `meteor build` or `meteor run --production`. These data are served by + the application web server at the same URL as the minified `.js` bundle, + except with a `.stats.json` file extension instead of `.js`. If you're + using a different minifier plugin, and would like to support similar + functionality, refer to + [these](https://github.com/meteor/meteor/pull/8327/commits/084801237a8c288d99ec82b0fbc1c76bdf1aab16) + [commits](https://github.com/meteor/meteor/pull/8327/commits/1c8bc7353e9a8d526880634a58c506b423c4a55e) + for inspiration. + +## v1.4.4.2, 2017-05-02 + +* Node has been upgraded to version 4.8.2. + +* The `npm` npm package has been upgraded to version 4.5.0. + Note that when using npm `scripts` there has been a change regarding + what happens when `SIGINT` (Ctrl-C) is received. Read more + [here](https://github.com/npm/npm/releases/tag/v4.5.0). + +* Fix a regression which prevented us from displaying a helpful banner when + running `meteor debug` because of a change in Node.js. + +* Update `node-inspector` npm to 1.1.1, fixing a problem encountered when trying + to press "Enter" in the inspector console. + [Issue #8469](https://github.com/meteor/meteor/issues/8469) + +* The `email` package has had its `mailcomposer` npm package swapped with + a Node 4 fork of `nodemailer` due to its ability to support connection pooling + in a similar fashion as the original `mailcomposer`. + [Issue #8591](https://github.com/meteor/meteor/issues/8591) + [PR #8605](https://github.com/meteor/meteor/pull/8605) + + > Note: The `MAIL_URL` should be configured with a scheme which matches the + > protocol desired by your e-mail vendor/mail-transport agent. For + > encrypted connections (typically listening on port 465 or 587), this means + > using `smtps://`. Unencrypted connections should continue to use + > `smtp://`. + +* A new `Tracker.inFlush()` has been added to provide a global Tracker + "flushing" state. + [PR #8565](https://github.com/meteor/meteor/pull/8565). + +* The `meteor-babel` npm package has been upgraded to version 0.20.1, and + the `reify` npm package has been upgraded to version 0.7.4, fixing + [issue #8595](https://github.com/meteor/meteor/issues/8595). + (This was fixed between full Meteor releases, but is being mentioned here.) + ## v1.4.4.1, 2017-04-07 * A change in Meteor 1.4.4 to remove "garbage" directories asynchronously diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index 9ff6e6f8f8..99bcdcdd81 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -28,6 +28,9 @@ export class AccountsClient extends AccountsCommon { // Defined in localstorage_token.js. this._initLocalStorage(); + + // This is for .registerClientLoginFunction & .callLoginFunction. + this._loginFuncs = {}; } /// @@ -62,6 +65,49 @@ export class AccountsClient extends AccountsCommon { return this._loggingOut.get(); } + /** + * @summary Register a new login function on the client. Intended for OAuth package authors. You can call the login function by using + `Accounts.callLoginFunction` or `Accounts.callLoginFunction`. + * @locus Client + * @param {String} funcName The name of your login function. Used by `Accounts.callLoginFunction` and `Accounts.applyLoginFunction`. + Should be the OAuth provider name accordingly. + * @param {Function} func The actual function you want to call. Just write it in the manner of `loginWithFoo`. + */ + registerClientLoginFunction(funcName, func) { + if (this._loginFuncs[funcName]) { + throw new Error(`${funcName} has been defined already`); + } + this._loginFuncs[funcName] = func; + } + + /** + * @summary Call a login function defined using `Accounts.registerClientLoginFunction`. Excluding the first argument, all remaining + arguments are passed to the login function accordingly. Use `applyLoginFunction` if you want to pass in an arguments array that contains + all arguments for the login function. + * @locus Client + * @param {String} funcName The name of the login function you wanted to call. + */ + callLoginFunction(funcName, ...funcArgs) { + if (!this._loginFuncs[funcName]) { + throw new Error(`${funcName} was not defined`); + } + return this._loginFuncs[funcName].apply(this, funcArgs); + } + + /** + * @summary Same as ``callLoginFunction` but accept an `arguments` which contains all arguments for the login + function. + * @locus Client + * @param {String} funcName The name of the login function you wanted to call. + * @param {Array} funcArgs The `arguments` for the login function. + */ + applyLoginFunction(funcName, funcArgs) { + if (!this._loginFuncs[funcName]) { + throw new Error(`${funcName} was not defined`); + } + return this._loginFuncs[funcName].apply(this, funcArgs); + } + /** * @summary Log the user out. * @locus Client diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index ab62e14618..eca4c3e5a8 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -1502,6 +1502,8 @@ function setupUsersCollection(users) { { sparse: 1 }); // For expiring login tokens users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 }); + // For expiring password tokens + users._ensureIndex('services.password.reset.when', { sparse: 1 }); } /// diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 568d29e33a..80ac334d76 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "A user account system", - version: "1.2.16" + version: "1.3.0" }); Package.onUse(function (api) { diff --git a/packages/accounts-facebook/facebook.js b/packages/accounts-facebook/facebook.js index 4517bf1d5a..4179610e4b 100644 --- a/packages/accounts-facebook/facebook.js +++ b/packages/accounts-facebook/facebook.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('facebook'); if (Meteor.isClient) { - Meteor.loginWithFacebook = function(options, callback) { + const loginWithFacebook = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -11,6 +11,10 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Facebook.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('facebook', loginWithFacebook); + Meteor.loginWithFacebook = function () { + return Accounts.applyLoginFunction('facebook', arguments); + }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 1a272a6dc7..060f01f006 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Login service for Facebook accounts", - version: "1.1.1" + version: "1.2.0" }); Package.onUse(function(api) { diff --git a/packages/accounts-github/github.js b/packages/accounts-github/github.js index bf71477695..b2478518e5 100644 --- a/packages/accounts-github/github.js +++ b/packages/accounts-github/github.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('github'); if (Meteor.isClient) { - Meteor.loginWithGithub = function(options, callback) { + const loginWithGithub = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -11,6 +11,10 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Github.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('github', loginWithGithub); + Meteor.loginWithGithub = function () { + return Accounts.applyLoginFunction('github', arguments); + }; } else { Accounts.addAutopublishFields({ // not sure whether the github api can be used from the browser, diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index a612794ade..9e9a7abeb2 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Login service for Github accounts', - version: '1.2.1' + version: '1.3.0' }); Package.onUse(function (api) { diff --git a/packages/accounts-google/google.js b/packages/accounts-google/google.js index ab5c4c97ea..306ef7882c 100644 --- a/packages/accounts-google/google.js +++ b/packages/accounts-google/google.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('google'); if (Meteor.isClient) { - Meteor.loginWithGoogle = function(options, callback) { + const loginWithGoogle = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -30,6 +30,10 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Google.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('google', loginWithGoogle); + Meteor.loginWithGoogle = function () { + return Accounts.applyLoginFunction('google', arguments); + }; } else { Accounts.addAutopublishFields({ forLoggedInUser: _.map( diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index 6c624c9e08..0300c8159d 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Login service for Google accounts", - version: "1.1.2" + version: "1.2.0" }); Package.onUse(function(api) { diff --git a/packages/accounts-meetup/meetup.js b/packages/accounts-meetup/meetup.js index 08e8ec0938..5a25dde195 100644 --- a/packages/accounts-meetup/meetup.js +++ b/packages/accounts-meetup/meetup.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('meetup'); if (Meteor.isClient) { - Meteor.loginWithMeetup = function(options, callback) { + const loginWithMeetup = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -11,6 +11,10 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Meetup.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('meetup', loginWithMeetup); + Meteor.loginWithMeetup = function () { + return Accounts.applyLoginFunction('meetup', arguments); + }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index ee19a9a8b9..325b535eda 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Login service for Meetup accounts', - version: '1.2.1' + version: '1.3.0' }); Package.onUse(function (api) { diff --git a/packages/accounts-meteor-developer/meteor-developer.js b/packages/accounts-meteor-developer/meteor-developer.js index 79ac4e4c2c..fed49ba759 100644 --- a/packages/accounts-meteor-developer/meteor-developer.js +++ b/packages/accounts-meteor-developer/meteor-developer.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService("meteor-developer"); if (Meteor.isClient) { - Meteor.loginWithMeteorDeveloperAccount = function (options, callback) { + const loginWithMeteorDeveloperAccount = function (options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -12,6 +12,10 @@ if (Meteor.isClient) { Accounts.oauth.credentialRequestCompleteHandler(callback); MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('meteor-developer', loginWithMeteorDeveloperAccount); + Meteor.loginWithMeteorDeveloperAccount = function () { + return Accounts.applyLoginFunction('meteor-developer', arguments); + }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately be used diff --git a/packages/accounts-meteor-developer/package.js b/packages/accounts-meteor-developer/package.js index a051ef61af..e788bad6b8 100644 --- a/packages/accounts-meteor-developer/package.js +++ b/packages/accounts-meteor-developer/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Login service for Meteor developer accounts', - version: '1.2.1' + version: '1.3.0' }); Package.onUse(function (api) { diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index 77fb819816..c43ea38a0b 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Password support for accounts", - version: "1.3.5" + version: "1.3.6" }); Package.onUse(function(api) { diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 9102743fec..b088b0aafd 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1126,5 +1126,3 @@ Meteor.users._ensureIndex('services.email.verificationTokens.token', {unique: 1, sparse: 1}); Meteor.users._ensureIndex('services.password.reset.token', {unique: 1, sparse: 1}); -Meteor.users._ensureIndex('services.password.reset.when', - {sparse: 1}); diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index e07a79061d..df68761a26 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Login service for Twitter accounts", - version: "1.2.1" + version: "1.3.0" }); Package.onUse(function(api) { diff --git a/packages/accounts-twitter/twitter.js b/packages/accounts-twitter/twitter.js index 176d82a655..1d44f474f2 100644 --- a/packages/accounts-twitter/twitter.js +++ b/packages/accounts-twitter/twitter.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('twitter'); if (Meteor.isClient) { - Meteor.loginWithTwitter = function(options, callback) { + const loginWithTwitter = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -11,6 +11,10 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Twitter.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('twitter', loginWithTwitter); + Meteor.loginWithTwitter = function () { + return Accounts.applyLoginFunction('twitter', arguments); + }; } else { var autopublishedFields = _.map( // don't send access token. https://dev.twitter.com/discussions/5025 diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index 6129b8a90a..b266fd83e9 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Login service for Sina Weibo accounts", - version: "1.1.1" + version: "1.2.0" }); Package.onUse(function(api) { diff --git a/packages/accounts-weibo/weibo.js b/packages/accounts-weibo/weibo.js index f55ec19bc7..b3fc92df0f 100644 --- a/packages/accounts-weibo/weibo.js +++ b/packages/accounts-weibo/weibo.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('weibo'); if (Meteor.isClient) { - Meteor.loginWithWeibo = function(options, callback) { + const loginWithWeibo = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -11,6 +11,10 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Weibo.requestCredential(options, credentialRequestCompleteCallback); }; + Accounts.registerClientLoginFunction('weibo', loginWithWeibo); + Meteor.loginWithWeibo = function () { + return Accounts.applyLoginFunction('weibo', arguments); + }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately diff --git a/packages/babel-compiler/.npm/package/npm-shrinkwrap.json b/packages/babel-compiler/.npm/package/npm-shrinkwrap.json index 214f819c77..a88dedc6c0 100644 --- a/packages/babel-compiler/.npm/package/npm-shrinkwrap.json +++ b/packages/babel-compiler/.npm/package/npm-shrinkwrap.json @@ -1,9 +1,9 @@ { "dependencies": { "acorn": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.11.tgz", - "from": "acorn@>=4.0.5 <4.1.0" + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "from": "acorn@>=5.0.0 <5.1.0" }, "ansi-regex": { "version": "2.1.1", @@ -21,29 +21,29 @@ "from": "babel-code-frame@>=6.22.0 <7.0.0" }, "babel-core": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", "from": "babel-core@>=6.22.1 <7.0.0" }, "babel-generator": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.0.tgz", - "from": "babel-generator@>=6.24.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", + "from": "babel-generator@>=6.24.1 <7.0.0" }, "babel-helper-builder-react-jsx": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.23.0.tgz", - "from": "babel-helper-builder-react-jsx@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.24.1.tgz", + "from": "babel-helper-builder-react-jsx@>=6.24.1 <7.0.0" }, "babel-helper-call-delegate": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.22.0.tgz", - "from": "babel-helper-call-delegate@>=6.22.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "from": "babel-helper-call-delegate@>=6.24.1 <7.0.0" }, "babel-helper-define-map": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.23.0.tgz", - "from": "babel-helper-define-map@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", + "from": "babel-helper-define-map@>=6.24.1 <7.0.0" }, "babel-helper-evaluate-path": { "version": "0.0.3", @@ -56,19 +56,19 @@ "from": "babel-helper-flip-expressions@>=0.0.2 <0.0.3" }, "babel-helper-function-name": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz", - "from": "babel-helper-function-name@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "from": "babel-helper-function-name@>=6.24.1 <7.0.0" }, "babel-helper-get-function-arity": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.22.0.tgz", - "from": "babel-helper-get-function-arity@>=6.22.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "from": "babel-helper-get-function-arity@>=6.24.1 <7.0.0" }, "babel-helper-hoist-variables": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.22.0.tgz", - "from": "babel-helper-hoist-variables@>=6.22.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "from": "babel-helper-hoist-variables@>=6.24.1 <7.0.0" }, "babel-helper-is-nodes-equiv": { "version": "0.0.1", @@ -86,14 +86,14 @@ "from": "babel-helper-mark-eval-scopes@>=0.0.3 <0.0.4" }, "babel-helper-optimise-call-expression": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.23.0.tgz", - "from": "babel-helper-optimise-call-expression@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "from": "babel-helper-optimise-call-expression@>=6.24.1 <7.0.0" }, "babel-helper-regex": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.22.0.tgz", - "from": "babel-helper-regex@>=6.22.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", + "from": "babel-helper-regex@>=6.24.1 <7.0.0" }, "babel-helper-remove-or-void": { "version": "0.0.1", @@ -101,9 +101,9 @@ "from": "babel-helper-remove-or-void@>=0.0.1 <0.0.2" }, "babel-helper-replace-supers": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.23.0.tgz", - "from": "babel-helper-replace-supers@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "from": "babel-helper-replace-supers@>=6.24.1 <7.0.0" }, "babel-helper-to-multiple-sequence-expressions": { "version": "0.0.3", @@ -111,9 +111,9 @@ "from": "babel-helper-to-multiple-sequence-expressions@>=0.0.3 <0.0.4" }, "babel-helpers": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.23.0.tgz", - "from": "babel-helpers@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "from": "babel-helpers@>=6.24.1 <7.0.0" }, "babel-messages": { "version": "6.23.0", @@ -131,8 +131,8 @@ "from": "babel-plugin-minify-constant-folding@>=0.0.4 <0.0.5", "dependencies": { "jsesc": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.4.0.tgz", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", "from": "jsesc@>=2.4.0 <3.0.0" } } @@ -199,6 +199,11 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", "from": "babel-plugin-syntax-async-generators@>=6.13.0 <7.0.0" }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "from": "babel-plugin-syntax-class-properties@>=6.8.0 <7.0.0" + }, "babel-plugin-syntax-dynamic-import": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", @@ -224,6 +229,11 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", "from": "babel-plugin-syntax-trailing-function-commas@>=6.22.0 <7.0.0" }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "from": "babel-plugin-transform-class-properties@>=6.24.1 <7.0.0" + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", @@ -235,18 +245,18 @@ "from": "babel-plugin-transform-es2015-block-scoped-functions@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-block-scoping": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.23.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", "from": "babel-plugin-transform-es2015-block-scoping@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-classes": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.23.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", "from": "babel-plugin-transform-es2015-classes@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-computed-properties": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.22.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", "from": "babel-plugin-transform-es2015-computed-properties@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-destructuring": { @@ -265,28 +275,28 @@ "from": "babel-plugin-transform-es2015-literals@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", "from": "babel-plugin-transform-es2015-modules-commonjs@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-modules-reify": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-reify/-/babel-plugin-transform-es2015-modules-reify-0.6.2.tgz", - "from": "babel-plugin-transform-es2015-modules-reify@>=0.6.0 <0.7.0" + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-reify/-/babel-plugin-transform-es2015-modules-reify-0.11.0.tgz", + "from": "babel-plugin-transform-es2015-modules-reify@>=0.11.0 <0.12.0" }, "babel-plugin-transform-es2015-object-super": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.22.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", "from": "babel-plugin-transform-es2015-object-super@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-parameters": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.23.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", "from": "babel-plugin-transform-es2015-parameters@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.22.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", "from": "babel-plugin-transform-es2015-shorthand-properties@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-spread": { @@ -295,8 +305,8 @@ "from": "babel-plugin-transform-es2015-spread@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.22.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", "from": "babel-plugin-transform-es2015-sticky-regex@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-template-literals": { @@ -310,8 +320,8 @@ "from": "babel-plugin-transform-es2015-typeof-symbol@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.22.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", "from": "babel-plugin-transform-es2015-unicode-regex@>=6.22.0 <7.0.0" }, "babel-plugin-transform-es3-property-literals": { @@ -360,9 +370,9 @@ "from": "babel-plugin-transform-react-display-name@>=6.23.0 <7.0.0" }, "babel-plugin-transform-react-jsx": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.23.0.tgz", - "from": "babel-plugin-transform-react-jsx@>=6.23.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", + "from": "babel-plugin-transform-react-jsx@>=6.24.1 <7.0.0" }, "babel-plugin-transform-react-jsx-self": { "version": "6.22.0", @@ -375,8 +385,8 @@ "from": "babel-plugin-transform-react-jsx-source@>=6.22.0 <7.0.0" }, "babel-plugin-transform-regenerator": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.22.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", "from": "babel-plugin-transform-regenerator@>=6.22.0 <7.0.0" }, "babel-plugin-transform-regexp-constructors": { @@ -410,9 +420,9 @@ "from": "babel-plugin-transform-simplify-comparison-operators@>=6.8.1 <7.0.0" }, "babel-plugin-transform-strict-mode": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.22.0.tgz", - "from": "babel-plugin-transform-strict-mode@>=6.22.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "from": "babel-plugin-transform-strict-mode@>=6.24.1 <7.0.0" }, "babel-plugin-transform-undefined-to-void": { "version": "6.8.0", @@ -430,19 +440,19 @@ "from": "babel-preset-flow@>=6.23.0 <7.0.0" }, "babel-preset-meteor": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-preset-meteor/-/babel-preset-meteor-6.25.0.tgz", - "from": "babel-preset-meteor@6.25.0" + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-preset-meteor/-/babel-preset-meteor-6.26.0.tgz", + "from": "babel-preset-meteor@6.26.0" }, "babel-preset-react": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.23.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", "from": "babel-preset-react@>=6.22.0 <7.0.0" }, "babel-register": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.0.tgz", - "from": "babel-register@>=6.24.0 <7.0.0" + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", + "from": "babel-register@>=6.24.1 <7.0.0" }, "babel-runtime": { "version": "6.23.0", @@ -450,23 +460,23 @@ "from": "babel-runtime@>=6.22.0 <7.0.0" }, "babel-template": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.23.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz", "from": "babel-template@>=6.22.0 <7.0.0" }, "babel-traverse": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.23.1.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz", "from": "babel-traverse@>=6.22.1 <7.0.0" }, "babel-types": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.23.0.tgz", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz", "from": "babel-types@>=6.22.0 <7.0.0" }, "babylon": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.16.1.tgz", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.0.tgz", "from": "babylon@>=6.15.0 <7.0.0" }, "balanced-match": { @@ -475,9 +485,9 @@ "from": "balanced-match@>=0.4.1 <0.5.0" }, "brace-expansion": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", - "from": "brace-expansion@>=1.0.0 <2.0.0" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "from": "brace-expansion@>=1.1.7 <2.0.0" }, "chalk": { "version": "1.1.3", @@ -490,8 +500,8 @@ "from": "concat-map@0.0.1" }, "convert-source-map": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.4.0.tgz", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", "from": "convert-source-map@>=1.3.0 <2.0.0" }, "core-js": { @@ -500,8 +510,8 @@ "from": "core-js@>=2.4.0 <3.0.0" }, "debug": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.3.tgz", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz", "from": "debug@>=2.1.1 <3.0.0" }, "detect-indent": { @@ -579,15 +589,10 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "from": "loose-envify@>=1.0.0 <2.0.0" }, - "magic-string": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.19.0.tgz", - "from": "magic-string@>=0.19.0 <0.20.0" - }, "meteor-babel": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/meteor-babel/-/meteor-babel-0.19.1.tgz", - "from": "meteor-babel@0.19.1" + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/meteor-babel/-/meteor-babel-0.21.2.tgz", + "from": "meteor-babel@0.21.2" }, "meteor-babel-helpers": { "version": "0.0.3", @@ -595,8 +600,8 @@ "from": "meteor-babel-helpers@0.0.3" }, "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "from": "minimatch@>=3.0.2 <4.0.0" }, "minimist": { @@ -604,15 +609,25 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "from": "minimist@0.0.8" }, + "minipass": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.0.1.tgz", + "from": "minipass@>=2.0.0 <3.0.0" + }, + "minizlib": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.3.tgz", + "from": "minizlib@>=1.0.3 <2.0.0" + }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "from": "mkdirp@>=0.5.1 <0.6.0" }, "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "from": "ms@0.7.2" + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.3.tgz", + "from": "ms@0.7.3" }, "number-is-nan": { "version": "1.0.1", @@ -645,14 +660,14 @@ "from": "regenerate@>=1.2.1 <2.0.0" }, "regenerator-runtime": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", "from": "regenerator-runtime@>=0.10.0 <0.11.0" }, "regenerator-transform": { - "version": "0.9.8", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.8.tgz", - "from": "regenerator-transform@0.9.8" + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", + "from": "regenerator-transform@0.9.11" }, "regexpu-core": { "version": "2.0.0", @@ -677,15 +692,20 @@ } }, "reify": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/reify/-/reify-0.6.6.tgz", - "from": "reify@>=0.6.2 <0.7.0" + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/reify/-/reify-0.11.0.tgz", + "from": "reify@>=0.11.0 <0.12.0" }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "from": "repeating@>=2.0.0 <3.0.0" }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "from": "semver@>=5.3.0 <6.0.0" + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -697,8 +717,8 @@ "from": "source-map@>=0.5.0 <0.6.0" }, "source-map-support": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.14.tgz", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", "from": "source-map-support@>=0.4.2 <0.5.0" }, "strip-ansi": { @@ -712,8 +732,8 @@ "from": "supports-color@>=2.0.0 <3.0.0" }, "to-fast-properties": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.2.tgz", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", "from": "to-fast-properties@>=1.0.1 <2.0.0" }, "trim-right": { @@ -721,10 +741,10 @@ "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "from": "trim-right@>=1.0.1 <2.0.0" }, - "vlq": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.1.tgz", - "from": "vlq@>=0.2.1 <0.3.0" + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "from": "yallist@>=3.0.0 <4.0.0" } } } diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 7a06290419..c10289c199 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -86,8 +86,8 @@ BCp.processOneFileForTarget = function (inputFile, source) { babelOptions.sourceMap = true; babelOptions.filename = babelOptions.sourceFileName = packageName - ? "/packages/" + packageName + "/" + inputFilePath - : "/" + inputFilePath; + ? "packages/" + packageName + "/" + inputFilePath + : inputFilePath; babelOptions.sourceMapTarget = babelOptions.filename + ".map"; diff --git a/packages/babel-compiler/babel.js b/packages/babel-compiler/babel.js index ac518d1034..13306f306b 100644 --- a/packages/babel-compiler/babel.js +++ b/packages/babel-compiler/babel.js @@ -21,6 +21,10 @@ Babel = { // Deprecated, now a no-op. validateExtraFeatures: Function.prototype, + parse: function (source) { + return Npm.require('meteor-babel').parse(source); + }, + compile: function (source, options) { var meteorBabel = Npm.require('meteor-babel'); options = options || getDefaultOptions(); diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index b229f435b1..c7f9d900bb 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -6,15 +6,15 @@ Package.describe({ // isn't possible because you can't publish a non-recommended // release with package versions that don't have a pre-release // identifier at the end (eg, -dev) - version: '6.19.0-beta.14' + version: '6.19.1-rc.1' }); Npm.depends({ - 'meteor-babel': '0.19.1' + 'meteor-babel': '0.21.2' }); Package.onUse(function (api) { - api.use('ecmascript-runtime'); + api.use('ecmascript-runtime', 'server'); api.addFiles([ 'babel.js', diff --git a/packages/boilerplate-generator/boilerplate-generator.js b/packages/boilerplate-generator/boilerplate-generator.js index 8385aef6b1..22e06e98f0 100644 --- a/packages/boilerplate-generator/boilerplate-generator.js +++ b/packages/boilerplate-generator/boilerplate-generator.js @@ -73,7 +73,10 @@ Boilerplate.prototype._generateBoilerplateFromManifestAndSource = if (item.type === 'css' && item.where === 'client') { boilerplateBaseData.css.push(itemObj); } - if (item.type === 'js' && item.where === 'client') { + if (item.type === 'js' && item.where === 'client' && + // Dynamic JS modules should not be loaded eagerly in the + // initial HTML of the app. + ! item.path.startsWith('dynamic/')) { boilerplateBaseData.js.push(itemObj); } if (item.type === 'head') { diff --git a/packages/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index 357701a5a2..b0afb5a3ea 100644 --- a/packages/boilerplate-generator/package.js +++ b/packages/boilerplate-generator/package.js @@ -1,15 +1,15 @@ Package.describe({ summary: "Generates the boilerplate html from program's manifest", - version: '1.0.11' + version: '1.1.0-rc.1' }); Package.onUse(function (api) { api.use([ - 'underscore@1.0.9', - 'spacebars-compiler@1.0.12', - 'spacebars@1.0.12', - 'htmljs@1.0.10', - 'ui@1.0.11', + 'underscore', + 'spacebars-compiler', + 'spacebars', + 'htmljs', + 'ui', ], 'server'); api.addFiles(['boilerplate-generator.js'], 'server'); api.export(['Boilerplate'], 'server'); diff --git a/packages/dynamic-import/cache.js b/packages/dynamic-import/cache.js index 49f883ea67..e78b9b90de 100644 --- a/packages/dynamic-import/cache.js +++ b/packages/dynamic-import/cache.js @@ -98,15 +98,18 @@ exports.checkMany = function (versions) { return Promise.all(ids.map(function (id) { return new Promise(function (resolve, reject) { - var sourceRequest = sourcesByVersion.get(versions[id]); - sourceRequest.onerror = makeOnError(reject, "sourcesByVersion.get"); - sourceRequest.onsuccess = function (event) { - var result = event.target.result; - if (result) { - sourcesById[id] = result.source; - } - resolve(); - }; + var version = versions[id]; + if (version) { + var sourceRequest = sourcesByVersion.get(versions[id]); + sourceRequest.onerror = makeOnError(reject, "sourcesByVersion.get"); + sourceRequest.onsuccess = function (event) { + var result = event.target.result; + if (result) { + sourcesById[id] = result.source; + } + resolve(); + }; + } else resolve(); }); })).then(finish, finish); }); diff --git a/packages/dynamic-import/client.js b/packages/dynamic-import/client.js index cf04e8a304..a4bce902b8 100644 --- a/packages/dynamic-import/client.js +++ b/packages/dynamic-import/client.js @@ -1,161 +1,130 @@ var Module = module.constructor; -var delayPromise = Promise.resolve(); -var requireMeta = meteorInstall._requireMeta; var cache = require("./cache.js"); +// Call module.dynamicImport(id) to fetch a module and any/all of its +// dependencies that have not already been fetched, and evaluate them as +// soon as they arrive. This runtime API makes it very easy to implement +// ECMAScript dynamic import(...) syntax. Module.prototype.dynamicImport = function (id) { - // The real (not meta) parent module. var module = this; - - function get() { + return module.prefetch(id).then(function () { return getNamespace(module, id); - } - - return delayPromise.then(get).catch(function (error) { - var message = error.message; - if (! (message && - message.startsWith("Cannot find module"))) { - throw error; - } - - // Require the parent module from the complete meta graph. - var meta = requireMeta(module.id); - var versions = Object.create(null); - - function walk(meta) { - if (meta.dynamic && ! meta.pending) { - meta.pending = true; - versions[meta.module.id] = meta.version; - meta.eachChild(walkChild); - } - } - - function walkChild(childModule) { - return walk(childModule.exports); - } - - meta.eachChild(walkChild, [id]); - - var localTree; - var missingTree; - - return cache.checkMany(versions).then(function (sources) { - Object.keys(sources).forEach(function (id) { - var source = sources[id]; - if (source) { - addToTree(localTree = localTree || Object.create(null), id, source); - } else { - addToTree(missingTree = missingTree || Object.create(null), id, 1); - } - }); - - if (localTree) { - installResults(localTree, true); - } - - return missingTree && fetchMissing(missingTree); - }).then(get); }); }; -// Results from fetchMissing must be delivered in the same order as calls -// to fetchMissing, because previous results may include modules needed by -// more recent calls. In practice, results are usually delivered in order, -// but might be delivered out of order because the __dynamicImport method -// calls this.unblock(). To achieve this ordering of results while still -// allowing parallel __dynamicImport method calls, we keep track of the -// most recent Promise returned by fetchMissing, and delay resolving the -// next Promise until the previous Promise has been resolved or rejected. -var lastFetchMissingPromise = delayPromise; +// Called by Module.prototype.prefetch if there are any missing dynamic +// modules that need to be fetched. +meteorInstall.fetch = function (ids) { + var tree = Object.create(null); + var versions = Object.create(null); + var dynamicVersions = require("./dynamic-versions.js"); + var missing; + + Object.keys(ids).forEach(function (id) { + var version = getFromTree(dynamicVersions, id); + if (version) { + versions[id] = version; + } else { + addToTree(missing = missing || Object.create(null), id, 1); + } + }); + + return cache.checkMany(versions).then(function (sources) { + Object.keys(sources).forEach(function (id) { + var source = sources[id]; + if (source) { + var info = ids[id]; + addToTree(tree, id, makeModuleFunction(source, info.options)); + } else { + addToTree(missing = missing || Object.create(null), id, 1); + } + }); + + return missing && fetchMissing(missing).then(function (results) { + var versionsAndSourcesById = Object.create(null); + var flatResults = flattenModuleTree(results); + + Object.keys(flatResults).forEach(function (id) { + var source = flatResults[id]; + var info = ids[id]; + + addToTree(tree, id, makeModuleFunction(source, info.options)); + + var version = getFromTree(dynamicVersions, id); + if (version) { + versionsAndSourcesById[id] = { + version: version, + source: source + }; + } + }); + + cache.setMany(versionsAndSourcesById); + }); + + }).then(function () { + return tree; + }); +}; + +function flattenModuleTree(tree) { + var parts = [""]; + var result = Object.create(null); + + function walk(t) { + if (t && typeof t === "object") { + Object.keys(t).forEach(function (key) { + parts.push(key); + walk(t[key]); + parts.pop(); + }); + } else if (typeof t === "string") { + result[parts.join("/")] = t; + } + } + + walk(tree); + + return result; +} + +function makeModuleFunction(source, options) { + // By calling (options && options.eval || eval) in a wrapper function, + // we delay the cost of parsing and evaluating the module code until the + // module is first imported. + return function () { + // If an options.eval function was provided in the second argument to + // meteorInstall when this bundle was first installed, use that + // function to parse and evaluate the dynamic module code in the scope + // of the package. Otherwise fall back to indirect (global) eval. + return (options && options.eval || eval)( + // Wrap the function(require,exports,module){...} expression in + // parentheses to force it to be parsed as an expression. + "(" + source + ")" + ).apply(this, arguments); + }; +} function fetchMissing(missingTree) { - // Save the Promise that was most recent when fetchMissing was called. - var previousPromise = lastFetchMissingPromise; - // Update lastFetchMissingPromise immediately, without waiting for // the results to be delivered. - return lastFetchMissingPromise = new Promise(function (resolve, reject) { + return new Promise(function (resolve, reject) { Meteor.call( "__dynamicImport", missingTree, function (error, resultsTree) { - if (error) { - reject(error); - } else { - resolve = resolve.bind(null, resultsTree) - // Continue even if previousPromise was rejected. - previousPromise.then(resolve, resolve); - } + error ? reject(error) : resolve(resultsTree); } ); - }).then(installResults); + }); } -function installResults(resultsTree, doNotCache) { - var parts = [""]; - var trees = []; - var options = []; - var versionsAndSourcesById = Object.create(null); - - function walk(tree) { - if (typeof tree === "string") { - var meta = requireMeta(parts.join("/")); - var optionsIndex = options.indexOf(meta.options); - if (optionsIndex < 0) { - options[optionsIndex = options.length] = meta.options; - trees.push(Object.create(null)); - } - - // The results tree is partitioned into separate trees according - // to the meta.options object that governs the tree. Usually the - // number of trees will be approximately one, because options - // are shared by entire bundles. - addToTree( - trees[optionsIndex], - meta.module.id, - // By calling (meta.options.eval || eval) in a wrapper function, - // we delay the cost of parsing and evaluating the module code - // until the module is first imported. - function () { - // If an options.eval function was provided in the second - // argument to meteorInstall when this bundle was first - // installed, use that function to parse and evaluate the - // dynamic module code in the scope of the package. Otherwise - // fall back to indirect (global) eval. - return (meta.options.eval || eval)( - // Wrap the function(require,exports,module){...} expression - // in parentheses to force it to be parsed as an expression. - "(" + tree + ")" - ).apply(this, arguments); - } - ); - - // Intentionally do not delay resolution waiting for the cache. - if (! doNotCache) { - versionsAndSourcesById[meta.module.id] = { - version: meta.version, - source: tree - }; - } - - } else { - Object.keys(tree).forEach(function (name) { - parts.push(name); - walk(tree[name]); - parts.pop(name); - }); - } - } - - walk(resultsTree); - - trees.forEach(function (tree, i) { - meteorInstall(tree, options[i]); +function getFromTree(tree, id) { + id.split("/").every(function (part) { + return ! part || (tree = tree[part]); }); - if (! doNotCache) { - cache.setMany(versionsAndSourcesById); - } + return tree; } function addToTree(tree, id, value) { @@ -172,7 +141,7 @@ function addToTree(tree, id, value) { function getNamespace(module, id) { var namespace = Object.create(null); - module.import(id, { + module.watch(module.require(id), { "*": function (value, name) { namespace[name] = value; } diff --git a/packages/dynamic-import/dynamic-versions.js b/packages/dynamic-import/dynamic-versions.js new file mode 100644 index 0000000000..7599278028 --- /dev/null +++ b/packages/dynamic-import/dynamic-versions.js @@ -0,0 +1,4 @@ +// This magic double-underscored identifier gets replaced in +// tools/isobuild/bundler.js with a tree of hashes of all dynamic +// modules, for use in client.js and cache.js. +module.exports = __DYNAMIC_VERSIONS__; diff --git a/packages/dynamic-import/package.js b/packages/dynamic-import/package.js index 6b59645bbd..9d77d594d1 100644 --- a/packages/dynamic-import/package.js +++ b/packages/dynamic-import/package.js @@ -1,11 +1,15 @@ Package.describe({ name: "dynamic-import", - version: "0.1.0-beta.14", - summary: "Support for module.dynamicImport(id).then(namespace => ...)", + version: "0.1.0-rc.1", + summary: "Runtime support for Meteor 1.5 dynamic import(...) syntax", documentation: "README.md" }); Package.onUse(function (api) { + // Do not allow this package to be used in pre-Meteor 1.5 apps. + api.use("isobuild:dynamic-import@1.5.0"); + + // Modify browser policy only if browser-policy packages are used. api.use("browser-policy-content", { weak: true }); api.use("modules"); diff --git a/packages/ecmascript-runtime-client/README.md b/packages/ecmascript-runtime-client/README.md new file mode 100644 index 0000000000..1b24dc649f --- /dev/null +++ b/packages/ecmascript-runtime-client/README.md @@ -0,0 +1,6 @@ +# ecmascript-runtime-client +[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/ecmascript-runtime-client) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client) +*** + +[![Build Status](https://travis-ci.org/meteor/ecmascript-runtime.svg?branch=master)](https://travis-ci.org/meteor/ecmascript-runtime) +Polyfills for new ECMAScript 2015 APIs like Map and Set diff --git a/packages/ecmascript-runtime-client/package.js b/packages/ecmascript-runtime-client/package.js new file mode 100644 index 0000000000..1bd6b8466a --- /dev/null +++ b/packages/ecmascript-runtime-client/package.js @@ -0,0 +1,20 @@ +Package.describe({ + name: "ecmascript-runtime-client", + version: "0.4.0", + summary: "Polyfills for new ECMAScript 2015 APIs like Map and Set", + git: "https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client", + documentation: "README.md" +}); + +Package.onUse(function(api) { + // If the es5-shim package is installed, make sure it loads before + // ecmascript-runtime-server, since the runtime uses some ES5 APIs like + // Object.defineProperties that are buggy in older browsers. + api.use("es5-shim", { weak: true }); + api.use("modules", "client"); + api.use("promise", "client"); + api.mainModule("runtime.js", "client"); + api.export("Symbol", "client"); + api.export("Map", "client"); + api.export("Set", "client"); +}); diff --git a/packages/ecmascript-runtime-client/runtime.js b/packages/ecmascript-runtime-client/runtime.js new file mode 100644 index 0000000000..58e3dbe682 --- /dev/null +++ b/packages/ecmascript-runtime-client/runtime.js @@ -0,0 +1,127 @@ +try { + require("core-js/modules/es6.symbol"); + require("core-js/modules/es6.map"); + require("core-js/modules/es6.set"); +} catch (e) { + throw new Error([ + "The core-js npm package could not be found in your node_modules ", + "directory. Please run the following command to install it:", + "", + " meteor npm install --save core-js", + "" + ].join("\n")); +} + +var core = require("core-js/modules/_core"); +Symbol = exports.Symbol = core.Symbol; +Map = exports.Map = core.Map; +Set = exports.Set = core.Set; + +// List of polyfills generated by babel-preset-env with the following +// .babelrc configuration: +// +// { +// "presets": [ +// ["env", { +// "targets": { +// "browsers": [ +// "last 3 versions" +// ] +// }, +// "polyfill": true, +// "useBuiltIns": true +// }] +// ] +// } +// +// Supported browsers: http://browserl.ist/?q=last+3+versions +// +// Note that the es6.reflect.* modules have been commented out for bundle +// size reasons, and the es6.promise modules are not used because Meteor +// provides its own Promise polyfill. + +require("core-js/modules/es6.typed.array-buffer"); +require("core-js/modules/es6.typed.data-view"); +require("core-js/modules/es6.typed.int8-array"); +require("core-js/modules/es6.typed.uint8-array"); +require("core-js/modules/es6.typed.uint8-clamped-array"); +require("core-js/modules/es6.typed.int16-array"); +require("core-js/modules/es6.typed.uint16-array"); +require("core-js/modules/es6.typed.int32-array"); +require("core-js/modules/es6.typed.uint32-array"); +require("core-js/modules/es6.typed.float32-array"); +require("core-js/modules/es6.typed.float64-array"); +require("core-js/modules/es6.weak-map"); +require("core-js/modules/es6.weak-set"); +// require("core-js/modules/es6.reflect.apply"); +// require("core-js/modules/es6.reflect.construct"); +// require("core-js/modules/es6.reflect.define-property"); +// require("core-js/modules/es6.reflect.delete-property"); +// require("core-js/modules/es6.reflect.get"); +// require("core-js/modules/es6.reflect.get-own-property-descriptor"); +// require("core-js/modules/es6.reflect.get-prototype-of"); +// require("core-js/modules/es6.reflect.has"); +// require("core-js/modules/es6.reflect.is-extensible"); +// require("core-js/modules/es6.reflect.own-keys"); +// require("core-js/modules/es6.reflect.prevent-extensions"); +// require("core-js/modules/es6.reflect.set"); +// require("core-js/modules/es6.reflect.set-prototype-of"); +require("core-js/modules/es6.object.assign"); +require("core-js/modules/es6.object.is"); +require("core-js/modules/es6.object.set-prototype-of"); +// require("core-js/modules/es6.promise"); +require("core-js/modules/es6.function.bind"); +require("core-js/modules/es6.function.name"); +require("core-js/modules/es6.function.has-instance"); +require("core-js/modules/es6.string.raw"); +require("core-js/modules/es6.string.from-code-point"); +require("core-js/modules/es6.string.code-point-at"); +require("core-js/modules/es6.string.repeat"); +require("core-js/modules/es6.string.starts-with"); +require("core-js/modules/es6.string.ends-with"); +require("core-js/modules/es6.string.includes"); +require("core-js/modules/es6.regexp.flags"); +require("core-js/modules/es6.regexp.match"); +require("core-js/modules/es6.regexp.replace"); +require("core-js/modules/es6.regexp.split"); +require("core-js/modules/es6.regexp.search"); +require("core-js/modules/es6.array.from"); +require("core-js/modules/es6.array.of"); +require("core-js/modules/es6.array.copy-within"); +require("core-js/modules/es6.array.find"); +require("core-js/modules/es6.array.find-index"); +require("core-js/modules/es6.array.fill"); +require("core-js/modules/es6.array.iterator"); +require("core-js/modules/es6.number.is-finite"); +require("core-js/modules/es6.number.is-integer"); +require("core-js/modules/es6.number.is-safe-integer"); +require("core-js/modules/es6.number.is-nan"); +require("core-js/modules/es6.number.epsilon"); +require("core-js/modules/es6.number.min-safe-integer"); +require("core-js/modules/es6.number.max-safe-integer"); +require("core-js/modules/es6.math.acosh"); +require("core-js/modules/es6.math.asinh"); +require("core-js/modules/es6.math.atanh"); +require("core-js/modules/es6.math.cbrt"); +require("core-js/modules/es6.math.clz32"); +require("core-js/modules/es6.math.cosh"); +require("core-js/modules/es6.math.expm1"); +require("core-js/modules/es6.math.fround"); +require("core-js/modules/es6.math.hypot"); +require("core-js/modules/es6.math.imul"); +require("core-js/modules/es6.math.log1p"); +require("core-js/modules/es6.math.log10"); +require("core-js/modules/es6.math.log2"); +require("core-js/modules/es6.math.sign"); +require("core-js/modules/es6.math.sinh"); +require("core-js/modules/es6.math.tanh"); +require("core-js/modules/es6.math.trunc"); +require("core-js/modules/es7.array.includes"); +require("core-js/modules/es7.object.values"); +require("core-js/modules/es7.object.entries"); +require("core-js/modules/es7.object.get-own-property-descriptors"); +require("core-js/modules/es7.string.pad-start"); +require("core-js/modules/es7.string.pad-end"); +require("core-js/modules/web.timers"); +require("core-js/modules/web.immediate"); +require("core-js/modules/web.dom.iterable"); diff --git a/packages/ecmascript-runtime/.npm/package/.gitignore b/packages/ecmascript-runtime-server/.npm/package/.gitignore similarity index 100% rename from packages/ecmascript-runtime/.npm/package/.gitignore rename to packages/ecmascript-runtime-server/.npm/package/.gitignore diff --git a/packages/ecmascript-runtime/.npm/package/README b/packages/ecmascript-runtime-server/.npm/package/README similarity index 100% rename from packages/ecmascript-runtime/.npm/package/README rename to packages/ecmascript-runtime-server/.npm/package/README diff --git a/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json b/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json new file mode 100644 index 0000000000..e54b59acd6 --- /dev/null +++ b/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "core-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "from": "core-js@2.4.1" + } + } +} diff --git a/packages/ecmascript-runtime-server/README.md b/packages/ecmascript-runtime-server/README.md new file mode 100644 index 0000000000..d92f6a94ee --- /dev/null +++ b/packages/ecmascript-runtime-server/README.md @@ -0,0 +1,6 @@ +# ecmascript-runtime-server +[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/ecmascript-runtime-server) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-server) +*** + +[![Build Status](https://travis-ci.org/meteor/ecmascript-runtime.svg?branch=master)](https://travis-ci.org/meteor/ecmascript-runtime) +Polyfills for new ECMAScript 2015 APIs like Map and Set diff --git a/packages/ecmascript-runtime-server/package.js b/packages/ecmascript-runtime-server/package.js new file mode 100644 index 0000000000..38b80d3836 --- /dev/null +++ b/packages/ecmascript-runtime-server/package.js @@ -0,0 +1,23 @@ +Package.describe({ + name: "ecmascript-runtime-server", + version: "0.4.0", + summary: "Polyfills for new ECMAScript 2015 APIs like Map and Set", + git: "https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client", + documentation: "README.md" +}); + +Npm.depends({ + "core-js": "2.4.1" +}); + +Package.onUse(function(api) { + // If the es5-shim package is installed, make sure it loads before + // ecmascript-runtime-server, since the runtime uses some ES5 APIs like + // Object.defineProperties that are buggy in older browsers. + api.use("es5-shim", { weak: true }); + api.use(["modules", "promise"], "server"); + api.mainModule("runtime.js", "server"); + api.export("Symbol", "server"); + api.export("Map", "server"); + api.export("Set", "server"); +}); diff --git a/packages/ecmascript-runtime-server/runtime.js b/packages/ecmascript-runtime-server/runtime.js new file mode 100644 index 0000000000..1e41296fb4 --- /dev/null +++ b/packages/ecmascript-runtime-server/runtime.js @@ -0,0 +1,71 @@ +// The ecmascript-runtime-server package depends on its own copy of +// core-js using Npm.depends, so we don't have to check that core-js is +// available (as we do in ecmascript-runtime-client/runtime.js). + +require("core-js/modules/es6.symbol"); +require("core-js/modules/es6.map"); +require("core-js/modules/es6.set"); + +var core = require("core-js/modules/_core"); +Symbol = exports.Symbol = core.Symbol; +Map = exports.Map = core.Map; +Set = exports.Set = core.Set; + +// List of polyfills generated by babel-preset-env with the following +// .babelrc configuration: +// +// { +// "presets": [ +// ["env", { +// "targets": { +// "node": 4 +// }, +// "polyfill": true, +// "useBuiltIns": true +// }] +// ] +// } +// +// Note that the es6.reflect.* modules have been commented out for bundle +// size reasons. + +require("core-js/modules/es6.typed.array-buffer"); +require("core-js/modules/es6.typed.int8-array"); +require("core-js/modules/es6.typed.uint8-array"); +require("core-js/modules/es6.typed.uint8-clamped-array"); +require("core-js/modules/es6.typed.int16-array"); +require("core-js/modules/es6.typed.uint16-array"); +require("core-js/modules/es6.typed.int32-array"); +require("core-js/modules/es6.typed.uint32-array"); +require("core-js/modules/es6.typed.float32-array"); +require("core-js/modules/es6.typed.float64-array"); +require("core-js/modules/es6.weak-map"); +require("core-js/modules/es6.weak-set"); +// require("core-js/modules/es6.reflect.apply"); +// require("core-js/modules/es6.reflect.construct"); +// require("core-js/modules/es6.reflect.define-property"); +// require("core-js/modules/es6.reflect.delete-property"); +// require("core-js/modules/es6.reflect.get"); +// require("core-js/modules/es6.reflect.get-own-property-descriptor"); +// require("core-js/modules/es6.reflect.get-prototype-of"); +// require("core-js/modules/es6.reflect.has"); +// require("core-js/modules/es6.reflect.is-extensible"); +// require("core-js/modules/es6.reflect.own-keys"); +// require("core-js/modules/es6.reflect.prevent-extensions"); +// require("core-js/modules/es6.reflect.set"); +// require("core-js/modules/es6.reflect.set-prototype-of"); +require("core-js/modules/es6.function.bind"); +require("core-js/modules/es6.function.name"); +require("core-js/modules/es6.function.has-instance"); +require("core-js/modules/es6.regexp.flags"); +require("core-js/modules/es6.regexp.match"); +require("core-js/modules/es6.regexp.replace"); +require("core-js/modules/es6.regexp.split"); +require("core-js/modules/es6.regexp.search"); +require("core-js/modules/es6.array.from"); +require("core-js/modules/es7.array.includes"); +require("core-js/modules/es7.object.values"); +require("core-js/modules/es7.object.entries"); +require("core-js/modules/es7.object.get-own-property-descriptors"); +require("core-js/modules/es7.string.pad-start"); +require("core-js/modules/es7.string.pad-end"); diff --git a/packages/ecmascript-runtime/.npm/package/npm-shrinkwrap.json b/packages/ecmascript-runtime/.npm/package/npm-shrinkwrap.json deleted file mode 100644 index 2d4e33d663..0000000000 --- a/packages/ecmascript-runtime/.npm/package/npm-shrinkwrap.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "dependencies": { - "core-js": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "from": "core-js@2.4.1" - }, - "meteor-ecmascript-runtime": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/meteor-ecmascript-runtime/-/meteor-ecmascript-runtime-0.2.9.tgz", - "from": "meteor-ecmascript-runtime@0.2.9" - } - } -} diff --git a/packages/ecmascript-runtime/package.js b/packages/ecmascript-runtime/package.js index 78b193e14e..1f7b66f00a 100644 --- a/packages/ecmascript-runtime/package.js +++ b/packages/ecmascript-runtime/package.js @@ -1,29 +1,14 @@ Package.describe({ name: "ecmascript-runtime", - version: "0.3.15", + version: "0.4.0", summary: "Polyfills for new ECMAScript 2015 APIs like Map and Set", git: "https://github.com/meteor/ecmascript-runtime", documentation: "README.md" }); -Npm.depends({ - "meteor-ecmascript-runtime": "0.2.9", -}); - Package.onUse(function(api) { - // If the es5-shim package is installed, make sure it loads before - // ecmascript-runtime, since ecmascript-runtime uses some ES5 APIs like - // Object.defineProperties that are buggy in older browsers. - api.use("es5-shim@4.6.13", { weak: true }); - - api.use("modules@0.7.5"); - api.use("promise@0.8.3"); - - api.mainModule("runtime.js"); - - api.export("Symbol"); - api.export("Map"); - api.export("Set"); + api.imply("ecmascript-runtime-client", "client"); + api.imply("ecmascript-runtime-server", "server"); }); Package.onTest(function(api) { diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index b40144c74e..63c75277ad 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.8.0-beta.14', + version: '0.8.0-rc.1', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md' }); diff --git a/packages/email/.npm/package/npm-shrinkwrap.json b/packages/email/.npm/package/npm-shrinkwrap.json index 4028b03706..aebb613b70 100644 --- a/packages/email/.npm/package/npm-shrinkwrap.json +++ b/packages/email/.npm/package/npm-shrinkwrap.json @@ -1,79 +1,14 @@ { "dependencies": { - "addressparser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", - "from": "addressparser@1.0.1" - }, - "buildmail": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-4.0.1.tgz", - "from": "buildmail@4.0.1" - }, - "httpntlm": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz", - "from": "httpntlm@1.6.1" - }, - "httpreq": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.23.tgz", - "from": "httpreq@>=0.4.22" - }, - "iconv-lite": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "from": "iconv-lite@0.4.15" - }, - "libbase64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz", - "from": "libbase64@0.1.0" - }, - "libmime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-3.0.0.tgz", - "from": "libmime@3.0.0" - }, - "libqp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz", - "from": "libqp@1.1.0" - }, - "mailcomposer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-4.0.1.tgz", - "from": "mailcomposer@4.0.1" - }, - "nodemailer-fetch": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz", - "from": "nodemailer-fetch@1.6.0" - }, - "nodemailer-shared": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz", - "from": "nodemailer-shared@1.1.0" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "from": "punycode@1.4.1" - }, - "smtp-connection": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.2.tgz", - "from": "smtp-connection@2.12.2" + "node4mailer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/node4mailer/-/node4mailer-4.0.2.tgz", + "from": "node4mailer@4.0.2" }, "stream-buffers": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-0.2.5.tgz", "from": "stream-buffers@0.2.5" - }, - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "from": "underscore@>=1.7.0 <1.8.0" } } } diff --git a/packages/email/email.js b/packages/email/email.js index ae8e652eba..af3c633d69 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -1,6 +1,6 @@ var Future = Npm.require('fibers/future'); var urlModule = Npm.require('url'); -var SMTPConnection = Npm.require('smtp-connection'); +var nodemailer = Npm.require('node4mailer'); Email = {}; EmailTest = {}; @@ -8,61 +8,60 @@ EmailTest = {}; EmailInternals = { NpmModules: { mailcomposer: { - version: Npm.require('mailcomposer/package.json').version, - module: Npm.require('mailcomposer') + version: Npm.require('node4mailer/package.json').version, + module: Npm.require('node4mailer/lib/mail-composer') + }, + nodemailer: { + version: Npm.require('node4mailer/package.json').version, + module: Npm.require('node4mailer') } } }; -var mailcomposer = EmailInternals.NpmModules.mailcomposer.module; +var MailComposer = EmailInternals.NpmModules.mailcomposer.module; -var makePool = function (mailUrlString) { - var mailUrl = urlModule.parse(mailUrlString); - if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') +var makeTransport = function (mailUrlString) { + var mailUrl = urlModule.parse(mailUrlString, true); + + if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') { throw new Error("Email protocol in $MAIL_URL (" + mailUrlString + ") must be 'smtp' or 'smtps'"); - - var port = +(mailUrl.port); - var auth = false; - if (mailUrl.auth) { - var parts = mailUrl.auth.split(':', 2); - auth = {user: parts[0], - pass: parts[1]}; } - var pool = new SMTPConnection({ - port: port, // Defaults to 25 - host: mailUrl.hostname, // Defaults to "localhost" - secure: (port === 465) || (mailUrl.protocol === 'smtps:') - }); - Meteor.wrapAsync(pool.connect, pool)(); - if (auth) { - //_.bind(Future.wrap(pool.login), pool)(auth).wait(); - Meteor.wrapAsync(pool.login, pool)(auth); + // Allow overriding pool setting, but default to true. + if (!mailUrl.query) { + mailUrl.query = {}; } - pool._syncSend = Meteor.wrapAsync(pool.send, pool); - return pool; + if (!mailUrl.query.pool) { + mailUrl.query.pool = 'true'; + } + + var transport = nodemailer.createTransport( + urlModule.format(mailUrl)); + + transport._syncSendMail = Meteor.wrapAsync(transport.sendMail, transport); + return transport; }; -var getPool = function() { +var getTransport = function() { // We delay this check until the first call to Email.send, in case someone // set process.env.MAIL_URL in startup code. Then we store in a cache until // process.env.MAIL_URL changes. var url = process.env.MAIL_URL; if (this.cacheKey === undefined || this.cacheKey !== url) { this.cacheKey = url; - this.cache = url ? makePool(url) : null; + this.cache = url ? makeTransport(url) : null; } return this.cache; } -var next_devmode_mail_id = 0; +var nextDevModeMailId = 0; var output_stream = process.stdout; // Testing hooks EmailTest.overrideOutputStream = function (stream) { - next_devmode_mail_id = 0; + nextDevModeMailId = 0; output_stream = stream; }; @@ -70,27 +69,27 @@ EmailTest.restoreOutputStream = function () { output_stream = process.stdout; }; -var devModeSend = function (mc) { - var devmode_mail_id = next_devmode_mail_id++; +var devModeSend = function (mail) { + var devModeMailId = nextDevModeMailId++; var stream = output_stream; // This approach does not prevent other writers to stdout from interleaving. - stream.write("====== BEGIN MAIL #" + devmode_mail_id + " ======\n"); + stream.write("====== BEGIN MAIL #" + devModeMailId + " ======\n"); stream.write("(Mail not sent; to enable sending, set the MAIL_URL " + "environment variable.)\n"); - var readStream = mc.createReadStream(); + var readStream = new MailComposer(mail).compile().createReadStream(); readStream.pipe(stream, {end: false}); var future = new Future; readStream.on('end', function () { - stream.write("====== END MAIL #" + devmode_mail_id + " ======\n"); + stream.write("====== END MAIL #" + devModeMailId + " ======\n"); future.return(); }); future.wait(); }; -var smtpSend = function (pool, mc) { - pool._syncSend(mc.getEnvelope(), mc.createReadStream()); +var smtpSend = function (transport, mail) { + transport._syncSendMail(mail); }; /** @@ -105,28 +104,6 @@ EmailTest.hookSend = function (f) { sendHooks.push(f); }; -// Old comment below -/** - * Send an email. - * - * Connects to the mail server configured via the MAIL_URL environment - * variable. If unset, prints formatted message to stdout. The "from" option - * is required, and at least one of "to", "cc", and "bcc" must be provided; - * all other options are optional. - * - * @param options - * @param options.from {String} RFC5322 "From:" address - * @param options.to {String|String[]} RFC5322 "To:" address[es] - * @param options.cc {String|String[]} RFC5322 "Cc:" address[es] - * @param options.bcc {String|String[]} RFC5322 "Bcc:" address[es] - * @param options.replyTo {String|String[]} RFC5322 "Reply-To:" address[es] - * @param options.subject {String} RFC5322 "Subject:" line - * @param options.text {String} RFC5322 mail body (plain text) - * @param options.html {String} RFC5322 mail body (HTML) - * @param options.headers {Object} custom RFC5322 headers (dictionary) - */ - -// New API doc comment below /** * @summary Send an email. Throws an `Error` on failure to contact mail server * or if mail server returns an error. All fields should match @@ -135,10 +112,9 @@ EmailTest.hookSend = function (f) { * If the `MAIL_URL` environment variable is set, actually sends the email. * Otherwise, prints the contents of the email to standard out. * - * Note that this package is based on mailcomposer version `4.0.1`, so make - * sure to refer to the documentation for that version if using the - * `attachments` or `mailComposer` options. - * [Click here to read the mailcomposer 4.0.1 docs](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md). + * Note that this package is based on **mailcomposer 4**, so make sure to refer to + * [the documentation](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md) + * for that version when using the `attachments` or `mailComposer` options. * * @locus Server * @param {Object} options @@ -150,44 +126,29 @@ EmailTest.hookSend = function (f) { * @param {String} [options.messageId] Message-ID for this message; otherwise, will be set to a random value * @param {String} [options.subject] "Subject:" line * @param {String} [options.text|html] Mail body (in plain text and/or HTML) - * @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch - * @param {String} [options.icalEvent] iCalendar event attachment + * @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch + * @param {String} [options.icalEvent] iCalendar event attachment * @param {Object} [options.headers] Dictionary of custom headers * @param {Object[]} [options.attachments] Array of attachment objects, as * described in the [mailcomposer documentation](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md#attachments). - * @param {MailComposer} [options.mailComposer] A [MailComposer](https://github.com/andris9/mailcomposer) - * object (or its `compile()` output) representing the message to be sent. - * Overrides all other options. You can access the `mailcomposer` npm module at - * `EmailInternals.NpmModules.mailcomposer.module`. This module is a function - * which assembles a MailComposer object and immediately `compile()`s it. - * Alternatively, you can create and pass a MailComposer object via - * `new EmailInternals.NpmModules.mailcomposer.module.MailComposer`. + * @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields) + * object representing the message to be sent. Overrides all other options. + * You can create a `MailComposer` object via + * `new EmailInternals.NpmModules.mailcomposer.module`. */ Email.send = function (options) { for (var i = 0; i < sendHooks.length; i++) if (! sendHooks[i](options)) return; - var mc; if (options.mailComposer) { - mc = options.mailComposer; - if (mc.compile) { - mc = mc.compile(); - } - } else { - // mailcomposer now automatically adds date if omitted - //if (!options.hasOwnProperty('date') && - // (!options.headers || !options.headers.hasOwnProperty('Date'))) { - // options['date'] = new Date().toUTCString().replace(/GMT/, '+0000'); - //} - - mc = mailcomposer(options); + options = options.mailComposer.mail; } - var pool = getPool(); - if (pool) { - smtpSend(pool, mc); + var transport = getTransport(); + if (transport) { + smtpSend(transport, options); } else { - devModeSend(mc); + devModeSend(options); } }; diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index 6fe9a30954..97c0977660 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -22,7 +22,7 @@ function canonicalize(string) { // Remove generated content for test.equal to succeed. return string.replace(/Message-ID: <[^<>]*>\r\n/, "Message-ID: <...>\r\n") .replace(/Date: (?!dummy).*\r\n/, "Date: ...\r\n") - .replace(/----[^\s"]+/g, "----..."); + .replace(/(boundary="|^--)--[^\s"]+?(-Part|")/mg, "$1--...$2"); } Tinytest.add("email - fully customizable", function (test) { @@ -55,7 +55,7 @@ Tinytest.add("email - fully customizable", function (test) { "\r\n" + "This is the body\n" + "of the message\n" + - "From us." + + "From us.\r\n" + "====== END MAIL #0 ======\n"); }); }); @@ -108,33 +108,23 @@ Tinytest.add("email - multiple e-mails same stream", function (test) { Tinytest.add("email - using mail composer", function (test) { smokeEmailTest(function (stream) { // Test direct MailComposer usage. - var mcs = [ - // Test with MailComposer object (without compiling). - new EmailInternals.NpmModules.mailcomposer.module.MailComposer({ - from: "a@b.com", - text: "body" - }), - // Test calling module as a function, which compiles MailComposer object. - EmailInternals.NpmModules.mailcomposer.module({ - from: "a@b.com", - text: "body" - }) - ]; - for (var i = 0; i < mcs.length; i++) { - Email.send({mailComposer: mcs[i]}); - test.equal(canonicalize(stream.getContentsAsString("utf8")), - "====== BEGIN MAIL #"+i+" ======\n" + - devWarningBanner + - "Content-Type: text/plain\r\n" + - "From: a@b.com\r\n" + - "Message-ID: <...>\r\n" + - "Content-Transfer-Encoding: 7bit\r\n" + - "Date: ...\r\n" + - "MIME-Version: 1.0\r\n" + - "\r\n" + - "body" + - "====== END MAIL #"+i+" ======\n"); - } + var mc = new EmailInternals.NpmModules.mailcomposer.module({ + from: "a@b.com", + text: "body" + }); + Email.send({mailComposer: mc}); + test.equal(canonicalize(stream.getContentsAsString("utf8")), + "====== BEGIN MAIL #0 ======\n" + + devWarningBanner + + "Content-Type: text/plain\r\n" + + "From: a@b.com\r\n" + + "Message-ID: <...>\r\n" + + "Content-Transfer-Encoding: 7bit\r\n" + + "Date: ...\r\n" + + "MIME-Version: 1.0\r\n" + + "\r\n" + + "body\r\n" + + "====== END MAIL #0 ======\n"); }); }); @@ -181,7 +171,7 @@ Tinytest.add("email - long lines", function (test) { "MIME-Version: 1.0\r\n" + "\r\n" + "This is a very very very very very very very very very very " + - "very very long =\r\ntext" + + "very very long =\r\ntext\r\n" + "====== END MAIL #0 ======\n"); }); }); @@ -208,7 +198,7 @@ Tinytest.add("email - unicode", function (test) { "Date: ...\r\n" + "MIME-Version: 1.0\r\n" + "\r\n" + - "I =E2=99=A5 Meteor" + + "I =E2=99=A5 Meteor\r\n" + "====== END MAIL #0 ======\n"); }); }); @@ -227,24 +217,24 @@ Tinytest.add("email - text and html", function (test) { "====== BEGIN MAIL #0 ======\n" + devWarningBanner + "Content-Type: multipart/alternative;\r\n" + - ' boundary="----..."\r\n' + + ' boundary="--...-Part_1"\r\n' + "From: foo@example.com\r\n" + "To: bar@example.com\r\n" + "Message-ID: <...>\r\n" + "Date: ...\r\n" + "MIME-Version: 1.0\r\n" + "\r\n" + - "----...\r\n" + + "----...-Part_1\r\n" + "Content-Type: text/plain\r\n" + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "*Cool*, man\r\n" + - "----...\r\n" + + "----...-Part_1\r\n" + "Content-Type: text/html\r\n" + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "Cool, man\r\n" + - "----...\r\n" + + "----...-Part_1--\r\n" + "====== END MAIL #0 ======\n"); }); }); diff --git a/packages/email/package.js b/packages/email/package.js index 36f09e1601..7e5167f350 100644 --- a/packages/email/package.js +++ b/packages/email/package.js @@ -1,17 +1,14 @@ Package.describe({ summary: "Send email messages", - version: "1.2.0" + version: "1.2.1" }); Npm.depends({ - mailcomposer: "4.0.1", - // Using smtp-connection@2 (instead of latest) because it shares - // nodemailer-shared with mailcomposer@4: - "smtp-connection": "2.12.2", - "stream-buffers": "0.2.5"}); + node4mailer: "4.0.2", + "stream-buffers": "0.2.5" +}); Package.onUse(function (api) { - api.use('underscore', 'server'); api.export(['Email', 'EmailInternals'], 'server'); api.export('EmailTest', 'server', {testOnly: true}); api.addFiles('email.js', 'server'); diff --git a/packages/facebook-oauth/facebook_server.js b/packages/facebook-oauth/facebook_server.js index 04a5179f31..f124676deb 100644 --- a/packages/facebook-oauth/facebook_server.js +++ b/packages/facebook-oauth/facebook_server.js @@ -1,4 +1,5 @@ Facebook = {}; +var crypto = Npm.require('crypto'); Facebook.handleAuthFromAccessToken = function handleAuthFromAccessToken(accessToken, expiresAt) { // include all fields from facebook @@ -79,10 +80,20 @@ var getTokenResponse = function (query) { }; var getIdentity = function (accessToken, fields) { + var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); + if (!config) + throw new ServiceConfiguration.ConfigError(); + + // Generate app secret proof that is a sha256 hash of the app access token, with the app secret as the key + // https://developers.facebook.com/docs/graph-api/securing-requests#appsecret_proof + var hmac = crypto.createHmac('sha256', OAuth.openSecret(config.secret)); + hmac.update(accessToken); + try { return HTTP.get("https://graph.facebook.com/v2.8/me", { params: { access_token: accessToken, + appsecret_proof: hmac.digest('hex'), fields: fields.join(",") } }).data; diff --git a/packages/facebook-oauth/package.js b/packages/facebook-oauth/package.js index e6acd883db..5d426bc5ec 100644 --- a/packages/facebook-oauth/package.js +++ b/packages/facebook-oauth/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Facebook OAuth flow", - version: "1.3.0" + version: "1.3.1-rc.1" }); Package.onUse(function(api) { diff --git a/packages/google-oauth/google_client.js b/packages/google-oauth/google_client.js index c7b54dccfb..a45c080011 100644 --- a/packages/google-oauth/google_client.js +++ b/packages/google-oauth/google_client.js @@ -1,5 +1,13 @@ var Google = require("./namespace.js"); +var ILLEGAL_PARAMETERS = { + 'response_type': 1, + 'client_id': 1, + 'scope': 1, + 'redirect_uri': 1, + 'state': 1 +}; + // Request Google credentials for the user // @param options {optional} // @param credentialRequestCompleteCallback {Function} Callback function to call on @@ -24,24 +32,26 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) var credentialToken = Random.secret(); // we need the email scope to get user id from google. - var requiredScope = ['email']; - var scope = ['profile']; - if (options.requestPermissions) - scope = options.requestPermissions; - scope = _.union(scope, requiredScope); + var requiredScopes = { 'email': 1 }; + var scopes = options.requestPermissions || ['profile']; + scopes.forEach(function (scope) { + requiredScopes[scope] = 1; + }); + scopes = Object.keys(requiredScopes); var loginUrlParameters = {}; if (config.loginUrlParameters){ - _.extend(loginUrlParameters, config.loginUrlParameters) + Object.assign(loginUrlParameters, config.loginUrlParameters); } if (options.loginUrlParameters){ - _.extend(loginUrlParameters, options.loginUrlParameters) + Object.assign(loginUrlParameters, options.loginUrlParameters); } - var ILLEGAL_PARAMETERS = ['response_type', 'client_id', 'scope', 'redirect_uri', 'state']; - // validate options keys - _.each(_.keys(loginUrlParameters), function (key) { - if (_.contains(ILLEGAL_PARAMETERS, key)) + + // validate options keys + Object.keys(loginUrlParameters).forEach(function (key) { + if (ILLEGAL_PARAMETERS.hasOwnProperty(key)) { throw new Error("Google.requestCredential: Invalid loginUrlParameter: " + key); + } }); // backwards compatible options @@ -60,16 +70,17 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) var loginStyle = OAuth._loginStyle('google', config, options); // https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl - _.extend(loginUrlParameters, { + Object.assign(loginUrlParameters, { "response_type": "code", "client_id": config.clientId, - "scope": scope.join(' '), // space delimited + "scope": scopes.join(' '), // space delimited "redirect_uri": OAuth._redirectUri('google', config), "state": OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl) }); var loginUrl = 'https://accounts.google.com/o/oauth2/auth?' + - _.map(loginUrlParameters, function(value, param){ - return encodeURIComponent(param) + '=' + encodeURIComponent(value); + Object.keys(loginUrlParameters).map(function (param) { + return encodeURIComponent(param) + '=' + + encodeURIComponent(loginUrlParameters[param]); }).join("&"); OAuth.launchLogin({ diff --git a/packages/google-oauth/google_server.js b/packages/google-oauth/google_server.js index 4b647a37d5..f553f33825 100644 --- a/packages/google-oauth/google_server.js +++ b/packages/google-oauth/google_server.js @@ -1,67 +1,88 @@ var Google = require("./namespace.js"); var Accounts = require("meteor/accounts-base").Accounts; +var hasOwn = Object.prototype.hasOwnProperty; // https://developers.google.com/accounts/docs/OAuth2Login#userinfocall Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name', 'family_name', 'picture', 'locale', 'timezone', 'gender']; +function getServiceDataFromTokens(tokens) { + var accessToken = tokens.accessToken; + var idToken = tokens.idToken; + var scopes = getScopes(accessToken); + var identity = getIdentity(accessToken); + var serviceData = { + accessToken: accessToken, + idToken: idToken, + scope: scopes + }; + + if (hasOwn.call(tokens, "expiresAt")) { + serviceData.expiresAt = + Date.now() + 1000 * parseInt(tokens.expiresIn, 10); + } + + var fields = Object.create(null); + Google.whitelistedFields.forEach(function (name) { + if (hasOwn.call(identity, name)) { + fields[name] = identity[name]; + } + }); + + Object.assign(serviceData, fields); + + // 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 (tokens.refreshToken) { + serviceData.refreshToken = tokens.refreshToken; + } + + return { + serviceData: serviceData, + options: { + profile: { + name: identity.name + } + } + }; +} + Accounts.registerLoginHandler(function (request) { if (request.googleSignIn !== true) { return; } - var res = HTTP.get( - "https://www.googleapis.com/oauth2/v3/tokeninfo", - { headers: { "User-Agent": "Meteor/1.0" }, - params: { id_token: request.idToken }} - ); - - if (res.error) { - throw res.error; - } - - if (res.statusCode === 200 && - res.data.sub === request.userId) { - return Accounts.updateOrCreateUserFromExternalService("google", { - id: request.userId, - idToken: request.idToken, - accessToken: request.accessToken, - email: request.email, - picture: request.imageUrl - }); - } -}); - -OAuth.registerService('google', 2, null, function(query) { - var response = getTokens(query); - var expiresAt = (+new Date) + (1000 * parseInt(response.expiresIn, 10)); - var accessToken = response.accessToken; - var idToken = response.idToken; - var scopes = getScopes(accessToken); - var identity = getIdentity(accessToken); - - var serviceData = { - accessToken: accessToken, - idToken: idToken, - expiresAt: expiresAt, - scope: scopes + const tokens = { + accessToken: request.accessToken, + refreshToken: request.refreshToken, + idToken: request.idToken, }; - var fields = _.pick(identity, Google.whitelistedFields); - _.extend(serviceData, fields); + if (request.serverAuthCode) { + Object.assign(tokens, getTokens({ + code: request.serverAuthCode + })); + } - // 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; + const result = getServiceDataFromTokens(tokens); - return { - serviceData: serviceData, - options: {profile: {name: identity.name}} - }; + return Accounts.updateOrCreateUserFromExternalService("google", { + id: request.userId, + idToken: request.idToken, + accessToken: request.accessToken, + email: request.email, + picture: request.imageUrl, + ...result.serviceData, + }, result.options); }); +function getServiceData(query) { + return getServiceDataFromTokens(getTokens(query)); +} + +OAuth.registerService('google', 2, null, getServiceData); + // returns an object containing: // - accessToken // - expiresIn: lifetime of token in seconds @@ -82,8 +103,10 @@ var getTokens = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - throw _.extend(new Error("Failed to complete OAuth handshake with Google. " + err.message), - {response: err.response}); + throw Object.assign( + new Error("Failed to complete OAuth handshake with Google. " + err.message), + { response: err.response } + ); } if (response.data.error) { // if the http response was a json object with an error attribute @@ -104,8 +127,10 @@ var getIdentity = function (accessToken) { "https://www.googleapis.com/oauth2/v1/userinfo", {params: {access_token: accessToken}}).data; } catch (err) { - throw _.extend(new Error("Failed to fetch identity from Google. " + err.message), - {response: err.response}); + throw Object.assign( + new Error("Failed to fetch identity from Google. " + err.message), + { response: err.response } + ); } }; @@ -115,8 +140,10 @@ var getScopes = function (accessToken) { "https://www.googleapis.com/oauth2/v1/tokeninfo", {params: {access_token: accessToken}}).data.scope.split(' '); } catch (err) { - throw _.extend(new Error("Failed to fetch tokeninfo from Google. " + err.message), - {response: err.response}); + throw Object.assign( + new Error("Failed to fetch tokeninfo from Google. " + err.message), + { response: err.response } + ); } }; diff --git a/packages/google-oauth/google_sign-in.js b/packages/google-oauth/google_sign-in.js index 4d21403264..3ba3a54ea8 100644 --- a/packages/google-oauth/google_sign-in.js +++ b/packages/google-oauth/google_sign-in.js @@ -59,12 +59,14 @@ exports.signIn = Google.signIn = function (options, callback) { function getScopes(options) { // we need the email scope to get user id from google. - var requiredScopes = ['email']; - var scopes = ['profile']; - if (options && options.requestPermissions) { - scopes = options.requestPermissions; - } - return _.union(scopes, requiredScopes); + var requiredScopes = { 'email': 1 }; + var scopes = options.requestPermissions || ['profile']; + + scopes.forEach(function (scope) { + requiredScopes[scope] = 1; + }); + + return Object.keys(requiredScopes); } exports.signOut = Google.signOut = function () { diff --git a/packages/google-oauth/package.js b/packages/google-oauth/package.js index d977bb6d16..09ee7166c5 100644 --- a/packages/google-oauth/package.js +++ b/packages/google-oauth/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Google OAuth flow", - version: "1.2.1" + version: "1.2.4" }); var cordovaPluginGooglePlusURL = @@ -14,12 +14,11 @@ Cordova.depends({ }); Package.onUse(function(api) { - api.use("modules"); - api.use("promise"); + api.use("ecmascript"); api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); api.use('http', ['server']); - api.use(['underscore', 'service-configuration'], ['client', 'server']); + api.use('service-configuration'); api.use('random', 'client'); api.addFiles('google_server.js', 'server'); diff --git a/packages/localstorage/package.js b/packages/localstorage/package.js index e7e1684173..7662121407 100644 --- a/packages/localstorage/package.js +++ b/packages/localstorage/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Simulates local storage on IE 6,7 using userData", - version: "1.1.0-beta.14" + version: "1.1.0-rc.1" }); Package.onUse(function (api) { diff --git a/packages/meetup-oauth/package.js b/packages/meetup-oauth/package.js index fb39d443f3..ff189c5ad4 100644 --- a/packages/meetup-oauth/package.js +++ b/packages/meetup-oauth/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Meetup OAuth flow', - version: '1.0.0' + version: '1.0.1' }); Package.onUse(function (api) { diff --git a/packages/meteor-base/package.js b/packages/meteor-base/package.js index cd962dfbc9..2c24630265 100644 --- a/packages/meteor-base/package.js +++ b/packages/meteor-base/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'meteor-base', - version: '1.0.4', + version: '1.1.0-rc.1', // Brief, one-line summary of the package. summary: 'Packages that every Meteor app needs', // By default, Meteor will default to using README.md for documentation. @@ -25,6 +25,9 @@ Package.onUse(function(api) { 'ddp', 'livedata', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0. + // Runtime support for Meteor 1.5 dynamic import(...) syntax. + 'dynamic-import', + // Push code changes to the client and automatically reload the page 'hot-code-push' ]); diff --git a/packages/minimongo/helpers.js b/packages/minimongo/helpers.js index 759ce0ca62..a21c1dc49c 100644 --- a/packages/minimongo/helpers.js +++ b/packages/minimongo/helpers.js @@ -42,4 +42,4 @@ isOperatorObject = function (valueSelector, inconsistentOK) { // string can be converted to integer isNumericKey = function (s) { return /^[0-9]+$/.test(s); -}; +}; \ No newline at end of file diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index f79f2d4bc1..b97663a8a7 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1,3 +1,5 @@ +import { assertHasValidFieldNames } from './validation.js'; + // XXX type checking on selectors (graceful error if malformed) // LocalCollection: a set of documents that supports queries and modifiers. @@ -40,7 +42,11 @@ Minimongo = {}; // Use it to export private functions to test in Tinytest. MinimongoTest = {}; -MinimongoError = function (message) { +MinimongoError = function (message, options={}) { + if (typeof message === "string" && options.field) { + message += ` for field '${options.field}'`; + } + var e = new Error(message); e.name = "MinimongoError"; return e; @@ -536,31 +542,13 @@ LocalCollection.Cursor.prototype._depend = function (changers, _allow_unordered) } }; -// XXX enforce rule that field names can't start with '$' or contain '.' -// (real mongodb does in fact enforce this) // XXX possibly enforce that 'undefined' does not appear (we assume // this in our handling of null and $exists) LocalCollection.prototype.insert = function (doc, callback) { var self = this; doc = EJSON.clone(doc); - // Make sure field names do not contain Mongo restricted - // characters ('.', '$', '\0'). - // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - if (doc) { - const invalidCharMsg = { - '.': "contain '.'", - '$': "start with '$'", - '\0': "contain null bytes", - }; - JSON.stringify(doc, (key, value) => { - let match; - if (_.isString(key) && (match = key.match(/^\$|\.|\0/))) { - throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); - } - return value; - }); - } + assertHasValidFieldNames(doc); if (!_.has(doc, '_id')) { // if you really want to use ObjectIDs, set this global. @@ -711,7 +699,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { } if (!options) options = {}; - var matcher = new Minimongo.Matcher(selector); + var matcher = new Minimongo.Matcher(selector, true); // Save the original results of any query that we might need to // _recomputeResults on, because _modifyAndNotify will mutate the objects in @@ -794,8 +782,24 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { // generate an id for it. var insertedId; if (updateCount === 0 && options.upsert) { - var newDoc = LocalCollection._removeDollarOperators(selector); + + let selectorModifier = LocalCollection._selectorIsId(selector) + ? { _id: selector } + : selector; + + selectorModifier = LocalCollection._removeDollarOperators(selectorModifier); + + const newDoc = {}; + if (selectorModifier._id) { + newDoc._id = selectorModifier._id; + delete selectorModifier._id; + } + + // This double _modify call is made to help work around an issue where collection + // upserts won't work properly, with nested properties (see issue #8631). + LocalCollection._modify(newDoc, {$set: selectorModifier}); LocalCollection._modify(newDoc, mod, {isInsert: true}); + if (! newDoc._id && options.insertedId) newDoc._id = options.insertedId; insertedId = self.insert(newDoc); diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 69e728448e..57b006e03e 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -163,6 +163,22 @@ Tinytest.add("minimongo - basics", function (test) { }); +Tinytest.add("minimongo - error - no options", function (test) { + try { + throw MinimongoError("Not fun to have errors"); + } catch (e) { + test.equal(e.message, "Not fun to have errors"); + } +}); + +Tinytest.add("minimongo - error - with field", function (test) { + try { + throw MinimongoError("Cats are no fun", { field: "mice" }); + } catch (e) { + test.equal(e.message, "Cats are no fun for field 'mice'"); + } +}); + Tinytest.add("minimongo - cursors", function (test) { var c = new LocalCollection(); var res; @@ -2191,6 +2207,13 @@ Tinytest.add("minimongo - modify", function (test) { test.equal(actual, expected); }; + var upsertException = function (query, mod) { + var coll = new LocalCollection; + test.throws(function(){ + coll.upsert(query, mod); + }); + }; + // document replacement modify({}, {}, {}); modify({a: 12}, {}, {}); // tested against mongodb @@ -2199,6 +2222,11 @@ Tinytest.add("minimongo - modify", function (test) { exception({a: 12}, {a: 13, $set: {b: 13}}); exception({a: 12}, {$set: {b: 13}, a: 13}); + exception({a: 12}, {$a: 13}); //invalid operator + exception({a: 12}, {b:{$a: 13}}); + exception({a: 12}, {b:{'a.b': 13}}); + exception({a: 12}, {b:{'\0a': 13}}); + // keys modify({}, {$set: {'a': 12}}, {a: 12}); modify({}, {$set: {'a.b': 12}}, {a: {b: 12}}); @@ -2273,13 +2301,6 @@ Tinytest.add("minimongo - modify", function (test) { {'a.x': 1, 'a.y': 3}, {$set: {'a.$.z': 5}}, {a: [{x: 1}, {y: 3, z: 5}]}); - // with $near, make sure it finds the closest one - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {a: [{b: [1,1]}, {b: 'k'}, {b: [9,9]}]}); modifyWithQuery({a: [{x: 1}, {y: 1}, {x: 1, y: 1}]}, {a: {$elemMatch: {x: 1, y: 1}}}, {$set: {'a.$.x': 2}}, @@ -2288,6 +2309,56 @@ Tinytest.add("minimongo - modify", function (test) { {'a.b': {$elemMatch: {x: 1, y: 1}}}, {$set: {'a.$.b': 3}}, {a: [{b: 3}]}); + // with $near, make sure it does not find the closest one (#3599) + modifyWithQuery({a: []}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[]}); + modifyWithQuery({a: [{b: [ [3,3], [4,4] ]}]}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"}]}); + modifyWithQuery({a: [{b: [1,1]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b: [1,1]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [9, 9], $maxDistance: 1}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b: [1,1]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [9, 9]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b: [9,9]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [9, 9]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b:[4,3]}, + {c: [1,1]}]}, + {'a.c': {$near: [1, 1]}}, + {$set: {'a.$.c': 'k'}}, + {"a":[{"c": "k", "b":[4,3]},{"c":[1,1]}]}); + modifyWithQuery({a: [{c: [9,9]}, + {b: [ [3,3], [4,4] ]}, + {b: [1,1]}]}, + {'a.b': {$near: [1, 1]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); + modifyWithQuery({a: [{c: [9,9], b:[4,3]}, + {b: [ [3,3], [4,4] ]}, + {b: [1,1]}]}, + {'a.b': {$near: [1, 1]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); // $inc modify({a: 1, b: 2}, {$inc: {a: 10}}, {a: 11, b: 2}); @@ -2367,6 +2438,12 @@ Tinytest.add("minimongo - modify", function (test) { modify({}, {$set: {'x._id': 4}}, {x: {_id: 4}}); exception({}, {$set: {_id: 4}}); exception({_id: 4}, {$set: {_id: 4}}); // even not-changing _id is bad + //restricted field names + exception({a:{}}, {$set:{a:{$a:1}}}); + exception({ a: {} }, { $set: { a: { c: + [{ b: { $a: 1 } }] } } }); + exception({a:{}}, {$set:{a:{'\0a':1}}}); + exception({a:{}}, {$set:{a:{'a.b':1}}}); // $unset modify({}, {$unset: {a: 1}}, {}); @@ -2400,8 +2477,6 @@ Tinytest.add("minimongo - modify", function (test) { {a: [1, 2, 3]}); modify({a: [true]}, {$push: {a: {$each: [1, 2, 3]}}}, {a: [true, 1, 2, 3]}); - // No positive numbers for $slice - exception({}, {$push: {a: {$each: [], $slice: 5}}}); modify({a: [true]}, {$push: {a: {$each: [1, 2, 3], $slice: -2}}}, {a: [2, 3]}); modify({a: [false, true]}, {$push: {a: {$each: [1], $slice: -2}}}, @@ -2441,6 +2516,26 @@ Tinytest.add("minimongo - modify", function (test) { {$push: {a: {$each: [{x: 3}], $position: 0, $sort: {x: 1}, $slice: 0}}}, {a: []} ); + //restricted field names + exception({}, {$push: {$a: 1}}); + exception({}, {$push: {'\0a': 1}}); + exception({}, {$push: {a: {$a:1}}}); + exception({}, {$push: {a: {$each: [{$a:1}]}}}); + exception({}, {$push: {a: {$each: [{"a.b":1}]}}}); + exception({}, {$push: {a: {$each: [{'\0a':1}]}}}); + modify({}, {$push: {a: {$each: [{'':1}]}}}, {a: [ { '': 1 } ]}); + modify({}, {$push: {a: {$each: [{' ':1}]}}}, {a: [ { ' ': 1 } ]}); + exception({}, {$push: {a: {$each: [{'.':1}]}}}); + + // #issue 5167 + // $push $slice with positive numbers + modify({}, {$push: {a: {$each: [], $slice: 5}}}, {a:[]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [], $slice: 1}}}, {a:[1]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 1}}}, {a:[1]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 2}}}, {a:[1,2]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 4}}}, {a:[1,2,3,4]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 5}}}, {a:[1,2,3,4,5]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 10}}}, {a:[1,2,3,4,5]}); // $pushAll @@ -2459,6 +2554,9 @@ Tinytest.add("minimongo - modify", function (test) { modify({a: []}, {$pushAll: {'a.1': []}}, {a: [null, []]}); modify({a: {}}, {$pushAll: {'a.x': [99]}}, {a: {x: [99]}}); modify({a: {}}, {$pushAll: {'a.x': []}}, {a: {x: []}}); + exception({a: [1]}, {$pushAll: {a: [{$a:1}]}}); + exception({a: [1]}, {$pushAll: {a: [{'\0a':1}]}}); + exception({a: [1]}, {$pushAll: {a: [{"a.b":1}]}}); // $addToSet modify({}, {$addToSet: {a: 1}}, {a: [1]}); @@ -2477,15 +2575,26 @@ Tinytest.add("minimongo - modify", function (test) { modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {y: 2, x: 1}}}, {a: [{x: 1, y: 2}, {y: 2, x: 1}]}); modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4]}}}, {a: [1, 2, 3, 4]}); - modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}}, - {a: [1, 2, 3, 4]}); // tested - modify({a: [1, 2]}, {$addToSet: {a: {b: 12, $each: [3, 1, 4]}}}, - {a: [1, 2, {b: 12, $each: [3, 1, 4]}]}); // tested modify({}, {$addToSet: {a: {$each: []}}}, {a: []}); modify({}, {$addToSet: {a: {$each: [1]}}}, {a: [1]}); modify({a: []}, {$addToSet: {'a.1': 99}}, {a: [null, [99]]}); modify({a: {}}, {$addToSet: {'a.x': 99}}, {a: {x: [99]}}); + // invalid field names + exception({}, {$addToSet: {a: {$b:1}}}); + exception({}, {$addToSet: {a: {"a.b":1}}}); + exception({}, {$addToSet: {a: {"a.":1}}}); + exception({}, {$addToSet: {a: {'\u0000a':1}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {$a:1}]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {'\0a':1}]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{$a:1}]]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{b: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); + //$each is first element and thus an operator + modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}},{a: [ 1, 2, 3, 4 ]}); + // this should fail because $each is now a field name (not first in object) and thus invalid field name with $ + exception({a: [1, 2]}, {$addToSet: {a: {b: 12, $each: [3, 1, 4]}}}); + // $pop modify({}, {$pop: {a: 1}}, {}); // tested modify({}, {$pop: {a: -1}}, {}); // tested @@ -2558,11 +2667,23 @@ Tinytest.add("minimongo - modify", function (test) { exception({}, {$rename: {'a': 'a'}}); exception({}, {$rename: {'a.b': 'a.b'}}); modify({a: 12, b: 13}, {$rename: {a: 'b'}}, {b: 12}); + exception({a: [12]}, {$rename: {a: '$b'}}); + exception({a: [12]}, {$rename: {a: '\0a'}}); // $setOnInsert modify({a: 0}, {$setOnInsert: {a: 12}}, {a: 0}); upsert({a: 12}, {$setOnInsert: {b: 12}}, {a: 12, b: 12}); upsert({a: 12}, {$setOnInsert: {_id: 'test'}}, {_id: 'test', a: 12}); + upsert({"a.b": 10}, {$setOnInsert: {a: {b: 10, c: 12}}}, {a: {b: 10, c: 12}}); + upsert({"a.b": 10}, {$setOnInsert: {c: 12}}, {a: {b: 10}, c: 12}); + upsert({"_id": 'test'}, {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); + upsert('test', {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); + upsertException({a: 0}, {$setOnInsert: {$a: 12}}); + upsertException({a: 0}, {$setOnInsert: {'\0a': 12}}); + upsert({a: 0}, {$setOnInsert: {b: {a:1}}}, {a:0, b:{a:1}}); + upsertException({a: 0}, {$setOnInsert: {b: {$a:1}}}); + upsertException({a: 0}, {$setOnInsert: {b: {'a.b':1}}}); + upsertException({a: 0}, {$setOnInsert: {b: {'\0a':1}}}); exception({}, {$set: {_id: 'bad'}}); @@ -3142,13 +3263,14 @@ Tinytest.add("minimongo - $near operator tests", function (test) { // 'y'. testNear([2, 2], 1000, ['x', 'y']); - // Ensure that distance is used as a tie-breaker for sort. + // issue #3599 + // Ensure that distance is not used as a tie-breaker for sort. test.equal( _.pluck(coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch(), '_id'), ['x', 'y']); test.equal( _.pluck(coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch(), '_id'), - ['y', 'x']); + ['x', 'y']); var operations = []; var cbs = log_callbacks(operations); @@ -3167,6 +3289,35 @@ Tinytest.add("minimongo - $near operator tests", function (test) { handle.stop(); }); +// issue #2077 +Tinytest.add("minimongo - $near and $geometry for legacy coordinates", function(test){ + var coll = new LocalCollection(); + + coll.insert({ + loc: { + x: 1, + y: 1 + } + }); + coll.insert({ + loc: [-1,-1] + }); + coll.insert({ + loc: [40,-10] + }); + coll.insert({ + loc: { + x: -10, + y: 40 + } + }); + + test.equal(coll.find({ 'loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 2); + test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}}} }).count(), 4); + test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}, $maxDistance:200000}}}).count(), 2); + +}); + // Regression test for #4377. Previously, "replace" updates didn't clone the // argument. Tinytest.add("minimongo - update should clone", function (test) { diff --git a/packages/minimongo/modify.js b/packages/minimongo/modify.js index e5a81ba8f6..e0df49a274 100644 --- a/packages/minimongo/modify.js +++ b/packages/minimongo/modify.js @@ -1,3 +1,5 @@ +import { assertHasValidFieldNames, assertIsValidFieldName } from './validation.js'; + // XXX need a strategy for passing the binding of $ into this // function, from the compiled selector // @@ -27,11 +29,7 @@ LocalCollection._modify = function (doc, mod, options) { throw MinimongoError("Cannot change the _id of a document"); // replace the whole document - for (var k in mod) { - if (/\./.test(k)) - throw MinimongoError( - "When replacing document, field name may not contain '.'"); - } + assertHasValidFieldNames(mod); newDoc = mod; } else { // apply modifiers to the doc. @@ -155,8 +153,7 @@ var findModTarget = function (doc, keyparts, options) { "' of list value " + JSON.stringify(doc[keypart])); } } else { - if (keypart.length && keypart.substr(0, 1) === '$') - throw MinimongoError("can't set field named " + keypart); + assertIsValidFieldName(keypart); if (!(keypart in doc)) { if (options.noCreate) return undefined; @@ -185,20 +182,24 @@ var MODIFIERS = { $currentDate: function (target, field, arg) { if (typeof arg === "object" && arg.hasOwnProperty("$type")) { if (arg.$type !== "date") { - throw MinimongoError("Minimongo does currently only support the date type in $currentDate modifiers"); + throw MinimongoError( + "Minimongo does currently only support the date type " + + "in $currentDate modifiers", + { field }); } } else if (arg !== true) { - throw MinimongoError("Invalid $currentDate modifier"); + throw MinimongoError("Invalid $currentDate modifier", { field }); } target[field] = new Date(); }, $min: function (target, field, arg) { if (typeof arg !== "number") { - throw MinimongoError("Modifier $min allowed for numbers only"); + throw MinimongoError("Modifier $min allowed for numbers only", { field }); } if (field in target) { if (typeof target[field] !== "number") { - throw MinimongoError("Cannot apply $min modifier to non-number"); + throw MinimongoError( + "Cannot apply $min modifier to non-number", { field }); } if (target[field] > arg) { target[field] = arg; @@ -209,11 +210,12 @@ var MODIFIERS = { }, $max: function (target, field, arg) { if (typeof arg !== "number") { - throw MinimongoError("Modifier $max allowed for numbers only"); + throw MinimongoError("Modifier $max allowed for numbers only", { field }); } if (field in target) { if (typeof target[field] !== "number") { - throw MinimongoError("Cannot apply $max modifier to non-number"); + throw MinimongoError( + "Cannot apply $max modifier to non-number", { field }); } if (target[field] < arg) { target[field] = arg; @@ -224,10 +226,11 @@ var MODIFIERS = { }, $inc: function (target, field, arg) { if (typeof arg !== "number") - throw MinimongoError("Modifier $inc allowed for numbers only"); + throw MinimongoError("Modifier $inc allowed for numbers only", { field }); if (field in target) { if (typeof target[field] !== "number") - throw MinimongoError("Cannot apply $inc modifier to non-number"); + throw MinimongoError( + "Cannot apply $inc modifier to non-number", { field }); target[field] += arg; } else { target[field] = arg; @@ -235,20 +238,17 @@ var MODIFIERS = { }, $set: function (target, field, arg) { if (!_.isObject(target)) { // not an array or an object - var e = MinimongoError("Cannot set property on non-object field"); + var e = MinimongoError( + "Cannot set property on non-object field", { field }); e.setPropertyError = true; throw e; } if (target === null) { - var e = MinimongoError("Cannot set property on null"); + var e = MinimongoError("Cannot set property on null", { field }); e.setPropertyError = true; throw e; } - if (_.isString(field) && field.indexOf('\0') > -1) { - // Null bytes are not allowed in Mongo field names - // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - throw MinimongoError(`Key ${field} must not contain null bytes`); - } + assertHasValidFieldNames(arg); target[field] = arg; }, $setOnInsert: function (target, field, arg) { @@ -267,10 +267,12 @@ var MODIFIERS = { if (target[field] === undefined) target[field] = []; if (!(target[field] instanceof Array)) - throw MinimongoError("Cannot apply $push modifier to non-array"); + throw MinimongoError( + "Cannot apply $push modifier to non-array", { field }); if (!(arg && arg.$each)) { // Simple mode: not $each + assertHasValidFieldNames(arg); target[field].push(arg); return; } @@ -278,16 +280,18 @@ var MODIFIERS = { // Fancy mode: $each (and maybe $slice and $sort and $position) var toPush = arg.$each; if (!(toPush instanceof Array)) - throw MinimongoError("$each must be an array"); + throw MinimongoError("$each must be an array", { field }); + assertHasValidFieldNames(toPush); // Parse $position var position = undefined; if ('$position' in arg) { if (typeof arg.$position !== "number") - throw MinimongoError("$position must be a numeric value"); + throw MinimongoError("$position must be a numeric value", { field }); // XXX should check to make sure integer if (arg.$position < 0) - throw MinimongoError("$position in $push must be zero or positive"); + throw MinimongoError( + "$position in $push must be zero or positive", { field }); position = arg.$position; } @@ -295,10 +299,8 @@ var MODIFIERS = { var slice = undefined; if ('$slice' in arg) { if (typeof arg.$slice !== "number") - throw MinimongoError("$slice must be a numeric value"); + throw MinimongoError("$slice must be a numeric value", { field }); // XXX should check to make sure integer - if (arg.$slice > 0) - throw MinimongoError("$slice in $push must be zero or negative"); slice = arg.$slice; } @@ -306,7 +308,7 @@ var MODIFIERS = { var sortFunction = undefined; if (arg.$sort) { if (slice === undefined) - throw MinimongoError("$sort requires $slice to be present"); + throw MinimongoError("$sort requires $slice to be present", { field }); // XXX this allows us to use a $sort whose value is an array, but that's // actually an extension of the Node driver, so it won't work // server-side. Could be confusing! @@ -315,7 +317,7 @@ var MODIFIERS = { for (var i = 0; i < toPush.length; i++) { if (LocalCollection._f._type(toPush[i]) !== 3) { throw MinimongoError("$push like modifiers using $sort " + - "require all elements to be objects"); + "require all elements to be objects", { field }); } } } @@ -339,18 +341,22 @@ var MODIFIERS = { if (slice !== undefined) { if (slice === 0) target[field] = []; // differs from Array.slice! - else + else if (slice < 0) target[field] = target[field].slice(slice); + else + target[field] = target[field].slice(0, slice); } }, $pushAll: function (target, field, arg) { if (!(typeof arg === "object" && arg instanceof Array)) throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); + assertHasValidFieldNames(arg); var x = target[field]; if (x === undefined) target[field] = arg; else if (!(x instanceof Array)) - throw MinimongoError("Cannot apply $pushAll modifier to non-array"); + throw MinimongoError( + "Cannot apply $pushAll modifier to non-array", { field }); else { for (var i = 0; i < arg.length; i++) x.push(arg[i]); @@ -360,18 +366,19 @@ var MODIFIERS = { var isEach = false; if (typeof arg === "object") { //check if first key is '$each' - for (var k in arg) { - if (k === "$each") - isEach = true; - break; + const keys = Object.keys(arg); + if (keys[0] === "$each"){ + isEach = true; } } var values = isEach ? arg["$each"] : [arg]; + assertHasValidFieldNames(values); var x = target[field]; if (x === undefined) target[field] = values; else if (!(x instanceof Array)) - throw MinimongoError("Cannot apply $addToSet modifier to non-array"); + throw MinimongoError( + "Cannot apply $addToSet modifier to non-array", { field }); else { _.each(values, function (value) { for (var i = 0; i < x.length; i++) @@ -388,7 +395,8 @@ var MODIFIERS = { if (x === undefined) return; else if (!(x instanceof Array)) - throw MinimongoError("Cannot apply $pop modifier to non-array"); + throw MinimongoError( + "Cannot apply $pop modifier to non-array", { field }); else { if (typeof arg === 'number' && arg < 0) x.splice(0, 1); @@ -403,7 +411,8 @@ var MODIFIERS = { if (x === undefined) return; else if (!(x instanceof Array)) - throw MinimongoError("Cannot apply $pull/pullAll modifier to non-array"); + throw MinimongoError( + "Cannot apply $pull/pullAll modifier to non-array", { field }); else { var out = []; if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { @@ -430,14 +439,16 @@ var MODIFIERS = { }, $pullAll: function (target, field, arg) { if (!(typeof arg === "object" && arg instanceof Array)) - throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); + throw MinimongoError( + "Modifier $pushAll/pullAll allowed for arrays only", { field }); if (target === undefined) return; var x = target[field]; if (x === undefined) return; else if (!(x instanceof Array)) - throw MinimongoError("Cannot apply $pull/pullAll modifier to non-array"); + throw MinimongoError( + "Cannot apply $pull/pullAll modifier to non-array", { field }); else { var out = []; for (var i = 0; i < x.length; i++) { @@ -457,15 +468,17 @@ var MODIFIERS = { $rename: function (target, field, arg, keypath, doc) { if (keypath === arg) // no idea why mongo has this restriction.. - throw MinimongoError("$rename source must differ from target"); + throw MinimongoError("$rename source must differ from target", { field }); if (target === null) - throw MinimongoError("$rename source field invalid"); + throw MinimongoError("$rename source field invalid", { field }); if (typeof arg !== "string") - throw MinimongoError("$rename target must be a string"); + throw MinimongoError("$rename target must be a string", { field }); if (arg.indexOf('\0') > -1) { // Null bytes are not allowed in Mongo field names // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - throw MinimongoError("The 'to' field for $rename cannot contain an embedded null byte"); + throw MinimongoError( + "The 'to' field for $rename cannot contain an embedded null byte", + { field }); } if (target === undefined) return; @@ -475,13 +488,13 @@ var MODIFIERS = { var keyparts = arg.split('.'); var target2 = findModTarget(doc, keyparts, {forbidArray: true}); if (target2 === null) - throw MinimongoError("$rename target field invalid"); + throw MinimongoError("$rename target field invalid", { field }); var field2 = keyparts.pop(); target2[field2] = v; }, $bit: function (target, field, arg) { // XXX mongo only supports $bit on integers, and we only support // native javascript numbers (doubles) so far, so we can't support $bit - throw MinimongoError("$bit is not supported"); + throw MinimongoError("$bit is not supported", { field }); } }; diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index e4353c687a..734d49eeca 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -1,12 +1,13 @@ Package.describe({ summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", - version: '1.0.21' + version: '1.2.0-rc.1' }); Package.onUse(function (api) { api.export('LocalCollection'); api.export('Minimongo'); api.export('MinimongoTest', { testOnly: true }); + api.export('MinimongoError', { testOnly: true }); api.use([ 'underscore', 'ejson', diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 7d2fcb6676..7088fa751a 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -18,7 +18,7 @@ // Main entry point. // var matcher = new Minimongo.Matcher({a: {$gt: 5}}); // if (matcher.documentMatches({a: 7})) ... -Minimongo.Matcher = function (selector) { +Minimongo.Matcher = function (selector, isUpdate = false) { var self = this; // A set (object mapping string -> *) of all of the document paths looked // at by the selector. Also includes the empty string if it may look at any @@ -41,6 +41,10 @@ Minimongo.Matcher = function (selector) { // Sorter._useWithMatcher. self._selector = null; self._docMatcher = self._compileSelector(selector); + // Set to true if selection is done for an update operation + // Default is false + // Used for $near array update (issue #3599) + self._isUpdate = isUpdate; }; _.extend(Minimongo.Matcher.prototype, { @@ -435,9 +439,10 @@ var VALUE_OPERATORS = { throw Error("$near can't be inside another $ operator"); matcher._hasGeoQuery = true; - // There are two kinds of geodata in MongoDB: coordinate pairs and + // There are two kinds of geodata in MongoDB: legacy coordinate pairs and // GeoJSON. They use different distance metrics, too. GeoJSON queries are - // marked with a $geometry property. + // marked with a $geometry property, though legacy coordinates can be + // matched using $geometry. var maxDistance, point, distance; if (isPlainObject(operand) && _.has(operand, '$geometry')) { @@ -448,8 +453,11 @@ var VALUE_OPERATORS = { // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. - if (!value || !value.type) + if (!value) return null; + if(!value.type) + return GeoJSON.pointDistance(point, + { type: "Point", coordinates: pointToArray(value) }); if (value.type === "Point") { return GeoJSON.pointDistance(point, value); } else { @@ -480,20 +488,29 @@ var VALUE_OPERATORS = { // each within-$maxDistance branching point. branchedValues = expandArraysInBranches(branchedValues); var result = {result: false}; - _.each(branchedValues, function (branch) { - var curDistance = distance(branch.value); - // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) - return; - // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) - return; + _.every(branchedValues, function (branch) { + // if operation is an update, don't skip branches, just return the first one (#3599) + if (!matcher._isUpdate){ + if (!(typeof branch.value === "object")){ + return true; + } + var curDistance = distance(branch.value); + // Skip branches that aren't real points or are too far away. + if (curDistance === null || curDistance > maxDistance) + return true; + // Skip anything that's a tie. + if (result.distance !== undefined && result.distance <= curDistance) + return true; + } result.result = true; result.distance = curDistance; if (!branch.arrayIndices) delete result.arrayIndices; else result.arrayIndices = branch.arrayIndices; + if (matcher._isUpdate) + return false; + return true; }); return result; }; diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js index 7c90b8f650..aebbcc2d4f 100644 --- a/packages/minimongo/sort.js +++ b/packages/minimongo/sort.js @@ -81,23 +81,25 @@ _.extend(Minimongo.Sorter.prototype, { getComparator: function (options) { var self = this; - // If we have no distances, just use the comparator from the source - // specification (which defaults to "everything is equal". - if (!options || !options.distances) { + // If sort is specified or have no distances, just use the comparator from + // the source specification (which defaults to "everything is equal". + // issue #3599 + // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation + // sort effectively overrides $near + if (self._sortSpecParts.length || !options || !options.distances) { return self._getBaseComparator(); } var distances = options.distances; - // Return a comparator which first tries the sort specification, and if that - // says "it's equal", breaks ties using $near distances. - return composeComparators([self._getBaseComparator(), function (a, b) { + // Return a comparator which compares using $near distances. + return function (a, b) { if (!distances.has(a._id)) throw Error("Missing distance for " + a._id); if (!distances.has(b._id)) throw Error("Missing distance for " + b._id); return distances.get(a._id) - distances.get(b._id); - }]); + }; }, _getPaths: function () { diff --git a/packages/minimongo/validation.js b/packages/minimongo/validation.js new file mode 100644 index 0000000000..ef73560a72 --- /dev/null +++ b/packages/minimongo/validation.js @@ -0,0 +1,24 @@ +// Make sure field names do not contain Mongo restricted +// characters ('.', '$', '\0'). +// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names +const invalidCharMsg = { + '.': "contain '.'", + '$': "start with '$'", + '\0': "contain null bytes", +}; +export function assertIsValidFieldName(key) { + let match; + if (_.isString(key) && (match = key.match(/^\$|\.|\0/))) { + throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); + } +}; + +// checks if all field names in an object are valid +export function assertHasValidFieldNames(doc){ + if (doc && typeof doc === "object") { + JSON.stringify(doc, (key, value) => { + assertIsValidFieldName(key); + return value; + }); + } +}; \ No newline at end of file diff --git a/packages/modules-runtime/.npm/package/npm-shrinkwrap.json b/packages/modules-runtime/.npm/package/npm-shrinkwrap.json index 5186366969..1bbd59e1e2 100644 --- a/packages/modules-runtime/.npm/package/npm-shrinkwrap.json +++ b/packages/modules-runtime/.npm/package/npm-shrinkwrap.json @@ -1,9 +1,9 @@ { "dependencies": { "install": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/install/-/install-0.8.8.tgz", - "from": "install@0.8.8" + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/install/-/install-0.10.1.tgz", + "from": "install@0.10.1" } } } diff --git a/packages/modules-runtime/client.js b/packages/modules-runtime/client.js index 570609ef60..d902671864 100644 --- a/packages/modules-runtime/client.js +++ b/packages/modules-runtime/client.js @@ -2,4 +2,4 @@ // package.json files to the "main" field. makeInstallerOptions.browser = true; -install = makeInstaller(makeInstallerOptions); +meteorInstall = makeInstaller(makeInstallerOptions); diff --git a/packages/modules-runtime/meteor-install.js b/packages/modules-runtime/meteor-install.js deleted file mode 100644 index ae9f6ca33c..0000000000 --- a/packages/modules-runtime/meteor-install.js +++ /dev/null @@ -1,149 +0,0 @@ -// The metaInstall function will be used to create a module graph that -// parallels the installed module graph. This parallel graph consists of -// metadata about all available modules, not only those already installed -// but also modules that can be fetched dynamically. -var metaInstall = makeInstaller({ - browser: makeInstallerOptions.browser -}); - -meteorInstall = function (tree, options) { - if (isObject(tree)) { - var meta = Object.create(null); - var real = Object.create(null); - walk(options, tree, meta, real); - metaInstall(meta, options); - return install(real, options); - } - - return install(); -}; - -// Other packages (namely the dynamic-import package) call this function -// to retrieve module metadata from the metaInstall graph. -meteorInstall._requireMeta = metaInstall(); - -function isObject(value) { - return value && typeof value === "object"; -} - -function getOrSet(obj, name) { - return obj[name] = obj[name] || Object.create(null); -} - -function walk(options, input, meta, real) { - Object.keys(input).forEach(function (name) { - var value = input[name]; - - if (tryChild(value, name, meta, real, options)) { - // If the value was a leaf node that we were able to handle, then we - // don't need to (and can't) keep walking it. - return; - } - - if (isObject(value)) { - walk( - options, - value, - getOrSet(meta, name), - getOrSet(real, name) - ); - } - }); - - return this; -} - -function tryChild(value, name, meta, real, options) { - function tryFunc(value) { - if (typeof value === "function") { - meta[name] = makeMetaFunc({}, false, options); - real[name] = value; - return true; - } - } - - if (Array.isArray(value)) { - return value.some(function (value) { - // Dynamic stub modules are represented by objects wrapped in array - // brackets. When we find one of these objects, we install it in the - // meta graph, but not in the installed modules graph. Later, this - // information may be used to fetch dynamic modules from the server, - // which will then be installed into the real graph. - if (isObject(value)) { - meta[name] = makeMetaFunc(value, true, options); - return true; - } - - // Older versions of the install.js library supported wrapping a - // module function in an array that also contained the dependency - // identifier strings of that module. That style should no longer be - // used, but we might as well handle it gracefully, since it is not - // ambiguous with the [{...}] style. - return tryFunc(value); - }); - } - - // Installed (immediately importable) modules are represented by - // function expressions with the parameters (require, exports, module). - if (tryFunc(value)) { - return true; - } - - // The install.js library supports a notion of aliases, represented by - // module identifier strings. This functionality works the same way in - // both the meta graph and the real graph. - if (typeof value === "string") { - meta[name] = value; - real[name] = value; - return true; - } -} - -var requireReal = install(); - -function makeMetaFunc(value, dynamic, options) { - return function (require, exports, module) { - Object.assign(exports, value); - - if (! dynamic && - module.id.endsWith("/package.json")) { - // If the package.json file is not dynamic, require it from the real - // graph to read its "main" and "browser" properties. - var pkg = requireReal(module.id); - - if (typeof pkg.main === "string") { - exports.main = pkg.main; - } - - if (typeof pkg.browser === "string") { - exports.browser = pkg.browser; - } - } - - exports.module = module; - exports.dynamic = !! dynamic; - exports.options = options; - - // One of the purposes of the meta graph is to support traversing - // module dependencies without evaluating any actual module code. - // The eachChild function is essential to that traversal. - exports.eachChild = function (callback, idsToRequire) { - // By default, this function requires all value.deps dependencies - // before iterating over the resulting children, but the caller can - // provide a custom array of modules to require instead. - idsToRequire = idsToRequire || (value && value.deps); - - if (Array.isArray(idsToRequire)) { - idsToRequire.forEach(require); - } - - // After requiring any/all dependencies of this module, iterate over - // the children according to module.childrenById. Note that this - // includes all children ever imported by this module, including - // implicit modules such as package.json files. - Object.keys(module.childrenById).forEach(function (id) { - callback(module.childrenById[id]); - }); - }; - }; -} diff --git a/packages/modules-runtime/package.js b/packages/modules-runtime/package.js index e4ba74d9b2..ef1b1d0668 100644 --- a/packages/modules-runtime/package.js +++ b/packages/modules-runtime/package.js @@ -1,13 +1,13 @@ Package.describe({ name: "modules-runtime", - version: "0.8.0-beta.14", + version: "0.8.0-rc.1", summary: "CommonJS module system", git: "https://github.com/benjamn/install", documentation: "README.md" }); Npm.depends({ - install: "0.8.8" + install: "0.10.1" }); Package.onUse(function(api) { @@ -21,7 +21,6 @@ Package.onUse(function(api) { api.addFiles("options.js"); api.addFiles("client.js", "client"); api.addFiles("server.js", "server"); - api.addFiles("meteor-install.js"); api.export("meteorInstall"); }); diff --git a/packages/modules-runtime/server.js b/packages/modules-runtime/server.js index 1ce8ffb0be..367a60c769 100644 --- a/packages/modules-runtime/server.js +++ b/packages/modules-runtime/server.js @@ -32,8 +32,8 @@ makeInstallerOptions.fallback.resolve = function (id, parentId, error) { throw error; }; -install = makeInstaller(makeInstallerOptions); -var Module = install.Module; +meteorInstall = makeInstaller(makeInstallerOptions); +var Module = meteorInstall.Module; Module.prototype.useNode = function () { if (typeof npmRequire !== "function") { diff --git a/packages/modules/.npm/package/npm-shrinkwrap.json b/packages/modules/.npm/package/npm-shrinkwrap.json index 0ec396bdbd..0c95dcf9fd 100644 --- a/packages/modules/.npm/package/npm-shrinkwrap.json +++ b/packages/modules/.npm/package/npm-shrinkwrap.json @@ -1,24 +1,34 @@ { "dependencies": { "acorn": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.11.tgz", - "from": "acorn@>=4.0.5 <4.1.0" + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "from": "acorn@>=5.0.0 <5.1.0" }, - "magic-string": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.19.0.tgz", - "from": "magic-string@>=0.19.0 <0.20.0" + "minipass": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.0.1.tgz", + "from": "minipass@>=2.0.0 <3.0.0" + }, + "minizlib": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.3.tgz", + "from": "minizlib@>=1.0.3 <2.0.0" }, "reify": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/reify/-/reify-0.6.6.tgz", - "from": "reify@0.6.6" + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/reify/-/reify-0.11.0.tgz", + "from": "reify@0.11.0" }, - "vlq": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.1.tgz", - "from": "vlq@>=0.2.1 <0.3.0" + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "from": "semver@>=5.3.0 <6.0.0" + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "from": "yallist@>=3.0.0 <4.0.0" } } } diff --git a/packages/modules/buffer.js b/packages/modules/buffer.js deleted file mode 100644 index a894cdea97..0000000000 --- a/packages/modules/buffer.js +++ /dev/null @@ -1,3 +0,0 @@ -try { - Buffer = global.Buffer || require("buffer").Buffer; -} catch (noBuffer) {} diff --git a/packages/modules/client.js b/packages/modules/client.js index b69fd2b188..8b2ff2c406 100644 --- a/packages/modules/client.js +++ b/packages/modules/client.js @@ -1,6 +1,5 @@ require("./install-packages.js"); require("./stubs.js"); -require("./buffer.js"); require("./process.js"); require("./reify.js"); diff --git a/packages/modules/package.js b/packages/modules/package.js index e986ca01fa..e859d0340e 100644 --- a/packages/modules/package.js +++ b/packages/modules/package.js @@ -1,12 +1,12 @@ Package.describe({ name: "modules", - version: "0.9.0-beta.14", + version: "0.9.0-rc.1", summary: "CommonJS module system", documentation: "README.md" }); Npm.depends({ - reify: "0.6.6" + reify: "0.11.0" }); Package.onUse(function(api) { @@ -15,6 +15,5 @@ Package.onUse(function(api) { api.mainModule("client.js", "client"); api.mainModule("server.js", "server"); api.export("meteorInstall"); - api.export("Buffer"); api.export("process"); }); diff --git a/packages/modules/reify.js b/packages/modules/reify.js index 5b8fa26bcf..6bbb48af8a 100644 --- a/packages/modules/reify.js +++ b/packages/modules/reify.js @@ -1,5 +1,5 @@ var Module = module.constructor; -require("reify/lib/runtime").enable(Module); var Mp = Module.prototype; +require("reify/lib/runtime").enable(Mp); Mp.importSync = Mp.importSync || Mp.import; Mp.import = Mp.import || Mp.importSync; diff --git a/packages/modules/server.js b/packages/modules/server.js index aa8bc7c394..0f127659b1 100644 --- a/packages/modules/server.js +++ b/packages/modules/server.js @@ -1,4 +1,3 @@ require("./install-packages.js"); -require("./buffer.js"); require("./process.js"); require("./reify.js"); diff --git a/packages/mongo/collection.js b/packages/mongo/collection.js index 000e84e850..45b296d860 100644 --- a/packages/mongo/collection.js +++ b/packages/mongo/collection.js @@ -558,7 +558,7 @@ Mongo.Collection.prototype.update = function update(selector, modifier, ...optio if (!(typeof options.insertedId === 'string' || options.insertedId instanceof Mongo.ObjectID)) throw new Error("insertedId must be string or ObjectID"); insertedId = options.insertedId; - } else if (! selector._id) { + } else if (!selector || !selector._id) { insertedId = this._makeNewID(); options.insertedId = insertedId; } diff --git a/packages/mongo/mongo_livedata_tests.js b/packages/mongo/mongo_livedata_tests.js index 09759f0097..1e1ea0174e 100644 --- a/packages/mongo/mongo_livedata_tests.js +++ b/packages/mongo/mongo_livedata_tests.js @@ -1301,6 +1301,29 @@ testAsyncMulti('mongo-livedata - upsert without callback, ' + idGeneration, [ } ]); +// Regression test for https://github.com/meteor/meteor/issues/8666. +testAsyncMulti('mongo-livedata - upsert with an undefined selector, ' + idGeneration, [ + function (test, expect) { + this.collectionName = Random.id(); + if (Meteor.isClient) { + Meteor.call('createInsecureCollection', this.collectionName); + Meteor.subscribe('c-' + this.collectionName, expect()); + } + }, function (test, expect) { + var coll = new Mongo.Collection(this.collectionName, collectionOptions); + var testWidget = { + name: 'Widget name' + }; + coll.upsert(testWidget._id, testWidget, expect(function (error, insertDetails) { + test.isFalse(error); + test.equal( + coll.findOne(insertDetails.insertedId), + Object.assign({ _id: insertDetails.insertedId }, testWidget) + ); + })); + } +]); + // See https://github.com/meteor/meteor/issues/594. testAsyncMulti('mongo-livedata - document with length, ' + idGeneration, [ function (test, expect) { diff --git a/packages/mongo/package.js b/packages/mongo/package.js index f268d568cc..c5ea346556 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -9,7 +9,7 @@ Package.describe({ summary: "Adaptor for using MongoDB and Minimongo over DDP", - version: '1.1.16' + version: '1.1.18-rc.1' }); Npm.depends({ diff --git a/packages/non-core/bundle-visualizer/.npm/package/.gitignore b/packages/non-core/bundle-visualizer/.npm/package/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/non-core/bundle-visualizer/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/non-core/bundle-visualizer/.npm/package/README b/packages/non-core/bundle-visualizer/.npm/package/README new file mode 100644 index 0000000000..3d492553a4 --- /dev/null +++ b/packages/non-core/bundle-visualizer/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/non-core/bundle-visualizer/.npm/package/npm-shrinkwrap.json b/packages/non-core/bundle-visualizer/.npm/package/npm-shrinkwrap.json new file mode 100644 index 0000000000..6b10c34bd8 --- /dev/null +++ b/packages/non-core/bundle-visualizer/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,189 @@ +{ + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "from": "commander@>=2.0.0 <3.0.0" + }, + "d3": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-4.8.0.tgz", + "from": "d3@4.8.0" + }, + "d3-array": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.0.tgz", + "from": "d3-array@1.2.0" + }, + "d3-axis": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.6.tgz", + "from": "d3-axis@1.0.6" + }, + "d3-brush": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.4.tgz", + "from": "d3-brush@1.0.4" + }, + "d3-chord": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.4.tgz", + "from": "d3-chord@1.0.4" + }, + "d3-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.3.tgz", + "from": "d3-collection@1.0.3" + }, + "d3-color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.0.3.tgz", + "from": "d3-color@1.0.3" + }, + "d3-dispatch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.3.tgz", + "from": "d3-dispatch@1.0.3" + }, + "d3-drag": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.0.4.tgz", + "from": "d3-drag@1.0.4" + }, + "d3-dsv": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.5.tgz", + "from": "d3-dsv@1.0.5" + }, + "d3-ease": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.3.tgz", + "from": "d3-ease@1.0.3" + }, + "d3-force": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.0.6.tgz", + "from": "d3-force@1.0.6" + }, + "d3-format": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.2.0.tgz", + "from": "d3-format@1.2.0" + }, + "d3-geo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.6.3.tgz", + "from": "d3-geo@1.6.3" + }, + "d3-hierarchy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.4.tgz", + "from": "d3-hierarchy@1.1.4" + }, + "d3-interpolate": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.1.4.tgz", + "from": "d3-interpolate@1.1.4" + }, + "d3-path": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.5.tgz", + "from": "d3-path@1.0.5" + }, + "d3-polygon": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.3.tgz", + "from": "d3-polygon@1.0.3" + }, + "d3-quadtree": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.3.tgz", + "from": "d3-quadtree@1.0.3" + }, + "d3-queue": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/d3-queue/-/d3-queue-3.0.5.tgz", + "from": "d3-queue@3.0.5" + }, + "d3-random": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.0.3.tgz", + "from": "d3-random@1.0.3" + }, + "d3-request": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-request/-/d3-request-1.0.5.tgz", + "from": "d3-request@1.0.5" + }, + "d3-scale": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.5.tgz", + "from": "d3-scale@1.0.5" + }, + "d3-selection": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.0.5.tgz", + "from": "d3-selection@1.0.5" + }, + "d3-shape": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.0.6.tgz", + "from": "d3-shape@1.0.6" + }, + "d3-time": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.6.tgz", + "from": "d3-time@1.0.6" + }, + "d3-time-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.0.5.tgz", + "from": "d3-time-format@2.0.5" + }, + "d3-timer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.5.tgz", + "from": "d3-timer@1.0.5" + }, + "d3-transition": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.0.4.tgz", + "from": "d3-transition@1.0.4" + }, + "d3-voronoi": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", + "from": "d3-voronoi@1.1.2" + }, + "d3-zoom": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.1.4.tgz", + "from": "d3-zoom@1.1.4" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "from": "graceful-readlink@>=1.0.0" + }, + "iconv-lite": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz", + "from": "iconv-lite@>=0.4.0 <0.5.0" + }, + "pretty-bytes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", + "from": "pretty-bytes@4.0.2" + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "from": "rw@>=1.0.0 <2.0.0" + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "from": "xmlhttprequest@>=1.0.0 <2.0.0" + } + } +} diff --git a/packages/non-core/bundle-visualizer/README.md b/packages/non-core/bundle-visualizer/README.md new file mode 100644 index 0000000000..90e47671a1 --- /dev/null +++ b/packages/non-core/bundle-visualizer/README.md @@ -0,0 +1 @@ +# bundle-visualizer \ No newline at end of file diff --git a/packages/non-core/bundle-visualizer/classNames.js b/packages/non-core/bundle-visualizer/classNames.js new file mode 100644 index 0000000000..60e508062c --- /dev/null +++ b/packages/non-core/bundle-visualizer/classNames.js @@ -0,0 +1,3 @@ +import { prefixedClass } from "./common.js"; +export const rootContainer = prefixedClass("rootContainer"); +export const mask = prefixedClass("mask"); diff --git a/packages/non-core/bundle-visualizer/client.js b/packages/non-core/bundle-visualizer/client.js new file mode 100644 index 0000000000..2c61989c3c --- /dev/null +++ b/packages/non-core/bundle-visualizer/client.js @@ -0,0 +1,46 @@ +import { Meteor } from "meteor/meteor"; +import { + classPrefix, + methodNameStats, + packageName, +} from "./common.js"; +import * as classes from "./classNames.js"; + +import "./style.css"; + +Meteor.startup(() => { + import("./sunburst.js").then(s => main(s.Sunburst)); +}); + +function main(builder) { + const { container, mask } = frameStage(); + + document.body.appendChild(mask); + document.body.appendChild(container); + + Meteor.call(methodNameStats, (error, result) => { + if (error) { + console.error([ + `${packageName}: Couldn't load stats for visualization.`, + "Are you using standard-minifier-js >= 2.1.0 as the minifier?", + ].join(" ")); + return; + } + + // Load the JSON, which is `d3-hierarchy` digestible. + if (result) { + new builder({ container }).loadJson(result); + } + }); +} + +function frameStage() { + // Create the mask which will block out the main application. + const mask = document.createElement("div"); + mask.setAttribute("class", `${classPrefix} ${classes.mask}`); + + // Create the container which the SVG elements will be drawn into. + const container = document.createElement("div"); + container.setAttribute("class", `${classPrefix} ${classes.rootContainer}`); + return { container, mask }; +} diff --git a/packages/non-core/bundle-visualizer/common.js b/packages/non-core/bundle-visualizer/common.js new file mode 100644 index 0000000000..c36a0704ad --- /dev/null +++ b/packages/non-core/bundle-visualizer/common.js @@ -0,0 +1,14 @@ +export const packageName = "bundle-visualizer"; +export const classPrefix = "meteorBundleVisualizer"; +export const methodNameStats = `_meteor/${packageName}/stats`; +export const typeBundle = "bundle"; +export const typePackage = "package"; +export const typeNodeModules = "node_modules"; + +function capitalizeFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function prefixedClass(className) { + return `${classPrefix}${capitalizeFirstLetter(className)}`; +} diff --git a/packages/non-core/bundle-visualizer/package.js b/packages/non-core/bundle-visualizer/package.js new file mode 100644 index 0000000000..c6878afde4 --- /dev/null +++ b/packages/non-core/bundle-visualizer/package.js @@ -0,0 +1,23 @@ +Package.describe({ + version: '1.0.0', + summary: 'Meteor bundle analysis and visualization.', + documentation: 'README.md', +}); + +Npm.depends({ + "d3-selection": "1.0.5", + "d3-shape": "1.0.6", + "d3-hierarchy": "1.1.4", + "d3-transition": "1.0.4", + "pretty-bytes": "4.0.2", +}); + +Package.onUse(function(api) { + api.use('isobuild:dynamic-import@1.5.0'); + api.use([ + 'ecmascript', + 'dynamic-import', + ]); + api.mainModule('server.js', 'server'); + api.mainModule('client.js', 'client'); +}); diff --git a/packages/non-core/bundle-visualizer/server.js b/packages/non-core/bundle-visualizer/server.js new file mode 100644 index 0000000000..2a38b09da2 --- /dev/null +++ b/packages/non-core/bundle-visualizer/server.js @@ -0,0 +1,144 @@ +import assert from "assert"; +import { readFileSync as fsReadFileSync } from "fs"; + +import { Meteor } from "meteor/meteor"; +import { WebAppInternals } from "meteor/webapp"; + +import { + methodNameStats, + packageName, + typeBundle, + typeNodeModules, + typePackage, +} from "./common.js"; + +if (Meteor.isProduction) { + console.warn([ + `=> The "${packageName}" package is currently enabled. Visit your`, + "application in a web browser to view the client bundle analysis and", + "'meteor remove' the package before building/deploying the final bundle.", + ].join(" ")); +} else { + console.warn([ + "=> In order to provide accurate measurements using minified bundles,", + `the "${packageName}" package requires running 'meteor --production'`, + "to simulate production bundling." + ].join(" ")); +} + +function getStatBundles() { + const statFileFilter = f => + f.type === "json" && + f.absolutePath && + f.absolutePath.endsWith(".stats.json"); + + // Read the stat file, but if it's in any way unusable just return null. + const readOrNull = file => { + try { + return JSON.parse(fsReadFileSync(file, "utf8")); + } catch (err) { + return null; + } + }; + + return Object.keys(WebAppInternals.staticFiles) + .map(staticFile => WebAppInternals.staticFiles[staticFile]) + .filter(statFileFilter) + .map(statFile => ({ + name: statFile.hash, + stats: readOrNull(statFile.absolutePath), + })); +} + +function _childModules(node) { + return Object.keys(node) + .map(module => { + const result = { + name: module, + type: typeNodeModules, + }; + + if (typeof node[module] === "object") { + result.children = _childModules(node[module]); + } else { + result.size = node[module]; + } + + return result; + }); +} + +function d3TreeFromStats(stats) { + assert.strictEqual(typeof stats, "object", + "Must pass a stats object"); + assert.strictEqual(typeof stats.minifiedBytesByPackage, "object", + "Stats object must contain a `minifiedBytesByPackage` object"); + + const sizeOrDetail = (name, node) => { + const result = { + name, + type: typePackage, + }; + + // A non-leaf is: [size (Number), limb (Object)] + // A leaf is size (Number) + if (Array.isArray(node)) { + const [, detail] = node; + result.children = _childModules(detail); + } else { + result.size = node; + } + + return result; + }; + + // Main entry into the stats is the `minifiedBytesByPackage` attribute. + return Object.keys(stats.minifiedBytesByPackage) + .map(name => + sizeOrDetail(name + // Change the "packages/bundle.js" name to "(bundle)" + .replace(/^[^\/]+\/(.*)\.js$/, "($1)"), + stats.minifiedBytesByPackage[name])); +} + +Meteor.methods({ + [methodNameStats]() { + const statBundles = getStatBundles(); + + // Silently return no data if not simulating production. + if (! Meteor.isProduction) { + return null; + } + + if (! (statBundles && statBundles.length)) { + throw new Meteor.Error("no-stats-bundles", "Unable to retrieve stats"); + } + + const validStatBundles = statBundles.filter(statBundle => { + if (statBundle && + statBundle.stats && + statBundle.stats.minifier && + statBundle.stats.minifier.name === "standard-minifier-js" && + statBundle.stats.minifier.version.startsWith("2.1.") + ) { + return true; + } + }); + + if (! validStatBundles.length) { + throw new Meteor.Error("no-valid-stats", "No valid stats bundles") + } + + return { + name: "main", + children: validStatBundles.map((statBundle, index, array) => ({ + // TODO: If multiple bundles, could + // show abbr. bundle names with: + // `...${bundle.name.substr(-3)}`, + name: "bundle" + (array.length > 1 ? ` (${index + 1})` : ""), + type: typeBundle, + children: d3TreeFromStats(statBundle.stats), + })), + }; + } +}); diff --git a/packages/non-core/bundle-visualizer/style.css b/packages/non-core/bundle-visualizer/style.css new file mode 100644 index 0000000000..f92bcbc50c --- /dev/null +++ b/packages/non-core/bundle-visualizer/style.css @@ -0,0 +1,101 @@ +.meteorBundleVisualizer.meteorBundleVisualizerMask { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: #cdcdcd; + opacity: 0.5; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer { + position: absolute; + top: 0; + font-family: 'Open Sans', sans-serif; + font-size: 12px; + font-weight: 400; + width: 960px; + height: 700px; + margin-top: 10px; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerMain +{ + float: left; + width: 950px; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerSequence +{ + width: 800px; + height: 70px; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerSequence text, +{ + font-weight: 600; + fill: #fff; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerChart +{ + position: relative; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerChart path +{ + stroke: #fff; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerExplanation +{ + position: absolute; + top: 260px; + left: 405px; + width: 140px; + text-align: center; + color: #000; + z-index: -1; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerPercentage +{ + font-size: 2.5em; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerBytes +{ + font-size: 2em; + font-weight: bold; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerTrail +{ + position: absolute; + font-family: 'Open Sans', sans-serif; + font-size: 12px; + left: 0; + min-width: 180px; + font-weight: bold; +} + +.meteorBundleVisualizer.meteorBundleVisualizerRootContainer + .meteorBundleVisualizerTrail .meteorBundleVisualizerTrailSegment +{ + height: 40px; + border-radius: 3px; + border: 2px dotted black; + text-align: center; + vertical-align: middle; + line-height: 40px; + margin-bottom: 5px; +} diff --git a/packages/non-core/bundle-visualizer/sunburst.js b/packages/non-core/bundle-visualizer/sunburst.js new file mode 100644 index 0000000000..612f8938cc --- /dev/null +++ b/packages/non-core/bundle-visualizer/sunburst.js @@ -0,0 +1,275 @@ +/** + Inspired-by, borrowed-from and improved-upon another sundial provided under + the Apache License: + + https://bl.ocks.org/kerryrodden/766f8f6d31f645c39f488a0befa1e3c8 + + Copyright 2013 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import assert from "assert"; +import prettyBytes from "pretty-bytes"; + +// Make a custom "d3" object containing exactly what we need from the +// modularized d3 bundles. +const d3 = Object.assign({}, + { selectAll, select, mouse } = require("d3-selection"), + { arc } = require("d3-shape"), + { hierarchy, partition } = require("d3-hierarchy"), + { keys, entries } = require("d3-collection"), +); + +// This is imported only for its side effects, which affect the d3 namespace. +import "d3-transition"; + +import { + typeBundle, + typePackage, + typeNodeModules, + prefixedClass, +} from "./common.js"; + +import * as classes from "./classNames.js"; + +// Dimensions of sunburst. +const width = 950; +const height = 600; +const radius = Math.min(width, height) / 2; + +// Mapping of step names to colors. +const DEFAULT_COLORS = { + "_default_": "#ababab", + [typeBundle]: "#de4f4f", + [typePackage]: "#de783b", + [typeNodeModules]: "#7b615c", + "meteor": "#6ab975", + "javascript": "#a173d1", +}; + +export class Sunburst { + constructor({ + container, + colors = DEFAULT_COLORS, + } = {}) { + this.elements = {}; + this.colors = colors; + this.totalSize = 0; + + assert.strictEqual(typeof container, "object", + "Must pass a 'container' element"); + + this.elements.container = d3.select(container); + + this.elements.main = + this.elements.container + .append("div") + .attr("class", prefixedClass("main")); + + this.elements.sequence = + this.elements.main + .append("div") + .attr("class", prefixedClass("sequence")); + + this.elements.chart = + this.elements.main + .append("div") + .attr("class", prefixedClass("chart")); + + this.elements.explanation = + this.elements.chart + .append("div") + .attr("class", prefixedClass("explanation")); + + this.elements.percentage = + this.elements.explanation + .append("span") + .attr("class", prefixedClass("percentage")); + + // BR between percentage and bytes. + this.elements.explanation.append("br"); + + this.elements.bytes = + this.elements.explanation + .append("span") + .attr("class", prefixedClass("bytes")); + + this.svg = this.elements.chart + .append("svg:svg") + .attr("width", width) + .attr("height", height); + + this.vis = this.svg + .append("svg:g") + .attr("class", prefixedClass("top")) + .attr("transform", `translate(${width / 2},${height / 2})`); + + this.partition = d3.partition() + .size([2 * Math.PI, radius * radius]); + + this.arc = d3.arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .innerRadius(d => Math.sqrt(d.y0)) + .outerRadius(d => Math.sqrt(d.y1)); + } + + getColor(data) { + if (data.type === typePackage) { + return this.colors[typePackage]; + } + + if (data.name.endsWith(".js")) { + return this.colors.javascript; + } + + if (this.colors[data.name]) { + return this.colors[data.name]; + } + + return this.colors._default_; + } + + initializeBreadcrumbTrail() { + // Add the svg area. + this.elements.trail = + this.elements.container + .append("div") + .attr("class", prefixedClass("trail")); + } + + loadJson(json) { + // Basic setup of page elements. + this.initializeBreadcrumbTrail(); + + // Bounding circle underneath the sunburst, to make it easier to detect + // when the mouse leaves the parent g. + this.vis + .append("svg:circle") + .attr("r", radius) + .style("opacity", 0); + + // Turn the data into a d3 hierarchy and calculate the sums. + this.root = d3.hierarchy(json) + .sum(d => d.size) + .sort((a, b) => b.value - a.value); + + // For efficiency, filter nodes to keep only those large enough to see. + this.nodes = this + .partition(this.root) + .descendants() + .filter(d => d.x1 - d.x0 > 0.005); // 0.005 radians = 0.29 degrees + + this.path = this.vis.data([json]).selectAll("path") + .data(this.nodes) + .enter() + .append("svg:path") + .attr("display", d => d.depth ? null : "none") + .attr("d", this.arc) + .attr("fill-rule", "evenodd") + .style("fill", d => this.getColor(d.data)) + .style("opacity", 1) + .on("mouseover", this.mouseoverEvent()); + + // Add the mouseleave handler to the bounding circle. + this.vis.on("mouseleave", this.mouseleaveEvent()); + + // // Get total size of the tree = value of root node from partition. + this.totalSize = this.path.datum().value; + } + + mouseoverEvent() { + const self = this; + return self.mouseover || (self.mouseover = function (d) { + const percentage = (100 * d.value / self.totalSize).toPrecision(3); + let percentageString = `${percentage}%`; + if (percentage < 0.1) { + percentageString = "< 0.1%"; + } + + self.elements.percentage + .text(percentageString); + + self.elements.bytes + .text(prettyBytes(d.value || 0)); + + self.elements.explanation + .style("display", null); + + const sequenceArray = d.ancestors().reverse(); + sequenceArray.shift(); // remove root node from the array + self.updateBreadcrumbs(sequenceArray, percentageString); + + // Fade all the segments. + d3.selectAll("path") + .style("opacity", 0.3); + + // Then highlight only those that are an ancestor of the current segment. + self.vis.selectAll("path") + .filter((node) => sequenceArray.indexOf(node) >= 0) + .style("opacity", 1); + }); + } + + // Restore everything to full opacity when moving off the visualization. + mouseleaveEvent() { + const self = this; + return self.mouseleave || (self.mouseleave = function (d) { + // Hide the breadcrumb trail + self.elements.trail + .style("visibility", "hidden"); + + // Deactivate all segments during transition. + d3.selectAll("path").on("mouseover", null); + + // Transition each segment to full opacity and then reactivate it. + d3.selectAll("path") + .transition() + .duration(1000) + .style("opacity", 1) + .on("end", function() { + d3.select(this).on("mouseover", self.mouseoverEvent()); + }); + + self.elements.explanation + .style("display", "none"); + }); + } + + // Update the breadcrumb trail to show the current sequence and percentage. + updateBreadcrumbs(nodeArray, percentageString) { + // Data join; key function combines name and depth (= position in sequence). + const trail = this.elements.trail + .selectAll("div") + .data(nodeArray, d => d.data.name + d.depth); + + // Remove exiting nodes. + trail.exit().remove(); + + // Add breadcrumb and label for entering nodes. + const entering = trail.enter() + .append("div") + .attr("class", prefixedClass("trailSegment")) + .style("background-color", d => this.getColor(d.data)) + .text(d => d.data.name); + + // Merge enter and update selections; set position for all nodes. + entering + .merge(trail); + + // Make the breadcrumb trail visible, if it's hidden. + this.elements.trail + .style("visibility", ""); + } +} diff --git a/packages/reactive-dict/package.js b/packages/reactive-dict/package.js index c7d75f7fd0..7764a40bd9 100644 --- a/packages/reactive-dict/package.js +++ b/packages/reactive-dict/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Reactive dictionary", - version: '1.1.8' + version: '1.1.9-rc.1' }); Package.onUse(function (api) { diff --git a/packages/reactive-dict/reactive-dict-tests.js b/packages/reactive-dict/reactive-dict-tests.js index f67530d011..fdf5ad7d9d 100644 --- a/packages/reactive-dict/reactive-dict-tests.js +++ b/packages/reactive-dict/reactive-dict-tests.js @@ -6,6 +6,16 @@ Tinytest.add('ReactiveDict - set to undefined', function (test) { test.equal(dict.get('foo'), undefined); }); +Tinytest.add('ReactiveDict - initialize with data', function (test) { + var now = new Date(); + var dict = new ReactiveDict({ + now: now + }); + + var nowFromDict = dict.get('now'); + test.equal(nowFromDict, now); +}); + Tinytest.add('ReactiveDict - setDefault', function (test) { var dict = new ReactiveDict; dict.set('A', 'blah'); diff --git a/packages/reactive-dict/reactive-dict.js b/packages/reactive-dict/reactive-dict.js index a4d4592a54..4ff1a94634 100644 --- a/packages/reactive-dict/reactive-dict.js +++ b/packages/reactive-dict/reactive-dict.js @@ -27,7 +27,10 @@ ReactiveDict = function (dictName) { this.name = dictName; } else if (typeof dictName === 'object') { // back-compat case: dictName is actually migrationData - this.keys = dictName; + this.keys = {}; + for (let [key, value] of Object.entries(dictName)) { + this.keys[key] = stringify(value); + } } else { throw new Error("Invalid ReactiveDict argument: " + dictName); } diff --git a/packages/standard-minifier-js/package.js b/packages/standard-minifier-js/package.js index e2bc14e36e..db8fd550dc 100644 --- a/packages/standard-minifier-js/package.js +++ b/packages/standard-minifier-js/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'standard-minifier-js', - version: '2.0.0', + version: '2.1.0-rc.1', summary: 'Standard javascript minifiers used with Meteor apps by default.', documentation: 'README.md', }); @@ -9,9 +9,14 @@ Package.registerBuildPlugin({ name: "minifyStdJS", use: [ 'minifier-js', + 'babel-compiler', + 'ecmascript' ], sources: [ 'plugin/minify-js.js', + 'plugin/stats.js', + 'plugin/visitor.js', + 'plugin/utils.js', ], }); diff --git a/packages/standard-minifier-js/plugin/minify-js.js b/packages/standard-minifier-js/plugin/minify-js.js index 202f3b2a55..d1d67482d1 100644 --- a/packages/standard-minifier-js/plugin/minify-js.js +++ b/packages/standard-minifier-js/plugin/minify-js.js @@ -1,3 +1,5 @@ +import { extractModuleSizesTree } from "./stats.js"; + Plugin.registerMinifier({ extensions: ['js'], archMatching: 'web' @@ -108,37 +110,52 @@ MeteorBabelMinifier.prototype.processFilesForBundle = function(files, options) { } } - var allJs = ''; - files.forEach(function (file) { - // Don't reminify *.min.js. - if (/\.min\.js$/.test(file.getPathInBundle())) { - allJs += file.getContentsAsString(); - } else { - var minified; + const toBeAdded = { + data: "", + stats: Object.create(null) + }; - try { - minified = meteorJsMinify(file.getContentsAsString()); + files.forEach(file => { + // Don't reminify *.min.js. + if (/\.min\.js$/.test(file.getPathInBundle())) { + toBeAdded.data += file.getContentsAsString(); + } else { + var minified; - if (!(minified && typeof minified.code === "string")) { - throw new Error(); - } - } catch (err) { - var filePath = file.getPathInBundle(); + try { + minified = meteorJsMinify(file.getContentsAsString()); - maybeThrowMinifyErrorBySourceFile(err, file); - - err.message += " while minifying " + filePath; - throw err; + if (!(minified && typeof minified.code === "string")) { + throw new Error(); } - allJs += minified.code; - } - allJs += '\n\n'; + } catch (err) { + var filePath = file.getPathInBundle(); - Plugin.nudge(); - }); + maybeThrowMinifyErrorBySourceFile(err, file); + + err.message += " while minifying " + filePath; + throw err; + } + + const tree = extractModuleSizesTree(minified.code); + if (tree) { + toBeAdded.stats[file.getPathInBundle()] = + [Buffer.byteLength(minified.code), tree]; + } else { + toBeAdded.stats[file.getPathInBundle()] = + Buffer.byteLength(minified.code); + } + + toBeAdded.data += minified.code; + } + + toBeAdded.data += '\n\n'; + + Plugin.nudge(); + }); if (files.length) { - files[0].addJavaScript({ data: allJs }); + files[0].addJavaScript(toBeAdded); } }; diff --git a/packages/standard-minifier-js/plugin/stats.js b/packages/standard-minifier-js/plugin/stats.js new file mode 100644 index 0000000000..b8c5d408c9 --- /dev/null +++ b/packages/standard-minifier-js/plugin/stats.js @@ -0,0 +1,83 @@ +import Visitor from "./visitor.js"; + +// This RegExp will be used to scan the source for calls to meteorInstall, +// taking into consideration that the function name may have been mangled +// to something other than "meteorInstall" by the minifier. +const meteorInstallRegExp = new RegExp([ + // If meteorInstall is called by its unminified name, then that's what + // we should be looking for in the AST. + /\b(meteorInstall)\(\{/, + // If the meteorInstall function name has been minified, we can figure + // out its mangled name by examining the import assingment. + /\b(\w+)=Package.modules.meteorInstall\b/, + /\b(\w+)=Package\["modules-runtime"\].meteorInstall\b/, +].map(exp => exp.source).join("|")); + +export function extractModuleSizesTree(source) { + const match = meteorInstallRegExp.exec(source); + if (match) { + const ast = Babel.parse(source); + const name = match[1] || match[2] || match[3]; + meteorInstallVisitor.visit(ast, name, source); + return meteorInstallVisitor.tree; + } +} + +const meteorInstallVisitor = new (class extends Visitor { + reset(root, meteorInstallName, source) { + this.name = meteorInstallName; + this.source = source; + this.tree = null; + } + + visitCallExpression(node) { + if (this.tree !== null) { + return; + } + + if (isIdWithName(node.callee, this.name)) { + const source = this.source; + + function walk(expr) { + if (expr.type !== "ObjectExpression") { + return Buffer.byteLength(source.slice(expr.start, expr.end)); + } + + const contents = Object.create(null); + + expr.properties.forEach(prop => { + const keyName = getKeyName(prop.key); + if (typeof keyName === "string") { + contents[keyName] = walk(prop.value); + } + }); + + return contents; + } + + this.tree = walk(node.arguments[0]); + + } else { + this.visitChildren(node); + } + } +}); + +function isIdWithName(node, name) { + return node && + node.type === "Identifier" && + node.name === name; +} + +function getKeyName(key) { + if (key.type === "Identifier") { + return key.name; + } + + if (key.type === "StringLiteral" || + key.type === "Literal") { + return key.value; + } + + return null; +} \ No newline at end of file diff --git a/packages/standard-minifier-js/plugin/utils.js b/packages/standard-minifier-js/plugin/utils.js new file mode 100644 index 0000000000..0ce207b24e --- /dev/null +++ b/packages/standard-minifier-js/plugin/utils.js @@ -0,0 +1,26 @@ +"use strict"; + +const codeOfA = "A".charCodeAt(0); +const codeOfZ = "Z".charCodeAt(0); + +export function isObject(value) { + return typeof value === "object" && value !== null; +} + +// Without a complete list of Node .type names, we have to settle for this +// fuzzy matching of object shapes. However, the infeasibility of +// maintaining a complete list of type names is one of the reasons we're +// using the FastPath/Visitor abstraction in the first place. +export function isNodeLike(value) { + return isObject(value) && + ! Array.isArray(value) && + isCapitalized(value.type); +} + +function isCapitalized(string) { + if (typeof string !== "string") { + return false; + } + const code = string.charCodeAt(0); + return code >= codeOfA && code <= codeOfZ; +} diff --git a/packages/standard-minifier-js/plugin/visitor.js b/packages/standard-minifier-js/plugin/visitor.js new file mode 100644 index 0000000000..495dbb7307 --- /dev/null +++ b/packages/standard-minifier-js/plugin/visitor.js @@ -0,0 +1,57 @@ +"use strict"; + +import { + isObject, + isNodeLike, +} from "./utils.js"; + +const codeOfUnderscore = "_".charCodeAt(0); + +export default class Visitor { + visit(root) { + this.reset.apply(this, arguments); + this.visitWithoutReset(root); + } + + visitWithoutReset(node) { + if (Array.isArray(node)) { + node.forEach(this.visitWithoutReset, this); + } else if (isNodeLike(node)) { + const method = this["visit" + node.type]; + if (typeof method === "function") { + // The method must call this.visitChildren(node) to continue + // traversing. + method.call(this, node); + } else { + this.visitChildren(node); + } + } + } + + visitChildren(node) { + if (! isNodeLike(node)) { + return; + } + + const keys = Object.keys(node); + const keyCount = keys.length; + + for (let i = 0; i < keyCount; ++i) { + const key = keys[i]; + + if (key === "loc" || // Ignore .loc.{start,end} objects. + // Ignore "private" properties added by Babel. + key.charCodeAt(0) === codeOfUnderscore) { + continue; + } + + const child = node[key]; + if (! isObject(child)) { + // Ignore properties whose values aren't objects. + continue; + } + + this.visitWithoutReset(child); + } + } +} diff --git a/packages/tracker/package.js b/packages/tracker/package.js index 467854148e..e18baa9527 100644 --- a/packages/tracker/package.js +++ b/packages/tracker/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Dependency tracker to allow reactive callbacks", - version: '1.1.2' + version: '1.1.3' }); Package.onUse(function (api) { diff --git a/packages/webapp/package.js b/packages/webapp/package.js index c283fe57d0..ab2d77443f 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Serves a Meteor app over HTTP", - version: '1.3.15' + version: '1.3.16-rc.1' }); Npm.depends({connect: "2.30.2", diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 24a6cd7ad7..ff555ad387 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -395,7 +395,8 @@ WebAppInternals.staticFilesMiddleware = function (staticFiles, req, res, next) { info.sourceMapUrl); } - if (info.type === "js") { + if (info.type === "js" || + info.type === "dynamic js") { res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); } else if (info.type === "css") { res.setHeader("Content-Type", "text/css; charset=UTF-8"); diff --git a/scripts/admin/meteor-release-official.json b/scripts/admin/meteor-release-official.json index 748abf611f..1e5f97cce6 100644 --- a/scripts/admin/meteor-release-official.json +++ b/scripts/admin/meteor-release-official.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "1.4.4.1", + "version": "1.4.4.2", "recommended": false, "official": true, "description": "The Official Meteor Distribution" diff --git a/scripts/build-dev-bundle-common.sh b/scripts/build-dev-bundle-common.sh index 92daf529ed..478b158f15 100644 --- a/scripts/build-dev-bundle-common.sh +++ b/scripts/build-dev-bundle-common.sh @@ -7,7 +7,7 @@ UNAME=$(uname) ARCH=$(uname -m) MONGO_VERSION=3.2.12 NODE_VERSION=6.10.2 -NPM_VERSION=4.4.4 +NPM_VERSION=4.5.0 if [ "$UNAME" == "Linux" ] ; then if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 8f158bc44d..1e3a5bb437 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -11,17 +11,18 @@ var packageJson = { dependencies: { // Explicit dependency because we are replacing it with a bundled version // and we want to make sure there are no dependencies on a higher version - npm: "4.4.4", + npm: "4.5.0", "node-gyp": "3.6.0", "node-pre-gyp": "0.6.34", - "meteor-babel": "0.19.1", "meteor-promise": "0.8.2", + "meteor-babel": "0.21.1", + reify: "0.11.0", fibers: "1.0.15", promise: "7.1.1", // So that Babel 6 can emit require("babel-runtime/helpers/...") calls. "babel-runtime": "6.9.2", // For various ES2015 polyfills, such as Map and Set. - "meteor-ecmascript-runtime": "0.2.9", + "meteor-ecmascript-runtime": "0.3.0", // Not yet upgrading Underscore from 1.5.2 to 1.7.0 (which should be done // in the package too) because we should consider using lodash instead // (and there are backwards-incompatible changes either way). @@ -33,8 +34,8 @@ var packageJson = { tar: "2.2.1", kexec: "2.0.2", "source-map": "0.5.3", - "node-inspector": "0.12.8", - "v8-profiler": "5.6.5", + "node-inspector": "1.1.1", + "v8-profiler": "5.7.0", chalk: "0.5.1", sqlite3: "3.1.8", netroute: "1.0.2", diff --git a/tools/inspector.js b/tools/inspector.js index 21cc3ebaf6..c23087a135 100644 --- a/tools/inspector.js +++ b/tools/inspector.js @@ -145,7 +145,7 @@ DEp.connectToChildProcess = function connectToChildProcess(child) { // port (not debugPort!), and create a connection to that port so that // the child process can communicate with node-inspector. child.stderr.on("data", function onData(buffer) { - var match = /debugger listening on port (\d+)/i + var match = /debugger listening on .+:(\d+)\n/i .exec(buffer.toString("utf8")); if (match) { child.stderr.removeListener("data", onData); diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 3ee9c57c80..28129d3b39 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -159,6 +159,7 @@ import Builder from './builder.js'; var compilerPluginModule = require('./compiler-plugin.js'); import { JsFile, CssFile } from './minifier-plugin.js'; var meteorNpm = require('./meteor-npm.js'); +import { addToTree } from "./linker.js"; var files = require('../fs/files.js'); var archinfo = require('../utils/archinfo.js'); @@ -170,6 +171,7 @@ var packageVersionParser = require('../packaging/package-version-parser.js'); var release = require('../packaging/release.js'); import { load as loadIsopacket } from '../tool-env/isopackets.js'; import { CORDOVA_PLATFORM_VERSIONS } from '../cordova'; +import { gzipSync } from "zlib"; // files to ignore when bundling. node has no globs, so use regexps exports.ignoreFiles = [ @@ -353,7 +355,7 @@ export class NodeModulesDirectory { // Options consumed by readDirsFromJSON are listed above. Any other // options will be passed on to NodeModulesDirectory constructor via // this callerInfo object: - ...callerInfo, + ...callerInfo }) { assert.strictEqual(typeof callerInfo.sourceRoot, "string"); @@ -1056,6 +1058,9 @@ class Target { const jsOutputFilesMap = compilerPluginModule.PackageSourceBatch .computeJsOutputFilesMap(sourceBatches); + const versions = {}; + const dynamicImportFiles = new Set; + // Copy their resources into the bundle in order sourceBatches.forEach((sourceBatch) => { const unibuild = sourceBatch.unibuild; @@ -1177,6 +1182,16 @@ class Target { throw new Error('Unknown type ' + resource.type); }); + this.js.forEach(file => { + if (file.targetPath === "packages/dynamic-import.js") { + dynamicImportFiles.add(file); + } + + if (file.targetPath.startsWith("dynamic/")) { + addToTree(file.hash(), file.targetPath, versions); + } + }); + // Depend on the source files that produced these resources. this.watchSet.merge(unibuild.watchSet); @@ -1185,6 +1200,15 @@ class Target { // XXX assumes that this merges cleanly this.watchSet.merge(unibuild.pkg.pluginWatchSet); }); + + dynamicImportFiles.forEach(file => { + file.setContents( + new Buffer(file.contents("utf8").replace( + "__DYNAMIC_VERSIONS__", + () => JSON.stringify(versions.dynamic || {}) + ), "utf8") + ); + }); } // Minify the JS in this target @@ -1236,7 +1260,7 @@ class Target { const js = []; function handle(source, dynamic) { - const newFiles = source._minifiedFiles.map(file => { + source._minifiedFiles.forEach(file => { // Remove the function name __minifyJs that was added above. file.data = file.data .toString("utf8") @@ -1263,10 +1287,44 @@ class Target { newFile.setUrlToHash('.js', '?meteor_js_resource=true'); } - return newFile; - }); + js.push(newFile); - js.push(...newFiles); + if (file.stats && + ! dynamic && + minifyMode === "production") { + // If the minifier reported any statistics, serve those data as + // a .stats.json file alongside the newFile. + const contents = newFile.contents(); + const statsFile = new File({ + info: "bundle size stats JSON", + data: new Buffer(JSON.stringify({ + minifier: { + name: minifierDef.isopack.name, + version: minifierDef.isopack.version, + }, + totalMinifiedBytes: contents.length, + totalMinifiedGzipBytes: gzipSync(contents).length, + minifiedBytesByPackage: file.stats, + }, null, 2) + "\n", "utf8") + }); + + statsFile.url = newFile.url.replace(/\.js\b/, ".stats.json"); + statsFile.targetPath = + newFile.targetPath.replace(/\.js\b/, ".stats.json"); + statsFile.cacheable = true; + statsFile.type = "json"; + + if (statsFile.url !== newFile.url && + statsFile.targetPath !== newFile.targetPath) { + // If the minifier used a file extension other than .js, the + // .replace calls above won't inject the .stats.json extension + // into the statsFile.{url,targetPath} strings, and it would + // be a mistake to serve the statsFile with the same URL as + // the real JS bundle. This should be a very uncommon case. + js.push(statsFile); + } + } + }); } staticFiles.forEach(file => handle(file, false)); @@ -1484,7 +1542,7 @@ class ClientTarget extends Target { const eachResource = function (f) { ["js", "css", "asset"].forEach((type) => { this[type].forEach((file) => { - f(file, type); + f(file, file.type || type); }); }); }.bind(this); @@ -1540,20 +1598,28 @@ class ClientTarget extends Target { manifestItem.size = file.size(); manifestItem.hash = file.hash(); - if (! file.targetPath.startsWith("dynamic/")) { - writeFile(file, builder); - manifest.push(manifestItem); + writeFile(file, builder); + manifest.push(manifestItem); - } else if (manifestItem.sourceMapUrl) { + if (! file.targetPath.startsWith("dynamic/")) { + return; + } + + // Another measure for preventing this file from being loaded + // eagerly as a