mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'devel' of github.com:meteor/meteor into minimongo-without-underscore
This commit is contained in:
91
History.md
91
History.md
@@ -1,17 +1,51 @@
|
||||
## v.NEXT
|
||||
|
||||
* The `accounts-facebook` and `facebook-oauth` packages have been updated to
|
||||
use the v2.9 of the Facebook Graph API for the Login Dialog since the v2.2
|
||||
version will be deprecated by Facebook in July. There shouldn't be a problem
|
||||
regardless since Facebook simply rolls over to the next active version
|
||||
(v2.3, in this case) however this should assist in avoiding deprecation
|
||||
warnings and should enable any new functionality which has become available.
|
||||
[PR #8858](https://github.com/meteor/meteor/pull/8858)
|
||||
## v1.5.1, 2017-07-12
|
||||
|
||||
* `observe`/`observeChanges` callbacks are now bound using `Meteor.bindEnvironment`.
|
||||
The same `EnvironmentVariable`s that were present when `observe`/`observeChanges`
|
||||
was called are now available inside the callbacks.
|
||||
[PR #8734](https://github.com/meteor/meteor/pull/8734)
|
||||
* Node has been upgraded to version 4.8.4.
|
||||
|
||||
* A new core Meteor package called `server-render` provides generic
|
||||
support for server-side rendering of HTML, as described in the package's
|
||||
[`README.md`](https://github.com/meteor/meteor/blob/release-1.5.1/packages/server-render/README.md).
|
||||
[PR #8841](https://github.com/meteor/meteor/pull/8841)
|
||||
|
||||
* To reduce the total number of file descriptors held open by the Meteor
|
||||
build system, native file watchers will now be started only for files
|
||||
that have changed at least once. This new policy means you may have to
|
||||
[wait up to 5000ms](https://github.com/meteor/meteor/blob/6bde360b9c075f1c78c3850eadbdfa7fe271f396/tools/fs/safe-watcher.js#L20-L21)
|
||||
for changes to be detected when you first edit a file, but thereafter
|
||||
changes will be detected instantaneously. In return for that small
|
||||
initial waiting time, the number of open file descriptors will now be
|
||||
bounded roughly by the number of files you are actively editing, rather
|
||||
than the number of files involved in the build (often thousands), which
|
||||
should help with issues like
|
||||
[#8648](https://github.com/meteor/meteor/issues/8648). If you need to
|
||||
disable the new behavior for any reason, simply set the
|
||||
`METEOR_WATCH_PRIORITIZE_CHANGED` environment variable to `"false"`, as
|
||||
explained in [PR #8866](https://github.com/meteor/meteor/pull/8866).
|
||||
|
||||
* All `observe` and `observeChanges` callbacks are now bound using
|
||||
`Meteor.bindEnvironment`. The same `EnvironmentVariable`s that were
|
||||
present when `observe` or `observeChanges` was called are now available
|
||||
inside the callbacks. [PR #8734](https://github.com/meteor/meteor/pull/8734)
|
||||
|
||||
* A subscription's `onReady` is now fired again during a re-subscription, even
|
||||
if the subscription has the same arguments. Previously, when subscribing
|
||||
to a publication the `onReady` would have only been called if the arguments
|
||||
were different, creating a confusing difference in functionality. This may be
|
||||
breaking behavior if an app uses the firing of `onReady` as an assumption
|
||||
that the data was just received from the server. If such functionality is
|
||||
still necessary, consider using
|
||||
[`observe`](https://docs.meteor.com/api/collections.html#Mongo-Cursor-observe)
|
||||
or
|
||||
[`observeChanges`](https://docs.meteor.com/api/collections.html#Mongo-Cursor-observeChanges)
|
||||
[PR #8754](https://github.com/meteor/meteor/pull/8754)
|
||||
[Issue #1173](https://github.com/meteor/meteor/issues/1173)
|
||||
|
||||
* The `minimongo` and `mongo` packages are now compliant with the upsert behavior
|
||||
of MongoDB 2.6 and higher. **As a result support for MongoDB 2.4 has been dropped.**
|
||||
This mainly changes the effect of the selector on newly inserted documents.
|
||||
[PR #8815](https://github.com/meteor/meteor/pull/8815)
|
||||
|
||||
* `reactive-dict` now supports setting initial data when defining a named
|
||||
`ReactiveDict`. No longer run migration logic when used on the server,
|
||||
@@ -26,6 +60,14 @@
|
||||
proper domain in all applications.
|
||||
[PR #8760](https://github.com/meteor/meteor/issues/8760)
|
||||
|
||||
* The `accounts-facebook` and `facebook-oauth` packages have been updated to
|
||||
use the v2.9 of the Facebook Graph API for the Login Dialog since the v2.2
|
||||
version will be deprecated by Facebook in July. There shouldn't be a problem
|
||||
regardless since Facebook simply rolls over to the next active version
|
||||
(v2.3, in this case) however this should assist in avoiding deprecation
|
||||
warnings and should enable any new functionality which has become available.
|
||||
[PR #8858](https://github.com/meteor/meteor/pull/8858)
|
||||
|
||||
* Add `DDP._CurrentPublicationInvocation` and `DDP._CurrentMethodInvocation`.
|
||||
`DDP._CurrentInvocation` remains for backwards-compatibility. This change
|
||||
allows method calls from publications to inherit the `connection` from the
|
||||
@@ -52,7 +94,14 @@
|
||||
context and with its `EnvironmentVariable`s bound.
|
||||
[PR #8629](https://github.com/meteor/meteor/pull/8629)
|
||||
|
||||
* The `reify` npm package has been upgraded to version 0.11.22.
|
||||
* The `minifier-js` package will now replace `process.env.NODE_ENV` with
|
||||
its string value (or `"development"` if unspecified).
|
||||
|
||||
* The `meteor-babel` npm package has been upgraded to version 0.22.0.
|
||||
|
||||
* The `reify` npm package has been upgraded to version 0.11.24.
|
||||
|
||||
* The `uglify-js` npm package has been upgraded to version 3.0.18.
|
||||
|
||||
* Illegal characters in paths written in build output directories will now
|
||||
be replaced with `_`s rather than removed, so that file and directory
|
||||
@@ -61,7 +110,7 @@
|
||||
|
||||
* Additional "extra" packages (packages that aren't saved in `.meteor/packages`)
|
||||
can be included temporarily using the `--extra-packages`
|
||||
option. For example: `meteor run --extra-packages "bundle-visualizer"`.
|
||||
option. For example: `meteor run --extra-packages bundle-visualizer`.
|
||||
Both `meteor test` and `meteor test-packages` also support the
|
||||
`--extra-packages` option and commas separate multiple package names.
|
||||
[PR #8769](https://github.com/meteor/meteor/pull/8769)
|
||||
@@ -72,6 +121,22 @@
|
||||
* The `coffeescript` package has been updated to use CoffeeScript version
|
||||
1.12.6. [PR #8777](https://github.com/meteor/meteor/pull/8777)
|
||||
|
||||
* It's now possible to pipe a series of statements to `meteor shell`,
|
||||
whereas previously the input had to be an expression; for example:
|
||||
```sh
|
||||
> echo 'import pkg from "babel-runtime/package.json";
|
||||
quote> pkg.version' |
|
||||
pipe> meteor shell
|
||||
"6.23.0"
|
||||
```
|
||||
[Issue #8823](https://github.com/meteor/meteor/issues/8823)
|
||||
[PR #8833](https://github.com/meteor/meteor/pull/8833)
|
||||
|
||||
* Any `Error` thrown by a DDP method with the `error.isClientSafe`
|
||||
property set to `true` will now be serialized and displayed to the
|
||||
client, whereas previously only `Meteor.Error` objects were considered
|
||||
client-safe. [PR #8756](https://github.com/meteor/meteor/pull/8756)
|
||||
|
||||
## v1.5, 2017-05-30
|
||||
|
||||
* The `meteor-base` package implies a new `dynamic-import` package, which
|
||||
|
||||
7
meteor
7
meteor
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BUNDLE_VERSION=4.8.18
|
||||
BUNDLE_VERSION=4.8.21
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
@@ -132,4 +132,7 @@ fi
|
||||
# the script take precedence over $NODE_PATH; it used to be that users would
|
||||
# screw up their meteor installs by have a ~/node_modules
|
||||
|
||||
exec "$DEV_BUNDLE/bin/node" ${TOOL_NODE_FLAGS} "$METEOR" "$@"
|
||||
exec "$DEV_BUNDLE/bin/node" \
|
||||
--expose-gc \
|
||||
${TOOL_NODE_FLAGS} \
|
||||
"$METEOR" "$@"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "A user account system",
|
||||
version: "1.3.0"
|
||||
version: "1.3.1"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
Package.describe({
|
||||
summary: "Password support for accounts",
|
||||
version: "2.0.0"
|
||||
// This version was bumped to 2.0.0 temporarily during the Meteor 1.5.1
|
||||
// release process, so versions 2.0.0-beta.2 through -beta.5 and -rc.0
|
||||
// have already been published. The next time this package reaches 2.x
|
||||
// territory, I would recommend jumping straight to 2.1.0.
|
||||
version: "1.4.0"
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
name: 'allow-deny',
|
||||
version: '1.0.5',
|
||||
version: '1.0.6',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'Implements functionality for allow/deny and client-side db operations',
|
||||
// URL to the Git repository containing the source code for this package.
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
"from": "babel-code-frame@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babel-core": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz",
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.25.0.tgz",
|
||||
"from": "babel-core@>=6.22.1 <7.0.0"
|
||||
},
|
||||
"babel-generator": {
|
||||
"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"
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.25.0.tgz",
|
||||
"from": "babel-generator@>=6.25.0 <7.0.0"
|
||||
},
|
||||
"babel-helper-builder-react-jsx": {
|
||||
"version": "6.24.1",
|
||||
@@ -138,8 +138,8 @@
|
||||
}
|
||||
},
|
||||
"babel-plugin-minify-dead-code-elimination": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.1.6.tgz",
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.1.7.tgz",
|
||||
"from": "babel-plugin-minify-dead-code-elimination@>=0.1.3 <0.2.0"
|
||||
},
|
||||
"babel-plugin-minify-flip-comparisons": {
|
||||
@@ -280,8 +280,8 @@
|
||||
"from": "babel-plugin-transform-es2015-modules-commonjs@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-es2015-modules-reify": {
|
||||
"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",
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-reify/-/babel-plugin-transform-es2015-modules-reify-0.11.2.tgz",
|
||||
"from": "babel-plugin-transform-es2015-modules-reify@>=0.11.0 <0.12.0"
|
||||
},
|
||||
"babel-plugin-transform-es2015-object-super": {
|
||||
@@ -340,13 +340,13 @@
|
||||
"from": "babel-plugin-transform-inline-consecutive-adds@>=0.0.2 <0.0.3"
|
||||
},
|
||||
"babel-plugin-transform-member-expression-literals": {
|
||||
"version": "6.8.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.8.3.tgz",
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.8.4.tgz",
|
||||
"from": "babel-plugin-transform-member-expression-literals@>=6.8.1 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-merge-sibling-variables": {
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.8.4.tgz",
|
||||
"version": "6.8.5",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.8.5.tgz",
|
||||
"from": "babel-plugin-transform-merge-sibling-variables@>=6.8.2 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-minify-booleans": {
|
||||
@@ -360,13 +360,13 @@
|
||||
"from": "babel-plugin-transform-object-rest-spread@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-property-literals": {
|
||||
"version": "6.8.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.8.3.tgz",
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.8.4.tgz",
|
||||
"from": "babel-plugin-transform-property-literals@>=6.8.1 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-react-display-name": {
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.23.0.tgz",
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz",
|
||||
"from": "babel-plugin-transform-react-display-name@>=6.23.0 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-react-jsx": {
|
||||
@@ -395,13 +395,13 @@
|
||||
"from": "babel-plugin-transform-regexp-constructors@>=0.0.5 <0.0.6"
|
||||
},
|
||||
"babel-plugin-transform-remove-console": {
|
||||
"version": "6.8.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.8.3.tgz",
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.8.4.tgz",
|
||||
"from": "babel-plugin-transform-remove-console@>=6.8.0 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-remove-debugger": {
|
||||
"version": "6.8.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.8.3.tgz",
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.8.4.tgz",
|
||||
"from": "babel-plugin-transform-remove-debugger@>=6.8.0 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-remove-undefined": {
|
||||
@@ -415,8 +415,8 @@
|
||||
"from": "babel-plugin-transform-runtime@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-simplify-comparison-operators": {
|
||||
"version": "6.8.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.8.3.tgz",
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.8.4.tgz",
|
||||
"from": "babel-plugin-transform-simplify-comparison-operators@>=6.8.1 <7.0.0"
|
||||
},
|
||||
"babel-plugin-transform-strict-mode": {
|
||||
@@ -460,33 +460,33 @@
|
||||
"from": "babel-runtime@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babel-template": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz",
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz",
|
||||
"from": "babel-template@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babel-traverse": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz",
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz",
|
||||
"from": "babel-traverse@>=6.22.1 <7.0.0"
|
||||
},
|
||||
"babel-types": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz",
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz",
|
||||
"from": "babel-types@>=6.22.0 <7.0.0"
|
||||
},
|
||||
"babylon": {
|
||||
"version": "6.17.2",
|
||||
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.2.tgz",
|
||||
"version": "6.17.4",
|
||||
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.4.tgz",
|
||||
"from": "babylon@>=6.15.0 <7.0.0"
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
|
||||
"from": "balanced-match@>=0.4.1 <0.5.0"
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"from": "balanced-match@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz",
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
||||
"from": "brace-expansion@>=1.1.7 <2.0.0"
|
||||
},
|
||||
"chalk": {
|
||||
@@ -530,8 +530,8 @@
|
||||
"from": "esutils@>=2.0.2 <3.0.0"
|
||||
},
|
||||
"globals": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-9.17.0.tgz",
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
|
||||
"from": "globals@>=9.0.0 <10.0.0"
|
||||
},
|
||||
"has-ansi": {
|
||||
@@ -555,8 +555,8 @@
|
||||
"from": "is-finite@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||
"from": "js-tokens@>=3.0.0 <4.0.0"
|
||||
},
|
||||
"jsesc": {
|
||||
@@ -590,9 +590,9 @@
|
||||
"from": "loose-envify@>=1.0.0 <2.0.0"
|
||||
},
|
||||
"meteor-babel": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/meteor-babel/-/meteor-babel-0.21.4.tgz",
|
||||
"from": "meteor-babel@0.21.4"
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/meteor-babel/-/meteor-babel-0.22.0.tgz",
|
||||
"from": "meteor-babel@0.22.0"
|
||||
},
|
||||
"meteor-babel-helpers": {
|
||||
"version": "0.0.3",
|
||||
@@ -610,8 +610,8 @@
|
||||
"from": "minimist@0.0.8"
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.0.2.tgz",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz",
|
||||
"from": "minipass@>=2.0.0 <3.0.0"
|
||||
},
|
||||
"minizlib": {
|
||||
@@ -692,8 +692,8 @@
|
||||
}
|
||||
},
|
||||
"reify": {
|
||||
"version": "0.11.22",
|
||||
"resolved": "https://registry.npmjs.org/reify/-/reify-0.11.22.tgz",
|
||||
"version": "0.11.24",
|
||||
"resolved": "https://registry.npmjs.org/reify/-/reify-0.11.24.tgz",
|
||||
"from": "reify@>=0.11.18 <0.12.0"
|
||||
},
|
||||
"repeating": {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
var meteorBabel = null;
|
||||
function getMeteorBabel() {
|
||||
return meteorBabel || (meteorBabel = Npm.require("meteor-babel"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new object containing default options appropriate for
|
||||
*/
|
||||
function getDefaultOptions(extraFeatures) {
|
||||
var meteorBabel = Npm.require('meteor-babel');
|
||||
|
||||
// See https://github.com/meteor/babel/blob/master/options.js for more
|
||||
// information about what the default options are.
|
||||
var options = meteorBabel.getDefaultOptions(extraFeatures);
|
||||
var options = getMeteorBabel().getDefaultOptions(extraFeatures);
|
||||
|
||||
// The sourceMap option should probably be removed from the default
|
||||
// options returned by meteorBabel.getDefaultOptions.
|
||||
@@ -22,22 +25,24 @@ Babel = {
|
||||
validateExtraFeatures: Function.prototype,
|
||||
|
||||
parse: function (source) {
|
||||
return Npm.require('meteor-babel').parse(source);
|
||||
return getMeteorBabel().parse(source);
|
||||
},
|
||||
|
||||
compile: function (source, options) {
|
||||
var meteorBabel = Npm.require('meteor-babel');
|
||||
options = options || getDefaultOptions();
|
||||
return meteorBabel.compile(source, options);
|
||||
return getMeteorBabel().compile(source, options);
|
||||
},
|
||||
|
||||
setCacheDir: function (cacheDir) {
|
||||
Npm.require('meteor-babel').setCacheDir(cacheDir);
|
||||
getMeteorBabel().setCacheDir(cacheDir);
|
||||
},
|
||||
|
||||
minify: function(source, options) {
|
||||
var meteorBabel = Npm.require('meteor-babel');
|
||||
var options = options || meteorBabel.getMinifierOptions();
|
||||
return meteorBabel.minify(source, options);
|
||||
minify: function (source, options) {
|
||||
var options = options || getMeteorBabel().getMinifierOptions();
|
||||
return getMeteorBabel().minify(source, options);
|
||||
},
|
||||
|
||||
getMinifierOptions: function (extraFeatures) {
|
||||
return getMeteorBabel().getMinifierOptions(extraFeatures);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,11 +6,11 @@ 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.2'
|
||||
version: '6.19.4'
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
'meteor-babel': '0.21.4'
|
||||
'meteor-babel': '0.22.0'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
1
packages/boilerplate-generator-tests/.npm/package/.gitignore
vendored
Normal file
1
packages/boilerplate-generator-tests/.npm/package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
7
packages/boilerplate-generator-tests/.npm/package/README
Normal file
7
packages/boilerplate-generator-tests/.npm/package/README
Normal file
@@ -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.
|
||||
9
packages/boilerplate-generator-tests/.npm/package/npm-shrinkwrap.json
generated
Normal file
9
packages/boilerplate-generator-tests/.npm/package/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"parse5": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.2.tgz",
|
||||
"from": "parse5@3.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
0
packages/boilerplate-generator-tests/README.md
Normal file
0
packages/boilerplate-generator-tests/README.md
Normal file
13
packages/boilerplate-generator-tests/package.js
Normal file
13
packages/boilerplate-generator-tests/package.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Npm.depends({'parse5': '3.0.2'});
|
||||
Package.describe({
|
||||
// These tests are in a separate package so that we can Npm.depend on parse5, a html parsing library
|
||||
summary: "Tests for the boilerplate-generator package",
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.use(['tinytest', 'boilerplate-generator'], 'server');
|
||||
api.addFiles('web.browser-tests.js', 'server');
|
||||
api.addFiles('web.cordova-tests.js', 'server');
|
||||
});
|
||||
54
packages/boilerplate-generator-tests/test-lib.js
Normal file
54
packages/boilerplate-generator-tests/test-lib.js
Normal file
@@ -0,0 +1,54 @@
|
||||
export function generateHTMLForArch(arch) {
|
||||
// Use a dummy manifest. None of these paths will be read from the filesystem, but css / js should be handled differently
|
||||
const manifest = [
|
||||
{
|
||||
path: 'packages/bootstrap/css/bootstrap-responsive.css',
|
||||
where: 'client',
|
||||
type: 'css',
|
||||
cacheable: true,
|
||||
url: '/packages/bootstrap/css/bootstrap-responsive.css?hash=785760fc5ad665d7b54d56a3c2522797bb2cc150&v="1"',
|
||||
size: 22111,
|
||||
hash: '785760fc5ad665d7b54d56a3c2522797bb2cc150'
|
||||
},
|
||||
{
|
||||
path: 'packages/templating-runtime.js',
|
||||
where: 'client',
|
||||
type: 'js',
|
||||
cacheable: true,
|
||||
url: '/packages/templating-runtime.js?hash=c18de19afda6e9f0db7faf3d4382a4c953cabe18&v="1"',
|
||||
size: 24132,
|
||||
hash: 'c18de19afda6e9f0db7faf3d4382a4c953cabe18'
|
||||
},
|
||||
];
|
||||
|
||||
// Set some extra options for boilerplate data.
|
||||
// webapp_server usually constructs a Boilerplate object similarly
|
||||
const inline = true;
|
||||
const inlineScriptsAllowed = true;
|
||||
const additionalStaticJs = [];
|
||||
const meteorRuntimeConfig = 'config123';
|
||||
const rootUrlPathPrefix = 'rootUrlPathPrefix';
|
||||
const htmlAttributes = {
|
||||
foo: 'foobar',
|
||||
gems: '&"',
|
||||
};
|
||||
|
||||
// A dummy rewrite hook to test ampersands
|
||||
function bundledJsCssUrlRewriteHook(url) {
|
||||
return url + '+rewritten_url=true';
|
||||
}
|
||||
|
||||
const boilerplate = new Boilerplate(arch, manifest, {
|
||||
baseDataExtension: {
|
||||
htmlAttributes,
|
||||
additionalStaticJs,
|
||||
meteorRuntimeConfig,
|
||||
rootUrlPathPrefix,
|
||||
bundledJsCssUrlRewriteHook,
|
||||
inlineScriptsAllowed,
|
||||
inline
|
||||
},
|
||||
});
|
||||
|
||||
return boilerplate.toHTML();
|
||||
};
|
||||
41
packages/boilerplate-generator-tests/web.browser-tests.js
Normal file
41
packages/boilerplate-generator-tests/web.browser-tests.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { parse, serialize } from 'parse5';
|
||||
import { generateHTMLForArch } from './test-lib';
|
||||
|
||||
const html = generateHTMLForArch('web.browser');
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - well-formed html", function (test) {
|
||||
const formatted = serialize(parse(html));
|
||||
test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, ''));
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - include htmlAttributes", function (test) {
|
||||
test.matches(html, /foo="foobar"/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - escape htmlAttributes", function (test) {
|
||||
test.matches(html, /gems="&""/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - include js", function (test) {
|
||||
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - escape js", function (test) {
|
||||
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*&v="1"[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - include css", function (test) {
|
||||
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - escape css", function (test) {
|
||||
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*&v="1"[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - call rewriteHook", function (test) {
|
||||
test.matches(html, /\+rewritten_url=true/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.browser - include runtime config", function (test) {
|
||||
test.matches(html, /<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/);
|
||||
});
|
||||
33
packages/boilerplate-generator-tests/web.cordova-tests.js
Normal file
33
packages/boilerplate-generator-tests/web.cordova-tests.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { parse, serialize } from 'parse5';
|
||||
import { generateHTMLForArch } from './test-lib';
|
||||
|
||||
const html = generateHTMLForArch('web.cordova');
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - well-formed html", function (test) {
|
||||
const formatted = serialize(parse(html));
|
||||
test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, ''));
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - include js", function (test) {
|
||||
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - escape js", function (test) {
|
||||
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*&v="1"[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - include css", function (test) {
|
||||
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - escape css", function (test) {
|
||||
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*&v="1"[^<>]*">/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - do not call rewriteHook", function (test) {
|
||||
test.notMatches(html, /\+rewritten_url=true/);
|
||||
});
|
||||
|
||||
Tinytest.add("boilerplate-generator-tests - web.cordova - include runtime config", function (test) {
|
||||
test.matches(html, /<script[^<>]*>[^<>]*\n[^<>]*__meteor_runtime_config__ =[^<>]*decodeURIComponent\(config123\)\)/);
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
var fs = Npm.require('fs');
|
||||
var path = Npm.require('path');
|
||||
|
||||
// Copied from webapp_server
|
||||
var readUtf8FileSync = function (filename) {
|
||||
return Meteor.wrapAsync(fs.readFile)(filename, 'utf8');
|
||||
};
|
||||
|
||||
Boilerplate = function (arch, manifest, options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
self.template = _getTemplate(arch);
|
||||
self.baseData = null;
|
||||
self.func = null;
|
||||
|
||||
self._generateBoilerplateFromManifestAndSource(
|
||||
manifest,
|
||||
self.template,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
// The 'extraData' argument can be used to extend 'self.baseData'. Its
|
||||
// purpose is to allow you to specify data that you might not know at
|
||||
// the time that you construct the Boilerplate object. (e.g. it is used
|
||||
// by 'webapp' to specify data that is only known at request-time).
|
||||
Boilerplate.prototype.toHTML = function (extraData) {
|
||||
var self = this;
|
||||
|
||||
if (! self.baseData || ! self.func)
|
||||
throw new Error('Boilerplate did not instantiate correctly.');
|
||||
|
||||
return "<!DOCTYPE html>\n" +
|
||||
Blaze.toHTML(Blaze.With(_.extend({}, self.baseData, extraData),
|
||||
self.func));
|
||||
};
|
||||
|
||||
// XXX Exported to allow client-side only changes to rebuild the boilerplate
|
||||
// without requiring a full server restart.
|
||||
// Produces an HTML string with given manifest and boilerplateSource.
|
||||
// Optionally takes urlMapper in case urls from manifest need to be prefixed
|
||||
// or rewritten.
|
||||
// Optionally takes pathMapper for resolving relative file system paths.
|
||||
// Optionally allows to override fields of the data context.
|
||||
Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
|
||||
function (manifest, boilerplateSource, options) {
|
||||
var self = this;
|
||||
// map to the identity by default
|
||||
var urlMapper = options.urlMapper || _.identity;
|
||||
var pathMapper = options.pathMapper || _.identity;
|
||||
|
||||
var boilerplateBaseData = {
|
||||
css: [],
|
||||
js: [],
|
||||
head: '',
|
||||
body: '',
|
||||
meteorManifest: JSON.stringify(manifest)
|
||||
};
|
||||
|
||||
// allow the caller to extend the default base data
|
||||
_.extend(boilerplateBaseData, options.baseDataExtension);
|
||||
|
||||
_.each(manifest, function (item) {
|
||||
var urlPath = urlMapper(item.url);
|
||||
var itemObj = { url: urlPath };
|
||||
|
||||
if (options.inline) {
|
||||
itemObj.scriptContent = readUtf8FileSync(
|
||||
pathMapper(item.path));
|
||||
itemObj.inline = true;
|
||||
}
|
||||
|
||||
if (item.type === 'css' && item.where === 'client') {
|
||||
boilerplateBaseData.css.push(itemObj);
|
||||
}
|
||||
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') {
|
||||
boilerplateBaseData.head =
|
||||
readUtf8FileSync(pathMapper(item.path));
|
||||
}
|
||||
if (item.type === 'body') {
|
||||
boilerplateBaseData.body =
|
||||
readUtf8FileSync(pathMapper(item.path));
|
||||
}
|
||||
});
|
||||
var boilerplateRenderCode = SpacebarsCompiler.compile(
|
||||
boilerplateSource, { isBody: true });
|
||||
|
||||
// Note that we are actually depending on eval's local environment capture
|
||||
// so that UI and HTML are visible to the eval'd code.
|
||||
// XXX the template we are evaluating relies on the fact that UI is globally
|
||||
// available.
|
||||
global.UI = UI;
|
||||
self.func = eval(boilerplateRenderCode);
|
||||
self.baseData = boilerplateBaseData;
|
||||
};
|
||||
|
||||
var _getTemplate = _.memoize(function (arch) {
|
||||
var filename = 'boilerplate_' + arch + '.html';
|
||||
return Assets.getText(filename);
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
<html {{htmlAttributes}}>
|
||||
<head>
|
||||
{{#each css}} <link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{../bundledJsCssUrlRewriteHook url}}">{{/each}}
|
||||
{{{head}}}
|
||||
{{{dynamicHead}}}
|
||||
</head>
|
||||
<body>
|
||||
{{{body}}}
|
||||
{{{dynamicBody}}}
|
||||
{{#if inlineScriptsAllowed}}
|
||||
<script type='text/javascript'>__meteor_runtime_config__ = JSON.parse(decodeURIComponent({{meteorRuntimeConfig}}));</script>
|
||||
{{else}}
|
||||
<script type='text/javascript' src='{{rootUrlPathPrefix}}/meteor_runtime_config.js'></script>
|
||||
{{/if}}
|
||||
{{#each js}} <script type="text/javascript" src="{{../bundledJsCssUrlRewriteHook url}}"></script>
|
||||
{{/each}}
|
||||
{{#each additionalStaticJs}}
|
||||
{{#if ../inlineScriptsAllowed}}
|
||||
<script type='text/javascript'>
|
||||
{{contents}}
|
||||
</script>
|
||||
{{else}}
|
||||
<script type='text/javascript'
|
||||
src='{{rootUrlPathPrefix}}{{pathname}}'></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,46 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height">
|
||||
<meta name="msapplication-tap-highlight" content="no">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * gap: data: blob: 'unsafe-inline' 'unsafe-eval' ws: wss:;">
|
||||
|
||||
{{! We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense }}
|
||||
|
||||
{{#each css}} <link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{url}}">{{/each}}
|
||||
|
||||
<script type='text/javascript'>
|
||||
__meteor_runtime_config__ = JSON.parse(decodeURIComponent({{meteorRuntimeConfig}}));
|
||||
|
||||
if (/Android/i.test(navigator.userAgent)) {
|
||||
// When Android app is emulated, it cannot connect to localhost,
|
||||
// instead it should connect to 10.0.2.2
|
||||
// (unless we're using an http proxy; then it works!)
|
||||
if (!__meteor_runtime_config__.httpProxyPort) {
|
||||
__meteor_runtime_config__.ROOT_URL = (__meteor_runtime_config__.ROOT_URL || '').replace(/localhost/i, '10.0.2.2');
|
||||
__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL = (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL || '').replace(/localhost/i, '10.0.2.2');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="/cordova.js"></script>
|
||||
{{#each js}} <script type="text/javascript" src="{{url}}"></script>
|
||||
{{/each}}
|
||||
{{#each additionalStaticJs}}
|
||||
{{#if ../inlineScriptsAllowed}}
|
||||
<script type='text/javascript'>
|
||||
{{contents}}
|
||||
</script>
|
||||
{{else}}
|
||||
<script type='text/javascript'
|
||||
src='{{rootUrlPathPrefix}}{{pathname}}'></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{{head}}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{{body}}}
|
||||
</body>
|
||||
</html>
|
||||
102
packages/boilerplate-generator/generator.js
Normal file
102
packages/boilerplate-generator/generator.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { readFile } from 'fs';
|
||||
|
||||
import WebBrowserTemplate from './template-web.browser';
|
||||
import WebCordovaTemplate from './template-web.cordova';
|
||||
|
||||
// Copied from webapp_server
|
||||
const readUtf8FileSync = filename => Meteor.wrapAsync(readFile)(filename, 'utf8');
|
||||
|
||||
export class Boilerplate {
|
||||
constructor(arch, manifest, options = {}) {
|
||||
this.template = _getTemplate(arch);
|
||||
this.baseData = null;
|
||||
|
||||
this._generateBoilerplateFromManifest(
|
||||
manifest,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
// The 'extraData' argument can be used to extend 'self.baseData'. Its
|
||||
// purpose is to allow you to specify data that you might not know at
|
||||
// the time that you construct the Boilerplate object. (e.g. it is used
|
||||
// by 'webapp' to specify data that is only known at request-time).
|
||||
toHTML(extraData) {
|
||||
if (!this.baseData || !this.template) {
|
||||
throw new Error('Boilerplate did not instantiate correctly.');
|
||||
}
|
||||
|
||||
return "<!DOCTYPE html>\n" +
|
||||
this.template({ ...this.baseData, ...extraData });
|
||||
}
|
||||
|
||||
// XXX Exported to allow client-side only changes to rebuild the boilerplate
|
||||
// without requiring a full server restart.
|
||||
// Produces an HTML string with given manifest and boilerplateSource.
|
||||
// Optionally takes urlMapper in case urls from manifest need to be prefixed
|
||||
// or rewritten.
|
||||
// Optionally takes pathMapper for resolving relative file system paths.
|
||||
// Optionally allows to override fields of the data context.
|
||||
_generateBoilerplateFromManifest(manifest, {
|
||||
urlMapper = _.identity,
|
||||
pathMapper = _.identity,
|
||||
baseDataExtension,
|
||||
inline,
|
||||
} = {}) {
|
||||
|
||||
const boilerplateBaseData = {
|
||||
css: [],
|
||||
js: [],
|
||||
head: '',
|
||||
body: '',
|
||||
meteorManifest: JSON.stringify(manifest),
|
||||
...baseDataExtension,
|
||||
};
|
||||
|
||||
_.each(manifest, item => {
|
||||
const urlPath = urlMapper(item.url);
|
||||
const itemObj = { url: urlPath };
|
||||
|
||||
if (inline) {
|
||||
itemObj.scriptContent = readUtf8FileSync(
|
||||
pathMapper(item.path));
|
||||
itemObj.inline = true;
|
||||
}
|
||||
|
||||
if (item.type === 'css' && item.where === 'client') {
|
||||
boilerplateBaseData.css.push(itemObj);
|
||||
}
|
||||
|
||||
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') {
|
||||
boilerplateBaseData.head =
|
||||
readUtf8FileSync(pathMapper(item.path));
|
||||
}
|
||||
|
||||
if (item.type === 'body') {
|
||||
boilerplateBaseData.body =
|
||||
readUtf8FileSync(pathMapper(item.path));
|
||||
}
|
||||
});
|
||||
|
||||
this.baseData = boilerplateBaseData;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a template function that, when called, produces the boilerplate
|
||||
// html as a string.
|
||||
const _getTemplate = arch => {
|
||||
if (arch === 'web.browser') {
|
||||
return WebBrowserTemplate;
|
||||
} else if (arch === 'web.cordova') {
|
||||
return WebCordovaTemplate;
|
||||
} else {
|
||||
throw new Error('Unsupported arch: ' + arch);
|
||||
}
|
||||
};
|
||||
@@ -1,24 +1,13 @@
|
||||
Package.describe({
|
||||
summary: "Generates the boilerplate html from program's manifest",
|
||||
version: '1.1.1'
|
||||
version: '1.1.2'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
Package.onUse(api => {
|
||||
api.use('ecmascript');
|
||||
api.use([
|
||||
'underscore',
|
||||
'spacebars-compiler',
|
||||
'spacebars',
|
||||
'htmljs',
|
||||
'ui',
|
||||
], 'server');
|
||||
api.addFiles(['boilerplate-generator.js'], 'server');
|
||||
api.export(['Boilerplate'], 'server');
|
||||
// These are spacebars templates, but we process them manually with the
|
||||
// spacebars compiler rather than letting the 'templating' package (which
|
||||
// isn't fully supported on the server yet) handle it. That also means that
|
||||
// they don't contain the outer "<template>" tag.
|
||||
api.addAssets([
|
||||
'boilerplate_web.browser.html',
|
||||
'boilerplate_web.cordova.html'
|
||||
], 'server');
|
||||
api.mainModule('generator.js', 'server');
|
||||
api.export('Boilerplate', 'server');
|
||||
});
|
||||
|
||||
76
packages/boilerplate-generator/template-web.browser.js
Normal file
76
packages/boilerplate-generator/template-web.browser.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// Template function for rendering the boilerplate html for browsers
|
||||
|
||||
export default function({
|
||||
meteorRuntimeConfig,
|
||||
rootUrlPathPrefix,
|
||||
inlineScriptsAllowed,
|
||||
css,
|
||||
js,
|
||||
additionalStaticJs,
|
||||
htmlAttributes,
|
||||
bundledJsCssUrlRewriteHook,
|
||||
head,
|
||||
body,
|
||||
dynamicHead,
|
||||
dynamicBody,
|
||||
}) {
|
||||
return [].concat(
|
||||
[
|
||||
'<html' +_.map(htmlAttributes, (value, key) =>
|
||||
_.template(' <%= attrName %>="<%- attrValue %>"')({
|
||||
attrName: key,
|
||||
attrValue: value
|
||||
})
|
||||
).join('') + '>',
|
||||
'<head>'
|
||||
],
|
||||
|
||||
_.map(css, ({url}) =>
|
||||
_.template(' <link rel="stylesheet" type="text/css" class="__meteor-css__" href="<%- href %>">')({
|
||||
href: bundledJsCssUrlRewriteHook(url)
|
||||
})
|
||||
),
|
||||
|
||||
[
|
||||
head,
|
||||
dynamicHead,
|
||||
'</head>',
|
||||
'<body>',
|
||||
body,
|
||||
dynamicBody,
|
||||
'',
|
||||
(inlineScriptsAllowed
|
||||
? _.template(' <script type="text/javascript">__meteor_runtime_config__ = JSON.parse(decodeURIComponent(<%= conf %>))</script>')({
|
||||
conf: meteorRuntimeConfig
|
||||
})
|
||||
: _.template(' <script type="text/javascript" src="<%- src %>/meteor_runtime_config.js"></script>')({
|
||||
src: rootUrlPathPrefix
|
||||
})
|
||||
) ,
|
||||
''
|
||||
],
|
||||
|
||||
_.map(js, ({url}) =>
|
||||
_.template(' <script type="text/javascript" src="<%- src %>"></script>')({
|
||||
src: bundledJsCssUrlRewriteHook(url)
|
||||
})
|
||||
),
|
||||
|
||||
_.map(additionalStaticJs, ({contents, pathname}) => (
|
||||
(inlineScriptsAllowed
|
||||
? _.template(' <script><%= contents %></script>')({
|
||||
contents: contents
|
||||
})
|
||||
: _.template(' <script type="text/javascript" src="<%- src %>"></script>')({
|
||||
src: rootUrlPathPrefix + pathname
|
||||
}))
|
||||
)),
|
||||
|
||||
[
|
||||
'', '',
|
||||
'</body>',
|
||||
'</html>'
|
||||
],
|
||||
).join('\n');
|
||||
}
|
||||
|
||||
79
packages/boilerplate-generator/template-web.cordova.js
Normal file
79
packages/boilerplate-generator/template-web.cordova.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// Template function for rendering the boilerplate html for cordova
|
||||
|
||||
export default function({
|
||||
meteorRuntimeConfig,
|
||||
rootUrlPathPrefix,
|
||||
inlineScriptsAllowed,
|
||||
css,
|
||||
js,
|
||||
additionalStaticJs,
|
||||
htmlAttributes,
|
||||
bundledJsCssUrlRewriteHook,
|
||||
head,
|
||||
body,
|
||||
dynamicHead,
|
||||
dynamicBody,
|
||||
}) {
|
||||
return [].concat(
|
||||
[
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <meta charset="utf-8">',
|
||||
' <meta name="format-detection" content="telephone=no">',
|
||||
' <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height">',
|
||||
' <meta name="msapplication-tap-highlight" content="no">',
|
||||
' <meta http-equiv="Content-Security-Policy" content="default-src * gap: data: blob: \'unsafe-inline\' \'unsafe-eval\' ws: wss:;">',
|
||||
],
|
||||
// We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense
|
||||
_.map(css, ({url}) =>
|
||||
_.template(' <link rel="stylesheet" type="text/css" class="__meteor-css__" href="<%- href %>">')({
|
||||
href: url
|
||||
})
|
||||
),
|
||||
[
|
||||
' <script type="text/javascript">',
|
||||
_.template(' __meteor_runtime_config__ = JSON.parse(decodeURIComponent(<%= conf %>));')({
|
||||
conf: meteorRuntimeConfig
|
||||
}),
|
||||
' if (/Android/i.test(navigator.userAgent)) {',
|
||||
// When Android app is emulated, it cannot connect to localhost,
|
||||
// instead it should connect to 10.0.2.2
|
||||
// (unless we\'re using an http proxy; then it works!)
|
||||
' if (!__meteor_runtime_config__.httpProxyPort) {',
|
||||
' __meteor_runtime_config__.ROOT_URL = (__meteor_runtime_config__.ROOT_URL || \'\').replace(/localhost/i, \'10.0.2.2\');',
|
||||
' __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL = (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL || \'\').replace(/localhost/i, \'10.0.2.2\');',
|
||||
' }',
|
||||
' }',
|
||||
' </script>',
|
||||
'',
|
||||
' <script type="text/javascript" src="/cordova.js"></script>'
|
||||
],
|
||||
_.map(js, ({url}) =>
|
||||
_.template(' <script type="text/javascript" src="<%- src %>"></script>')({
|
||||
src: url
|
||||
})
|
||||
),
|
||||
|
||||
_.map(additionalStaticJs, ({contents, pathname}) => (
|
||||
(inlineScriptsAllowed
|
||||
? _.template(' <script><%= contents %></script>')({
|
||||
contents: contents
|
||||
})
|
||||
: _.template(' <script type="text/javascript" src="<%- src %>"></script>')({
|
||||
src: rootUrlPathPrefix + pathname
|
||||
}))
|
||||
)),
|
||||
|
||||
[
|
||||
'',
|
||||
head,
|
||||
'</head>',
|
||||
'',
|
||||
'<body>',
|
||||
body,
|
||||
'</body>',
|
||||
'</html>'
|
||||
],
|
||||
).join('\n');
|
||||
}
|
||||
|
||||
@@ -582,8 +582,15 @@ _.extend(Connection.prototype, {
|
||||
// an onReady callback inside an autorun; the semantics we provide is
|
||||
// that at the time the sub first becomes ready, we call the last
|
||||
// onReady callback provided, if any.)
|
||||
if (!existing.ready)
|
||||
// If the sub is already ready, run the ready callback right away.
|
||||
// It seems that users would expect an onReady callback inside an
|
||||
// autorun to trigger once the the sub first becomes ready and also
|
||||
// when re-subs happens.
|
||||
if (existing.ready) {
|
||||
callbacks.onReady();
|
||||
} else {
|
||||
existing.readyCallback = callbacks.onReady;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX COMPAT WITH 1.0.3.1 we used to have onError but now we call
|
||||
|
||||
@@ -291,11 +291,11 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
|
||||
// Change the foo subscription and flush. We should sub to the new foo
|
||||
// subscription, re-sub to the stopper subscription, and then unsub from the old
|
||||
// foo subscription. The bar subscription should be unaffected. The completer
|
||||
// subscription should *NOT* call its new onReady callback, because we only
|
||||
// call at most one onReady for a given reactively-saved subscription.
|
||||
// foo subscription. The bar subscription should be unaffected. The completer
|
||||
// subscription should call its new onReady callback, because we always
|
||||
// call onReady for a given reactively-saved subscription.
|
||||
// The completerHandle should have been reestablished to the ready handle.
|
||||
rFoo.set("foo2");
|
||||
rFoo.set("foo2");
|
||||
Tracker.flush();
|
||||
test.length(stream.sent, 3);
|
||||
|
||||
@@ -312,17 +312,17 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
test.equal(message, {msg: 'unsub', id: idFoo1});
|
||||
|
||||
test.equal(onReadyCount, {completer: 1});
|
||||
test.equal(onReadyCount, {completer: 2});
|
||||
test.isTrue(completerReady);
|
||||
|
||||
// Ready the stopper and bar subs. Completing stopper should call only the
|
||||
// onReady from the new subscription because they were separate subscriptions
|
||||
// started at different times and the first one was explicitly torn down by
|
||||
// the client; completing bar should call only the onReady from the new
|
||||
// subscription because we only call at most one onReady per reactively-saved
|
||||
// the client; completing bar should call the onReady from the new
|
||||
// subscription because we always call onReady for a given reactively-saved
|
||||
// subscription.
|
||||
stream.receive({msg: 'ready', 'subs': [idStopperAgain, idBar1]});
|
||||
test.equal(onReadyCount, {completer: 1, bar1: 1, stopper: 1});
|
||||
test.equal(onReadyCount, {completer: 2, bar1: 1, stopper: 1});
|
||||
|
||||
// Shut down the autorun. This should unsub us from all current subs at flush
|
||||
// time.
|
||||
@@ -1310,15 +1310,24 @@ Tinytest.add("livedata stub - reactive resub", function (test) {
|
||||
});
|
||||
|
||||
markAllReady();
|
||||
var message = JSON.parse(stream.sent.shift());
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'sub', name: 'foo-sub', params: ['A']});
|
||||
test.equal(fooReady, 1);
|
||||
|
||||
// Rerun the inner autorun with different subscription
|
||||
// arguments. Detect the re-sub via onReady.
|
||||
// arguments.
|
||||
fooArg.set('B');
|
||||
test.isTrue(inner.invalidated);
|
||||
Tracker.flush();
|
||||
test.isFalse(inner.invalidated);
|
||||
markAllReady();
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'sub', name: 'foo-sub', params: ['B']});
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'unsub'});
|
||||
test.equal(fooReady, 2);
|
||||
|
||||
// Rerun inner again with same args; should be no re-sub.
|
||||
@@ -1327,7 +1336,8 @@ Tinytest.add("livedata stub - reactive resub", function (test) {
|
||||
Tracker.flush();
|
||||
test.isFalse(inner.invalidated);
|
||||
markAllReady();
|
||||
test.equal(fooReady, 2);
|
||||
test.isUndefined(stream.sent.shift()); test.isUndefined(stream.sent.shift());
|
||||
test.equal(fooReady, 3);
|
||||
|
||||
// Rerun outer! Should still be no re-sub even though
|
||||
// the inner computation is stopped and a new one is
|
||||
@@ -1337,13 +1347,20 @@ Tinytest.add("livedata stub - reactive resub", function (test) {
|
||||
Tracker.flush();
|
||||
test.isFalse(inner.invalidated);
|
||||
markAllReady();
|
||||
test.equal(fooReady, 2);
|
||||
test.isUndefined(stream.sent.shift());
|
||||
test.equal(fooReady, 4);
|
||||
|
||||
// Change the subscription. Now we should get an onReady.
|
||||
fooArg.set('C');
|
||||
Tracker.flush();
|
||||
markAllReady();
|
||||
test.equal(fooReady, 3);
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'sub', name: 'foo-sub', params: ['C']});
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'unsub'});
|
||||
test.equal(fooReady, 5);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Meteor's latency-compensated distributed data client",
|
||||
version: '1.3.4',
|
||||
version: '2.0.0',
|
||||
documentation: null
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Code shared beween ddp-client and ddp-server",
|
||||
version: '1.2.8',
|
||||
version: '1.2.9',
|
||||
documentation: null
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Meteor's latency-compensated distributed data server",
|
||||
version: '1.3.14',
|
||||
version: '2.0.0',
|
||||
documentation: null
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Meteor's latency-compensated distributed data framework",
|
||||
version: '1.2.5'
|
||||
version: '1.3.0'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
name: "ecmascript-runtime-client",
|
||||
version: "0.4.2",
|
||||
version: "0.4.3",
|
||||
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"
|
||||
|
||||
@@ -31,6 +31,7 @@ require("core-js/es6/array");
|
||||
require("core-js/es6/function");
|
||||
require("core-js/es6/math");
|
||||
require("core-js/es6/object");
|
||||
require("core-js/es6/regexp");
|
||||
require("core-js/es6/string");
|
||||
require("core-js/es6/weak-map");
|
||||
require("core-js/es6/weak-set");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
name: 'ecmascript',
|
||||
version: '0.8.0',
|
||||
version: '0.8.2',
|
||||
summary: 'Compiler plugin that supports ES2015+ in all .js files',
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ var storage;
|
||||
|
||||
try {
|
||||
storage = global.localStorage;
|
||||
|
||||
|
||||
if (storage) {
|
||||
storage.setItem(key, key);
|
||||
retrieved = storage.getItem(key);
|
||||
@@ -21,7 +21,23 @@ try {
|
||||
} catch (ignored) {}
|
||||
|
||||
if (key === retrieved) {
|
||||
Meteor._localStorage = storage;
|
||||
if (Meteor.isServer) {
|
||||
Meteor._localStorage = storage;
|
||||
} else {
|
||||
// Some browsers (e.g. IE11) don't properly handle attempts to change
|
||||
// window.localStorage methods. By using proxy methods to expose
|
||||
// window.localStorage functionality, developers can change the
|
||||
// behavior of Meteor._localStorage methods without breaking
|
||||
// window.localStorage.
|
||||
["getItem",
|
||||
"setItem",
|
||||
"removeItem",
|
||||
].forEach(function (name) {
|
||||
this[name] = function () {
|
||||
return storage[name].apply(storage, arguments);
|
||||
};
|
||||
}, Meteor._localStorage = {});
|
||||
}
|
||||
}
|
||||
|
||||
if (! Meteor._localStorage) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Simulates local storage on IE 6,7 using userData",
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "The Meteor command-line tool",
|
||||
version: '1.5.0'
|
||||
version: '1.5.1'
|
||||
});
|
||||
|
||||
Package.includeTool();
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"from": "source-map@>=0.5.1 <0.6.0"
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.0.13",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.0.13.tgz",
|
||||
"from": "uglify-js@3.0.13"
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.0.18.tgz",
|
||||
"from": "uglify-js@3.0.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ var uglify;
|
||||
|
||||
meteorJsMinify = function (source) {
|
||||
var result = {};
|
||||
var NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
uglify = uglify || Npm.require("uglify-js");
|
||||
|
||||
try {
|
||||
@@ -9,7 +11,10 @@ meteorJsMinify = function (source) {
|
||||
compress: {
|
||||
drop_debugger: false,
|
||||
unused: false,
|
||||
dead_code: false
|
||||
dead_code: false,
|
||||
global_defs: {
|
||||
"process.env.NODE_ENV": NODE_ENV
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +29,14 @@ meteorJsMinify = function (source) {
|
||||
// Although Babel.minify can handle a wider variety of ECMAScript
|
||||
// 2015+ syntax, it is substantially slower than UglifyJS, so we use
|
||||
// it only as a fallback.
|
||||
result.code = Babel.minify(source).code;
|
||||
if (Babel.getMinifierOptions) {
|
||||
var options = Babel.getMinifierOptions({
|
||||
inlineNodeEnv: NODE_ENV
|
||||
});
|
||||
result.code = Babel.minify(source, options).code;
|
||||
} else {
|
||||
result.code = Babel.minify(source).code;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Package.describe({
|
||||
summary: "JavaScript minifier",
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
"uglify-js": "3.0.13"
|
||||
"uglify-js": "3.0.18"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
@@ -650,6 +650,24 @@ function getValueBitmask(value, length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Actually inserts a key value into the selector document
|
||||
// However, this checks there is no ambiguity in setting
|
||||
// the value for the given key, throws otherwise
|
||||
function insertIntoDocument(document, key, value) {
|
||||
Object.keys(document).forEach(existingKey => {
|
||||
if (
|
||||
(existingKey.length > key.length && existingKey.indexOf(key) === 0) ||
|
||||
(key.length > existingKey.length && key.indexOf(existingKey) === 0)
|
||||
) {
|
||||
throw new Error(`cannot infer query fields to set, both paths '${existingKey}' and '${key}' are matched`);
|
||||
} else if (existingKey === key) {
|
||||
throw new Error(`cannot infer query fields to set, path '${key}' is matched twice`);
|
||||
}
|
||||
});
|
||||
|
||||
document[key] = value;
|
||||
}
|
||||
|
||||
// Returns a branched matcher that matches iff the given matcher does not.
|
||||
// Note that this implicitly "deMorganizes" the wrapped function. ie, it
|
||||
// means that ALL branch values need to fail to match innerBranchedMatcher.
|
||||
@@ -979,6 +997,87 @@ function pointToArray(point) {
|
||||
return Array.isArray(point) ? point.slice() : [point.x, point.y];
|
||||
}
|
||||
|
||||
// Creating a document from an upsert is quite tricky.
|
||||
// E.g. this selector: {"$or": [{"b.foo": {"$all": ["bar"]}}]}, should result in: {"b.foo": "bar"}
|
||||
// But this selector: {"$or": [{"b": {"foo": {"$all": ["bar"]}}}]} should throw an error
|
||||
|
||||
// Some rules (found mainly with trial & error, so there might be more):
|
||||
// - handle all childs of $and (or implicit $and)
|
||||
// - handle $or nodes with exactly 1 child
|
||||
// - ignore $or nodes with more than 1 child
|
||||
// - ignore $nor and $not nodes
|
||||
// - throw when a value can not be set unambiguously
|
||||
// - every value for $all should be dealt with as separate $eq-s
|
||||
// - threat all children of $all as $eq setters (=> set if $all.length === 1, otherwise throw error)
|
||||
// - you can not mix '$'-prefixed keys and non-'$'-prefixed keys
|
||||
// - you can only have dotted keys on a root-level
|
||||
// - you can not have '$'-prefixed keys more than one-level deep in an object
|
||||
|
||||
// Handles one key/value pair to put in the selector document
|
||||
function populateDocumentWithKeyValue(document, key, value) {
|
||||
if (value && Object.getPrototypeOf(value) === Object.prototype) {
|
||||
populateDocumentWithObject(document, key, value);
|
||||
} else if (!(value instanceof RegExp)) {
|
||||
insertIntoDocument(document, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a key, value pair to put in the selector document
|
||||
// if the value is an object
|
||||
function populateDocumentWithObject(document, key, value) {
|
||||
const keys = Object.keys(value);
|
||||
const unprefixedKeys = keys.filter(op => op[0] !== '$');
|
||||
|
||||
if (unprefixedKeys.length > 0 || !keys.length) {
|
||||
// Literal (possibly empty) object ( or empty object )
|
||||
// Don't allow mixing '$'-prefixed with non-'$'-prefixed fields
|
||||
if (keys.length !== unprefixedKeys.length)
|
||||
throw new Error(`unknown operator: ${unprefixedKeys[0]}`);
|
||||
|
||||
validateObject(value, key);
|
||||
insertIntoDocument(document, key, value);
|
||||
} else {
|
||||
Object.keys(value).forEach(op => {
|
||||
const object = value[op];
|
||||
|
||||
if (op === '$eq') {
|
||||
populateDocumentWithKeyValue(document, key, object);
|
||||
} else if (op === '$all') {
|
||||
// every value for $all should be dealt with as separate $eq-s
|
||||
object.forEach(element => populateDocumentWithKeyValue(document, key, element));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fills a document with certain fields from an upsert selector
|
||||
export function populateDocumentWithQueryFields(query, document = {}) {
|
||||
if (Object.getPrototypeOf(query) === Object.prototype) {
|
||||
// handle implicit $and
|
||||
Object.keys(query).forEach(key => {
|
||||
const value = query[key];
|
||||
|
||||
if (key === '$and') {
|
||||
// handle explicit $and
|
||||
value.forEach(element => populateDocumentWithQueryFields(element, document));
|
||||
} else if (key === '$or') {
|
||||
// handle $or nodes with exactly 1 child
|
||||
if (value.length === 1)
|
||||
populateDocumentWithQueryFields(value[0], document);
|
||||
} else if (key[0] !== '$') {
|
||||
// Ignore other '$'-prefixed logical selectors
|
||||
populateDocumentWithKeyValue(document, key, value);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Handle meteor-specific shortcut for selecting _id
|
||||
if (LocalCollection._selectorIsId(query))
|
||||
insertIntoDocument(document, '_id', query);
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
// Traverses the keys of passed projection and constructs a tree where all
|
||||
// leaves are either all True or all False
|
||||
// @returns Object:
|
||||
@@ -1062,3 +1161,23 @@ export function regexpElementMatcher(regexp) {
|
||||
return regexp.test(value);
|
||||
};
|
||||
}
|
||||
|
||||
// Validates the key in a path.
|
||||
// Objects that are nested more then 1 level cannot have dotted fields
|
||||
// or fields starting with '$'
|
||||
function validateKeyInPath(key, path) {
|
||||
if (key.includes('.'))
|
||||
throw new Error(`The dotted field '${key}' in '${path}.${key} is not valid for storage.`);
|
||||
if (key[0] === '$')
|
||||
throw new Error(`The dollar ($) prefixed field '${path}.${key} is not valid for storage.`);
|
||||
}
|
||||
|
||||
// Recursively validates an object that is nested more than one level deep
|
||||
function validateObject(object, path) {
|
||||
if (object && Object.getPrototypeOf(object) === Object.prototype) {
|
||||
Object.keys(object).forEach(key => {
|
||||
validateKeyInPath(key, path);
|
||||
validateObject(object[key], path + '.' + key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isIndexable,
|
||||
isNumericKey,
|
||||
isOperatorObject,
|
||||
populateDocumentWithQueryFields,
|
||||
projectionDetails,
|
||||
} from './common.js';
|
||||
|
||||
@@ -405,19 +406,7 @@ export default class LocalCollection {
|
||||
// generate an id for it.
|
||||
let insertedId;
|
||||
if (updateCount === 0 && options.upsert) {
|
||||
const selectorModifier = LocalCollection._removeDollarOperators(LocalCollection._selectorIsId(selector) ? {_id: selector} : selector);
|
||||
const doc = {};
|
||||
|
||||
if (selectorModifier._id) {
|
||||
doc._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(doc, {$set: selectorModifier});
|
||||
LocalCollection._modify(doc, mod, {isInsert: true});
|
||||
|
||||
const doc = LocalCollection._createUpsertDocument(selector, mod);
|
||||
if (! doc._id && options.insertedId)
|
||||
doc._id = options.insertedId;
|
||||
|
||||
@@ -828,6 +817,37 @@ LocalCollection._compileProjection = fields => {
|
||||
};
|
||||
};
|
||||
|
||||
// Calculates the document to insert in case we're doing an upsert and the selector
|
||||
// does not match any elements
|
||||
LocalCollection._createUpsertDocument = (selector, modifier) => {
|
||||
const selectorDocument = populateDocumentWithQueryFields(selector);
|
||||
const isModify = LocalCollection._isModificationMod(modifier);
|
||||
|
||||
const newDoc = {};
|
||||
|
||||
if (selectorDocument._id) {
|
||||
newDoc._id = selectorDocument._id;
|
||||
delete selectorDocument._id;
|
||||
}
|
||||
|
||||
// This double _modify call is made to help with nested properties (see issue #8631).
|
||||
// We do this even if it's a replacement for validation purposes (e.g. ambiguous id's)
|
||||
LocalCollection._modify(newDoc, {$set: selectorDocument});
|
||||
LocalCollection._modify(newDoc, modifier, {isInsert: true});
|
||||
|
||||
if (isModify) {
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
// Replacement can take _id from query document
|
||||
const replacement = Object.assign({}, modifier);
|
||||
if (newDoc._id) {
|
||||
replacement._id = newDoc._id;
|
||||
}
|
||||
|
||||
return replacement;
|
||||
};
|
||||
|
||||
LocalCollection._diffObjects = (left, right, callbacks) => {
|
||||
return DiffSequence.diffObjects(left, right, callbacks);
|
||||
};
|
||||
@@ -945,6 +965,25 @@ LocalCollection._insertInSortedList = (cmp, array, value) => {
|
||||
return i;
|
||||
};
|
||||
|
||||
LocalCollection._isModificationMod = mod => {
|
||||
let isModify = false;
|
||||
let isReplace = false;
|
||||
|
||||
for (const key in mod) {
|
||||
if (key.substr(0, 1) === '$') {
|
||||
isModify = true;
|
||||
} else {
|
||||
isReplace = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModify && isReplace) {
|
||||
throw new Error('Update parameter cannot have both modifier and non-modifier fields.');
|
||||
}
|
||||
|
||||
return isModify;
|
||||
};
|
||||
|
||||
// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about
|
||||
// RegExp
|
||||
// XXX note that _type(undefined) === 3!!!!
|
||||
@@ -990,9 +1029,6 @@ LocalCollection._modify = (doc, modifier, options = {}) => {
|
||||
if (keypath === '')
|
||||
throw MinimongoError('An empty update path is not valid.');
|
||||
|
||||
if (keypath === '_id' && operator !== '$setOnInsert')
|
||||
throw MinimongoError('Mod on _id not allowed');
|
||||
|
||||
const keyparts = keypath.split('.');
|
||||
|
||||
if (!keyparts.every(Boolean))
|
||||
@@ -1009,9 +1045,12 @@ LocalCollection._modify = (doc, modifier, options = {}) => {
|
||||
modFunc(target, keyparts.pop(), arg, keypath, newDoc);
|
||||
});
|
||||
});
|
||||
|
||||
if (doc._id && !EJSON.equals(doc._id, newDoc._id))
|
||||
throw MinimongoError(`After applying the update to the document {_id: "${doc._id}", ...}, the (immutable) field '_id' was found to have been altered to _id: "${newDoc._id}"`);
|
||||
} else {
|
||||
if (modifier._id && !EJSON.equals(doc._id, modifier._id))
|
||||
throw MinimongoError('Cannot change the _id of a document');
|
||||
throw MinimongoError(`The _id field cannot be changed from {_id: "${doc._id}"} to {_id: "${modifier._id}"}`);
|
||||
|
||||
// replace the whole document
|
||||
assertHasValidFieldNames(modifier);
|
||||
@@ -1151,31 +1190,6 @@ LocalCollection._observeChangesCallbacksAreOrdered = callbacks => {
|
||||
return !!(callbacks.addedBefore || callbacks.movedBefore);
|
||||
};
|
||||
|
||||
// When performing an upsert, the incoming selector object can be re-used as
|
||||
// the upsert modifier object, as long as Mongo query and projection
|
||||
// operators (prefixed with a $ character) are removed from the newly
|
||||
// created modifier object. This function attempts to strip all $ based Mongo
|
||||
// operators when creating the upsert modifier object.
|
||||
// NOTE: There is a known issue here in that some Mongo $ based opeartors
|
||||
// should not actually be stripped.
|
||||
// See https://github.com/meteor/meteor/issues/8806.
|
||||
LocalCollection._removeDollarOperators = selector => {
|
||||
const cleansed = {};
|
||||
|
||||
Object.keys(selector).forEach((key) => {
|
||||
const value = selector[key];
|
||||
|
||||
if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) {
|
||||
if (value !== null && value.constructor && Object.getPrototypeOf(value) === Object.prototype)
|
||||
cleansed[key] = LocalCollection._removeDollarOperators(value);
|
||||
else
|
||||
cleansed[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return cleansed;
|
||||
};
|
||||
|
||||
LocalCollection._removeFromResults = (query, doc) => {
|
||||
if (query.ordered) {
|
||||
const i = LocalCollection._findInOrderedResults(query, doc);
|
||||
@@ -1688,8 +1702,3 @@ function findModTarget(doc, keyparts, options = {}) {
|
||||
|
||||
// notreached
|
||||
}
|
||||
|
||||
function objectOnlyHasDollarKeys(object) {
|
||||
const keys = Object.keys(object);
|
||||
return keys.length > 0 && keys.every(key => key.charAt(0) === '$');
|
||||
}
|
||||
|
||||
@@ -2325,7 +2325,10 @@ Tinytest.add('minimongo - modify', test => {
|
||||
// The query is relevant for 'a.$.b'.
|
||||
coll.update(query, mod);
|
||||
const actual = coll.findOne();
|
||||
delete actual._id; // added by insert
|
||||
|
||||
if (!expected._id) {
|
||||
delete actual._id; // added by insert
|
||||
}
|
||||
|
||||
if (typeof expected === 'function') {
|
||||
expected(actual, EJSON.stringify({input: doc, mod}));
|
||||
@@ -2363,6 +2366,21 @@ Tinytest.add('minimongo - modify', test => {
|
||||
test.equal(actual, expected);
|
||||
};
|
||||
|
||||
const upsertUpdate = (initialDoc, query, mod, expected) => {
|
||||
const collection = new LocalCollection;
|
||||
|
||||
collection.insert(initialDoc);
|
||||
|
||||
const result = collection.upsert(query, mod);
|
||||
const actual = collection.findOne();
|
||||
|
||||
if (!expected._id) {
|
||||
delete actual._id;
|
||||
}
|
||||
|
||||
test.equal(actual, expected);
|
||||
};
|
||||
|
||||
const upsertException = (query, mod) => {
|
||||
const coll = new LocalCollection;
|
||||
test.throws(() => {
|
||||
@@ -2592,8 +2610,11 @@ Tinytest.add('minimongo - modify', test => {
|
||||
modify({a: [1], b: 2}, {$set: {'a.2': 9}}, {a: [1, null, 9], b: 2});
|
||||
modify({a: {b: 1}}, {$set: {'a.c': 9}}, {a: {b: 1, c: 9}});
|
||||
modify({}, {$set: {'x._id': 4}}, {x: {_id: 4}});
|
||||
|
||||
// Changing _id is disallowed
|
||||
exception({}, {$set: {_id: 4}});
|
||||
exception({_id: 4}, {$set: {_id: 4}}); // even not-changing _id is bad
|
||||
exception({_id: 1}, {$set: {_id: 4}});
|
||||
modify({_id: 4}, {$set: {_id: 4}}, {_id: 4}); // not-changing _id is not bad
|
||||
// restricted field names
|
||||
exception({a: {}}, {$set: {a: {$a: 1}}});
|
||||
exception({ a: {} }, { $set: { a: { c:
|
||||
@@ -2895,6 +2916,69 @@ Tinytest.add('minimongo - modify', test => {
|
||||
{ foo: {}, bar: 'baz' }
|
||||
);
|
||||
|
||||
// Tests for https://github.com/meteor/meteor/issues/8806
|
||||
upsert({"a": {"b": undefined, "c": null}}, {"$set": {"c": "foo"}}, {"a": {"b": undefined, "c": null}, "c": "foo"})
|
||||
upsert({"a": {"$eq": "bar" }}, {"$set": {"c": "foo"}}, {"a": "bar", "c": "foo"})
|
||||
// $all with 1 statement is similar to $eq
|
||||
upsert({"a": {"$all": ["bar"] }}, {"$set": {"c": "foo"}}, {"a": "bar", "c": "foo"})
|
||||
upsert({"a": {"$eq": "bar" }, "b": "baz"}, {"$set": {"c": "foo"}}, {"a": "bar", "b": "baz", "c": "foo"})
|
||||
upsert({"a": {"$exists": true}}, {"$set": {"c": "foo"}}, {"c": "foo"})
|
||||
upsert({"a": {"$exists": true, "$eq": "foo"}}, {"$set": {"c": "foo"}}, {"a": "foo", "c": "foo"})
|
||||
upsert({"a": {"$gt": 3, "$eq": 2}}, {"$set": {"c": "foo"}}, {"a": 2, "c": "foo"})
|
||||
// $and
|
||||
upsert({"$and": [{"a": {"$eq": "bar"}}]}, {"$set": {"c": "foo"}}, {"a": "bar", "c": "foo"})
|
||||
upsert({"$and": [{"a": {"$all": ["bar"]}}]}, {"$set": {"c": "foo"}}, {"a": "bar", "c": "foo"})
|
||||
upsert({"$and": [{"a": {"$all": ["bar"]}}]}, {"$set": {"c": "foo"}}, {"a": "bar", "c": "foo"})
|
||||
// $or with one statement is handled similar to $and
|
||||
upsert({"$or": [{"a": "bar"}]}, {"$set": {"c": "foo"}}, {"a": "bar", "c": "foo"})
|
||||
// $or with multiple statements is ignored
|
||||
upsert({"$or": [{"a": "bar"}, {"b": "baz"}]}, {"$set": {"c": "foo"}}, {"c": "foo"})
|
||||
// Negative logical operators are ignored
|
||||
upsert({"$nor": [{"a": "bar"}]}, {"$set": {"c": "foo"}}, {"c": "foo"})
|
||||
// Filter out empty objects after filtering out operators
|
||||
upsert({"a": {"$exists": true}}, {"$set": {"c": "foo"}}, {"c": "foo"})
|
||||
// But leave actual empty objects
|
||||
upsert({"a": {}}, {"$set": {"c": "foo"}}, {"a": {}, "c": "foo"})
|
||||
// Also filter out shorthand regexp notation
|
||||
upsert({"a": /a/}, {"$set": {"c": "foo"}}, {"c": "foo"})
|
||||
// Test nested fields
|
||||
upsert({"$and": [{"a.a": "foo"}, {"$or": [{"a.b": "baz"}]}]}, {"$set": {"c": "foo"}}, {"a": {"a": "foo", "b": "baz"}, "c": "foo"})
|
||||
// Test for https://github.com/meteor/meteor/issues/5294
|
||||
upsert({"a": {"$ne": 444}}, {"$push": {"a": 123}}, {"a": [123]})
|
||||
// Mod takes precedence over query
|
||||
upsert({"a": "foo"}, {"a": "bar"}, {"a": "bar"})
|
||||
upsert({"a": "foo"}, {"$set":{"a": "bar"}}, {"a": "bar"})
|
||||
// Replacement can take _id from query
|
||||
upsert({"_id": "foo", "foo": "bar"}, {"bar": "foo"}, {"_id": "foo", "bar": "foo"})
|
||||
// Replacement update keeps _id
|
||||
upsertUpdate({"_id": "foo", "bar": "baz"}, {"_id":"foo"}, {"bar": "crow"}, {"_id": "foo", "bar": "crow"});
|
||||
|
||||
// Nested fields don't work with literal objects
|
||||
upsertException({"a": {}, "a.b": "foo"}, {});
|
||||
// You can't have an ambiguious ID
|
||||
upsertException({"_id":"foo"}, {"_id":"bar"});
|
||||
upsertException({"_id":"foo"}, {"$set":{"_id":"bar"}});
|
||||
// You can't set the same field twice
|
||||
upsertException({"$and": [{"a": "foo"}, {"a": "foo"}]}, {}); //not even with same value
|
||||
upsertException({"a": {"$all": ["foo", "bar"]}}, {});
|
||||
upsertException({"$and": [{"a": {"$eq": "foo"}}, {"$or": [{"a": {"$all": ["bar"]}}]}]}, {});
|
||||
// You can't have nested dotted fields
|
||||
upsertException({"a": {"foo.bar": "baz"}}, {});
|
||||
// You can't have dollar-prefixed fields above the first level (logical operators not counted)
|
||||
upsertException({"a": {"a": {"$eq": "foo"}}}, {});
|
||||
upsertException({"a": {"a": {"$exists": true}}}, {});
|
||||
// You can't mix operators with other fields
|
||||
upsertException({"a": {"$eq": "bar", "b": "foo"}}, {})
|
||||
upsertException({"a": {"b": "foo", "$eq": "bar"}}, {})
|
||||
|
||||
const mongoIdForUpsert = new MongoID.ObjectID('44915733af80844fa1cef07a');
|
||||
upsert({_id: mongoIdForUpsert}, {$setOnInsert: {a: 123}}, {a: 123})
|
||||
|
||||
// Test for https://github.com/meteor/meteor/issues/7758
|
||||
upsert({n_id: mongoIdForUpsert, c_n: "bar"},
|
||||
{$set: { t_t_o: "foo"}},
|
||||
{n_id: mongoIdForUpsert, t_t_o: "foo", c_n: "bar"});
|
||||
|
||||
exception({}, {$set: {_id: 'bad'}});
|
||||
|
||||
// $bit
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"from": "minizlib@>=1.0.3 <2.0.0"
|
||||
},
|
||||
"reify": {
|
||||
"version": "0.11.22",
|
||||
"resolved": "https://registry.npmjs.org/reify/-/reify-0.11.22.tgz",
|
||||
"from": "reify@0.11.22"
|
||||
"version": "0.11.24",
|
||||
"resolved": "https://registry.npmjs.org/reify/-/reify-0.11.24.tgz",
|
||||
"from": "reify@0.11.24"
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.3.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Package.describe({
|
||||
name: "modules",
|
||||
version: "0.9.1",
|
||||
version: "0.9.2",
|
||||
summary: "CommonJS module system",
|
||||
documentation: "README.md"
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
reify: "0.11.22"
|
||||
reify: "0.11.24"
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
|
||||
@@ -562,6 +562,7 @@ Mongo.Collection.prototype.update = function update(selector, modifier, ...optio
|
||||
insertedId = options.insertedId;
|
||||
} else if (!selector || !selector._id) {
|
||||
insertedId = this._makeNewID();
|
||||
options.generatedId = true;
|
||||
options.insertedId = insertedId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,10 +511,9 @@ MongoConnection.prototype._update = function (collection_name, selector, mod,
|
||||
var mongoSelector = replaceTypes(selector, replaceMeteorAtomWithMongo);
|
||||
var mongoMod = replaceTypes(mod, replaceMeteorAtomWithMongo);
|
||||
|
||||
var isModify = isModificationMod(mongoMod);
|
||||
var knownId = selector._id || mod._id;
|
||||
var isModify = LocalCollection._isModificationMod(mongoMod);
|
||||
|
||||
if (options._forbidReplace && ! isModify) {
|
||||
if (options._forbidReplace && !isModify) {
|
||||
var err = new Error("Invalid modifier. Replacements are forbidden.");
|
||||
if (callback) {
|
||||
return callback(err);
|
||||
@@ -523,22 +522,45 @@ MongoConnection.prototype._update = function (collection_name, selector, mod,
|
||||
}
|
||||
}
|
||||
|
||||
if (options.upsert && (! knownId) && options.insertedId) {
|
||||
// XXX If we know we're using Mongo 2.6 (and this isn't a replacement)
|
||||
// we should be able to just use $setOnInsert instead of this
|
||||
// simulated upsert thing. (We can't use $setOnInsert with
|
||||
// replacements because there's nowhere to write it, and $setOnInsert
|
||||
// can't set _id on Mongo 2.4.)
|
||||
//
|
||||
// Also, in the future we could do a real upsert for the mongo id
|
||||
// generation case, if the the node mongo driver gives us back the id
|
||||
// of the upserted doc (which our current version does not).
|
||||
//
|
||||
// For more context, see
|
||||
// https://github.com/meteor/meteor/issues/2278#issuecomment-64252706
|
||||
// We've already run replaceTypes/replaceMeteorAtomWithMongo on
|
||||
// selector and mod. We assume it doesn't matter, as far as
|
||||
// the behavior of modifiers is concerned, whether `_modify`
|
||||
// is run on EJSON or on mongo-converted EJSON.
|
||||
|
||||
// Run this code up front so that it fails fast if someone uses
|
||||
// a Mongo update operator we don't support.
|
||||
let knownId;
|
||||
if (options.upsert) {
|
||||
try {
|
||||
let newDoc = LocalCollection._createUpsertDocument(selector, mod);
|
||||
knownId = newDoc._id;
|
||||
} catch (err) {
|
||||
if (callback) {
|
||||
return callback(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.upsert &&
|
||||
! isModify &&
|
||||
! knownId &&
|
||||
options.insertedId &&
|
||||
! (options.insertedId instanceof Mongo.ObjectID &&
|
||||
options.generatedId)) {
|
||||
// In case of an upsert with a replacement, where there is no _id defined
|
||||
// in either the query or the replacement doc, mongo will generate an id itself.
|
||||
// Therefore we need this special strategy if we want to control the id ourselves.
|
||||
|
||||
// We don't need to do this when:
|
||||
// - This is not a replacement, so we can add an _id to $setOnInsert
|
||||
// - The id is defined by query or mod we can just add it to the replacement doc
|
||||
// - The user did not specify any id preference and the id is a Mongo ObjectId,
|
||||
// then we can just let Mongo generate the id
|
||||
|
||||
simulateUpsertWithInsertedId(
|
||||
collection, mongoSelector, mongoMod,
|
||||
isModify, options,
|
||||
collection, mongoSelector, mongoMod, options,
|
||||
// This callback does not need to be bindEnvironment'ed because
|
||||
// simulateUpsertWithInsertedId() wraps it and then passes it through
|
||||
// bindEnvironmentForWrite.
|
||||
@@ -554,6 +576,15 @@ MongoConnection.prototype._update = function (collection_name, selector, mod,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
||||
if (options.upsert && !knownId && options.insertedId && isModify) {
|
||||
if (!mongoMod.hasOwnProperty('$setOnInsert')) {
|
||||
mongoMod.$setOnInsert = {};
|
||||
}
|
||||
knownId = options.insertedId;
|
||||
Object.assign(mongoMod.$setOnInsert, replaceTypes({_id: options.insertedId}, replaceMeteorAtomWithMongo));
|
||||
}
|
||||
|
||||
collection.update(
|
||||
mongoSelector, mongoMod, mongoOpts,
|
||||
bindEnvironmentForWrite(function (err, result) {
|
||||
@@ -563,10 +594,14 @@ MongoConnection.prototype._update = function (collection_name, selector, mod,
|
||||
// If this was an upsert() call, and we ended up
|
||||
// inserting a new doc and we know its id, then
|
||||
// return that id as well.
|
||||
|
||||
if (options.upsert && meteorResult.insertedId && knownId) {
|
||||
meteorResult.insertedId = knownId;
|
||||
if (options.upsert && meteorResult.insertedId) {
|
||||
if (knownId) {
|
||||
meteorResult.insertedId = knownId;
|
||||
} else if (meteorResult.insertedId instanceof MongoDB.ObjectID) {
|
||||
meteorResult.insertedId = new Mongo.ObjectID(meteorResult.insertedId.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, meteorResult);
|
||||
} else {
|
||||
callback(err, meteorResult.numberAffected);
|
||||
@@ -582,23 +617,6 @@ MongoConnection.prototype._update = function (collection_name, selector, mod,
|
||||
}
|
||||
};
|
||||
|
||||
var isModificationMod = function (mod) {
|
||||
var isReplace = false;
|
||||
var isModify = false;
|
||||
for (var k in mod) {
|
||||
if (k.substr(0, 1) === '$') {
|
||||
isModify = true;
|
||||
} else {
|
||||
isReplace = true;
|
||||
}
|
||||
}
|
||||
if (isModify && isReplace) {
|
||||
throw new Error(
|
||||
"Update parameter cannot have both modifier and non-modifier fields.");
|
||||
}
|
||||
return isModify;
|
||||
};
|
||||
|
||||
var transformResult = function (driverResult) {
|
||||
var meteorResult = { numberAffected: 0 };
|
||||
if (driverResult) {
|
||||
@@ -626,26 +644,18 @@ var NUM_OPTIMISTIC_TRIES = 3;
|
||||
|
||||
// exposed for testing
|
||||
MongoConnection._isCannotChangeIdError = function (err) {
|
||||
// First check for what this error looked like in Mongo 2.4. Either of these
|
||||
// checks should work, but just to be safe...
|
||||
if (err.code === 13596) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mongo 3.2.* returns error as next Object:
|
||||
// {name: String, code: Number, err: String}
|
||||
// Older Mongo returns:
|
||||
// {name: String, code: Number, errmsg: String}
|
||||
// Older Mongo returns:
|
||||
// {name: String, code: Number, err: String}
|
||||
var error = err.errmsg || err.err;
|
||||
|
||||
if (error.indexOf('cannot change _id of a document') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Now look for what it looks like in Mongo 2.6. We don't use the error code
|
||||
// here, because the error code we observed it producing (16837) appears to be
|
||||
// We don't use the error code here
|
||||
// because the error code we observed it producing (16837) appears to be
|
||||
// a far more generic error code based on examining the source.
|
||||
if (error.indexOf('The _id field cannot be changed') === 0) {
|
||||
if (error.indexOf('The _id field cannot be changed') === 0
|
||||
|| error.indexOf("the (immutable) field '_id' was found to have been altered to _id") !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -653,66 +663,20 @@ MongoConnection._isCannotChangeIdError = function (err) {
|
||||
};
|
||||
|
||||
var simulateUpsertWithInsertedId = function (collection, selector, mod,
|
||||
isModify, options, callback) {
|
||||
// STRATEGY: First try doing a plain update. If it affected 0 documents,
|
||||
// then without affecting the database, we know we should probably do an
|
||||
// insert. We then do a *conditional* insert that will fail in the case
|
||||
// of a race condition. This conditional insert is actually an
|
||||
// upsert-replace with an _id, which will never successfully update an
|
||||
// existing document. If this upsert fails with an error saying it
|
||||
// couldn't change an existing _id, then we know an intervening write has
|
||||
// caused the query to match something. We go back to step one and repeat.
|
||||
options, callback) {
|
||||
// STRATEGY: First try doing an upsert with a generated ID.
|
||||
// If this throws an error about changing the ID on an existing document
|
||||
// then without affecting the database, we know we should probably try
|
||||
// an update without the generated ID. If it affected 0 documents,
|
||||
// then without affecting the database, we the document that first
|
||||
// gave the error is probably removed and we need to try an insert again
|
||||
// We go back to step one and repeat.
|
||||
// Like all "optimistic write" schemes, we rely on the fact that it's
|
||||
// unlikely our writes will continue to be interfered with under normal
|
||||
// circumstances (though sufficiently heavy contention with writers
|
||||
// disagreeing on the existence of an object will cause writes to fail
|
||||
// in theory).
|
||||
|
||||
var newDoc;
|
||||
// Run this code up front so that it fails fast if someone uses
|
||||
// a Mongo update operator we don't support.
|
||||
if (isModify) {
|
||||
// We've already run replaceTypes/replaceMeteorAtomWithMongo on
|
||||
// selector and mod. We assume it doesn't matter, as far as
|
||||
// the behavior of modifiers is concerned, whether `_modify`
|
||||
// is run on EJSON or on mongo-converted EJSON.
|
||||
var selectorDoc = LocalCollection._removeDollarOperators(selector);
|
||||
|
||||
newDoc = selectorDoc;
|
||||
|
||||
// Convert dotted keys into objects. (Resolves issue #4522).
|
||||
_.each(newDoc, function (value, key) {
|
||||
var trail = key.split(".");
|
||||
|
||||
if (trail.length > 1) {
|
||||
//Key is dotted. Convert it into an object.
|
||||
delete newDoc[key];
|
||||
|
||||
var obj = newDoc,
|
||||
leaf = trail.pop();
|
||||
|
||||
// XXX It is not quite certain what should be done if there are clashing
|
||||
// keys on the trail of the dotted key. For now we will just override it
|
||||
// It wouldn't be a very sane query in the first place, but should look
|
||||
// up what mongo does in this case.
|
||||
|
||||
while ((key = trail.shift())) {
|
||||
if (typeof obj[key] !== "object") {
|
||||
obj[key] = {};
|
||||
}
|
||||
|
||||
obj = obj[key];
|
||||
}
|
||||
|
||||
obj[leaf] = value;
|
||||
}
|
||||
});
|
||||
|
||||
LocalCollection._modify(newDoc, mod, {isInsert: true});
|
||||
} else {
|
||||
newDoc = mod;
|
||||
}
|
||||
|
||||
var insertedId = options.insertedId; // must exist
|
||||
var mongoOptsForUpdate = {
|
||||
safe: true,
|
||||
@@ -723,6 +687,10 @@ var simulateUpsertWithInsertedId = function (collection, selector, mod,
|
||||
upsert: true
|
||||
};
|
||||
|
||||
var replacementWithId = Object.assign(
|
||||
replaceTypes({_id: insertedId}, replaceMeteorAtomWithMongo),
|
||||
mod);
|
||||
|
||||
var tries = NUM_OPTIMISTIC_TRIES;
|
||||
|
||||
var doUpdate = function () {
|
||||
@@ -746,9 +714,6 @@ var simulateUpsertWithInsertedId = function (collection, selector, mod,
|
||||
};
|
||||
|
||||
var doConditionalInsert = function () {
|
||||
var replacementWithId = _.extend(
|
||||
replaceTypes({_id: insertedId}, replaceMeteorAtomWithMongo),
|
||||
newDoc);
|
||||
collection.update(selector, replacementWithId, mongoOptsForInsert,
|
||||
bindEnvironmentForWrite(function (err, result) {
|
||||
if (err) {
|
||||
|
||||
@@ -1646,10 +1646,10 @@ if (Meteor.isServer) {
|
||||
var run = test.runId();
|
||||
var coll = new Mongo.Collection("livedata_upsert_errorparse_collection_"+run, collectionOptions);
|
||||
|
||||
coll.insert({_id: 'foobar'});
|
||||
coll.insert({_id:'foobar', foo: 'bar'});
|
||||
var err;
|
||||
try {
|
||||
coll.update({_id: 'foobar'}, {_id: 'cowbar'});
|
||||
coll.update({foo: 'bar'}, {_id: 'cowbar'});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Adaptor for using MongoDB and Minimongo over DDP",
|
||||
version: '1.1.18'
|
||||
version: '1.1.19'
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
|
||||
1
packages/server-render/.npm/package/.gitignore
vendored
Normal file
1
packages/server-render/.npm/package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
7
packages/server-render/.npm/package/README
Normal file
7
packages/server-render/.npm/package/README
Normal file
@@ -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.
|
||||
19
packages/server-render/.npm/package/npm-shrinkwrap.json
generated
Normal file
19
packages/server-render/.npm/package/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"magic-string": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.21.3.tgz",
|
||||
"from": "magic-string@0.21.3"
|
||||
},
|
||||
"parse5": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.2.tgz",
|
||||
"from": "parse5@3.0.2"
|
||||
},
|
||||
"vlq": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.2.tgz",
|
||||
"from": "vlq@>=0.2.1 <0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
109
packages/server-render/README.md
Normal file
109
packages/server-render/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# server-render
|
||||
[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/server-render) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/server-render)
|
||||
***
|
||||
|
||||
This package implements generic support for server-side rendering in
|
||||
Meteor apps, by providing a mechanism for injecting fragments of HTML into
|
||||
the `<head>` and/or `<body>` of the application's initial HTML response.
|
||||
|
||||
### Usage
|
||||
|
||||
This package exports a function named `onPageLoad` which takes a callback
|
||||
function that will be called at page load (on the client) or whenever a
|
||||
new request happens (on the server).
|
||||
|
||||
The callback receives a `sink` object, which is an instance of either
|
||||
`ClientSink` or `ServerSink` depending on the environment. Both types of
|
||||
`sink` have the same methods, though the server version accepts only HTML
|
||||
strings as content, whereas the client version also accepts DOM nodes.
|
||||
|
||||
The current interface of `{Client,Server}Sink` objects is as follows:
|
||||
|
||||
```js
|
||||
class Sink {
|
||||
// Appends content to the <head>.
|
||||
appendToHead(content)
|
||||
|
||||
// Appends content to the <body>.
|
||||
appendToBody(content)
|
||||
|
||||
// Appends content to the identified element.
|
||||
appendToElementById(id, content)
|
||||
|
||||
// Replaces the content of the identified element.
|
||||
renderIntoElementById(id, content)
|
||||
}
|
||||
```
|
||||
|
||||
The `sink` object may also expose additional properties depending on the
|
||||
environment. For example, on the server, `sink.request` provides access to
|
||||
the current `request` object, and `sink.arch` identifies the target
|
||||
architecture of the pending HTTP response (e.g. "web.browser").
|
||||
|
||||
Here is a basic example of `onPageLoad` usage on the server:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { onPageLoad } from "meteor/server-render";
|
||||
import App from "/imports/Server.js";
|
||||
|
||||
onPageLoad(sink => {
|
||||
sink.renderIntoElementById("app", renderToString(
|
||||
<App location={sink.request.url} />
|
||||
));
|
||||
});
|
||||
```
|
||||
|
||||
Likewise on the client:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { onPageLoad } from "meteor/server-render";
|
||||
|
||||
onPageLoad(async sink => {
|
||||
const App = (await import("/imports/Client.js")).default;
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById("app")
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
Note that the `onPageLoad` callback function is allowed to return a
|
||||
`Promise` if it needs to do any asynchronous work, and thus may be
|
||||
implemented by an `async` function (as in the client case above).
|
||||
|
||||
Note also that the client example does not end up calling any methods of
|
||||
the `sink` object, because `ReactDOM.render` has its own similar API. In
|
||||
fact, you are not even required to use the `onPageLoad` API on the client,
|
||||
if you have your own ideas about how the client should do its rendering.
|
||||
|
||||
Here is a more complicated example of `onPageLoad` usage on the server,
|
||||
involving the [`styled-components`](https://www.styled-components.com/docs/advanced#server-side-rendering) npm package:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import { onPageLoad } from "meteor/server-render";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { ServerStyleSheet } from "styled-components"
|
||||
import App from "/imports/Server";
|
||||
|
||||
onPageLoad(sink => {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const html = renderToString(sheet.collectStyles(
|
||||
<App location={sink.request.url} />
|
||||
));
|
||||
|
||||
sink.renderIntoElementById("app", html);
|
||||
sink.appendToHead(sheet.getStyleTags());
|
||||
});
|
||||
```
|
||||
|
||||
In this example, the callback not only renders the `<App />` element into
|
||||
the element with `id="app"`, but also appends any `<style>` tag(s)
|
||||
generated during rendering to the `<head>` of the response document.
|
||||
|
||||
Although these examples have all involved React, the `onPageLoad` API is
|
||||
designed to be generically useful for any kind of server-side rendering.
|
||||
43
packages/server-render/client-sink.js
Normal file
43
packages/server-render/client-sink.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const doc = document;
|
||||
const head = doc.getElementsByTagName("head")[0];
|
||||
const body = doc.body;
|
||||
|
||||
export class ClientSink {
|
||||
appendToHead(nodeOrHtml) {
|
||||
appendContent(head, nodeOrHtml);
|
||||
}
|
||||
|
||||
appendToBody(nodeOrHtml) {
|
||||
appendContent(body, nodeOrHtml);
|
||||
}
|
||||
|
||||
appendToElementById(id, nodeOrHtml) {
|
||||
appendContent(doc.getElementById(id), nodeOrHtml);
|
||||
}
|
||||
|
||||
renderIntoElementById(id, nodeOrHtml) {
|
||||
const element = doc.getElementById(id);
|
||||
while (element.lastChild) {
|
||||
element.removeChild(element.lastChild);
|
||||
}
|
||||
appendContent(element, nodeOrHtml);
|
||||
}
|
||||
}
|
||||
|
||||
function appendContent(destination, nodeOrHtml) {
|
||||
if (typeof nodeOrHtml === "string") {
|
||||
// Make a shallow clone of the destination node to ensure the new
|
||||
// children can legally be appended to it.
|
||||
const container = destination.cloneNode(false);
|
||||
// Parse the HTML into the container, allowing for multiple children.
|
||||
container.innerHTML = nodeOrHtml;
|
||||
// Transplant the children to the destination.
|
||||
while (container.firstChild) {
|
||||
destination.appendChild(container.firstChild);
|
||||
}
|
||||
} else if (Array.isArray(nodeOrHtml)) {
|
||||
nodeOrHtml.forEach(elem => appendContent(destination, elem));
|
||||
} else {
|
||||
destination.appendChild(nodeOrHtml);
|
||||
}
|
||||
}
|
||||
9
packages/server-render/client.js
Normal file
9
packages/server-render/client.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import { ClientSink } from "./client-sink.js";
|
||||
|
||||
let promise = new Promise(Meteor.startup);
|
||||
let sink = new ClientSink();
|
||||
|
||||
export function onPageLoad(callback) {
|
||||
promise = promise.then(() => callback(sink));
|
||||
}
|
||||
25
packages/server-render/package.js
Normal file
25
packages/server-render/package.js
Normal file
@@ -0,0 +1,25 @@
|
||||
Package.describe({
|
||||
name: "server-render",
|
||||
version: "0.1.0",
|
||||
summary: "Generic support for server-side rendering in Meteor apps",
|
||||
documentation: "README.md"
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
"magic-string": "0.21.3",
|
||||
"parse5": "3.0.2"
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use("ecmascript");
|
||||
api.use("webapp@1.3.17");
|
||||
api.mainModule("client.js", "client");
|
||||
api.mainModule("server.js", "server");
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use("ecmascript");
|
||||
api.use("tinytest");
|
||||
api.use("server-render");
|
||||
api.mainModule("server-render-tests.js", "server");
|
||||
});
|
||||
70
packages/server-render/server-register.js
Normal file
70
packages/server-render/server-register.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { WebAppInternals } from "meteor/webapp";
|
||||
import { SAXParser } from "parse5";
|
||||
import MagicString from "magic-string";
|
||||
import { ServerSink } from "./server-sink.js";
|
||||
import { onPageLoad } from "./server.js";
|
||||
|
||||
WebAppInternals.registerBoilerplateDataCallback(
|
||||
"meteor/server-render",
|
||||
(request, data, arch) => {
|
||||
const sink = new ServerSink(request, arch);
|
||||
|
||||
return onPageLoad.chain(
|
||||
callback => callback(sink, request)
|
||||
).then(() => {
|
||||
if (! sink.maybeMadeChanges) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let reallyMadeChanges = false;
|
||||
|
||||
function rewrite(property) {
|
||||
const html = data[property];
|
||||
if (typeof html !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const magic = new MagicString(html);
|
||||
const parser = new SAXParser({
|
||||
locationInfo: true
|
||||
});
|
||||
|
||||
parser.on("startTag", (name, attrs, selfClosing, loc) => {
|
||||
attrs.some(attr => {
|
||||
if (attr.name === "id") {
|
||||
const html = sink.htmlById[attr.value];
|
||||
if (html) {
|
||||
magic.appendRight(loc.endOffset, html);
|
||||
reallyMadeChanges = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
parser.write(html);
|
||||
|
||||
data[property] = magic.toString();
|
||||
}
|
||||
|
||||
if (sink.head) {
|
||||
data.dynamicHead = (data.dynamicHead || "") + sink.head;
|
||||
reallyMadeChanges = true;
|
||||
}
|
||||
|
||||
if (Object.keys(sink.htmlById).length > 0) {
|
||||
// We don't currently allow injecting HTML into the <head> except
|
||||
// by calling sink.appendHead(html).
|
||||
rewrite("body");
|
||||
rewrite("dynamicBody");
|
||||
}
|
||||
|
||||
if (sink.body) {
|
||||
data.dynamicBody = (data.dynamicBody || "") + sink.body;
|
||||
reallyMadeChanges = true;
|
||||
}
|
||||
|
||||
return reallyMadeChanges;
|
||||
});
|
||||
}
|
||||
);
|
||||
104
packages/server-render/server-render-tests.js
Normal file
104
packages/server-render/server-render-tests.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Tinytest } from "meteor/tinytest";
|
||||
import { WebAppInternals } from "meteor/webapp";
|
||||
import { onPageLoad } from "meteor/server-render";
|
||||
import { parse } from "parse5";
|
||||
|
||||
const skeleton = `
|
||||
<h1>Look, Ma... static HTML!</h1>
|
||||
<div id="container-2"></div>
|
||||
<p>
|
||||
<div id="container-1">
|
||||
</div>
|
||||
</p>`;
|
||||
|
||||
Tinytest.add('server-render - boilerplate', function (test) {
|
||||
// This test is not a very good demonstration of the server-render
|
||||
// abstraction. In normal usage, you would call renderIntoElementById
|
||||
// and not think about the rest of this stuff. The extra complexity owes
|
||||
// to the trickiness of testing this package without using a real
|
||||
// browser to parse the resulting HTTP response.
|
||||
|
||||
const realCallback =
|
||||
// Use the underlying abstraction to set the static HTML skeleton.
|
||||
WebAppInternals.registerBoilerplateDataCallback(
|
||||
"meteor/server-render",
|
||||
(request, data, arch) => {
|
||||
if (request.isServerRenderTest) {
|
||||
test.equal(arch, "web.browser");
|
||||
test.equal(request.url, "/server-render/test");
|
||||
data.body = skeleton;
|
||||
}
|
||||
return realCallback.call(this, request, data, arch);
|
||||
}
|
||||
);
|
||||
|
||||
const callback1 = onPageLoad(sink => {
|
||||
sink.renderIntoElementById("container-1", "<oyez/>");
|
||||
});
|
||||
|
||||
// This callback is async, and that's fine because
|
||||
// WebAppInternals.getBoilerplate is able to yield. Internally the
|
||||
// webapp package uses a function called getBoilerplateAsync, so the
|
||||
// Fiber power-tools need not be involved in typical requests.
|
||||
const callback2 = onPageLoad(async sink => {
|
||||
sink.renderIntoElementById(
|
||||
"container-2",
|
||||
(await "oy") + (await "ez")
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
const boilerplate = WebAppInternals.getBoilerplate({
|
||||
isServerRenderTest: true,
|
||||
browser: { name: "fake" },
|
||||
url: "/server-render/test"
|
||||
}, "web.browser");
|
||||
|
||||
const ids = [];
|
||||
const seen = new Set;
|
||||
|
||||
function walk(node) {
|
||||
if (node && ! seen.has(node)) {
|
||||
seen.add(node);
|
||||
|
||||
if (node.nodeName === "div" && node.attrs) {
|
||||
node.attrs.some(attr => {
|
||||
if (attr.name === "id") {
|
||||
const id = attr.value;
|
||||
|
||||
if (id === "container-1") {
|
||||
test.equal(node.childNodes[0].nodeName, "oyez");
|
||||
ids.push(id);
|
||||
} else if (id === "container-2") {
|
||||
const child = node.childNodes[0];
|
||||
test.equal(child.nodeName, "#text");
|
||||
test.equal(child.value.trim(), "oyez");
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node.childNodes) {
|
||||
node.childNodes.forEach(walk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(parse(boilerplate));
|
||||
|
||||
test.equal(ids, ["container-2", "container-1"]);
|
||||
|
||||
} finally {
|
||||
// Cleanup to minimize interference with other tests:
|
||||
WebAppInternals.registerBoilerplateDataCallback(
|
||||
"meteor/server-render",
|
||||
realCallback
|
||||
);
|
||||
|
||||
onPageLoad.remove(callback1);
|
||||
onPageLoad.remove(callback2);
|
||||
}
|
||||
});
|
||||
50
packages/server-render/server-sink.js
Normal file
50
packages/server-render/server-sink.js
Normal file
@@ -0,0 +1,50 @@
|
||||
export class ServerSink {
|
||||
constructor(request, arch) {
|
||||
this.request = request;
|
||||
this.arch = arch;
|
||||
this.head = "";
|
||||
this.body = "";
|
||||
this.htmlById = Object.create(null);
|
||||
this.maybeMadeChanges = false;
|
||||
}
|
||||
|
||||
appendToHead(html) {
|
||||
if (appendContent(this, "head", html)) {
|
||||
this.maybeMadeChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
appendToBody(html) {
|
||||
if (appendContent(this, "body", html)) {
|
||||
this.maybeMadeChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
appendToElementById(id, html) {
|
||||
if (appendContent(this.htmlById, id, html)) {
|
||||
this.maybeMadeChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
renderIntoElementById(id, html) {
|
||||
this.htmlById[id] = "";
|
||||
this.appendToElementById(id, html);
|
||||
}
|
||||
}
|
||||
|
||||
function appendContent(object, property, content) {
|
||||
let madeChanges = false;
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
content.forEach(elem => {
|
||||
if (appendContent(object, property, elem)) {
|
||||
madeChanges = true;
|
||||
}
|
||||
});
|
||||
} else if ((content = content && content.toString("utf8"))) {
|
||||
object[property] = (object[property] || "") + content;
|
||||
madeChanges = true;
|
||||
}
|
||||
|
||||
return madeChanges;
|
||||
}
|
||||
32
packages/server-render/server.js
Normal file
32
packages/server-render/server.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import "./server-register.js";
|
||||
|
||||
const startupPromise = new Promise(Meteor.startup);
|
||||
const pageLoadCallbacks = new Set;
|
||||
|
||||
export function onPageLoad(callback) {
|
||||
if (typeof callback === "function") {
|
||||
pageLoadCallbacks.add(callback);
|
||||
}
|
||||
|
||||
// Return the callback so that it can be more easily removed later.
|
||||
return callback;
|
||||
}
|
||||
|
||||
onPageLoad.remove = function (callback) {
|
||||
pageLoadCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
onPageLoad.clear = function () {
|
||||
pageLoadCallbacks.clear();
|
||||
};
|
||||
|
||||
onPageLoad.chain = function (handler) {
|
||||
return startupPromise.then(() => {
|
||||
let promise = Promise.resolve();
|
||||
pageLoadCallbacks.forEach(callback => {
|
||||
promise = promise.then(() => handler(callback));
|
||||
});
|
||||
return promise;
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
name: "shell-server",
|
||||
version: "0.2.3",
|
||||
version: "0.2.4",
|
||||
summary: "Server-side component of the `meteor shell` command.",
|
||||
documentation: "README.md"
|
||||
});
|
||||
|
||||
@@ -418,17 +418,14 @@ function evalCommand(command, context, filename, callback) {
|
||||
}
|
||||
|
||||
evalCommandPromise.then(function () {
|
||||
|
||||
if (repl.input) {
|
||||
callback(null, script.runInThisContext());
|
||||
} else {
|
||||
// If repl didn't start, `require` and `module` are not visible
|
||||
// in the vm context
|
||||
// in the vm context.
|
||||
setRequireAndModule(global);
|
||||
|
||||
callback(null, script.runInThisContext());
|
||||
}
|
||||
|
||||
}).catch(callback);
|
||||
}
|
||||
|
||||
@@ -474,7 +471,6 @@ function isRecoverableError(e, repl) {
|
||||
}
|
||||
|
||||
function setRequireAndModule(context) {
|
||||
|
||||
if (Package.modules) {
|
||||
// Use the same `require` function and `module` object visible to the
|
||||
// application.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
name: 'standard-minifier-js',
|
||||
version: '2.1.0',
|
||||
version: '2.1.1',
|
||||
summary: 'Standard javascript minifiers used with Meteor apps by default.',
|
||||
documentation: 'README.md',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Serves a Meteor app over HTTP",
|
||||
version: '1.3.16'
|
||||
version: '1.3.17'
|
||||
});
|
||||
|
||||
Npm.depends({connect: "2.30.2",
|
||||
@@ -30,15 +30,19 @@ Package.onUse(function (api) {
|
||||
// (browser-policy depends on webapp). So we don't explicitly depend in any
|
||||
// way on browser-policy here, but we use it when it is loaded, and it can be
|
||||
// loaded after webapp.
|
||||
api.export(['WebApp', 'main', 'WebAppInternals'], 'server');
|
||||
api.export(['WebApp'], 'client');
|
||||
api.addFiles('webapp_server.js', 'server');
|
||||
api.addFiles('webapp_client.js', 'client');
|
||||
api.addFiles('webapp_cordova.js', 'web.cordova');
|
||||
api.mainModule('webapp_server.js', 'server');
|
||||
api.export('WebApp', 'server');
|
||||
api.export('WebAppInternals', 'server');
|
||||
api.export('main', 'server');
|
||||
|
||||
api.mainModule('webapp_client.js', 'client');
|
||||
api.export('WebApp', 'client');
|
||||
|
||||
api.mainModule('webapp_cordova.js', 'web.cordova');
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(['tinytest', 'webapp', 'http', 'underscore']);
|
||||
api.use(['tinytest', 'ecmascript', 'webapp', 'http', 'underscore']);
|
||||
api.addFiles('webapp_tests.js', 'server');
|
||||
api.addFiles('webapp_client_tests.js', 'client');
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
WebApp = {
|
||||
|
||||
_isCssLoaded: function () {
|
||||
if (document.styleSheets.length === 0)
|
||||
export const WebApp = {
|
||||
_isCssLoaded() {
|
||||
if (document.styleSheets.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.find(document.styleSheets, function (sheet) {
|
||||
if (sheet.cssText && !sheet.cssRules) // IE8
|
||||
return !sheet.cssText.match(/meteor-css-not-found-error/);
|
||||
return !_.find(sheet.cssRules, function (rule) {
|
||||
return rule.selectorText === '.meteor-css-not-found-error';
|
||||
});
|
||||
return _.find(document.styleSheets, sheet => {
|
||||
if (sheet.cssText && ! sheet.cssRules) { // IE8
|
||||
return ! sheet.cssText.match(/meteor-css-not-found-error/);
|
||||
}
|
||||
|
||||
return ! _.find(
|
||||
sheet.cssRules,
|
||||
rule => rule.selectorText === '.meteor-css-not-found-error'
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
////////// Requires //////////
|
||||
|
||||
var fs = Npm.require("fs");
|
||||
var http = Npm.require("http");
|
||||
var os = Npm.require("os");
|
||||
var path = Npm.require("path");
|
||||
var url = Npm.require("url");
|
||||
var crypto = Npm.require("crypto");
|
||||
|
||||
var connect = Npm.require('connect');
|
||||
var parseurl = Npm.require('parseurl');
|
||||
var useragent = Npm.require('useragent');
|
||||
var send = Npm.require('send');
|
||||
|
||||
var Future = Npm.require('fibers/future');
|
||||
var Fiber = Npm.require('fibers');
|
||||
import assert from "assert";
|
||||
import { readFile } from "fs";
|
||||
import { createServer } from "http";
|
||||
import {
|
||||
join as pathJoin,
|
||||
dirname as pathDirname,
|
||||
} from "path";
|
||||
import { parse as parseUrl } from "url";
|
||||
import { createHash } from "crypto";
|
||||
import connect from "connect";
|
||||
import parseRequest from "parseurl";
|
||||
import { lookup as lookupUserAgent } from "useragent";
|
||||
import send from "send";
|
||||
|
||||
var SHORT_SOCKET_TIMEOUT = 5*1000;
|
||||
var LONG_SOCKET_TIMEOUT = 120*1000;
|
||||
|
||||
WebApp = {};
|
||||
WebAppInternals = {};
|
||||
export const WebApp = {};
|
||||
export const WebAppInternals = {};
|
||||
|
||||
WebAppInternals.NpmModules = {
|
||||
connect: {
|
||||
@@ -43,13 +40,13 @@ var bundledJsCssUrlRewriteHook = function (url) {
|
||||
};
|
||||
|
||||
var sha1 = function (contents) {
|
||||
var hash = crypto.createHash('sha1');
|
||||
var hash = createHash('sha1');
|
||||
hash.update(contents);
|
||||
return hash.digest('hex');
|
||||
};
|
||||
|
||||
var readUtf8FileSync = function (filename) {
|
||||
return Meteor.wrapAsync(fs.readFile)(filename, 'utf8');
|
||||
return Meteor.wrapAsync(readFile)(filename, 'utf8');
|
||||
};
|
||||
|
||||
// #BrowserIdentification
|
||||
@@ -98,7 +95,7 @@ var camelCase = function (name) {
|
||||
};
|
||||
|
||||
var identifyBrowser = function (userAgentString) {
|
||||
var userAgent = useragent.lookup(userAgentString);
|
||||
var userAgent = lookupUserAgent(userAgentString);
|
||||
return {
|
||||
name: camelCase(userAgent.family),
|
||||
major: +userAgent.major,
|
||||
@@ -113,7 +110,7 @@ WebAppInternals.identifyBrowser = identifyBrowser;
|
||||
WebApp.categorizeRequest = function (req) {
|
||||
return _.extend({
|
||||
browser: identifyBrowser(req.headers['user-agent']),
|
||||
url: url.parse(req.url, true)
|
||||
url: parseUrl(req.url, true)
|
||||
}, _.pick(req, 'dynamicHead', 'dynamicBody'));
|
||||
};
|
||||
|
||||
@@ -241,6 +238,29 @@ WebApp._timeoutAdjustmentRequestCallback = function (req, res) {
|
||||
// - baseData: XXX
|
||||
var boilerplateByArch = {};
|
||||
|
||||
// Register a callback function that can selectively modify boilerplate
|
||||
// data given arguments (request, data, arch). The key should be a unique
|
||||
// identifier, to prevent accumulating duplicate callbacks from the same
|
||||
// call site over time. Callbacks will be called in the order they were
|
||||
// registered. A callback should return false if it did not make any
|
||||
// changes affecting the boilerplate. Passing null deletes the callback.
|
||||
// Any previous callback registered for this key will be returned.
|
||||
const boilerplateDataCallbacks = Object.create(null);
|
||||
WebAppInternals.registerBoilerplateDataCallback = function (key, callback) {
|
||||
const previousCallback = boilerplateDataCallbacks[key];
|
||||
|
||||
if (typeof callback === "function") {
|
||||
boilerplateDataCallbacks[key] = callback;
|
||||
} else {
|
||||
assert.strictEqual(callback, null);
|
||||
delete boilerplateDataCallbacks[key];
|
||||
}
|
||||
|
||||
// Return the previous callback in case the new callback needs to call
|
||||
// it; for example, when the new callback is a wrapper for the old.
|
||||
return previousCallback || null;
|
||||
};
|
||||
|
||||
// Given a request (as returned from `categorizeRequest`), return the
|
||||
// boilerplate HTML to serve for that request.
|
||||
//
|
||||
@@ -250,35 +270,61 @@ var boilerplateByArch = {};
|
||||
// scripts are currently allowed.
|
||||
// XXX so far this function is always called with arch === 'web.browser'
|
||||
var memoizedBoilerplate = {};
|
||||
var getBoilerplate = function (request, arch) {
|
||||
var useMemoized = ! (request.dynamicHead || request.dynamicBody);
|
||||
var htmlAttributes = getHtmlAttributes(request);
|
||||
|
||||
if (useMemoized) {
|
||||
// The only thing that changes from request to request (unless extra
|
||||
// content is added to the head or body) are the HTML attributes
|
||||
// (used by, eg, appcache) and whether inline scripts are allowed, so we
|
||||
// can memoize based on that.
|
||||
|
||||
function getBoilerplate(request, arch) {
|
||||
return getBoilerplateAsync(request, arch).await();
|
||||
}
|
||||
|
||||
function getBoilerplateAsync(request, arch) {
|
||||
const boilerplate = boilerplateByArch[arch];
|
||||
const data = Object.assign({}, boilerplate.baseData, {
|
||||
htmlAttributes: getHtmlAttributes(request),
|
||||
}, _.pick(request, "dynamicHead", "dynamicBody"));
|
||||
|
||||
let madeChanges = false;
|
||||
let promise = Promise.resolve();
|
||||
|
||||
Object.keys(boilerplateDataCallbacks).forEach(key => {
|
||||
promise = promise.then(() => {
|
||||
const callback = boilerplateDataCallbacks[key];
|
||||
return callback(request, data, arch);
|
||||
}).then(result => {
|
||||
// Callbacks should return false if they did not make any changes.
|
||||
if (result !== false) {
|
||||
madeChanges = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise.then(() => {
|
||||
const useMemoized = ! (
|
||||
data.dynamicHead ||
|
||||
data.dynamicBody ||
|
||||
madeChanges
|
||||
);
|
||||
|
||||
if (! useMemoized) {
|
||||
return boilerplate.toHTML(data);
|
||||
}
|
||||
|
||||
// The only thing that changes from request to request (unless extra
|
||||
// content is added to the head or body, or boilerplateDataCallbacks
|
||||
// modified the data) are the HTML attributes (used by, eg, appcache)
|
||||
// and whether inline scripts are allowed, so memoize based on that.
|
||||
var memHash = JSON.stringify({
|
||||
inlineScriptsAllowed: inlineScriptsAllowed,
|
||||
htmlAttributes: htmlAttributes,
|
||||
arch: arch
|
||||
inlineScriptsAllowed,
|
||||
htmlAttributes: data.htmlAttributes,
|
||||
arch,
|
||||
});
|
||||
|
||||
if (! memoizedBoilerplate[memHash]) {
|
||||
memoizedBoilerplate[memHash] = boilerplateByArch[arch].toHTML({
|
||||
htmlAttributes: htmlAttributes
|
||||
});
|
||||
memoizedBoilerplate[memHash] =
|
||||
boilerplateByArch[arch].toHTML(data);
|
||||
}
|
||||
|
||||
return memoizedBoilerplate[memHash];
|
||||
}
|
||||
|
||||
var boilerplateOptions = _.extend({
|
||||
htmlAttributes: htmlAttributes
|
||||
}, _.pick(request, 'dynamicHead', 'dynamicBody'));
|
||||
|
||||
return boilerplateByArch[arch].toHTML(boilerplateOptions);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
WebAppInternals.generateBoilerplateInstance = function (arch,
|
||||
manifest,
|
||||
@@ -292,7 +338,7 @@ WebAppInternals.generateBoilerplateInstance = function (arch,
|
||||
return new Boilerplate(arch, manifest,
|
||||
_.extend({
|
||||
pathMapper: function (itemPath) {
|
||||
return path.join(archPath[arch], itemPath); },
|
||||
return pathJoin(archPath[arch], itemPath); },
|
||||
baseDataExtension: {
|
||||
additionalStaticJs: _.map(
|
||||
additionalStaticJs || [],
|
||||
@@ -338,7 +384,7 @@ WebAppInternals.staticFilesMiddleware = function (staticFiles, req, res, next) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
var pathname = parseurl(req).pathname;
|
||||
var pathname = parseRequest(req).pathname;
|
||||
try {
|
||||
pathname = decodeURIComponent(pathname);
|
||||
} catch (e) {
|
||||
@@ -449,12 +495,12 @@ WebAppInternals.parsePort = function (port) {
|
||||
return parseInt(port);
|
||||
};
|
||||
|
||||
var runWebAppServer = function () {
|
||||
function runWebAppServer() {
|
||||
var shuttingDown = false;
|
||||
var syncQueue = new Meteor._SynchronousQueue();
|
||||
|
||||
var getItemPathname = function (itemUrl) {
|
||||
return decodeURIComponent(url.parse(itemUrl).pathname);
|
||||
return decodeURIComponent(parseUrl(itemUrl).pathname);
|
||||
};
|
||||
|
||||
WebAppInternals.reloadClientPrograms = function () {
|
||||
@@ -462,9 +508,9 @@ var runWebAppServer = function () {
|
||||
staticFiles = {};
|
||||
var generateClientProgram = function (clientPath, arch) {
|
||||
// read the control for the client we'll be serving up
|
||||
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
||||
var clientJsonPath = pathJoin(__meteor_bootstrap__.serverDir,
|
||||
clientPath);
|
||||
var clientDir = path.dirname(clientJsonPath);
|
||||
var clientDir = pathDirname(clientJsonPath);
|
||||
var clientJson = JSON.parse(readUtf8FileSync(clientJsonPath));
|
||||
if (clientJson.format !== "web-program-pre1")
|
||||
throw new Error("Unsupported format for client assets: " +
|
||||
@@ -479,7 +525,7 @@ var runWebAppServer = function () {
|
||||
_.each(manifest, function (item) {
|
||||
if (item.url && item.where === "client") {
|
||||
staticFiles[urlPrefix + getItemPathname(item.url)] = {
|
||||
absolutePath: path.join(clientDir, item.path),
|
||||
absolutePath: pathJoin(clientDir, item.path),
|
||||
cacheable: item.cacheable,
|
||||
hash: item.hash,
|
||||
// Link from source to its map
|
||||
@@ -491,7 +537,7 @@ var runWebAppServer = function () {
|
||||
// Serve the source map too, under the specified URL. We assume all
|
||||
// source maps are cacheable.
|
||||
staticFiles[urlPrefix + getItemPathname(item.sourceMapUrl)] = {
|
||||
absolutePath: path.join(clientDir, item.sourceMap),
|
||||
absolutePath: pathJoin(clientDir, item.sourceMap),
|
||||
cacheable: true
|
||||
};
|
||||
}
|
||||
@@ -526,7 +572,7 @@ var runWebAppServer = function () {
|
||||
try {
|
||||
var clientPaths = __meteor_bootstrap__.configJson.clientPaths;
|
||||
_.each(clientPaths, function (clientPath, arch) {
|
||||
archPath[arch] = path.dirname(clientPath);
|
||||
archPath[arch] = pathDirname(clientPath);
|
||||
generateClientProgram(clientPath, arch);
|
||||
});
|
||||
|
||||
@@ -643,9 +689,9 @@ var runWebAppServer = function () {
|
||||
// Serve static files from the manifest.
|
||||
// This is inspired by the 'static' middleware.
|
||||
app.use(function (req, res, next) {
|
||||
Fiber(function () {
|
||||
WebAppInternals.staticFilesMiddleware(staticFiles, req, res, next);
|
||||
}).run();
|
||||
Promise.resolve().then(() => {
|
||||
WebAppInternals.staticFilesMiddleware(staticFiles, req, res, next);
|
||||
});
|
||||
});
|
||||
|
||||
// Packages and apps can add handlers to this via WebApp.connectHandlers.
|
||||
@@ -667,15 +713,18 @@ var runWebAppServer = function () {
|
||||
});
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
Fiber(function () {
|
||||
if (!appUrl(req.url))
|
||||
Promise.resolve().then(() => {
|
||||
if (! appUrl(req.url)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
};
|
||||
if (shuttingDown)
|
||||
|
||||
if (shuttingDown) {
|
||||
headers['Connection'] = 'Close';
|
||||
}
|
||||
|
||||
var request = WebApp.categorizeRequest(req);
|
||||
|
||||
@@ -692,7 +741,7 @@ var runWebAppServer = function () {
|
||||
res.writeHead(200, headers);
|
||||
res.write(".meteor-css-not-found-error { width: 0px;}");
|
||||
res.end();
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.url.query && request.url.query['meteor_js_resource']) {
|
||||
@@ -703,7 +752,7 @@ var runWebAppServer = function () {
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
res.writeHead(404, headers);
|
||||
res.end("404 Not Found");
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.url.query && request.url.query['meteor_dont_serve_index']) {
|
||||
@@ -714,11 +763,11 @@ var runWebAppServer = function () {
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
res.writeHead(404, headers);
|
||||
res.end("404 Not Found");
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// /packages/asdfsad ... /__cordova/dafsdf.js
|
||||
var pathname = parseurl(req).pathname;
|
||||
var pathname = parseRequest(req).pathname;
|
||||
var archKey = pathname.split('/')[1];
|
||||
var archKeyCleaned = 'web.' + archKey.replace(/^__/, '');
|
||||
|
||||
@@ -728,22 +777,20 @@ var runWebAppServer = function () {
|
||||
archKey = archKeyCleaned;
|
||||
}
|
||||
|
||||
var boilerplate;
|
||||
try {
|
||||
boilerplate = getBoilerplate(request, archKey);
|
||||
} catch (e) {
|
||||
Log.error("Error running template: " + e.stack);
|
||||
return getBoilerplateAsync(
|
||||
request,
|
||||
archKey
|
||||
).then(boilerplate => {
|
||||
var statusCode = res.statusCode ? res.statusCode : 200;
|
||||
res.writeHead(statusCode, headers);
|
||||
res.write(boilerplate);
|
||||
res.end();
|
||||
}, error => {
|
||||
Log.error("Error running template: " + error.stack);
|
||||
res.writeHead(500, headers);
|
||||
res.end();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var statusCode = res.statusCode ? res.statusCode : 200;
|
||||
res.writeHead(statusCode, headers);
|
||||
res.write(boilerplate);
|
||||
res.end();
|
||||
return undefined;
|
||||
}).run();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Return 404 by default, if no other handlers serve this URL.
|
||||
@@ -753,7 +800,7 @@ var runWebAppServer = function () {
|
||||
});
|
||||
|
||||
|
||||
var httpServer = http.createServer(app);
|
||||
var httpServer = createServer(app);
|
||||
var onListeningCallbacks = [];
|
||||
|
||||
// After 5 seconds w/o data on a socket, kill it. On the other hand, if
|
||||
@@ -809,7 +856,7 @@ var runWebAppServer = function () {
|
||||
// Let the rest of the packages (and Meteor.startup hooks) insert connect
|
||||
// middlewares and update __meteor_runtime_config__, then keep going to set up
|
||||
// actually serving HTML.
|
||||
main = function (argv) {
|
||||
exports.main = function (argv) {
|
||||
WebAppInternals.generateBoilerplate();
|
||||
|
||||
// only start listening after all the startup code has run.
|
||||
@@ -817,8 +864,9 @@ var runWebAppServer = function () {
|
||||
var host = process.env.BIND_IP;
|
||||
var localIp = host || '0.0.0.0';
|
||||
httpServer.listen(localPort, localIp, Meteor.bindEnvironment(function() {
|
||||
if (process.env.METEOR_PRINT_ON_LISTEN)
|
||||
if (process.env.METEOR_PRINT_ON_LISTEN) {
|
||||
console.log("LISTENING"); // must match run-app.js
|
||||
}
|
||||
|
||||
var callbacks = onListeningCallbacks;
|
||||
onListeningCallbacks = null;
|
||||
@@ -831,7 +879,7 @@ var runWebAppServer = function () {
|
||||
|
||||
return 'DAEMON';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
runWebAppServer();
|
||||
|
||||
@@ -78,7 +78,7 @@ Tinytest.add("webapp - additional static javascript", function (test) {
|
||||
// before settng it back to what it was originally.
|
||||
WebAppInternals.setInlineScriptsAllowed(true);
|
||||
|
||||
Meteor._noYieldsAllowed(function () {
|
||||
(function () {
|
||||
var boilerplate = WebAppInternals.getBoilerplate({
|
||||
browser: "doesn't-matter",
|
||||
url: "also-doesnt-matter"
|
||||
@@ -100,14 +100,14 @@ Tinytest.add("webapp - additional static javascript", function (test) {
|
||||
nextCalled = true;
|
||||
});
|
||||
test.isTrue(nextCalled);
|
||||
});
|
||||
})();
|
||||
|
||||
// When inline scripts are disallowed, the script body should not be
|
||||
// inlined, and the script should be included in a <script src="..">
|
||||
// tag.
|
||||
WebAppInternals.setInlineScriptsAllowed(false);
|
||||
|
||||
Meteor._noYieldsAllowed(function () {
|
||||
(function () {
|
||||
var boilerplate = WebAppInternals.getBoilerplate({
|
||||
browser: "doesn't-matter",
|
||||
url: "also-doesnt-matter"
|
||||
@@ -129,7 +129,7 @@ Tinytest.add("webapp - additional static javascript", function (test) {
|
||||
var resBody = res.getBody();
|
||||
test.isTrue(resBody.indexOf(additionalScript) !== -1);
|
||||
test.equal(res.statusCode, 200);
|
||||
});
|
||||
})();
|
||||
|
||||
WebAppInternals.setInlineScriptsAllowed(origInlineScriptsAllowed);
|
||||
});
|
||||
@@ -156,6 +156,44 @@ Tinytest.add("webapp - generating boilerplate should not change runtime config",
|
||||
test.isFalse(__meteor_runtime_config__.WEBAPP_TEST_KEY);
|
||||
});
|
||||
|
||||
Tinytest.add("webapp - WebAppInternals.registerBoilerplateDataCallback", function (test) {
|
||||
const key = "from webapp_tests.js";
|
||||
let callCount = 0;
|
||||
|
||||
function callback(request, data, arch) {
|
||||
test.equal(arch, "web.browser");
|
||||
test.equal(request.url, "http://example.com");
|
||||
test.equal(data.dynamicHead, "so dynamic");
|
||||
test.equal(data.body, "");
|
||||
data.body = "<div>oyez</div>";
|
||||
++callCount;
|
||||
}
|
||||
|
||||
WebAppInternals.registerBoilerplateDataCallback(key, callback);
|
||||
|
||||
test.equal(callCount, 0);
|
||||
|
||||
const req = new http.IncomingMessage();
|
||||
req.url = "http://example.com";
|
||||
req.browser = { name: "headless" };
|
||||
req.dynamicHead = "so dynamic";
|
||||
|
||||
const html = WebAppInternals.getBoilerplate(req, "web.browser");
|
||||
|
||||
test.equal(callCount, 1);
|
||||
|
||||
test.isTrue(html.indexOf([
|
||||
"<body>",
|
||||
"<div>oyez</div>"
|
||||
].join("\n")) >= 0);
|
||||
|
||||
test.equal(
|
||||
// Make sure this callback doesn't get called again after this test.
|
||||
WebAppInternals.registerBoilerplateDataCallback(key, null),
|
||||
callback
|
||||
);
|
||||
});
|
||||
|
||||
// Support 'named pipes' (strings) as ports for support of Windows Server / Azure deployments
|
||||
Tinytest.add("webapp - port should be parsed as int unless it is a named pipe", function(test){
|
||||
// Named pipes on Windows Server follow the format: \\.\pipe\{randomstring} or \\{servername}\pipe\{randomstring}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"track": "METEOR",
|
||||
"version": "1.5-rc.13",
|
||||
"version": "1.5.1-rc.5",
|
||||
"recommended": false,
|
||||
"official": false,
|
||||
"description": "Meteor"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"track": "METEOR",
|
||||
"version": "1.5",
|
||||
"version": "1.5.1",
|
||||
"recommended": false,
|
||||
"official": true,
|
||||
"description": "The Official Meteor Distribution"
|
||||
|
||||
@@ -6,7 +6,7 @@ set -u
|
||||
UNAME=$(uname)
|
||||
ARCH=$(uname -m)
|
||||
MONGO_VERSION=3.2.12
|
||||
NODE_VERSION=4.8.3
|
||||
NODE_VERSION=4.8.4
|
||||
NPM_VERSION=4.6.1
|
||||
|
||||
if [ "$UNAME" == "Linux" ] ; then
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# A regex or list of additional regexes to skip.
|
||||
|
||||
# Export this one so it's available in the node environment.
|
||||
export TIMEOUT_SCALE_FACTOR=${TIMEOUT_SCALE_FACTOR:-15}
|
||||
export TIMEOUT_SCALE_FACTOR=${TIMEOUT_SCALE_FACTOR:-4}
|
||||
|
||||
# Skip these tests always. Add other tests with ADDL_SELF_TEST_EXCLUDE.
|
||||
SELF_TEST_EXCLUDE="^old cli tests|^minifiers can't register non-js|^minifiers: apps can't use|^compiler plugins - addAssets"
|
||||
|
||||
@@ -14,8 +14,8 @@ var packageJson = {
|
||||
npm: "4.6.1",
|
||||
"node-gyp": "3.6.0",
|
||||
"node-pre-gyp": "0.6.34",
|
||||
"meteor-babel": "0.21.4",
|
||||
reify: "0.11.22",
|
||||
"meteor-babel": "0.22.0",
|
||||
reify: "0.11.24",
|
||||
"meteor-promise": "0.8.4",
|
||||
fibers: "1.0.15",
|
||||
promise: "7.1.1",
|
||||
|
||||
@@ -660,8 +660,8 @@ main.registerCommand({
|
||||
|
||||
// We do mind if there are non-hidden directories, because we don't want
|
||||
// to recursively check everything to do some crazy heuristic to see if
|
||||
// we should try to creat an app.
|
||||
var stats = files.stat(filePath);
|
||||
// we should try to create an app.
|
||||
var stats = files.stat(files.pathJoin(appPath, filePath));
|
||||
if (stats.isDirectory()) {
|
||||
// Could contain code
|
||||
return true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,15 @@ if (process.platform === "win32") {
|
||||
WATCHER_ENABLED = false;
|
||||
}
|
||||
|
||||
// Default to prioritizing changed files, but disable that behavior (and
|
||||
// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED
|
||||
// is explicitly set to a string that parses to a falsy value.
|
||||
var PRIORITIZE_CHANGED = true;
|
||||
if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED &&
|
||||
! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) {
|
||||
PRIORITIZE_CHANGED = false;
|
||||
}
|
||||
|
||||
var DEFAULT_POLLING_INTERVAL =
|
||||
~~process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000;
|
||||
|
||||
@@ -27,16 +36,31 @@ var NO_WATCHER_POLLING_INTERVAL =
|
||||
// file watchers, but it's to our advantage if they survive restarts.
|
||||
const WATCHER_CLEANUP_DELAY_MS = 30000;
|
||||
|
||||
const watchers = Object.create(null);
|
||||
const entries = Object.create(null);
|
||||
|
||||
// Pathwatcher complains (using console.error, ugh) if you try to watch
|
||||
// two files with the same stat.ino number but different paths, so we have
|
||||
// to deduplicate files by ino.
|
||||
const watchersByIno = new Map;
|
||||
const entriesByIno = new Map;
|
||||
|
||||
// Set of paths for which a change event has been fired, watched with
|
||||
// watchLibrary.watch if available. This could be an LRU cache, but in
|
||||
// practice it should never grow large enough for that to matter.
|
||||
const changedPaths = new Set;
|
||||
|
||||
function hasPriority(absPath) {
|
||||
// If we're not prioritizing changed files, then all files have
|
||||
// priority, which means they should be watched with native file
|
||||
// watchers if the platform supports them. If we are prioritizing
|
||||
// changed files, then only changed files get priority.
|
||||
return PRIORITIZE_CHANGED
|
||||
? changedPaths.has(absPath)
|
||||
: true;
|
||||
}
|
||||
|
||||
function acquireWatcher(absPath, callback) {
|
||||
const entry = watchers[absPath] || (
|
||||
watchers[absPath] = startNewWatcher(absPath));
|
||||
const entry = entries[absPath] || (
|
||||
entries[absPath] = startNewWatcher(absPath));
|
||||
|
||||
// Watches successfully established in the past may have become invalid
|
||||
// because the watched file was deleted or renamed, so we need to make
|
||||
@@ -53,8 +77,8 @@ function acquireWatcher(absPath, callback) {
|
||||
function startNewWatcher(absPath) {
|
||||
const stat = statOrNull(absPath);
|
||||
const ino = stat && stat.ino;
|
||||
if (ino > 0 && watchersByIno.has(ino)) {
|
||||
return watchersByIno.get(ino);
|
||||
if (ino > 0 && entriesByIno.has(ino)) {
|
||||
return entriesByIno.get(ino);
|
||||
}
|
||||
|
||||
function safeUnwatch() {
|
||||
@@ -62,7 +86,7 @@ function startNewWatcher(absPath) {
|
||||
watcher.close();
|
||||
watcher = null;
|
||||
if (ino > 0) {
|
||||
watchersByIno.delete(ino);
|
||||
entriesByIno.delete(ino);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +95,22 @@ function startNewWatcher(absPath) {
|
||||
const callbacks = new Set;
|
||||
let watcherCleanupTimer = null;
|
||||
let watcher;
|
||||
let pollingInterval;
|
||||
|
||||
function getPollingInterval() {
|
||||
if (watcher) {
|
||||
return DEFAULT_POLLING_INTERVAL;
|
||||
}
|
||||
|
||||
if (hasPriority(absPath)) {
|
||||
return NO_WATCHER_POLLING_INTERVAL;
|
||||
}
|
||||
|
||||
if (WATCHER_ENABLED) {
|
||||
return DEFAULT_POLLING_INTERVAL;
|
||||
}
|
||||
|
||||
return NO_WATCHER_POLLING_INTERVAL;
|
||||
}
|
||||
|
||||
function fire(event) {
|
||||
if (event !== "change") {
|
||||
@@ -86,6 +125,10 @@ function startNewWatcher(absPath) {
|
||||
// "delete" or "rename" event, since it is now our only reliable
|
||||
// source of file change notifications.
|
||||
lastWatcherEventTime = 0;
|
||||
|
||||
} else {
|
||||
changedPaths.add(absPath);
|
||||
rewatch();
|
||||
}
|
||||
|
||||
callbacks.forEach(cb => cb.call(this, event));
|
||||
@@ -104,17 +147,16 @@ function startNewWatcher(absPath) {
|
||||
}
|
||||
|
||||
function rewatch() {
|
||||
if (watcher) {
|
||||
// Already watching; nothing to do.
|
||||
return;
|
||||
if (hasPriority(absPath)) {
|
||||
if (watcher) {
|
||||
// Already watching; nothing to do.
|
||||
return;
|
||||
}
|
||||
watcher = watchLibraryWatch(absPath, watchWrapper);
|
||||
} else if (watcher) {
|
||||
safeUnwatch();
|
||||
}
|
||||
|
||||
watcher = watchLibraryWatch(absPath, watchWrapper);
|
||||
|
||||
pollingInterval = watcher
|
||||
? DEFAULT_POLLING_INTERVAL
|
||||
: NO_WATCHER_POLLING_INTERVAL;
|
||||
|
||||
// This is a no-op if we're not watching the file.
|
||||
unwatchFile(absPath, watchFileWrapper);
|
||||
|
||||
@@ -125,7 +167,7 @@ function startNewWatcher(absPath) {
|
||||
// CPU cycles.
|
||||
watchFile(absPath, {
|
||||
persistent: false,
|
||||
interval: pollingInterval,
|
||||
interval: getPollingInterval(),
|
||||
}, watchFileWrapper);
|
||||
}
|
||||
|
||||
@@ -142,19 +184,17 @@ function startNewWatcher(absPath) {
|
||||
|
||||
// If a watcher event fired in the last polling interval, ignore
|
||||
// this event.
|
||||
if (new Date - lastWatcherEventTime > pollingInterval) {
|
||||
if (new Date - lastWatcherEventTime > getPollingInterval()) {
|
||||
fire.call(this, "change");
|
||||
}
|
||||
}
|
||||
|
||||
rewatch();
|
||||
|
||||
const entry = {
|
||||
callbacks,
|
||||
rewatch,
|
||||
|
||||
release(callback) {
|
||||
if (! watchers[absPath]) {
|
||||
if (! entries[absPath]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,8 +217,8 @@ function startNewWatcher(absPath) {
|
||||
},
|
||||
|
||||
close() {
|
||||
if (watchers[absPath] !== entry) return;
|
||||
watchers[absPath] = null;
|
||||
if (entries[absPath] !== entry) return;
|
||||
entries[absPath] = null;
|
||||
|
||||
if (watcherCleanupTimer) {
|
||||
clearTimeout(watcherCleanupTimer);
|
||||
@@ -192,15 +232,15 @@ function startNewWatcher(absPath) {
|
||||
};
|
||||
|
||||
if (ino > 0) {
|
||||
watchersByIno.set(ino, entry);
|
||||
entriesByIno.set(ino, entry);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function closeAllWatchers() {
|
||||
Object.keys(watchers).forEach(absPath => {
|
||||
const entry = watchers[absPath];
|
||||
Object.keys(entries).forEach(absPath => {
|
||||
const entry = entries[absPath];
|
||||
if (entry) {
|
||||
entry.close();
|
||||
}
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
|
||||
meteor-base@1.1.0 # Packages every Meteor app needs to have
|
||||
mobile-experience@1.0.4 # Packages for a great mobile UX
|
||||
mongo@1.1.18 # The database Meteor supports right now
|
||||
mongo@1.1.19 # The database Meteor supports right now
|
||||
blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
|
||||
reactive-var@1.0.11 # Reactive variable for tracker
|
||||
jquery@1.11.10 # Helpful client-side library
|
||||
tracker@1.1.3 # Meteor's client-side reactive programming library
|
||||
|
||||
standard-minifier-css@1.3.4 # CSS minifier run for production mode
|
||||
standard-minifier-js@2.1.0 # JS minifier run for production mode
|
||||
standard-minifier-js@2.1.1 # JS minifier run for production mode
|
||||
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
|
||||
ecmascript@0.8.0 # Enable ECMAScript2015+ syntax in app code
|
||||
shell-server@0.2.3 # Server-side component of the `meteor shell` command
|
||||
ecmascript@0.8.2 # Enable ECMAScript2015+ syntax in app code
|
||||
shell-server@0.2.4 # Server-side component of the `meteor shell` command
|
||||
|
||||
autopublish@1.0.7 # Publish all data to the clients (for prototyping)
|
||||
insecure@1.0.7 # Allow all DB writes from clients (for prototyping)
|
||||
dynamic-import
|
||||
dynamic-import@0.1.1
|
||||
dispatch:mocha-phantomjs
|
||||
dispatch:mocha-browser
|
||||
lazy-test-package
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@1.5
|
||||
METEOR@1.5.1
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
|
||||
meteor-base@1.1.0 # Packages every Meteor app needs to have
|
||||
mobile-experience@1.0.4 # Packages for a great mobile UX
|
||||
mongo@1.1.18 # The database Meteor supports right now
|
||||
mongo@1.1.19 # The database Meteor supports right now
|
||||
blaze-html-templates # Compile .html files into Meteor Blaze views
|
||||
session@1.1.7 # Client-side reactive dictionary for your app
|
||||
jquery@1.11.10 # Helpful client-side library
|
||||
tracker@1.1.3 # Meteor's client-side reactive programming library
|
||||
|
||||
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
|
||||
ecmascript@0.8.0 # Enable ECMAScript2015+ syntax in app code
|
||||
ecmascript@0.8.2 # Enable ECMAScript2015+ syntax in app code
|
||||
|
||||
coffeescript
|
||||
modules-test-package
|
||||
dispatch:mocha-phantomjs
|
||||
dispatch:mocha-browser
|
||||
standard-minifier-css@1.3.4
|
||||
standard-minifier-js@2.1.0
|
||||
standard-minifier-js@2.1.1
|
||||
client-only-ecmascript
|
||||
modules-test-plugin
|
||||
shell-server@0.2.3
|
||||
dynamic-import
|
||||
shell-server@0.2.4
|
||||
dynamic-import@0.1.1
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@1.5
|
||||
METEOR@1.5.1
|
||||
|
||||
@@ -22,6 +22,7 @@ function startRun(sandbox) {
|
||||
// Tests the actual cache logic used by coffeescript.
|
||||
selftest.define("compiler plugin caching - coffee", () => {
|
||||
var s = new Sandbox({ fakeMongo: true });
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
s.createApp("myapp", "caching-coffee");
|
||||
s.cd("myapp");
|
||||
@@ -91,6 +92,7 @@ selftest.define("compiler plugin caching - coffee", () => {
|
||||
|
||||
selftest.define("compiler plugin caching - " + packageName, () => {
|
||||
var s = new Sandbox({ fakeMongo: true });
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
s.createApp("myapp", "caching-" + packageName);
|
||||
s.cd("myapp");
|
||||
@@ -278,6 +280,7 @@ selftest.define("compiler plugin caching - local plugin", function () {
|
||||
// Test error on duplicate compiler plugins.
|
||||
selftest.define("compiler plugins - duplicate extension", () => {
|
||||
const s = new Sandbox({ fakeMongo: true });
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
s.createApp("myapp", "duplicate-compiler-extensions");
|
||||
s.cd("myapp");
|
||||
|
||||
2
tools/tests/cordova-plugins.js
vendored
2
tools/tests/cordova-plugins.js
vendored
@@ -135,6 +135,8 @@ selftest.define("change cordova plugins", ["cordova"], function () {
|
||||
var s = new Sandbox();
|
||||
var run;
|
||||
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
// Starting a run
|
||||
s.createApp("myapp", "package-tests");
|
||||
s.cd("myapp");
|
||||
|
||||
@@ -7,6 +7,8 @@ selftest.define("css hot code push", function (options) {
|
||||
clients: options.clients
|
||||
});
|
||||
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
s.createApp("myapp", "css-injection-test");
|
||||
s.cd("myapp");
|
||||
s.testWithAllClients(function (run) {
|
||||
|
||||
@@ -100,6 +100,8 @@ selftest.define("change packages during hot code push", [], function () {
|
||||
var s = new Sandbox();
|
||||
var run;
|
||||
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
// Starting a run
|
||||
s.createApp("myapp", "package-tests");
|
||||
s.cd("myapp");
|
||||
|
||||
@@ -22,6 +22,8 @@ selftest.define("run", function () {
|
||||
var s = new Sandbox({ fakeMongo: true });
|
||||
var run;
|
||||
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
// Starting a run
|
||||
s.createApp("myapp", "standard-app");
|
||||
s.cd("myapp");
|
||||
@@ -239,6 +241,8 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function
|
||||
});
|
||||
var run;
|
||||
|
||||
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
|
||||
|
||||
s.createApp("myapp", "packageless", { release: DEFAULT_RELEASE_TRACK + '@v1' });
|
||||
s.cd("myapp");
|
||||
|
||||
@@ -249,6 +253,7 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function
|
||||
run.match('localhost:3000');
|
||||
s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v2');
|
||||
run.matchErr('to Meteor v2 from Meteor v1');
|
||||
run.waitSecs(10);
|
||||
run.expectExit(254);
|
||||
|
||||
// But not if the release was forced (case 1)
|
||||
@@ -261,6 +266,7 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function
|
||||
s.write('empty.js', '');
|
||||
run.waitSecs(2);
|
||||
run.match('restarted');
|
||||
run.waitSecs(10);
|
||||
run.stop();
|
||||
run.forbidAll("updated");
|
||||
|
||||
@@ -274,6 +280,7 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function
|
||||
s.write('empty.js', '');
|
||||
run.waitSecs(2);
|
||||
run.match('restarted');
|
||||
run.waitSecs(10);
|
||||
run.stop();
|
||||
run.forbidAll("updated");
|
||||
|
||||
@@ -287,10 +294,12 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function
|
||||
run.tellMongo(MONGO_LISTENING);
|
||||
run.waitSecs(2);
|
||||
run.match('localhost:3000');
|
||||
run.waitSecs(10);
|
||||
s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v2');
|
||||
s.write('empty.js', '');
|
||||
run.waitSecs(2);
|
||||
run.match('restarted');
|
||||
run.waitSecs(10);
|
||||
run.stop();
|
||||
run.forbidAll("updated");
|
||||
});
|
||||
|
||||
@@ -1045,7 +1045,13 @@ _.extend(PhantomClient.prototype, {
|
||||
files.convertToOSPath(scriptPath), self.url],
|
||||
{}, function (error, stdout, stderr) {
|
||||
if (self._logError && error) {
|
||||
console.log("PhantomJS exited with error ", error, "\nstdout:\n", stdout, "\nstderr:\n", stderr);
|
||||
console.log(
|
||||
"PhantomJS exited with error ", error,
|
||||
"\nstdout:\n", stdout,
|
||||
"\nstderr:\n", stderr
|
||||
);
|
||||
} else if (stderr) {
|
||||
console.log("PhantomJS stderr:\n", stderr);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// For this global function to be defined, the --expose-gc flag must have
|
||||
// been passed to node at the bottom of the ../../meteor script, probably
|
||||
// via the TOOL_NODE_FLAGS environment variable.
|
||||
const gc = global.gc;
|
||||
import { throttle } from "underscore";
|
||||
|
||||
// In the future, this function may become smarter about how often it
|
||||
// actually calls the gc function, but that's an implementation detail.
|
||||
export function requestGarbageCollection() {
|
||||
if (typeof gc === "function") {
|
||||
gc();
|
||||
}
|
||||
}
|
||||
export const requestGarbageCollection =
|
||||
// For this global function to be defined, the --expose-gc flag must
|
||||
// have been passed to node at the bottom of the ../../meteor script,
|
||||
// probably via the TOOL_NODE_FLAGS environment variable.
|
||||
typeof global.gc === "function"
|
||||
// Restrict actual garbage collections to once per 500ms.
|
||||
? throttle(global.gc, 500)
|
||||
: function () {};
|
||||
|
||||
Reference in New Issue
Block a user