Merge branch 'modern-bundler-integration' of https://github.com/meteor/meteor into modern-bundler-integration

This commit is contained in:
Nacho Codoñer
2025-09-01 23:15:13 +02:00
41 changed files with 837 additions and 164 deletions

View File

@@ -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

2
meteor
View File

@@ -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.

View File

@@ -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": {

View File

@@ -2,7 +2,7 @@
"name": "meteor-node-stubs",
"author": "Ben Newman <ben@meteor.com>",
"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"
]
}

View File

@@ -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<Meteor.User | null | undefined>;
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<string, unknown>,
extraParams?: Record<string, unknown>
): Promise<void>;
): Promise<SendEmailResult>;
function sendResetPasswordEmail(
userId: string,
email?: string,
extraTokenData?: Record<string, unknown>,
extraParams?: Record<string, unknown>
): Promise<void>;
): Promise<SendEmailResult>;
function sendVerificationEmail(
userId: string,
email?: string,
extraTokenData?: Record<string, unknown>,
extraParams?: Record<string, unknown>
): Promise<void>;
): Promise<SendEmailResult>;
function setUsername(userId: string, newUsername: string): Promise<void>;

View File

@@ -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<Object>} 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<Object>} A user if found, else null
* @memberof Accounts
* @importFromPackage accounts-base
*/
findUserByUsername = async (username, options) =>
await this._findUserByQuery({ username }, options);
///
/// LOGIN METHODS
///

View File

@@ -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 => {

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "A user account system",
version: "3.1.1",
version: "3.1.2",
});
Package.onUse((api) => {

View File

@@ -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.

View File

@@ -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({

View File

@@ -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<Object>} 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<Object>} 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 });

View File

@@ -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;
}
});
})();

View File

@@ -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 => {

View File

@@ -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,

View File

@@ -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 &&

View File

@@ -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({

View File

@@ -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 => {

View File

@@ -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 <https://js.foundation/>
* 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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;
});
};
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);
}

View File

@@ -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',
});

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "JavaScript minifier",
version: '3.0.3',
version: '3.0.4',
});
Npm.depends({

View File

@@ -678,6 +678,7 @@ export default class LocalCollection {
return this.finishUpdate({
options,
insertedId,
updateCount,
callback,
selector,

View File

@@ -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'
);
});
}

View File

@@ -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');
});

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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<void>[] = [];
const addPromises: (Promise<void> | 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<void>((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();
}
}
}

View File

@@ -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({

View File

@@ -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();
}
});

View File

@@ -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');
}
]);

View File

@@ -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({

View File

@@ -3,7 +3,7 @@
Package.describe({
summary: "Wrapper around the mongo npm package",
version: "6.16.0",
version: "6.16.1",
documentation: null,
});

View File

@@ -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.`);

View File

@@ -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"
});

View File

@@ -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',

View File

@@ -1,6 +1,6 @@
{
"track": "METEOR",
"version": "3.3.1",
"version": "3.3.2",
"recommended": false,
"official": true,
"description": "The Official Meteor Distribution"

View File

@@ -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",

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)
✨✨✨

View File

@@ -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.
Lets begin building your app!

View File

@@ -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 Vues 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 dont 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. Lets 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 }}
</span>
<button
<button
class="ml-auto bg-red-500 hover:bg-red-600 text-white font-bold py-0.5 px-2 rounded"
@click="deleteTask"> &times;
@click="deleteTask"> &times;
</button>
...
```
@@ -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]
<script setup>
...
...
const incompleteTasksCount = autorun(() => {
return TasksCollection.find({ checked: { $ne: true } }).count();
}).result;
@@ -1235,7 +1234,7 @@ async function setIsCheckedTask(taskId, checked) {
if (!Meteor.userId()) {
throw new Meteor.Error('Not authorized.');
}
await TasksCollection.updateAsync(taskId, {
$set: {
checked
@@ -1329,13 +1328,13 @@ Make sure you replace `vue3-meteor-3` by a custom name that you want as subdomai
```shell
meteor deploy vue3-meteor-3.meteorapp.com --settings private/settings.json
Talking to Galaxy servers at https://us-east-1.galaxy-deploy.meteor.com
Preparing to build your app...
Preparing to upload your app...
Preparing to build your app...
Preparing to upload your app...
Uploaded app bundle for new app at vue-tutorial.meteorapp.com.
Galaxy is building the app into a native image.
Waiting for deployment updates from Galaxy...
Building app image...
Deploying app...
Waiting for deployment updates from Galaxy...
Building app image...
Deploying app...
You have successfully deployed the first version of your app.
For details, visit https://galaxy.meteor.com/app/vue3-meteor-3.meteorapp.com
```