diff --git a/docs/history.md b/docs/history.md index 2f932f7780..cde5b8b41f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -8,6 +8,76 @@ [//]: # (go to meteor/docs/generators/changelog/docs) +## v3.3.2, 01-09-2025 + +### Highlights + +- Async-compatible account URLs and email-sending coverage [#13740](https://github.com/meteor/meteor/pull/13740) +- Move `findUserByEmail` method from `accounts-password` to `accounts-base` [#13859](https://github.com/meteor/meteor/pull/13859) +- Return `insertedId` on client `upsert` to match Meteor 2.x behavior [#13891](https://github.com/meteor/meteor/pull/13891) +- Unrecognized operator bug fixed [#13895](https://github.com/meteor/meteor/pull/13895) +- Security fix for `sha.js` [#13908](https://github.com/meteor/meteor/pull/13908) + + +All Merged PRs@[GitHub PRs 3.3.2](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3.2) + +#### Breaking Changes + +N/A + +##### Cordova Upgrade + +- Enable modern browser support for Cordova unless explicitly disabled [#13896](https://github.com/meteor/meteor/pull/13896) + +#### Internal API changes + +- lodash.template dependency was removed [#13898](https://github.com/meteor/meteor/pull/13898) + +#### Migration Steps + +Please run the following command to update your project: + +```bash +meteor update --release 3.3.2 +``` + +--- + +If you find any issues, please report them to the [Meteor issues tracker](https://github.com/meteor/meteor). + +#### Bumped Meteor Packages + +- accounts-base@3.1.2 +- accounts-password@3.2.1 +- accounts-passwordless@3.0.2 +- meteor-node-stubs@1.2.24 +- babel-compiler@7.12.2 +- boilerplate-generator@2.0.2 +- ecmascript@0.16.13 +- minifier@3.0.4 +- minimongo@2.0.4 +- mongo@2.1.4 +- coffeescript-compiler@2.4.3 +- npm-mongo@6.16.1 +- shell-server@0.6.2 +- typescript@5.6.6 + +#### Bumped NPM Packages + +- meteor-node-stubs@1.2.23 + +#### Special thanks to + +✨✨✨ + +- [@italojs](https://github.com/italojs) +- [@nachocodoner](https://github.com/nachocodoner) +- [@graemian](https://github.com/graemian) +- [@Grubba27](https://github.com/Grubba27) +- [@copleykj](https://github.com/copleykj) + +✨✨✨ + ## v3.3.1, 05-08-2025 ### Highlights diff --git a/meteor b/meteor index 09c92b4601..5c1ca3d768 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.18.0.20 +BUNDLE_VERSION=22.18.0.21 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/npm-packages/meteor-node-stubs/package-lock.json b/npm-packages/meteor-node-stubs/package-lock.json index a727d323f2..960afee14a 100644 --- a/npm-packages/meteor-node-stubs/package-lock.json +++ b/npm-packages/meteor-node-stubs/package-lock.json @@ -1,12 +1,12 @@ { "name": "meteor-node-stubs", - "version": "1.2.21", + "version": "1.2.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meteor-node-stubs", - "version": "1.2.13", + "version": "1.2.23", "bundleDependencies": [ "@meteorjs/crypto-browserify", "assert", @@ -41,7 +41,6 @@ "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "domain-browser": "^4.23.0", - "elliptic": "^6.6.1", "events": "^3.3.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", @@ -50,6 +49,7 @@ "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.2", + "sha.js": "^2.4.12", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", @@ -695,27 +695,6 @@ "dev": true, "license": "MIT" }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1646,17 +1625,24 @@ "license": "MIT" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "inBundle": true, "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { diff --git a/npm-packages/meteor-node-stubs/package.json b/npm-packages/meteor-node-stubs/package.json index 1468f2df9a..3e2867429e 100644 --- a/npm-packages/meteor-node-stubs/package.json +++ b/npm-packages/meteor-node-stubs/package.json @@ -2,7 +2,7 @@ "name": "meteor-node-stubs", "author": "Ben Newman ", "description": "Stub implementations of Node built-in modules, a la Browserify", - "version": "1.2.21", + "version": "1.2.24", "main": "index.js", "license": "MIT", "homepage": "https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-node-stubs/README.md", @@ -18,7 +18,6 @@ "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "domain-browser": "^4.23.0", - "elliptic": "^6.6.1", "events": "^3.3.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", @@ -27,6 +26,7 @@ "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.2", + "sha.js": "^2.4.12", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", @@ -81,5 +81,30 @@ ], "bugs": { "url": "https://github.com/meteor/node-stubs/issues" - } + }, + "bundleDependencies": [ + "@meteorjs/crypto-browserify", + "assert", + "browserify-zlib", + "buffer", + "console-browserify", + "constants-browserify", + "domain-browser", + "events", + "https-browserify", + "os-browserify", + "path-browserify", + "process", + "punycode", + "querystring-es3", + "readable-stream", + "stream-browserify", + "stream-http", + "string_decoder", + "timers-browserify", + "tty-browserify", + "url", + "util", + "vm-browserify" + ] } diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index b11344ce08..6aa2a58e2a 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -6,6 +6,7 @@ import { DDP } from 'meteor/ddp'; export interface URLS { resetPassword: (token: string) => string; verifyEmail: (token: string) => string; + loginToken: (token: string) => string; enrollAccount: (token: string) => string; } @@ -204,26 +205,43 @@ export namespace Accounts { options?: { fields?: Mongo.FieldSpecifier | undefined } ): Promise; + interface SendEmailOptions { + from: string; + to: string; + subject: string; + text: string; + html: string; + headers?: Header | undefined; + } + + interface SendEmailResult { + email: string; + user: Meteor.User; + token: string; + url: string; + options: SendEmailOptions; + } + function sendEnrollmentEmail( userId: string, email?: string, extraTokenData?: Record, extraParams?: Record - ): Promise; + ): Promise; function sendResetPasswordEmail( userId: string, email?: string, extraTokenData?: Record, extraParams?: Record - ): Promise; + ): Promise; function sendVerificationEmail( userId: string, email?: string, extraTokenData?: Record, extraParams?: Record - ): Promise; + ): Promise; function setUsername(userId: string, newUsername: string): Promise; diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index ed45af5a0f..6f0ba2098e 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -84,6 +84,11 @@ export class AccountsServer extends AccountsCommon { this._skipCaseInsensitiveChecksForTest = {}; + // Helper function to resolve promises if needed + this._resolvePromise = async (value) => { + return Meteor._isPromise(value) ? await value : value; + }; + this.urls = { resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams), verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), @@ -333,6 +338,32 @@ export class AccountsServer extends AccountsCommon { return user; } + /** + * @summary Find a user by one of their email addresses. + * @locus Server + * @param {String} email The email address to look for + * @param {Object} [options] + * @param {Object} options.fields Limit the fields to return from the user document + * @returns {Promise} A user if found, else null + * @memberof Accounts + * @importFromPackage accounts-base + */ + findUserByEmail = async (email, options) => + await this._findUserByQuery({ email }, options); + + /** + * @summary Find a user by their username. + * @locus Server + * @param {String} username The username to look for + * @param {Object} [options] + * @param {Object} options.fields Limit the fields to return from the user document + * @returns {Promise} A user if found, else null + * @memberof Accounts + * @importFromPackage accounts-base + */ + findUserByUsername = async (username, options) => + await this._findUserByQuery({ username }, options); + /// /// LOGIN METHODS /// diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index f6cdf49c71..26860656b1 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -775,8 +775,8 @@ if (Meteor.isServer) { }); }); - Tinytest.add( - 'accounts - make sure that extra params to accounts urls are added', + Tinytest.addAsync( + 'accounts - urls work with sync resolution', async test => { // No extra params const verifyEmailURL = new URL(Accounts.urls.verifyEmail('test')); @@ -790,6 +790,49 @@ if (Meteor.isServer) { test.equal(enrollAccountURL.searchParams.get('test'), extraParams.test); } ); + + Tinytest.addAsync( + 'accounts - urls work with async resolution', + async test => { + // Save original urls + const originalUrls = Accounts.urls; + try { + // Override urls methods to return Promises + Accounts.urls = { + resetPassword: (token, extraParams) => + new Promise(resolve => resolve(originalUrls.resetPassword(token, extraParams))), + verifyEmail: (token, extraParams) => + new Promise(resolve => resolve(originalUrls.verifyEmail(token, extraParams))), + loginToken: (selector, token, extraParams) => + new Promise(resolve => resolve(originalUrls.loginToken(selector, token, extraParams))), + enrollAccount: (token, extraParams) => + new Promise(resolve => resolve(originalUrls.enrollAccount(token, extraParams))), + }; + + // Test with no extra params + const verifyEmailUrl = await Accounts.urls.verifyEmail('test'); + const verifyEmailURL = new URL(verifyEmailUrl); + test.equal(verifyEmailURL.searchParams.toString(), ""); + + // Test with extra params + const extraParams = { test: 'async-success' }; + const resetPasswordUrl = await Accounts.urls.resetPassword('test', extraParams); + const resetPasswordURL = new URL(resetPasswordUrl); + test.equal(resetPasswordURL.searchParams.get('test'), extraParams.test); + + const enrollAccountUrl = await Accounts.urls.enrollAccount('test', extraParams); + const enrollAccountURL = new URL(enrollAccountUrl); + test.equal(enrollAccountURL.searchParams.get('test'), extraParams.test); + + const loginTokenUrl = await Accounts.urls.loginToken('email', 'token', extraParams); + const loginTokenURL = new URL(loginTokenUrl); + test.equal(loginTokenURL.searchParams.get('test'), extraParams.test); + } finally { + // Restore original urls + Accounts.urls = originalUrls; + } + } + ); } Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Facebook', async test => { diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 31711cdf33..4190011694 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: "3.1.1", + version: "3.1.2", }); Package.onUse((api) => { diff --git a/packages/accounts-base/server_main.js b/packages/accounts-base/server_main.js index dde1781f82..a9ed15c950 100644 --- a/packages/accounts-base/server_main.js +++ b/packages/accounts-base/server_main.js @@ -7,6 +7,7 @@ import { AccountsServer } from "./accounts_server.js"; Accounts = new AccountsServer(Meteor.server, { ...Meteor.settings.packages?.accounts, ...Meteor.settings.packages?.['accounts-base'] }); // TODO[FIBERS]: I need TLA Accounts.init().then(); + // Users table. Don't use the normal autopublish, since we want to hide // some fields. Code to autopublish this is in accounts_server.js. // XXX Allow users to configure this collection name. diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index 447bfbefc0..f41e5b8127 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: "3.2.0", + version: "3.2.1", }); Npm.depends({ diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 6477bdcc3d..151f9c8d5f 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -287,37 +287,6 @@ Accounts._checkPasswordAsync = checkPasswordAsync; /// -/** - * @summary Finds the user asynchronously with the specified username. - * First tries to match username case sensitively; if that fails, it - * tries case insensitively; but if more than one user matches the case - * insensitive search, it returns null. - * @locus Server - * @param {String} username The username to look for - * @param {Object} [options] - * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. - * @returns {Promise} A user if found, else null - * @importFromPackage accounts-base - */ -Accounts.findUserByUsername = - async (username, options) => - await Accounts._findUserByQuery({ username }, options); - -/** - * @summary Finds the user asynchronously with the specified email. - * First tries to match email case sensitively; if that fails, it - * tries case insensitively; but if more than one user matches the case - * insensitive search, it returns null. - * @locus Server - * @param {String} email The email address to look for - * @param {Object} [options] - * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. - * @returns {Promise} A user if found, else null - * @importFromPackage accounts-base - */ -Accounts.findUserByEmail = - async (email, options) => - await Accounts._findUserByQuery({ email }, options); // XXX maybe this belongs in the check package const NonEmptyString = Match.Where(x => { @@ -715,7 +684,7 @@ Accounts.sendResetPasswordEmail = async (userId, email, extraTokenData, extraParams) => { const { email: realEmail, user, token } = await Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData); - const url = Accounts.urls.resetPassword(token, extraParams); + const url = await Accounts._resolvePromise(Accounts.urls.resetPassword(token, extraParams)); const options = await Accounts.generateOptionsForEmail(realEmail, user, url, 'resetPassword'); await Email.sendAsync(options); @@ -749,7 +718,7 @@ Accounts.sendEnrollmentEmail = const { email: realEmail, user, token } = await Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData); - const url = Accounts.urls.enrollAccount(token, extraParams); + const url = await Accounts._resolvePromise(Accounts.urls.enrollAccount(token, extraParams)); const options = await Accounts.generateOptionsForEmail(realEmail, user, url, 'enrollAccount'); @@ -938,7 +907,7 @@ Accounts.sendVerificationEmail = const { email: realEmail, user, token } = await Accounts.generateVerificationToken(userId, email, extraTokenData); - const url = Accounts.urls.verifyEmail(token, extraParams); + const url = await Accounts._resolvePromise(Accounts.urls.verifyEmail(token, extraParams)); const options = await Accounts.generateOptionsForEmail(realEmail, user, url, 'verifyEmail'); await Email.sendAsync(options); if (Meteor.isDevelopment && !Meteor.isPackageTest) { @@ -1345,4 +1314,3 @@ await Meteor.users.createIndexAsync('services.password.reset.token', { unique: true, sparse: true }); await Meteor.users.createIndexAsync('services.password.enroll.token', { unique: true, sparse: true }); - diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index f34e172dc7..13f9927018 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1789,7 +1789,7 @@ if (Meteor.isServer) (() => { ]); }); - + Tinytest.addAsync("accounts emails - replace email", async test => { const origEmail = `originalemail@test.com`; @@ -1811,7 +1811,7 @@ Tinytest.addAsync("accounts emails - replace email", async test => { { address: newEmail, verified: false } ]); }) - + Tinytest.addAsync("passwords - remove email", async test => { const origEmail = `${ Random.id() }@turing.com`; @@ -1947,4 +1947,105 @@ Tinytest.addAsync("accounts emails - replace email", async test => { }); }, 'already exists'); }); + + Tinytest.addAsync('passwords - send email functions', async test => { + // Create a user with an unverified email + const username = Random.id(); + const email = `${username}-intercept@example.com`; + const password = 'password'; + + const userId = await Accounts.createUserAsync({ + username: username, + email: email, + password: password + }); + + test.isTrue(userId, 'User ID should be returned'); + + // Mock Email.sendAsync to track if it was called + const originalSendAsync = Email.sendAsync; + let emailSent = 0; + Email.sendAsync = async (options) => { + emailSent++; + return originalSendAsync(options); + }; + + try { + // Test sendVerificationEmail + const verificationResult = await Accounts.sendVerificationEmail(userId, email); + + // Verify the result contains expected properties + test.isTrue(verificationResult, 'Result should be returned for verification email'); + test.equal(verificationResult.email, email, 'Email in verification result should match'); + test.isTrue(verificationResult.user, 'User object should be in verification result'); + test.isTrue(verificationResult.token, 'Token should be in verification result'); + test.isTrue(verificationResult.url, 'URL should be in verification result'); + test.isTrue(verificationResult.options, 'Email options should be in verification result'); + + // Test sendEnrollmentEmail + const enrollmentResult = await Accounts.sendEnrollmentEmail(userId, email); + + // Verify the result contains expected properties + test.isTrue(enrollmentResult, 'Result should be returned for enrollment email'); + test.equal(enrollmentResult.email, email, 'Email in enrollment result should match'); + test.isTrue(enrollmentResult.user, 'User object should be in enrollment result'); + test.isTrue(enrollmentResult.token, 'Token should be in enrollment result'); + test.isTrue(enrollmentResult.url, 'URL should be in enrollment result'); + test.isTrue(enrollmentResult.options, 'Email options should be in enrollment result'); + + // Test sendResetPasswordEmail + const resetResult = await Accounts.sendResetPasswordEmail(userId, email); + + // Verify the result contains expected properties + test.isTrue(resetResult, 'Result should be returned for reset password email'); + test.equal(resetResult.email, email, 'Email in reset result should match'); + test.isTrue(resetResult.user, 'User object should be in reset result'); + test.isTrue(resetResult.token, 'Token should be in reset result'); + test.isTrue(resetResult.url, 'URL should be in reset result'); + test.isTrue(resetResult.options, 'Email options should be in reset result'); + + // Verify Email.sendAsync was called for all three emails + test.equal(emailSent, 3, 'Email.sendAsync should have been called three times'); + + // Get the intercepted emails + const interceptedEmails = await Meteor.callAsync("getInterceptedEmails", email); + test.equal(interceptedEmails.length, 3, 'Three emails should have been intercepted'); + + // Verify the verification email content + const verificationEmailOptions = interceptedEmails[0]; + test.isTrue(verificationEmailOptions, 'Verification email should have been intercepted'); + const verificationRe = new RegExp(`${Meteor.absoluteUrl()}#/verify-email/(\\S*)`); + const verificationMatch = verificationEmailOptions.text.match(verificationRe); + test.isTrue(verificationMatch, 'Verification email should contain verification URL'); + const verificationTokenFromUrl = verificationMatch[1]; + test.isTrue(verificationResult.url.includes(verificationTokenFromUrl), 'Verification URL in result should contain the token'); + + // Verify the enrollment email content + const enrollmentEmailOptions = interceptedEmails[1]; + test.isTrue(enrollmentEmailOptions, 'Enrollment email should have been intercepted'); + const enrollmentRe = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`); + const enrollmentMatch = enrollmentEmailOptions.text.match(enrollmentRe); + test.isTrue(enrollmentMatch, 'Enrollment email should contain enrollment URL'); + const enrollmentTokenFromUrl = enrollmentMatch[1]; + test.isTrue(enrollmentResult.url.includes(enrollmentTokenFromUrl), 'Enrollment URL in result should contain the token'); + + // Verify the reset password email content + const resetEmailOptions = interceptedEmails[2]; + test.isTrue(resetEmailOptions, 'Reset password email should have been intercepted'); + const resetRe = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`); + const resetMatch = resetEmailOptions.text.match(resetRe); + test.isTrue(resetMatch, 'Reset password email should contain reset URL'); + const resetTokenFromUrl = resetMatch[1]; + test.isTrue(resetResult.url.includes(resetTokenFromUrl), 'Reset URL in result should contain the token'); + + // Verify email headers and from address for all emails + for (const emailOptions of interceptedEmails) { + test.equal(emailOptions.from, 'test@meteor.com', 'From address should match'); + test.equal(emailOptions.headers['My-Custom-Header'], 'Cool', 'Custom header should be present'); + } + } finally { + // Restore the original Email.sendAsync + Email.sendAsync = originalSendAsync; + } + }); })(); diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index c9da46ee47..060171cd54 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'No-password login/sign-up support for accounts', - version: '3.0.1', + version: '3.0.2', }); Package.onUse(api => { diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 1ef908612c..20c23e17e8 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -220,7 +220,7 @@ Meteor.methods({ */ Accounts.sendLoginTokenEmail = async ({ userId, sequence, email, extra = {} }) => { const user = await getUserById(userId); - const url = Accounts.urls.loginToken(email, sequence, extra); + const url = await Accounts._resolvePromise(Accounts.urls.loginToken(email, sequence, extra)); const options = await Accounts.generateOptionsForEmail( email, user, diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index a09da565a2..860061818a 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -287,6 +287,8 @@ BCp.processOneFileForTarget = function (inputFile, source) { features.nodeMajorVersion = parseInt(process.versions.node, 10); } else if (arch === "web.browser") { features.modernBrowsers = true; + } else if (arch === "web.cordova") { + features.modernBrowsers = ! getMeteorConfig()?.cordova?.disableModern; } features.topLevelAwait = inputFile.supportsTopLevelAwait && diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 4fdba21272..2e69cdc028 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "babel-compiler", summary: "Parser/transpiler for ECMAScript 2015+ syntax", - version: '7.12.1', + version: '7.12.2', }); Npm.depends({ diff --git a/packages/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index b1bed1ae86..de1d8f8e40 100644 --- a/packages/boilerplate-generator/package.js +++ b/packages/boilerplate-generator/package.js @@ -1,11 +1,10 @@ Package.describe({ summary: "Generates the boilerplate html from program's manifest", - version: '2.0.1', + version: '2.0.2', }); Npm.depends({ - "combined-stream2": "1.1.2", - "lodash.template": "4.5.0" + "combined-stream2": "1.1.2" }); Package.onUse(api => { diff --git a/packages/boilerplate-generator/template.js b/packages/boilerplate-generator/template.js index 5f38837c42..f1656bf16e 100644 --- a/packages/boilerplate-generator/template.js +++ b/packages/boilerplate-generator/template.js @@ -1,14 +1,134 @@ -import lodashTemplate from 'lodash.template'; +/** + * Internal full-featured implementation of lodash.template (inspired by v4.5.0) + * embedded to eliminate the external dependency while preserving functionality. + * + * MIT License (c) JS Foundation and other contributors + * Adapted for Meteor boilerplate-generator (only the pieces required by template were extracted). + */ -// As identified in issue #9149, when an application overrides the default -// _.template settings using _.templateSettings, those new settings are -// used anywhere _.template is used, including within the -// boilerplate-generator. To handle this, _.template settings that have -// been verified to work are overridden here on each _.template call. -export default function template(text) { - return lodashTemplate(text, null, { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g, +// --------------------------------------------------------------------------- +// Utility & regex definitions (mirroring lodash pieces used by template) +// --------------------------------------------------------------------------- + +const reEmptyStringLeading = /\b__p \+= '';/g; +const reEmptyStringMiddle = /\b(__p \+=) '' \+/g; +const reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; + +const reEscape = /<%-([\s\S]+?)%>/g; // escape delimiter +const reEvaluate = /<%([\s\S]+?)%>/g; // evaluate delimiter +const reInterpolate = /<%=([\s\S]+?)%>/g; // interpolate delimiter +const reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; // ES6 template literal capture +const reUnescapedString = /['\\\n\r\u2028\u2029]/g; // string literal escapes + +// HTML escape +const htmlEscapes = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; +const reHasUnescapedHtml = /[&<>"']/; + +function escapeHtml(string) { + return string && reHasUnescapedHtml.test(string) + ? string.replace(/[&<>"']/g, chr => htmlEscapes[chr]) + : (string || ''); +} + +// Escape characters for inclusion into a string literal +const escapes = { "'": "'", '\\': '\\', '\n': 'n', '\r': 'r', '\u2028': 'u2028', '\u2029': 'u2029' }; +function escapeStringChar(match) { return '\\' + escapes[match]; } + +// Basic Object helpers ------------------------------------------------------ +function isObject(value) { return value != null && typeof value === 'object'; } +function toStringSafe(value) { return value == null ? '' : (value + ''); } +function baseValues(object, props) { return props.map(k => object[k]); } + + +function attempt(fn) { + try { return fn(); } catch (e) { return e; } +} +function isError(value) { return value instanceof Error || (isObject(value) && value.name === 'Error'); } + + +// --------------------------------------------------------------------------- +// Main template implementation +// --------------------------------------------------------------------------- +let templateCounter = -1; // used for sourceURL generation + +function _template(string) { + string = toStringSafe(string); + + const imports = { '_': { escape: escapeHtml } }; + const importKeys = Object.keys(imports); + const importValues = baseValues(imports, importKeys); + + let index = 0; + let isEscaping; + let isEvaluating; + let source = "__p += '"; + + + // Build combined regex of delimiters + const reDelimiters = RegExp( + reEscape.source + '|' + + reInterpolate.source + '|' + + reEsTemplate.source + '|' + + reEvaluate.source + '|$' + , 'g'); + + const sourceURL = `//# sourceURL=lodash.templateSources[${++templateCounter}]\n`; + + // Tokenize + string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) { + interpolateValue || (interpolateValue = esTemplateValue); + // Append preceding string portion with escaped literal chars + source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar); + if (escapeValue) { + isEscaping = true; + source += "' +\n__e(" + escapeValue + ") +\n'"; + } + if (evaluateValue) { + isEvaluating = true; + source += "';\n" + evaluateValue + ";\n__p += '"; + } + if (interpolateValue) { + source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; + } + index = offset + match.length; + return match; }); -}; \ No newline at end of file + + source += "';\n"; + + source = 'with (obj) {\n' + source + '\n}\n'; + + // Remove unnecessary concatenations + source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source) + .replace(reEmptyStringMiddle, '$1') + .replace(reEmptyStringTrailing, '$1;'); + + // Frame as function body + source = 'function(obj) {\n' + + 'obj || (obj = {});\n' + + "var __t, __p = ''" + + (isEscaping ? ', __e = _.escape' : '') + + (isEvaluating + ? ', __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, \'\') }\n' + : ';\n' + ) + + source + + 'return __p\n}'; + + // Actual compile step + const result = attempt(function() { + return Function(importKeys, sourceURL + 'return ' + source).apply(undefined, importValues); // eslint-disable-line no-new-func + }); + + if (isError(result)) { + result.source = source; // expose for debugging if error + throw result; + } + // Expose compiled source + result.source = source; + return result; +} + +export default function template(text) { + return _template(text); +} diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index 3236405da8..dcc86c7f0b 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.16.12', + version: '0.16.13', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/minifier-js/package.js b/packages/minifier-js/package.js index 22d44ba09e..788cf783b6 100644 --- a/packages/minifier-js/package.js +++ b/packages/minifier-js/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "JavaScript minifier", - version: '3.0.3', + version: '3.0.4', }); Npm.depends({ diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 590a6e9658..7d564ca564 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -678,6 +678,7 @@ export default class LocalCollection { return this.finishUpdate({ options, + insertedId, updateCount, callback, selector, diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 9f16b1c9a3..9801384815 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -58,3 +58,31 @@ Tinytest.add('minimongo - wrapTransform', test => { }); handle.stop(); }); + +if (Meteor.isClient) { + Tinytest.add('minimongo - $geoIntersects should throw error', function(test) { + const collection = new LocalCollection(); + collection.insert({ _id: 'a', loc: { type: 'Point', coordinates: [0, 0] } }); + + const query = { + loc: { + $geoIntersects: { + $geometry: { + type: 'Polygon', + coordinates: [ + [ + [0, 0], [0, 1], [1, 1], [1, 0], [0, 0] + ] + ] + } + } + } + }; + + test.throws( + () => collection.findOne(query), + /Unrecognized operator: \$geoIntersects/, + 'Should throw error for $geoIntersects in Minimongo' + ); + }); +} \ No newline at end of file diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index e2aab4032c..074705f33c 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -4010,3 +4010,55 @@ Tinytest.addAsync('minimongo - asyncIterator', async (test) => { test.equal(itemIds.length, 2); test.equal(itemIds, ['a', 'b']); }); + +Tinytest.add('minimongo - operation result fields (sync)', test => { + const c = new LocalCollection(); + + // Test insert + const insertedId = c.insert({name: 'doc1'}); + test.isTrue(insertedId !== undefined, 'insert should return an ID'); + + // Test update + const updateResult = c.update({name: 'doc1'}, {$set: {value: 1}}); + test.equal(updateResult, 1, 'update should return affected count'); + + // Test upsert (update case) + const upsertUpdateResult = c.upsert({name: 'doc1'}, {$set: {value: 2}}); + test.equal(upsertUpdateResult.numberAffected, 1); + test.isFalse(upsertUpdateResult.hasOwnProperty('insertedId')); + + // Test upsert (insert case) + const upsertInsertResult = c.upsert({name: 'doc2'}, {$set: {value: 3}}); + test.equal(upsertInsertResult.numberAffected, 1); + test.isTrue(upsertInsertResult.hasOwnProperty('insertedId')); + + // Test remove + const removeResult = c.remove({name: 'doc1'}); + test.equal(removeResult, 1, 'remove should return removed count'); +}); + +Tinytest.addAsync('minimongo - operation result fields (async)', async test => { + const c = new LocalCollection(); + + // Test insert + const insertedId = await c.insertAsync({name: 'doc1'}); + test.isTrue(insertedId !== undefined, 'insert should return an ID'); + + // Test update + const updateResult = await c.updateAsync({name: 'doc1'}, {$set: {value: 1}}); + test.equal(updateResult, 1, 'update should return affected count'); + + // Test upsert (update case) + const upsertUpdateResult = await c.upsertAsync({name: 'doc1'}, {$set: {value: 2}}); + test.equal(upsertUpdateResult.numberAffected, 1); + test.isFalse(upsertUpdateResult.hasOwnProperty('insertedId')); + + // Test upsert (insert case) + const upsertInsertResult = await c.upsertAsync({name: 'doc2'}, {$set: {value: 3}}); + test.equal(upsertInsertResult.numberAffected, 1); + test.isTrue(upsertInsertResult.hasOwnProperty('insertedId')); + + // Test remove + const removeResult = await c.removeAsync({name: 'doc1'}); + test.equal(removeResult, 1, 'remove should return removed count'); +}); diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index d29b90c5ac..b3ff2000be 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", - version: "2.0.3", + version: "2.0.4", }); Package.onUse((api) => { diff --git a/packages/mongo/mongo_connection.js b/packages/mongo/mongo_connection.js index b093bcfa00..c0aace0973 100644 --- a/packages/mongo/mongo_connection.js +++ b/packages/mongo/mongo_connection.js @@ -850,6 +850,7 @@ Object.assign(MongoConnection.prototype, { const oplogOptions = self?._oplogHandle?._oplogOptions || {}; const { includeCollections, excludeCollections } = oplogOptions; if (firstHandle) { + var matcher, sorter; var canUseOplog = [ function () { @@ -887,7 +888,7 @@ Object.assign(MongoConnection.prototype, { } catch (e) { // XXX make all compilation errors MinimongoError or something // so that this doesn't ignore unrelated exceptions - if (e instanceof MiniMongoQueryError) { + if (Meteor.isClient && e instanceof MiniMongoQueryError) { throw e; } return false; diff --git a/packages/mongo/observe_multiplex.ts b/packages/mongo/observe_multiplex.ts index a99d64d562..1aec5ede95 100644 --- a/packages/mongo/observe_multiplex.ts +++ b/packages/mongo/observe_multiplex.ts @@ -1,12 +1,17 @@ -import isEmpty from 'lodash.isempty'; -import { ObserveHandle } from './observe_handle'; +import isEmpty from "lodash.isempty"; +import { ObserveHandle } from "./observe_handle"; interface ObserveMultiplexerOptions { ordered: boolean; onStop?: () => void; } -export type ObserveHandleCallback = 'added' | 'addedBefore' | 'changed' | 'movedBefore' | 'removed'; +export type ObserveHandleCallback = + | "added" + | "addedBefore" + | "changed" + | "movedBefore" + | "removed"; /** * Allows multiple identical ObserveHandles to be driven by a single observe driver. @@ -29,8 +34,12 @@ export class ObserveMultiplexer { if (ordered === undefined) throw Error("must specify ordered"); // @ts-ignore - Package['facts-base'] && Package['facts-base'] - .Facts.incrementServerFact("mongo-livedata", "observe-multiplexers", 1); + Package["facts-base"] && + Package["facts-base"].Facts.incrementServerFact( + "mongo-livedata", + "observe-multiplexers", + 1 + ); this._ordered = ordered; this._onStop = onStop; @@ -38,12 +47,14 @@ export class ObserveMultiplexer { this._handles = {}; this._resolver = null; this._isReady = false; - this._readyPromise = new Promise(r => this._resolver = r).then(() => this._isReady = true); + this._readyPromise = new Promise((r) => (this._resolver = r)).then( + () => (this._isReady = true) + ); // @ts-ignore this._cache = new LocalCollection._CachingChangeObserver({ ordered }); this._addHandleTasksScheduledButNotPerformed = 0; - this.callbackNames().forEach(callbackName => { + this.callbackNames().forEach((callbackName) => { (this as any)[callbackName] = (...args: any[]) => { this._applyCallback(callbackName, args); }; @@ -58,14 +69,19 @@ export class ObserveMultiplexer { ++this._addHandleTasksScheduledButNotPerformed; // @ts-ignore - Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact( - "mongo-livedata", "observe-handles", 1); + Package["facts-base"] && + Package["facts-base"].Facts.incrementServerFact( + "mongo-livedata", + "observe-handles", + 1 + ); await this._queue.runTask(async () => { this._handles![handle._id] = handle; await this._sendAdds(handle); --this._addHandleTasksScheduledButNotPerformed; }); + await this._readyPromise; } @@ -76,11 +92,17 @@ export class ObserveMultiplexer { delete this._handles![id]; // @ts-ignore - Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact( - "mongo-livedata", "observe-handles", -1); + Package["facts-base"] && + Package["facts-base"].Facts.incrementServerFact( + "mongo-livedata", + "observe-handles", + -1 + ); - if (isEmpty(this._handles) && - this._addHandleTasksScheduledButNotPerformed === 0) { + if ( + isEmpty(this._handles) && + this._addHandleTasksScheduledButNotPerformed === 0 + ) { await this._stop(); } } @@ -92,8 +114,12 @@ export class ObserveMultiplexer { await this._onStop(); // @ts-ignore - Package['facts-base'] && Package['facts-base'] - .Facts.incrementServerFact("mongo-livedata", "observe-multiplexers", -1); + Package["facts-base"] && + Package["facts-base"].Facts.incrementServerFact( + "mongo-livedata", + "observe-multiplexers", + -1 + ); this._handles = null; } @@ -144,8 +170,11 @@ export class ObserveMultiplexer { if (!this._handles) return; await this._cache.applyChange[callbackName].apply(null, args); - if (!this._ready() && - (callbackName !== 'added' && callbackName !== 'addedBefore')) { + if ( + !this._ready() && + callbackName !== "added" && + callbackName !== "addedBefore" + ) { throw new Error(`Got ${callbackName} during initial adds`); } @@ -158,10 +187,20 @@ export class ObserveMultiplexer { if (!callback) continue; - handle.initialAddsSent.then(callback.apply( + const result = callback.apply( null, handle.nonMutatingCallbacks ? args : EJSON.clone(args) - )) + ); + + if (result && Meteor._isPromise(result)) { + result.catch((error) => { + console.error( + `Error in observeChanges callback ${callbackName}:`, + error + ); + }); + } + handle.initialAddsSent.then(result); } }); } @@ -170,24 +209,38 @@ export class ObserveMultiplexer { const add = this._ordered ? handle._addedBefore : handle._added; if (!add) return; - const addPromises: Promise[] = []; + const addPromises: (Promise | void)[] = []; + // note: docs may be an _IdMap or an OrderedDict this._cache.docs.forEach((doc: any, id: string) => { if (!(handle._id in this._handles!)) { throw Error("handle got removed before sending initial adds!"); } - const { _id, ...fields } = handle.nonMutatingCallbacks ? doc : EJSON.clone(doc); + const { _id, ...fields } = handle.nonMutatingCallbacks + ? doc + : EJSON.clone(doc); - const promise = this._ordered ? - add(id, fields, null) : - add(id, fields); + const promise = new Promise((resolve, reject) => { + try { + const r = this._ordered ? add(id, fields, null) : add(id, fields); + resolve(r); + } catch (error) { + reject(error); + } + }); addPromises.push(promise); }); - await Promise.all(addPromises); + await Promise.allSettled(addPromises).then((p) => { + p.forEach((result) => { + if (result.status === "rejected") { + console.error(`Error in adds for handle: ${result.reason}`); + } + }); + }); handle.initialAddsSentResolver(); } -} \ No newline at end of file +} diff --git a/packages/mongo/package.js b/packages/mongo/package.js index de5ab8e6f6..c0ff30242f 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -9,7 +9,7 @@ Package.describe({ summary: "Adaptor for using MongoDB and Minimongo over DDP", - version: "2.1.3", + version: "2.1.4", }); Npm.depends({ diff --git a/packages/mongo/tests/mongo_livedata_tests.js b/packages/mongo/tests/mongo_livedata_tests.js index 0578665f80..af69aee7b4 100644 --- a/packages/mongo/tests/mongo_livedata_tests.js +++ b/packages/mongo/tests/mongo_livedata_tests.js @@ -4489,6 +4489,49 @@ testAsyncMulti( ); +Meteor.isServer && testAsyncMulti( + "mongo-livedata - observeChangesAsync callback errors should not crash the process", + [ + async (test) => { + const Collection = new Mongo.Collection( + `observe_changes_async_error_async_method${test.runId()}`, + { resolverType: 'stub' } + ); + + let insertId; + await Collection.find({}).observeChangesAsync({ + async added(_id, fields) { + insertId = _id; + throw new Error('Test error in async added observeChangesAsync'); + }, + }); + + return Collection.insertAsync({ foo: { bar: 123 } }).finally((id, bad) => { + test.equal(insertId, id); + }) + }, + + async (test) => { + const Collection = new Mongo.Collection( + `observe_changes_async_error_sync_method${test.runId()}`, + { resolverType: 'stub' } + ); + + let insertId; + await Collection.find({}).observeChangesAsync({ + added(id) { + insertId = _id; + throw new Error('Test error in sync added observeChangesAsync'); + }, + }); + + return Collection.insertAsync({ foo: { bar: 123 } }).finally((id, bad) => { + test.equal(insertId, id); + }) + } + ] +); + Meteor.methods({ [`methodThrowException`]: async () => { if (Meteor.isClient) { @@ -4516,3 +4559,60 @@ Tinytest.addAsync( } }, ); + +const geoPolygonSchema = { + type: 'Polygon', + coordinates: Match.Where(coords => + Array.isArray(coords) && + coords.length > 0 && + coords.every( + ring => Array.isArray(ring) && ring.every( + point => Array.isArray(point) && point.length === 2 && + typeof point[0] === 'number' && typeof point[1] === 'number' + ) + ) + ) +}; + +Tinytest.addAsync('mongo-livedata - publish with $geoIntersects returns correct docs', async function(test, onComplete) { + if (Meteor.isServer) { + const Features = new Mongo.Collection('Features_' + Random.id()); + const insidePoly = { + _id: 'inside', + hull: { + type: 'Polygon', + coordinates: [ + [ [0.2,0.2], [0.2,0.8], [0.8,0.8], [0.8,0.2], [0.2,0.2] ] + ] + } + }; + const outsidePoly = { + _id: 'outside', + hull: { + type: 'Polygon', + coordinates: [ + [ [2,2], [2,3], [3,3], [3,2], [2,2] ] + ] + } + }; + await Features.insertAsync(insidePoly); + await Features.insertAsync(outsidePoly); + + const viewport = { + bounds: { + type: 'Polygon', + coordinates: [ + [ [0,0], [0,1], [1,1], [1,0], [0,0] ] + ] + } + }; + const cursor = Features.find({ hull: { $geoIntersects: { $geometry: viewport.bounds } } }); + const docs = await cursor.fetchAsync(); + test.equal(docs.length, 1); + test.equal(docs[0]._id, 'inside'); + onComplete(); + } + if (Meteor.isClient) { + onComplete(); + } +}); \ No newline at end of file diff --git a/packages/mongo/tests/observe_changes_tests.js b/packages/mongo/tests/observe_changes_tests.js index 0fe94d2ebf..8dd50eed5d 100644 --- a/packages/mongo/tests/observe_changes_tests.js +++ b/packages/mongo/tests/observe_changes_tests.js @@ -545,6 +545,7 @@ if (Meteor.isServer) { } +// Those errors should throw since mongo doent support {$in: null} testAsyncMulti("observeChanges - bad query", [ async function (test, expect) { var c = makeCollection(); @@ -563,15 +564,9 @@ testAsyncMulti("observeChanges - bad query", [ return; } - const p1 = new Promise(r => { - observeThrows().finally(() => r()); - }); - const p2 = new Promise(r => { - observeThrows().finally(() => r()); - }); - - await p1; - await p2; + await test.throwsAsync(async function () { + await c.find({__id: {$in: null}}).countAsync(); + }, '$in needs an array'); } ]); diff --git a/packages/non-core/coffeescript-compiler/package.js b/packages/non-core/coffeescript-compiler/package.js index c0ff2e50d8..8899f67cf7 100644 --- a/packages/non-core/coffeescript-compiler/package.js +++ b/packages/non-core/coffeescript-compiler/package.js @@ -3,7 +3,7 @@ Package.describe({ summary: 'Compiler for CoffeeScript code, supporting the coffeescript package', // This version of NPM `coffeescript` module, with _1, _2 etc. // If you change this, make sure to also update ../coffeescript/package.js to match. - version: '2.4.2', + version: '2.4.3', }); Npm.depends({ diff --git a/packages/npm-mongo/package.js b/packages/npm-mongo/package.js index be29b54a9a..2c99201181 100644 --- a/packages/npm-mongo/package.js +++ b/packages/npm-mongo/package.js @@ -3,7 +3,7 @@ Package.describe({ summary: "Wrapper around the mongo npm package", - version: "6.16.0", + version: "6.16.1", documentation: null, }); diff --git a/packages/npm-mongo/wrapper.js b/packages/npm-mongo/wrapper.js index fd01b4fd0d..983b5b3868 100644 --- a/packages/npm-mongo/wrapper.js +++ b/packages/npm-mongo/wrapper.js @@ -1,9 +1,10 @@ -const { MongoClient, MongoCompatibilityError } = Npm.require('mongodb'); +const { MongoClient } = Npm.require('mongodb'); function connect(client) { return client.connect() .catch(error => { - if (error.cause instanceof MongoCompatibilityError && error.message.includes('maximum wire version')) { + // we just check the message since multiples errors can be catch this situation, e.g: instanceof MongoServerSelectionError or MongoCompatibilityError + if (error.message.includes('maximum wire version')) { console.warn(`[DEPRECATION] Legacy MongoDB version detected, using mongo-legacy package: ${error.message} Warning: MongoDB versions <= 3.6 are deprecated. Some Meteor features may not work properly with this version. It is recommended to use MongoDB >= 4.`); diff --git a/packages/shell-server/package.js b/packages/shell-server/package.js index 3bdcb189ed..1dd7551714 100644 --- a/packages/shell-server/package.js +++ b/packages/shell-server/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "shell-server", - version: '0.6.1', + version: '0.6.2', summary: "Server-side component of the `meteor shell` command.", documentation: "README.md" }); diff --git a/packages/typescript/package.js b/packages/typescript/package.js index 2f4d514837..2a4372edf4 100644 --- a/packages/typescript/package.js +++ b/packages/typescript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'typescript', - version: '5.6.5', + version: '5.6.6', summary: 'Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files', documentation: 'README.md', diff --git a/scripts/admin/meteor-release-official.json b/scripts/admin/meteor-release-official.json index 3b26432a73..7b89d5c0a6 100644 --- a/scripts/admin/meteor-release-official.json +++ b/scripts/admin/meteor-release-official.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "3.3.1", + "version": "3.3.2", "recommended": false, "official": true, "description": "The Official Meteor Distribution" diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index a77b391737..78e895ebbd 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -72,10 +72,6 @@ export default defineConfig({ text: "Packages on Atmosphere", link: "https://atmospherejs.com/", }, - { - text: "VS Code Extension", - link: "https://marketplace.visualstudio.com/items?itemName=meteor-toolbox.meteor-toolbox", - }, { text: "DevTools - Chrome Extension", link: "https://chromewebstore.google.com/detail/ibniinmoafhgbifjojidlagmggecmpgf", diff --git a/v3-docs/docs/about/cordova.md b/v3-docs/docs/about/cordova.md index b6beabbe2b..1b698a4b6f 100644 --- a/v3-docs/docs/about/cordova.md +++ b/v3-docs/docs/about/cordova.md @@ -279,3 +279,19 @@ After building your Cordova project with Meteor, you can use **Android Studio** 5. Go to **Product > Archive** to create an archive of your app 6. In the **Organizer** window, click **Distribute App** and follow the prompts to configure signing and export the IPA file. 7. Upload the IPA file to the App Store or distribute via TestFlight. + +# Legacy device support + +Meteor distinguishes between legacy and modern browsers - see the [modern browsers package](../packages/modern-browsers). Web apps include different code bundles for each, but Cordova apps only have a single code bundle. From Meteor 3.3.2 onwards, the default code bundle changed from legacy to modern. + +You can force Meteor to use the legacy browser code bundle by setting the variable `cordova.disableModern` to `true` in `package.json` when running or building your app. For example: + +``` + "meteor": { + "mainModule": { ... }, + "testModule": { ... }, + "cordova": { "disableModern": true} + } +``` + +Both the App Store and Google Play will only publish new and updated apps for a certain minimum mobile OS version. As of 2025, these minimum OS versions support the modern browser code bundle. diff --git a/v3-docs/docs/about/what-is.md b/v3-docs/docs/about/what-is.md index 11863a0e2b..b87832fa90 100644 --- a/v3-docs/docs/about/what-is.md +++ b/v3-docs/docs/about/what-is.md @@ -39,8 +39,6 @@ Meteor is a full-stack JavaScript platform for developing modern web and mobile - Explore and contribute to our [GitHub repository](https://github.com/meteor). You can access our code, request new features, and start contributing. -- Enhance your coding experience with the [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=meteor-toolbox.meteor-toolbox), which provides IntelliSense support for Meteor's core and packages. - - Use the [Chrome Extension](https://chrome.google.com/webstore/detail/meteor-devtools-evolved/ibniinmoafhgbifjojidlagmggecmpgf) or [Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/meteor-devtools-evolved/) for insights into your app's performance and to understand what is going on under the hood of your application. - Discover [Meteor Examples](https://github.com/meteor/examples) to see a range of projects built with Meteor. diff --git a/v3-docs/docs/generators/changelog/versions/3.3.2.md b/v3-docs/docs/generators/changelog/versions/3.3.2.md new file mode 100644 index 0000000000..eeffe6468e --- /dev/null +++ b/v3-docs/docs/generators/changelog/versions/3.3.2.md @@ -0,0 +1,69 @@ +## v3.3.2, 01-09-2025 + +### Highlights + +- Async-compatible account URLs and email-sending coverage [#13740](https://github.com/meteor/meteor/pull/13740) +- Move `findUserByEmail` method from `accounts-password` to `accounts-base` [#13859](https://github.com/meteor/meteor/pull/13859) +- Return `insertedId` on client `upsert` to match Meteor 2.x behavior [#13891](https://github.com/meteor/meteor/pull/13891) +- Unrecognized operator bug fixed [#13895](https://github.com/meteor/meteor/pull/13895) +- Security fix for `sha.js` [#13908](https://github.com/meteor/meteor/pull/13908) + + +All Merged PRs@[GitHub PRs 3.3.2](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3.2) + +#### Breaking Changes + +N/A + +##### Cordova Upgrade + +- Enable modern browser support for Cordova unless explicitly disabled [#13896](https://github.com/meteor/meteor/pull/13896) + +#### Internal API changes + +- lodash.template dependency was removed [#13898](https://github.com/meteor/meteor/pull/13898) + +#### Migration Steps + +Please run the following command to update your project: + +```bash +meteor update --release 3.3.2 +``` + +--- + +If you find any issues, please report them to the [Meteor issues tracker](https://github.com/meteor/meteor). + +#### Bumped Meteor Packages + +- accounts-base@3.1.2 +- accounts-password@3.2.1 +- accounts-passwordless@3.0.2 +- meteor-node-stubs@1.2.24 +- babel-compiler@7.12.2 +- boilerplate-generator@2.0.2 +- ecmascript@0.16.13 +- minifier@3.0.4 +- minimongo@2.0.4 +- mongo@2.1.4 +- coffeescript-compiler@2.4.3 +- npm-mongo@6.16.1 +- shell-server@0.6.2 +- typescript@5.6.6 + +#### Bumped NPM Packages + +- meteor-node-stubs@1.2.23 + +#### Special thanks to + +✨✨✨ + +- [@italojs](https://github.com/italojs) +- [@nachocodoner](https://github.com/nachocodoner) +- [@graemian](https://github.com/graemian) +- [@Grubba27](https://github.com/Grubba27) +- [@copleykj](https://github.com/copleykj) + +✨✨✨ \ No newline at end of file diff --git a/v3-docs/docs/tutorials/react/index.md b/v3-docs/docs/tutorials/react/index.md index 1599e2d186..ef62761bb1 100644 --- a/v3-docs/docs/tutorials/react/index.md +++ b/v3-docs/docs/tutorials/react/index.md @@ -4,7 +4,7 @@ In this tutorial, we will create a simple To-Do app using [React](https://react. React is a popular JavaScript library for building user interfaces. It allows you to create dynamic and interactive applications by composing UI components. React uses a declarative approach, where you define how the UI should look based on the state, and it efficiently updates the view when the state changes. With JSX, a syntax extension that combines JavaScript and HTML, React makes it easy to create reusable components that manage their own state and render seamlessly in the browser. -To start building your React app, you'll need a code editor. If you're unsure which one to choose, [Visual Studio Code](https://code.visualstudio.com/) is a good option. After installing it, you can enhance your experience by adding extensions like [Meteor Toolbox](https://marketplace.visualstudio.com/items?itemName=meteor-toolbox.meteor-toolbox). +To start building your React app, you'll need a code editor. If you're unsure which one to choose, [Visual Studio Code](https://code.visualstudio.com/) is a good option. Let’s begin building your app! diff --git a/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md b/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md index 171d56d105..5b426a8599 100644 --- a/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md +++ b/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md @@ -4,8 +4,7 @@ In this tutorial, we will create a simple To-Do app using [Vue 3](https://vuejs. Vue.js is a powerful JavaScript framework for making user interfaces. It helps you build interactive applications by using templates that connect to data and update automatically when the data changes. Vue.js templates use a simple syntax similar to HTML and work with Vue’s reactivity system to show components in the browser. -To start building your Vue.js app, you'll need a code editor. If you're unsure which one to choose, [Visual Studio Code](https://code.visualstudio.com/) is a good option. After installing it, you can enhance your experience by adding extensions like [Meteor Toolbox](https://marketplace.visualstudio.com/items?itemName=meteor-toolbox.meteor-toolbox) and [Vue Language Features](https://marketplace.visualstudio.com/items?itemName=Vue.volar). - +To start building your Vue.js app, you'll need a code editor. If you're unsure which one to choose, [Visual Studio Code](https://code.visualstudio.com/) is a good option. :::warning This tutorial uses the `vue-meteor-tracker` package, which is currently in beta and does not support async calls yet. However, it is still a valuable package, and we hope it will be updated soon. We are also working on a new tutorial that will use Meteor core packages instead. ::: @@ -206,7 +205,7 @@ Before creating our collection, let's remove the `links.js` file from the `impor ::: code-group ```javascript [imports/api/tasksCollection.js] import { Mongo } from 'meteor/mongo'; - + export const TasksCollection = new Mongo.Collection('tasks'); ``` ::: @@ -259,7 +258,7 @@ Meteor works with Meteor packages and NPM packages, usually Meteor packages are The `vue-meteor-tracker` package is already included in the Vue skeleton, so you don’t need to add it. -When importing code from a Meteor package the only difference from NPM modules is that you need to prepend `meteor/` in the from part of your import. +When importing code from a Meteor package the only difference from NPM modules is that you need to prepend `meteor/` in the from part of your import. First we need to implement a subscription at the `App` component to get the tasks updated from the server. It can be done simply by using the `subscribe` and `autorun` functions from `vue-meteor-tracker`. ::: info @@ -500,7 +499,7 @@ Until now, you have only inserted documents to our collection. Let’s see how y ### 4.1: Add Checkbox -First, you need to add a `checkbox` element to your `Task` component, and we need to add the `v-model` directive to the checkbox. This will allow us to bind the value of the checkbox to the `checked` field of the task document. +First, you need to add a `checkbox` element to your `Task` component, and we need to add the `v-model` directive to the checkbox. This will allow us to bind the value of the checkbox to the `checked` field of the task document. To do this, we need to add a `ref` to the task document. This will allow us to access the task document in the template. And add a computed property `isChecked` for the state management of the checkbox. We also have a prop called `task` that is passed to the component. This prop is an object that represents the task document. @@ -604,7 +603,7 @@ const isChecked = computed(() => taskRef.value.checked); const handleCheckboxChange = async (event) => { const newCheckedValue = event.target.checked; taskRef.value.checked = newCheckedValue; - + try { await Meteor.callAsync('setIsCheckedTask', taskRef.value._id, newCheckedValue); } catch (error) { @@ -644,9 +643,9 @@ First add a button after the text in your `Task` component and receive a callbac {{ task.text }} - ... ``` @@ -829,7 +828,7 @@ You should avoid adding zero to your app bar when there are no pending tasks. ::: code-group ```vue [imports/ui/App.vue]