diff --git a/.github/scripts/__tests__/inactive-issues.test.js b/.github/scripts/__tests__/inactive-issues.test.js new file mode 100644 index 0000000000..91f9ddf60f --- /dev/null +++ b/.github/scripts/__tests__/inactive-issues.test.js @@ -0,0 +1,198 @@ +// Tests for inactive-issues.js using Node's built-in test runner (node:test) +// We only care about the last comments (per user instruction), so we don't need pagination logic. + +const { test, beforeEach } = require('node:test'); +const assert = require('node:assert'); +const path = require('node:path'); + +// Load the script dynamically so we can pass mocks. +const scriptPath = path.join(__dirname, '..', 'inactive-issues.js'); + +// Helper to advance days +const daysAgo = (days) => { + const d = new Date(); + d.setUTCDate(d.getUTCDate() - days); + return d.toISOString(); +}; + +// Factory for github REST mock structure +function buildGithubMock({ issues, commentsByIssue }) { + return { + rest: { + issues: { + listForRepo: async ({ page, per_page }) => { + // simple pagination slice + const start = (page - 1) * per_page; + const end = start + per_page; + return { data: issues.slice(start, end) }; + }, + listComments: async ({ issue_number }) => { + return { data: commentsByIssue[issue_number] || [] }; + }, + createComment: async ({ issue_number, body }) => { + // push comment to structure to allow assertions on side effects if needed + const arr = commentsByIssue[issue_number] || (commentsByIssue[issue_number] = []); + arr.push({ + id: Math.random(), + body, + created_at: new Date().toISOString(), + user: { login: 'github-actions[bot]', type: 'Bot' } + }); + return {}; + }, + addLabels: async ({ issue_number, labels }) => { + const issue = issues.find(i => i.number === issue_number); + if (issue) { + (issue.labels || (issue.labels = [])).push(...labels.map(l => ({ name: l }))); + } + return {}; + } + } + } + }; +} + +// Wrap script invocation for reuse +async function runScript({ issues, commentsByIssue }) { + delete require.cache[require.resolve(scriptPath)]; + const fn = require(scriptPath); + const github = buildGithubMock({ issues, commentsByIssue }); + await fn({ github, context: { repo: { owner: 'meteor', repo: 'meteor' } } }); + return { issues, commentsByIssue }; +} + +let baseIssueNumber = 1000; +function makeIssue({ daysSinceHumanActivity, isPR = false, labels = [], user = 'user1' }) { + // We'll simulate by setting created_at to the human activity date if no comments. + const updated_at = daysAgo(daysSinceHumanActivity); + return { + number: baseIssueNumber++, + pull_request: isPR ? {} : undefined, + labels: labels.map(n => ({ name: n })), + user: { login: user }, + created_at: daysAgo(daysSinceHumanActivity), + updated_at + }; +} + +// TESTS + +beforeEach(() => { + baseIssueNumber = 1000; +}); + +test('60 days inactivity -> adds reminder comment (no prior reminder)', async () => { + const issue = makeIssue({ daysSinceHumanActivity: 60 }); + const issues = [issue]; + const commentsByIssue = { [issue.number]: [] }; // no comments + + await runScript({ issues, commentsByIssue }); + + const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]'); + assert.equal(botComments.length, 1, 'Should have 1 reminder comment'); + assert.match(botComments[0].body, /60 days/); + assert.ok(!issue.labels.some(l => l.name === 'idle'), 'Should not label yet'); +}); + +test('60 days inactivity but already reminded -> no duplicate comment', async () => { + const issue = makeIssue({ daysSinceHumanActivity: 65 }); + const issues = [issue]; + const commentsByIssue = { + [issue.number]: [ + { + id: 1, + body: 'đź‘‹ @user1 This issue has been open with no human activity for 60 days. Is this issue still relevant? If there is no human response or activity within the next 30 days, this issue will be labeled as `idle`.', + created_at: daysAgo(5), // 5 days ago bot comment (means last human is 65 days, bot after human) + user: { login: 'github-actions[bot]', type: 'Bot' } + } + ] + }; + + await runScript({ issues, commentsByIssue }); + + const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]'); + assert.equal(botComments.length, 1, 'Should still have only the existing reminder'); +}); + +test('90 days inactivity -> label + comment', async () => { + const issue = makeIssue({ daysSinceHumanActivity: 95 }); + const issues = [issue]; + const commentsByIssue = { [issue.number]: [] }; + + await runScript({ issues, commentsByIssue }); + + assert.ok(issue.labels.some(l => l.name === 'idle'), 'Should add idle label'); + const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]'); + assert.equal(botComments.length, 1, 'Should comment when labeling'); + assert.match(botComments[0].body, /90 days/i); +}); + +test('90 days inactivity but already labeled -> no action', async () => { + const issue = makeIssue({ daysSinceHumanActivity: 100, labels: ['idle'] }); + const issues = [issue]; + const commentsByIssue = { [issue.number]: [] }; + + await runScript({ issues, commentsByIssue }); + + const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]'); + assert.equal(botComments.length, 0, 'Should not comment again'); +}); + +test('90 days inactivity but already labeled `in-development` -> no action', async () => { + const issue = makeIssue({ daysSinceHumanActivity: 100, labels: ['in-development'] }); + const issues = [issue]; + const commentsByIssue = { [issue.number]: [] }; + + await runScript({ issues, commentsByIssue }); + + const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]'); + assert.equal(botComments.length, 0, 'Should not comment again'); +}); + + +test('Human reply after reminder resets cycle (no immediate labeling)', async () => { + // Scenario: last human activity 10 days ago, bot commented 40 days ago (which was 50 days after original). Should NOT comment again or label. + const issue = makeIssue({ daysSinceHumanActivity: 10 }); + const issues = [issue]; + const commentsByIssue = { + [issue.number]: [ + { + id: 1, + body: 'đź‘‹ @user1 This issue has been open with no human activity for 60 days... ', + created_at: daysAgo(50), + user: { login: 'github-actions[bot]', type: 'Bot' } + }, + { + id: 2, + body: 'I am still seeing this problem', + created_at: daysAgo(10), + user: { login: 'some-human', type: 'User' } + } + ] + }; + + await runScript({ issues, commentsByIssue }); + + const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]'); + assert.equal(botComments.length, 1, 'Should not add a new bot comment'); + assert.ok(!issue.labels.some(l => l.name === 'idle'), 'Should not label'); +}); + +test('Only bot comments (no human ever) counts from creation date', async () => { + const issue = makeIssue({ daysSinceHumanActivity: 61 }); + const issues = [issue]; + const commentsByIssue = { + [issue.number]: [ + { + id: 1, + body: 'Automated maintenance notice', + created_at: daysAgo(30), + user: { login: 'github-actions[bot]', type: 'Bot' } + } + ] + }; + + await runScript({ issues, commentsByIssue }); + const botComments = commentsByIssue[issue.number].filter(c => /60 days/.test(c.body)); + assert.equal(botComments.length, 1, 'Should add a 60-day reminder'); +}); diff --git a/.github/scripts/inactive-issues.js b/.github/scripts/inactive-issues.js index cbca2f31aa..a75f5b81a1 100644 --- a/.github/scripts/inactive-issues.js +++ b/.github/scripts/inactive-issues.js @@ -1,190 +1,200 @@ +/** +* Mark issues as idle after a period of inactivity +* and post reminders after a shorter period of inactivity. +* +* 1. Issues with no human activity for 60 days get a reminder comment. +* 2. Issues with no human activity for 90 days get labeled as "idle" and get a comment. +* +* Human activity is defined as any comment from a non-bot user. +* +* This script is intended to be run as a GitHub Action on a schedule (e.g., daily). + */ module.exports = async ({ github, context }) => { const daysToComment = 60; const daysToLabel = 90; + + const idleTimeComment = daysToComment * 24 * 60 * 60 * 1000; + const idleTimeLabel = daysToLabel * 24 * 60 * 60 * 1000; const now = new Date(); - const idleTimeComment = daysToComment * 24 * 60 * 60 * 1000; // 60 days in milliseconds - const idleTimeLabel = daysToLabel * 24 * 60 * 60 * 1000; // 90 days in milliseconds - - // Function to fetch issues until we find recently updated ones + + const BOT_LOGIN = 'github-actions[bot]'; + const REMINDER_PHRASE = 'Is this issue still relevant?'; + + const COMMENT_60_TEMPLATE = (login) => + `đź‘‹ @${login} This issue has been open with no human activity for ${daysToComment} days. Is this issue still relevant? If there is no human response or activity within the next ${daysToLabel - daysToComment} days, this issue will be labeled as \`idle\`.`; + + const COMMENT_90_TEXT = + 'This issue has been automatically labeled as `idle` due to 90 days of inactivity (no human interaction). If this is still relevant or if someone is working on it, please comment or add `in-development` label.'; + + // Fetch all open issues async function fetchAllIssues() { - let allIssues = []; let page = 1; - let hasNextPage = true; - const now = new Date(); - const minInactivity = idleTimeComment; // 60 days in milliseconds - - while (hasNextPage) { - const response = await github.rest.issues.listForRepo({ + const per_page = 100; + const results = []; + let keepGoing = true; + + while (keepGoing) { + const { data } = await github.rest.issues.listForRepo({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', - per_page: 100, - page: page, + per_page, + page, sort: 'updated', - direction: 'asc' // Oldest updated first + direction: 'asc' }); - - // Check if the most recently updated issue on this page is too recent - let recentIssueFound = false; - if (response.data.length > 0) { - // Check the last issue on the page (most recently updated) - const lastIssue = response.data[response.data.length - 1]; - const lastIssueUpdatedAt = new Date(lastIssue.updated_at); - const timeSinceLastIssueUpdate = now.getTime() - lastIssueUpdatedAt.getTime(); - - if (timeSinceLastIssueUpdate < minInactivity) { - // This page already has issues that are too recent, filter them out - const filteredIssues = response.data.filter(issue => { - const issueUpdatedAt = new Date(issue.updated_at); - const timeSinceUpdate = now.getTime() - issueUpdatedAt.getTime(); - return timeSinceUpdate >= minInactivity; - }); - - allIssues = allIssues.concat(filteredIssues); - recentIssueFound = true; - hasNextPage = false; - } else { - // All issues on this page are old enough, keep them all - allIssues = allIssues.concat(response.data); - } - } - - // Stop if we found recent issues or reached the end of pagination - if (recentIssueFound) { - hasNextPage = false; - } else if (response.data.length < 100) { - hasNextPage = false; + + if (!data.length) break; + results.push(...data); + + if (data.length < per_page) { + keepGoing = false; } else { page++; - // Small delay to avoid hitting rate limits - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((r) => setTimeout(r, 120)); } } - - return allIssues; + return results; } - - // Fetch all issues - const allIssues = await fetchAllIssues(); - - let processedCount = 0; - let commentedCount = 0; - let labeledCount = 0; - - for (const issue of allIssues) { - processedCount++; - - // Skip pull requests - if (issue.pull_request) { - continue; - } - - // Skip issues that already have the idle label - if (issue.labels.some(label => label.name === 'idle')) { - continue; - } - - // Get latest comment or update date - const issueUpdatedAt = new Date(issue.updated_at); - const timeSinceUpdate = now.getTime() - issueUpdatedAt.getTime(); - - // Handle 60-day idle issues (comment) - if (timeSinceUpdate > idleTimeComment && timeSinceUpdate <= idleTimeLabel) { - // Check if bot already commented to avoid duplicate comments - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - per_page: 100 - }); - - // Check if there's a recent bot comment - const botCommented = comments.data.some(comment => { - const commentDate = new Date(comment.created_at); - const timeSinceComment = now.getTime() - commentDate.getTime(); - const isBot = comment.user.login === 'github-actions[bot]'; - const isRecent = timeSinceComment < idleTimeComment; - const hasRightContent = comment.body.includes('Is this issue still relevant?'); - - return isBot && isRecent && hasRightContent; - }); - - if (!botCommented) { - try { - const result = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `đź‘‹ @${issue.user.login} This issue has been open for 60 days with no activity. Is this issue still relevant? If there is no response or activity within the next 30 days, this issue will be labeled as \`idle\`.` - }); - commentedCount++; - } catch (error) { - // Add retry logic - try { - // Wait for 5 seconds before retrying - await new Promise(resolve => setTimeout(resolve, 5000)); - - const retryResult = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `đź‘‹ @${issue.user.login} This issue has been open for 60 days with no activity. Is this issue still relevant? If there is no response or activity within the next 30 days, this issue will be labeled as \`idle\`.` - }); - commentedCount++; - } catch (retryError) { - // Failed retry, continue with other issues - } - } + // analyse comments to find last human activity and if a reminder was already posted after that + async function analyzeComments(issueNumber, issueCreatedAt) { + const commentsResp = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + per_page: 100 + }); + + const comments = commentsResp.data; + let lastHumanActivity = null; + + for (let i = comments.length - 1; i >= 0; i--) { + const c = comments[i]; + const isBot = c.user?.type === 'Bot' || c.user?.login === BOT_LOGIN; + if (!isBot) { + lastHumanActivity = new Date(c.created_at); + break; } } - - // Handle 90-day idle issues (add label) - else if (timeSinceUpdate > idleTimeLabel) { - // Check if the issue has the idle label - if (!issue.labels.some(label => label.name === 'idle')) { + + if (!lastHumanActivity) { + lastHumanActivity = new Date(issueCreatedAt); + } + + const hasReminderAfterLastHuman = comments.some( + (c) => + c.user?.login === BOT_LOGIN && + c.body?.includes(REMINDER_PHRASE) && + new Date(c.created_at) > lastHumanActivity + ); + + return { lastHumanActivity, hasReminderAfterLastHuman }; + } + + const issues = await fetchAllIssues(); + + let processed = 0; + let commented = 0; + let labeled = 0; + let skippedPR = 0; + + for (const issue of issues) { + processed++; + + if (issue.pull_request) { + skippedPR++; + continue; + } + + if (issue.labels.some((l) => l.name === 'idle' || l.name === 'in-development')) { + continue; + } + + let analysis; + try { + analysis = await analyzeComments(issue.number, issue.created_at); + } catch (err) { + continue; // fail to get comments, skip + } + + const { lastHumanActivity, hasReminderAfterLastHuman } = analysis; + const inactivityMs = now.getTime() - lastHumanActivity.getTime(); + + // 90+ days => label + comment + if (inactivityMs >= idleTimeLabel) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['idle'] + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: COMMENT_90_TEXT + }); + labeled++; + continue; + } catch (err) { + // retry simples try { - // Add the label - const labelResult = await github.rest.issues.addLabels({ + await new Promise((r) => setTimeout(r, 5000)); + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, labels: ['idle'] }); - - // Add a comment when labeling as idle - const commentResult = await github.rest.issues.createComment({ + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - body: `This issue has been automatically labeled as \`idle\` due to 90 days of inactivity. If this issue is still relevant, please comment to reactivate it.` + body: COMMENT_90_TEXT }); - labeledCount++; - } catch (error) { - // Add retry logic with exponential backoff - try { - // Wait for 5 seconds before retrying - await new Promise(resolve => setTimeout(resolve, 5000)); - - const retryLabelResult = await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ['idle'] - }); - - // Retry adding comment - const retryCommentResult = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `This issue has been automatically labeled as \`idle\` due to 90 days of inactivity. If this issue is still relevant, please comment to reactivate it.` - }); - labeledCount++; - } catch (retryError) { - // Continue with other issues if retry fails - } - } + labeled++; + } catch {} + continue; + } + } + + // 60-89 days => comment (once) + if ( + inactivityMs >= idleTimeComment && + inactivityMs < idleTimeLabel && + !hasReminderAfterLastHuman + ) { + const body = COMMENT_60_TEMPLATE(issue.user.login); + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body + }); + commented++; + } catch (err) { + try { + await new Promise((r) => setTimeout(r, 5000)); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body + }); + commented++; + } catch {} } } } + + // Log summary for CI + console.log( + JSON.stringify( + { processed, commented, labeled, skippedPR }, + null, + 2 + ) + ); }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index beebcdb1ac..c646fad457 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -155,7 +155,7 @@ Learn how we use GitHub labels [here](LABELS.md) ## Documentation -If you'd like to contribute to Meteor's documentation, head over to https://docs.meteor.com or https://guide.meteor.com and if you find something that could be better click in "Edit on GitHub" footer to edit and submit a PR. +If you'd like to contribute to Meteor's documentation, head over to https://docs.meteor.com/about/contributing.html for guidelines. ## Blaze diff --git a/docs/source/commandline.md b/docs/source/commandline.md index 11138250d5..e1494f5a7c 100644 --- a/docs/source/commandline.md +++ b/docs/source/commandline.md @@ -677,7 +677,7 @@ Your project should be a git repository as the commit hash is going to be used t The `cache-build` option is available since Meteor 1.11. {% endpullquote %} -With the argument `--container-size` you can change your app's container size using the deploy command. The valid arguments are: `tiny`, `compact`, `standard`, `double`, `quad`, `octa`, and `dozen`. One more thing to note here is that the `--container-size` flag can only be used when the `--plan` option is already specified, otherwise using the `--container-size` option will throw an error with the message : `Error deploying application: Internal error`. To see more about the difference and prices of each one you can check [here](https://www.meteor.com/cloud#pricing-section). +With the argument `--container-size` you can change your app's container size using the deploy command. The valid arguments are: `tiny`, `compact`, `standard`, `double`, `quad`, `octa`, and `dozen`. One more thing to note here is that the `--container-size` flag can only be used when the `--plan` option is already specified, otherwise using the `--container-size` option will throw an error with the message : `Error deploying application: Internal error`. To see more about the difference and prices of each one you can check [here](https://galaxycloud.app/meteorjs/pricing). {% pullquote warning %} The `--container-size` option is available since Meteor 2.4.1. diff --git a/guide/source/deployment.md b/guide/source/deployment.md index c254c1a196..fa9d8f08de 100644 --- a/guide/source/deployment.md +++ b/guide/source/deployment.md @@ -194,7 +194,7 @@ MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://my-app.com PORT=3000 n ``` * `ROOT_URL` is the base URL for your Meteor project -* `PORT` is the port at which the application is running +* `PORT` is the port at which the application is running * `MONGO_URL` is a [Mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/) supplied by the MongoDB provider. @@ -322,7 +322,7 @@ Galaxy's UI provides a detailed logging system, which can be invaluable to deter If you really want to understand the ins and outs of running your Meteor application, you should use an Application Performance Monitoring (APM) service. There are multiple services designed for Meteor apps: -- [Meteor APM](https://www.meteor.com/cloud) +- [Meteor APM](https://galaxycloud.app/) - [Monti APM](https://montiapm.com/) - [Meteor Elastic APM](https://github.com/Meteor-Community-Packages/meteor-elastic-apm) diff --git a/guide/source/performance-improvement.md b/guide/source/performance-improvement.md index f6721550f7..28298e8232 100644 --- a/guide/source/performance-improvement.md +++ b/guide/source/performance-improvement.md @@ -3,10 +3,10 @@ title: Performance improvements description: How to optimize your Meteor application for higher performance when you start growing. --- -This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling). -It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB, -so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps. -Also do note that every app is different so there are unique challenges to each, therefore +This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling). +It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB, +so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps. +Also do note that every app is different so there are unique challenges to each, therefore practices describe in this guide should be used as a guiding posts rather than absolutes. This guide has been heavily inspired by [Marcin Szuster's Vazco article](https://www.vazco.eu/blog/how-to-optimize-and-scale-meteor-projects), the official [Meteor Galaxy guide](https://galaxy-guide.meteor.com/), @@ -15,11 +15,11 @@ and talk by Paulo MogollĂłn's talk at Impact 2022 titled ["First steps on scalin

Performance monitoring

Before any optimization can take place we need to know what is our problem. This is where APM (Application Performance Monitor) comes in. -If you are hosting on Galaxy then this is automatically included in the [Professional plan](https://www.meteor.com/cloud/pricing) -and you can learn more about in its [own dedicated guide article](https://cloud-guide.meteor.com/apm-getting-started.html). -For those hosting outside of Galaxy the most popular solution is to go with [Monti APM](https://montiapm.com/) which shares -all the main functionality with Galaxy APM. You can also choose other APM for Node.js, but they will not show you Meteor -specific data that Galaxy APM and Monti APM specialize in. For this guide we will focus on showing how to work with Galaxy APM, +If you are hosting on Galaxy then this is automatically included in the [Professional plan](https://galaxycloud.app/meteorjs/pricing) +and you can learn more about in its [own dedicated guide article](https://cloud-guide.meteor.com/apm-getting-started.html). +For those hosting outside of Galaxy the most popular solution is to go with [Monti APM](https://montiapm.com/) which shares +all the main functionality with Galaxy APM. You can also choose other APM for Node.js, but they will not show you Meteor +specific data that Galaxy APM and Monti APM specialize in. For this guide we will focus on showing how to work with Galaxy APM, which is the same as with Monti APM, for simplicity. Once you setup either of those APMs you will need to add a package to your Meteor app to start sending them data. @@ -37,9 +37,9 @@ meteor add montiapm:agent ```

Finding issues in APM

-APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of -publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed -tabs looking for methods and publications to improve and analyzing the impact of your actions. The process, for example for +APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of +publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed +tabs looking for methods and publications to improve and analyzing the impact of your actions. The process, for example for optimizing methods, will look like this: 1. Go to the detailed view under the Methods tab. @@ -52,14 +52,14 @@ Not every long-performing method has to be improved. Take a look at the followin * methodX - mean response time 1 515 ms, throughput 100,05/min * methodY - mean response time 34 000 ms, throughput 0,03/min -At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY -is more relevant to improvement. But don’t ignore the fact that this method is being used only once in +At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY +is more relevant to improvement. But don’t ignore the fact that this method is being used only once in a few hours by the system administrators or scheduled cron action. -And now, let’s take a look at the methodX. Its response time is evidently lower BUT compared to the frequency +And now, let’s take a look at the methodX. Its response time is evidently lower BUT compared to the frequency of use, it is still high, and without any doubt should be optimized first. -It’s also absolutely vital to remember that you shouldn't optimize everything as it goes. +It’s also absolutely vital to remember that you shouldn't optimize everything as it goes. The key is to think strategically and match the most critical issues with your product priorities. For more information about all the things you can find in Galaxy APM take a look at the Meteor APM section in [Galaxy Guide](https://galaxy-guide.meteor.com/apm-getting-started.html). @@ -71,75 +71,75 @@ At the same this is the most resource intensive part of a Meteor application. Under the hood WebSockets are being used with additional abilities provided by DDP.

Proper use of publications

-Since publications can get resource intensive they should be reserved for usage that requires up to date, live data or +Since publications can get resource intensive they should be reserved for usage that requires up to date, live data or that are changing frequently and you need the users to see that. -You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not -required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed, +You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not +required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed, in most cases the re-fetching shouldn't be necessary. -But even before you proceed any further there are a few improvements that you can make here. +But even before you proceed any further there are a few improvements that you can make here. First make sure that you only get the fields you need, limit the number of documents you send to the client to what you need (aka always set the `limit` option) and ensure that you have set all your indexes.

Methods over publications

-The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication -and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements +The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication +and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements to get the method work faster apply here, but once called it sends the data and you don't have the overhead of a publication. -What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once +What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once to load the data or when specifically needed (for example when the data gets updated due to user action or when the user requests it).

Publication replacements

Using methods has its limitations and there are other tools that you might want to evaluate as a potential replacement. -[Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with another -replacement which is [GraphQL](https://graphql.org/) and in particular [Apollo GraphQL](https://www.apollographql.com/), +[Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with another +replacement which is [GraphQL](https://graphql.org/) and in particular [Apollo GraphQL](https://www.apollographql.com/), which also has an integration [package](https://atmospherejs.com/meteor/apollo) with Meteor. Finally, you can also go back to using REST as well. Do note, that you can mix all of these based on your needs.

Low observer reuse

-Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes. +Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes. Creating them is an expensive operations, so you want to make sure that Meteor reuses them as much as possible. > [Learn more about observers](https://galaxy-guide.meteor.com/apm-know-your-observers.html) -The key for observer reuse is to make sure that the queries requested are identical. This means that user given values -should be standardised and so should any dynamic input like time. Publications for users should check if user is signed in +The key for observer reuse is to make sure that the queries requested are identical. This means that user given values +should be standardised and so should any dynamic input like time. Publications for users should check if user is signed in first before returning publication and if user is not signed in, then it should instead call `this.ready();`. > [Learn more on improving observer reuse](https://galaxy-guide.meteor.com/apm-improve-cpu-and-network-usage)

Redis Oplog

-[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing -(which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name -suggests uses [redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on +[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing +(which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name +suggests uses [redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on the server and database, allows you to track only the data that you want and only publish the changes you need.

Methods

-While methods are listed as one of the possible replacements for publications, they themselves can be made more performant, -after all it really depends on what you put inside them and APM will provide you with the necessary insight on which +While methods are listed as one of the possible replacements for publications, they themselves can be made more performant, +after all it really depends on what you put inside them and APM will provide you with the necessary insight on which methods are the problem.

Heavy actions

-In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out -and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server +In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out +and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server or even better something specifically optimized for that given task.

Reoccurring jobs

-Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have -an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to +Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have +an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to the list and be recipient of the results, most likely via database results.

Rate limiting

-Rate limit your methods to reduce effectiveness of DDOS attack and spare your server. This is also a good practice to -ensure that you don't accidentally DDOS your self. For example a user who clicks multiple time on a button that triggers -an expensive function. In this example you should also in general ensure that any button that triggers a server event +Rate limit your methods to reduce effectiveness of DDOS attack and spare your server. This is also a good practice to +ensure that you don't accidentally DDOS your self. For example a user who clicks multiple time on a button that triggers +an expensive function. In this example you should also in general ensure that any button that triggers a server event should be disabled until there is a response from the server that the event has finished. You can and should rate limit both methods and collections. @@ -154,15 +154,15 @@ These are all applicable, and you should spend some time researching into them a

IP whitelisting

-If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers. -If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in. +If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers. +If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in. Besides the security risk this also impacts performance as authentication is not a cheap operation and it will impact performance. See [Galaxy guide](https://galaxy-guide.meteor.com/container-environment.html#network-outgoing) on IP whitelisting to get IPs for your Galaxy servers.

Indexes

-While single indexes on one field are helpful on simple query calls, you will most likely have more advance queries with +While single indexes on one field are helpful on simple query calls, you will most likely have more advance queries with multiple variables. To cover those you will need to create compound indexes. For example: ```javascript @@ -177,7 +177,7 @@ Statistics.createIndexAsync( ``` When creating indexes you should sort the variables in ESR (equity, sort, range) style. Meaning, first you put variables that will be equal to something specific. Second you put variables that sort things, -and third variables that provide range for that query. +and third variables that provide range for that query. Further you should order these variables in a way that the fields that filter the most should be first. Make sure that all the indexes are used and remove unused indexes as leaving unused indexes will have negative impact @@ -187,7 +187,7 @@ on performance as the database will have to still keep track on all the indexed To optimize finds ensure that all queries have are indexed. Meaning that any `.find()` variables should be indexed as described above. -All your finds should have a limit on the return so that the database stops going through the data once it has reached +All your finds should have a limit on the return so that the database stops going through the data once it has reached the limit, and you only return the limited number of results instead of the whole database. Beware of queries with `n + 1` issue. For example in a database that has cars and car owners. You don't want to get cars, @@ -202,14 +202,14 @@ If you still have issues make sure that you read data from secondaries.

Beware of collection hooks

-While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might -create additional queries that you might not know about. Make sure to review packages that use them so that they won't +While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might +create additional queries that you might not know about. Make sure to review packages that use them so that they won't create additional queries.

Caching

Once your user base increases you want to invest into query caching like using Redis, Redis Oplog and other. -For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/) +For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/) and save their results.

Scaling

@@ -229,13 +229,13 @@ Galaxy has these as well. Learn more about [setting triggers for scaling on Gala Setting this is vital, so that your application can keep on running when you have extra people come and then saves you money by scaling down when the containers are not in use. When initially setting these pay a close attention to the performance of your app. you need to learn when is the right time to scale your app so it has enough time to spin up new containers before the existing one get overwhelmed by traffic and so on. -There are other points to pay attention to as well. For example if your app is used by corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and the then decrease the minimum to 1 for after hours and on weekends. +There are other points to pay attention to as well. For example if your app is used by corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and the then decrease the minimum to 1 for after hours and on weekends. Usually when you are working on performance issues you will have higher numbers of containers as you optimize your app. It is therefore vital to revisit your scaling setting after each rounds of improvements to ensure that scaling triggers are properly optimized.

Packages

-During development, it is very tempting to add packages to solve issue or support some features. -This should be done carefully and each package should be wetted carefully if it is a good fit for the application. -Besides security and maintenance issues you also want to know which dependencies given package introduces and +During development, it is very tempting to add packages to solve issue or support some features. +This should be done carefully and each package should be wetted carefully if it is a good fit for the application. +Besides security and maintenance issues you also want to know which dependencies given package introduces and as a whole what will be the impact on performance. diff --git a/guide/source/ui-ux.md b/guide/source/ui-ux.md index 4578cced7b..d4235c0f1e 100644 --- a/guide/source/ui-ux.md +++ b/guide/source/ui-ux.md @@ -11,9 +11,9 @@ Meteor supports many view layers. The most popular are: - [React](react.html): official [page](http://reactjs.org/) - [Blaze](blaze.html): official [page](http://blazejs.org/) -- [Angular](http://www.angular-meteor.com): official [page](https://angular.io/) +- [Angular](angular.html): official [page](https://angular.io/) - [Vue](vue.html): official [page](https://vuejs.org/) -- [Svelte](https://www.meteor.com/tutorials/svelte/creating-an-app): official [page](https://svelte.dev/) +- [Svelte](svelte.html): official [page](https://svelte.dev/) If you are starting with web development we recommend that you use Blaze as it's very simple to learn. diff --git a/npm-packages/eslint-plugin-meteor/README.md b/npm-packages/eslint-plugin-meteor/README.md index fd322c5f93..fff5044a51 100644 --- a/npm-packages/eslint-plugin-meteor/README.md +++ b/npm-packages/eslint-plugin-meteor/README.md @@ -56,7 +56,7 @@ meteor Building an application with Meteor? -* Deploy on Galaxy hosting: https://www.meteor.com/cloud +* Deploy on Galaxy hosting: https://galaxycloud.app/ * Announcement list: sign up at https://www.meteor.com/ * Discussion forums: https://forums.meteor.com/ * Join the Meteor community Slack by clicking this [invite link](https://join.slack.com/t/meteor-community/shared_invite/enQtODA0NTU2Nzk5MTA3LWY5NGMxMWRjZDgzYWMyMTEyYTQ3MTcwZmU2YjM5MTY3MjJkZjQ0NWRjOGZlYmIxZjFlYTA5Mjg4OTk3ODRiOTc). diff --git a/npm-packages/meteor-installer/install.js b/npm-packages/meteor-installer/install.js index 486b4207b8..0c3d7b7922 100644 --- a/npm-packages/meteor-installer/install.js +++ b/npm-packages/meteor-installer/install.js @@ -360,7 +360,7 @@ Or see the docs at: Deploy and host your app with Cloud: - www.meteor.com/cloud + https://galaxycloud.app/ *************************************** You might need to open a new terminal window to have access to the meteor command, or run this in your terminal: diff --git a/package.json b/package.json index 28c1cf54f4..ff1baa3e4c 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,16 @@ { "name": "meteor", "version": "0.0.1", - "description": "Used to apply Prettier and ESLint manually", + "description": "Meteor's main repository, containing the Meteor tool, core packages, and documentation.", "repository": { "type": "git", "url": "git+https://github.com/meteor/meteor.git" }, - "author": "Filipe Névola", "license": "MIT", "bugs": { "url": "https://github.com/meteor/meteor/issues" }, - "homepage": "https://github.com/meteor/meteor#readme", + "homepage": "https://www.meteor.com/", "devDependencies": { "@babel/core": "^7.21.3", "@babel/eslint-parser": "^7.21.3", @@ -35,6 +34,9 @@ "prettier": "^2.8.8", "typescript": "^5.4.5" }, + "scripts": { + "test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js" + }, "jshintConfig": { "esversion": 11 }, diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index c910659684..48234064d8 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -205,7 +205,7 @@ export class AccountsCommon { * @locus Anywhere * @param {Object} options * @param {Boolean} options.sendVerificationEmail New users with an email address will receive an address verification email. - * @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available. + * @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available. **Important**: This option must be set on both the client and server to take full effect. If only set on the server, account creation will be blocked but the UI will still show the "Create account" link. * @param {String | Function} options.restrictCreationByEmailDomain If set to a string, only allows new users if the domain part of their email address matches the string. If set to a function, only allows new users if the function returns true. The function is passed the full email address of the proposed new user. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmailDomain: 'school.edu' })`. * @param {Number} options.loginExpiration The number of milliseconds from when a user logs in until their token expires and they are logged out, for a more granular control. If `loginExpirationInDays` is set, it takes precedent. * @param {Number} options.loginExpirationInDays The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration. @@ -226,6 +226,19 @@ export class AccountsCommon { * @param {Number} options.loginTokenExpirationHours When using the package `accounts-2fa`, use this to set the amount of time a token sent is valid. As it's just a number, you can use, for example, 0.5 to make the token valid for just half hour. The default is 1 hour. * @param {Number} options.tokenSequenceLength When using the package `accounts-2fa`, use this to the size of the token sequence generated. The default is 6. * @param {'session' | 'local'} options.clientStorage By default login credentials are stored in local storage, setting this to true will switch to using session storage. + * + * @example + * // For UI-related options like forbidClientAccountCreation, call Accounts.config on both client and server + * // Create a shared configuration file (e.g., lib/accounts-config.js): + * import { Accounts } from 'meteor/accounts-base'; + * + * Accounts.config({ + * forbidClientAccountCreation: true, + * sendVerificationEmail: true, + * }); + * + * // Then import this file in both client/main.js and server/main.js: + * // import '../lib/accounts-config.js'; */ config(options) { // We don't want users to accidentally only call Accounts.config on the diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 26860656b1..1ad449e928 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -730,7 +730,7 @@ if (Meteor.isServer) { // create same user in two different collections - should pass const email = "test-collection@testdomain.com" - const collection0 = new Mongo.Collection('test1'); + const collection0 = new Mongo.Collection(`test1_${Random.id()}`); Accounts.config({ collection: collection0, @@ -738,7 +738,7 @@ if (Meteor.isServer) { const uid0 = await Accounts.createUser({email}) await Meteor.users.removeAsync(uid0); - const collection1 = new Mongo.Collection('test2'); + const collection1 = new Mongo.Collection(`test2_${Random.id()}`); Accounts.config({ collection: collection1, }) @@ -757,13 +757,13 @@ if (Meteor.isServer) { const email = "test-collection@testdomain.com" Accounts.config({ - collection: 'collection0', + collection: `collection0_${Random.id()}`, }) const uid0 = await Accounts.createUser({email}) await Meteor.users.removeAsync(uid0); Accounts.config({ - collection: 'collection1', + collection: `collection1_${Random.id()}`, }) const uid1 = await Accounts.createUser({email}) await Meteor.users.removeAsync(uid1); diff --git a/packages/accounts-base/accounts_tests_setup.js b/packages/accounts-base/accounts_tests_setup.js index f77c6e7c99..0e211828c0 100644 --- a/packages/accounts-base/accounts_tests_setup.js +++ b/packages/accounts-base/accounts_tests_setup.js @@ -14,6 +14,8 @@ const getTokenFromSecret = async ({ selector, secret: secretParam }) => { return token; }; +Accounts.config({ ambiguousErrorMessages: false }); + Meteor.methods({ async removeAccountsTestUser(username) { await Meteor.users.removeAsync({ username }); diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 13f9927018..49f94544a0 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -54,23 +54,37 @@ if (Meteor.isClient) (() => { const removeSkipCaseInsensitiveChecksForTest = (value, test, expect) => Meteor.call('removeSkipCaseInsensitiveChecksForTest', value); - const createUserStep = function (test, expect) { + // Make logout steps awaitable so subsequent test steps don't race. + const logoutStep = async (test, expect) => + new Promise(resolve => { + Meteor.logout(err => { + if (err) { + // keep original behavior: fail the test if logout errored + test.fail(err.message); + // still resolve so test runner can continue + return resolve(); + } + test.equal(Meteor.user(), null); + resolve(); + }); + }); + + // Create user only after a confirmed logout to avoid races between + // tests that do login/logout operations. + const createUserStep = async function (test, expect) { + // Wait for the logout to complete synchronously. + await logoutStep(test, expect); + // Hack because Tinytest does not clean the database between tests/runs this.randomSuffix = Random.id(10); this.username = `AdaLovelace${ this.randomSuffix }`; this.email = `Ada-intercept@lovelace.com${ this.randomSuffix }`; this.password = 'password'; - Accounts.createUser( - { username: this.username, email: this.email, password: this.password }, - loggedInAs(this.username, test, expect)); - }; - const logoutStep = (test, expect) => - Meteor.logout(expect(error => { - if (error) { - test.fail(error.message); - } - test.equal(Meteor.user(), null); - })); + + Accounts.createUser( + { username: this.username, email: this.email, password: this.password }, + loggedInAs(this.username, test, expect)); + }; const loggedInAs = (someUsername, test, expect) => { return expect(error => { if (error) { @@ -79,18 +93,7 @@ if (Meteor.isClient) (() => { test.equal(Meteor.userId() && Meteor.user().username, someUsername); }); }; - const loggedInUserHasEmail = (someEmail, test, expect) => { - return expect(error => { - if (error) { - test.fail(error.message); - } - const user = Meteor.user(); - test.isTrue(user && user.emails.reduce( - (prev, email) => prev || email.address === someEmail, - false - )); - }); - }; + const expectError = (expectedError, test, expect) => expect(actualError => { test.equal(actualError && actualError.error, expectedError.error); test.equal(actualError && actualError.reason, expectedError.reason); @@ -1300,6 +1303,7 @@ if (Meteor.isServer) (() => { await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password")), /Token has invalid email address/ ); + Accounts._options.ambiguousErrorMessages = true; await test.throwsAsync( async () => await Meteor.callAsync( @@ -1380,6 +1384,7 @@ if (Meteor.isServer) (() => { }) } + Accounts._options.ambiguousErrorMessages = true; await test.throwsAsync( async () => await Meteor.callAsync( "login", @@ -1669,6 +1674,7 @@ if (Meteor.isServer) (() => { test.isTrue(userId1); test.isTrue(userId2); + Accounts._options.ambiguousErrorMessages = false; await test.throwsAsync( async () => await Accounts.setUsername(userId2, usernameUpper), /Username already exists/ diff --git a/tools/cli/commands.js b/tools/cli/commands.js index d81c03ffb5..bb4bbea6cc 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -451,7 +451,8 @@ var runCommandOptions = { ...inspectOptions, 'no-release-check': { type: Boolean }, production: { type: Boolean }, - 'raw-logs': { type: Boolean }, + 'raw-logs': { type: Boolean, default: true }, + timestamps: { type: Boolean, default: false }, // opposite of --raw-logs settings: { type: String, short: "s" }, verbose: { type: Boolean, short: "v" }, // With --once, meteor does not re-run the project if it crashes @@ -535,10 +536,7 @@ async function doRunCommand(options) { ); } - if (options['raw-logs']) { - runLog.setRawLogs(true); - } - + runLog.setRawLogs(options['raw-logs'] && !options.timestamps); let webArchs = projectContext.platformList.getWebArchs(); if (! _.isEmpty(runTargets) || @@ -1127,7 +1125,7 @@ main.registerCommand({ "If you are new to Meteor, try some of the learning resources here:" ); Console.info( - Console.url("https://www.meteor.com/tutorials"), + Console.url("https://docs.meteor.com/"), Console.options({ indent: 2 }) ); @@ -1136,7 +1134,7 @@ main.registerCommand({ "When you’re ready to deploy and host your new Meteor application, check out Cloud:" ); Console.info( - Console.url("https://www.meteor.com/cloud"), + Console.url("https://galaxycloud.app/"), Console.options({ indent: 2 }) ); @@ -2125,7 +2123,8 @@ testCommandOptions = { // like progress bars and spinners are unimportant. headless: { type: Boolean }, verbose: { type: Boolean, short: "v" }, - 'raw-logs': { type: Boolean }, + 'raw-logs': { type: Boolean, default: true }, + timestamps: { type: Boolean, default: false }, // opposite of --raw-logs // Undocumented. See #Once once: { type: Boolean }, @@ -2176,7 +2175,7 @@ testCommandOptions = { 'extra-packages': { type: String }, 'exclude-archs': { type: String }, - + // Same as TINYTEST_FILTER filter: { type: String, short: 'f' }, } @@ -2260,9 +2259,8 @@ async function doTestCommand(options) { serverArchitectures.push(DEPLOY_ARCH); } - if (options['raw-logs']) { - runLog.setRawLogs(true); - } + runLog.setRawLogs(options['raw-logs'] && !options.timestamps); + var includePackages = []; if (options['extra-packages']) { diff --git a/tools/cli/help.txt b/tools/cli/help.txt index 7f9e083861..96e611feca 100644 --- a/tools/cli/help.txt +++ b/tools/cli/help.txt @@ -90,7 +90,8 @@ Options: Meteor app source code as by default the port is generated using the id inside .meteor/.id file. --production Simulate production mode. Minify and bundle CSS and JS files. - --raw-logs Run without parsing logs from stdout and stderr. + --raw-logs Run without parsing logs from stdout and stderr (default: true). + --timestamps Run with timestamps in logs, the same as passing `--raw-logs=false`. --settings, -s Set optional data for Meteor.settings on the server. --release Specify the release of Meteor to use. --verbose Print all output from builds logs. @@ -747,7 +748,9 @@ Options: important when multiple Cordova apps are build from the same Meteor app source code as by default the port is generated using the id inside .meteor/.id file. - --raw-logs Run without parsing logs from stdout and stderr. + --raw-logs Run without parsing logs from stdout and stderr (default: true). + --timestamps Run with timestamps in logs, the same as passing `--raw-logs=false`. + --settings, -s Set optional data for Meteor.settings on the server --ios, Run tests in an emulator or on a mobile device. All of diff --git a/tools/tests/run.js b/tools/tests/run.js index 719ce0767d..0093676d50 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -403,7 +403,7 @@ selftest.define("run and SIGKILL parent process", ["yet-unsolved-windows-failure await s.createApp("myapp", "app-prints-pid"); s.cd("myapp"); - run = s.run(); + run = s.run("run", "--timestamps"); run.waitSecs(30); var match = await run.match(/My pid is (\d+)/); var childPid; @@ -434,7 +434,8 @@ selftest.define("run and SIGKILL parent process", ["yet-unsolved-windows-failure // Test that passing a bad pid in $METEOR_PARENT_PID logs an error and exits // immediately. s.set("METEOR_BAD_PARENT_PID_FOR_TEST", "t"); - run = s.run(); + run = s.run("run", "--timestamps"); + run.waitSecs(120); await run.match("must be a valid process ID"); await run.match("Your application is crashing"); diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 5680608b44..dfdb6f956c 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -40,6 +40,18 @@ export default defineConfig({ text: "Meteor + Vue + vue-meteor-tracker", link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", }, + { + text: "Meteor.js 3 + Solid", + link: "/tutorials/solid/index", + }, + { + text: "Meteor.js 3 + Blaze", + link: "/tutorials/blaze/index", + }, + { + text: "Meteor.js 3 + Svelte", + link: "/tutorials/svelte/index", + }, { link: "/tutorials/application-structure/index", text: "Application structure", @@ -114,7 +126,7 @@ export default defineConfig({ ], }, { text: "API", link: "/api/" }, - { text: "Galaxy Cloud", link: "https://www.meteor.com/cloud" }, + { text: "Galaxy Cloud", link: "https://galaxycloud.app" }, { text: metadata.currentVersion, items: metadata.versions.reverse().map((v) => { @@ -140,15 +152,21 @@ export default defineConfig({ { text: "What is Meteor?", link: "/about/what-is#introduction", - }, - { - text: "Meteor resources", - link: "/about/what-is#learning-more", + items:[ + { + text: "Meteor resources", + link: "/about/what-is#learning-more", + }, + ], }, { text: "Roadmap", link: "/about/roadmap", }, + { + text: "Contributing", + link: "/about/contributing", + } ], collapsed: true, }, @@ -472,6 +490,18 @@ export default defineConfig({ link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", text: "Meteor + Vue + vue-meteor-tracker", }, + { + text: "Meteor.js 3 + Solid", + link: "/tutorials/solid/index", + }, + { + text: "Meteor.js 3 + Blaze", + link: "/tutorials/blaze/index", + }, + { + text: "Meteor.js 3 + Svelte", + link: "/tutorials/svelte/index", + }, { link: "/tutorials/application-structure/index", text: "Application structure", diff --git a/v3-docs/docs/CONTRIBUTING.md b/v3-docs/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/v3-docs/docs/about/contributing.md b/v3-docs/docs/about/contributing.md new file mode 100644 index 0000000000..b5d6d395b0 --- /dev/null +++ b/v3-docs/docs/about/contributing.md @@ -0,0 +1,40 @@ +## Contributing to Meteor + +Ongoing Meteor development takes place in the open [on GitHub](https://github.com/meteor/meteor). We encourage pull requests and issues to discuss problems with any changes that could be made to the content. The contribution guidelines are available in the [Meteor GitHub repository](https://github.com/meteor/meteor/blob/devel/CONTRIBUTING.md). + +A great start to understand how to contribute to Meteor Core is this video from Meteor Impact 2025: + + + +## Contributing to this documentation + +We welcome contributions to the documentation! + +Since documentation is always evolving, your help is really valuable to ensure content is accurate and up-to-date. + +### How to Contribute + +#### Identify elements that need improvement + +Are you new here? Please check our [documentation issues](https://github.com/meteor/meteor/issues?q=is%3Aissue%20state%3Aopen%20label%3AProject%3ADocs). + +If in doubt about the best way to implement something, please create additional conversation on the issue. + +Issues are not assigned, you don’t need to wait for approval before contributing. Jump right in and open a PR — this increases your chances of getting your work merged, since issues can be claimed fast. + + +#### Do the changes and share them + +Documentation live in the `v3-docs/docs` directory of the [Meteor GitHub repository](https://github.com/meteor/meteor/tree/devel/v3-docs/docs) + +For small changes, such as fixing typos or formatting, you can simply click the "Edit this page on GitHub" button in the footer to edit the file and submit a PR. + +For larger changes, you need to fork meteor repo and start your work from the `devel` branch. + +You must test your contribution locally before submitting a pull request. To do so, here are the steps: +1. `cd v3-docs/docs && npm run docs:dev` will run the docs locally at [http://localhost:5173/](http://localhost:5173/) +2. Make your changes and verify them in the browser. +3. run `npm run docs:build` to ensure the build works correctly. +4. Push your work and submit a documented pull request to the `devel` branch. + +If you add a new page to the documentation, please make sure the configuration creates a link to access it (see [.vitepress/config.mts](https://github.com/meteor/meteor/blob/devel/v3-docs/docs/.vitepress/config.mts)). diff --git a/v3-docs/docs/api/app.md b/v3-docs/docs/api/app.md index fc31692154..b89d07b19c 100644 --- a/v3-docs/docs/api/app.md +++ b/v3-docs/docs/api/app.md @@ -57,7 +57,7 @@ App.launchScreens({ }); // Set PhoneGap/Cordova preferences. -App.setPreference('BackgroundColor', '0xff0000ff'); +App.setPreference('BackgroundColor', '#000000ff'); App.setPreference('HideKeyboardFormAccessoryBar', true); App.setPreference('Orientation', 'default'); App.setPreference('Orientation', 'all', 'ios'); diff --git a/v3-docs/docs/api/meteor.md b/v3-docs/docs/api/meteor.md index 5f3899014a..01d7010f93 100644 --- a/v3-docs/docs/api/meteor.md +++ b/v3-docs/docs/api/meteor.md @@ -30,7 +30,7 @@ Meteor.startup(async () => { if ((await LinksCollection.find().countAsync()) === 0) { await LinksCollection.insertAsync({ title: "Do the Tutorial", - url: "https://www.meteor.com/tutorials/react/creating-an-app", + url: "https://docs.meteor.com/tutorials/react", }); } }); @@ -148,10 +148,7 @@ import { Meteor } from "meteor/meteor"; function Component() { const addLink = () => - Meteor.callAsync( - "addLink", - "https://www.meteor.com/tutorials/react/creating-an-app" - ); + Meteor.callAsync("addLink", "https://docs.meteor.com/tutorials/react/"); return (
@@ -398,7 +395,6 @@ even if the method's writes are not available yet, you can specify an Use `Meteor.call` only to call methods that do not have a stub, or have a sync stub. If you want to call methods with an async stub, `Meteor.callAsync` can be used with any method. ::: - `Meteor.callAsync` is just like `Meteor.call`, except that it'll return a promise that you need to solve to get the server result. Along with the promise returned by `callAsync`, you can also handle `stubPromise` and `serverPromise` for managing client-side simulation and server response. @@ -409,64 +405,63 @@ The following sections guide you in understanding these promises and how to mana ```javascript try { - await Meteor.callAsync('greetUser', 'John'); - // 🟢 Server ended with success -} catch(e) { - console.error("Error:", error.reason); // 🔴 Server ended with error + await Meteor.callAsync("greetUser", "John"); + // 🟢 Server ended with success +} catch (e) { + console.error("Error:", error.reason); // 🔴 Server ended with error } -Greetings.findOne({ name: 'John' }); // 🗑️ Data is NOT available +Greetings.findOne({ name: "John" }); // 🗑️ Data is NOT available ``` #### stubPromise ```javascript -await Meteor.callAsync('greetUser', 'John').stubPromise; +await Meteor.callAsync("greetUser", "John").stubPromise; // 🔵 Client simulation -Greetings.findOne({ name: 'John' }); // 🧾 Data is available (Optimistic-UI) +Greetings.findOne({ name: "John" }); // 🧾 Data is available (Optimistic-UI) ``` #### stubPromise and serverPromise ```javascript -const { stubPromise, serverPromise } = Meteor.callAsync('greetUser', 'John'); +const { stubPromise, serverPromise } = Meteor.callAsync("greetUser", "John"); await stubPromise; // 🔵 Client simulation -Greetings.findOne({ name: 'John' }); // 🧾 Data is available (Optimistic-UI) +Greetings.findOne({ name: "John" }); // 🧾 Data is available (Optimistic-UI) try { await serverPromise; // 🟢 Server ended with success -} catch(e) { +} catch (e) { console.error("Error:", error.reason); // 🔴 Server ended with error } -Greetings.findOne({ name: 'John' }); // 🗑️ Data is NOT available +Greetings.findOne({ name: "John" }); // 🗑️ Data is NOT available ``` #### Meteor 2.x contrast For those familiar with legacy Meteor 2.x, the handling of client simulation and server response was managed using fibers, as explained in the following section. This comparison illustrates how async inclusion with standard promises has transformed the way Meteor operates in modern versions. -``` javascript -Meteor.call('greetUser', 'John', function(error, result) { +```javascript +Meteor.call("greetUser", "John", function (error, result) { if (error) { console.error("Error:", error.reason); // 🔴 Server ended with error } else { console.log("Result:", result); // 🟢 Server ended with success } - Greetings.findOne({ name: 'John' }); // 🗑️ Data is NOT available + Greetings.findOne({ name: "John" }); // 🗑️ Data is NOT available }); // 🔵 Client simulation -Greetings.findOne({ name: 'John' }); // 🧾 Data is available (Optimistic-UI) +Greetings.findOne({ name: "John" }); // 🧾 Data is available (Optimistic-UI) ``` - `Meteor.apply` is just like `Meteor.call`, except that the method arguments are @@ -504,8 +499,6 @@ different collections. We hope to lift this restriction in a future release. - - ```js import { Meteor } from "meteor/meteor"; import { check } from "meteor/check"; diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index c72bded0bc..00bdfa2916 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -53,7 +53,8 @@ This is the default command. Simply running `meteor` is the same as `meteor run` | `--mobile-server ` | Location where mobile builds connect (defaults to local IP and port). Can include URL scheme (e.g., https://example.com:443) | | `--cordova-server-port ` | Local port where Cordova will serve content | | `--production` | Simulate production mode. Minify and bundle CSS and JS files | -| `--raw-logs` | Run without parsing logs from stdout and stderr | +| `--raw-logs` | Run without parsing logs from stdout and stderr (default: true) | +| `--timestamps` | Run with timestamps in logs, the same as passing `--raw-logs=false`. | | `--settings`, `-s ` | Set optional data for Meteor.settings on the server | | `--release ` | Specify the release of Meteor to use | | `--verbose` | Print all output from builds logs | @@ -247,10 +248,10 @@ If you run `meteor create` without arguments, Meteor will launch an interactive Typescript # To create an app using TypeScript and React Vue # To create a basic Vue3-based app Svelte # To create a basic Svelte app - Tailwind # To create an app using React and Tailwind - Chakra-ui # To create an app Chakra UI and React - Solid # To create a basic Solid app - Apollo # To create a basic Apollo + React app + Tailwind # To create an app using React and Tailwind + Chakra-ui # To create an app Chakra UI and React + Solid # To create a basic Solid app + Apollo # To create a basic Apollo + React app Bare # To create an empty app ``` ::: @@ -359,7 +360,7 @@ To learn more about the recommended file structure for Meteor apps, check the [M ## meteor generate {meteorgenerate} -``meteor generate`` is a command to generate boilerplate for your current project. `meteor generate` receives a name as a parameter, and generates files containing code to create a [Collection](https://docs.meteor.com/api/collections.html) with that name, [Methods](https://docs.meteor.com/api/meteor.html#methods) to perform basic CRUD operations on that Collection, and a [Subscription](https://docs.meteor.com/api/meteor.html#Meteor-publish) to read its data with reactivity from the client. +``meteor generate`` is a command to generate boilerplate for your current project. `meteor generate` receives a name as a parameter, and generates files containing code to create a [Collection](https://docs.meteor.com/api/collections.html) with that name, [Methods](https://docs.meteor.com/api/meteor.html#methods) to perform basic CRUD operations on that Collection, and a [Subscription](https://docs.meteor.com/api/meteor.html#Meteor-publish) to read its data with reactivity from the client. If you run ``meteor generate`` without arguments, it will ask you for a name, and name the auto-generated Collection accordingly. It will also ask if you do want Methods for your API and Publications to be generated as well. diff --git a/v3-docs/docs/components/helpers/ParamTable.vue b/v3-docs/docs/components/helpers/ParamTable.vue index c3d1cdf9ed..8251d3a7a1 100644 --- a/v3-docs/docs/components/helpers/ParamTable.vue +++ b/v3-docs/docs/components/helpers/ParamTable.vue @@ -44,6 +44,9 @@ const sourceCode = `https://github.com/meteor/meteor/blob/devel/packages/${props + + + diff --git a/v3-docs/docs/tutorials/blaze/1.creating-the-app.md b/v3-docs/docs/tutorials/blaze/1.creating-the-app.md new file mode 100644 index 0000000000..5434274dbd --- /dev/null +++ b/v3-docs/docs/tutorials/blaze/1.creating-the-app.md @@ -0,0 +1,190 @@ +## 1: Creating the app + +### 1.1: Install Meteor + +First, we need to install Meteor by following this [installation guide](https://docs.meteor.com/about/install.html). + +### 1.2: Create Meteor Project + +The easiest way to setup Meteor with Blaze is by using the command `meteor create` with the option `--blaze` and your project name: + +```shell +meteor create --blaze simple-todos-blaze +``` + +Meteor will create all the necessary files for you. + +The files located in the `client` directory are setting up your client side (web), you can see for example `client/main.html` where Meteor is rendering your App main component into the HTML. + +Also, check the `server` directory where Meteor is setting up the server side (Node.js), you can see the `server/main.js` which would be a good place to initialize your MongoDB database with some data. You don't need to install MongoDB as Meteor provides an embedded version of it ready for you to use. + +You can now run your Meteor app using: + +```shell +meteor +``` + +Don't worry, Meteor will keep your app in sync with all your changes from now on. + +Take a quick look at all the files created by Meteor, you don't need to understand them now but it's good to know where they are. + +Your Blaze code will be located inside the `imports/ui` directory, and the `App.html` and `App.js` files will be the root component of your Blaze To-do app. We haven't made those yet but will soon. + +### 1.3: Create Task Component + +To start working on our todo list app, let’s replace the code of the default starter app with the code below. From there, we’ll talk about what it does. + +First, let’s remove the body from our HTML entry-point (leaving just the `` tag): + +::: code-group + +```html [client/main.html] + + Simple todo + +``` +::: + +Create a new directory named `imports` inside the `simple-todos-blaze` folder. In the `imports` folder, create another directory with the name `ui` and add an `App.html` file inside of it with the content below: + +::: code-group + +```html [imports/ui/App.html] + + {{> mainContainer }} + + + + + +``` +::: + +We just created two templates, the `mainContainer`, which will be rendered in the body of our app, and it will show a header and a list of tasks that will render each item using the `task` template. Now, we need some data to present sample tasks on this page. + +### 1.4: Create Sample Tasks + +Create a new file called `App.js` in your `ui` folder. + +Inside your entry-point `main.js` file, remove all the previous content and just add the code below to import the new file `imports/ui/App.js`: + +::: code-group + +```js [client/main.js] +import '../imports/ui/App.js'; +``` +::: + +As you are not connecting to your server and database yet, let’s define some sample data, which we will use shortly to render a list of tasks. Add the code below to the `App.js` file: + +::: code-group + +```js [imports/ui/App.js] +import { Template } from 'meteor/templating'; + +import './App.html'; + +Template.mainContainer.helpers({ + tasks: [ + { text: 'This is task 1' }, + { text: 'This is task 2' }, + { text: 'This is task 3' }, + ], +}); +``` +::: + +Adding a helper to the `mainContainer` template, you are able to define the array of tasks. When the app starts, the client-side entry-point will import the `App.js` file, which will also import the `App.html` template we created in the previous step. + +At this point meteor should be running on port 3000 so you can visit the running app and see your list with three tasks displayed at [http://localhost:3000/](http://localhost:3000/) - but if meteor is not running, go to your terminal and move to the top directory of your project and type `meteor` then press return to launch the app. + +All right! Let’s find out what all these bits of code are doing! + +### 1.5: Rendering Data + + + +Meteor parses HTML files and identifies three top-level tags: ``, ``, and `