diff --git a/History.md b/History.md index 60ce0c0116..5e55774b0f 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,43 @@ +## vNEXT, unreleased + +### Breaking changes + +N/A + +### Migration steps + +N/A + +### Changes + +* `email` package now exposes `hookSend` that runs before emails are sent. + +## v1.10.3, TBD + +### Breaking changes + +* `email` package dependencies have been update and package version has been bumped to 2.0.0 + There is a potential breaking change as the underlying package started to use `dns.resolve()` + instead of `dns.lookup()` which might be breaking on some environments. + See [nodemailer changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) for more information. + +### Migration steps + +N/A + +### Changes + +* Fixes error when removing cordova plugin that depends on cli variables. PR [#10976](https://github.com/meteor/meteor/pull/11052) + +* `email` package now exposes `hookSend` that runs before emails are send. + +* The version of MongoDB used by Meteor in development has been updated + from 4.2.5 to 4.2.8 + +* Node.js has been updated to version + [12.18.2](https://nodejs.org/en/blog/release/v12.18.2/) + +* Updated npm to version 6.14.5 ## v1.10.2, 2020-04-21 diff --git a/meteor b/meteor index 15264e56ef..d18f5dfd05 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=12.16.1.8 +BUNDLE_VERSION=12.18.2.1 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 02bd42a85b..eeea919ae5 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -71,9 +71,9 @@ export class AccountsServer extends AccountsCommon { resetPassword: token => Meteor.absoluteUrl(`#/reset-password/${token}`), verifyEmail: token => Meteor.absoluteUrl(`#/verify-email/${token}`), enrollAccount: token => Meteor.absoluteUrl(`#/enroll-account/${token}`), - } + }; - this.addDefaultRateLimit() + this.addDefaultRateLimit(); } /// @@ -117,6 +117,19 @@ export class AccountsServer extends AccountsCommon { this._validateNewUserHooks.push(func); } + /** + * @summary Validate login from external service + * @locus Server + * @param {Function} func Called whenever login/user creation from external service is attempted. Login or user creation based on this login can be aborted by passing a falsy value or throwing an exception. + */ + beforeExternalLogin(func) { + if (this._beforeExternalLoginHook) { + throw new Error("Can only call beforeExternalLogin once"); + } + + this._beforeExternalLoginHook = func; + } + /// /// CREATE USER HOOKS /// @@ -1211,6 +1224,11 @@ export class AccountsServer extends AccountsCommon { let user = this.users.findOne(selector, {fields: this._options.defaultFieldSelector}); + // Before continuing, run user hook to see if we should continue + if (this._beforeExternalLoginHook && !this._beforeExternalLoginHook(serviceName, serviceData, user)) { + throw new Meteor.Error(403, "Login forbidden"); + } + // When creating a new user we pass through all options. When updating an // existing user, by default we only process/pass through the serviceData // (eg, so that we keep an unexpired access token and don't cache old email diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 0aa92459c9..fa52871a21 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -623,3 +623,44 @@ Tinytest.add( Accounts._options = accountsOptions; } ); + +Tinytest.add( + 'accounts - verify beforeExternalLogin hook can stop user login', + test => { + // Verify user data is saved properly when not using the + // beforeExternalLogin hook. + let facebookId = Random.id(); + const uid1 = Accounts.updateOrCreateUserFromExternalService( + 'facebook', + { id: facebookId }, + { profile: { foo: 1 } }, + ).userId; + const ignoreFieldName = "bigArray"; + const c = Meteor.users.update(uid1, {$set: {[ignoreFieldName]: [1]}}); + let users = + Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); + test.length(users, 1); + test.equal(users[0].profile.foo, 1); + test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - before limit fields'); + + // Verify that when beforeExternalLogin returns false + // that an error throws and user is not saved + Accounts.beforeExternalLogin((serviceName, serviceData, user) => { + // Check that we get the correct data + test.equal(serviceName, 'facebook'); + test.equal(serviceData, { id: facebookId }); + test.equal(user._id, uid1); + return false + }); + + test.throws(() => Accounts.updateOrCreateUserFromExternalService( + 'facebook', + { id: facebookId }, + { profile: { foo: 1 } }, + )); + + // Cleanup + Meteor.users.remove(uid1); + Accounts._beforeExternalLoginHook = null; + } +); diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index d0cae1538f..f499aac40f 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "A user account system", - version: "1.6.0", + version: "1.7.0-beta1103.6", }); Package.onUse(api => { diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index 32c080a4a5..346390e63e 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -20,7 +20,7 @@ Accounts.emailTemplates.headers = { 'My-Custom-Header' : 'Cool' }; -EmailTest.hookSend(options => { +Email.hookSend(options => { const { to } = options; if (!to || !to.toUpperCase().includes('INTERCEPT')) { return true; // go ahead and send diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index fb9bc6324c..3019c48428 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -5,7 +5,7 @@ Package.describe({ // 2.2.x in the future. The version was also 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. - version: "1.6.1" + version: "1.6.2-beta1103.6" }); Package.onUse(api => { diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 022c1cdf65..871393127a 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -20,7 +20,7 @@ const reportError = (error, callback) => { /** * @summary Log the user in with a password. * @locus Client - * @param {Object | String} user + * @param {Object | String} selector * Either a string interpreted as a username or an email; or an object with a * single key: `email`, `username` or `id`. Username or email match in a case * insensitive manner. diff --git a/packages/ddp-client/.npm/package/npm-shrinkwrap.json b/packages/ddp-client/.npm/package/npm-shrinkwrap.json index 83ff1746e4..5d2c645d18 100644 --- a/packages/ddp-client/.npm/package/npm-shrinkwrap.json +++ b/packages/ddp-client/.npm/package/npm-shrinkwrap.json @@ -2,9 +2,9 @@ "lockfileVersion": 1, "dependencies": { "lolex": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.1.tgz", - "integrity": "sha512-mQuW55GhduF3ppo+ZRUTz1PRjEh1hS5BbqU7d8D0ez2OKxHDod7StPPeAVKisZR5aLkHZjdGWSL42LSONUJsZw==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz", + "integrity": "sha512-A5pN2tkFj7H0dGIAM6MFvHKMJcPnjZsOMvR7ujCjfgW5TbV6H9vb1PgxLtHvjqNZTHsUolz+6/WEO0N1xNx2ng==" } } } diff --git a/packages/ddp-rate-limiter/package.js b/packages/ddp-rate-limiter/package.js index 730cededb7..7a325c5f70 100644 --- a/packages/ddp-rate-limiter/package.js +++ b/packages/ddp-rate-limiter/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ddp-rate-limiter', - version: '1.0.7', + version: '1.0.9-beta1103.6', // Brief, one-line summary of the package. summary: 'The DDPRateLimiter allows users to add rate limits to DDP' + ' methods and subscriptions.', diff --git a/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json b/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json index c5366d30b1..a69db9f9a5 100644 --- a/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json +++ b/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json @@ -2,9 +2,9 @@ "lockfileVersion": 1, "dependencies": { "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" } } } diff --git a/packages/ecmascript-runtime-client/package.js b/packages/ecmascript-runtime-client/package.js index 0d301514e8..efa54a63d3 100644 --- a/packages/ecmascript-runtime-client/package.js +++ b/packages/ecmascript-runtime-client/package.js @@ -1,13 +1,13 @@ Package.describe({ name: "ecmascript-runtime-client", - version: "0.10.0", + version: "0.11.0-beta1103.6", summary: "Polyfills for new ECMAScript 2015 APIs like Map and Set", git: "https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client", documentation: "README.md" }); Npm.depends({ - "core-js": "3.2.1" + "core-js": "3.6.5" }); Package.onUse(function(api) { diff --git a/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json b/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json index c5366d30b1..a69db9f9a5 100644 --- a/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json +++ b/packages/ecmascript-runtime-server/.npm/package/npm-shrinkwrap.json @@ -2,9 +2,9 @@ "lockfileVersion": 1, "dependencies": { "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" } } } diff --git a/packages/ecmascript-runtime-server/package.js b/packages/ecmascript-runtime-server/package.js index 12e5a683c0..ca506d1790 100644 --- a/packages/ecmascript-runtime-server/package.js +++ b/packages/ecmascript-runtime-server/package.js @@ -1,13 +1,13 @@ Package.describe({ name: "ecmascript-runtime-server", - version: "0.9.0", + version: "0.10.0-beta1103.6", summary: "Polyfills for new ECMAScript 2015 APIs like Map and Set", git: "https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client", documentation: "README.md" }); Npm.depends({ - "core-js": "3.2.1" + "core-js": "3.6.5" }); Package.onUse(function(api) { diff --git a/packages/email/.npm/package/npm-shrinkwrap.json b/packages/email/.npm/package/npm-shrinkwrap.json index 38652fe742..e4ffb8f728 100644 --- a/packages/email/.npm/package/npm-shrinkwrap.json +++ b/packages/email/.npm/package/npm-shrinkwrap.json @@ -1,15 +1,15 @@ { "lockfileVersion": 1, "dependencies": { - "node4mailer": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/node4mailer/-/node4mailer-4.0.3.tgz", - "integrity": "sha1-jwx6ZzdSehKFMBhaFMLoeZiaiRA=" + "nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" }, "stream-buffers": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-0.2.5.tgz", - "integrity": "sha1-+TBTnTzwjXSKNArWE5+Vss60jwU=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==" } } } diff --git a/packages/email/email.js b/packages/email/email.js index 4809122f99..c9425114fa 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -1,6 +1,6 @@ var Future = Npm.require('fibers/future'); var urlModule = Npm.require('url'); -var nodemailer = Npm.require('node4mailer'); +var nodemailer = Npm.require('nodemailer'); Email = {}; EmailTest = {}; @@ -8,12 +8,12 @@ EmailTest = {}; EmailInternals = { NpmModules: { mailcomposer: { - version: Npm.require('node4mailer/package.json').version, - module: Npm.require('node4mailer/lib/mail-composer') + version: Npm.require('nodemailer/package.json').version, + module: Npm.require('nodemailer/lib/mail-composer') }, nodemailer: { - version: Npm.require('node4mailer/package.json').version, - module: Npm.require('node4mailer') + version: Npm.require('nodemailer/package.json').version, + module: Npm.require('nodemailer') } } }; @@ -98,15 +98,15 @@ var smtpSend = function (transport, mail) { transport._syncSendMail(mail); }; +var sendHooks = []; /** - * Mock out email sending (eg, during a test.) This is private for now. + * Hook that runs before email is sent. * - * f receives the arguments to Email.send and should return true to go + * @param f {function} receives the arguments to Email.send and should return true to go * ahead and send the email (or at least, try subsequent hooks), or * false to skip sending. */ -var sendHooks = []; -EmailTest.hookSend = function (f) { +Email.hookSend = function (f) { sendHooks.push(f); }; @@ -118,9 +118,9 @@ EmailTest.hookSend = function (f) { * If the `MAIL_URL` environment variable is set, actually sends the email. * Otherwise, prints the contents of the email to standard out. * - * Note that this package is based on **mailcomposer 4**, so make sure to refer to - * [the documentation](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md) - * for that version when using the `attachments` or `mailComposer` options. + * Note that this package is based on **nodemailer**, so make sure to refer to + * [the documentation](http://nodemailer.com/) + * when using the `attachments` or `mailComposer` options. * * @locus Server * @param {Object} options @@ -136,7 +136,7 @@ EmailTest.hookSend = function (f) { * @param {String} [options.icalEvent] iCalendar event attachment * @param {Object} [options.headers] Dictionary of custom headers - e.g. `{ "header name": "header value" }`. To set an object under a header name, use `JSON.stringify` - e.g. `{ "header name": JSON.stringify({ tracking: { level: 'full' } }) }`. * @param {Object[]} [options.attachments] Array of attachment objects, as - * described in the [mailcomposer documentation](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md#attachments). + * described in the [nodemailer documentation](https://nodemailer.com/message/attachments/). * @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields) * object representing the message to be sent. Overrides all other options. * You can create a `MailComposer` object via diff --git a/packages/email/package.js b/packages/email/package.js index 836e70f6f9..07fb357eba 100644 --- a/packages/email/package.js +++ b/packages/email/package.js @@ -1,11 +1,11 @@ Package.describe({ summary: "Send email messages", - version: "1.2.3" + version: "2.0.0-beta1103.6" }); Npm.depends({ - node4mailer: "4.0.3", - "stream-buffers": "0.2.5" + nodemailer: "6.4.6", + "stream-buffers": "3.0.2" }); Package.onUse(function (api) { diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index 651e2fe3c2..70870eef45 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "The Meteor command-line tool", - version: '1.10.2' + version: '1.10.3-beta.6' }); Package.includeTool(); diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index bb569315d0..fef6889523 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "1.10.2-rc.0", + "version": "1.10.3-beta.6", "recommended": false, "official": false, "description": "Meteor" diff --git a/scripts/build-dev-bundle-common.sh b/scripts/build-dev-bundle-common.sh index 026a66f569..760c02c55b 100644 --- a/scripts/build-dev-bundle-common.sh +++ b/scripts/build-dev-bundle-common.sh @@ -5,10 +5,10 @@ set -u UNAME=$(uname) ARCH=$(uname -m) -NODE_VERSION=12.16.1 +NODE_VERSION=12.18.2 MONGO_VERSION_64BIT=4.2.5 MONGO_VERSION_32BIT=3.2.22 -NPM_VERSION=6.14.0 +NPM_VERSION=6.14.6 # If we built Node from source on Jenkins, this is the build number. NODE_BUILD_NUMBER= diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 266f2d7fd8..570183b4be 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -10,7 +10,7 @@ var packageJson = { dependencies: { // Explicit dependency because we are replacing it with a bundled version // and we want to make sure there are no dependencies on a higher version - npm: "6.14.0", + npm: "6.14.6", pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8", "node-gyp": "6.0.1", "node-pre-gyp": "0.14.0", diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 2005c0e3fe..d46b4f1602 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -517,6 +517,7 @@ main.registerCommand({ minimal: { type: Boolean }, full: { type: Boolean }, react: { type: Boolean }, + vue: { type: Boolean }, typescript: { type: Boolean }, }, catalogRefresh: new catalog.Refresh.Never() @@ -770,6 +771,8 @@ main.registerCommand({ skelName += "-full"; } else if (options.react) { skelName += "-react"; + } else if (options.vue) { + skelName += "-vue"; } else if (options.typescript) { skelName += "-typescript"; } @@ -886,8 +889,9 @@ main.registerCommand({ ! options.minimal && ! options.full && ! options.react && + ! options.vue && ! options.typescript) { - // Notify people about --bare, --minimal, --full, --react, and --typescript. + // Notify people about --bare, --minimal, --full, --react, --vue, and --typescript. Console.info([ "", "To start with a different app template, try one of the following:", @@ -898,6 +902,7 @@ main.registerCommand({ cmd("meteor create --minimal # to create an app with as few Meteor packages as possible"); cmd("meteor create --full # to create a more complete scaffolded app"); cmd("meteor create --react # to create a basic React-based app"); + cmd("meteor create --vue # to create a basic Vue-based app"); cmd("meteor create --typescript # to create an app using TypeScript and React"); } diff --git a/tools/cordova/project.js b/tools/cordova/project.js index 75d263ae6f..828c565982 100644 --- a/tools/cordova/project.js +++ b/tools/cordova/project.js @@ -74,6 +74,34 @@ const pinnedPluginVersions = { "cordova-plugin-wkwebview-engine": "1.1.3" } +/** + * To fix Cordova error: Variable(s) missing we convert the cli_variables + * when removing plugins we want to convert for each plugin, for instance, + * cordova-plugin-facebook4: + * commandOptions { + * ... + * cli_variables: { + * 'cordova-plugin-googleplus': { + * REVERSED_CLIENT_ID: 'com.googleusercontent.apps.11111111-xxkodsuusaiusixuaix' + * }, + * 'cordova-plugin-facebook4': { APP_ID: '1111111111111111', APP_NAME: 'appname' } + * } + * } + * into this + * commandOptions { + * ... + * cli_variables: { APP_ID: '1111111111111111', APP_NAME: 'appname' } + * } + * + * @param plugin + * @param commandOptions + */ +const getCommandOptionsForPlugin = (plugin, commandOptions = {}) => { + const cli_variables = commandOptions && commandOptions.cli_variables + && commandOptions.cli_variables[plugin] || {}; + return {...commandOptions, cli_variables}; +} + export class CordovaProject { constructor(projectContext, options = {}) { @@ -521,7 +549,7 @@ from Cordova project`, async () => { buildmessage.assertInJob(); if (utils.isUrlWithSha(version)) { - return convertToGitUrl(version); + return `${id}@${convertToGitUrl(version)}`; } else if (utils.isUrlWithFileScheme(version)) { // Strip file:// and resolve the path relative to the cordova-build // directory @@ -560,18 +588,28 @@ from Cordova project`, async () => { { cli_variables: config, link: utils.isUrlWithFileScheme(version) }); this.runCommands(`adding plugin ${target} \ -to Cordova project`, cordova_lib.plugin.bind(undefined, 'add', [target], commandOptions)); +to Cordova project`, cordova_lib.plugin.bind(undefined, 'add', [target], + commandOptions)); } } // plugins is an array of plugin IDs. - removePlugins(plugins) { + removePlugins(plugins, config = {}) { if (_.isEmpty(plugins)) { return; } - this.runCommands(`removing plugins ${plugins} \ -from Cordova project`, cordova_lib.plugin.bind(undefined, 'rm', plugins, this.defaultOptions)); + const commandOptions = _.extend(this.defaultOptions, + { cli_variables: config }); + + plugins.forEach(plugin => { + const commandOptionsPlugin = getCommandOptionsForPlugin(plugin, + commandOptions); + + this.runCommands(`removing plugin ${plugin} \ + from Cordova project`, cordova_lib.plugin.bind(undefined, 'rm --force', [plugin], + commandOptionsPlugin)); + }); } // Ensures that the Cordova plugins are synchronized with the app-level @@ -699,7 +737,7 @@ perform cordova plugins reinstall`); Object.keys(installedPluginVersions)); } - this.removePlugins(pluginsToRemove); + this.removePlugins(pluginsToRemove, pluginsConfiguration); let pluginVersionsToInstall; @@ -735,7 +773,7 @@ perform cordova plugins reinstall`); // cordova-plugin-whitelist@1.3.2 => { 'cordova-plugin-whitelist': '1.3.2' } // com.cordova.plugin@file://.cordova-plugins/plugin => { 'com.cordova.plugin': 'file://.cordova-plugins/plugin' } // @scope/plugin@1.0.0 => { 'com.cordova.plugin': 'scope/plugin' } - const installed = this.listInstalledPluginVersions(true); + const installed = this.listInstalledPluginVersions(); const installedPluginsNames = Object.keys(installed); const installedPluginsVersions = Object.values(installed); const missingPlugins = {}; diff --git a/tools/fs/files.ts b/tools/fs/files.ts index 024f5e8298..b5e37a1618 100644 --- a/tools/fs/files.ts +++ b/tools/fs/files.ts @@ -5,7 +5,7 @@ /// import assert from "assert"; -import fs, { PathLike, Stats } from "fs"; +import fs, { PathLike, Stats, Dirent } from "fs"; import path from "path"; import os from "os"; import { spawn, execFile } from "child_process"; @@ -1757,6 +1757,14 @@ wrapFsFunc<[string], string[]>("readdir", fs.readdirSync, [0], { }, }); +export const readdirWithTypes = wrapFsFunc<[string], Dirent[]>("readdirWithTypes", (dir) => { + return fs.readdirSync(dir, { + withFileTypes: true + }); + }, [0], { + cached: true +}); + export const appendFile = wrapDestructiveFsFunc("appendFile", fs.appendFileSync); export const chmod = wrapDestructiveFsFunc("chmod", fs.chmodSync); export const close = wrapFsFunc("close", fs.closeSync, []); diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 2b548341ce..c90fa56c18 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -35,6 +35,14 @@ var NO_WATCHER_POLLING_INTERVAL = // file watchers, but it's to our advantage if they survive restarts. const WATCHER_CLEANUP_DELAY_MS = 30000; +// Pathwatcher complains (using console.error, ugh) if you try to watch +// two files with the same stat.ino number but different paths on linux, so we have +// to deduplicate files by ino. +const DEDUPLICATE_BY_INO = process.platform !== "win32"; + +const entriesByIno = new Map; + + export type SafeWatcher = { close: () => void; } @@ -49,11 +57,6 @@ interface Entry extends SafeWatcher { const entries: Record = 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 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. @@ -86,10 +89,19 @@ function acquireWatcher(absPath: string, callback: EntryCallback) { } function startNewWatcher(absPath: string): Entry { - const stat = statOrNull(absPath); - if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) { - const entry = entriesByIno.get(stat.ino); - if (entries[absPath] === entry) { + let stat: Stats | null = null; + + if (DEDUPLICATE_BY_INO) { + stat = statOrNull(absPath); + if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) { + const entry = entriesByIno.get(stat.ino); + if (entries[absPath] === entry) { + return entry; + } + } + } else { + let entry = entries[absPath]; + if (entry) { return entry; } } diff --git a/tools/fs/watch.ts b/tools/fs/watch.ts index 44afacdde1..f473840331 100644 --- a/tools/fs/watch.ts +++ b/tools/fs/watch.ts @@ -1,4 +1,4 @@ -import { Stats, FSWatcher } from "fs"; +import { Stats, FSWatcher, Dirent } from "fs"; import * as files from "./files"; import * as safeWatcher from "./safe-watcher"; import { createHash } from "crypto"; @@ -339,7 +339,7 @@ export const sha512 = Profile("sha512", function (...args: (string | Buffer)[]) function readAndStatDirectory(absPath: string) { // Read the directory. try { - var contents = files.readdir(absPath); + var contents = files.readdirWithTypes(absPath); } catch (e) { // If the path is not a directory, return null; let other errors through. if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) { @@ -351,9 +351,14 @@ function readAndStatDirectory(absPath: string) { // Add slashes to the end of directories. const contentsWithSlashes: string[] = []; contents.forEach(entry => { - // We do stat instead of lstat here, so that we treat symlinks to - // directories just like directories themselves. - const stat = optimisticStatOrNull(files.pathJoin(absPath, entry)); + let stat: Dirent | Stats | null = entry; + let name = entry.name; + + if (entry.isSymbolicLink()) { + // We do stat instead of lstat here, so that we treat symlinks to + // directories just like directories themselves. + stat = optimisticStatOrNull(files.pathJoin(absPath, entry.name)); + } if (! stat) { // Disappeared after the readdir (or a dangling symlink)? // Eh, pretend it was never there in the first place. @@ -361,10 +366,10 @@ function readAndStatDirectory(absPath: string) { } if (stat.isDirectory()) { - entry += '/'; + name += '/'; } - contentsWithSlashes.push(entry); + contentsWithSlashes.push(name); }); return contentsWithSlashes; diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js index 04a23919ec..797dca477c 100644 --- a/tools/isobuild/builder.js +++ b/tools/isobuild/builder.js @@ -1,7 +1,7 @@ import assert from "assert"; import {WatchSet, readAndWatchFile, sha1} from '../fs/watch'; import files, { - symlinkWithOverwrite, + symlinkWithOverwrite, realpath, } from '../fs/files'; import NpmDiscards from './npm-discards.js'; import {Profile} from '../tool-env/profile'; @@ -540,7 +540,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` // as well as node_modules/meteor and the parent directories of any // scoped npm packages. this._ensureAllNonPackageDirectories( - files.realpath(options.from), + realpath(options.from), options.to ); } @@ -637,7 +637,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` }); } - const rootDir = files.realpath(from); + const rootDir = realpath(from); const walk = (absFrom, relTo) => { if (symlink && ! (relTo in this.usedAsFile)) { @@ -661,7 +661,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` return; } - // Returns files.realpath(thisAbsFrom), iff it is external to + // Returns files.realpath(thisAbsFrom), if it is external to // rootDir, using caching because this function might be called // more than once. let cachedExternalPath; @@ -671,7 +671,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` } try { - var real = files.realpath(thisAbsFrom); + var real = realpath(thisAbsFrom); } catch (e) { if (e.code !== "ENOENT" && e.code !== "ELOOP") { diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 5311ebbfd6..fc71b2ca59 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -173,6 +173,7 @@ import { loadIsopackage } from '../tool-env/isopackets.js'; import { CORDOVA_PLATFORM_VERSIONS } from '../cordova'; import { gzipSync } from "zlib"; import { PackageRegistry } from "../../packages/meteor/define-package.js"; +import { optimisticLStatOrNull } from '../fs/optimistic'; const SOURCE_URL_PREFIX = "meteor://\u{1f4bb}app"; @@ -486,12 +487,11 @@ export class NodeModulesDirectory { return true; } - const real = files.realpathOrNull(path); - if (typeof real === "string" && - real !== path) { + const fileStatus = optimisticLStatOrNull(path); + if (fileStatus && fileStatus.isSymbolicLink()) { // If node_modules/.bin/command is a symlink, determine the // answer by calling isWithinProdPackage(real). - return isWithinProdPackage(real); + return isWithinProdPackage(files.realpathOrNull(path)); } // If node_modules/.bin/command is not a symlink, then it's hard @@ -2844,20 +2844,18 @@ var writeTargetToPath = Profile( previousBuilder = null, buildMode, minifyMode, + forceInPlaceBuild }) { var builder = new Builder({ outputPath: files.pathJoin(outputPath, 'programs', name), previousBuilder, - // We do not force an in-place build for individual targets like - // .meteor/local/build/programs/web.browser.legacy, because they - // tend to be written atomically, and it's important on Windows to - // avoid overwriting files that might be open currently in the build - // or server process. - // Server builds do use an in-place build since the server is always stopped - // during the build. - // If client in-place builds were safer on Windows, they - // would be much quicker than from-scratch rebuilds. - forceInPlaceBuild: name === 'server', + // We do not force an in-place build for individual targets + // like .meteor/local/build/programs/web.browser.legacy, because they tend + // to be written atomically, and it's important on Windows to avoid + // overwriting files that might be open currently in the server + // process. There are some exceptions when we know the server process + // is not using the files, such as during a full build when it is stopped. + forceInPlaceBuild }); var targetBuild = target.write(builder, { @@ -2910,6 +2908,7 @@ var writeSiteArchive = Profile("bundler writeSiteArchive", function ( buildMode, minifyMode, sourceRoot, + forceInPlaceBuild, }) { const builders = {}; @@ -3004,7 +3003,8 @@ Find out more about Meteor at meteor.com. releaseName, previousBuilder: previousBuilders[name] || null, buildMode, - minifyMode + minifyMode, + forceInPlaceBuild }); builders[name] = targetBuilder; @@ -3083,6 +3083,10 @@ Find out more about Meteor at meteor.com. * - hasCachedBundle: true if we already have a cached bundle stored in * /build. When true, we only build the new client targets in the bundle. * + * - forceInPlaceBuild On Windows, in place builds are disabled by default + * since they are only safe when the output files from the previous build + * are not being used. This can be set to true when it is safe. + * * Returns an object with keys: * - errors: A buildmessage.MessageSet, or falsy if bundling succeeded. * - serverWatchSet: Information about server files and paths that were @@ -3117,6 +3121,7 @@ function bundle({ previousBuilders = Object.create(null), hasCachedBundle, allowDelayedClientBuilds = false, + forceInPlaceBuild, }) { buildOptions = buildOptions || {}; @@ -3271,6 +3276,7 @@ function bundle({ builtBy, releaseName, minifyMode, + forceInPlaceBuild, }; function writeClientTarget(target) { diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js index 7a62748f5a..b8b476d9c4 100644 --- a/tools/isobuild/compiler-plugin.js +++ b/tools/isobuild/compiler-plugin.js @@ -1714,7 +1714,7 @@ export class PackageSourceBatch { if (cacheFilename) { let diskCached = null; try { - diskCached = optimisticReadJsonOrNull(cacheFilename); + diskCached = files.readJSONOrNull(cacheFilename); } catch (e) { // Ignore JSON parse errors; pretend there was no cache. if (!(e instanceof SyntaxError)) { diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index 355ad9d0d5..e6c09760e2 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -25,6 +25,7 @@ import { convertToPosixPath, realpathOrNull, writeFileAtomically, + readFile, } from "../fs/files"; const { SourceNode, SourceMapConsumer } = require("source-map"); @@ -74,9 +75,18 @@ const reifyCompileWithCache = Profile("reifyCompileWithCache", wrap(function ( source, _hash, bundleArch, + cacheFilePath, ) { + if (cacheFilePath) { + try { + return readFile(cacheFilePath, "utf8"); + } catch (e) { + if (e.code !== "ENOENT") throw e; + } + } + const isLegacy = isLegacyArch(bundleArch); - return reifyCompile(stripHashBang(source), { + let result = reifyCompile(stripHashBang(source), { parse: reifyBabelParse, generateLetDeclarations: !isLegacy, avoidModernSyntax: isLegacy, @@ -84,6 +94,14 @@ const reifyCompileWithCache = Profile("reifyCompileWithCache", wrap(function ( dynamicImport: true, ast: false, }).code; + + if (cacheFilePath) { + Promise.resolve().then( + () => writeFileAtomically(cacheFilePath, result), + ); + } + + return result; }, { makeCacheKey(_source, hash, bundleArch) { return JSON.stringify([hash, bundleArch]); @@ -132,29 +150,16 @@ class DefaultHandlers { } } - if (this.cacheDir) { - const cacheFileName = this.getCacheFileName(file)!; - try { - return optimisticReadFile(cacheFileName, "utf8"); - } catch (e) { - if (e.code !== "ENOENT") throw e; - const code = reifyCompileWithCache( - file.dataString, - file.hash, - this.bundleArch, - ); - Promise.resolve().then( - () => writeFileAtomically(cacheFileName, code), - ); - return code; - } - } else { - return reifyCompileWithCache( - file.dataString, - file.hash, - this.bundleArch, - ); - } + const cacheFileName = this.cacheDir ? + this.getCacheFileName(file) : + null; + + return reifyCompileWithCache( + file.dataString, + file.hash, + this.bundleArch, + cacheFileName + ) } // Files with an .mjs extension are just JavaScript plus module syntax. diff --git a/tools/isobuild/package-source.js b/tools/isobuild/package-source.js index 27c332c746..253c5fda18 100644 --- a/tools/isobuild/package-source.js +++ b/tools/isobuild/package-source.js @@ -79,6 +79,9 @@ var loadOrderSort = function (sourceProcessorSet, arch) { }); return function (a, b) { + const aBasename = files.pathBasename(a); + const bBasename = files.pathBasename(b); + // XXX MODERATELY SIZED HACK -- // push template files ahead of everything else. this is // important because the user wants to be able to say @@ -87,15 +90,15 @@ var loadOrderSort = function (sourceProcessorSet, arch) { // before the corresponding .html file. // // maybe all of the templates should go in one file? - var isTemplate_a = isTemplate(files.pathBasename(a)); - var isTemplate_b = isTemplate(files.pathBasename(b)); + var isTemplate_a = isTemplate(aBasename); + var isTemplate_b = isTemplate(bBasename); if (isTemplate_a !== isTemplate_b) { return (isTemplate_a ? -1 : 1); } // main.* loaded last - var ismain_a = (files.pathBasename(a).indexOf('main.') === 0); - var ismain_b = (files.pathBasename(b).indexOf('main.') === 0); + var ismain_a = (aBasename.indexOf('main.') === 0); + var ismain_b = (bBasename.indexOf('main.') === 0); if (ismain_a !== ismain_b) { return (ismain_a ? 1 : -1); } @@ -213,14 +216,44 @@ var getExcerptFromReadme = function (text) { class SymlinkLoopChecker { constructor(sourceRoot) { this.sourceRoot = sourceRoot; + this._realSourceRoot = files.realpath(sourceRoot); this._seenPaths = {}; + this._cache = new Map(); } + // Avoids running realpath unless necessary + // since it is relatively slow on windows + _realpath = Profile('_realpath', function (relDir) { + const absPath = files.pathJoin(this._realSourceRoot, relDir); + + if (files.lstat(absPath).isSymbolicLink()) { + const result = files.realpath(absPath); + this._cache.set(relDir, result); + + return result; + } + + let result; + const parentDir = files.pathDirname(relDir); + const parentEntry = this._cache.get(parentDir); + if (parentDir === '.') { + result = absPath; + } else if (parentEntry) { + result = files.pathJoin(parentEntry, files.pathBasename(relDir)); + } else { + // The parent dir was never checked, which prevents us from + // skipping realpath + result = files.realpath(absPath); + } + + this._cache.set(relDir, result); + return result; + }) + check(relDir, quietly = true) { - const absPath = files.pathJoin(this.sourceRoot, relDir); try { - var realPath = files.realpath(absPath); + var realPath = this._realpath(relDir); } catch (e) { if (!e || e.code !== 'ELOOP') { throw e; @@ -834,10 +867,21 @@ _.extend(PackageSource.prototype, { }), _readAndWatchDirectory(relDir, watchSet, {include, exclude, names}) { - return watch.readAndWatchDirectory(watchSet, { + const options = { absPath: files.pathJoin(this.sourceRoot, relDir), include, exclude, names - }).map(name => files.pathJoin(relDir, name)); + }; + + const contents = watch.readDirectory(options); + + if (watchSet) { + watchSet.addDirectory({ + contents, + ...options + }); + } + + return contents.map(name => files.pathJoin(relDir, name)); }, // Initialize a package from an application directory (has .meteor/packages). @@ -1099,7 +1143,7 @@ _.extend(PackageSource.prototype, { // complete list of source files for directories within node_modules. _findSourcesCache: Object.create(null), - _findSources: Profile("PackageSource#_findSources", function ({ + _findSources: Profile(({ sourceArch }) => `PackageSource#_findSources for ${sourceArch.arch}`, function ({ sourceProcessorSet, watchSet, isApp, @@ -1216,9 +1260,10 @@ _.extend(PackageSource.prototype, { const baseCacheKey = JSON.stringify({ isApp, - arch, sourceRoot: self.sourceRoot, excludes: anyLevelExcludes, + names: sourceReadOptions.names, + include: sourceReadOptions.include }, (key, value) => { if (_.isRegExp(value)) { return [value.source, value.flags]; @@ -1260,13 +1305,13 @@ _.extend(PackageSource.prototype, { return array; } - function find(dir, depth, inNodeModules) { + function find(dir, depth, { inNodeModules = false, cache = false } = {}) { // Remove trailing slash. dir = dir.replace(/\/$/, ""); // If we're in a node_modules directory, cache the results of the // find function for the duration of the process. - let cacheKey = inNodeModules && makeCacheKey(dir); + let cacheKey = inNodeModules && cache && makeCacheKey(dir); if (cacheKey && cacheKey in self._findSourcesCache) { return self._findSourcesCache[cacheKey]; @@ -1305,13 +1350,16 @@ _.extend(PackageSource.prototype, { } const sources = _.difference( - self._readAndWatchDirectory(dir, watchSet, readOptions), + self._readAndWatchDirectory(dir, inNodeModules ? null : watchSet, readOptions), depth > 0 ? [] : controlFiles ); - const subdirectories = self._readAndWatchDirectory(dir, watchSet, { - include: [/\/$/], - exclude: depth > 0 + const subdirectories = self._readAndWatchDirectory( + dir, + inNodeModules ? null : watchSet, + { + include: [/\/$/], + exclude: depth > 0 ? anyLevelExcludes : topLevelExcludes }); @@ -1342,7 +1390,7 @@ _.extend(PackageSource.prototype, { } } else { - sources.push(...find(subdir, depth + 1, inNodeModules)); + sources.push(...find(subdir, depth + 1, { inNodeModules, cache: !inNodeModules })); } }); @@ -1353,7 +1401,7 @@ _.extend(PackageSource.prototype, { // subdirectories, continue searching this node_modules directory, // so that any non-.js(on) files it contains can be imported by // the app (#6037). - sources.push(...find(nodeModulesDir, depth + 1, true)); + sources.push(...find(nodeModulesDir, depth + 1, { inNodeModules: true, cache: !inNodeModules})); } delete dotMeteorIgnoreFiles[dir]; diff --git a/tools/runners/run-app.js b/tools/runners/run-app.js index 5354188cdb..c5aaf49ba3 100644 --- a/tools/runners/run-app.js +++ b/tools/runners/run-app.js @@ -579,6 +579,10 @@ _.extend(AppRunner.prototype, { // Permit delayed bundling of client architectures if the // console is interactive. allowDelayedClientBuilds: ! Console.isHeadless(), + + // None of the targets are used during full rebuilds + // so we can safely build in place on Windows + forceInPlaceBuild: !cachedServerWatchSet }); }); diff --git a/tools/runners/run-mongo.js b/tools/runners/run-mongo.js index 731b5863ee..ed4e7a3ff1 100644 --- a/tools/runners/run-mongo.js +++ b/tools/runners/run-mongo.js @@ -938,6 +938,10 @@ _.extend(MRp, { "Looks like you are out of free disk space under .meteor/local."; } else if (explanation) { message += "\n" + explanation.longText; + } else if (process.platform === 'win32') { + message += "\n\n" + + "Check how to troubleshoot here " + + "https://docs.meteor.com/windows.html#cant-start-mongo-server"; } if (explanation && explanation.symbol === 'EXIT_NET_ERROR') { diff --git a/tools/static-assets/skel-vue/.gitignore b/tools/static-assets/skel-vue/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/static-assets/skel-vue/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/static-assets/skel-vue/.meteor/.gitignore b/tools/static-assets/skel-vue/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/static-assets/skel-vue/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/static-assets/skel-vue/.meteor/packages b/tools/static-assets/skel-vue/.meteor/packages new file mode 100644 index 0000000000..83be6b3a62 --- /dev/null +++ b/tools/static-assets/skel-vue/.meteor/packages @@ -0,0 +1,24 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command + +tracker # Dependency tracker to allow reactive callbacks +static-html # Define static page content in .html files +akryum:vue-component # Vue-CLI template to publish components + +meteortesting:mocha # A package for writing and running your meteor app and package tests with mocha +johanbrook:publication-collector # Test a Meteor publication by collecting its output diff --git a/tools/static-assets/skel-vue/.meteor/platforms b/tools/static-assets/skel-vue/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/static-assets/skel-vue/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/static-assets/skel-vue/package.json b/tools/static-assets/skel-vue/package.json new file mode 100644 index 0000000000..97a009989c --- /dev/null +++ b/tools/static-assets/skel-vue/package.json @@ -0,0 +1,23 @@ +{ + "name": "skel", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.8.3", + "meteor-node-stubs": "^1.0.0", + "vue": "^2.6.11", + "vue-meteor-tracker": "^2.0.0-beta.5" + }, + "meteor": { + "mainModule": { + "client": "src/client.js", + "server": "src/server.js" + }, + "testModule": "tests/main.js" + } +} diff --git a/tools/static-assets/skel-vue/src/App.vue b/tools/static-assets/skel-vue/src/App.vue new file mode 100644 index 0000000000..e126098ccb --- /dev/null +++ b/tools/static-assets/skel-vue/src/App.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/tools/static-assets/skel-vue/src/client.js b/tools/static-assets/skel-vue/src/client.js new file mode 100644 index 0000000000..835acaec7b --- /dev/null +++ b/tools/static-assets/skel-vue/src/client.js @@ -0,0 +1,12 @@ +import Vue from 'vue' + +import './plugins' + +import App from './App.vue' + +Meteor.startup(() => { + new Vue({ + el: '#app', + ...App, + }) +}) diff --git a/tools/static-assets/skel-vue/src/collections/Links.js b/tools/static-assets/skel-vue/src/collections/Links.js new file mode 100644 index 0000000000..de6a43c4a4 --- /dev/null +++ b/tools/static-assets/skel-vue/src/collections/Links.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export default new Mongo.Collection('links'); diff --git a/tools/static-assets/skel-vue/src/collections/Links.tests.js b/tools/static-assets/skel-vue/src/collections/Links.tests.js new file mode 100644 index 0000000000..0bc5b437dd --- /dev/null +++ b/tools/static-assets/skel-vue/src/collections/Links.tests.js @@ -0,0 +1,24 @@ +// Tests for the behavior of the links collection +// +// https://guide.meteor.com/testing.html + +import { Meteor } from 'meteor/meteor'; +import { assert } from 'chai'; +import Links from './links.js'; + +if (Meteor.isServer) { + describe('links collection', function () { + it('insert correctly', function () { + const linkId = Links.insert({ + title: 'meteor homepage', + url: 'https://www.meteor.com', + }); + const added = Links.find({ _id: linkId }); + const collectionName = added._getCollectionName(); + const count = added.count(); + + assert.equal(collectionName, 'links'); + assert.equal(count, 1); + }); + }); +} diff --git a/tools/static-assets/skel-vue/src/components/Hello.vue b/tools/static-assets/skel-vue/src/components/Hello.vue new file mode 100644 index 0000000000..64947eb06a --- /dev/null +++ b/tools/static-assets/skel-vue/src/components/Hello.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/tools/static-assets/skel-vue/src/components/Info.vue b/tools/static-assets/skel-vue/src/components/Info.vue new file mode 100644 index 0000000000..f20fc4c800 --- /dev/null +++ b/tools/static-assets/skel-vue/src/components/Info.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/tools/static-assets/skel-vue/src/fixtures.js b/tools/static-assets/skel-vue/src/fixtures.js new file mode 100644 index 0000000000..f629f5ca0b --- /dev/null +++ b/tools/static-assets/skel-vue/src/fixtures.js @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; +import Links from './collections/Links.js'; + +Meteor.startup(() => { + // if the Links collection is empty + if (Links.find().count() === 0) { + const data = [ + { + title: 'Do the Tutorial', + url: 'https://www.meteor.com/try', + createdAt: new Date(), + }, + { + title: 'Follow the Guide', + url: 'http://guide.meteor.com', + createdAt: new Date(), + }, + { + title: 'Read the Docs', + url: 'https://docs.meteor.com', + createdAt: new Date(), + }, + { + title: 'Discussions', + url: 'https://forums.meteor.com', + createdAt: new Date(), + }, + ]; + + data.forEach(link => Links.insert(link)); + } +}); diff --git a/tools/static-assets/skel-vue/src/main.html b/tools/static-assets/skel-vue/src/main.html new file mode 100644 index 0000000000..99c3dfb74c --- /dev/null +++ b/tools/static-assets/skel-vue/src/main.html @@ -0,0 +1,7 @@ + + ~name~ + + + +
+ diff --git a/tools/static-assets/skel-vue/src/methods/createLink.js b/tools/static-assets/skel-vue/src/methods/createLink.js new file mode 100644 index 0000000000..876f710978 --- /dev/null +++ b/tools/static-assets/skel-vue/src/methods/createLink.js @@ -0,0 +1,16 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Links from '../collections/Links.js'; + +Meteor.methods({ + 'createLink'(title, url) { + check(url, String); + check(title, String); + + return Links.insert({ + url, + title, + createdAt: new Date(), + }); + }, +}); diff --git a/tools/static-assets/skel-vue/src/methods/createLink.tests.js b/tools/static-assets/skel-vue/src/methods/createLink.tests.js new file mode 100644 index 0000000000..d899a7338f --- /dev/null +++ b/tools/static-assets/skel-vue/src/methods/createLink.tests.js @@ -0,0 +1,20 @@ +import { Meteor } from 'meteor/meteor'; +import { assert } from 'chai'; +import Links from '../collections/Links.js'; +import './methods.js'; + +if (Meteor.isServer) { + describe('method: createLink', function () { + beforeEach(function () { + Links.remove({}); + }); + + it('can add a new link', function () { + const addLink = Meteor.server.method_handlers['createLink']; + + addLink.apply({}, ['meteor.com', 'https://www.meteor.com']); + + assert.equal(Links.find().count(), 1); + }); + }); +} diff --git a/tools/static-assets/skel-vue/src/methods/index.js b/tools/static-assets/skel-vue/src/methods/index.js new file mode 100644 index 0000000000..c52403a2e8 --- /dev/null +++ b/tools/static-assets/skel-vue/src/methods/index.js @@ -0,0 +1 @@ +import './createLink' diff --git a/tools/static-assets/skel-vue/src/plugins.js b/tools/static-assets/skel-vue/src/plugins.js new file mode 100644 index 0000000000..eb976c92e5 --- /dev/null +++ b/tools/static-assets/skel-vue/src/plugins.js @@ -0,0 +1,4 @@ +import Vue from 'vue' +import VueMeteorTracker from 'vue-meteor-tracker' + +Vue.use(VueMeteorTracker) diff --git a/tools/static-assets/skel-vue/src/publications/index.js b/tools/static-assets/skel-vue/src/publications/index.js new file mode 100644 index 0000000000..d161b580e7 --- /dev/null +++ b/tools/static-assets/skel-vue/src/publications/index.js @@ -0,0 +1 @@ +import './links' diff --git a/tools/static-assets/skel-vue/src/publications/links.js b/tools/static-assets/skel-vue/src/publications/links.js new file mode 100644 index 0000000000..f085d75045 --- /dev/null +++ b/tools/static-assets/skel-vue/src/publications/links.js @@ -0,0 +1,6 @@ +import { Meteor } from 'meteor/meteor'; +import Links from '../collections/Links.js'; + +Meteor.publish('links', function () { + return Links.find(); +}); diff --git a/tools/static-assets/skel-vue/src/publications/links.tests.js b/tools/static-assets/skel-vue/src/publications/links.tests.js new file mode 100644 index 0000000000..37f337c869 --- /dev/null +++ b/tools/static-assets/skel-vue/src/publications/links.tests.js @@ -0,0 +1,22 @@ +import { assert } from 'chai' +import { PublicationCollector } from 'meteor/johanbrook:publication-collector' +import Links from '../collections/Links.js' +import './publications.js' + +describe('Publish links', function () { + beforeEach(function () { + Links.remove({}) + Links.insert({ + title: 'meteor homepage', + url: 'https://www.meteor.com' + }) + }) + + it('sends all links', function (done) { + const collector = new PublicationCollector() + collector.collect('links', (collections) => { + assert.equal(collections.links.length, 1) + done() + }) + }) +}) diff --git a/tools/static-assets/skel-vue/src/server.js b/tools/static-assets/skel-vue/src/server.js new file mode 100644 index 0000000000..c48a837760 --- /dev/null +++ b/tools/static-assets/skel-vue/src/server.js @@ -0,0 +1,3 @@ +import './fixtures' +import './methods' +import './publications' diff --git a/tools/static-assets/skel-vue/tests/main.js b/tools/static-assets/skel-vue/tests/main.js new file mode 100644 index 0000000000..6d2a32e09d --- /dev/null +++ b/tools/static-assets/skel-vue/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("skel", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "skel"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +});