diff --git a/.circleci/config.yml b/.circleci/config.yml
index 62b5e0a3aa..6736ff3860 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -76,10 +76,10 @@ run_save_node_bin: &run_save_node_bin
fi
# This environment is set to every job (and the initial build).
-build_machine_environment: &build_machine_environment
- # Specify that we want an actual machine (ala Circle 1.0), not a Docker image.
+build_machine_environment:
+ &build_machine_environment # Specify that we want an actual machine (ala Circle 1.0), not a Docker image.
docker:
- - image: meteor/circleci:2024.09.11-android-34-node-20
+ - image: meteor/circleci:2025.07.8-android-35-node-22
resource_class: large
environment:
# This multiplier scales the waitSecs for selftests.
@@ -104,8 +104,8 @@ build_machine_environment: &build_machine_environment
# These will be evaled before each command.
PRE_TEST_COMMANDS: |-
- ulimit -c unlimited; # Set core dump size as Ubuntu 14.04 lacks prlimit.
- ulimit -a # Display all ulimit settings for transparency.
+ ulimit -c unlimited; # Set core dump size as Ubuntu 14.04 lacks prlimit.
+ ulimit -a # Display all ulimit settings for transparency.
# This is only to make Meteor self-test not remind us that we can set
# this argument for self-tests.
@@ -115,6 +115,9 @@ build_machine_environment: &build_machine_environment
NUM_GROUPS: 12
RUNNING_AVG_LENGTH: 6
+ # Force modern bundler test
+ METEOR_MODERN: true
+
jobs:
Get Ready:
<<: *build_machine_environment
@@ -178,7 +181,7 @@ jobs:
command: |
eval $PRE_TEST_COMMANDS;
cd dev_bundle/lib
- ../../meteor npm install @types/node@20.10.5 --save-dev
+ ../../meteor npm install @types/node@22.7.4 --save-dev
# Ensure that meteor/tools has no TypeScript errors.
../../meteor npm install -g typescript
cd ../../
@@ -753,7 +756,7 @@ jobs:
Docs:
docker:
# This Node version should match that in the meteor/docs CircleCI config.
- - image: meteor/circleci:2024.09.11-android-34-node-20
+ - image: meteor/circleci:2025.07.8-android-35-node-22
resource_class: large
environment:
CHECKOUT_METEOR_DOCS: /home/circleci/test_docs
@@ -765,7 +768,9 @@ jobs:
if [[ -n "$CIRCLE_PULL_REQUEST" ]]; then
PR_NUMBER=$(echo $CIRCLE_PULL_REQUEST | sed 's|.*/pull/\([0-9]*\)|\1|')
PR_BRANCH=$(curl -s https://api.github.com/repos/meteor/meteor/pulls/$PR_NUMBER | jq -r .head.ref)
- git clone --branch $PR_BRANCH https://github.com/meteor/meteor.git ${CHECKOUT_METEOR_DOCS}
+ git clone https://github.com/meteor/meteor.git ${CHECKOUT_METEOR_DOCS}
+ cd ${CHECKOUT_METEOR_DOCS}
+ git fetch origin pull/$PR_NUMBER/head:$PR_BRANCH
else
git clone --branch $CIRCLE_BRANCH https://github.com/meteor/meteor.git ${CHECKOUT_METEOR_DOCS}
fi
diff --git a/.envrc b/.envrc
index 7909587fdd..e023176dc5 100644
--- a/.envrc
+++ b/.envrc
@@ -15,6 +15,10 @@ function @meteor {
"$ROOT_DIR/meteor" "$@"
}
+function @get-ready {
+ @meteor --get-ready
+}
+
function @test-packages {
TINYTEST_FILTER="$1" @meteor test-packages --exclude-archs=web.browser.legacy,web.cordova
}
@@ -23,6 +27,10 @@ function @test-self {
@meteor self-test "$@"
}
+function @test-in-console {
+ "$ROOT_DIR/packages/test-in-console/run.sh" "$@"
+}
+
function @check-syntax {
node "$ROOT_DIR/scripts/admin/check-legacy-syntax/check-syntax.js"
}
@@ -46,4 +54,60 @@ function @docs-start {
function @docs-migration-start {
npm run docs:dev --prefix "$ROOT_DIR/v3-docs/v3-migration-docs"
-}
\ No newline at end of file
+}
+
+function @get-changes {
+ git diff --numstat HEAD~1 HEAD | awk '($1 + $2) <= 5000 {print $3}'
+}
+
+function @summarize-changes {
+ changes=$(@get-changes)
+
+ if [ -n "$changes" ]; then
+ changes=$(git diff HEAD~1 HEAD -- $(echo "$changes" | tr '\n' ' '))
+ else
+ changes=$(git diff HEAD~1 HEAD)
+ fi
+
+ echo "$changes" | llm -s "Summarize the following changes in a few sentences:"
+}
+
+function @packages-bumped {
+ git diff --name-only devel...$(git branch --show-current) | grep "packages/.*/package.js$" | while IFS= read -r file; do
+ if ! git show devel:$file > /dev/null 2>&1; then
+ continue
+ fi
+
+ old=$(git show devel:$file | grep -o "version: *['\"][^'\"]*['\"]" | sed "s/version: *.['\"]//;s/['\"].*//")
+ version=$(grep -o "version: *['\"][^'\"]*['\"]" "$file" | sed "s/version: *.['\"]//;s/['\"].*//")
+ name=$(grep -o "name: *['\"][^'\"]*['\"]" "$file" | sed "s/name: *.['\"]//;s/['\"].*//")
+
+ pkg_name=$(echo "$file" | sed -E 's|packages/([^/]*/)?([^/]*)/package\.js|\2|')
+
+ version_in_red=$(tput setaf 1)$version$(tput sgr0)
+
+ if [[ "$version" != "$old" ]]; then
+ echo "- $pkg_name@$version_in_red"
+ fi
+ done
+}
+
+function @packages-bumped-npm {
+ git diff --name-only devel...$(git branch --show-current) | grep "npm-packages/.*/package.json$" | while IFS= read -r file; do
+ if ! git show devel:$file > /dev/null 2>&1; then
+ continue
+ fi
+
+ old=$(git show devel:$file | grep -o "version: *['\"][^'\"]*['\"]" | sed "s/version: *.['\"]//;s/['\"].*//")
+ version=$(grep -o "\"version\": *['\"][^'\"]*['\"]" "$file" | sed "s/\"version\": *.['\"]//;s/['\"].*//")
+ name=$(grep -o "\"name\": *['\"][^'\"]*['\"]" "$file" | sed "s/\"name\": *.['\"]//;s/['\"].*//")
+
+ pkg_name=$(echo "$file" | sed -E 's|npm-packages/([^/]*/)?([^/]*)/package\.json|\2|')
+
+ version_in_red=$(tput setaf 1)$version$(tput sgr0)
+
+ if [[ "$version" != "$old" ]]; then
+ echo "- $pkg_name@$version_in_red"
+ fi
+ done
+}
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 51963d8eed..343cc5ee6a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,124 +1,178 @@
Project:Accounts:Password:
- - packages/accounts-password/**/*
+ - changed-files:
+ - any-glob-to-any-file: packages/accounts-password/**/*
Project:Accounts:UI:
- - packages/meteor-developer-config-ui/**/*
- - packages/github-config-ui/**/*
- - packages/google-config-ui/**/*
- - packages/twitter-config-ui/**/*
- - packages/facebook-config-ui/**/*
- - packages/accounts-ui/**/*
- - packages/accounts-ui-unstyled/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/meteor-developer-config-ui/**/*
+ - packages/github-config-ui/**/*
+ - packages/google-config-ui/**/*
+ - packages/twitter-config-ui/**/*
+ - packages/facebook-config-ui/**/*
+ - packages/accounts-ui/**/*
+ - packages/accounts-ui-unstyled/**/*
Project:CSS:
- - packages/non-core/less/**/*
- - packages/minifier-css/**/*
- - packages/standard-minifier-css/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/non-core/less/**/*
+ - packages/minifier-css/**/*
+ - packages/standard-minifier-css/**/*
Project:DDP:
- - packages/ddp-common/**/*
- - packages/ddp-rate-limiter/**/*
- - packages/ddp-server/**/*
- - packages/ddp-client/**/*
- - packages/ddp/**/*
- - packages/socket-stream-client/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/ddp-common/**/*
+ - packages/ddp-rate-limiter/**/*
+ - packages/ddp-server/**/*
+ - packages/ddp-client/**/*
+ - packages/ddp/**/*
+ - packages/socket-stream-client/**/*
Project:EJSON:
- - packages/ejson/**/*
+ - changed-files:
+ - any-glob-to-any-file: packages/ejson/**/*
Project:HMR:
- - packages/hot-code-push/**/*
- - packages/hot-module-replacement/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/hot-code-push/**/*
+ - packages/hot-module-replacement/**/*
Project:Isobuild:Minifiers:
- - packages/minifier-css/**/*
- - packages/minifier-js/**/*
- - packages/standard-minifier-js/**/*
- - packages/standard-minifier-css/**/*
- - packages/standard-minifiers/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/minifier-css/**/*
+ - packages/minifier-js/**/*
+ - packages/standard-minifier-js/**/*
+ - packages/standard-minifier-css/**/*
+ - packages/standard-minifiers/**/*
Project:Isobuild:
- - tools/isobuild/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - tools/isobuild/**/*
Project:JS Environment:Typescript:
- - packages/typescript/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/typescript/**/*
Project:JS Environment:
- - packages/babel-compiler/**/*
- - packages/babel-runtime/**/*
- - packages/ecmascript/**/*
- - packages/ecmascript-runtime/**/*
- - packages/ecmascript-runtime-client/**/*
- - packages/ecmascript-runtime-server/**/*
- - packages/es5-shim/**/*
- - packages/jshint/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/babel-compiler/**/*
+ - packages/babel-runtime/**/*
+ - packages/ecmascript/**/*
+ - packages/ecmascript-runtime/**/*
+ - packages/ecmascript-runtime-client/**/*
+ - packages/ecmascript-runtime-server/**/*
+ - packages/es5-shim/**/*
+ - packages/jshint/**/*
Project:Livequery:
- - packages/livedata/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/livedata/**/*
Project:Minimongo:
- - packages/minimongo
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/minimongo
Project:Mobile:
- - tools/cordova/**/*
- - packages/launch-screen/**/*
- - packages/mobile-experience/**/*
- - packages/mobile-status-bar/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - tools/cordova/**/*
+ - packages/launch-screen/**/*
+ - packages/mobile-experience/**/*
+ - packages/mobile-status-bar/**/*
Project:Mongo Driver:
- - packages/mongo/**/*
- - packages/mongo-dev-server/**/*
- - packages/mongo-id/**/*
- - packages/mongo-livedata/**/*
- - packages/disable-oplog/**/*
- - packages/non-core/mongo-decimal/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/mongo/**/*
+ - packages/mongo-dev-server/**/*
+ - packages/mongo-id/**/*
+ - packages/mongo-livedata/**/*
+ - packages/disable-oplog/**/*
+ - packages/non-core/mongo-decimal/**/*
Project:NPM:
- - npm-packages/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - npm-packages/**/*
Project:Release Process:
- - scripts/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - scripts/**/*
Project:Tool:
- - tools/**/*
- - packages/meteor-tool/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - tools/**/*
+ - packages/meteor-tool/**/*
Project:Tool:Shell:
- - tools/console/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - tools/console/**/*
Project:Utilities:Email:
- - packages/email/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/email/**/*
Project:Utilities:HTTP:
- - packages/deprecated/http/**/*
- - packages/fetch/**/*
- - packages/url/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/deprecated/http/**/*
+ - packages/fetch/**/*
+ - packages/url/**/*
Project:Webapp:
- - packages/webapp/**/*
- - packages/webapp-hashing/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/webapp/**/*
+ - packages/webapp-hashing/**/*
Project:Windows:
- - scripts/windows/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - scripts/windows/**/*
Project:Webapp:Browser Policy:
- - packages/browser-policy/**/*
- - packages/browser-policy-common/**/*
- - packages/browser-policy-content/**/*
- - packages/browser-policy-framing/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/browser-policy/**/*
+ - packages/browser-policy-common/**/*
+ - packages/browser-policy-content/**/*
+ - packages/browser-policy-framing/**/*
Project:Examples:
- - tools/cli/example-repositories.js
+ - changed-files:
+ - any-glob-to-any-file:
+ - tools/cli/example-repositories.js
Project:Dynamic Import:
- - packages/dynamic-import/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - packages/dynamic-import/**/*
Project:Docs:
- - docs/**/*
- - v3-docs/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - docs/**/*
+ - v3-docs/**/*
Project:Guide:
- - guide/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - guide/**/*
github_actions:
- - ./github/**/*
+ - changed-files:
+ - any-glob-to-any-file:
+ - ./github/**/*
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
new file mode 100644
index 0000000000..a75f5b81a1
--- /dev/null
+++ b/.github/scripts/inactive-issues.js
@@ -0,0 +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 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 page = 1;
+ 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,
+ page,
+ sort: 'updated',
+ direction: 'asc'
+ });
+
+ if (!data.length) break;
+ results.push(...data);
+
+ if (data.length < per_page) {
+ keepGoing = false;
+ } else {
+ page++;
+ await new Promise((r) => setTimeout(r, 120));
+ }
+ }
+ return results;
+ }
+ // 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;
+ }
+ }
+
+ 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 {
+ 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']
+ });
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body: COMMENT_90_TEXT
+ });
+ 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/.github/workflows/check-code-style.yml b/.github/workflows/check-code-style.yml
index 5b7dbbac4c..8b24944b3f 100644
--- a/.github/workflows/check-code-style.yml
+++ b/.github/workflows/check-code-style.yml
@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version: 22.x
- run: npm ci
- name: Run ESLint@8
run: npx eslint@8 "./npm-packages/meteor-installer/**/*.js"
diff --git a/.github/workflows/check-syntax.yml b/.github/workflows/check-syntax.yml
index 1f9f151c94..a20bb011ca 100644
--- a/.github/workflows/check-syntax.yml
+++ b/.github/workflows/check-syntax.yml
@@ -1,6 +1,5 @@
name: Check legacy syntax
on:
- - push
- pull_request
jobs:
check-code-style:
@@ -9,7 +8,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version: 22.x
- run: cd scripts/admin/check-legacy-syntax && npm ci
- name: Check syntax
run: cd scripts/admin/check-legacy-syntax && node check-syntax.js
diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml
index f485145f18..bd3d801966 100644
--- a/.github/workflows/guide.yml
+++ b/.github/workflows/guide.yml
@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 12.x
+ node-version: 22.x
- name: Build the Guide
run: npm ci && npm run build
- name: Deploy to Netlify for preview
diff --git a/.github/workflows/inactive-issues.yml b/.github/workflows/inactive-issues.yml
new file mode 100644
index 0000000000..2d8cba7a3f
--- /dev/null
+++ b/.github/workflows/inactive-issues.yml
@@ -0,0 +1,24 @@
+name: Inactive Issues Management
+
+on:
+ schedule:
+ # “At 01:00 on Saturday.”
+ - cron: '0 1 * * 6'
+ # Allows to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ manage-inactive-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+
+ - name: Manage inactive issues
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const script = require('./.github/scripts/inactive-issues.js')
+ await script({github, context})
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index a9d25b1e47..da6690818c 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -17,6 +17,6 @@ jobs:
label:
runs-on: ubuntu-latest
steps:
- - uses: actions/labeler@v4
+ - uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/meteor-selftest-windows.yml b/.github/workflows/meteor-selftest-windows.yml
index d78f633ec3..c328e7bf2d 100644
--- a/.github/workflows/meteor-selftest-windows.yml
+++ b/.github/workflows/meteor-selftest-windows.yml
@@ -18,19 +18,27 @@ env:
TIMEOUT_SCALE_FACTOR: 20
METEOR_HEADLESS: true
SELF_TEST_EXCLUDE: '^NULL-LEAVE-THIS-HERE-NULL$'
+ METEOR_MODERN: true
jobs:
test:
runs-on: windows-2019-meteor
+ concurrency:
+ group: ${{ github.head_ref }}-meteor-selftest-windows
+ cancel-in-progress: true
steps:
+ - name: cleanup
+ shell: powershell
+ run: Remove-Item -Recurse -Force ${{ github.workspace }}\*
+
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
with:
- node-version: 20.x
+ node-version: 22.x
- name: Install dependencies
shell: pwsh
@@ -45,7 +53,7 @@ jobs:
.\scripts\windows\ci\test.ps1
- name: Cache dependencies
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: |
.\dev_bundle
diff --git a/.github/workflows/npm-eslint-plugin-meteor.yml b/.github/workflows/npm-eslint-plugin-meteor.yml
index 29a72f0005..ff51a2e847 100644
--- a/.github/workflows/npm-eslint-plugin-meteor.yml
+++ b/.github/workflows/npm-eslint-plugin-meteor.yml
@@ -21,7 +21,7 @@ jobs:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version: 22.x
cache: npm
- run: npm ci
- run: npm test
diff --git a/.github/workflows/run-profiler.yml b/.github/workflows/run-profiler.yml
new file mode 100644
index 0000000000..c19ff83a7d
--- /dev/null
+++ b/.github/workflows/run-profiler.yml
@@ -0,0 +1,46 @@
+name: Run Profiler
+
+on:
+ issue_comment:
+ types: [created]
+
+jobs:
+ run-profiler:
+ if: github.event.issue.pull_request && contains(github.event.comment.body , '/profile')
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Checkout Pull Request
+ run: gh pr checkout ${{ github.event.issue.number }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22.x
+
+ - name: Set ENVs
+ run: |
+ value="VALUE"
+ echo "Key=$value" >> $GITHUB_ENV
+
+ PR_NUMBER="${{ github.event.issue.number }}"
+ echo "PrNumber=$PR_NUMBER" >> $GITHUB_ENV
+
+ - name: Run CI
+ run: |
+ echo "Running meteor profiler..."
+ echo $PR_NUMBER
+ git status
+ ls
+
+ - name: Comment PR
+ uses: thollander/actions-comment-pull-request@v3
+ with:
+ message: |
+ Hello world !!!! :wave:
+ this is pr number: #${{ env.PrNumber }}
+ testing value: ${{ env.Key }}
+ pr-number: ${{ github.event.issue.number }}
diff --git a/.github/workflows/test-deprecated-packages.yml b/.github/workflows/test-deprecated-packages.yml
new file mode 100644
index 0000000000..e4ed12fe92
--- /dev/null
+++ b/.github/workflows/test-deprecated-packages.yml
@@ -0,0 +1,52 @@
+name: Test Deprecated Packages
+
+# Disabled until we figure out how to fix the error from puppeteer
+# Runs on Travis CI for now
+#
+#on:
+# push:
+# branches:
+# - main
+# pull_request:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ concurrency:
+ group: ${{ github.head_ref }}-test-deprecated-packages
+ cancel-in-progress: true
+ timeout-minutes: 60
+
+ env:
+ PUPPETEER_DOWNLOAD_PATH: /home/runner/.npm/chromium
+
+ steps:
+ - name: Update and install dependencies
+ run: sudo apt-get update && sudo apt-get install -y libnss3 g++-12
+
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.15.1
+
+ - name: Cache Node.js modules
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.npm
+ .meteor
+ .babel-cache
+ dev_bundle
+ /home/runner/.npm/chromium
+ key: ${{ runner.os }}-node-${{ hashFiles('meteor', '**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: ./packages/test-in-console/run.sh
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fe19b9b3e9..4742e13056 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,7 @@ packages/**/.npm
# doc files should not be committed
packages/**/*.docs.js
+
+#cursor
+.cursorignore
+.cursorrules
diff --git a/.travis.yml b/.travis.yml
index 4d9997a4d7..3e63726f4c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,7 +4,7 @@ dist: jammy
sudo: required
services: xvfb
node_js:
- - "20.15.1"
+ - "22.17.0"
cache:
directories:
- ".meteor"
@@ -16,6 +16,8 @@ env:
- CXX=g++-12
- phantom=false
- PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium
+ - TEST_PACKAGES_EXCLUDE=stylus
+ - METEOR_MODERN=true
addons:
apt:
sources:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 92ef3e891f..fa910d2ee5 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -19,7 +19,7 @@ To report an issue in one of the projects listed below, please email code-of-con
The Code of Conduct panel is a moderation team that handle code of conduct issues. The makeup of this team is as follows:
* CEO at Meteor Software - Frederico Maia Arantes
-* Software Engineer at Meteor Software - Denilson Silva
+* CTO at Meteor Software - Henrique Albert
* CEO at High Impact Tech - Alim S. Gafar
Members of the CoCP team will be added for a 1-year term and will be re-confirmed on a yearly basis.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1694ca521f..c646fad457 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,7 +21,7 @@ There are many ways to contribute to the Meteor Project. Here’s a list of tech
There are also several ways to contribute to the Meteor Project outside of GitHub, like organizing or speaking at [Meetups](https://forums.meteor.com/c/meetups) and events and helping to moderate our [forums](https://forums.meteor.com/).
-If you can think of any changes to the project, [documentation](https://github.com/meteor/meteor/tree/devel/docs), or [guide](https://github.com/meteor/meteor/tree/devel/guide) that would improve the contributor experience, let us know by opening an issue!
+If you can think of any changes to the project, [documentation](https://github.com/meteor/meteor/tree/devel/v3-docs), or [guide](https://github.com/meteor/meteor/tree/devel/guide) that would improve the contributor experience, let us know by opening an issue!
### Finding work
@@ -43,15 +43,14 @@ Reviewers are members of the community who help with Pull Requests reviews.
Current Reviewers:
- [meteor](https://github.com/meteor/meteor)
- - [@denihs](https://github.com/denihs)
- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
- [@henriquealbert](https://github.com/henriquealbert)
- [@aquinoit](https://github.com/aquinoit)
- [@Grubba27](https://github.com/Grubba27)
-- [@filipenevola](https://github.com/filipenevola)
+ - [@italojs](https://github.com/italojs)
+ - [@nachocodoner](https://github.com/nachocodoner)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
- [@zodern](https://github.com/zodern)
-- [@CaptainN](https://github.com/CaptainN)
- [@radekmie](https://github.com/radekmie)
#### Core Committer
@@ -59,16 +58,15 @@ Current Reviewers:
The contributors with commit access to meteor/meteor are employees of Meteor Software LP or community members who have distinguished themselves in other contribution areas or members of partner companies. If you want to become a core committer, please start writing PRs.
Current Core Team:
-- [@denihs](https://github.com/denihs)
-- [@zodern](https://github.com/zodern)
-- [@filipenevola](https://github.com/filipenevola)
-- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
-- [@henriquealbert](https://github.com/henriquealbert)
-- [@Grubba27](https://github.com/Grubba27)
+- [meteor](https://github.com/meteor/meteor)
+ - [@fredmaiaarantes](https://github.com/fredmaiaarantes)
+ - [@henriquealbert](https://github.com/henriquealbert)
+ - [@Grubba27](https://github.com/Grubba27)
+ - [@italojs](https://github.com/italojs)
+ - [@nachocodoner](https://github.com/nachocodoner)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
-- [@CaptainN](https://github.com/CaptainN)
+- [@zodern](https://github.com/zodern)
- [@radekmie](https://github.com/radekmie)
-- [@matheusccastroo](https://github.com/matheusccastroo)
### Tracking project work
@@ -157,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/README.md b/README.md
index a6249e4157..11aa1c4c6c 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,18 @@
[](https://app.travis-ci.com/github/meteor/meteor)
[](https://app.circleci.com/pipelines/github/meteor/meteor?branch=devel)
-[](https://meteor.com)
+[](https://meteor.com)



@@ -54,23 +56,20 @@ How about trying a tutorial to get started with your favorite technology?
| [
React](https://docs.meteor.com/tutorials/react/) |
| - |
| [
Blaze](https://blaze-tutorial.meteor.com/) |
-| [
Vue](https://vue-tutorial.meteor.com/) |
-| [
Svelte](https://svelte-tutorial.meteor.com/) |
-
-Next, read the [documentation](https://docs.meteor.com/) and get some [examples](https://github.com/meteor/examples).
+| [
Vue](https://docs.meteor.com/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.html) |
# 🚀 Quick Start
On your platform, use this line:
```shell
-> npm install -g meteor
+> npx meteor
```
🚀 To create a project:
```shell
-> meteor create my-app
+> meteor create
```
☄️ Run it:
@@ -84,10 +83,9 @@ meteor
**Building an application with Meteor?**
-* Deploy on [Meteor Cloud](https://www.meteor.com/cloud)
+* Deploy on [Galaxy](https://galaxycloud.app)
* Discuss on [Forums](https://forums.meteor.com/)
-* Join the Meteor Discord by clicking this [invite link](https://discord.gg/hZkTCaVjmT).
-* Announcement list. Subscribe in the [footer](https://www.meteor.com/).
+* Join the [Meteor Discord](https://discord.gg/hZkTCaVjmT)
Interested in helping or contributing to Meteor? These resources will help:
@@ -96,15 +94,3 @@ Interested in helping or contributing to Meteor? These resources will help:
* [Contribution guidelines](CONTRIBUTING.md)
* [Feature requests](https://github.com/meteor/meteor/discussions/)
* [Issue tracker](https://github.com/meteor/meteor/issues)
-
-To uninstall Meteor:
- - If installed via npm, run:
- ```shell
- meteor-installer uninstall
- ```
- - If installed via curl, run:
- ```shell
- rm -rf ~/.meteor
- sudo rm /usr/local/bin/meteor
- ```
-To find more information about installation, [read here](https://docs.meteor.com/about/install.html#uninstall).
diff --git a/SECURITY.md b/SECURITY.md
index 278969b992..aaf7b92c93 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,7 +4,8 @@
| Version | Support Status
| ------- | --------------
-| 2.x.y | ✅ all security issues
+| 3.x.y | ✅ all security issues
+| 2.x.y | ⚠️ only major security issues (Until 2025-07)
| <= 1.12.x | ❌ no longer supported
## Reporting a Vulnerability
diff --git a/docs/_config.yml b/docs/_config.yml
index 0556242aac..395aaca44c 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -88,7 +88,6 @@ sidebar_categories:
- packages/server-render
- packages/spacebars
- packages/standard-minifier-css
- - packages/underscore
- packages/url
- packages/webapp
- packages/packages-listing
@@ -214,6 +213,7 @@ redirects:
/#/full/accounts-setusername: 'api/passwords.html#accounts-setusername'
/#/full/accounts-addemail: 'api/passwords.html#accounts-addemail'
/#/full/accounts-removeemail: 'api/passwords.html#accounts-removeemail'
+ /#/full/accounts_replaceemail: 'api/passwords.html#Accounts-replaceEmail'
/#/full/accounts_verifyemail: 'api/passwords.html#Accounts-verifyEmail'
/#/full/accounts-finduserbyusername: 'api/passwords.html#accounts-finduserbyusername'
/#/full/accounts-finduserbyemail: 'api/passwords.html#accounts-finduserbyemail'
@@ -393,7 +393,6 @@ redirects:
/#/full/oauth-encryption: 'packages/oauth-encryption.html'
/#/full/random: 'packages/random.html'
/#/full/spiderable: 'packages/spiderable.html'
- /#/full/underscore: 'packages/underscore.html'
/#/full/webapp: 'packages/webapp.html'
'#meteor_isclient': 'api/core.html#Meteor-isClient'
'#meteor_isserver': 'api/core.html#Meteor-isServer'
@@ -669,6 +668,5 @@ redirects:
'#oauth-encryption': 'packages/oauth-encryption.html'
'#random': 'packages/random.html'
'#spiderable': 'packages/spiderable.html'
- '#underscore': 'packages/underscore.html'
'#webapp': 'packages/webapp.html'
'#pkg_spacebars': 'packages/spacebars.html'
diff --git a/docs/generators/changelog/versions/0-before-2.10.md b/docs/generators/changelog/versions/0-before-2.10.md
index 1774e87739..54c856eaa8 100644
--- a/docs/generators/changelog/versions/0-before-2.10.md
+++ b/docs/generators/changelog/versions/0-before-2.10.md
@@ -2935,6 +2935,7 @@ N/A
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
+ firefoxIOS: 100,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobile_safari: [9, 2], // 9.2.0+
diff --git a/docs/generators/changelog/versions/3.0.0.md b/docs/generators/changelog/versions/3.0.0.md
index c5fa1f1a1e..4c863db6ad 100644
--- a/docs/generators/changelog/versions/3.0.0.md
+++ b/docs/generators/changelog/versions/3.0.0.md
@@ -55,6 +55,7 @@
- `Accounts.sendVerificationEmail`
- `Accounts.addEmail`
- `Accounts.removeEmail`
+ - `Accounts.replaceEmailAsync`
- `Accounts.verifyEmail`
- `Accounts.createUserVerifyingEmail`
- `Accounts.createUser`
diff --git a/docs/history.md b/docs/history.md
index 5f9df5f8d7..3d5da68a9d 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -8,6 +8,308 @@
[//]: # (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
+
+- **MongoDB Driver Upgrades**
+ - Upgraded core MongoDB driver to `6.16.0` to address latest issues reported [#13710](https://github.com/meteor/meteor/pull/13710)
+ - Introduced `npm-mongo-legacy` to maintain compatibility with MongoDB 3.6 via `mongodb@6.9.0` [#13736](https://github.com/meteor/meteor/pull/13736)
+ - Mitigated a cursor leak issue by synchronizing `next()` and `close()` operations [#13786](https://github.com/meteor/meteor/pull/13786)
+
+- **Improved SWC integration**
+ - Fixed edge cases in config cache invalidation [#13809](https://github.com/meteor/meteor/pull/13809)
+ - Ensured `@swc/helpers` is consistently used for better bundle size and performance [#13820](https://github.com/meteor/meteor/pull/13820)
+ - Updated to SWC `1.12.14` [#13851](https://github.com/meteor/meteor/pull/13851)
+
+- **Tooling and Build System**
+ - Fixed regression affecting rebuild behavior [#13810](https://github.com/meteor/meteor/pull/13810)
+ - Addressed issues getting performance profiles in mounted volumes [#13827](https://github.com/meteor/meteor/pull/13827)
+ - Fallback to Babel parser when Acorn fails to parse source code [#13844](https://github.com/meteor/meteor/pull/13844)
+
+- **Mobile Support**
+ - Upgraded Cordova platform to version 14 [#13837](https://github.com/meteor/meteor/pull/13837)
+
+- **Developer Experience**
+ - Added TypeScript types for `isModern` and `getMinimumBrowserVersions` functions [#13704](https://github.com/meteor/meteor/pull/13704)
+ - Enhanced CLI help output and documented admin commands [#13826](https://github.com/meteor/meteor/pull/13826)
+
+- **Vite Tooling**
+ - Updated official Meteor + Vite skeletons [#13835](https://github.com/meteor/meteor/pull/13835)
+
+- **Runtime & Dependencies**
+ - Updated to Node.js `22.18.0` and NPM `10.9.3` [#13877](https://github.com/meteor/meteor/pull/13877)
+ - Bumped `meteor-node-stubs` to `1.2.21` [#13825](https://github.com/meteor/meteor/pull/13825)
+
+All Merged PRs@[GitHub PRs 3.3.1](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3.1)
+
+#### Breaking Changes
+
+##### MongoDB Driver Upgrades
+
+If you're using MongoDB 3.6 or earlier, install the new legacy package:
+
+```bash
+meteor add npm-mongo-legacy
+```
+This will pin the MongoDB driver to 6.9.0 for compatibility.
+
+If you’re on MongoDB 4+, the default [MongoDB driver 6.16.0](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.16.0) is applied automatically.
+
+Please migrate your database as soon as possible to MongoDB 5 onward, as [MongoDB driver 6.17.0](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.17.0) will drop MongoDB 4 support. We’ll keep offering `npm-mongo-legacy` so you can keep getting Meteor updates with your existing MongoDB legacy version.
+
+##### Cordova Upgrade
+
+The Cordova platform has been upgraded to version 14. Refer to the [Cordova Changelog](https://cordova.apache.org/announcements/2025/03/26/cordova-android-14.0.0.html) for more details on the changes and migration steps.
+
+#### Internal API changes
+
+N/A
+
+#### Migration Steps
+
+Please run the following command to update your project:
+
+```bash
+meteor update --release 3.3.1
+```
+
+---
+
+While this is a patch release, Meteor 3.3, a recent minor update, introduced a modern build stack that’s now the default for new apps. Here’s how you can migrate to it.
+
+**Add this to your `package.json` to enable the new modern build stack:**
+
+```json
+"meteor": {
+ "modern": true
+}
+```
+
+Check the docs for help with the SWC migration, especially if your project uses many Babel plugins.
+
+[Modern Transpiler: SWC docs](https://docs.meteor.com/about/modern-build-stack/transpiler-swc.html)
+
+If you find any issues, please report them to the [Meteor issues tracker](https://github.com/meteor/meteor).
+
+#### Bumped Meteor Packages
+
+- babel-compiler@7.12.1
+- callback-hook@1.6.1
+- ecmascript@0.16.12
+- minifier-js@3.0.3
+- minimongo@2.0.3
+- modern-browsers@0.2.3
+- mongo@2.1.3
+- npm-mongo-legacy@6.9.0
+- npm-mongo@6.16.0
+- standard-minifier-js@3.1.1
+- tinytest@1.3.2
+- typescript@5.6.5
+- meteor-tool@3.3.1
+
+#### Bumped NPM Packages
+
+- meteor-node-stubs@1.2.21
+
+#### Special thanks to
+
+✨✨✨
+
+- [@nachocodoner](https://github.com/nachocodoner)
+- [@italojs](https://github.com/italojs)
+- [@StorytellerCZ](https://github.com/StorytellerCZ)
+- [@JorgenVatle](https://github.com/JorgenVatle)
+- [@welkinwong](https://github.com/welkinwong)
+- [@Saksham-Goel1107](https://github.com/Saksham-Goel1107)
+
+✨✨✨
+
+## v3.3.0, 2025-06-11
+
+### Highlights
+
+- Support SWC transpiler and minifier for faster dev and builds [PR#13657](https://github.com/meteor/meteor/pull/13657), [PR#13715](https://github.com/meteor/meteor/pull/13715)
+- Switch to `@parcel/watcher` for improved native file watching [PR#13699](https://github.com/meteor/meteor/pull/13699), [#13707](https://github.com/meteor/meteor/pull/13707)
+- Default to modern architecture, skip legacy processing [PR#13665](https://github.com/meteor/meteor/pull/13665), [PR#13698](https://github.com/meteor/meteor/pull/13698)
+- Optimize SQLite for faster startup and better performance [PR#13702](https://github.com/meteor/meteor/pull/13702)
+- Support CPU profiling in Meteor 3 bundler [PR#13650](https://github.com/meteor/meteor/pull/13650)
+- Improve `meteor profile`: show rebuild steps and total, support `--build` [PR#16](https://github.com/meteor/performance/pull/16), [PR#13694](https://github.com/meteor/meteor/pull/13694)
+- Improve `useFind` and `useSubscribe` React hooks
+- Add `replaceEmailAsync` helper to Accounts [PR#13677](https://github.com/meteor/meteor/pull/13677)
+- Fix user agent detection and oplog collection filtering
+- Refine type definitions for Meteor methods and SSR's ServerSink
+- Allow opting out of usage stats with `DO_NOT_TRACK`
+- Update Node to 22.16.0 and Express to 5.1.0
+
+All Merged PRs@[GitHub PRs 3.3](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3)
+
+React Packages Changelog: [react-meteor-data@4.0.0](https://github.com/meteor/react-packages/tree/master/packages/react-meteor-data/CHANGELOG.md#v400-2025-06-11)
+
+#### Breaking Changes
+
+- File watching strategy switched to `@parcel/watcher`
+ - Most setups should be fine, but if issues appear, like when using WSL with host, volumes, or remote setups—switch to polling.
+ - Set `METEOR_WATCH_FORCE_POLLING=true` to enable polling.
+ - Set `METEOR_WATCH_POLLING_INTERVAL_MS=1000` to adjust the interval.
+
+- `react-meteor-data@4.0.0`
+ - Independent from the core, only applies if upgraded manually.
+ - useFind describes no deps by default [PR#431](https://github.com/meteor/react-packages/pull/431)
+
+#### Internal API changes
+
+- `express@5.1.0` - Depends on Meteor’s `webapp` package.
+ - Deprecates non-native promise usage [#154](https://github.com/pillarjs/router/pull/154)
+ - Use `async/await` or `Promise.resolve` when defining endpoints to avoid deprecation warnings.
+
+#### Migration Steps
+
+Please run the following command to update your project:
+
+```bash
+meteor update --release 3.3
+```
+
+To apply react-meteor-data changes:
+
+```bash
+meteor add react-meteor-data@4.0.0
+```
+
+**Add this to your `package.json` to enable the new modern build stack:**
+
+```json
+"meteor": {
+ "modern": true
+}
+```
+
+> These settings are on by default for new apps.
+
+On activate `modern` your app will be updated to use SWC transpiler. It will automatically fallback to Babel if your code can't be transpiled with SWC.
+
+Check the docs for help with the SWC migration, especially if your project uses many Babel plugins.
+
+[Modern Transpiler: SWC docs](https://docs.meteor.com/about/modern-build-stack/transpiler-swc.html)
+
+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.1
+- accounts-password@3.2.0
+- autoupdate@2.0.1
+- babel-compiler@7.12.0
+- boilerplate-generator@2.0.1
+- ddp-client@3.1.1
+- ecmascript@0.16.11
+- ejson@1.1.5
+- meteor@2.1.1
+- minifier-js@3.0.2
+- modern-browsers@0.2.2
+- mongo@2.1.2
+- server-render@0.4.3
+- socket-stream-client@0.6.1
+- standard-minifier-js@3.1.0
+- typescript@5.6.4
+- webapp@2.0.7
+- meteor-tool@3.3.0
+
+#### Bumped NPM Packages
+
+- meteor-node-stubs@1.2.17
+
+#### Special thanks to
+
+✨✨✨
+
+- [@nachocodoner](https://github.com/nachocodoner)
+- [@italojs](https://github.com/italojs)
+- [@Grubba27](https://github.com/Grubba27)
+- [@zodern](https://github.com/zodern)
+- [@9Morello](https://github.com/9Morello)
+- [@welkinwong](https://github.com/welkinwong)
+- [@Poyoman39](https://github.com/Poyoman39)
+- [@PedroMarianoAlmeida](https://github.com/PedroMarianoAlmeida)
+- [@harryadel](https://github.com/harryadel)
+- [@ericm546](https://github.com/ericm546)
+- [@StorytellerCZ](https://github.com/StorytellerCZ)
+
+✨✨✨
+
+
## v3.0.1, 2024-07-16
### Highlights
@@ -4651,6 +4953,7 @@ N/A
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
+ firefoxIOS: 100,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobile_safari: [9, 2], // 9.2.0+
diff --git a/docs/jsdoc/jsdoc-conf.json b/docs/jsdoc/jsdoc-conf.json
index 82ed30dd10..4fd2956fc1 100644
--- a/docs/jsdoc/jsdoc-conf.json
+++ b/docs/jsdoc/jsdoc-conf.json
@@ -4,7 +4,6 @@
"packages/ddp/sockjs-0.3.4.js",
"packages/test-in-browser/diff_match_patch_uncompressed.js",
"packages/jquery/jquery.js",
- "packages/underscore/underscore.js",
"packages/json/json2.js",
"packages/minimongo/minimongo_tests.js",
"tools/node_modules",
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 7965d6e9ba..9d4a412012 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -4,16 +4,41 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
- "@babel/parser": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
- "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
+ "@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true
},
+ "@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true
+ },
+ "@babel/parser": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.28.0"
+ }
+ },
+ "@babel/types": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ }
+ },
"@jsdoc/salty": {
- "version": "0.2.6",
- "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
- "integrity": "sha512-aA+awb5yoml8TQ3CzI5Ue7sM3VMRC4l1zJJW4fgZ8OCL1wshJZhNzaf0PL85DSnOUw6QuFgeHGD/eq/xwwAF2g==",
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
+ "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==",
"dev": true,
"requires": {
"lodash": "^4.17.21"
@@ -26,31 +51,31 @@
"dev": true
},
"@meteorjs/meteor-theme-hexo": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/@meteorjs/meteor-theme-hexo/-/meteor-theme-hexo-2.0.8.tgz",
- "integrity": "sha512-LQIFN05wBMjX7SXgW5CFVTfolDWMuknoypwQ0czl/44LYRBR4/LYZUgX6c+1vLjloJb+5+2HTvMGlVN9Wo1MKA==",
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@meteorjs/meteor-theme-hexo/-/meteor-theme-hexo-2.0.9.tgz",
+ "integrity": "sha512-8ncpsN8MAe1F7cJBtcPgH3JE36WV03oo5mPkA1yMdRmv2kq8AQpKnd4ok0U1cr5NIIBMupLtsHDLm8PhTQcUdw==",
"dev": true
},
"@types/linkify-it": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
- "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"@types/markdown-it": {
- "version": "12.2.3",
- "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
- "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"requires": {
- "@types/linkify-it": "*",
- "@types/mdurl": "*"
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
}
},
"@types/mdurl": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
- "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"JSONStream": {
@@ -75,16 +100,6 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
- "accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "dev": true,
- "requires": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- }
- },
"acorn": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
@@ -239,9 +254,9 @@
"optional": true
},
"aws4": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
- "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
+ "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"dev": true,
"optional": true
},
@@ -479,9 +494,9 @@
}
},
"brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -506,9 +521,9 @@
"dev": true
},
"bytes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
- "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true
},
"cache-base": {
@@ -537,17 +552,38 @@
}
},
"call-bind": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
- "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"optional": true,
"requires": {
+ "call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.1"
+ "set-function-length": "^1.2.2"
+ }
+ },
+ "call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ }
+ },
+ "call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
}
},
"camel-case": {
@@ -801,18 +837,26 @@
}
},
"compression": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
- "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dev": true,
"requires": {
- "accepts": "~1.3.5",
- "bytes": "3.0.0",
- "compressible": "~2.0.16",
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
"debug": "2.6.9",
- "on-headers": "~1.0.2",
- "safe-buffer": "5.1.2",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
"vary": "~1.1.2"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ }
}
},
"concat-map": {
@@ -1040,6 +1084,18 @@
"domelementtype": "1"
}
},
+ "dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ }
+ },
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -1092,14 +1148,11 @@
}
},
"es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
- "optional": true,
- "requires": {
- "get-intrinsic": "^1.2.4"
- }
+ "optional": true
},
"es-errors": {
"version": "1.3.0",
@@ -1108,6 +1161,16 @@
"dev": true,
"optional": true
},
+ "es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "es-errors": "^1.3.0"
+ }
+ },
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1323,17 +1386,33 @@
"dev": true
},
"get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"optional": true,
"requires": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ }
+ },
+ "get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
}
},
"get-value": {
@@ -1401,14 +1480,11 @@
"dev": true
},
"gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
- "optional": true,
- "requires": {
- "get-intrinsic": "^1.1.3"
- }
+ "optional": true
},
"graceful-fs": {
"version": "4.2.11",
@@ -1480,17 +1556,10 @@
"es-define-property": "^1.0.0"
}
},
- "has-proto": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
- "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
- "dev": true,
- "optional": true
- },
"has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"optional": true
},
@@ -2390,9 +2459,9 @@
},
"dependencies": {
"cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
@@ -2416,12 +2485,6 @@
"striptags": "^3.1.1"
}
},
- "marked": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
- "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
- "dev": true
- },
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -2652,12 +2715,12 @@
"dev": true
},
"is-core-module": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
- "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"requires": {
- "hasown": "^2.0.0"
+ "hasown": "^2.0.2"
}
},
"is-data-descriptor": {
@@ -2839,21 +2902,21 @@
"optional": true
},
"jsdoc": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz",
- "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz",
+ "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==",
"dev": true,
"requires": {
"@babel/parser": "^7.20.15",
"@jsdoc/salty": "^0.2.1",
- "@types/markdown-it": "^12.2.3",
+ "@types/markdown-it": "^14.1.1",
"bluebird": "^3.7.2",
"catharsis": "^0.9.0",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.2",
"klaw": "^3.0.0",
- "markdown-it": "^12.3.2",
- "markdown-it-anchor": "^8.4.1",
+ "markdown-it": "^14.1.0",
+ "markdown-it-anchor": "^8.6.7",
"marked": "^4.0.10",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
@@ -2861,12 +2924,6 @@
"underscore": "~1.13.2"
},
"dependencies": {
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@@ -2886,9 +2943,9 @@
"dev": true
},
"underscore": {
- "version": "1.13.6",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
- "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
+ "version": "1.13.7",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
+ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"dev": true
}
}
@@ -2901,13 +2958,14 @@
"optional": true
},
"json-stable-stringify": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
- "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
+ "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"optional": true,
"requires": {
- "call-bind": "^1.0.5",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
@@ -3005,12 +3063,12 @@
}
},
"linkify-it": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
- "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"requires": {
- "uc.micro": "^1.0.1"
+ "uc.micro": "^2.0.0"
}
},
"locate-path": {
@@ -3157,16 +3215,17 @@
}
},
"markdown-it": {
- "version": "12.3.2",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
- "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"requires": {
"argparse": "^2.0.1",
- "entities": "~2.1.0",
- "linkify-it": "^3.0.1",
- "mdurl": "^1.0.1",
- "uc.micro": "^1.0.5"
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
},
"dependencies": {
"argparse": {
@@ -3176,9 +3235,9 @@
"dev": true
},
"entities": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
- "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true
}
}
@@ -3189,6 +3248,19 @@
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
"dev": true
},
+ "marked": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
+ "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
+ "dev": true
+ },
+ "math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "optional": true
+ },
"math-random": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz",
@@ -3196,9 +3268,9 @@
"dev": true
},
"mdurl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
- "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true
},
"micromatch": {
@@ -3239,6 +3311,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
+ "optional": true,
"requires": {
"mime-db": "1.52.0"
}
@@ -3302,25 +3375,25 @@
"dev": true
},
"moment-timezone": {
- "version": "0.5.45",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
- "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
+ "version": "0.5.48",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"dev": true,
"requires": {
"moment": "^2.29.4"
}
},
"morgan": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
- "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"dev": true,
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
- "on-headers": "~1.0.2"
+ "on-headers": "~1.1.0"
}
},
"ms": {
@@ -3342,9 +3415,9 @@
}
},
"nan": {
- "version": "2.19.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
- "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
+ "version": "2.23.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
+ "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"dev": true,
"optional": true
},
@@ -3395,9 +3468,9 @@
"optional": true
},
"negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"dev": true
},
"neo-async": {
@@ -3561,9 +3634,9 @@
}
},
"on-headers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
- "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true
},
"once": {
@@ -3754,6 +3827,12 @@
"dev": true,
"optional": true
},
+ "punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "dev": true
+ },
"qs": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.1.tgz",
@@ -4124,12 +4203,12 @@
}
},
"resolve": {
- "version": "1.22.8",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
- "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"requires": {
- "is-core-module": "^2.13.0",
+ "is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
@@ -4201,9 +4280,9 @@
"dev": true
},
"send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"requires": {
"debug": "2.6.9",
@@ -4245,15 +4324,23 @@
}
},
"serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"requires": {
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
- "send": "0.18.0"
+ "send": "0.19.0"
+ },
+ "dependencies": {
+ "encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true
+ }
}
},
"set-blocking": {
@@ -4789,15 +4876,15 @@
"optional": true
},
"uc.micro": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
- "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true
},
"uglify-js": {
- "version": "3.17.4",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
- "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"optional": true
},
diff --git a/docs/package.json b/docs/package.json
index 79c7f3bf31..f8aea85ac7 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -6,6 +6,8 @@
"version": "3.9.0"
},
"devDependencies": {
+ "@meteorjs/meteor-hexo-config": "1.0.14",
+ "@meteorjs/meteor-theme-hexo": "^2.0.9",
"canonical-json": "0.0.4",
"chexo": "1.0.7",
"handlebars": "4.7.7",
@@ -17,15 +19,13 @@
"hexo-server": "1.0.0",
"hexo-versioned-netlify-redirects": "1.1.0",
"jsdoc": "^4.0.2",
- "@meteorjs/meteor-hexo-config": "1.0.14",
- "@meteorjs/meteor-theme-hexo": "2.0.8",
"showdown": "1.9.1",
"underscore": "1.13.1"
},
"scripts": {
"list-core-packages": "node ./generators/packages-listing/script.js",
"generate-history": "node ./generators/changelog/script.js",
- "build": "npm run list-core-packages && npm run generate-history && jsdoc/jsdoc.sh && chexo @meteorjs/meteor-hexo-config -- generate",
+ "build": "npm run list-core-packages && jsdoc/jsdoc.sh && chexo @meteorjs/meteor-hexo-config -- generate",
"clean": "hexo clean; rm data/data.js data/names.json",
"test": "npm run clean; npm run build",
"predeploy": "npm run build",
diff --git a/docs/source/api/accounts-multi.md b/docs/source/api/accounts-multi.md
index 5dbc358c66..a008b7d807 100644
--- a/docs/source/api/accounts-multi.md
+++ b/docs/source/api/accounts-multi.md
@@ -151,7 +151,8 @@ password-based users or from an external service login flow. `options` may come
from an untrusted client so make sure to validate any values you read from
it. The `user` argument is created on the server and contains a
proposed user object with all the automatically generated fields
-required for the user to log in, including the `_id`.
+required for the user to log in, including a temporary `_id` (the final _id is
+generated upon document insertion and not available in this function).
The function should return the user document (either the one passed in or a
newly-created object) with whatever modifications are desired. The returned
diff --git a/docs/source/api/collections.md b/docs/source/api/collections.md
index a90c3e24ae..355686e820 100644
--- a/docs/source/api/collections.md
+++ b/docs/source/api/collections.md
@@ -201,10 +201,10 @@ changes the documents in a cursor will trigger a recomputation. To
disable this behavior, pass `{reactive: false}` as an option to
`find`.
-Note that when `fields` are specified, only changes to the included
+Note that when `projection` are specified, only changes to the included
fields will trigger callbacks in `observe`, `observeChanges` and
invalidations in reactive computations using this cursor. Careful use
-of `fields` allows for more fine-grained reactivity for computations
+of `projection` allows for more fine-grained reactivity for computations
that don't depend on an entire document.
On the client, there will be a period of time between when the page loads and
@@ -216,6 +216,8 @@ collections will be empty.
Equivalent to [`find`](#find)`(selector, options).`[`fetch`](#fetch)`()[0]` with
`options.limit = 1`.
+> **Note**: The `fields` option is deprecated in favor of `projection`, which aligns with MongoDB's official terminology and driver. Using `projection` ensures consistency and clarity in specifying which fields to include or exclude in query results.
+
{% apibox "Mongo.Collection#findOneAsync" %}
Async version of [`findOne`](#findOne) that return a `Promise`.
@@ -950,30 +952,30 @@ document objects, and returns -1 if the first document comes first in order,
1 if the second document comes first, or 0 if neither document comes before
the other. This is a Minimongo extension to MongoDB.
-
Field Specifiers
+
Projection Specifiers
Queries can specify a particular set of fields to include or exclude from the
-result object.
+result object using the `projection` option.
-To exclude specific fields from the result objects, the field specifier is a
+To exclude specific fields from the result objects, the projection specifier is a
dictionary whose keys are field names and whose values are `0`. All unspecified
fields are included.
```js
-Users.find({}, { fields: { password: 0, hash: 0 } });
+Users.find({}, { projection: { password: 0, hash: 0 } });
```
To include only specific fields in the result documents, use `1` as
the value. The `_id` field is still included in the result.
```js
-Users.find({}, { fields: { firstname: 1, lastname: 1 } });
+Users.find({}, { projection: { firstname: 1, lastname: 1 } });
```
With one exception, it is not possible to mix inclusion and exclusion styles:
the keys must either be all 1 or all 0. The exception is that you may specify
`_id: 0` in an inclusion specifier, which will leave `_id` out of the result
-object as well. However, such field specifiers can not be used with
+object as well. However, such projection specifiers can not be used with
[`observeChanges`](#observe_changes), [`observe`](#observe), cursors returned
from a [publish function](#meteor_publish), or cursors used in
`{% raw %}{{#each}}{% endraw %}` in a template. They may be used with [`fetch`](#fetch),
@@ -994,10 +996,12 @@ Users.insert({
name: 'Yagami Light',
});
-Users.findOne({}, { fields: { 'alterEgos.name': 1, _id: 0 } });
+Users.findOne({}, { projection: { 'alterEgos.name': 1, _id: 0 } });
// Returns { alterEgos: [{ name: 'Kira' }, { name: 'L' }] }
```
+> Note: The `fields` option is deprecated in favor of `projection`, which is the standard term used by MongoDB. Using `projection` ensures compatibility with MongoDB's documentation and drivers.
+
See
the MongoDB docs for details of the nested field rules and array behavior.
diff --git a/docs/source/api/passwords.md b/docs/source/api/passwords.md
index bb0ddf6b17..908617aa3e 100644
--- a/docs/source/api/passwords.md
+++ b/docs/source/api/passwords.md
@@ -59,6 +59,8 @@ By default, an email address is added with `{ verified: false }`. Use
[`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail) to send an
email with a link the user can use to verify their email address.
+{% apibox "Accounts.replaceEmailAsync" %}
+
{% apibox "Accounts.removeEmail" %}
{% apibox "Accounts.verifyEmail" %}
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/docs/source/index.md b/docs/source/index.md
index 441b4dbe2b..208309701d 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -2,7 +2,7 @@
title: Docs
---
-> Meteor 2.x uses the deprecated Node.js 14. Meteor 3.0 has been released and runs on Node.js 20. Check out our [v3 docs](https://v3-docs.meteor.com) and [migration guide](https://v3-migration-docs.meteor.com/).
+> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 has been released with support for the latest Node.js LTS version. For more information, please consult our [migration guide](https://v3-migration-docs.meteor.com/) and the [latest docs](https://v3-docs.meteor.com).
What is Meteor?
diff --git a/docs/source/install.md b/docs/source/install.md
index 1db8b8f1c6..6a58d65cfd 100644
--- a/docs/source/install.md
+++ b/docs/source/install.md
@@ -8,7 +8,7 @@ You need to install the Meteor command line tool to create, run, and manage your
Node.js version
-> Meteor 2.x uses the deprecated Node.js 14. Meteor 3.0 has been released and runs on Node.js 20. Check out our [v3 docs](https://v3-docs.meteor.com) and [migration guide](https://v3-migration-docs.meteor.com/).
+> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 has been released with support for the latest Node.js LTS version. For more information, please consult our [migration guide](https://v3-migration-docs.meteor.com/) and the [latest docs](https://v3-docs.meteor.com).
- Node.js version >= 10 and <= 14 is required.
- We recommend you using [nvm](https://github.com/nvm-sh/nvm) or [Volta](https://volta.sh/) for managing Node.js versions.
@@ -30,7 +30,8 @@ You need to install the Meteor command line tool to create, run, and manage your
Install the latest official version of Meteor.js from your terminal by running one of the commands below. You can check our [changelog](https://docs.meteor.com/changelog.html) for the release notes.
-> Run `node -v` to ensure you are using Node.js 14. Meteor 3.0, currently in its Release Candidate version, runs on Node.js v20.
+> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 is released with support for the latest Node.js LTS version.
+> For more information, please consult our [migration guide](https://guide.meteor.com/3.0-migration.html) and the [new docs](https://docs.meteor.com/).
For Windows, Linux and OS X, you can run the following command:
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/index.md b/guide/source/index.md
index 427c62ae52..0f801b9b88 100644
--- a/guide/source/index.md
+++ b/guide/source/index.md
@@ -3,7 +3,7 @@ title: Introduction
description: This is the guide for using Meteor, a full-stack JavaScript platform for developing modern web and mobile applications.
---
-> Meteor 2.x uses the deprecated Node.js 14. Meteor 3.0 has been released and runs on Node.js 20. Check out our [v3 docs](https://v3-docs.meteor.com) and [migration guide](https://v3-migration-docs.meteor.com/).
+> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3.x has been released with support for the latest Node.js LTS version. For more information, please consult our [migration guide](https://v3-migration-docs.meteor.com/) and the [latest docs](https://docs.meteor.com).
What is Meteor?
diff --git a/guide/source/methods.md b/guide/source/methods.md
index a9689a2ef1..3db5ca7c3c 100644
--- a/guide/source/methods.md
+++ b/guide/source/methods.md
@@ -171,35 +171,35 @@ updateText.run.call({ userId: 'abcd' }, {
As you can see, this approach to calling Methods results in a better development workflow - you can more easily deal with the different parts of the Method separately and test your code without having to deal with Meteor internals. But this approach requires you to write a lot of boilerplate on the Method definition side.
-
Advanced Methods with mdg:validated-method
+
Advanced Methods with jam:method
-To alleviate some of the boilerplate that's involved in correct Method definitions, we've published a wrapper package called `mdg:validated-method` that does most of this for you. Here's the same Method as above, but defined with the package:
+To alleviate some of the boilerplate that's involved in correct Method definitions, you can use a package called `jam:method` that does most of this for you. Here's the same Method as above, but defined with the package:
```js
-import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { createMethod } from 'meteor/jam:method';
-export const updateText = new ValidatedMethod({
+export const updateText = createMethod({
name: 'todos.updateText',
- validate: new SimpleSchema({
+ schema: new SimpleSchema({
todoId: { type: String },
newText: { type: String }
- }).validator(),
- run({ todoId, newText }) {
- const todo = Todos.findOne(todoId);
-
+ }),
+ async run({ todoId, newText }) {
+ const todo = await Todos.findOneAsync(todoId);
+
if (!todo.editableBy(this.userId)) {
throw new Meteor.Error('todos.updateText.unauthorized',
'Cannot edit todos in a private list that is not yours');
}
- Todos.update(todoId, {
+ Todos.updateAsync(todoId, {
$set: { text: newText }
});
}
});
```
-You call it the same way you call the advanced Method above, but the Method definition is significantly simpler. We believe this style of Method lets you clearly see the important parts - the name of the Method sent over the wire, the format of the expected arguments, and the JavaScript namespace by which the Method can be referenced. Validated methods only accept a single argument and a callback function.
+You call it the same way you call the advanced Method above, but the Method definition is significantly simpler. We believe this style of Method lets you clearly see the important parts - the name of the Method sent over the wire, the format of the expected arguments, and the JavaScript namespace by which the Method can be referenced.
Error handling
@@ -227,17 +227,13 @@ When the server was not able to complete the user's desired action because of a
When a Method call fails because the arguments are of the wrong type, it's good to throw a `ValidationError`. This works like `Meteor.Error`, but is a custom constructor that enforces a standard error format that can be read by different form and validation libraries. In particular, if you are calling this Method from a form, throwing a `ValidationError` will make it possible to display nice error messages next to particular fields in the form.
-When you use `mdg:validated-method` with `simpl-schema` as demonstrated above, this type of error is thrown for you.
-
-Read more about the error format in the [`mdg:validation-error` docs](https://atmospherejs.com/mdg/validation-error).
-
Handling errors
When you call a Method, any errors thrown by it will be returned in the callback. At this point, you should identify which error type it is and display the appropriate message to the user. In this case, it is unlikely that the Method will throw a `ValidationError` or an internal server error, so we will only handle the unauthorized error:
```js
// Call the Method
-updateText.call({
+updateText({
todoId: '12345',
newText: 'This is a todo item.'
}, (err, res) => {
@@ -261,7 +257,7 @@ We'll talk about how to handle the `ValidationError` in the section on forms bel
Errors in Method simulation
-When a Method is called, it usually runs twice---once on the client to simulate the result for Optimistic UI, and again on the server to make the actual change to the database. This means that if your Method throws an error, it will likely fail on the client _and_ the server. For this reason, `ValidatedMethod` turns on undocumented option in Meteor to avoid calling the server-side implementation if the simulation throws an error.
+When a Method is called, it usually runs twice---once on the client to simulate the result for Optimistic UI, and again on the server to make the actual change to the database. This means that if your Method throws an error, it will likely fail on the client _and_ the server. For this reason, `jam:method` turns on [an option](https://github.com/jamauro/method#options-for-meteorapplyasync) in Meteor to avoid calling the server-side implementation if the simulation throws an error.
While this behavior is good for saving server resources in cases where a Method will certainly fail, it's important to make sure that the simulation doesn't throw an error in cases where the server Method would have succeeded (for example, if you didn't load some data on the client that the Method needs to do the simulation properly). In this case, you can wrap server-side-only logic in a block that checks for a method simulation:
@@ -283,13 +279,13 @@ const amountRegEx = /^\d*\.(\d\d)?$/;
// This Method encodes the form validation requirements.
// By defining them in the Method, we do client and server-side
// validation in one place.
-export const insert = new ValidatedMethod({
+export const insert = createMethod({
name: 'Invoices.methods.insert',
- validate: new SimpleSchema({
+ schema: new SimpleSchema({
email: { type: String, regEx: emailRegEx },
description: { type: String, min: 5 },
amount: { type: String, regEx: amountRegEx }
- }).validator(),
+ }),
run(newInvoice) {
// In here, we can be sure that the newInvoice argument is
// validated.
@@ -299,7 +295,7 @@ export const insert = new ValidatedMethod({
'Must be logged in to create an invoice.');
}
- Invoices.insert(newInvoice)
+ Invoices.insertAsync(newInvoice)
}
});
```
@@ -355,7 +351,7 @@ Template.Invoices_newInvoice.events({
amount: event.target.amount.value
};
- insert.call(data, (err, res) => {
+ insert(data, (err, res) => {
if (err) {
if (err.error === 'validation-error') {
// Initialize error object
@@ -434,9 +430,9 @@ If we defined this Method in client and server code, as all Methods should be, a
The client enters a special mode where it tracks all changes made to client-side collections, so that they can be rolled back later. When this step is complete, the user of your app sees their UI update instantly with the new content of the client-side database, but the server hasn't received any data yet.
-If an exception is thrown from the Method simulation, then by default Meteor ignores it and continues to step (2). If you are using `ValidatedMethod` or pass a special `throwStubExceptions` option to `Meteor.apply`, then an exception thrown from the simulation will stop the server-side Method from running at all.
+If an exception is thrown from the Method simulation, then by default Meteor ignores it and continues to step (2). If you are using `jam:method` or pass a special `throwStubExceptions` [option](https://github.com/jamauro/method#options-for-meteorapplyasync) to `Meteor.apply`, then an exception thrown from the simulation will stop the server-side Method from running at all.
-The return value of the Method simulation is discarded, unless the `returnStubValue` option is passed when calling the Method, in which case it is returned to the Method caller. ValidatedMethod passes this option by default.
+The return value of the Method simulation is discarded, unless the `returnStubValue` option is passed when calling the Method, in which case it is returned to the Method caller. `jam:method` passes this option by default.
2. A `method` DDP message is sent to the server
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
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/security.md b/guide/source/security.md
index e50420b98d..157c60dd01 100644
--- a/guide/source/security.md
+++ b/guide/source/security.md
@@ -36,9 +36,9 @@ Each of these points will have their own section below.
Avoid allow/deny
-In this guide, we're going to take a strong position that using [allow](http://docs.meteor.com/#/full/allow) or [deny](http://docs.meteor.com/#/full/deny) to run MongoDB queries directly from the client is not a good idea. The main reason is that it is hard to follow the principles outlined above. It's extremely difficult to validate the complete space of possible MongoDB operators, which could potentially grow over time with new versions of MongoDB.
+In this guide, we're going to take a strong position that using [allow](https://docs.meteor.com/api/collections.html#Mongo-Collection-allow) or [deny](https://docs.meteor.com/api/collections.html#Mongo-Collection-deny) to run MongoDB queries directly from the client is not a good idea. The main reason is that it is hard to follow the principles outlined above. It's extremely difficult to validate the complete space of possible MongoDB operators, which could potentially grow over time with new versions of MongoDB.
-There have been several articles about the potential pitfalls of accepting MongoDB update operators from the client, in particular the [Allow & Deny Security Challenge](https://www.discovermeteor.com/blog/allow-deny-security-challenge/) and its [results](https://www.discovermeteor.com/blog/allow-deny-challenge-results/), both on the Discover Meteor blog.
+There have been several articles about the potential pitfalls of accepting MongoDB update operators from the client, in particular the [Allow & Deny Security Challenge](https://web.archive.org/web/20220705130732/https://www.discovermeteor.com/blog/allow-deny-security-challenge/) and its [results](https://web.archive.org/web/20220819163744/https://www.discovermeteor.com/blog/allow-deny-challenge-results/), both on the Discover Meteor blog.
Given the points above, we recommend that all Meteor apps should use Methods to accept data input from the client, and restrict the arguments accepted by each Method as tightly as possible.
@@ -80,9 +80,9 @@ Meteor.methods({
If someone comes along and passes a non-ID selector like `{}`, they will end up deleting the entire collection.
-mdg:validated-method
+jam:method
-To help you write good Methods that exhaustively validate their arguments, we've written a wrapper package for Methods that enforces argument validation. Read more about how to use it in the [Methods article](methods.html#validated-method). The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different.
+To help you write good Methods that exhaustively validate their arguments, you can use a community package for Methods that enforces argument validation. Read more about how to use it in the [Methods article](methods.html#jam-method). The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different.
Don't pass userId from the client
@@ -116,25 +116,25 @@ The _only_ times you should be passing any user ID as an argument are the follow
The best way to make your app secure is to understand all of the possible inputs that could come from an untrusted source, and make sure that they are all handled correctly. The easiest way to understand what inputs can come from the client is to restrict them to as small of a space as possible. This means your Methods should all be specific actions, and shouldn't take a multitude of options that change the behavior in significant ways. The end goal is that you can look at each Method in your app and validate or test that it is secure. Here's a secure example Method from the Todos example app:
```js
-export const makePrivate = new ValidatedMethod({
+export const makePrivate = new createMethod({
name: 'lists.makePrivate',
validate: new SimpleSchema({
listId: { type: String }
}).validator(),
- run({ listId }) {
+ async run({ listId }) {
if (!this.userId) {
throw new Meteor.Error('lists.makePrivate.notLoggedIn',
'Must be logged in to make private lists.');
}
- const list = Lists.findOne(listId);
+ const list = await Lists.findOneAsync(listId);
if (list.isLastPublicList()) {
throw new Meteor.Error('lists.makePrivate.lastPublicList',
'Cannot make the last public list private.');
}
- Lists.update(listId, {
+ await Lists.updateAsync(listId, {
$set: { userId: this.userId }
});
@@ -148,16 +148,16 @@ You can see that this Method does a _very specific thing_ - it makes a single li
However, this doesn't mean you can't have any flexibility in your Methods. Let's look at an example:
```js
-Meteor.users.methods.setUserData = new ValidatedMethod({
+Meteor.users.methods.setUserData = new createMethod({
name: 'Meteor.users.methods.setUserData',
validate: new SimpleSchema({
fullName: { type: String, optional: true },
dateOfBirth: { type: Date, optional: true },
}).validator(),
- run(fieldsToSet) {
- Meteor.users.update(this.userId, {
+ async run(fieldsToSet) {
+ return (await Meteor.users.updateAsync(this.userId, {
$set: fieldsToSet
- });
+ }));
}
});
```
@@ -200,7 +200,8 @@ if (Meteor.isServer) {
This will make every Method only callable 5 times per second per connection. This is a rate limit that shouldn't be noticeable by the user at all, but will prevent a malicious script from totally flooding the server with requests. You will need to tune the limit parameters to match your app's needs.
-If you're using validated methods, there's an available [ddp-rate-limiter-mixin](https://github.com/nlhuykhang/ddp-rate-limiter-mixin).
+If you're using `jam:method`, it comes with built in [rate-limiting](https://github.com/jamauro/method#rate-limiting).
+
Publications
@@ -274,10 +275,10 @@ Publications are not reactive, and they only re-run when the currently logged in
```js
// #1: Bad! If the owner of the list changes, the old owner will still see it
-Meteor.publish('list', function (listId) {
+Meteor.publish('list', async function (listId) {
check(listId, String);
- const list = Lists.findOne(listId);
+ const list = await Lists.findOneAsync(listId);
if (list.userId !== this.userId) {
throw new Meteor.Error('list.unauthorized',
@@ -351,7 +352,7 @@ export const MMR = {
```js
// In a file loaded on client and server
-Meteor.users.methods.updateMMR = new ValidatedMethod({
+Meteor.users.methods.updateMMR = new createMethod({
name: 'Meteor.users.methods.updateMMR',
validate: null,
run() {
diff --git a/guide/source/testing.md b/guide/source/testing.md
index f7500c2db4..7dfffb4a51 100644
--- a/guide/source/testing.md
+++ b/guide/source/testing.md
@@ -238,9 +238,11 @@ import { Tracker } from 'meteor/tracker';
const withDiv = function withDiv(callback) {
const el = document.createElement('div');
document.body.appendChild(el);
+ let view = null
try {
- callback(el);
+ view = callback(el);
} finally {
+ if (view) Blaze.remove(view)
document.body.removeChild(el);
}
};
@@ -248,9 +250,10 @@ const withDiv = function withDiv(callback) {
export const withRenderedTemplate = function withRenderedTemplate(template, data, callback) {
withDiv((el) => {
const ourTemplate = isString(template) ? Template[template] : template;
- Blaze.renderWithData(ourTemplate, data, el);
+ const view = Blaze.renderWithData(ourTemplate, data, el);
Tracker.flush();
callback(el);
+ return view
});
};
```
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/meteor b/meteor
index 47016dc625..517e4bd0fe 100755
--- a/meteor
+++ b/meteor
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
-BUNDLE_VERSION=20.17.0.6
-
+BUNDLE_VERSION=22.18.0.3
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.
@@ -123,6 +122,7 @@ fi
DEV_BUNDLE="$SCRIPT_DIR/dev_bundle"
METEOR="$SCRIPT_DIR/tools/index.js"
+PROCESS_REQUIRES="$SCRIPT_DIR/tools/node-process-warnings.js"
# Set the nofile ulimit as high as permitted by the hard-limit/kernel
if [ "$(ulimit -Sn)" != "unlimited" ]; then
@@ -148,5 +148,6 @@ fi
exec "$DEV_BUNDLE/bin/node" \
--max-old-space-size=4096 \
--no-wasm-code-gc \
+ --require="$PROCESS_REQUIRES"\
${TOOL_NODE_FLAGS} \
"$METEOR" "$@"
diff --git a/meteor.bat b/meteor.bat
index aa6b05d750..1f0f75d081 100644
--- a/meteor.bat
+++ b/meteor.bat
@@ -48,6 +48,7 @@ SET BABEL_CACHE_DIR=%~dp0\.babel-cache
"%~dp0\dev_bundle\bin\node.exe" ^
--no-wasm-code-gc ^
+ --require="%~dp0\tools\node-process-warnings.js" ^
%TOOL_NODE_FLAGS% ^
"%~dp0\tools\index.js" %*
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/eslint-plugin-meteor/scripts/dev-bundle-server-package.js b/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-server-package.js
index 1108a6ef0d..b29072cb17 100644
--- a/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-server-package.js
+++ b/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-server-package.js
@@ -9,7 +9,7 @@ var packageJson = {
private: true,
dependencies: {
promise: "8.1.0",
- "@meteorjs/reify": "0.24.0",
+ "@meteorjs/reify": "0.25.3",
"@babel/parser": "7.17.0",
"@types/underscore": "1.11.4",
underscore: "1.13.6",
diff --git a/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js b/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js
index 09adbc5e91..aedf49c8c0 100644
--- a/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js
+++ b/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js
@@ -10,13 +10,13 @@ var packageJson = {
dependencies: {
// Explicit dependency because we are replacing it with a bundled version
// and we want to make sure there are no dependencies on a higher version
- npm: "10.8.2",
+ npm: "10.9.3",
pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8",
"node-gyp": "9.4.0",
"@mapbox/node-pre-gyp": "1.0.11",
- typescript: "5.4.5",
- "@meteorjs/babel": "7.19.0-beta.3",
- "@meteorjs/reify": "0.24.0",
+ typescript: "5.6.3",
+ "@meteorjs/babel": "7.20.0",
+ "@meteorjs/reify": "0.25.3",
// So that Babel can emit require("@babel/runtime/helpers/...") calls.
"@babel/runtime": "7.15.3",
// For backwards compatibility with isopackets that still depend on
diff --git a/npm-packages/meteor-babel/package-lock.json b/npm-packages/meteor-babel/package-lock.json
index 2bca50faa9..3ded2f7c42 100644
--- a/npm-packages/meteor-babel/package-lock.json
+++ b/npm-packages/meteor-babel/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@meteorjs/babel",
- "version": "7.20.0-beta.5",
+ "version": "7.20.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -786,9 +786,9 @@
}
},
"@meteorjs/reify": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@meteorjs/reify/-/reify-0.25.2.tgz",
- "integrity": "sha512-mkaSPyzovKf86wSA4ouCmXUQkASA8qNCXp71/Tbm0tD/bpiaja3measRB1HPA+yLXq9Xq3+8GLh8ytJu98cwIQ==",
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@meteorjs/reify/-/reify-0.25.4.tgz",
+ "integrity": "sha512-/HwynJK85QtS2Rm26M9TS8aEMnqVJ2TIzJNJTGAQz+G6cTYmJGWaU4nFH96oxiDIBbnT6Y3TfX92HDuS9TtNRg==",
"requires": {
"acorn": "^8.8.1",
"magic-string": "^0.25.3",
@@ -797,21 +797,21 @@
},
"dependencies": {
"semver": {
- "version": "7.6.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
- "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
}
}
},
"@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"acorn": {
- "version": "8.11.3",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
- "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg=="
+ "version": "8.13.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
+ "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w=="
},
"ansi-colors": {
"version": "3.2.3",
diff --git a/npm-packages/meteor-babel/package.json b/npm-packages/meteor-babel/package.json
index e065b0f555..24771f96c0 100644
--- a/npm-packages/meteor-babel/package.json
+++ b/npm-packages/meteor-babel/package.json
@@ -1,7 +1,7 @@
{
"name": "@meteorjs/babel",
"author": "Meteor ",
- "version": "7.20.0-beta.5",
+ "version": "7.20.1",
"license": "MIT",
"type": "commonjs",
"description": "Babel wrapper package for use with Meteor",
@@ -42,7 +42,7 @@
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
- "@meteorjs/reify": "0.25.2",
+ "@meteorjs/reify": "0.25.4",
"babel-preset-meteor": "^7.10.0",
"babel-preset-minify": "^0.5.1",
"convert-source-map": "^1.6.0",
diff --git a/npm-packages/meteor-installer/README.md b/npm-packages/meteor-installer/README.md
index 6ad22281f3..1430bcab86 100644
--- a/npm-packages/meteor-installer/README.md
+++ b/npm-packages/meteor-installer/README.md
@@ -1,105 +1,106 @@
-## Meteor Installer
-
-### Recommended Versions
-
-- For Meteor 2 (Legacy)
- - Use Node.js 14.x
- - Use npm 6.x
-- For Meteor 3
- - Use Node.js 20.x or higher
- - Use npm 9.x or higher
-
-### Installation
-
-To install Meteor, run the following command:
-
-```bash
-npx meteor
-```
-
-It will install Meteor's latest version, alternatively you can install a specific version by running:
-
-```bash
-npx meteor@
-```
-
-This command will execute the Meteor installer without adding it permanently to your global npm packages.
-
-For more information, visit:
-
-- [Meteor 2 Installation Guide (Legacy)](https://v2-docs.meteor.com/install.html)
-- [**Meteor 3 Installation Guide**](https://v3-docs.meteor.com/about/install.html)
-
-
-
-
-
-### Important Note
-
-This npm package is not the Meteor framework itself; it is just an installer. Do not include it as a dependency in your project, as doing so may break your deployment.
-
-### Path Management
-
-By default, the Meteor installer adds its install path (by default, `~/.meteor/`) to your PATH by updating either your `.bashrc`, `.bash_profile`, or `.zshrc` as appropriate. To disable this behavior, install Meteor by running:
-
-```bash
-npm install -g meteor --ignore-meteor-setup-exec-path
-```
-
-(or by setting the environment variable `npm_config_ignore_meteor_setup_exec_path=true`)
-
-### Proxy Configuration
-
-Set the `https_proxy` or `HTTPS_PROXY` environment variable to a valid proxy URL to download Meteor files through the configured proxy.
-
-### Meteor Version Compatibility
-
-| NPM Package | Meteor Official Release |
-|-------------|-------------------------|
-| 3.0.3 | 3.0.3 |
-| 3.0.2 | 3.0.2 |
-| 3.0.1 | 3.0.1 |
-| 3.0.0 | 3.0 |
-| 2.16.0 | 2.16.0 |
-| 2.15.0 | 2.15.0 |
-| 2.14.0 | 2.14.0 |
-| 2.13.3 | 2.13.3 |
-| 2.13.1 | 2.13.1 |
-| 2.13.0 | 2.13.0 |
-| 2.12.1 | 2.12.0 |
-| 2.12.0 | 2.12.0 |
-| 2.11.0 | 2.11.0 |
-| 2.10.0 | 2.10.0 |
-| 2.9.1 | 2.9.1 |
-| 2.9.0 | 2.9.0 |
-| 2.8.2 | 2.8.1 |
-| 2.8.1 | 2.8.1 |
-| 2.8.0 | 2.8.0 |
-| 2.7.5 | 2.7.3 |
-| 2.7.4 | 2.7.3 |
-| 2.7.3 | 2.7.2 |
-| 2.7.2 | 2.7.1 |
-| 2.7.1 | 2.7 |
-| 2.7.0 | 2.7 |
-| 2.6.2 | 2.6.1 |
-| 2.6.1 | 2.6 |
-| 2.6.0 | 2.6 |
-| 2.5.9 | 2.5.8 |
-| 2.5.8 | 2.5.7 |
-| 2.5.7 | 2.5.6 |
-| 2.5.6 | 2.5.5 |
-| 2.5.5 | 2.5.4 |
-| 2.5.4 | 2.5.3 |
-| 2.5.3 | 2.5.2 |
-| 2.5.2 | 2.5.1 |
-| 2.5.1 | 2.5.1 |
-| 2.5.0 | 2.5 |
-| 2.4.1 | 2.4 |
-| 2.4.0 | 2.4 |
-| 2.3.7 | 2.3.6 |
-| 2.3.6 | 2.3.5 |
-| 2.3.5 | 2.3.5 |
-| 2.3.4 | 2.3.4 |
-| 2.3.3 | 2.3.2 |
-| 2.3.2 | 2.3.1 |
-| 2.3.1 | 2.2.1 |
+## Meteor Installer
+
+### Recommended Versions
+
+- For Meteor 2 (Legacy)
+ - Use Node.js 14.x
+ - Use npm 6.x
+- For Meteor 3
+ - Use Node.js 20.x or higher
+ - Use npm 9.x or higher
+
+### Installation
+
+To install Meteor, run the following command:
+
+```bash
+npx meteor
+```
+
+It will install Meteor's latest version, alternatively you can install a specific version by running:
+
+```bash
+npx meteor@
+```
+
+This command will execute the Meteor installer without adding it permanently to your global npm packages.
+
+For more information, visit:
+
+- [Meteor 2 Installation Guide (Legacy)](https://v2-docs.meteor.com/install.html)
+- [**Meteor 3 Installation Guide**](https://v3-docs.meteor.com/about/install.html)
+
+
+
+
+
+### Important Note
+
+This npm package is not the Meteor framework itself; it is just an installer. Do not include it as a dependency in your project, as doing so may break your deployment.
+
+### Path Management
+
+By default, the Meteor installer adds its install path (by default, `~/.meteor/`) to your PATH by updating either your `.bashrc`, `.bash_profile`, or `.zshrc` as appropriate. To disable this behavior, install Meteor by running:
+
+```bash
+npm install -g meteor --ignore-meteor-setup-exec-path
+```
+
+(or by setting the environment variable `npm_config_ignore_meteor_setup_exec_path=true`)
+
+### Proxy Configuration
+
+Set the `https_proxy` or `HTTPS_PROXY` environment variable to a valid proxy URL to download Meteor files through the configured proxy.
+
+### Meteor Version Compatibility
+
+| NPM Package | Meteor Official Release |
+|-------------|-------------------------|
+| 3.0.4 | 3.0.4 |
+| 3.0.3 | 3.0.3 |
+| 3.0.2 | 3.0.2 |
+| 3.0.1 | 3.0.1 |
+| 3.0.0 | 3.0 |
+| 2.16.0 | 2.16.0 |
+| 2.15.0 | 2.15.0 |
+| 2.14.0 | 2.14.0 |
+| 2.13.3 | 2.13.3 |
+| 2.13.1 | 2.13.1 |
+| 2.13.0 | 2.13.0 |
+| 2.12.1 | 2.12.0 |
+| 2.12.0 | 2.12.0 |
+| 2.11.0 | 2.11.0 |
+| 2.10.0 | 2.10.0 |
+| 2.9.1 | 2.9.1 |
+| 2.9.0 | 2.9.0 |
+| 2.8.2 | 2.8.1 |
+| 2.8.1 | 2.8.1 |
+| 2.8.0 | 2.8.0 |
+| 2.7.5 | 2.7.3 |
+| 2.7.4 | 2.7.3 |
+| 2.7.3 | 2.7.2 |
+| 2.7.2 | 2.7.1 |
+| 2.7.1 | 2.7 |
+| 2.7.0 | 2.7 |
+| 2.6.2 | 2.6.1 |
+| 2.6.1 | 2.6 |
+| 2.6.0 | 2.6 |
+| 2.5.9 | 2.5.8 |
+| 2.5.8 | 2.5.7 |
+| 2.5.7 | 2.5.6 |
+| 2.5.6 | 2.5.5 |
+| 2.5.5 | 2.5.4 |
+| 2.5.4 | 2.5.3 |
+| 2.5.3 | 2.5.2 |
+| 2.5.2 | 2.5.1 |
+| 2.5.1 | 2.5.1 |
+| 2.5.0 | 2.5 |
+| 2.4.1 | 2.4 |
+| 2.4.0 | 2.4 |
+| 2.3.7 | 2.3.6 |
+| 2.3.6 | 2.3.5 |
+| 2.3.5 | 2.3.5 |
+| 2.3.4 | 2.3.4 |
+| 2.3.3 | 2.3.2 |
+| 2.3.2 | 2.3.1 |
+| 2.3.1 | 2.2.1 |
diff --git a/npm-packages/meteor-installer/config.js b/npm-packages/meteor-installer/config.js
index b683d5a53c..4f96ac3ad4 100644
--- a/npm-packages/meteor-installer/config.js
+++ b/npm-packages/meteor-installer/config.js
@@ -1,7 +1,7 @@
const os = require('os');
const path = require('path');
-const METEOR_LATEST_VERSION = '3.0.3';
+const METEOR_LATEST_VERSION = '3.3.2';
const sudoUser = process.env.SUDO_USER || '';
function isRoot() {
return process.getuid && process.getuid() === 0;
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/npm-packages/meteor-installer/package-lock.json b/npm-packages/meteor-installer/package-lock.json
index 2c21581f61..95184ff90f 100644
--- a/npm-packages/meteor-installer/package-lock.json
+++ b/npm-packages/meteor-installer/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "meteor",
- "version": "3.0.3",
+ "version": "3.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meteor",
- "version": "3.0.3",
+ "version": "3.3.2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -194,9 +194,10 @@
"license": "MIT"
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
diff --git a/npm-packages/meteor-installer/package.json b/npm-packages/meteor-installer/package.json
index 04d61922a0..e9fc928561 100644
--- a/npm-packages/meteor-installer/package.json
+++ b/npm-packages/meteor-installer/package.json
@@ -1,30 +1,30 @@
-{
- "name": "meteor",
- "version": "3.0.3",
- "description": "Install Meteor",
- "main": "install.js",
- "scripts": {
- "install": "node cli.js install"
- },
- "author": "zodern",
- "license": "MIT",
- "type": "commonjs",
- "dependencies": {
- "7zip-bin": "^5.2.0",
- "cli-progress": "^3.11.1",
- "https-proxy-agent": "^5.0.1",
- "node-7z": "^2.1.2",
- "node-downloader-helper": "^2.1.9",
- "rimraf": "^6.0.1",
- "semver": "^7.3.7",
- "tar": "^6.1.11",
- "tmp": "^0.2.1"
- },
- "bin": {
- "meteor-installer": "cli.js"
- },
- "engines": {
- "node": ">=20.x",
- "npm": ">=10.x"
- }
-}
+{
+ "name": "meteor",
+ "version": "3.3.2",
+ "description": "Install Meteor",
+ "main": "install.js",
+ "scripts": {
+ "install": "node cli.js install"
+ },
+ "author": "zodern",
+ "license": "MIT",
+ "type": "commonjs",
+ "dependencies": {
+ "7zip-bin": "^5.2.0",
+ "cli-progress": "^3.11.1",
+ "https-proxy-agent": "^5.0.1",
+ "node-7z": "^2.1.2",
+ "node-downloader-helper": "^2.1.9",
+ "rimraf": "^6.0.1",
+ "semver": "^7.3.7",
+ "tar": "^6.1.11",
+ "tmp": "^0.2.1"
+ },
+ "bin": {
+ "meteor-installer": "cli.js"
+ },
+ "engines": {
+ "node": ">=20.x",
+ "npm": ">=10.x"
+ }
+}
diff --git a/npm-packages/meteor-node-stubs/.npmignore b/npm-packages/meteor-node-stubs/.npmignore
index 07e6e472cc..e69de29bb2 100644
--- a/npm-packages/meteor-node-stubs/.npmignore
+++ b/npm-packages/meteor-node-stubs/.npmignore
@@ -1 +0,0 @@
-/node_modules
diff --git a/npm-packages/meteor-node-stubs/CHANGELOG.md b/npm-packages/meteor-node-stubs/CHANGELOG.md
index 775ab2be53..c510d86ea7 100644
--- a/npm-packages/meteor-node-stubs/CHANGELOG.md
+++ b/npm-packages/meteor-node-stubs/CHANGELOG.md
@@ -1,3 +1,15 @@
+v1.2.13 - 2025-02-27
+
+* Update `elliptic` to v6.6.1 to address a security vulnerability.
+
+v1.2.12 - 2024-10-31
+
+* Update `elliptic` to v6.6.0 to address a security vulnerability.
+
+v1.2.11 - 2024-10-25
+
+* Update `rimraf` to v5 to remove vulnerable `inflight` dependency.
+
v1.2.8 - 2024-04-01
* Add new dependency `@meteorjs/crypto-browserify` to replace `crypto-browserify` as it had unsafe dependencies.
diff --git a/npm-packages/meteor-node-stubs/package-lock.json b/npm-packages/meteor-node-stubs/package-lock.json
index 4fba62f619..960afee14a 100644
--- a/npm-packages/meteor-node-stubs/package-lock.json
+++ b/npm-packages/meteor-node-stubs/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "meteor-node-stubs",
- "version": "1.2.10",
+ "version": "1.2.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meteor-node-stubs",
- "version": "1.2.10",
+ "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.5.7",
"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",
@@ -60,18 +60,133 @@
"vm-browserify": "^1.1.2"
},
"devDependencies": {
- "rimraf": "^2.7.1"
+ "rimraf": "^5.0.10"
}
},
- "node_modules/@meteorjs/crypto-browserify": {
- "version": "3.12.1",
- "resolved": "https://registry.npmjs.org/@meteorjs/crypto-browserify/-/crypto-browserify-3.12.1.tgz",
- "integrity": "sha512-ku23zGjNb1XJXPSPlt4QCPetc4s/cPrSahhSF11mFeQff5wBuu7QVVn/MusdHh+l3aKl6L1XMLV+OYLXvNDQ6Q==",
- "inBundle": true,
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@meteorjs/browserify-sign": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/@meteorjs/browserify-sign/-/browserify-sign-4.2.6.tgz",
+ "integrity": "sha512-xnQRbIrjHxaVbOEbzbcdav4QDRTnfRAVHi21SPosnGNiIHTdTeGQGmTF/f7GwntxqynabSifdBHeGA7W8lIKSQ==",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "bn.js": "^5.2.1",
+ "brorand": "^1.1.0",
+ "browserify-rsa": "^4.1.0",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "hash-base": "~3.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",
+ "parse-asn1": "^5.1.7",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/@meteorjs/browserify-sign/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/@meteorjs/browserify-sign/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/@meteorjs/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/@meteorjs/browserify-sign/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/@meteorjs/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/@meteorjs/create-ecdh": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@meteorjs/create-ecdh/-/create-ecdh-4.0.5.tgz",
+ "integrity": "sha512-dhn3AICsDlIZ5qY/Qu+QOL+ZGKaHcGss4PQ3CfmAF3f+o5fPJ2aDJcxd5f2au2k6sxyNqvCsLAFYFHXxHoH9yQ==",
+ "inBundle": true,
+ "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/@meteorjs/create-ecdh/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==",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/@meteorjs/crypto-browserify": {
+ "version": "3.12.4",
+ "resolved": "https://registry.npmjs.org/@meteorjs/crypto-browserify/-/crypto-browserify-3.12.4.tgz",
+ "integrity": "sha512-K5Sgvxef93Zrw5T9cJxKuNVgpl1C2W8cfcicN6HKy98G6RoIrx6hikwWnq8FlagvOzdIQEC2s+SMn7UFNSK0eA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "@meteorjs/browserify-sign": "^4.2.3",
+ "@meteorjs/create-ecdh": "^4.0.4",
"browserify-cipher": "^1.0.1",
- "browserify-sign": "^4.2.3",
- "create-ecdh": "^4.0.4",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
"diffie-hellman": "^5.0.3",
@@ -89,17 +204,41 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/@meteorjs/crypto-browserify/node_modules/hash-base": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
- "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
- "inBundle": true,
- "dependencies": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
- },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
"engines": {
- "node": ">=4"
+ "node": ">=14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asn1.js": {
@@ -107,6 +246,7 @@
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
@@ -114,16 +254,18 @@
}
},
"node_modules/asn1.js/node_modules/bn.js": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
- "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
- "inBundle": true
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/assert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"call-bind": "^1.0.2",
"is-nan": "^1.3.2",
@@ -133,10 +275,14 @@
}
},
"node_modules/available-typed-arrays": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
- "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -148,7 +294,8 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -168,35 +315,39 @@
"url": "https://feross.org/support"
}
],
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/bn.js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
- "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==",
- "inBundle": true
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
+ "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "balanced-match": "^1.0.0"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
- "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
- "inBundle": true
+ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"buffer-xor": "^1.0.3",
"cipher-base": "^1.0.0",
@@ -211,6 +362,7 @@
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
"integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"browserify-aes": "^1.0.4",
"browserify-des": "^1.0.0",
@@ -222,6 +374,7 @@
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
"integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"cipher-base": "^1.0.1",
"des.js": "^1.0.0",
@@ -230,96 +383,26 @@
}
},
"node_modules/browserify-rsa": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
- "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
- "inBundle": true,
- "dependencies": {
- "bn.js": "^5.0.0",
- "randombytes": "^2.0.1"
- }
- },
- "node_modules/browserify-sign": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz",
- "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz",
+ "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"bn.js": "^5.2.1",
- "browserify-rsa": "^4.1.0",
- "create-hash": "^1.2.0",
- "create-hmac": "^1.1.7",
- "elliptic": "^6.5.5",
- "hash-base": "~3.0",
- "inherits": "^2.0.4",
- "parse-asn1": "^5.1.7",
- "readable-stream": "^2.3.8",
+ "randombytes": "^2.1.0",
"safe-buffer": "^5.2.1"
},
"engines": {
- "node": ">= 0.12"
+ "node": ">= 0.10"
}
},
- "node_modules/browserify-sign/node_modules/bn.js": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
- "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
- "inBundle": true
- },
- "node_modules/browserify-sign/node_modules/hash-base": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
- "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
- "inBundle": true,
- "dependencies": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/browserify-sign/node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
- "inBundle": true,
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "inBundle": true
- },
- "node_modules/browserify-sign/node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "inBundle": true,
- "dependencies": {
- "safe-buffer": "~5.1.0"
- }
- },
- "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "inBundle": true
- },
"node_modules/browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"pako": "~1.0.5"
}
@@ -343,6 +426,7 @@
}
],
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -351,27 +435,59 @@
"node_modules/buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
- "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
- "inBundle": true
+ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
- "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
- "inBundle": true
+ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/call-bind": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
- "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"inBundle": true,
"license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.1"
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
@@ -381,20 +497,38 @@
}
},
"node_modules/cipher-base": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
- "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
+ "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
}
},
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/console-browserify": {
"version": "1.2.0",
@@ -405,36 +539,23 @@
"node_modules/constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
- "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
- "inBundle": true
+ "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "inBundle": true
- },
- "node_modules/create-ecdh": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
- "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
"inBundle": true,
- "dependencies": {
- "bn.js": "^4.1.0",
- "elliptic": "^6.5.3"
- }
- },
- "node_modules/create-ecdh/node_modules/bn.js": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
- "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
- "inBundle": true
+ "license": "MIT"
},
"node_modules/create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
@@ -448,6 +569,7 @@
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"cipher-base": "^1.0.3",
"create-hash": "^1.1.0",
@@ -457,6 +579,21 @@
"sha.js": "^2.4.8"
}
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -480,6 +617,7 @@
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
@@ -493,10 +631,11 @@
}
},
"node_modules/des.js": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
- "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
@@ -507,6 +646,7 @@
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"bn.js": "^4.1.0",
"miller-rabin": "^4.0.0",
@@ -514,16 +654,18 @@
}
},
"node_modules/diffie-hellman/node_modules/bn.js": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
- "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
- "inBundle": true
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/domain-browser": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz",
"integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==",
"inBundle": true,
+ "license": "Artistic-2.0",
"engines": {
"node": ">=10"
},
@@ -531,37 +673,41 @@
"url": "https://bevry.me/fund"
}
},
- "node_modules/elliptic": {
- "version": "6.5.7",
- "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
- "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"inBundle": true,
"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"
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/elliptic/node_modules/bn.js": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
- "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
- "inBundle": true
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"inBundle": true,
"license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
"engines": {
"node": ">= 0.4"
}
@@ -576,11 +722,25 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">=0.8.x"
}
@@ -590,47 +750,20 @@
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"md5.js": "^1.3.4",
"safe-buffer": "^5.1.1"
}
},
"node_modules/for-each": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
- "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
- "inBundle": true,
- "dependencies": {
- "is-callable": "^1.1.3"
- }
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "inBundle": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
@@ -639,33 +772,101 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/glob": {
- "version": "7.1.7",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
- "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
+ "license": "ISC",
"dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
},
"engines": {
- "node": "*"
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "inBundle": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"inBundle": true,
- "dependencies": {
- "get-intrinsic": "^1.1.3"
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -684,23 +885,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
- "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
- "inBundle": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -709,12 +899,13 @@
}
},
"node_modules/has-tostringtag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
- "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "has-symbols": "^1.0.2"
+ "has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -724,17 +915,17 @@
}
},
"node_modules/hash-base": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
- "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
+ "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
+ "safe-buffer": "^5.2.1"
},
"engines": {
- "node": ">=4"
+ "node": ">= 0.10"
}
},
"node_modules/hash.js": {
@@ -742,16 +933,18 @@
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
- "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -762,8 +955,9 @@
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
- "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+ "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
@@ -773,8 +967,9 @@
"node_modules/https-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
- "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
- "inBundle": true
+ "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
@@ -794,32 +989,25 @@
"url": "https://feross.org/support"
}
],
- "inBundle": true
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
+ "inBundle": true,
+ "license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "inBundle": true
+ "inBundle": true,
+ "license": "ISC"
},
"node_modules/is-arguments": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
- "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -833,6 +1021,7 @@
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -840,13 +1029,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-generator-function": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
- "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+ "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.0",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -860,6 +1063,7 @@
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
@@ -871,13 +1075,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-typed-array": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
- "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "which-typed-array": "^1.1.11"
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
@@ -887,16 +1111,58 @@
}
},
"node_modules/isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
- "inBundle": true
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
@@ -908,6 +1174,7 @@
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"brorand": "^1.0.1"
@@ -917,39 +1184,56 @@
}
},
"node_modules/miller-rabin/node_modules/bn.js": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
- "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
- "inBundle": true
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
- "inBundle": true
+ "inBundle": true,
+ "license": "ISC"
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
- "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
- "inBundle": true
+ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "^2.0.1"
},
"engines": {
- "node": "*"
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
}
},
"node_modules/object-inspect": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
- "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"inBundle": true,
"license": "MIT",
"engines": {
@@ -960,13 +1244,14 @@
}
},
"node_modules/object-is": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
- "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
@@ -980,19 +1265,23 @@
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.assign": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
- "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "has-symbols": "^1.0.3",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
"object-keys": "^1.1.1"
},
"engines": {
@@ -1002,32 +1291,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
- "dependencies": {
- "wrappy": "1"
- }
- },
"node_modules/os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
- "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
- "inBundle": true
+ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "inBundle": true
+ "inBundle": true,
+ "license": "(MIT AND Zlib)"
},
"node_modules/parse-asn1": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
"integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==",
"inBundle": true,
+ "license": "ISC",
"dependencies": {
"asn1.js": "^4.10.1",
"browserify-aes": "^1.2.0",
@@ -1040,55 +1330,108 @@
"node": ">= 0.10"
}
},
- "node_modules/parse-asn1/node_modules/hash-base": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
- "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
- "inBundle": true,
- "dependencies": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
},
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pbkdf2": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
- "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz",
+ "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "create-hash": "^1.1.2",
- "create-hmac": "^1.1.4",
- "ripemd160": "^2.0.1",
- "safe-buffer": "^5.0.1",
- "sha.js": "^2.4.8"
+ "create-hash": "~1.1.3",
+ "create-hmac": "^1.1.7",
+ "ripemd160": "=2.0.1",
+ "safe-buffer": "^5.2.1",
+ "sha.js": "^2.4.11",
+ "to-buffer": "^1.2.0"
},
"engines": {
"node": ">=0.12"
}
},
+ "node_modules/pbkdf2/node_modules/create-hash": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
+ "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/pbkdf2/node_modules/hash-base": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
+ "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1"
+ }
+ },
+ "node_modules/pbkdf2/node_modules/ripemd160": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
+ "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^2.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
- "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
@@ -1097,13 +1440,15 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
"integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"bn.js": "^4.1.0",
"browserify-rsa": "^4.0.0",
@@ -1114,25 +1459,27 @@
}
},
"node_modules/public-encrypt/node_modules/bn.js": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
- "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
- "inBundle": true
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "inBundle": true
+ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/qs": {
- "version": "6.13.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"inBundle": true,
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.0.6"
+ "side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
@@ -1144,7 +1491,7 @@
"node_modules/querystring-es3": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
- "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+ "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
"inBundle": true,
"engines": {
"node": ">=0.4.x"
@@ -1155,6 +1502,7 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -1164,6 +1512,7 @@
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
"integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"randombytes": "^2.0.5",
"safe-buffer": "^5.1.0"
@@ -1174,6 +1523,7 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -1184,15 +1534,19 @@
}
},
"node_modules/rimraf": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
- "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "version": "5.0.10",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"dev": true,
+ "license": "ISC",
"dependencies": {
- "glob": "^7.1.3"
+ "glob": "^10.3.7"
},
"bin": {
- "rimraf": "bin.js"
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ripemd160": {
@@ -1200,6 +1554,7 @@
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
@@ -1223,7 +1578,26 @@
"url": "https://feross.org/support"
}
],
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/set-function-length": {
"version": "1.2.2",
@@ -1246,33 +1620,66 @@
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
- "inBundle": true
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "inBundle": true,
+ "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": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
}
},
"node_modules/side-channel": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
- "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.7",
"es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "object-inspect": "^1.13.1"
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -1281,11 +1688,81 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/stream-browserify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
"integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"inherits": "~2.0.4",
"readable-stream": "^3.5.0"
@@ -1296,6 +1773,7 @@
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz",
"integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"builtin-status-codes": "^3.0.0",
"inherits": "^2.0.4",
@@ -1308,15 +1786,121 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/timers-browserify": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
"integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"setimmediate": "^1.0.4"
},
@@ -1324,11 +1908,42 @@
"node": ">=0.6.0"
}
},
+ "node_modules/to-buffer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
+ "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "^2.0.5",
+ "safe-buffer": "^5.2.1",
+ "typed-array-buffer": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/tty-browserify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
"integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==",
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
},
"node_modules/url": {
"version": "0.11.4",
@@ -1349,6 +1964,7 @@
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
@@ -1361,25 +1977,46 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
},
"node_modules/vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
- "inBundle": true
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
},
"node_modules/which-typed-array": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
- "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"inBundle": true,
+ "license": "MIT",
"dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.4",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-tostringtag": "^1.0.0"
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -1388,17 +2025,110 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">=0.4"
}
diff --git a/npm-packages/meteor-node-stubs/package.json b/npm-packages/meteor-node-stubs/package.json
index 2068c222c4..3e2867429e 100644
--- a/npm-packages/meteor-node-stubs/package.json
+++ b/npm-packages/meteor-node-stubs/package.json
@@ -2,7 +2,7 @@
"name": "meteor-node-stubs",
"author": "Ben Newman ",
"description": "Stub implementations of Node built-in modules, a la Browserify",
- "version": "1.2.10",
+ "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.5.7",
"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",
@@ -62,7 +62,7 @@
"vm-browserify"
],
"devDependencies": {
- "rimraf": "^2.7.1"
+ "rimraf": "^5.0.10"
},
"repository": {
"type": "git",
@@ -81,5 +81,30 @@
],
"bugs": {
"url": "https://github.com/meteor/node-stubs/issues"
- }
+ },
+ "bundleDependencies": [
+ "@meteorjs/crypto-browserify",
+ "assert",
+ "browserify-zlib",
+ "buffer",
+ "console-browserify",
+ "constants-browserify",
+ "domain-browser",
+ "events",
+ "https-browserify",
+ "os-browserify",
+ "path-browserify",
+ "process",
+ "punycode",
+ "querystring-es3",
+ "readable-stream",
+ "stream-browserify",
+ "stream-http",
+ "string_decoder",
+ "timers-browserify",
+ "tty-browserify",
+ "url",
+ "util",
+ "vm-browserify"
+ ]
}
diff --git a/npm-packages/meteor-node-stubs/scripts/build-deps.js b/npm-packages/meteor-node-stubs/scripts/build-deps.js
index e85d8d7c1f..7c6808f6ec 100644
--- a/npm-packages/meteor-node-stubs/scripts/build-deps.js
+++ b/npm-packages/meteor-node-stubs/scripts/build-deps.js
@@ -2,6 +2,7 @@ var fs = require("fs");
var path = require("path");
var depsDir = path.join(__dirname, "..", "deps");
var map = require("../map.json");
+var rr = require("rimraf");
// Each file in the `deps` directory expresses the dependencies of a stub.
// For example, `deps/http.js` calls `require("http-browserify")` to
@@ -14,16 +15,15 @@ var map = require("../map.json");
// bundled. Note that these modules should not be `require`d at runtime,
// but merely scanned at bundling time.
-fs.mkdir(depsDir, function () {
- require("rimraf")("deps/*.js", function (error) {
- if (error) throw error;
- Object.keys(map).forEach(function (id) {
- fs.writeFileSync(
- path.join(depsDir, id + ".js"),
- typeof map[id] === "string"
- ? "require(" + JSON.stringify(map[id]) + ");\n"
- : ""
- );
- });
- });
+rr.rimrafSync(depsDir);
+
+fs.mkdirSync(depsDir);
+
+Object.keys(map).forEach(function (id) {
+ fs.writeFileSync(
+ path.join(depsDir, id + ".js"),
+ typeof map[id] === "string"
+ ? "require(" + JSON.stringify(map[id]) + ");\n"
+ : ""
+ );
});
diff --git a/package-lock.json b/package-lock.json
index 1ca8bd21d9..ea3a5d33e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,10 @@
"@babel/eslint-parser": "^7.21.3",
"@babel/eslint-plugin": "^7.19.1",
"@babel/preset-react": "^7.18.6",
+ "@types/lodash.isempty": "^4.4.9",
"@types/node": "^18.16.18",
+ "@types/sockjs": "^0.3.36",
+ "@types/sockjs-client": "^1.5.4",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"eslint": "^8.36.0",
@@ -25,7 +28,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
- "prettier": "^2.8.6",
+ "prettier": "^2.8.8",
"typescript": "^5.4.5"
}
},
@@ -1096,6 +1099,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.10",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
+ "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
+ "dev": true
+ },
+ "node_modules/@types/lodash.isempty": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/@types/lodash.isempty/-/lodash.isempty-4.4.9.tgz",
+ "integrity": "sha512-DPSFfnT2JmZiAWNWOU8IRZws/Ha6zyGF5m06TydfsY+0dVoQqby2J61Na2QU4YtwiZ+moC6cJS6zWYBJq4wBVw==",
+ "dev": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "18.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz",
@@ -1111,6 +1129,21 @@
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
+ "node_modules/@types/sockjs": {
+ "version": "0.3.36",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
+ "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sockjs-client": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.4.tgz",
+ "integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==",
+ "dev": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
@@ -1817,6 +1850,19 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
@@ -2992,39 +3038,6 @@
"node": ">=8.6.0"
}
},
- "node_modules/fast-glob/node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
- "dependencies": {
- "fill-range": "^7.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/fast-glob/node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/fast-glob/node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "engines": {
- "node": ">=0.12.0"
- }
- },
"node_modules/fast-glob/node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
@@ -3038,18 +3051,6 @@
"node": ">=8.6"
}
},
- "node_modules/fast-glob/node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3083,6 +3084,19 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -3626,6 +3640,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/is-number-object": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
@@ -4236,6 +4260,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
+ "license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
@@ -4695,6 +4720,19 @@
"node": ">=4"
}
},
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
diff --git a/package.json b/package.json
index 16de72f280..ff1baa3e4c 100644
--- a/package.json
+++ b/package.json
@@ -1,23 +1,25 @@
{
"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",
"@babel/eslint-plugin": "^7.19.1",
"@babel/preset-react": "^7.18.6",
+ "@types/lodash.isempty": "^4.4.9",
"@types/node": "^18.16.18",
+ "@types/sockjs": "^0.3.36",
+ "@types/sockjs-client": "^1.5.4",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"eslint": "^8.36.0",
@@ -29,9 +31,12 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
- "prettier": "^2.8.6",
+ "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-2fa/.npm/package/npm-shrinkwrap.json b/packages/accounts-2fa/.npm/package/npm-shrinkwrap.json
deleted file mode 100644
index 93c418735a..0000000000
--- a/packages/accounts-2fa/.npm/package/npm-shrinkwrap.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "lockfileVersion": 4,
- "dependencies": {
- "@types/node": {
- "version": "22.5.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
- "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg=="
- },
- "@types/notp": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@types/notp/-/notp-2.0.5.tgz",
- "integrity": "sha512-ZsZS0PYUa6ZE4K3yOGerBvaxCp4ePf6ZmkFbPeilcqz2Ui/lmXox7KlRt7XZkXzqUgXhFLkc09ixyVmFLCU3gQ=="
- },
- "node-2fa": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/node-2fa/-/node-2fa-2.0.3.tgz",
- "integrity": "sha512-PQldrOhjuoZyoydMvMSctllPN1ZPZ1/NwkEcgYwY9faVqE/OymxR+3awPpbWZxm6acLKqvmNqQmdqTsqYyflFw=="
- },
- "notp": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/notp/-/notp-2.0.3.tgz",
- "integrity": "sha512-oBig/2uqkjQ5AkBuw4QJYwkEWa/q+zHxI5/I5z6IeP2NT0alpJFsP/trrfCC+9xOAgQSZXssNi962kp5KBmypQ=="
- },
- "qrcode-svg": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz",
- "integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw=="
- },
- "thirty-two": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
- "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA=="
- },
- "tslib": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
- "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
- },
- "undici-types": {
- "version": "6.19.8",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
- "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
- }
- }
-}
diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts
index ce91667fa0..6aa2a58e2a 100644
--- a/packages/accounts-base/accounts-base.d.ts
+++ b/packages/accounts-base/accounts-base.d.ts
@@ -6,6 +6,7 @@ import { DDP } from 'meteor/ddp';
export interface URLS {
resetPassword: (token: string) => string;
verifyEmail: (token: string) => string;
+ loginToken: (token: string) => string;
enrollAccount: (token: string) => string;
}
@@ -47,7 +48,7 @@ export namespace Accounts {
profile?: Meteor.UserProfile | undefined;
},
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): string;
+ ): Promise;
function createUserAsync(
options: {
@@ -82,6 +83,11 @@ export namespace Accounts {
passwordEnrollTokenExpirationInDays?: number | undefined;
ambiguousErrorMessages?: boolean | undefined;
bcryptRounds?: number | undefined;
+ argon2Enabled?: string | false;
+ argon2Type?: string | undefined;
+ argon2TimeCost: number | undefined;
+ argon2MemoryCost: number | undefined;
+ argon2Parallelism: number | undefined;
defaultFieldSelector?: { [key: string]: 0 | 1 } | undefined;
collection?: string | undefined;
loginTokenExpirationHours?: number | undefined;
@@ -113,23 +119,23 @@ export namespace Accounts {
oldPassword: string,
newPassword: string,
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): void;
+ ): Promise;
function forgotPassword(
options: { email?: string | undefined },
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): void;
+ ): Promise;
function resetPassword(
token: string,
newPassword: string,
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): void;
+ ): Promise;
function verifyEmail(
token: string,
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): void;
+ ): Promise;
function onEmailVerificationLink(callback: Function): void;
@@ -143,11 +149,11 @@ export namespace Accounts {
function logout(
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): void;
+ ): Promise;
function logoutOtherClients(
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
- ): void;
+ ): Promise;
type PasswordSignupField = 'USERNAME_AND_EMAIL' | 'USERNAME_AND_OPTIONAL_EMAIL' | 'USERNAME_ONLY' | 'EMAIL_ONLY';
type PasswordlessSignupField = 'USERNAME_AND_EMAIL' | 'EMAIL_ONLY';
@@ -179,9 +185,11 @@ export interface EmailTemplates {
export namespace Accounts {
var emailTemplates: EmailTemplates;
- function addEmail(userId: string, newEmail: string, verified?: boolean): void;
+ function addEmailAsync(userId: string, newEmail: string, verified?: boolean): Promise;
- function removeEmail(userId: string, email: string): void;
+ function removeEmail(userId: string, email: string): Promise;
+
+ function replaceEmailAsync(userId: string, oldEmail: string, newEmail: string, verified?: boolean): Promise;
function onCreateUser(
func: (options: { profile?: {} | undefined }, user: Meteor.User) => void
@@ -190,35 +198,52 @@ export namespace Accounts {
function findUserByEmail(
email: string,
options?: { fields?: Mongo.FieldSpecifier | undefined }
- ): Meteor.User | null | undefined;
+ ): Promise;
function findUserByUsername(
username: string,
options?: { fields?: Mongo.FieldSpecifier | undefined }
- ): Meteor.User | null | undefined;
+ ): Promise;
+
+ interface SendEmailOptions {
+ from: string;
+ to: string;
+ subject: string;
+ text: string;
+ html: string;
+ headers?: Header | undefined;
+ }
+
+ interface SendEmailResult {
+ email: string;
+ user: Meteor.User;
+ token: string;
+ url: string;
+ options: SendEmailOptions;
+ }
function sendEnrollmentEmail(
userId: string,
email?: string,
extraTokenData?: Record,
extraParams?: Record
- ): void;
+ ): Promise;
function sendResetPasswordEmail(
userId: string,
email?: string,
extraTokenData?: Record,
extraParams?: Record
- ): void;
+ ): Promise;
function sendVerificationEmail(
userId: string,
email?: string,
extraTokenData?: Record,
extraParams?: Record
- ): void;
+ ): Promise;
- function setUsername(userId: string, newUsername: string): void;
+ function setUsername(userId: string, newUsername: string): Promise;
function setPasswordAsync(
userId: string,
@@ -353,10 +378,10 @@ export namespace Accounts {
/**
*
- * Check whether the provided password matches the bcrypt'ed password in
+ * Check whether the provided password matches the encrypted password in
* the database user record. `password` can be a string (in which case
- * it will be run through SHA256 before bcrypt) or an object with
- * properties `digest` and `algorithm` (in which case we bcrypt
+ * it will be run through SHA256 before bcrypt or argon2) or an object with
+ * properties `digest` and `algorithm` (in which case we bcrypt/argon2
* `password.digest`).
*/
function _checkPasswordAsync(
diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js
index 33073f2a6c..aef200ed39 100644
--- a/packages/accounts-base/accounts_client.js
+++ b/packages/accounts-base/accounts_client.js
@@ -362,18 +362,19 @@ export class AccountsClient extends AccountsCommon {
// Note that we need to call this even if _suppressLoggingIn is true,
// because it could be matching a _setLoggingIn(true) from a
// half-completed pre-reconnect login method.
- this._setLoggingIn(false);
if (error || !result) {
error = error || new Error(
`No result from call to ${options.methodName}`
);
loginCallbacks({ error });
+ this._setLoggingIn(false);
return;
}
try {
options.validateResult(result);
} catch (e) {
loginCallbacks({ error: e });
+ this._setLoggingIn(false);
return;
}
@@ -381,13 +382,15 @@ export class AccountsClient extends AccountsCommon {
this.makeClientLoggedIn(result.id, result.token, result.tokenExpires);
// use Tracker to make we sure have a user before calling the callbacks
- Tracker.autorun(async function (computation) {
+ Tracker.autorun(async (computation) => {
const user = await Tracker.withComputation(computation, () =>
Meteor.userAsync(),
);
if (user) {
- loginCallbacks({ loginDetails: result })
+ loginCallbacks({ loginDetails: result });
+ this._setLoggingIn(false);
+ computation.stop();
}
});
@@ -399,7 +402,7 @@ export class AccountsClient extends AccountsCommon {
this.connection.applyAsync(
options.methodName,
options.methodArguments,
- { wait: true, onResultReceived: onResultReceived },
+ { wait: true, onResultReceived },
loggedInAndDataReadyCallback);
}
@@ -686,7 +689,7 @@ export class AccountsClient extends AccountsCommon {
/**
* @summary Register a function to call when a reset password link is clicked
* in an email sent by
- * [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail).
+ * [`Accounts.sendResetPasswordEmail`](#Accounts-sendResetPasswordEmail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
@@ -694,7 +697,7 @@ export class AccountsClient extends AccountsCommon {
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: A password reset token that can be passed to
- * [`Accounts.resetPassword`](#accounts_resetpassword).
+ * [`Accounts.resetPassword`](#Accounts-resetPassword).
* 2. `done`: A function to call when the password reset UI flow is complete. The normal
* login process is suspended until this function is called, so that the
* password for user A can be reset even if user B was logged in.
@@ -712,7 +715,7 @@ export class AccountsClient extends AccountsCommon {
/**
* @summary Register a function to call when an email verification link is
* clicked in an email sent by
- * [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail).
+ * [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
@@ -720,7 +723,7 @@ export class AccountsClient extends AccountsCommon {
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: An email verification token that can be passed to
- * [`Accounts.verifyEmail`](#accounts_verifyemail).
+ * [`Accounts.verifyEmail`](#Accounts-verifyEmail).
* 2. `done`: A function to call when the email verification UI flow is complete.
* The normal login process is suspended until this function is called, so
* that the user can be notified that they are verifying their email before
@@ -739,7 +742,7 @@ export class AccountsClient extends AccountsCommon {
/**
* @summary Register a function to call when an account enrollment link is
* clicked in an email sent by
- * [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail).
+ * [`Accounts.sendEnrollmentEmail`](#Accounts-sendEnrollmentEmail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
@@ -747,7 +750,7 @@ export class AccountsClient extends AccountsCommon {
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: A password reset token that can be passed to
- * [`Accounts.resetPassword`](#accounts_resetpassword) to give the newly
+ * [`Accounts.resetPassword`](#Accounts-resetPassword) to give the newly
* enrolled account a password.
* 2. `done`: A function to call when the enrollment UI flow is complete.
* The normal login process is suspended until this function is called, so that
diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js
index fdd45a2890..48234064d8 100644
--- a/packages/accounts-base/accounts_common.js
+++ b/packages/accounts-base/accounts_common.js
@@ -14,6 +14,11 @@ const VALID_CONFIG_KEYS = [
'passwordEnrollTokenExpiration',
'ambiguousErrorMessages',
'bcryptRounds',
+ 'argon2Enabled',
+ 'argon2Type',
+ 'argon2TimeCost',
+ 'argon2MemoryCost',
+ 'argon2Parallelism',
'defaultFieldSelector',
'collection',
'loginTokenExpirationHours',
@@ -194,48 +199,13 @@ export class AccountsCommon {
? this.users.findOneAsync(userId, this._addDefaultFieldSelector(options))
: null;
}
- // Set up config for the accounts system. Call this on both the client
- // and the server.
- //
- // Note that this method gets overridden on AccountsServer.prototype, but
- // the overriding method calls the overridden method.
- //
- // XXX we should add some enforcement that this is called on both the
- // client and the server. Otherwise, a user can
- // 'forbidClientAccountCreation' only on the client and while it looks
- // like their app is secure, the server will still accept createUser
- // calls. https://github.com/meteor/meteor/issues/828
- //
- // @param options {Object} an object with fields:
- // - sendVerificationEmail {Boolean}
- // Send email address verification emails to new users created from
- // client signups.
- // - forbidClientAccountCreation {Boolean}
- // Do not allow clients to create accounts directly.
- // - restrictCreationByEmailDomain {Function or String}
- // Require created users to have an email matching the function or
- // having the string as domain.
- // - loginExpirationInDays {Number}
- // Number of days since login until a user is logged out (login token
- // expires).
- // - collection {String|Mongo.Collection}
- // A collection name or a Mongo.Collection object to hold the users.
- // - passwordResetTokenExpirationInDays {Number}
- // Number of days since password reset token creation until the
- // token can't be used any longer (password reset token expires).
- // - ambiguousErrorMessages {Boolean}
- // Return ambiguous error messages from login failures to prevent
- // user enumeration.
- // - bcryptRounds {Number}
- // Allows override of number of bcrypt rounds (aka work factor) used
- // to store passwords.
/**
* @summary Set global accounts options. You can also set these in `Meteor.settings.packages.accounts` without the need to call this function.
* @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.
@@ -244,13 +214,31 @@ export class AccountsCommon {
* @param {Number} options.passwordResetTokenExpiration The number of milliseconds from when a link to reset password is sent until token expires and user can't reset password with the link anymore. If `passwordResetTokenExpirationInDays` is set, it takes precedent.
* @param {Number} options.passwordEnrollTokenExpirationInDays The number of days from when a link to set initial password is sent until token expires and user can't set password with the link anymore. Defaults to 30.
* @param {Number} options.passwordEnrollTokenExpiration The number of milliseconds from when a link to set initial password is sent until token expires and user can't set password with the link anymore. If `passwordEnrollTokenExpirationInDays` is set, it takes precedent.
- * @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to `false`, but in production environments it is recommended it defaults to `true`.
+ * @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to `true`.
* @param {Number} options.bcryptRounds Allows override of number of bcrypt rounds (aka work factor) used to store passwords. The default is 10.
+ * @param {Boolean} options.argon2Enabled Enable argon2 algorithm usage in replacement for bcrypt. The default is `false`.
+ * @param {'argon2id' | 'argon2i' | 'argon2d'} options.argon2Type Allows override of the argon2 algorithm type. The default is `argon2id`.
+ * @param {Number} options.argon2TimeCost Allows override of number of argon2 iterations (aka time cost) used to store passwords. The default is 2.
+ * @param {Number} options.argon2MemoryCost Allows override of the amount of memory (in KiB) used by the argon2 algorithm. The default is 19456 (19MB).
+ * @param {Number} options.argon2Parallelism Allows override of the number of threads used by the argon2 algorithm. The default is 1.
* @param {MongoFieldSpecifier} options.defaultFieldSelector To exclude by default large custom fields from `Meteor.user()` and `Meteor.findUserBy...()` functions when called without a field selector, and all `onLogin`, `onLoginFailure` and `onLogout` callbacks. Example: `Accounts.config({ defaultFieldSelector: { myBigArray: 0 }})`. Beware when using this. If, for instance, you do not include `email` when excluding the fields, you can have problems with functions like `forgotPassword` that will break because they won't have the required data available. It's recommend that you always keep the fields `_id`, `username`, and `email`.
* @param {String|Mongo.Collection} options.collection A collection name or a Mongo.Collection object to hold the users.
* @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_server.js b/packages/accounts-base/accounts_server.js
index cf8f573b1d..6f0ba2098e 100644
--- a/packages/accounts-base/accounts_server.js
+++ b/packages/accounts-base/accounts_server.js
@@ -84,6 +84,11 @@ export class AccountsServer extends AccountsCommon {
this._skipCaseInsensitiveChecksForTest = {};
+ // Helper function to resolve promises if needed
+ this._resolvePromise = async (value) => {
+ return Meteor._isPromise(value) ? await value : value;
+ };
+
this.urls = {
resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams),
verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams),
@@ -333,6 +338,32 @@ export class AccountsServer extends AccountsCommon {
return user;
}
+ /**
+ * @summary Find a user by one of their email addresses.
+ * @locus Server
+ * @param {String} email The email address to look for
+ * @param {Object} [options]
+ * @param {Object} options.fields Limit the fields to return from the user document
+ * @returns {Promise} A user if found, else null
+ * @memberof Accounts
+ * @importFromPackage accounts-base
+ */
+ findUserByEmail = async (email, options) =>
+ await this._findUserByQuery({ email }, options);
+
+ /**
+ * @summary Find a user by their username.
+ * @locus Server
+ * @param {String} username The username to look for
+ * @param {Object} [options]
+ * @param {Object} options.fields Limit the fields to return from the user document
+ * @returns {Promise} A user if found, else null
+ * @memberof Accounts
+ * @importFromPackage accounts-base
+ */
+ findUserByUsername = async (username, options) =>
+ await this._findUserByQuery({ username }, options);
+
///
/// LOGIN METHODS
///
@@ -1806,21 +1837,6 @@ const setupUsersCollection = async users => {
return true;
},
- updateAsync: (userId, user, fields, modifier) => {
- // make sure it is our record
- if (user._id !== userId) {
- return false;
- }
-
- // user can only modify the 'profile' field. sets to multiple
- // sub-keys (eg profile.foo and profile.bar) are merged into entry
- // in the fields list.
- if (fields.length !== 1 || fields[0] !== 'profile') {
- return false;
- }
-
- return true;
- },
fetch: ['_id'] // we only look at _id.
});
@@ -1861,4 +1877,3 @@ const generateCasePermutationsForString = string => {
}
return permutations;
}
-
diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js
index f6cdf49c71..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);
@@ -775,8 +775,8 @@ if (Meteor.isServer) {
});
});
- Tinytest.add(
- 'accounts - make sure that extra params to accounts urls are added',
+ Tinytest.addAsync(
+ 'accounts - urls work with sync resolution',
async test => {
// No extra params
const verifyEmailURL = new URL(Accounts.urls.verifyEmail('test'));
@@ -790,6 +790,49 @@ if (Meteor.isServer) {
test.equal(enrollAccountURL.searchParams.get('test'), extraParams.test);
}
);
+
+ Tinytest.addAsync(
+ 'accounts - urls work with async resolution',
+ async test => {
+ // Save original urls
+ const originalUrls = Accounts.urls;
+ try {
+ // Override urls methods to return Promises
+ Accounts.urls = {
+ resetPassword: (token, extraParams) =>
+ new Promise(resolve => resolve(originalUrls.resetPassword(token, extraParams))),
+ verifyEmail: (token, extraParams) =>
+ new Promise(resolve => resolve(originalUrls.verifyEmail(token, extraParams))),
+ loginToken: (selector, token, extraParams) =>
+ new Promise(resolve => resolve(originalUrls.loginToken(selector, token, extraParams))),
+ enrollAccount: (token, extraParams) =>
+ new Promise(resolve => resolve(originalUrls.enrollAccount(token, extraParams))),
+ };
+
+ // Test with no extra params
+ const verifyEmailUrl = await Accounts.urls.verifyEmail('test');
+ const verifyEmailURL = new URL(verifyEmailUrl);
+ test.equal(verifyEmailURL.searchParams.toString(), "");
+
+ // Test with extra params
+ const extraParams = { test: 'async-success' };
+ const resetPasswordUrl = await Accounts.urls.resetPassword('test', extraParams);
+ const resetPasswordURL = new URL(resetPasswordUrl);
+ test.equal(resetPasswordURL.searchParams.get('test'), extraParams.test);
+
+ const enrollAccountUrl = await Accounts.urls.enrollAccount('test', extraParams);
+ const enrollAccountURL = new URL(enrollAccountUrl);
+ test.equal(enrollAccountURL.searchParams.get('test'), extraParams.test);
+
+ const loginTokenUrl = await Accounts.urls.loginToken('email', 'token', extraParams);
+ const loginTokenURL = new URL(loginTokenUrl);
+ test.equal(loginTokenURL.searchParams.get('test'), extraParams.test);
+ } finally {
+ // Restore original urls
+ Accounts.urls = originalUrls;
+ }
+ }
+ );
}
Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Facebook', async test => {
diff --git a/packages/accounts-base/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-base/package.js b/packages/accounts-base/package.js
index 2fc423d2bd..4190011694 100644
--- a/packages/accounts-base/package.js
+++ b/packages/accounts-base/package.js
@@ -1,6 +1,6 @@
Package.describe({
summary: "A user account system",
- version: "3.0.2",
+ version: "3.1.2",
});
Package.onUse((api) => {
diff --git a/packages/accounts-base/server_main.js b/packages/accounts-base/server_main.js
index dde1781f82..a9ed15c950 100644
--- a/packages/accounts-base/server_main.js
+++ b/packages/accounts-base/server_main.js
@@ -7,6 +7,7 @@ import { AccountsServer } from "./accounts_server.js";
Accounts = new AccountsServer(Meteor.server, { ...Meteor.settings.packages?.accounts, ...Meteor.settings.packages?.['accounts-base'] });
// TODO[FIBERS]: I need TLA
Accounts.init().then();
+
// Users table. Don't use the normal autopublish, since we want to hide
// some fields. Code to autopublish this is in accounts_server.js.
// XXX Allow users to configure this collection name.
diff --git a/packages/accounts-oauth/oauth_client.js b/packages/accounts-oauth/oauth_client.js
index d47736cc84..ad28401437 100644
--- a/packages/accounts-oauth/oauth_client.js
+++ b/packages/accounts-oauth/oauth_client.js
@@ -74,34 +74,38 @@ Meteor.startup(() => {
Accounts.oauth.tryLoginAfterPopupClosed = (
credentialToken,
callback,
- shouldRetry = true
+ timeout = 1000
) => {
- const credentialSecret =
- OAuth._retrieveCredentialSecret(credentialToken);
+ let startTime = Date.now();
+ let calledOnce = false;
+ let intervalId;
+ const checkForCredentialSecret = (clearInterval = false) => {
+ const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken);
+ if (!calledOnce && (credentialSecret || clearInterval)) {
+ calledOnce = true;
+ Meteor.clearInterval(intervalId);
+ Accounts.callLoginMethod({
+ methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
+ userCallback: callback ? err => callback(convertError(err)) : () => {},
+ });
+ } else if (clearInterval) {
+ Meteor.clearInterval(intervalId);
+ }
+ };
+ // Check immediately
+ checkForCredentialSecret();
+
+ // Then check on an interval
// In some case the function OAuth._retrieveCredentialSecret() can return null, because the local storage might not
// be ready. So we retry after a timeout.
-
- if (!credentialSecret) {
- if (!shouldRetry) {
- return;
+ intervalId = Meteor.setInterval(() => {
+ if (Date.now() - startTime > timeout) {
+ checkForCredentialSecret(true);
+ } else {
+ checkForCredentialSecret();
}
- Meteor.setTimeout(
- () =>
- Accounts.oauth.tryLoginAfterPopupClosed(
- credentialToken,
- callback,
- false
- ),
- 500
- );
- return;
- }
- // continue with the rest of the function
- Accounts.callLoginMethod({
- methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
- userCallback: callback && (err => callback(convertError(err))),
- });
+ }, 250);
};
Accounts.oauth.credentialRequestCompleteHandler = callback =>
@@ -112,4 +116,3 @@ Accounts.oauth.credentialRequestCompleteHandler = callback =>
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback);
}
}
-
diff --git a/packages/accounts-oauth/package.js b/packages/accounts-oauth/package.js
index e8d1716706..890762bd9b 100644
--- a/packages/accounts-oauth/package.js
+++ b/packages/accounts-oauth/package.js
@@ -1,6 +1,6 @@
Package.describe({
summary: "Common code for OAuth-based login services",
- version: '1.4.5',
+ version: '1.4.6',
});
Package.onUse(api => {
diff --git a/packages/accounts-password/.npm/package/npm-shrinkwrap.json b/packages/accounts-password/.npm/package/npm-shrinkwrap.json
deleted file mode 100644
index 4960480477..0000000000
--- a/packages/accounts-password/.npm/package/npm-shrinkwrap.json
+++ /dev/null
@@ -1,306 +0,0 @@
-{
- "lockfileVersion": 4,
- "dependencies": {
- "@mapbox/node-pre-gyp": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
- "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="
- },
- "abbrev": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
- "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
- },
- "agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="
- },
- "ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
- },
- "aproba": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
- "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
- },
- "are-we-there-yet": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
- "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw=="
- },
- "balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
- },
- "bcrypt": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
- "integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw=="
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="
- },
- "chownr": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
- "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
- },
- "color-support": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
- "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
- },
- "console-control-strings": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
- },
- "debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="
- },
- "delegates": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
- },
- "detect-libc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
- },
- "fs-minipass": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
- "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
- "dependencies": {
- "minipass": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="
- }
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
- },
- "gauge": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
- "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="
- },
- "glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="
- },
- "has-unicode": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
- "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
- },
- "https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
- },
- "make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
- "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
- "dependencies": {
- "semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
- }
- }
- },
- "minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="
- },
- "minipass": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
- "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="
- },
- "minizlib": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
- "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
- "dependencies": {
- "minipass": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="
- }
- }
- },
- "mkdirp": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
- "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
- },
- "ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node-addon-api": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
- "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
- },
- "node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="
- },
- "nopt": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
- "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="
- },
- "npmlog": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
- "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw=="
- },
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
- },
- "readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="
- },
- "rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="
- },
- "safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
- },
- "semver": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
- },
- "set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
- },
- "signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
- },
- "string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="
- },
- "string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="
- },
- "strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="
- },
- "tar": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
- "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="
- },
- "tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
- },
- "util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
- },
- "webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
- },
- "whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="
- },
- "wide-align": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
- "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- }
- }
-}
diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js
index eb63b6182c..f41e5b8127 100644
--- a/packages/accounts-password/package.js
+++ b/packages/accounts-password/package.js
@@ -1,49 +1,51 @@
-Package.describe({
- summary: "Password support for accounts",
- // Note: 2.2.0-beta.3 was published during the Meteor 1.6 prerelease
- // process, so it might be best to skip to 2.3.x instead of reusing
- // 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.0.2",
-});
-
-Npm.depends({
- bcrypt: "5.0.1",
-});
-
-Package.onUse((api) => {
- api.use(["accounts-base", "sha", "ejson", "ddp"], ["client", "server"]);
-
- // Export Accounts (etc) to packages using this one.
- api.imply("accounts-base", ["client", "server"]);
-
- api.use("email", "server");
- api.use("random", "server");
- api.use("check", "server");
- api.use("ecmascript");
-
- api.addFiles("email_templates.js", "server");
- api.addFiles("password_server.js", "server");
- api.addFiles("password_client.js", "client");
-});
-
-Package.onTest((api) => {
- api.use([
- "accounts-password",
- "sha",
- "tinytest",
- "test-helpers",
- "tracker",
- "accounts-base",
- "random",
- "email",
- "check",
- "ddp",
- "ecmascript",
- ]);
- api.addFiles("password_tests_setup.js", "server");
- api.addFiles("password_tests.js", ["client", "server"]);
- api.addFiles("email_tests_setup.js", "server");
- api.addFiles("email_tests.js", "client");
-});
+Package.describe({
+ summary: "Password support for accounts",
+ // Note: 2.2.0-beta.3 was published during the Meteor 1.6 prerelease
+ // process, so it might be best to skip to 2.3.x instead of reusing
+ // 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.1",
+});
+
+Npm.depends({
+ bcrypt: "5.0.1",
+ argon2: "0.41.1",
+});
+
+Package.onUse((api) => {
+ api.use(["accounts-base", "sha", "ejson", "ddp"], ["client", "server"]);
+
+ // Export Accounts (etc) to packages using this one.
+ api.imply("accounts-base", ["client", "server"]);
+
+ api.use("email", "server");
+ api.use("random", "server");
+ api.use("check", "server");
+ api.use("ecmascript");
+
+ api.addFiles("email_templates.js", "server");
+ api.addFiles("password_server.js", "server");
+ api.addFiles("password_client.js", "client");
+});
+
+Package.onTest((api) => {
+ api.use([
+ "accounts-password",
+ "sha",
+ "tinytest",
+ "test-helpers",
+ "tracker",
+ "accounts-base",
+ "random",
+ "email",
+ "check",
+ "ddp",
+ "ecmascript"
+ ]);
+ api.addFiles("password_tests_setup.js", "server");
+ api.addFiles("password_tests.js", ["client", "server"]);
+ api.addFiles("email_tests_setup.js", "server");
+ api.addFiles("email_tests.js", "client");
+ api.addFiles("password_argon_tests.js", ["client", "server"]);
+});
diff --git a/packages/accounts-password/password_argon_tests.js b/packages/accounts-password/password_argon_tests.js
new file mode 100644
index 0000000000..c1fb806d9a
--- /dev/null
+++ b/packages/accounts-password/password_argon_tests.js
@@ -0,0 +1,221 @@
+if (Meteor.isServer) {
+ Tinytest.addAsync("passwords Argon - migration from bcrypt encryption to argon2", async (test) => {
+ Accounts._options.argon2Enabled = false;
+ const username = Random.id();
+ const email = `${username}@bcrypt.com`;
+ const password = "password";
+ const userId = await Accounts.createUser(
+ {
+ username: username,
+ email: email,
+ password: password
+ }
+ );
+ Accounts._options.argon2Enabled = true;
+ let user = await Meteor.users.findOneAsync(userId);
+ const isValid = await Accounts._checkPasswordAsync(user, password);
+ test.equal(isValid.userId, userId, "checkPassword with bcrypt - User ID should be returned");
+ test.equal(typeof isValid.error, "undefined", "checkPassword with bcrypt - No error should be returned");
+
+ // wait for the migration to happen
+ await waitUntil(
+ async () => {
+ user = await Meteor.users.findOneAsync(userId);
+ return (
+ typeof user.services.password.bcrypt === "undefined" &&
+ typeof user.services.password.argon2 === "string"
+ );
+ },
+ { description: "bcrypt should be unset and argon2 should be set" }
+ );
+
+ // password is still valid using argon2
+ const isValidArgon = await Accounts._checkPasswordAsync(user, password);
+ test.equal(isValidArgon.userId, userId, "checkPassword with argon2 - User ID should be returned");
+ test.equal(typeof isValidArgon.error, "undefined", "checkPassword with argon2 - No error should be returned");
+
+ // cleanup
+ Accounts._options.argon2Enabled = false;
+ await Meteor.users.removeAsync(userId);
+ });
+
+
+ Tinytest.addAsync("passwords Argon - setPassword", async (test) => {
+ Accounts._options.argon2Enabled = true;
+ const username = Random.id();
+ const email = `${username}-intercept@example.com`;
+
+ const userId = await Accounts.createUser({ username: username, email: email });
+
+ let user = await Meteor.users.findOneAsync(userId);
+ // no services yet.
+ test.equal(user.services.password, undefined);
+
+ // set a new password.
+ await Accounts.setPasswordAsync(userId, "new password");
+ user = await Meteor.users.findOneAsync(userId);
+ const oldSaltedHash = user.services.password.argon2;
+ test.isTrue(oldSaltedHash);
+ // Send a reset password email (setting a reset token) and insert a login
+ // token.
+ await Accounts.sendResetPasswordEmail(userId, email);
+ await Accounts._insertLoginToken(userId, Accounts._generateStampedLoginToken());
+ const user2 = await Meteor.users.findOneAsync(userId);
+ test.isTrue(user2.services.password.reset);
+ test.isTrue(user2.services.resume.loginTokens);
+
+ // reset with the same password, see we get a different salted hash
+ await Accounts.setPasswordAsync(userId, "new password", { logout: false });
+ user = await Meteor.users.findOneAsync(userId);
+ const newSaltedHash = user.services.password.argon2;
+ test.isTrue(newSaltedHash);
+ test.notEqual(oldSaltedHash, newSaltedHash);
+ // No more reset token.
+ const user3 = await Meteor.users.findOneAsync(userId);
+ test.isFalse(user3.services.password.reset);
+ // But loginTokens are still here since we did logout: false.
+ test.isTrue(user3.services.resume.loginTokens);
+
+ // reset again, see that the login tokens are gone.
+ await Accounts.setPasswordAsync(userId, "new password");
+ user = await Meteor.users.findOneAsync(userId);
+ const newerSaltedHash = user.services.password.argon2;
+ test.isTrue(newerSaltedHash);
+ test.notEqual(oldSaltedHash, newerSaltedHash);
+ test.notEqual(newSaltedHash, newerSaltedHash);
+ // No more tokens.
+ const user4 = await Meteor.users.findOneAsync(userId);
+ test.isFalse(user4.services.password.reset);
+ test.isFalse(user4.services.resume.loginTokens);
+
+ // cleanup
+ Accounts._options.argon2Enabled = false;
+ await Meteor.users.removeAsync(userId);
+ });
+
+ Tinytest.addAsync("passwords Argon - migration from argon2 encryption to bcrypt", async (test) => {
+ Accounts._options.argon2Enabled = true;
+ const username = Random.id();
+ const email = `${username}@bcrypt.com`;
+ const password = "password";
+ const userId = await Accounts.createUser(
+ {
+ username: username,
+ email: email,
+ password: password
+ }
+ );
+ Accounts._options.argon2Enabled = false;
+ let user = await Meteor.users.findOneAsync(userId);
+ const isValidArgon = await Accounts._checkPasswordAsync(user, password);
+ test.equal(isValidArgon.userId, userId, "checkPassword with argon2 - User ID should be returned");
+ test.equal(typeof isValidArgon.error, "undefined", "checkPassword with argon2 - No error should be returned");
+
+ // wait for the migration to happen
+ await waitUntil(
+ async () => {
+ user = await Meteor.users.findOneAsync(userId);
+ return (
+ typeof user.services.password.bcrypt === "string" &&
+ typeof user.services.password.argon2 === "undefined"
+ );
+ },
+ { description: "bcrypt should be string and argon2 should be undefined" }
+ );
+
+ // password is still valid using bcrypt
+ const isValidBcrypt = await Accounts._checkPasswordAsync(user, password);
+ test.equal(isValidBcrypt.userId, userId, "checkPassword with argon2 - User ID should be returned");
+ test.equal(typeof isValidBcrypt.error, "undefined", "checkPassword with argon2 - No error should be returned");
+
+ // cleanup
+ await Meteor.users.removeAsync(userId);
+ });
+
+ const getUserHashArgon2Params = function (user) {
+ const hash = user?.services?.password?.argon2;
+ return Accounts._getArgon2Params(hash);
+ }
+ const hashPasswordWithSha = function (password) {
+ return {
+ digest: SHA256(password),
+ algorithm: "sha-256"
+ };
+ }
+
+ testAsyncMulti("passwords Argon - allow custom argon2 Params and ensure migration if changed", [
+ async function(test) {
+ Accounts._options.argon2Enabled = true;
+ // Verify that a argon2 hash generated for a new account uses the
+ // default params.
+ let username = Random.id();
+ this.password = hashPasswordWithSha("abc123");
+ this.userId1 = await Accounts.createUserAsync({ username, password: this.password });
+ this.user1 = await Meteor.users.findOneAsync(this.userId1);
+ let argon2Params = getUserHashArgon2Params(this.user1);
+ test.equal(argon2Params.type, Accounts._argon2Type());
+ test.equal(argon2Params.memoryCost, Accounts._argon2MemoryCost());
+ test.equal(argon2Params.timeCost, Accounts._argon2TimeCost());
+ test.equal(argon2Params.parallelism, Accounts._argon2Parallelism());
+
+
+ // When a custom number of argon2 TimeCost is set via Accounts.config,
+ // and an account was already created using the default number of TimeCost,
+ // make sure that a new hash is created (and stored) using the new number
+ // of TimeCost, the next time the password is checked.
+ this.customType = "argon2d"; // argon2.argon2d = 2
+ this.customTimeCost = 4;
+ this.customMemoryCost = 32768;
+ this.customParallelism = 1;
+ Accounts._options.argon2Type = this.customType;
+ Accounts._options.argon2TimeCost = this.customTimeCost;
+ Accounts._options.argon2MemoryCost = this.customMemoryCost;
+ Accounts._options.argon2Parallelism = this.customParallelism;
+
+ await Accounts._checkPasswordAsync(this.user1, this.password);
+ },
+ async function(test) {
+ const defaultType = Accounts._argon2Type();
+ const defaultTimeCost = Accounts._argon2TimeCost();
+ const defaultMemoryCost = Accounts._argon2MemoryCost();
+ const defaultParallelism = Accounts._argon2Parallelism();
+ let params;
+ let username;
+
+ let resolve;
+ const promise = new Promise(res => resolve = res);
+
+ Meteor.setTimeout(async () => {
+ this.user1 = await Meteor.users.findOneAsync(this.userId1);
+ params = getUserHashArgon2Params(this.user1);
+ test.equal(params.type, 2);
+ test.equal(params.timeCost, this.customTimeCost);
+ test.equal(params.memoryCost, this.customMemoryCost);
+ test.equal(params.parallelism, this.customParallelism);
+
+ // When a custom number of argon2 TimeCost is set, make sure it's
+ // used for new argon2 password hashes.
+ username = Random.id();
+ const userId2 = await Accounts.createUser({ username, password: this.password });
+ const user2 = await Meteor.users.findOneAsync(userId2);
+ params = getUserHashArgon2Params(user2);
+ test.equal(params.type, 2);
+ test.equal(params.timeCost, this.customTimeCost);
+ test.equal(params.memoryCost, this.customMemoryCost);
+ test.equal(params.parallelism, this.customParallelism);
+
+ // Cleanup
+ Accounts._options.argon2Enabled = false;
+ Accounts._options.argon2Type = defaultType;
+ Accounts._options.argon2TimeCost = defaultTimeCost;
+ Accounts._options.argon2MemoryCost = defaultMemoryCost;
+ Accounts._options.argon2Parallelism = defaultParallelism;
+ await Meteor.users.removeAsync(this.userId1);
+ await Meteor.users.removeAsync(userId2);
+ resolve();
+ }, 1000);
+
+ return promise;
+ }
+ ]);
+}
diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js
index e2537f2c5c..151f9c8d5f 100644
--- a/packages/accounts-password/password_server.js
+++ b/packages/accounts-password/password_server.js
@@ -1,4 +1,5 @@
-import { hash as bcryptHash, compare as bcryptCompare } from 'bcrypt';
+import argon2 from "argon2";
+import { hash as bcryptHash, compare as bcryptCompare } from "bcrypt";
import { Accounts } from "meteor/accounts-base";
// Utility for grabbing user
@@ -6,8 +7,9 @@ const getUserById =
async (id, options) =>
await Meteor.users.findOneAsync(id, Accounts._addDefaultFieldSelector(options));
-// User records have a 'services.password.bcrypt' field on them to hold
-// their hashed passwords.
+// User records have two fields that are used for password-based login:
+// - 'services.password.bcrypt', which stores the bcrypt password, which will be deprecated
+// - 'services.password.argon2', which stores the argon2 password
//
// When the client sends a password to the server, it can either be a
// string (the plaintext password) or an object with keys 'digest' and
@@ -17,127 +19,274 @@ const getUserById =
// strings.
//
// When the server receives a plaintext password as a string, it always
-// hashes it with SHA256 before passing it into bcrypt. When the server
+// hashes it with SHA256 before passing it into bcrypt / argon2. When the server
// receives a password as an object, it asserts that the algorithm is
-// "sha-256" and then passes the digest to bcrypt.
-
+// "sha-256" and then passes the digest to bcrypt / argon2.
Accounts._bcryptRounds = () => Accounts._options.bcryptRounds || 10;
-// Given a 'password' from the client, extract the string that we should
-// bcrypt. 'password' can be one of:
-// - String (the plaintext password)
-// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256".
-//
+Accounts._argon2Enabled = () => Accounts._options.argon2Enabled || false;
+
+const ARGON2_TYPES = {
+ argon2i: argon2.argon2i,
+ argon2d: argon2.argon2d,
+ argon2id: argon2.argon2id
+};
+
+Accounts._argon2Type = () => ARGON2_TYPES[Accounts._options.argon2Type] || argon2.argon2id;
+Accounts._argon2TimeCost = () => Accounts._options.argon2TimeCost || 2;
+Accounts._argon2MemoryCost = () => Accounts._options.argon2MemoryCost || 19456;
+Accounts._argon2Parallelism = () => Accounts._options.argon2Parallelism || 1;
+
+/**
+ * Extracts the string to be encrypted using bcrypt or Argon2 from the given `password`.
+ *
+ * @param {string|Object} password - The password provided by the client. It can be:
+ * - A plaintext string password.
+ * - An object with the following properties:
+ * @property {string} digest - The hashed password.
+ * @property {string} algorithm - The hashing algorithm used. Must be "sha-256".
+ *
+ * @returns {string} - The resulting password string to encrypt.
+ *
+ * @throws {Error} - If the `algorithm` in the password object is not "sha-256".
+ */
const getPasswordString = password => {
if (typeof password === "string") {
password = SHA256(password);
- } else { // 'password' is an object
+ }
+ else { // 'password' is an object
if (password.algorithm !== "sha-256") {
throw new Error("Invalid password hash algorithm. " +
- "Only 'sha-256' is allowed.");
+ "Only 'sha-256' is allowed.");
}
password = password.digest;
}
return password;
};
-// Use bcrypt to hash the password for storage in the database.
-// `password` can be a string (in which case it will be run through
-// SHA256 before bcrypt) or an object with properties `digest` and
-// `algorithm` (in which case we bcrypt `password.digest`).
-//
-const hashPassword = async password => {
+/**
+ * Encrypt the given `password` using either bcrypt or Argon2.
+ * @param password can be a string (in which case it will be run through SHA256 before encryption) or an object with properties `digest` and `algorithm` (in which case we bcrypt or Argon2 `password.digest`).
+ * @returns {Promise} The encrypted password.
+ */
+const hashPassword = async (password) => {
password = getPasswordString(password);
- return await bcryptHash(password, Accounts._bcryptRounds());
+ if (Accounts._argon2Enabled() === true) {
+ return await argon2.hash(password, {
+ type: Accounts._argon2Type(),
+ timeCost: Accounts._argon2TimeCost(),
+ memoryCost: Accounts._argon2MemoryCost(),
+ parallelism: Accounts._argon2Parallelism()
+ });
+ }
+ else {
+ return await bcryptHash(password, Accounts._bcryptRounds());
+ }
};
// Extract the number of rounds used in the specified bcrypt hash.
-const getRoundsFromBcryptHash = hash => {
+const getRoundsFromBcryptHash = (hash) => {
let rounds;
if (hash) {
- const hashSegments = hash.split('$');
+ const hashSegments = hash.split("$");
if (hashSegments.length > 2) {
rounds = parseInt(hashSegments[2], 10);
}
}
return rounds;
};
+Accounts._getRoundsFromBcryptHash = getRoundsFromBcryptHash;
-// Check whether the provided password matches the bcrypt'ed password in
-// the database user record. `password` can be a string (in which case
-// it will be run through SHA256 before bcrypt) or an object with
-// properties `digest` and `algorithm` (in which case we bcrypt
-// `password.digest`).
-//
-// The user parameter needs at least user._id and user.services
-Accounts._checkPasswordUserFields = {_id: 1, services: 1};
-//
+
+/**
+ * Extract readable parameters from an Argon2 hash string.
+ * @param {string} hash - The Argon2 hash string.
+ * @returns {object} An object containing the parsed parameters.
+ * @throws {Error} If the hash format is invalid.
+ */
+function getArgon2Params(hash) {
+ const regex = /^\$(argon2(?:i|d|id))\$v=\d+\$m=(\d+),t=(\d+),p=(\d+)/;
+
+ const match = hash.match(regex);
+
+ if (!match) {
+ throw new Error("Invalid Argon2 hash format.");
+ }
+
+ const [, type, memoryCost, timeCost, parallelism] = match;
+
+ return {
+ type: ARGON2_TYPES[type],
+ timeCost: parseInt(timeCost, 10),
+ memoryCost: parseInt(memoryCost, 10),
+ parallelism: parseInt(parallelism, 10)
+ };
+}
+
+Accounts._getArgon2Params = getArgon2Params;
+
+const getUserPasswordHash = user => {
+ return user.services?.password?.argon2 || user.services?.password?.bcrypt;
+};
+
+Accounts._checkPasswordUserFields = { _id: 1, services: 1 };
+
+const isBcrypt = (hash) => {
+ // bcrypt hashes start with $2a$ or $2b$
+ return hash.startsWith("$2");
+};
+
+const isArgon = (hash) => {
+ // argon2 hashes start with $argon2i$, $argon2d$ or $argon2id$
+ return hash.startsWith("$argon2");
+}
+
+const updateUserPasswordDefered = (user, formattedPassword) => {
+ Meteor.defer(async () => {
+ await updateUserPassword(user, formattedPassword);
+ });
+};
+
+/**
+ * Hashes the provided password and returns an object that can be used to update the user's password.
+ * @param formattedPassword
+ * @returns {Promise<{$set: {"services.password.bcrypt": string}}|{$unset: {"services.password.bcrypt": number}, $set: {"services.password.argon2": string}}>}
+ */
+const getUpdatorForUserPassword = async (formattedPassword) => {
+ const encryptedPassword = await hashPassword(formattedPassword);
+ if (Accounts._argon2Enabled() === false) {
+ return {
+ $set: {
+ "services.password.bcrypt": encryptedPassword
+ },
+ $unset: {
+ "services.password.argon2": 1
+ }
+ };
+ }
+ else if (Accounts._argon2Enabled() === true) {
+ return {
+ $set: {
+ "services.password.argon2": encryptedPassword
+ },
+ $unset: {
+ "services.password.bcrypt": 1
+ }
+ };
+ }
+};
+
+const updateUserPassword = async (user, formattedPassword) => {
+ const updator = await getUpdatorForUserPassword(formattedPassword);
+ await Meteor.users.updateAsync({ _id: user._id }, updator);
+};
+
+/**
+ * Checks whether the provided password matches the hashed password stored in the user's database record.
+ *
+ * @param {Object} user - The user object containing at least:
+ * @property {string} _id - The user's unique identifier.
+ * @property {Object} services - The user's services data.
+ * @property {Object} services.password - The user's password object.
+ * @property {string} [services.password.argon2] - The Argon2 hashed password.
+ * @property {string} [services.password.bcrypt] - The bcrypt hashed password, deprecated
+ *
+ * @param {string|Object} password - The password provided by the client. It can be:
+ * - A plaintext string password.
+ * - An object with the following properties:
+ * @property {string} digest - The hashed password.
+ * @property {string} algorithm - The hashing algorithm used. Must be "sha-256".
+ *
+ * @returns {Promise} - A result object with the following properties:
+ * @property {string} userId - The user's unique identifier.
+ * @property {Object} [error] - An error object if the password does not match or an error occurs.
+ *
+ * @throws {Error} - If an unexpected error occurs during the process.
+ */
const checkPasswordAsync = async (user, password) => {
const result = {
userId: user._id
};
const formattedPassword = getPasswordString(password);
- const hash = user.services.password.bcrypt;
- const hashRounds = getRoundsFromBcryptHash(hash);
+ const hash = getUserPasswordHash(user);
- if (! await bcryptCompare(formattedPassword, hash)) {
- result.error = Accounts._handleError("Incorrect password", false);
- } else if (hash && Accounts._bcryptRounds() != hashRounds) {
- // The password checks out, but the user's bcrypt hash needs to be updated.
- Meteor.defer(async () => {
- await Meteor.users.updateAsync({ _id: user._id }, {
- $set: {
- 'services.password.bcrypt':
- await bcryptHash(formattedPassword, Accounts._bcryptRounds())
+ const argon2Enabled = Accounts._argon2Enabled();
+ if (argon2Enabled === false) {
+ if (isArgon(hash)) {
+ // this is a rollback feature, enabling to switch back from argon2 to bcrypt if needed
+ // TODO : deprecate this
+ console.warn("User has an argon2 password and argon2 is not enabled, rolling back to bcrypt encryption");
+ const match = await argon2.verify(hash, formattedPassword);
+ if (!match) {
+ result.error = Accounts._handleError("Incorrect password", false);
+ }
+ else{
+ // The password checks out, but the user's stored password needs to be updated to argon2
+ updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
+ }
+ }
+ else {
+ const hashRounds = getRoundsFromBcryptHash(hash);
+ const match = await bcryptCompare(formattedPassword, hash);
+ if (!match) {
+ result.error = Accounts._handleError("Incorrect password", false);
+ }
+ else if (hash) {
+ const paramsChanged = hashRounds !== Accounts._bcryptRounds();
+ // The password checks out, but the user's bcrypt hash needs to be updated
+ // to match current bcrypt settings
+ if (paramsChanged === true) {
+ updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
}
- });
- });
+ }
+ }
}
+ else if (argon2Enabled === true) {
+ if (isBcrypt(hash)) {
+ // migration code from bcrypt to argon2
+ const match = await bcryptCompare(formattedPassword, hash);
+ if (!match) {
+ result.error = Accounts._handleError("Incorrect password", false);
+ }
+ else {
+ // The password checks out, but the user's stored password needs to be updated to argon2
+ updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
+ }
+ }
+ else {
+ // argon2 password
+ const argon2Params = getArgon2Params(hash);
+ const match = await argon2.verify(hash, formattedPassword);
+ if (!match) {
+ result.error = Accounts._handleError("Incorrect password", false);
+ }
+ else if (hash) {
+ const paramsChanged = argon2Params.memoryCost !== Accounts._argon2MemoryCost() ||
+ argon2Params.timeCost !== Accounts._argon2TimeCost() ||
+ argon2Params.parallelism !== Accounts._argon2Parallelism() ||
+ argon2Params.type !== Accounts._argon2Type();
+ if (paramsChanged === true) {
+ // The password checks out, but the user's argon2 hash needs to be updated with the right params
+ updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
+ }
+ }
+ }
+ }
+
return result;
};
-Accounts._checkPasswordAsync = checkPasswordAsync;
+Accounts._checkPasswordAsync = checkPasswordAsync;
///
/// LOGIN
///
-/**
- * @summary Finds the user asynchronously with the specified username.
- * First tries to match username case sensitively; if that fails, it
- * tries case insensitively; but if more than one user matches the case
- * insensitive search, it returns null.
- * @locus Server
- * @param {String} username The username to look for
- * @param {Object} [options]
- * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
- * @returns {Promise} A user if found, else null
- * @importFromPackage accounts-base
- */
-Accounts.findUserByUsername =
- async (username, options) =>
- await Accounts._findUserByQuery({ username }, options);
-
-/**
- * @summary Finds the user asynchronously with the specified email.
- * First tries to match email case sensitively; if that fails, it
- * tries case insensitively; but if more than one user matches the case
- * insensitive search, it returns null.
- * @locus Server
- * @param {String} email The email address to look for
- * @param {Object} [options]
- * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
- * @returns {Promise} A user if found, else null
- * @importFromPackage accounts-base
- */
-Accounts.findUserByEmail =
- async (email, options) =>
- await Accounts._findUserByQuery({ email }, options);
// XXX maybe this belongs in the check package
const NonEmptyString = Match.Where(x => {
@@ -185,9 +334,7 @@ Accounts.registerLoginHandler("password", async options => {
Accounts._handleError("User not found");
}
-
- if (!user.services || !user.services.password ||
- !user.services.password.bcrypt) {
+ if (!getUserPasswordHash(user)) {
Accounts._handleError("User has no password set");
}
@@ -267,51 +414,54 @@ Accounts.setUsername =
// `digest` and `algorithm` (representing the SHA256 of the password).
Meteor.methods(
{
- changePassword: async function (oldPassword, newPassword) {
- check(oldPassword, passwordValidator);
- check(newPassword, passwordValidator);
+ changePassword: async function(oldPassword, newPassword) {
+ check(oldPassword, passwordValidator);
+ check(newPassword, passwordValidator);
- if (!this.userId) {
- throw new Meteor.Error(401, "Must be logged in");
- }
+ if (!this.userId) {
+ throw new Meteor.Error(401, "Must be logged in");
+ }
- const user = await getUserById(this.userId, {fields: {
- services: 1,
- ...Accounts._checkPasswordUserFields,
- }});
- if (!user) {
- Accounts._handleError("User not found");
- }
+ const user = await getUserById(this.userId, {
+ fields: {
+ services: 1,
+ ...Accounts._checkPasswordUserFields
+ }
+ });
+ if (!user) {
+ Accounts._handleError("User not found");
+ }
- if (!user.services || !user.services.password || !user.services.password.bcrypt) {
- Accounts._handleError("User has no password set");
- }
+ if (!getUserPasswordHash(user)) {
+ Accounts._handleError("User has no password set");
+ }
- const result = await checkPasswordAsync(user, oldPassword);
- if (result.error) {
- throw result.error;
- }
+ const result = await checkPasswordAsync(user, oldPassword);
+ if (result.error) {
+ throw result.error;
+ }
- const hashed = await hashPassword(newPassword);
+ // It would be better if this removed ALL existing tokens and replaced
+ // the token for the current connection with a new one, but that would
+ // be tricky, so we'll settle for just replacing all tokens other than
+ // the one for the current connection.
+ const currentToken = Accounts._getLoginToken(this.connection.id);
+ const updator = await getUpdatorForUserPassword(newPassword);
- // It would be better if this removed ALL existing tokens and replaced
- // the token for the current connection with a new one, but that would
- // be tricky, so we'll settle for just replacing all tokens other than
- // the one for the current connection.
- const currentToken = Accounts._getLoginToken(this.connection.id);
- await Meteor.users.updateAsync(
- { _id: this.userId },
- {
- $set: { 'services.password.bcrypt': hashed },
- $pull: {
- 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }
- },
- $unset: { 'services.password.reset': 1 }
+ await Meteor.users.updateAsync(
+ { _id: this.userId },
+ {
+ $set: updator.$set,
+ $pull: {
+ "services.resume.loginTokens": { hashedToken: { $ne: currentToken } }
+ },
+ $unset: { "services.password.reset": 1, ...updator.$unset }
+ }
+ );
+
+ return { passwordChanged: true };
}
- );
-
- return {passwordChanged: true};
-}});
+ });
// Force change the users password.
@@ -320,37 +470,34 @@ Meteor.methods(
* @summary Forcibly change the password for a user.
* @locus Server
* @param {String} userId The id of the user to update.
- * @param {String} newPassword A new password for the user.
+ * @param {String} newPlaintextPassword A new password for the user.
* @param {Object} [options]
* @param {Object} options.logout Logout all current connections with this userId (default: true)
* @importFromPackage accounts-base
*/
Accounts.setPasswordAsync =
async (userId, newPlaintextPassword, options) => {
- check(userId, String);
- check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256));
- check(options, Match.Maybe({ logout: Boolean }));
- options = { logout: true , ...options };
+ check(userId, String);
+ check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256));
+ check(options, Match.Maybe({ logout: Boolean }));
+ options = { logout: true, ...options };
- const user = await getUserById(userId, { fields: { _id: 1 } });
- if (!user) {
- throw new Meteor.Error(403, "User not found");
- }
+ const user = await getUserById(userId, { fields: { _id: 1 } });
+ if (!user) {
+ throw new Meteor.Error(403, "User not found");
+ }
- const update = {
- $unset: {
- 'services.password.reset': 1
- },
- $set: {'services.password.bcrypt': await hashPassword(newPlaintextPassword)}
+ let updator = await getUpdatorForUserPassword(newPlaintextPassword);
+ updator.$unset = updator.$unset || {};
+ updator.$unset["services.password.reset"] = 1;
+
+ if (options.logout) {
+ updator.$unset["services.resume.loginTokens"] = 1;
+ }
+
+ await Meteor.users.updateAsync({ _id: user._id }, updator);
};
- if (options.logout) {
- update.$unset['services.resume.loginTokens'] = 1;
- }
-
- await Meteor.users.updateAsync({_id: user._id}, update);
-};
-
///
/// RESETTING VIA EMAIL
///
@@ -430,25 +577,32 @@ Accounts.generateResetToken =
// if this method is called from the enroll account work-flow then
// store the token record in 'services.password.enroll' db field
// else store the token record in in 'services.password.reset' db field
- if(reason === 'enrollAccount') {
- await Meteor.users.updateAsync({_id: user._id}, {
- $set : {
- 'services.password.enroll': tokenRecord
+ if (reason === "enrollAccount") {
+ await Meteor.users.updateAsync(
+ { _id: user._id },
+ {
+ $set: {
+ "services.password.enroll": tokenRecord
+ }
}
- });
+ );
// before passing to template, update user object with new token
- Meteor._ensure(user, 'services', 'password').enroll = tokenRecord;
- } else {
- await Meteor.users.updateAsync({_id: user._id}, {
- $set : {
- 'services.password.reset': tokenRecord
+ Meteor._ensure(user, "services", "password").enroll = tokenRecord;
+ }
+ else {
+ await Meteor.users.updateAsync(
+ { _id: user._id },
+ {
+ $set: {
+ "services.password.reset": tokenRecord
+ }
}
- });
+ );
// before passing to template, update user object with new token
- Meteor._ensure(user, 'services', 'password').reset = tokenRecord;
+ Meteor._ensure(user, "services", "password").reset = tokenRecord;
}
- return {email, user, token};
+ return { email, user, token };
};
/**
@@ -530,11 +684,11 @@ 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);
- if (Meteor.isDevelopment) {
+ if (Meteor.isDevelopment && !Meteor.isPackageTest) {
console.log(`\nReset password URL: ${ url }`);
}
return { email: realEmail, user, token, url, options };
@@ -564,13 +718,13 @@ 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');
await Email.sendAsync(options);
- if (Meteor.isDevelopment) {
+ if (Meteor.isDevelopment && !Meteor.isPackageTest) {
console.log(`\nEnrollment email URL: ${ url }`);
}
return { email: realEmail, user, token, url, options };
@@ -642,8 +796,6 @@ Meteor.methods(
error: new Meteor.Error(403, "Token has invalid email address")
};
- const hashed = await hashPassword(newPassword);
-
// NOTE: We're about to invalidate tokens on the user, who we might be
// logged in as. Make sure to avoid logging ourselves out if this
// happens. But also make sure not to leave the connection in a state
@@ -653,6 +805,8 @@ Meteor.methods(
const resetToOldToken = () =>
Accounts._setLoginToken(user._id, this.connection, oldToken);
+ const updator = await getUpdatorForUserPassword(newPassword);
+
try {
// Update the user record by:
// - Changing the password to the new one
@@ -664,29 +818,36 @@ Meteor.methods(
affectedRecords = await Meteor.users.updateAsync(
{
_id: user._id,
- 'emails.address': email,
- 'services.password.enroll.token': token
+ "emails.address": email,
+ "services.password.enroll.token": token
},
{
$set: {
- 'services.password.bcrypt': hashed,
- 'emails.$.verified': true
+ "emails.$.verified": true,
+ ...updator.$set
},
- $unset: { 'services.password.enroll': 1 }
+ $unset: {
+ "services.password.enroll": 1,
+ ...updator.$unset
+ }
});
- } else {
+ }
+ else {
affectedRecords = await Meteor.users.updateAsync(
{
_id: user._id,
- 'emails.address': email,
- 'services.password.reset.token': token
+ "emails.address": email,
+ "services.password.reset.token": token
},
{
$set: {
- 'services.password.bcrypt': hashed,
- 'emails.$.verified': true
+ "emails.$.verified": true,
+ ...updator.$set
},
- $unset: { 'services.password.reset': 1 }
+ $unset: {
+ "services.password.reset": 1,
+ ...updator.$unset
+ }
});
}
if (affectedRecords !== 1)
@@ -704,15 +865,16 @@ Meteor.methods(
await Accounts._clearAllLoginTokens(user._id);
if (Accounts._check2faEnabled?.(user)) {
- return {
- userId: user._id,
- error: Accounts._handleError(
- 'Changed password, but user not logged in because 2FA is enabled',
- false,
- '2fa-enabled'
- ),
- };
- }return { userId: user._id };
+ return {
+ userId: user._id,
+ error: Accounts._handleError(
+ 'Changed password, but user not logged in because 2FA is enabled',
+ false,
+ '2fa-enabled'
+ ),
+ };
+ }
+ return { userId: user._id };
}
);
}
@@ -745,10 +907,10 @@ 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) {
+ if (Meteor.isDevelopment && !Meteor.isPackageTest) {
console.log(`\nVerification email URL: ${ url }`);
}
return { email: realEmail, user, token, url, options };
@@ -829,6 +991,52 @@ Meteor.methods(
}
});
+
+/**
+ * @summary Asynchronously replace an email address for a user. Use this instead of directly
+ * updating the database. The operation will fail if there is a different user
+ * with an email only differing in case. If the specified user has an existing
+ * email only differing in case however, we replace it.
+ * @locus Server
+ * @param {String} userId The ID of the user to update.
+ * @param {String} oldEmail The email address to replace.
+ * @param {String} newEmail The new email address to use.
+ * @param {Boolean} [verified] Optional - whether the new email address should
+ * be marked as verified. Defaults to false.
+ * @importFromPackage accounts-base
+ */
+Accounts.replaceEmailAsync = async (userId, oldEmail, newEmail, verified) => {
+ check(userId, NonEmptyString);
+ check(oldEmail, NonEmptyString);
+ check(newEmail, NonEmptyString);
+ check(verified, Match.Optional(Boolean));
+
+ if (verified === void 0) {
+ verified = false;
+ }
+
+ const user = await getUserById(userId, { fields: { _id: 1 } });
+ if (!user)
+ throw new Meteor.Error(403, "User not found");
+
+ // Ensure no user already has this new email
+ await Accounts._checkForCaseInsensitiveDuplicates(
+ "emails.address",
+ "Email",
+ newEmail,
+ user._id
+ );
+
+ const result = await Meteor.users.updateAsync(
+ { _id: user._id, 'emails.address': oldEmail },
+ { $set: { 'emails.$.address': newEmail, 'emails.$.verified': verified } }
+ );
+
+ if (result.modifiedCount === 0) {
+ throw new Meteor.Error(404, "No user could be found with old email");
+ }
+};
+
/**
* @summary Asynchronously add an email address for a user. Use this instead of directly
* updating the database. The operation will fail if there is a different user
@@ -990,7 +1198,13 @@ const createUser =
const user = { services: {} };
if (password) {
const hashed = await hashPassword(password);
- user.services.password = { bcrypt: hashed };
+ const argon2Enabled = Accounts._argon2Enabled();
+ if (argon2Enabled === false) {
+ user.services.password = { bcrypt: hashed };
+ }
+ else {
+ user.services.password = { argon2: hashed };
+ }
}
return await Accounts._createUserCheckingDuplicates({ user, email, username, options });
@@ -1074,17 +1288,7 @@ Accounts.createUserVerifyingEmail =
// method calling Accounts.createUser could set?
//
-Accounts.createUserAsync =
- async (options, callback) => {
- options = { ...options };
-
- // XXX allow an optional callback?
- if (callback) {
- throw new Error("Accounts.createUser with callback not supported on the server yet.");
- }
-
- return createUser(options);
- };
+Accounts.createUserAsync = createUser
// Create user directly on the server.
//
@@ -1110,4 +1314,3 @@ await Meteor.users.createIndexAsync('services.password.reset.token',
{ unique: true, sparse: true });
await Meteor.users.createIndexAsync('services.password.enroll.token',
{ unique: true, sparse: true });
-
diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js
index 88461e7d91..49f94544a0 100644
--- a/packages/accounts-password/password_tests.js
+++ b/packages/accounts-password/password_tests.js
@@ -10,7 +10,7 @@ const makeTestConnAsync =
})
const simplePollAsync = (fn) =>
new Promise((resolve, reject) => simplePoll(fn,resolve,reject))
-function hashPassword(password) {
+function hashPasswordWithSha(password) {
return {
digest: SHA256(password),
algorithm: "sha-256"
@@ -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);
@@ -486,7 +489,7 @@ if (Meteor.isClient) (() => {
function (test, expect) {
this.secondConn = DDP.connect(Meteor.absoluteUrl());
this.secondConn.call('login',
- { user: { username: this.username }, password: hashPassword(this.password) },
+ { user: { username: this.username }, password: hashPasswordWithSha(this.password) },
expect((err, result) => {
test.isFalse(err);
this.secondConn.setUserId(result.id);
@@ -802,7 +805,7 @@ if (Meteor.isClient) (() => {
// Can update own profile using ID.
await Meteor.users.updateAsync(
this.userId, { $set: { 'profile.updated': 42 } },
- );
+ );
test.equal(42, Meteor.user().profile.updated);
},
logoutStep
@@ -1212,10 +1215,10 @@ if (Meteor.isServer) (() => {
// This test properly belongs in accounts-base/accounts_tests.js, but
// this is where the tests that actually log in are.
- Tinytest.addAsync('accounts - user() out of context', async test => {
+ Tinytest.addAsync('accounts - userAsync() out of context', async test => {
await test.throwsAsync(
async () =>
- await Meteor.user()
+ await Meteor.userAsync()
);
await Meteor.users.removeAsync({});
});
@@ -1230,7 +1233,7 @@ if (Meteor.isServer) (() => {
const username = Random.id();
const id = await Accounts.createUser({
username: username,
- password: hashPassword('password')
+ password: hashPasswordWithSha('password')
});
const {
@@ -1245,7 +1248,7 @@ if (Meteor.isServer) (() => {
const result = await clientConn.callAsync('login', {
user: { username: username },
- password: hashPassword('password')
+ password: hashPasswordWithSha('password')
});
test.isTrue(result);
@@ -1278,7 +1281,7 @@ if (Meteor.isServer) (() => {
const userId = await Accounts.createUser({
username: username,
email: email,
- password: hashPassword("old-password")
+ password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
@@ -1297,16 +1300,17 @@ if (Meteor.isServer) (() => {
await test.throwsAsync(
async () =>
- await Meteor.callAsync("resetPassword", resetPasswordToken, hashPassword("new-password")),
+ await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password")),
/Token has invalid email address/
);
+ Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () =>
await Meteor.callAsync(
"login",
{
user: { username: username },
- password: hashPassword("new-password")
+ password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
@@ -1321,7 +1325,7 @@ if (Meteor.isServer) (() => {
const userId = await Accounts.createUser({
username: username,
email: email,
- password: hashPassword("old-password")
+ password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
@@ -1338,11 +1342,11 @@ if (Meteor.isServer) (() => {
test.isTrue(await clientConn.callAsync(
"resetPassword",
resetPasswordToken,
- hashPassword("new-password")
+ hashPasswordWithSha("new-password")
));
test.isTrue(await clientConn.callAsync("login", {
user: { username },
- password: hashPassword("new-password")
+ password: hashPasswordWithSha("new-password")
}));
});
@@ -1355,7 +1359,7 @@ if (Meteor.isServer) (() => {
const userId = await Accounts.createUser({
username: username,
email: email,
- password: hashPassword("old-password")
+ password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
@@ -1373,19 +1377,20 @@ if (Meteor.isServer) (() => {
await Meteor.users.updateAsync(userId, { $set: { "services.password.reset.when": new Date(Date.now() + -5 * 24 * 3600 * 1000) } });
try {
- await Meteor.callAsync("resetPassword", resetPasswordToken, hashPassword("new-password"))
+ await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password"))
} catch (e) {
test.throws(() => {
throw e;
})
}
+ Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () => await Meteor.callAsync(
"login",
{
user: { username: username },
- password: hashPassword("new-password")
+ password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
@@ -1405,7 +1410,7 @@ if (Meteor.isServer) (() => {
{
username: username,
email: email,
- password: hashPassword(password)
+ password: hashPasswordWithSha(password)
},
);
@@ -1432,7 +1437,7 @@ if (Meteor.isServer) (() => {
await Accounts.createUser(
{
email: email,
- password: hashPassword('password')
+ password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
@@ -1452,7 +1457,7 @@ if (Meteor.isServer) (() => {
await Accounts.createUser(
{
email: email,
- password: hashPassword('password')
+ password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
@@ -1498,12 +1503,12 @@ if (Meteor.isServer) (() => {
await clientConn.callAsync(
"resetPassword",
enrollPasswordToken,
- hashPassword("new-password"))
+ hashPasswordWithSha("new-password"))
);
test.isTrue(
await clientConn.callAsync("login", {
user: { username },
- password: hashPassword("new-password")
+ password: hashPasswordWithSha("new-password")
})
);
@@ -1535,7 +1540,7 @@ if (Meteor.isServer) (() => {
await Meteor.users.updateAsync(userId, { $set: { "services.password.enroll.when": new Date(Date.now() + -35 * 24 * 3600 * 1000) } });
await test.throwsAsync(
- async () => await Meteor.callAsync("resetPassword", enrollPasswordToken, hashPassword("new-password")),
+ async () => await Meteor.callAsync("resetPassword", enrollPasswordToken, hashPasswordWithSha("new-password")),
/Token expired/
);
});
@@ -1544,7 +1549,7 @@ if (Meteor.isServer) (() => {
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
- await Accounts.createUser({ email: email, password: hashPassword('password') });
+ await Accounts.createUser({ email: email, password: hashPasswordWithSha('password') });
await Accounts.sendEnrollmentEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
@@ -1561,7 +1566,7 @@ if (Meteor.isServer) (() => {
const userId =
await Accounts.createUser({
email: email,
- password: hashPassword('password')
+ password: hashPasswordWithSha('password')
});
await Accounts.sendEnrollmentEmail(userId, email);
@@ -1580,7 +1585,7 @@ if (Meteor.isServer) (() => {
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
- await Accounts.createUser({ email: email, password: hashPassword('password') });
+ await Accounts.createUser({ email: email, password: hashPasswordWithSha('password') });
await Accounts.sendResetPasswordEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
@@ -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/
@@ -1727,108 +1733,131 @@ if (Meteor.isServer) (() => {
});
Tinytest.addAsync("passwords - add email when user has not an existing email",
- async test => {
- const userId = await Accounts.createUser({
- username: `user${ Random.id() }`
- });
+ async test => {
+ const userId = await Accounts.createUser({
+ username: `user${ Random.id() }`
+ });
- const newEmail = `${ Random.id() }@turing.com`;
- await Accounts.addEmailAsync(userId, newEmail);
- const u1 = await Accounts._findUserByQuery({ id: userId })
- test.equal(u1.emails, [
- { address: newEmail, verified: false },
- ]);
- });
+ const newEmail = `${ Random.id() }@turing.com`;
+ await Accounts.addEmailAsync(userId, newEmail);
+ const u1 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u1.emails, [
+ { address: newEmail, verified: false },
+ ]);
+ });
Tinytest.addAsync("passwords - add email when the user has an existing email " +
"only differing in case",
async test => {
- const origEmail = `${ Random.id() }@turing.com`;
- const userId = await Accounts.createUser({
- email: origEmail
+ const origEmail = `${ Random.id() }@turing.com`;
+ const userId = await Accounts.createUser({
+ email: origEmail
+ });
+
+ const newEmail = `${ Random.id() }@turing.com`;
+ await Accounts.addEmailAsync(userId, newEmail);
+
+ const thirdEmail = origEmail.toUpperCase();
+ await Accounts.addEmailAsync(userId, thirdEmail, true);
+ const u1 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u1.emails, [
+ { address: thirdEmail, verified: true },
+ { address: newEmail, verified: false }
+ ]);
});
- const newEmail = `${ Random.id() }@turing.com`;
- await Accounts.addEmailAsync(userId, newEmail);
-
- const thirdEmail = origEmail.toUpperCase();
- await Accounts.addEmailAsync(userId, thirdEmail, true);
- const u1 = await Accounts._findUserByQuery({ id: userId })
- test.equal(u1.emails, [
- { address: thirdEmail, verified: true },
- { address: newEmail, verified: false }
- ]);
- });
-
Tinytest.addAsync("passwords - add email should fail when there is an existing " +
"user with an email only differing in case",
async test => {
- const user1Email = `${ Random.id() }@turing.com`;
- const userId1 = await Accounts.createUser({
- email: user1Email
+ const user1Email = `${ Random.id() }@turing.com`;
+ const userId1 = await Accounts.createUser({
+ email: user1Email
+ });
+
+ const user2Email = `${ Random.id() }@turing.com`;
+ const userId2 = await Accounts.createUser({
+ email: user2Email
+ });
+
+ const dupEmail = user1Email.toUpperCase();
+ await test.throwsAsync(
+ async () => await Accounts.addEmailAsync(userId2, dupEmail),
+ /Email already exists/
+ );
+
+ const u1 = await Accounts._findUserByQuery({ id: userId1 })
+ test.equal(u1.emails, [
+ { address: user1Email, verified: false }
+ ]);
+ const u2 = await Accounts._findUserByQuery({ id: userId2 })
+ test.equal(u2.emails, [
+ { address: user2Email, verified: false }
+ ]);
});
- const user2Email = `${ Random.id() }@turing.com`;
- const userId2 = await Accounts.createUser({
- email: user2Email
- });
- const dupEmail = user1Email.toUpperCase();
- await test.throwsAsync(
- async () => await Accounts.addEmailAsync(userId2, dupEmail),
- /Email already exists/
- );
- const u1 = await Accounts._findUserByQuery({ id: userId1 })
- test.equal(u1.emails, [
- { address: user1Email, verified: false }
- ]);
- const u2 = await Accounts._findUserByQuery({ id: userId2 })
- test.equal(u2.emails, [
- { address: user2Email, verified: false }
- ]);
+Tinytest.addAsync("accounts emails - replace email", async test => {
+ const origEmail = `originalemail@test.com`;
+ const userId = await Accounts.createUserAsync({
+ email: origEmail,
+ password: 'password'
});
- Tinytest.addAsync("passwords - remove email",
+ const newEmail = `newemail@test.com`;
+
+ const u1 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u1.emails, [
+ { address: origEmail, verified: false }
+ ]);
+
+ await Accounts.replaceEmailAsync(userId, origEmail, newEmail);
+ const u2 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u2.emails, [
+ { address: newEmail, verified: false }
+ ]);
+})
+
+ Tinytest.addAsync("passwords - remove email",
async test => {
- const origEmail = `${ Random.id() }@turing.com`;
- const userId = await Accounts.createUser({
- email: origEmail
+ const origEmail = `${ Random.id() }@turing.com`;
+ const userId = await Accounts.createUser({
+ email: origEmail
+ });
+
+ const newEmail = `${ Random.id() }@turing.com`;
+ await Accounts.addEmailAsync(userId, newEmail);
+
+ const thirdEmail = `${ Random.id() }@turing.com`;
+ await Accounts.addEmailAsync(userId, thirdEmail, true);
+ const u1 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u1.emails, [
+ { address: origEmail, verified: false },
+ { address: newEmail, verified: false },
+ { address: thirdEmail, verified: true }
+ ]);
+
+ await Accounts.removeEmail(userId, newEmail);
+ const u2 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u2.emails, [
+ { address: origEmail, verified: false },
+ { address: thirdEmail, verified: true }
+ ]);
+
+ await Accounts.removeEmail(userId, origEmail);
+ const u3 = await Accounts._findUserByQuery({ id: userId })
+ test.equal(u3.emails, [
+ { address: thirdEmail, verified: true }
+ ]);
});
- const newEmail = `${ Random.id() }@turing.com`;
- await Accounts.addEmailAsync(userId, newEmail);
-
- const thirdEmail = `${ Random.id() }@turing.com`;
- await Accounts.addEmailAsync(userId, thirdEmail, true);
- const u1 = await Accounts._findUserByQuery({ id: userId })
- test.equal(u1.emails, [
- { address: origEmail, verified: false },
- { address: newEmail, verified: false },
- { address: thirdEmail, verified: true }
- ]);
-
- await Accounts.removeEmail(userId, newEmail);
- const u2 = await Accounts._findUserByQuery({ id: userId })
- test.equal(u2.emails, [
- { address: origEmail, verified: false },
- { address: thirdEmail, verified: true }
- ]);
-
- await Accounts.removeEmail(userId, origEmail);
- const u3 = await Accounts._findUserByQuery({ id: userId })
- test.equal(u3.emails, [
- { address: thirdEmail, verified: true }
- ]);
- });
-
const getUserHashRounds = user =>
Number(user.services.password.bcrypt.substring(4, 6));
testAsyncMulti("passwords - allow custom bcrypt rounds",[
async function (test) {
// Verify that a bcrypt hash generated for a new account uses the
let username = Random.id();
- this.password = hashPassword('abc123');
+ this.password = hashPasswordWithSha('abc123');
this.userId1 = await Accounts.createUser({ username, password: this.password });
this.user1 = await Meteor.users.findOneAsync(this.userId1);
let rounds = getUserHashRounds(this.user1);
@@ -1876,24 +1905,153 @@ if (Meteor.isServer) (() => {
Tinytest.addAsync('passwords - extra params in email urls',
async (test) => {
- const username = Random.id();
- const email = `${ username }-intercept@example.com`;
+ const username = Random.id();
+ const email = `${ username }-intercept@example.com`;
- const userId = await Accounts.createUser({
- username: username,
- email: email
+ const userId = await Accounts.createUser({
+ username: username,
+ email: email
+ });
+
+ const extraParams = { test: 'success' };
+ await Accounts.sendEnrollmentEmail(userId, email, null, extraParams);
+
+ const [enrollPasswordEmailOptions] =
+ await Meteor.callAsync("getInterceptedEmails", email);
+
+ const re = new RegExp(`${Meteor.absoluteUrl()}(\\S*)`);
+ const match = enrollPasswordEmailOptions.text.match(re);
+ const url = new URL(match)
+ test.equal(url.searchParams.get('test'), extraParams.test);
});
- const extraParams = { test: 'success' };
- await Accounts.sendEnrollmentEmail(userId, email, null, extraParams);
+ Tinytest.addAsync('passwords - createUserAsync', async test => {
+ const username = Random.id();
+ const email = `${username}-intercept@example.com`;
+ const password = 'password';
- const [enrollPasswordEmailOptions] =
- await Meteor.callAsync("getInterceptedEmails", email);
+ const userId = await Accounts.createUserAsync({
+ username: username,
+ email: email,
+ password: password
+ });
- const re = new RegExp(`${Meteor.absoluteUrl()}(\\S*)`);
- const match = enrollPasswordEmailOptions.text.match(re);
- const url = new URL(match)
- test.equal(url.searchParams.get('test'), extraParams.test);
+ test.isTrue(userId, 'User ID should be returned');
+ const user = await Meteor.users.findOneAsync(userId);
+ test.equal(user.username, username, 'Username should match');
+ test.equal(user.emails[0].address, email, 'Email should match');
+
+ Accounts.config({
+ ambiguousErrorMessages: false,
+ })
+
+ await test.throwsAsync(async () => {
+ await Accounts.createUserAsync({
+ username: username,
+ email: email,
+ password: password
+ });
+ }, 'already exists');
});
+ Tinytest.addAsync('passwords - send email functions', async test => {
+ // Create a user with an unverified email
+ const username = Random.id();
+ const email = `${username}-intercept@example.com`;
+ const password = 'password';
+
+ const userId = await Accounts.createUserAsync({
+ username: username,
+ email: email,
+ password: password
+ });
+
+ test.isTrue(userId, 'User ID should be returned');
+
+ // Mock Email.sendAsync to track if it was called
+ const originalSendAsync = Email.sendAsync;
+ let emailSent = 0;
+ Email.sendAsync = async (options) => {
+ emailSent++;
+ return originalSendAsync(options);
+ };
+
+ try {
+ // Test sendVerificationEmail
+ const verificationResult = await Accounts.sendVerificationEmail(userId, email);
+
+ // Verify the result contains expected properties
+ test.isTrue(verificationResult, 'Result should be returned for verification email');
+ test.equal(verificationResult.email, email, 'Email in verification result should match');
+ test.isTrue(verificationResult.user, 'User object should be in verification result');
+ test.isTrue(verificationResult.token, 'Token should be in verification result');
+ test.isTrue(verificationResult.url, 'URL should be in verification result');
+ test.isTrue(verificationResult.options, 'Email options should be in verification result');
+
+ // Test sendEnrollmentEmail
+ const enrollmentResult = await Accounts.sendEnrollmentEmail(userId, email);
+
+ // Verify the result contains expected properties
+ test.isTrue(enrollmentResult, 'Result should be returned for enrollment email');
+ test.equal(enrollmentResult.email, email, 'Email in enrollment result should match');
+ test.isTrue(enrollmentResult.user, 'User object should be in enrollment result');
+ test.isTrue(enrollmentResult.token, 'Token should be in enrollment result');
+ test.isTrue(enrollmentResult.url, 'URL should be in enrollment result');
+ test.isTrue(enrollmentResult.options, 'Email options should be in enrollment result');
+
+ // Test sendResetPasswordEmail
+ const resetResult = await Accounts.sendResetPasswordEmail(userId, email);
+
+ // Verify the result contains expected properties
+ test.isTrue(resetResult, 'Result should be returned for reset password email');
+ test.equal(resetResult.email, email, 'Email in reset result should match');
+ test.isTrue(resetResult.user, 'User object should be in reset result');
+ test.isTrue(resetResult.token, 'Token should be in reset result');
+ test.isTrue(resetResult.url, 'URL should be in reset result');
+ test.isTrue(resetResult.options, 'Email options should be in reset result');
+
+ // Verify Email.sendAsync was called for all three emails
+ test.equal(emailSent, 3, 'Email.sendAsync should have been called three times');
+
+ // Get the intercepted emails
+ const interceptedEmails = await Meteor.callAsync("getInterceptedEmails", email);
+ test.equal(interceptedEmails.length, 3, 'Three emails should have been intercepted');
+
+ // Verify the verification email content
+ const verificationEmailOptions = interceptedEmails[0];
+ test.isTrue(verificationEmailOptions, 'Verification email should have been intercepted');
+ const verificationRe = new RegExp(`${Meteor.absoluteUrl()}#/verify-email/(\\S*)`);
+ const verificationMatch = verificationEmailOptions.text.match(verificationRe);
+ test.isTrue(verificationMatch, 'Verification email should contain verification URL');
+ const verificationTokenFromUrl = verificationMatch[1];
+ test.isTrue(verificationResult.url.includes(verificationTokenFromUrl), 'Verification URL in result should contain the token');
+
+ // Verify the enrollment email content
+ const enrollmentEmailOptions = interceptedEmails[1];
+ test.isTrue(enrollmentEmailOptions, 'Enrollment email should have been intercepted');
+ const enrollmentRe = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`);
+ const enrollmentMatch = enrollmentEmailOptions.text.match(enrollmentRe);
+ test.isTrue(enrollmentMatch, 'Enrollment email should contain enrollment URL');
+ const enrollmentTokenFromUrl = enrollmentMatch[1];
+ test.isTrue(enrollmentResult.url.includes(enrollmentTokenFromUrl), 'Enrollment URL in result should contain the token');
+
+ // Verify the reset password email content
+ const resetEmailOptions = interceptedEmails[2];
+ test.isTrue(resetEmailOptions, 'Reset password email should have been intercepted');
+ const resetRe = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
+ const resetMatch = resetEmailOptions.text.match(resetRe);
+ test.isTrue(resetMatch, 'Reset password email should contain reset URL');
+ const resetTokenFromUrl = resetMatch[1];
+ test.isTrue(resetResult.url.includes(resetTokenFromUrl), 'Reset URL in result should contain the token');
+
+ // Verify email headers and from address for all emails
+ for (const emailOptions of interceptedEmails) {
+ test.equal(emailOptions.from, 'test@meteor.com', 'From address should match');
+ test.equal(emailOptions.headers['My-Custom-Header'], 'Cool', 'Custom header should be present');
+ }
+ } finally {
+ // Restore the original Email.sendAsync
+ Email.sendAsync = originalSendAsync;
+ }
+ });
})();
diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js
index 48a34c8766..50a1e95862 100644
--- a/packages/accounts-password/password_tests_setup.js
+++ b/packages/accounts-password/password_tests_setup.js
@@ -121,7 +121,7 @@ Accounts.config({
Meteor.methods(
{
testMeteorUser:
- async () => await Meteor.user(),
+ async () => await Meteor.userAsync(),
clearUsernameAndProfile:
async function () {
diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js
index 902897a01d..060171cd54 100644
--- a/packages/accounts-passwordless/package.js
+++ b/packages/accounts-passwordless/package.js
@@ -1,6 +1,6 @@
Package.describe({
summary: 'No-password login/sign-up support for accounts',
- version: '3.0.0',
+ version: '3.0.2',
});
Package.onUse(api => {
diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js
index 33925be23a..20c23e17e8 100644
--- a/packages/accounts-passwordless/passwordless_server.js
+++ b/packages/accounts-passwordless/passwordless_server.js
@@ -220,7 +220,7 @@ Meteor.methods({
*/
Accounts.sendLoginTokenEmail = async ({ userId, sequence, email, extra = {} }) => {
const user = await getUserById(userId);
- const url = Accounts.urls.loginToken(email, sequence);
+ const url = await Accounts._resolvePromise(Accounts.urls.loginToken(email, sequence, extra));
const options = await Accounts.generateOptionsForEmail(
email,
user,
diff --git a/packages/allow-deny/allow-deny.js b/packages/allow-deny/allow-deny.js
index faf7f774c9..802cd467dd 100644
--- a/packages/allow-deny/allow-deny.js
+++ b/packages/allow-deny/allow-deny.js
@@ -49,7 +49,7 @@ const CollectionPrototype = AllowDeny.CollectionPrototype;
* @memberOf Mongo.Collection
* @instance
* @param {Object} options
- * @param {Function} options.insertAsync,updateAsync,removeAsync Functions that look at a proposed modification to the database and return true if it should be allowed.
+ * @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be allowed.
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
*/
@@ -64,7 +64,7 @@ CollectionPrototype.allow = function(options) {
* @memberOf Mongo.Collection
* @instance
* @param {Object} options
- * @param {Function} options.insertAsync,updateAsync,removeAsync Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
+ * @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
*/
@@ -174,9 +174,14 @@ CollectionPrototype._defineMutationMethods = function(options) {
// single-ID selectors.
if (!isInsert(method)) throwIfSelectorIsNotId(args[0], method);
+ const syncMethodName = method.replace('Async', '');
+ const syncValidatedMethodName = '_validated' + method.charAt(0).toUpperCase() + syncMethodName.slice(1);
+ // it forces to use async validated behavior
+ const validatedMethodName = syncValidatedMethodName + 'Async';
+
if (self._restricted) {
// short circuit if there is no way it will pass.
- if (self._validators[method].allow.length === 0) {
+ if (self._validators[syncMethodName].allow.length === 0) {
throw new Meteor.Error(
403,
'Access denied. No allow validators set on restricted ' +
@@ -186,11 +191,6 @@ CollectionPrototype._defineMutationMethods = function(options) {
);
}
- const syncMethodName = method.replace('Async', '');
- const syncValidatedMethodName = '_validated' + method.charAt(0).toUpperCase() + syncMethodName.slice(1);
- // it forces to use async validated behavior on the server
- const validatedMethodName = Meteor.isServer ? syncValidatedMethodName + 'Async' : syncValidatedMethodName;
-
args.unshift(this.userId);
isInsert(method) && args.push(generatedId);
return self[validatedMethodName].apply(self, args);
@@ -292,7 +292,7 @@ CollectionPrototype._validatedInsertAsync = async function(userId, doc,
const self = this;
// call user validators.
// Any deny returns true means denied.
- if (await asyncSome(self._validators.insertAsync.deny, async (validator) => {
+ if (await asyncSome(self._validators.insert.deny, async (validator) => {
const result = validator(userId, docToValidate(validator, doc, generatedId));
return Meteor._isPromise(result) ? await result : result;
})) {
@@ -300,7 +300,7 @@ CollectionPrototype._validatedInsertAsync = async function(userId, doc,
}
// Any allow returns true means proceed. Throw error if they all fail.
- if (await asyncEvery(self._validators.insertAsync.allow, async (validator) => {
+ if (await asyncEvery(self._validators.insert.allow, async (validator) => {
const result = validator(userId, docToValidate(validator, doc, generatedId));
return !(Meteor._isPromise(result) ? await result : result);
})) {
@@ -315,36 +315,6 @@ CollectionPrototype._validatedInsertAsync = async function(userId, doc,
return self._collection.insertAsync.call(self._collection, doc);
};
-CollectionPrototype._validatedInsert = function (userId, doc,
- generatedId) {
- const self = this;
-
- // call user validators.
- // Any deny returns true means denied.
- if (self._validators.insert.deny.some((validator) => {
- return validator(userId, docToValidate(validator, doc, generatedId));
- })) {
- throw new Meteor.Error(403, "Access denied");
- }
- // Any allow returns true means proceed. Throw error if they all fail.
-
- if (self._validators.insert.allow.every((validator) => {
- return !validator(userId, docToValidate(validator, doc, generatedId));
- })) {
- throw new Meteor.Error(403, "Access denied");
- }
-
- // If we generated an ID above, insert it now: after the validation, but
- // before actually inserting.
- if (generatedId !== null)
- doc._id = generatedId;
-
- return (Meteor.isServer
- ? self._collection.insertAsync
- : self._collection.insert
- ).call(self._collection, doc);
-};
-
// Simulate a mongo `update` operation while validating that the access
// control rules set by calls to `allow/deny` are satisfied. If all
// pass, rewrite the mongo operation to use $in to set the list of
@@ -414,7 +384,7 @@ CollectionPrototype._validatedUpdateAsync = async function(
// call user validators.
// Any deny returns true means denied.
- if (await asyncSome(self._validators.updateAsync.deny, async (validator) => {
+ if (await asyncSome(self._validators.update.deny, async (validator) => {
const factoriedDoc = transformDoc(validator, doc);
const result = validator(userId,
factoriedDoc,
@@ -424,8 +394,9 @@ CollectionPrototype._validatedUpdateAsync = async function(
})) {
throw new Meteor.Error(403, "Access denied");
}
+
// Any allow returns true means proceed. Throw error if they all fail.
- if (await asyncEvery(self._validators.updateAsync.allow, async (validator) => {
+ if (await asyncEvery(self._validators.update.allow, async (validator) => {
const factoriedDoc = transformDoc(validator, doc);
const result = validator(userId,
factoriedDoc,
@@ -447,102 +418,6 @@ CollectionPrototype._validatedUpdateAsync = async function(
self._collection, selector, mutator, options);
};
-CollectionPrototype._validatedUpdate = function(
- userId, selector, mutator, options) {
- const self = this;
-
- check(mutator, Object);
-
- options = Object.assign(Object.create(null), options);
-
- if (!LocalCollection._selectorIsIdPerhapsAsObject(selector))
- throw new Error("validated update should be of a single ID");
-
- // We don't support upserts because they don't fit nicely into allow/deny
- // rules.
- if (options.upsert)
- throw new Meteor.Error(403, "Access denied. Upserts not " +
- "allowed in a restricted collection.");
-
- const noReplaceError = "Access denied. In a restricted collection you can only" +
- " update documents, not replace them. Use a Mongo update operator, such " +
- "as '$set'.";
-
- const mutatorKeys = Object.keys(mutator);
-
- // compute modified fields
- const modifiedFields = {};
-
- if (mutatorKeys.length === 0) {
- throw new Meteor.Error(403, noReplaceError);
- }
- mutatorKeys.forEach((op) => {
- const params = mutator[op];
- if (op.charAt(0) !== '$') {
- throw new Meteor.Error(403, noReplaceError);
- } else if (!hasOwn.call(ALLOWED_UPDATE_OPERATIONS, op)) {
- throw new Meteor.Error(
- 403, "Access denied. Operator " + op + " not allowed in a restricted collection.");
- } else {
- Object.keys(params).forEach((field) => {
- // treat dotted fields as if they are replacing their
- // top-level part
- if (field.indexOf('.') !== -1)
- field = field.substring(0, field.indexOf('.'));
-
- // record the field we are trying to change
- modifiedFields[field] = true;
- });
- }
- });
-
- const fields = Object.keys(modifiedFields);
-
- const findOptions = {transform: null};
- if (!self._validators.fetchAllFields) {
- findOptions.fields = {};
- self._validators.fetch.forEach((fieldName) => {
- findOptions.fields[fieldName] = 1;
- });
- }
-
- const doc = self._collection.findOne(selector, findOptions);
- if (!doc) // none satisfied!
- return 0;
-
- // call user validators.
- // Any deny returns true means denied.
- if (self._validators.update.deny.some((validator) => {
- const factoriedDoc = transformDoc(validator, doc);
- return validator(userId,
- factoriedDoc,
- fields,
- mutator);
- })) {
- throw new Meteor.Error(403, "Access denied");
- }
- // Any allow returns true means proceed. Throw error if they all fail.
- if (self._validators.update.allow.every((validator) => {
- const factoriedDoc = transformDoc(validator, doc);
- return !validator(userId,
- factoriedDoc,
- fields,
- mutator);
- })) {
- throw new Meteor.Error(403, "Access denied");
- }
-
- options._forbidReplace = true;
-
- // Back when we supported arbitrary client-provided selectors, we actually
- // rewrote the selector to include an _id clause before passing to Mongo to
- // avoid races, but since selector is guaranteed to already just be an ID, we
- // don't have to any more.
-
- return self._collection.update.call(
- self._collection, selector, mutator, options);
-};
-
// Only allow these operations in validated updates. Specifically
// whitelist operations, rather than blacklist, so new complex
// operations that are added aren't automatically allowed. A complex
@@ -573,14 +448,14 @@ CollectionPrototype._validatedRemoveAsync = async function(userId, selector) {
// call user validators.
// Any deny returns true means denied.
- if (await asyncSome(self._validators.removeAsync.deny, async (validator) => {
+ if (await asyncSome(self._validators.remove.deny, async (validator) => {
const result = validator(userId, transformDoc(validator, doc));
return Meteor._isPromise(result) ? await result : result;
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
- if (await asyncEvery(self._validators.removeAsync.allow, async (validator) => {
+ if (await asyncEvery(self._validators.remove.allow, async (validator) => {
const result = validator(userId, transformDoc(validator, doc));
return !(Meteor._isPromise(result) ? await result : result);
})) {
@@ -595,43 +470,6 @@ CollectionPrototype._validatedRemoveAsync = async function(userId, selector) {
return self._collection.removeAsync.call(self._collection, selector);
};
-CollectionPrototype._validatedRemove = function(userId, selector) {
- const self = this;
-
- const findOptions = {transform: null};
- if (!self._validators.fetchAllFields) {
- findOptions.fields = {};
- self._validators.fetch.forEach((fieldName) => {
- findOptions.fields[fieldName] = 1;
- });
- }
-
- const doc = self._collection.findOne(selector, findOptions);
- if (!doc)
- return 0;
-
- // call user validators.
- // Any deny returns true means denied.
- if (self._validators.remove.deny.some((validator) => {
- return validator(userId, transformDoc(validator, doc));
- })) {
- throw new Meteor.Error(403, "Access denied");
- }
- // Any allow returns true means proceed. Throw error if they all fail.
- if (self._validators.remove.allow.every((validator) => {
- return !validator(userId, transformDoc(validator, doc));
- })) {
- throw new Meteor.Error(403, "Access denied");
- }
-
- // Back when we supported arbitrary client-provided selectors, we actually
- // rewrote the selector to {_id: {$in: [ids that we found]}} before passing to
- // Mongo to avoid races, but since selector is guaranteed to already just be
- // an ID, we don't have to any more.
-
- return self._collection.remove.call(self._collection, selector);
-};
-
CollectionPrototype._callMutatorMethodAsync = function _callMutatorMethodAsync(name, args, options = {}) {
// For two out of three mutator methods, the first argument is a selector
@@ -711,6 +549,13 @@ function addValidator(collection, allowOrDeny, options) {
Object.keys(options).forEach((key) => {
if (!validKeysRegEx.test(key))
throw new Error(allowOrDeny + ": Invalid key: " + key);
+
+ // TODO deprecated async config on future versions
+ const isAsyncKey = key.includes('Async');
+ if (isAsyncKey) {
+ const syncKey = key.replace('Async', '');
+ Meteor.deprecate(allowOrDeny + `: The "${key}" key is deprecated. Use "${syncKey}" instead.`);
+ }
});
collection._restricted = true;
@@ -740,7 +585,9 @@ function addValidator(collection, allowOrDeny, options) {
options.transform
);
}
- collection._validators[name][allowOrDeny].push(options[name]);
+ const isAsyncName = name.includes('Async');
+ const validatorSyncName = isAsyncName ? name.replace('Async', '') : name;
+ collection._validators[validatorSyncName][allowOrDeny].push(options[name]);
}
});
diff --git a/packages/allow-deny/package.js b/packages/allow-deny/package.js
index 98a3d427a9..c4dc9d3e78 100644
--- a/packages/allow-deny/package.js
+++ b/packages/allow-deny/package.js
@@ -1,6 +1,6 @@
Package.describe({
name: 'allow-deny',
- version: '2.0.0',
+ version: '2.1.0',
// Brief, one-line summary of the package.
summary: 'Implements functionality for allow/deny and client-side db operations',
// URL to the Git repository containing the source code for this package.
diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js
index 7d20443c27..82af4c65e6 100644
--- a/packages/autoupdate/autoupdate_server.js
+++ b/packages/autoupdate/autoupdate_server.js
@@ -25,6 +25,7 @@
// The ID of each document is the client architecture, and the fields of
// the document are the versions described above.
+import { onMessage } from "meteor/inter-process-messaging";
import { ClientVersions } from "./client_versions.js";
export const Autoupdate = __meteor_runtime_config__.autoupdate = {
@@ -152,7 +153,6 @@ function enqueueVersionsRefresh() {
const setupListeners = () => {
// Listen for messages pertaining to the client-refresh topic.
- import { onMessage } from "meteor/inter-process-messaging";
onMessage("client-refresh", enqueueVersionsRefresh);
// Another way to tell the process to refresh: send SIGHUP signal
diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js
index 4769c9e31b..514439ce1c 100644
--- a/packages/autoupdate/package.js
+++ b/packages/autoupdate/package.js
@@ -1,6 +1,6 @@
Package.describe({
summary: 'Update the client when new client code is available',
- version: '2.0.0',
+ version: '2.0.1',
});
Package.onUse(function(api) {
diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js
index 11cf63f923..5606e78665 100644
--- a/packages/babel-compiler/babel-compiler.js
+++ b/packages/babel-compiler/babel-compiler.js
@@ -1,14 +1,21 @@
var semver = Npm.require("semver");
var JSON5 = Npm.require("json5");
+var SWC = Npm.require("@meteorjs/swc-core");
+const reifyCompile = Npm.require("@meteorjs/reify/lib/compiler").compile;
+const reifyAcornParse = Npm.require("@meteorjs/reify/lib/parsers/acorn").parse;
+var fs = Npm.require('fs');
+var path = Npm.require('path');
+var vm = Npm.require('vm');
+var crypto = Npm.require('crypto');
/**
* A compiler that can be instantiated with features and used inside
* Plugin.registerCompiler
* @param {Object} extraFeatures The same object that getDefaultOptions takes
*/
-BabelCompiler = function BabelCompiler(extraFeatures, modifyBabelConfig) {
+BabelCompiler = function BabelCompiler(extraFeatures, modifyConfig) {
this.extraFeatures = extraFeatures;
- this.modifyBabelConfig = modifyBabelConfig;
+ this.modifyConfig = modifyConfig;
this._babelrcCache = null;
this._babelrcWarnings = Object.create(null);
this.cacheDirectory = null;
@@ -18,18 +25,180 @@ var BCp = BabelCompiler.prototype;
var excludedFileExtensionPattern = /\.(es5|min)\.js$/i;
var hasOwn = Object.prototype.hasOwnProperty;
+function getMeteorConfig() {
+ return Plugin?.getMeteorConfig() || {};
+}
+
+// Check if verbose mode is enabled either in the provided config or in extraFeatures
+BCp.isVerbose = function(config = getMeteorConfig()) {
+ if (config?.modern?.transpiler?.verbose) {
+ return true;
+ }
+ if (config?.modern?.verbose) {
+ return true;
+ }
+ if (config?.verbose) {
+ return true;
+ }
+ return !!this.extraFeatures?.verbose;
+};
+
// There's no way to tell the current Meteor version, but we can infer
// whether it's Meteor 1.4.4 or earlier by checking the Node version.
var isMeteorPre144 = semver.lt(process.version, "4.8.1");
var enableClientTLA = process.env.METEOR_ENABLE_CLIENT_TOP_LEVEL_AWAIT === 'true';
+function compileWithBabel(source, babelOptions, cacheOptions) {
+ return profile('Babel.compile', function () {
+ return Babel.compile(source, babelOptions, cacheOptions);
+ });
+}
+
+function compileWithSwc(source, swcOptions = {}, { features }) {
+ return profile('SWC.compile', function () {
+ // Perform SWC transformation.
+ const transformed = SWC.transformSync(source, swcOptions);
+
+ let content = transformed.code;
+
+ // Preserve Meteor-specific features: reify modules, nested imports, and top-level await support.
+ const result = reifyCompile(content, {
+ parse: reifyAcornParse,
+ generateLetDeclarations: false,
+ ast: false,
+ // Enforce reify options for proper compatibility.
+ avoidModernSyntax: true,
+ enforceStrictMode: false,
+ dynamicImport: true,
+ ...(features.topLevelAwait && { topLevelAwait: true }),
+ ...(features.compileForShell && { moduleAlias: 'module' }),
+ ...((features.modernBrowsers || features.nodeMajorVersion >= 8) && {
+ avoidModernSyntax: false,
+ generateLetDeclarations: true,
+ }),
+ });
+ content = result.code;
+
+ return {
+ code: content,
+ map: JSON.parse(transformed.map),
+ sourceType: 'module',
+ };
+ });
+}
+
+BCp.initializeMeteorAppConfig = function () {
+ const meteorConfig = getMeteorConfig();
+ if (this.isVerbose()) {
+ logConfigBlock('Meteor Config', meteorConfig);
+ }
+ return meteorConfig;
+};
+
+let lastModifiedSwcConfig;
+let lastModifiedSwcConfigTime;
+BCp.initializeMeteorAppSwcrc = function () {
+ const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`);
+ const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`);
+ if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) {
+ return;
+ }
+ const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc';
+ const filePath = `${getMeteorAppDir()}/${swcFile}`;
+ const fileStats = fs.statSync(filePath);
+ const fileModTime = fileStats?.mtime?.getTime();
+
+ let currentLastModifiedConfigTime;
+ if (hasSwcJs) {
+ // For dynamic JS files, first get the resolved configuration
+ const resolvedConfig = lastModifiedSwcConfigTime?.includes(`${fileModTime}`)
+ ? lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile)
+ : getMeteorAppSwcrc(swcFile);
+ // Calculate a hash of the resolved configuration to detect changes
+ const contentHash = crypto
+ .createHash('sha256')
+ .update(JSON.stringify(resolvedConfig))
+ .digest('hex');
+ // Combine file modification time and content hash to create a unique identifier
+ currentLastModifiedConfigTime = `${fileModTime}-${contentHash}`;
+ // Store the resolved configuration
+ lastModifiedSwcConfig = resolvedConfig;
+ } else {
+ // For static JSON files, just use the file modification time
+ currentLastModifiedConfigTime = fileModTime;
+ }
+
+ if (currentLastModifiedConfigTime !== lastModifiedSwcConfigTime) {
+ lastModifiedSwcConfigTime = currentLastModifiedConfigTime;
+ lastModifiedSwcConfig = getMeteorAppSwcrc(swcFile);
+
+ if (this.isVerbose()) {
+ logConfigBlock('SWC Custom Config', lastModifiedSwcConfig);
+ }
+
+ this._swcIncompatible = {};
+ }
+ return lastModifiedSwcConfig;
+};
+
+let lastModifiedSwcLegacyConfig;
+BCp.initializeMeteorAppLegacyConfig = function () {
+ const swcLegacyConfig = convertBabelTargetsForSwc(Babel.getMinimumModernBrowserVersions());
+ if (this.isVerbose() && !lastModifiedSwcLegacyConfig) {
+ logConfigBlock('SWC Legacy Config', swcLegacyConfig);
+ }
+ lastModifiedSwcLegacyConfig = swcLegacyConfig;
+ return lastModifiedSwcConfig;
+};
+
+// Helper function to check if @swc/helpers is available
+function hasSwcHelpers() {
+ return fs.existsSync(`${getMeteorAppDir()}/node_modules/@swc/helpers`);
+}
+
+// Helper function to log friendly messages about SWC helpers
+function logSwcHelpersStatus(isAvailable) {
+ const label = color('[SWC Helpers]', 36);
+
+ if (isAvailable) {
+ // Green message for when helpers are available
+ console.log(`${label} ${color('✓ @swc/helpers is available in your project!', 32)}`);
+ console.log(` ${color('Benefits:', 32)}`);
+ console.log(` ${color('• Smaller bundle size: External helpers reduce code duplication', 32)}`);
+ console.log(` ${color('• Faster loads: less code to parse on first download', 32)}`);
+ console.log(` ${color('• Optional caching: separate vendor chunk can be cached by browsers', 32)}`);
+ } else {
+ // Yellow message for when helpers are not available
+ console.log(`${label} ${color('⚠ @swc/helpers is not available in your project', 33)}`);
+ console.log(` ${color('Suggestion:', 33)}`);
+ console.log(` ${color('• Add @swc/helpers to your project:', 33)}`);
+ console.log(` ${color('meteor npm install --save @swc/helpers', 33)}`);
+ console.log(` ${color('• This will reduce bundle size and improve performance', 33)}`);
+ }
+ console.log();
+}
+
+let hasSwcHelpersAvailable = false;
+BCp.initializeMeteorAppSwcHelpersAvailable = function () {
+ hasSwcHelpersAvailable = hasSwcHelpers();
+ if (this.isVerbose()) {
+ logSwcHelpersStatus(hasSwcHelpersAvailable);
+ }
+ return hasSwcHelpersAvailable;
+};
+
BCp.processFilesForTarget = function (inputFiles) {
var compiler = this;
// Reset this cache for each batch processed.
this._babelrcCache = null;
+ this.initializeMeteorAppConfig();
+ this.initializeMeteorAppSwcrc();
+ this.initializeMeteorAppLegacyConfig();
+ this.initializeMeteorAppSwcHelpersAvailable();
+
inputFiles.forEach(function (inputFile) {
if (inputFile.supportsLazyCompilation) {
inputFile.addJavaScript({
@@ -51,6 +220,8 @@ BCp.processFilesForTarget = function (inputFiles) {
// null to indicate there was an error, and nothing should be added.
BCp.processOneFileForTarget = function (inputFile, source) {
this._babelrcCache = this._babelrcCache || Object.create(null);
+ this._swcCache = this._swcCache || Object.create(null);
+ this._swcIncompatible = this._swcIncompatible || Object.create(null);
if (typeof source !== "string") {
// Other compiler plugins can call processOneFileForTarget with a
@@ -92,6 +263,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 &&
@@ -121,32 +294,202 @@ BCp.processOneFileForTarget = function (inputFile, source) {
},
};
- this.inferTypeScriptConfig(
- features, inputFile, cacheOptions.cacheDeps);
-
- var babelOptions = Babel.getDefaultOptions(features);
- babelOptions.caller = { name: "meteor", arch };
-
- this.inferExtraBabelOptions(
- inputFile,
- babelOptions,
- cacheOptions.cacheDeps
- );
-
- babelOptions.sourceMaps = true;
- babelOptions.filename =
- babelOptions.sourceFileName = packageName
- ? "packages/" + packageName + "/" + inputFilePath
+ const filename = packageName
+ ? `packages/${packageName}/${inputFilePath}`
: inputFilePath;
- if (this.modifyBabelConfig) {
- this.modifyBabelConfig(babelOptions, inputFile);
- }
+ const setupBabelOptions = () => {
+ this.inferTypeScriptConfig(features, inputFile, cacheOptions.cacheDeps);
+ var babelOptions = Babel.getDefaultOptions(features);
+ babelOptions.caller = { name: "meteor", arch };
+
+ babelOptions.sourceMaps = true;
+ babelOptions.filename = babelOptions.sourceFileName = filename;
+
+ this.inferExtraBabelOptions(inputFile, babelOptions, cacheOptions.cacheDeps);
+
+ if (this.modifyConfig) {
+ this.modifyConfig(babelOptions, inputFile);
+ }
+
+ return babelOptions;
+ };
+
+ const setupSWCOptions = () => {
+ const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx');
+ const hasTSXSupport = inputFilePath.endsWith('.tsx');
+ const hasJSXSupport = inputFilePath.endsWith('.jsx');
+ const isLegacyWebArch = arch.includes('legacy');
+
+ var swcOptions = {
+ jsc: {
+ ...(!isLegacyWebArch && { target: 'es2015' }),
+ parser: {
+ syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript',
+ jsx: hasJSXSupport,
+ tsx: hasTSXSupport,
+ },
+ ...(hasSwcHelpersAvailable &&
+ (packageName == null ||
+ !['modules-runtime'].includes(packageName)) && {
+ externalHelpers: true,
+ }),
+ },
+ module: { type: 'es6' },
+ minify: false,
+ sourceMaps: true,
+ filename,
+ sourceFileName: filename,
+ ...(isLegacyWebArch && {
+ env: { targets: lastModifiedSwcLegacyConfig || {} },
+ }),
+ };
+
+ // Merge with app-level SWC config
+ if (lastModifiedSwcConfig) {
+ swcOptions = deepMerge(swcOptions, lastModifiedSwcConfig, [
+ 'env.targets',
+ 'module.type',
+ ]);
+ }
+
+ this.inferExtraSWCOptions(inputFile, swcOptions, cacheOptions.cacheDeps);
+
+ if (!!this.extraFeatures?.swc && this.modifyConfig) {
+ this.modifyConfig(swcOptions, inputFile);
+ }
+
+ // Resolve custom baseUrl to an absolute path pointing to the project root
+ if (swcOptions.jsc && swcOptions.jsc.baseUrl) {
+ swcOptions.jsc.baseUrl = path.resolve(process.cwd(), swcOptions.jsc.baseUrl);
+ }
+
+ return swcOptions;
+ };
+
+ var babelOptions = { filename };
try {
- var result = profile('Babel.compile', function () {
- return Babel.compile(source, babelOptions, cacheOptions);
- });
+ var result = (() => {
+ const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/");
+ const isAppCode = packageName == null && !isNodeModulesCode;
+ const isPackageCode = packageName != null;
+ const isLegacyWebArch = arch.includes('legacy');
+
+ const transpConfig = getMeteorConfig()?.modern?.transpiler;
+ const hasModernTranspiler = transpConfig != null && transpConfig !== false;
+ const shouldSkipSwc =
+ !hasModernTranspiler ||
+ (isAppCode && transpConfig?.excludeApp === true) ||
+ (isNodeModulesCode && transpConfig?.excludeNodeModules === true) ||
+ (isPackageCode && transpConfig?.excludePackages === true) ||
+ (isLegacyWebArch && transpConfig?.excludeLegacy === true) ||
+ (isAppCode &&
+ Array.isArray(transpConfig?.excludeApp) &&
+ isExcludedConfig(inputFilePath, transpConfig?.excludeApp || [])) ||
+ (isNodeModulesCode &&
+ Array.isArray(transpConfig?.excludeNodeModules) &&
+ (isExcludedConfig(inputFilePath, transpConfig?.excludeNodeModules || []) ||
+ isExcludedConfig(
+ inputFilePath.replace('node_modules/', ''),
+ transpConfig?.excludeNodeModules || [],
+ true,
+ ))) ||
+ (isPackageCode &&
+ Array.isArray(transpConfig?.excludePackages) &&
+ (isExcludedConfig(packageName, transpConfig?.excludePackages || []) ||
+ isExcludedConfig(
+ `${packageName}/${inputFilePath}`,
+ transpConfig?.excludePackages || [],
+ )));
+
+ const cacheKey = [
+ toBeAdded.hash,
+ lastModifiedSwcConfigTime,
+ isLegacyWebArch ? 'legacy' : '',
+ hasSwcHelpersAvailable,
+ ]
+ .filter(Boolean)
+ .join('-');
+ // Determine if SWC should be used based on package and file criteria.
+ const shouldUseSwc =
+ (!shouldSkipSwc || this.extraFeatures?.swc) &&
+ !this._swcIncompatible[cacheKey];
+ let compilation;
+ try {
+ let usedSwc = false;
+ if (shouldUseSwc) {
+ // Create a cache key based on the source hash and the compiler used
+ // Check cache
+ compilation = this.readFromSwcCache({ cacheKey });
+ // Return cached result if found.
+ if (compilation) {
+ if (this.isVerbose()) {
+ logTranspilation({
+ usedSwc: true,
+ inputFilePath,
+ packageName,
+ isNodeModulesCode,
+ cacheHit: true,
+ arch,
+ });
+ }
+ return compilation;
+ }
+
+ const swcOptions = setupSWCOptions();
+ compilation = compileWithSwc(
+ source,
+ swcOptions,
+ { features },
+ );
+ // Save result in cache
+ this.writeToSwcCache({ cacheKey, compilation });
+ usedSwc = true;
+ } else {
+ // Set up Babel options only when compiling with Babel
+ babelOptions = setupBabelOptions();
+
+ compilation = compileWithBabel(source, babelOptions, cacheOptions);
+ usedSwc = false;
+ }
+
+ if (this.isVerbose()) {
+ logTranspilation({
+ usedSwc,
+ inputFilePath,
+ packageName,
+ isNodeModulesCode,
+ cacheHit: false,
+ arch,
+ });
+ }
+ } catch (e) {
+ this._swcIncompatible[cacheKey] = true;
+ // If SWC fails, fall back to Babel
+
+ babelOptions = setupBabelOptions();
+ compilation = compileWithBabel(source, babelOptions, cacheOptions);
+ if (this.isVerbose()) {
+ logTranspilation({
+ usedSwc: false,
+ inputFilePath,
+ packageName,
+ isNodeModulesCode,
+ cacheHit: false,
+ arch,
+ errorMessage: e?.message,
+ ...(e?.message?.includes(
+ 'cannot be used outside of module code',
+ ) && {
+ tip: 'Remove nested imports or replace them with require to support SWC and improve speed.',
+ }),
+ });
+ }
+ }
+
+ return compilation;
+ })();
} catch (e) {
if (e.loc) {
// Error is from @babel/parser.
@@ -256,6 +599,15 @@ BCp.inferExtraBabelOptions = function (inputFile, babelOptions, cacheDeps) {
);
};
+BCp.inferExtraSWCOptions = function (inputFile, swcOptions, cacheDeps) {
+ if (! inputFile.require ||
+ ! inputFile.findControlFile ||
+ ! inputFile.readAndWatchFile) {
+ return false;
+ }
+ return this._inferFromSwcRc(inputFile, swcOptions, cacheDeps);
+};
+
BCp._inferFromBabelRc = function (inputFile, babelOptions, cacheDeps) {
var babelrcPath = inputFile.findControlFile(".babelrc");
if (babelrcPath) {
@@ -308,6 +660,65 @@ BCp._inferFromPackageJson = function (inputFile, babelOptions, cacheDeps) {
}
};
+BCp._inferFromSwcRc = function (inputFile, swcOptions, cacheDeps) {
+ var swcrcPath = inputFile.findControlFile(".swcrc");
+ if (swcrcPath) {
+ if (! hasOwn.call(this._babelrcCache, swcrcPath)) {
+ try {
+ this._babelrcCache[swcrcPath] = {
+ controlFilePath: swcrcPath,
+ controlFileData: JSON.parse(
+ inputFile.readAndWatchFile(swcrcPath)),
+ deps: Object.create(null),
+ };
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ e.message = ".swcrc is not a valid JSON file: " + e.message;
+ }
+ throw e;
+ }
+ }
+
+ const cacheEntry = this._babelrcCache[swcrcPath];
+
+ if (this._inferHelperForSwc(inputFile, cacheEntry)) {
+ deepMerge(swcOptions, cacheEntry.controlFileData);
+ Object.assign(cacheDeps, cacheEntry.deps);
+ return true;
+ }
+ }
+};
+
+BCp._inferHelperForSwc = function (inputFile, cacheEntry) {
+ if (! cacheEntry.controlFileData) {
+ return false;
+ }
+
+ if (hasOwn.call(cacheEntry, "finalInferHelperForSwcResult")) {
+ // We've already run _inferHelperForSwc and populated
+ // cacheEntry.controlFileData, so we can return early here.
+ return cacheEntry.finalInferHelperForSwcResult;
+ }
+
+ // First, ensure that the current file path is not excluded.
+ if (cacheEntry.controlFileData.exclude) {
+ const exclude = cacheEntry.controlFileData.exclude;
+ const path = inputFile.getPathInPackage();
+
+ if (exclude instanceof Array) {
+ for (let i = 0; i < exclude.length; ++i) {
+ if (path.match(exclude[i])) {
+ return cacheEntry.finalInferHelperForSwcResult = false;
+ }
+ }
+ } else if (path.match(exclude)) {
+ return cacheEntry.finalInferHelperForSwcResult = false;
+ }
+ }
+
+ return cacheEntry.finalInferHelperForSwcResult = true;
+};
+
BCp._inferHelper = function (inputFile, cacheEntry) {
if (! cacheEntry.controlFileData) {
return false;
@@ -564,3 +975,245 @@ function packageNameFromTopLevelModuleId(id) {
}
return parts[0];
}
+
+const SwcCacheContext = '.swc-cache';
+
+BCp.readFromSwcCache = function({ cacheKey }) {
+ // Check in-memory cache.
+ let compilation = this._swcCache[cacheKey];
+ // If not found, try file system cache if enabled.
+ if (!compilation && this.cacheDirectory) {
+ const cacheFilePath = path.join(this.cacheDirectory, SwcCacheContext, `${cacheKey}.json`);
+ if (fs.existsSync(cacheFilePath)) {
+ try {
+ compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8'));
+ // Save back to in-memory cache.
+ this._swcCache[cacheKey] = compilation;
+ } catch (err) {
+ // Ignore any errors reading/parsing the cache.
+ }
+ }
+ }
+ return compilation;
+};
+
+BCp.writeToSwcCache = function({ cacheKey, compilation }) {
+ // Save to in-memory cache.
+ this._swcCache[cacheKey] = compilation;
+ // If file system caching is enabled, write asynchronously.
+ if (this.cacheDirectory) {
+ const cacheFilePath = path.join(this.cacheDirectory, SwcCacheContext, `${cacheKey}.json`);
+ try {
+ const writeFileCache = async () => {
+ await fs.promises.mkdir(path.dirname(cacheFilePath), { recursive: true });
+ await fs.promises.writeFile(cacheFilePath, JSON.stringify(compilation), 'utf8');
+ };
+ // Invoke without blocking the main flow.
+ writeFileCache();
+ } catch (err) {
+ // If writing fails, ignore the error.
+ }
+ }
+};
+
+function getMeteorAppDir() {
+ return process.cwd();
+}
+
+function getMeteorAppPackageJson() {
+ return JSON.parse(
+ fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8'),
+ );
+}
+
+function getMeteorAppSwcrc(file = '.swcrc') {
+ try {
+ const filePath = `${getMeteorAppDir()}/${file}`;
+ if (file.endsWith('.js')) {
+ let content = fs.readFileSync(filePath, 'utf-8');
+ // Check if the content uses ES module syntax (export default)
+ if (content.includes('export default')) {
+ // Transform ES module syntax to CommonJS
+ content = content.replace(/export\s+default\s+/, 'module.exports = ');
+ }
+ const script = new vm.Script(`
+ (function() {
+ const module = {};
+ module.exports = {};
+ (function(exports, module) {
+ ${content}
+ })(module.exports, module);
+ return module.exports;
+ })()
+ `);
+ const context = vm.createContext({ process });
+ return script.runInContext(context);
+ } else {
+ // For .swcrc and other JSON files, parse as JSON
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
+ }
+ } catch (e) {
+ console.error(`Error parsing ${file} file`, e);
+ }
+}
+
+const _regexCache = new Map();
+
+function isRegexLike(str) {
+ return /[.*+?^${}()|[\]\\]/.test(str);
+}
+
+function isExcludedConfig(name, excludeList = [], startsWith) {
+ if (!name || !excludeList?.length) return false;
+ return excludeList.some(rule => {
+ if (name === rule) return true;
+ if (startsWith && name.startsWith(rule)) return true;
+ if (isRegexLike(rule)) {
+ let regex = _regexCache.get(rule);
+ if (!regex) {
+ try {
+ regex = new RegExp(rule);
+ _regexCache.set(rule, regex);
+ } catch (err) {
+ console.warn(`Invalid regex in exclude list: "${rule}"`);
+ return false;
+ }
+ }
+ return regex.test(name);
+ }
+
+ return false;
+ });
+}
+
+const disableTextColors = Boolean(JSON.parse(process.env.METEOR_DISABLE_COLORS || "false"));
+
+function color(text, code) {
+ return disableTextColors ? text : `\x1b[${code}m${text}\x1b[0m`;
+}
+
+function logTranspilation({
+ packageName,
+ inputFilePath,
+ usedSwc,
+ cacheHit,
+ isNodeModulesCode,
+ arch,
+ errorMessage = '',
+ tip = '',
+}) {
+ const transpiler = usedSwc ? 'SWC' : 'Babel';
+ const transpilerColor = usedSwc ? 32 : 33;
+ const label = color('[Transpiler]', 36);
+ const transpilerPart = `${label} Used ${color(
+ transpiler,
+ transpilerColor,
+ )} for`;
+ const filePathPadded = `${
+ packageName ? `${packageName}/` : ''
+ }${inputFilePath}`.padEnd(50);
+ let rawOrigin = '';
+ if (packageName) {
+ rawOrigin = `(package)`;
+ } else {
+ rawOrigin = isNodeModulesCode ? '(node_modules)' : '(app)';
+ }
+ const originPaddedRaw = rawOrigin.padEnd(35);
+ const originPaddedColored = packageName
+ ? originPaddedRaw
+ : isNodeModulesCode
+ ? color(originPaddedRaw, 90)
+ : color(originPaddedRaw, 35);
+ const cacheStatus = errorMessage
+ ? color('⚠️ Fallback', 33)
+ : usedSwc
+ ? cacheHit
+ ? color('🟢 Cache hit', 32)
+ : color('🔴 Cache miss', 31)
+ : '';
+ const archPart = arch ? color(` (${arch})`, 90) : '';
+ console.log(
+ `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}${archPart}`,
+ );
+ if (errorMessage) {
+ console.log();
+ console.log(` ↳ ${color('Error:', 31)} ${errorMessage}`);
+ if (tip) {
+ console.log();
+ console.log(` ${color('💡 Tip:', 33)} ${tip}`);
+ }
+ console.log();
+ }
+}
+
+function logConfigBlock(description, configObject) {
+ const label = color('[Config]', 36);
+ const descriptionColor = color(description, 90);
+
+ console.log(`${label} ${descriptionColor}`);
+
+ const configLines = JSON.stringify(configObject, null, 2)
+ .replace(/"([^"]+)":/g, '$1:')
+ .split('\n')
+ .map(line => ' ' + line);
+
+ configLines.forEach(line => console.log(line));
+ console.log();
+}
+
+function deepMerge(target, source, preservePaths = [], inPath = '') {
+ for (const key in source) {
+ const fullPath = inPath ? `${inPath}.${key}` : key;
+
+ // Skip preserved paths
+ if (preservePaths.includes(fullPath)) continue;
+
+ if (
+ typeof source[key] === 'object' &&
+ source[key] !== null &&
+ !Array.isArray(source[key])
+ ) {
+ target[key] = deepMerge(
+ target[key] || {},
+ source[key],
+ preservePaths,
+ fullPath,
+ );
+ } else {
+ target[key] = source[key];
+ }
+ }
+ return target;
+}
+
+function convertBabelTargetsForSwc(babelTargets) {
+ const allowedEnvs = new Set([
+ 'chrome', 'opera', 'edge', 'firefox', 'safari',
+ 'ie', 'ios', 'android', 'node', 'electron'
+ ]);
+
+ const filteredTargets = {};
+ for (const [env, version] of Object.entries(babelTargets)) {
+ if (allowedEnvs.has(env)) {
+ // Convert an array version (e.g., [10, 3]) into "10.3", otherwise convert to string.
+ filteredTargets[env] = Array.isArray(version) ? version.join('.') : version.toString();
+ }
+ }
+
+ return filteredTargets;
+}
+
+/**
+ * A compiler that extends BabelCompiler but always uses SWC
+ * @param {Object} extraFeatures Additional features to pass to BabelCompiler
+ * @param {Function} modifyConfig Function to modify the configuration
+ */
+SwcCompiler = function SwcCompiler(extraFeatures, modifyConfig) {
+ extraFeatures = extraFeatures || {};
+ extraFeatures.swc = true;
+ BabelCompiler.call(this, extraFeatures, modifyConfig);
+};
+
+// Inherit from BabelCompiler
+SwcCompiler.prototype = Object.create(BabelCompiler.prototype);
+SwcCompiler.prototype.constructor = SwcCompiler;
diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js
index eb539fb22c..2e69cdc028 100644
--- a/packages/babel-compiler/package.js
+++ b/packages/babel-compiler/package.js
@@ -1,13 +1,14 @@
Package.describe({
name: "babel-compiler",
summary: "Parser/transpiler for ECMAScript 2015+ syntax",
- version: '7.11.0',
+ version: '7.12.2',
});
Npm.depends({
- '@meteorjs/babel': '7.20.0-beta.4',
- 'json5': '2.1.1',
- 'semver': '7.3.8'
+ '@meteorjs/babel': '7.20.1',
+ 'json5': '2.2.3',
+ 'semver': '7.6.3',
+ "@meteorjs/swc-core": "1.12.14",
});
Package.onUse(function (api) {
@@ -22,4 +23,5 @@ Package.onUse(function (api) {
api.export('Babel', 'server');
api.export('BabelCompiler', 'server');
+ api.export('SwcCompiler', 'server');
});
diff --git a/packages/boilerplate-generator-tests/.npm/package/npm-shrinkwrap.json b/packages/boilerplate-generator-tests/.npm/package/npm-shrinkwrap.json
deleted file mode 100644
index d749aa7131..0000000000
--- a/packages/boilerplate-generator-tests/.npm/package/npm-shrinkwrap.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "lockfileVersion": 4,
- "dependencies": {
- "parse5": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
- "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
- }
- }
-}
diff --git a/packages/boilerplate-generator-tests/package.js b/packages/boilerplate-generator-tests/package.js
index 98a6db887d..8c95f5f034 100644
--- a/packages/boilerplate-generator-tests/package.js
+++ b/packages/boilerplate-generator-tests/package.js
@@ -2,7 +2,7 @@ Package.describe({
// These tests are in a separate package so that we can Npm.depend on
// parse5, a html parsing library.
summary: "Tests for the boilerplate-generator package",
- version: '1.5.2',
+ version: '1.5.3',
documentation: null
});
@@ -13,7 +13,6 @@ Npm.depends({
Package.onTest(function (api) {
api.use('ecmascript');
api.use([
- 'underscore',
'tinytest',
'boilerplate-generator'
], 'server');
diff --git a/packages/boilerplate-generator-tests/web.browser-tests.js b/packages/boilerplate-generator-tests/web.browser-tests.js
index 915e2f6ebf..48c0d06fd8 100644
--- a/packages/boilerplate-generator-tests/web.browser-tests.js
+++ b/packages/boilerplate-generator-tests/web.browser-tests.js
@@ -1,6 +1,5 @@
import { parse, serialize } from 'parse5';
import { generateHTMLForArch } from './test-lib';
-import { _ } from 'meteor/underscore';
Tinytest.addAsync(
"boilerplate-generator-tests - web.browser - basic output",
@@ -66,10 +65,6 @@ Tinytest.addAsync(
async function (test) {
const newHtml = await generateHTMLForArch("web.browser", false);
- _.templateSettings = {
- interpolate: /\{\{(.+?)\}\}/g
- };
-
test.matches(newHtml, /foo="foobar"/);
test.matches(newHtml, / ]*href="[^<>]*bootstrap[^<>]*">/);
test.matches(newHtml, /';
Tinytest.add("webapp - npm modules", function (test) {
// Make sure the version number looks like a version number.
- test.matches(WebAppInternals.NpmModules.express.version, /^4\.(\d+)\.(\d+)/);
+ test.matches(WebAppInternals.NpmModules.express.version, /^5\.(\d+)\.(\d+)/);
test.equal(typeof(WebAppInternals.NpmModules.express.module), 'function');
});
@@ -365,3 +382,53 @@ Tinytest.addAsync(
test.isTrue(/__meteor_runtime_config__ = (.*customKey[^"].*customValue.*)/.test(html));
}
);
+
+Tinytest.addAsync("webapp - parse url queries", async function (test) {
+ WebApp.handlers.get("/queries", async (req, res) => {
+ res.json(req.query);
+ });
+
+ const queriesTestCases = [
+ 'planet=Mars',
+ 'galaxy=Andromeda&star=Betelgeuse',
+ 'spacecraft=Voyager%202',
+ 'meteor=Perseid&meteor=Leonid',
+ 'astronaut[name]=Neil&astronaut[mission]=Apollo%2011',
+ 'galaxy[name]=Milky%20Way&galaxy[diameter]=105700',
+ 'constellation[name]=Orion&constellation[stars][]=Betelgeuse&constellation[stars][]=Rigel',
+ 'galaxy[name]=Andromeda&galaxy[age]=10&meteors[]=Perseid&meteors[]=Geminid',
+ 'astronaut[name]=Buzz&astronaut[missions][first]=Apollo%2011&astronaut[missions][second]=Apollo%2022',
+ 'spacecraft[]=Voyager&spacecraft[]=Pioneer&spacecraft[0][type]=orbiter',
+ 'comet=Halley&status=active%20comet',
+ 'planet=&galaxy='
+ ];
+ const queryResults = [
+ { planet: 'Mars' },
+ { galaxy: 'Andromeda', star: 'Betelgeuse' },
+ { spacecraft: 'Voyager 2' },
+ { meteor: ['Perseid', 'Leonid'] },
+ { astronaut: { name: 'Neil', mission: 'Apollo 11' } },
+ { galaxy: { name: 'Milky Way', diameter: '105700' } },
+ { constellation: { name: 'Orion', stars: ['Betelgeuse', 'Rigel'] } },
+ {
+ galaxy: { name: 'Andromeda', age: '10' },
+ meteors: ['Perseid', 'Geminid']
+ },
+ {
+ astronaut: {
+ name: 'Buzz',
+ missions: { first: 'Apollo 11', second: 'Apollo 22' }
+ }
+ },
+ { spacecraft: ['Voyager', 'Pioneer', { type: 'orbiter' }] },
+ { comet: 'Halley', status: 'active comet' },
+ { planet: '', galaxy: '' }
+ ];
+ let i = 0;
+ for await (const queriesTestCase of queriesTestCases) {
+ const resp = await asyncGet(`${Meteor.absoluteUrl()}/queries?${queriesTestCase}`);
+ const queryParsed = JSON.parse(resp.content);
+ test.equal(queryParsed, queryResults[i]);
+ i++;
+ }
+});
diff --git a/scripts/admin/bump-all-version-numbers.js b/scripts/admin/bump-all-version-numbers.js
index 915c999296..a4e45f0d12 100644
--- a/scripts/admin/bump-all-version-numbers.js
+++ b/scripts/admin/bump-all-version-numbers.js
@@ -1,21 +1,20 @@
// run as node scripts/admin/bump-all-version-numbers.js
-var fs = require("fs");
-var _ = require("../../packages/underscore/underscore.js")._;
+const fs = require("fs");
-var packageNames = _.rest(process.argv, 2);
+const packageNames = process.argv.slice(2);
-_.each(packageNames, function (name) {
+packageNames.forEach(name => {
// name = "packages/" + name + "/package.js";
- var content = fs.readFileSync(name, {encoding: "utf-8"});
+ const content = fs.readFileSync(name, {encoding: "utf-8"});
- var match = content.match(/\d+\.\d+\.\d+-winr.\d+/);
+ const match = content.match(/\d+\.\d+\.\d+-winr.\d+/);
if (match) {
- var versionNumber = match[0];
- var s = versionNumber.split(".");
- s[3] = (parseInt(s[3], 10) + 1);
- var incremented = s.join(".");
+ const versionNumber = match[0];
+ const s = versionNumber.split(".");
+ s[3] = (parseInt(s[3], 10) + 1).toString();
+ const incremented = s.join(".");
content = content.replace(versionNumber, incremented);
console.log(match[0], match[1], incremented);
diff --git a/scripts/admin/check-legacy-syntax/check-syntax.js b/scripts/admin/check-legacy-syntax/check-syntax.js
index 09fd3acd3a..58b1ce1284 100644
--- a/scripts/admin/check-legacy-syntax/check-syntax.js
+++ b/scripts/admin/check-legacy-syntax/check-syntax.js
@@ -34,6 +34,7 @@ const packages = {
// Ignored server files that has a features > 2016
ignoredFiles: [
"async_helpers.js",
+ "asynchronous_queue.js",
]
},
"accounts-ui": {},
@@ -41,6 +42,7 @@ const packages = {
autopublish: {},
"babel-compiler": {
serverFiles: ["babel.js", "babel-compiler.js"],
+ ignoredFiles: ["babel-compiler.js"],
},
"babel-runtime": {},
"browser-policy": {},
@@ -83,7 +85,7 @@ const packages = {
"logic-solver": {
// TODO: Fibers - Legacy
// Revisit when we remove fibers, this may break for legacy
- ignoredFiles: ["logic.js", "optimize.js"],
+ ignoredFiles: ["logic.js", "optimize.js", "minisat.js"],
},
"meteor-base": {},
"mobile-experience": {},
@@ -118,7 +120,6 @@ const packages = {
typescript: {
serverFiles: ["plugin.js"],
},
- underscore: {},
url: {},
};
diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json
index 6ae89cb992..2432e8857d 100644
--- a/scripts/admin/meteor-release-experimental.json
+++ b/scripts/admin/meteor-release-experimental.json
@@ -1,6 +1,6 @@
{
"track": "METEOR",
- "version": "3.0.3-rc.0",
+ "version": "3.3.1-rc.2",
"recommended": false,
"official": false,
"description": "Meteor experimental release"
diff --git a/scripts/admin/meteor-release-official.json b/scripts/admin/meteor-release-official.json
index d65d9e176b..7b89d5c0a6 100644
--- a/scripts/admin/meteor-release-official.json
+++ b/scripts/admin/meteor-release-official.json
@@ -1,7 +1,7 @@
-{
- "track": "METEOR",
- "version": "3.0.3",
- "recommended": false,
- "official": true,
- "description": "The Official Meteor Distribution"
-}
+{
+ "track": "METEOR",
+ "version": "3.3.2",
+ "recommended": false,
+ "official": true,
+ "description": "The Official Meteor Distribution"
+}
diff --git a/scripts/admin/update-semver/package-lock.json b/scripts/admin/update-semver/package-lock.json
index 80901d6328..10b77a0c9f 100644
--- a/scripts/admin/update-semver/package-lock.json
+++ b/scripts/admin/update-semver/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
- "semver": "^7.3.8"
+ "semver": "^7.5.2"
}
},
"node_modules/lru-cache": {
@@ -24,9 +24,10 @@
}
},
"node_modules/semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
+ "license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -53,9 +54,9 @@
}
},
"semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"requires": {
"lru-cache": "^6.0.0"
}
diff --git a/scripts/admin/update-semver/package.json b/scripts/admin/update-semver/package.json
index 3ba0bf697b..4f875a48cf 100644
--- a/scripts/admin/update-semver/package.json
+++ b/scripts/admin/update-semver/package.json
@@ -11,6 +11,6 @@
"author": "grubba27",
"license": "MIT",
"dependencies": {
- "semver": "^7.3.8"
+ "semver": "^7.5.2"
}
}
diff --git a/scripts/build-dev-bundle-common.sh b/scripts/build-dev-bundle-common.sh
index ef59103451..6f06d676a6 100644
--- a/scripts/build-dev-bundle-common.sh
+++ b/scripts/build-dev-bundle-common.sh
@@ -5,10 +5,10 @@ set -u
UNAME=$(uname)
ARCH=$(uname -m)
-NODE_VERSION=20.17.0
-MONGO_VERSION_64BIT=7.0.5
+NODE_VERSION=22.18.0
+MONGO_VERSION_64BIT=7.0.16
MONGO_VERSION_32BIT=3.2.22
-NPM_VERSION=10.8.2
+NPM_VERSION=10.9.3
if [ "$UNAME" == "Linux" ] ; then
diff --git a/scripts/dev-bundle-server-package.js b/scripts/dev-bundle-server-package.js
index 929cda5243..ef92f51625 100644
--- a/scripts/dev-bundle-server-package.js
+++ b/scripts/dev-bundle-server-package.js
@@ -11,7 +11,7 @@ var packageJson = {
// Keep the versions of these packages consistent with the versions
// found in dev-bundle-tool-package.js.
promise: "8.3.0",
- "@meteorjs/reify": "0.25.2",
+ "@meteorjs/reify": "0.25.4",
"@babel/parser": "7.25.0",
"lru-cache": "6.0.0",
underscore: "1.13.7",
diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js
index ea944379b7..b8043936a5 100644
--- a/scripts/dev-bundle-tool-package.js
+++ b/scripts/dev-bundle-tool-package.js
@@ -10,14 +10,14 @@ var packageJson = {
dependencies: {
// Explicit dependency because we are replacing it with a bundled version
// and we want to make sure there are no dependencies on a higher version
- npm: "10.8.2",
- "node-gyp": "9.4.0",
+ npm: "10.9.3",
+ "node-gyp": "10.2.0",
"@mapbox/node-pre-gyp": "1.0.11",
- typescript: "5.4.5",
- "@meteorjs/babel": "7.20.0-beta.5",
+ typescript: "5.6.3",
+ "@meteorjs/babel": "7.20.0",
// Keep the versions of these packages consistent with the versions
// found in dev-bundle-server-package.js.
- "@meteorjs/reify": "0.25.2",
+ "@meteorjs/reify": "0.25.4",
// So that Babel can emit require("@babel/runtime/helpers/...") calls.
"@babel/runtime": "7.25.0",
// For backwards compatibility with isopackets that still depend on
@@ -59,6 +59,7 @@ var packageJson = {
multipipe: "2.0.1",
pathwatcher: "8.1.2",
"vscode-nsfw": "2.1.8",
+ "@parcel/watcher": "2.5.1",
// The @wry/context package version must be compatible with the
// version constraint imposed by optimism/package.json.
optimism: "0.16.1",
@@ -67,7 +68,8 @@ var packageJson = {
"anser": "2.1.1",
'xmlbuilder2': '1.8.1',
"ws": "7.4.5",
- "open":"8.4.2"
+ "open":"8.4.2",
+ "acorn": "8.14.1",
}
};
diff --git a/scripts/generate-dev-bundle.ps1 b/scripts/generate-dev-bundle.ps1
index 2223d04b06..f96509b205 100644
--- a/scripts/generate-dev-bundle.ps1
+++ b/scripts/generate-dev-bundle.ps1
@@ -185,9 +185,8 @@ Function Add-NodeAndNpm {
# Let's install the npm version we really want.
Write-Host "Installing npm@${NPM_VERSION}..." -ForegroundColor Magenta
- & "$tempNpmCmd" install --prefix="$dirLib" --no-bin-links --save `
- --cache="$dirNpmCache" --nodedir="$dirTempNode" npm@${NPM_VERSION} |
- Write-Debug
+ Write-Host (& "$tempNpmCmd" install --prefix="$dirLib" --no-bin-links --save `
+ --cache="$dirNpmCache" --nodedir="$dirTempNode" npm@${NPM_VERSION} 2>&1)
if ($LASTEXITCODE -ne 0) {
throw "Couldn't install npm@${NPM_VERSION}."
@@ -378,10 +377,6 @@ $npmToolArgs = @{
}
Add-NpmModulesFromJsBundleFile @npmToolArgs
-# Leaving these probably doesn't hurt, but are removed for consistency w/ Unix.
-Remove-Item $(Join-Path $dirLib 'package.json')
-Remove-Item $(Join-Path $dirLib 'package-lock.json')
-
Write-Host "Done writing node_modules build(s)..." -ForegroundColor Magenta
Write-Host "Removing temp scratch $dirTemp" -ForegroundColor Magenta
diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh
index 8e0de641e8..792539fe66 100755
--- a/scripts/generate-dev-bundle.sh
+++ b/scripts/generate-dev-bundle.sh
@@ -70,9 +70,8 @@ case $OS in
;;
esac
-
if [ $OS = "macos" ] && [ "$(uname -m)" = "arm64" ] ; then
- MONGO_NAME="mongodb-${OS}-x86_64-${MONGO_VERSION}"
+ MONGO_NAME="mongodb-${OS}-arm64-${MONGO_VERSION}"
elif [ $OS = "linux" ] && [ "$ARCH" = "aarch64" ] ; then
MONGO_NAME="mongodb-linux-aarch64-ubuntu2204-${MONGO_VERSION}"
else
@@ -84,6 +83,13 @@ MONGO_URL="${MONGO_BASE_URL}/${MONGO_TGZ}"
echo "Downloading Mongo from ${MONGO_URL}"
curl -L "${MONGO_URL}" | tar zx
+# The tarball outputs as folder name "mongodb-macos-aarch64-X.X.X" even though the URL and the tarball name suggest "mongodb-macos-arm64-X.X.X"
+# So we need to rename the folder to match the expected folder name
+# Watch out for newer versions of the tarball that might already be named correctly
+if [ $OS = "macos" ] && [ "$(uname -m)" = "arm64" ] ; then
+ MONGO_NAME=$(echo "$MONGO_NAME" | sed 's/arm64/aarch64/g')
+fi
+
# Put Mongo binaries in the right spot (mongodb/bin)
mkdir -p "mongodb/bin"
mv "${MONGO_NAME}/bin/mongod" "mongodb/bin"
diff --git a/scripts/windows/ci/install.ps1 b/scripts/windows/ci/install.ps1
index ecb7c9194c..3624e4c11d 100644
--- a/scripts/windows/ci/install.ps1
+++ b/scripts/windows/ci/install.ps1
@@ -19,6 +19,12 @@ If ($LASTEXITCODE -ne 0) {
throw "Updating submodules failed."
}
+# The `meteor npm install` subcommand should work
+& "$meteorBat" npm install
+If ($LASTEXITCODE -ne 0) {
+ throw "'meteor npm install' failed."
+}
+
# The `meteor --get-ready` command is susceptible to EPERM errors, so
# we attempt it three times.
$attempt = 3
diff --git a/tools/PERFORMANCE.md b/tools/PERFORMANCE.md
index 7a8864bd84..635f84f78e 100644
--- a/tools/PERFORMANCE.md
+++ b/tools/PERFORMANCE.md
@@ -167,6 +167,94 @@ The Profiler is activated using `Profile.run(.)`, which will cause a report to b
In addition to tool code, you can also use `Profile` in compiler plugins. If you want to use `Profile` in packages that are loaded into the tool (e.g. packages depended on by compiler plugins, or specially loaded into the tool as isopackets), you should always test `(typeof Profile !== 'undefined')` before accessing `Profile`, or pass it in from the tool as an option.
+## Inspector Profiling
+
+In addition to the standard profiler, Meteor includes a more advanced profiling system based on Node.js's `inspector` module. This system generates `.cpuprofile` files that provide much more detailed performance data.
+
+### How to Use Inspector Profiling
+
+To enable inspector profiling, you need to specify which functions you want to profile through the `METEOR_INSPECT` environment variable:
+
+```bash
+# Profile multiple functions
+METEOR_INSPECT=bundler.bundle,compiler.compile meteor build ./output-build
+```
+
+Complete list for METEOR_INSPECT:
+- bundler.bundle
+- compiler.compile
+- Babel.compile
+- _readProjectMetadata
+- initializeCatalog
+- _downloadMissingPackages
+- _saveChangeMetadata
+- _realpath
+- package-client
+
+
+### Additional Configuration Options
+
+You can customize the inspector profiling behavior with the following environment variables:
+
+```bash
+# Identifier for profile files
+METEOR_INSPECT_CONTEXT=context_name
+
+# Directory where .cpuprofile files will be saved (default: ./profiling)
+METEOR_INSPECT_OUTPUT=/path/to/directory
+
+# Sampling interval in ms - lower values = more details but uses more memory
+METEOR_INSPECT_INTERVAL=500
+
+# Maximum profile size in MB (default: 2000)
+METEOR_INSPECT_MAX_SIZE=1000
+```
+
+### Viewing the Results
+
+#### Chrome DevTools
+The generated `.cpuprofile` files can be visualized in Chrome DevTools:
+
+1. Open Chrome DevTools
+2. Go to the "Performance" or "Profiler" tab
+3. Click "Load Profile" and select the generated .cpuprofile file
+
+#### Discoveryjs cpupro
+
+For a opensource interactive analysis of your `.cpuprofile` files, you can use [cpupro](https://discoveryjs.github.io/cpupro/), an open-source CPU profile viewer:
+
+1. Visit https://discoveryjs.github.io/cpupro/ in your browser
+2. Drag and drop your `.cpuprofile` file onto the interface
+3. Use the interactive visualization to explore your profile data
+
+cpupro offers several advantages over Chrome DevTools:
+- Better handling of large profiles
+- More flexible filtering options
+- Advanced search capabilities
+- Multiple visualization modes
+- Ability to compare different profiles
+
+You can also run cpupro locally by installing it via npm:
+
+
+### Important Considerations
+
+- Inspector profiling consumes more memory than the standard profiler
+- To avoid out-of-memory (OOM) errors, consider increasing Node's memory limit:
+ ```bash
+ NODE_OPTIONS="--max-old-space-size=4096" METEOR_INSPECT=bundler.bundle meteor ...
+ ```
+- Very large profiles (>2GB) will be automatically truncated to avoid OOM errors
+
+### When to Use Inspector Profiling
+
+Use inspector profiling when:
+- You need more detailed analysis than the standard profiler provides
+- You're investigating complex performance issues
+- You want to identify specific bottlenecks in heavy functions like bundler or compiler
+
+The standard profiler (`METEOR_PROFILE`) remains the best option for quick and general analyses, while inspector profiling is more suitable for detailed diagnostics.
+
## Known Areas for Improvement
These are areas to improve or investigate, along with what's known, in no
diff --git a/tools/README.md b/tools/README.md
index c8c68fdb8c..d41096b175 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -105,6 +105,34 @@ Internally, every profiled function should be wrapped into a `Profile(fn)` call.
The entry point should be started explicitly with the `Profile.run()`
call. Otherwise, it won't start measuring anything.
+### Inspector Profiling
+
+In addition to the standard profiler, you can use the more advanced inspector profiling:
+
+```bash
+# Profile a specific function (e.g. bundler.bundle)
+METEOR_INSPECT=bundler.bundle meteor run
+
+# Profile multiple functions
+METEOR_INSPECT=bundler.bundle,compiler.compile meteor build
+```
+
+Additional options:
+```bash
+# Output directory for .cpuprofile files
+METEOR_INSPECT_OUTPUT=/path/to/directory
+
+# Name to identify profile files
+METEOR_INSPECT_CONTEXT=project_xyz
+
+# Sampling interval (lower = more details, more memory)
+METEOR_INSPECT_INTERVAL=500
+```
+
+The generated `.cpuprofile` files can be opened in Chrome DevTools through the "Performance" or "Profiler" tab.
+
+For more details, see the `PERFORMANCE.md` file.
+
## Debugging
Currently, to debug the tool with `node-inspector`, you can set the `
@@ -164,4 +192,4 @@ a custom try/catch/finally system with recovery. See
## More information
For more information about a particular part of Meteor Tool, see subdirectories'
-README.md files and the top-level intro comments in the bigger files.
+README.md files and the top-level intro comments in the bigger files.
\ No newline at end of file
diff --git a/tools/cli/commands-packages.js b/tools/cli/commands-packages.js
index e580246c48..408654581b 100644
--- a/tools/cli/commands-packages.js
+++ b/tools/cli/commands-packages.js
@@ -2851,7 +2851,7 @@ main.registerCommand({
}
try {
- var conn = packageClient.loggedInPackagesConnection();
+ var conn = await packageClient.loggedInPackagesConnection();
} catch (err) {
packageClient.handlePackageServerConnectionError(err);
return 1;
@@ -2961,16 +2961,16 @@ main.registerCommand({
try {
var status = options.success ? "successfully" : "unsuccessfully";
// XXX: This should probably use progress bars instead.
- _.each(versions, function (version) {
+ for (const version of versions) {
Console.rawInfo(
"Setting " + name + "@" + version + " as " +
status + " migrated ...\n");
- packageClient.callPackageServer(
+ await packageClient.callPackageServer(
conn,
'_changeVersionMigrationStatus',
name, version, !options.success);
Console.info("done.");
- });
+ }
} catch (err) {
packageClient.handlePackageServerConnectionError(err);
return 1;
diff --git a/tools/cli/commands.js b/tools/cli/commands.js
index cc79168e02..bb4bbea6cc 100644
--- a/tools/cli/commands.js
+++ b/tools/cli/commands.js
@@ -1,3 +1,5 @@
+import { getMeteorConfig } from "../tool-env/meteor-config";
+
var main = require('./main.js');
var _ = require('underscore');
var files = require('../fs/files');
@@ -20,6 +22,7 @@ const {
yellow
} = require('../console/console.js').colors;
const inquirer = require('inquirer');
+const semver = require("semver");
var projectContextModule = require('../project-context.js');
var release = require('../packaging/release.js');
@@ -27,7 +30,7 @@ var release = require('../packaging/release.js');
const { Profile } = require("../tool-env/profile");
const open = require('open')
-const { exec } = require("child_process");
+const { exec, spawn } = require("child_process");
/**
* Run a command in the shell.
* @param command
@@ -64,6 +67,59 @@ const bash =
(text, ...values) =>
tryRun(() => runCommand(String.raw({ raw: text }, ...values)));
+/**
+ * Run a command in the shell and stream output in real-time.
+ * @param {string} command The command to execute.
+ * @param {string[]} args Arguments for the command.
+ * @return {Promise} Resolves with the exit code.
+ */
+const runLiveCommand = (command, args = []) => {
+ return new Promise((resolve, reject) => {
+ const childProcess = spawn(command, args, {
+ shell: true,
+ env: { ...process.env, FORCE_COLOR: "1", TERM: "xterm-256color" },
+ stdio: "inherit",
+ });
+
+ const cleanup = () => {
+ childProcess.removeAllListeners();
+ };
+
+ childProcess.on("close", (code) => {
+ cleanup();
+ resolve(code);
+ });
+
+ childProcess.on("error", (error) => {
+ Console.error(error.message);
+ cleanup();
+ reject(error);
+ });
+ });
+};
+
+/**
+ * Executes an async function and captures success or error.
+ * @param {() => Promise} fn The async function to execute.
+ * @returns {Promise<[T, null] | [null, Error]>} Result or Error tuple.
+ */
+const tryRunLive = async (fn) => {
+ try {
+ return [await fn(), null];
+ } catch (e) {
+ return [null, e];
+ }
+};
+
+/**
+ * Runs a Bash command with live logging.
+ * @param {string} text The bash command to execute.
+ * @param {...string} values Additional arguments.
+ * @returns {Promise<[number, null] | [null, Error]>} Exit code or Error.
+ */
+const bashLive = (text, ...values) =>
+ tryRunLive(() => runLiveCommand(String.raw({ raw: text }, ...values)));
+
import { ensureDevBundleDependencies } from '../cordova/index.js';
import { CordovaRunner } from '../cordova/runner.js';
import { iOSRunTarget, AndroidRunTarget } from '../cordova/run-targets.js';
@@ -207,13 +263,49 @@ export function parseRunTargets(targets) {
});
};
-const excludableWebArchs = ['web.browser', 'web.browser.legacy', 'web.cordova'];
-function filterWebArchs(webArchs, excludeArchsOption) {
- if (excludeArchsOption) {
- const excludeArchs = excludeArchsOption.trim().split(/\s*,\s*/)
- .filter(arch => excludableWebArchs.includes(arch));
- webArchs = webArchs.filter(arch => !excludeArchs.includes(arch));
+function filterWebArchs(webArchs, excludeArchsOption, appDir, options) {
+ const platforms = (options.platforms || []);
+ const isBuildMode = platforms?.length > 0;
+ if (isBuildMode) {
+ // Build Mode
+ const isModernOnlyPlatform = platforms.includes('modern') && !platforms.includes('legacy');
+ if (isModernOnlyPlatform) {
+ webArchs = webArchs.filter(arch => arch !== 'web.browser.legacy');
+ }
+ const hasCordovaPlatforms = platforms.includes('android') || platforms.includes('ios');
+ if (!hasCordovaPlatforms) {
+ webArchs = webArchs.filter(arch => arch !== 'web.cordova');
+ }
+ } else {
+ // Dev & Test Mode
+ const isCordovaDev = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg));
+ if (!isCordovaDev) {
+ const excludeArchsOptions = excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : [];
+ const hasExcludeArchsOptions = (excludeArchsOptions?.length || 0) > 0;
+ const hasModernArchsOnlyEnabled = appDir && getMeteorConfig()?.modern?.webArchOnly !== false;
+ if (hasExcludeArchsOptions && hasModernArchsOnlyEnabled) {
+ console.warn('modern.webArchOnly and --exclude-archs are both active. If both are set, --exclude-archs takes priority.');
+ }
+ const automaticallyIgnoredLegacyArchs = (!hasExcludeArchsOptions && hasModernArchsOnlyEnabled) ? ['web.browser.legacy', 'web.cordova'] : [];
+ if (hasExcludeArchsOptions || automaticallyIgnoredLegacyArchs.length) {
+ const excludeArchs = [...excludeArchsOptions, ...automaticallyIgnoredLegacyArchs];
+ webArchs = webArchs.filter(arch => !excludeArchs.includes(arch));
+ }
+ }
}
+
+ const forcedInclArchs = process.env.METEOR_FORCE_INCLUDE_ARCHS;
+ if (forcedInclArchs != null) {
+ const nextInclArchs = forcedInclArchs.trim().split(/\s*,\s*/);
+ webArchs = Array.from(new Set([...webArchs, ...nextInclArchs]));
+ }
+
+ const forcedExclArchs = process.env.METEOR_FORCE_EXCLUDE_ARCHS;
+ if (forcedExclArchs != null) {
+ const nextExclArchs = forcedExclArchs.trim().split(/\s*,\s*/);
+ webArchs = webArchs.filter(_webArch => !nextExclArchs.includes(_webArch));
+ }
+
return webArchs;
}
@@ -359,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
@@ -443,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) ||
@@ -455,11 +545,16 @@ async function doRunCommand(options) {
webArchs.push("web.cordova");
}
}
- webArchs = filterWebArchs(webArchs, options['exclude-archs']);
+
+ webArchs = filterWebArchs(webArchs, options['exclude-archs'], options.appDir, options);
+ // Set the webArchs to include for compilation later
+ global.includedWebArchs = webArchs;
+
const buildMode = options.production ? 'production' : 'development';
let cordovaRunner;
- if (!_.isEmpty(runTargets)) {
+ const shouldDisableCordova = Boolean(JSON.parse(process.env.METEOR_CORDOVA_DISABLE || 'false'));
+ if (!shouldDisableCordova && !_.isEmpty(runTargets)) {
async function prepareCordovaProject() {
import { CordovaProject } from '../cordova/project.js';
@@ -516,7 +611,7 @@ async function doRunCommand(options) {
open(`http://localhost:${options.port}`)
}
}
- },
+ },
open: options.open,
});
}
@@ -1030,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 })
);
@@ -1039,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 })
);
@@ -1058,7 +1153,7 @@ main.registerCommand({
process.env.GIT_TERMINAL_PROMPT = 0;
const gitCommand = isWindows
- ? `git clone --progress ${url} ${files.convertToOSPath(appPath)}`
+ ? `git clone --progress ${url} "${files.convertToOSPath(appPath)}"`
: `git clone --progress ${url} ${appPath}`;
const [okClone, errClone] = await bash`${gitCommand}`;
const errorMessage = errClone && typeof errClone === "string" ? errClone : errClone?.message;
@@ -1106,71 +1201,85 @@ main.registerCommand({
toIgnore.push(/(\.html|\.js|\.css)/);
}
- try {
- // Prototype option should use local skeleton.
- // Maybe we should use a different skeleton for prototype
- if (options.prototype) throw new Error("Using prototype option");
- // if using the release option we should use the default skeleton
- // using it as it was before 2.x
- if (release.explicit) throw new Error("Using release option");
+ const copyFromLocalSkeleton = async () => {
+ await files.cp_r(
+ skeletonPath,
+ appPath,
+ {
+ transformFilename: function (f) {
+ return transform(f);
+ },
+ transformContents: function (contents, f) {
+ // check if this app is just for prototyping if it is then we need to add autopublish and insecure in the packages file
+ if (/packages/.test(f)) {
+ const prototypePackages = () =>
+ "autopublish # Publish all data to the clients (for prototyping)\n" +
+ "insecure # Allow all DB writes from clients (for prototyping)";
- await setupExampleByURL(`https://github.com/meteor/skel-${skeleton}`);
- } catch (e) {
+ // XXX: if there is the need to add more options maybe we should have a better abstraction for this if-else
+ if (options.prototype) {
+ return Buffer.from(
+ contents.toString().replace(/~prototype~/g, prototypePackages())
+ );
+ } else {
+ return Buffer.from(contents.toString().replace(/~prototype~/g, ""));
+ }
+ }
+ if (/(\.html|\.[jt]sx?|\.css)/.test(f)) {
+ return Buffer.from(transform(contents.toString()));
+ } else {
+ return contents;
+ }
+ },
+ ignore: toIgnore,
+ preserveSymlinks: true,
+ }
+ );
+ };
- if (
- e.message !== "Using prototype option" &&
- e.message !== "Using release option"
- ) {
- // something has happened while creating the app using git clone
- Console.error(
- `Something has happened while creating your app using git clone.
+ // Check if the local skeleton path exists
+ const skeletonPath = files.pathJoin(
+ __dirnameConverted,
+ "..",
+ "static-assets",
+ `skel-${skeleton}`
+ );
+
+ const useLocalSkeleton = files.exists(skeletonPath) ||
+ options.prototype ||
+ release.explicit;
+ if (useLocalSkeleton) {
+ // Use local skeleton
+ await copyFromLocalSkeleton();
+ } else {
+ try {
+ // Prototype option should use local skeleton.
+ // Maybe we should use a different skeleton for prototype
+ if (options.prototype) throw new Error("Using prototype option");
+ // if using the release option we should use the default skeleton
+ // using it as it was before 2.x
+ if (release.explicit) throw new Error("Using release option");
+
+ // If local skeleton doesn't exist, use setupExampleByURL
+ await setupExampleByURL(`https://github.com/meteor/skel-${skeleton}`);
+ } catch (e) {
+ if (
+ e.message !== "Using prototype option" &&
+ e.message !== "Using release option"
+ ) {
+ // something has happened while creating the app using git clone
+ Console.error(
+ `Something has happened while creating your app using git clone.
Will use cached version of skeletons.
Error message: `,
- e.message
- );
+ e.message
+ );
+ }
+ // For prototype or release options, use local skeleton
+ await copyFromLocalSkeleton();
}
-
- // TODO: decide if this should stay here or not.
- await files.cp_r(
- files.pathJoin(
- __dirnameConverted,
- "..",
- "static-assets",
- `skel-${skeleton}`
- ),
- appPath,
- {
- transformFilename: function (f) {
- return transform(f);
- },
- transformContents: function (contents, f) {
- // check if this app is just for prototyping if it is then we need to add autopublish and insecure in the packages file
- if (/packages/.test(f)) {
- const prototypePackages = () =>
- "autopublish # Publish all data to the clients (for prototyping)\n" +
- "insecure # Allow all DB writes from clients (for prototyping)";
-
- // XXX: if there is the need to add more options maybe we should have a better abstraction for this if-else
- if (options.prototype) {
- return Buffer.from(
- contents.toString().replace(/~prototype~/g, prototypePackages())
- );
- } else {
- return Buffer.from(contents.toString().replace(/~prototype~/g, ""));
- }
- }
- if (/(\.html|\.[jt]sx?|\.css)/.test(f)) {
- return Buffer.from(transform(contents.toString()));
- } else {
- return contents;
- }
- },
- ignore: toIgnore,
- preserveSymlinks: true,
- }
- );
- await setupMessages();
}
+ await setupMessages();
Console.info("");
});
@@ -1290,7 +1399,7 @@ var buildCommand = async function (options) {
let selectedPlatforms = null;
if (options.platforms) {
const platformsArray = options.platforms.split(",");
-
+ const excludableWebArchs = ['web.browser', 'web.browser.legacy', 'web.cordova'];
platformsArray.forEach(plat => {
if (![...excludableWebArchs, 'android', 'ios'].includes(plat)) {
throw new Error(`Not allowed platform on '--platforms' flag: ${plat}`)
@@ -1337,9 +1446,9 @@ on an OS X system.");
// For example, if we want to build only android, there is no need to build
// web.browser.
let webArchs;
+ const baseWebArchs = projectContext.platformList.getWebArchs();
if (selectedPlatforms) {
- const filteredArchs = projectContext.platformList
- .getWebArchs()
+ const filteredArchs = baseWebArchs
.filter(arch => selectedPlatforms.includes(arch));
if (
@@ -1350,6 +1459,11 @@ on an OS X system.");
}
webArchs = filteredArchs.length ? filteredArchs : undefined;
+ } else {
+ webArchs = filterWebArchs(baseWebArchs, options['exclude-archs'], options.appDir, {
+ ...options,
+ platforms: projectContext.platformList.getPlatforms(),
+ });
}
var buildDir = projectContext.getProjectLocalDirectory('build_tar');
@@ -1374,7 +1488,7 @@ ${Console.command("meteor build ../output")}`,
files.pathJoin(outputPath, 'bundle')) :
files.pathJoin(buildDir, 'bundle');
- await stats.recordPackages({
+ stats.recordPackages({
what: "sdk.bundle",
projectContext: projectContext
});
@@ -1507,7 +1621,7 @@ https://guide.meteor.com/cordova.html#submitting-android
});
}
- await files.rm_recursive(buildDir);
+ await files.rm_recursive_deferred(buildDir);
const npmShrinkwrapFilePath = files.pathJoin(bundlePath, 'programs/server/npm-shrinkwrap.json');
if (files.exists(npmShrinkwrapFilePath)) {
@@ -1713,6 +1827,7 @@ main.registerCommand({
maxArgs: 1,
options: {
db: { type: Boolean },
+ 'skip-cache': { type: Boolean },
},
requiresApp: true,
catalogRefresh: new catalog.Refresh.Never()
@@ -1739,6 +1854,10 @@ main.registerCommand({
"MONGO_URL will NOT be reset.");
}
+ const resetMeteorNmCachePromise = options['skip-cache'] ? Promise.resolve() : files.rm_recursive_async(
+ files.pathJoin(options.appDir, "node_modules", ".cache", "meteor")
+ );
+
if (options.db) {
// XXX detect the case where Meteor is running the app, but
// MONGO_URL was set, so we don't see a Mongo process
@@ -1753,9 +1872,13 @@ main.registerCommand({
return 1;
}
- await files.rm_recursive_async(
- files.pathJoin(options.appDir, '.meteor', 'local')
- );
+ await Promise.all([
+ files.rm_recursive_async(
+ files.pathJoin(options.appDir, ".meteor", "local")
+ ),
+ resetMeteorNmCachePromise,
+ ]);
+
Console.info("Project reset.");
return;
}
@@ -1767,9 +1890,12 @@ main.registerCommand({
return !path.includes('.meteor/local/db');
});
- var allRemovePromises = allExceptDb.map(_path => files.rm_recursive_async(
- files.pathJoin(options.appDir, _path)
- ));
+ var allRemovePromises = [
+ ...allExceptDb.map((_path) =>
+ files.rm_recursive_async(files.pathJoin(options.appDir, _path))
+ ),
+ resetMeteorNmCachePromise
+ ];
await Promise.all(allRemovePromises);
Console.info("Project reset.");
});
@@ -1997,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 },
@@ -2047,7 +2174,10 @@ testCommandOptions = {
'extra-packages': { type: String },
- 'exclude-archs': { type: String }
+ 'exclude-archs': { type: String },
+
+ // Same as TINYTEST_FILTER
+ filter: { type: String, short: 'f' },
}
};
@@ -2068,6 +2198,9 @@ main.registerCommand(Object.assign(
});
async function doTestCommand(options) {
+ if (options.filter) {
+ process.env.TINYTEST_FILTER = options.filter;
+ }
// This "metadata" is accessed in a few places. Using a global
// variable here was more expedient than navigating the many layers
// of abstraction across the build process.
@@ -2126,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']) {
@@ -2399,7 +2531,9 @@ var runTestAppForPackages = async function (projectContext, options) {
if (options.cordovaRunner) {
webArchs.push("web.cordova");
}
- buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs']);
+ buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs'], projectContext.appDirectory, options);
+ // Set the webArchs to include for compilation later
+ global.includedWebArchs = buildOptions.webArchs;
if (options.deploy) {
// Run the constraint solver and build local packages.
@@ -3266,3 +3400,129 @@ main.registerCommand({
}, function () {
throw new Error("testing stack traces!"); // #StackTraceTest this line is found in tests/source-maps.js
});
+
+const setupBenchmarkSuite = async (profilingPath) => {
+ if (await files.exists(profilingPath)) {
+ return;
+ }
+
+ // Check git availability and version
+ const [okGitVersion, errGitVersion] = await bash`git --version`;
+ if (errGitVersion) throw new Error("git is not installed");
+
+ const parsedGitVersion = semver.coerce(okGitVersion.match(/\d+\.\d+\.\d+/)?.[0] || '')?.version;
+ if (!parsedGitVersion || semver.lt(parsedGitVersion, '2.25.0')) {
+ throw new Error("git version is too old. Please upgrade to at least 2.25");
+ }
+
+ // Check tar availability
+ const [okTar, errTar] = await bash`tar --version`;
+ const hasTar = !errTar;
+
+ // Disable interactive git prompts
+ process.env.GIT_TERMINAL_PROMPT = 0;
+
+ const repoUrl = "https://github.com/meteor/performance";
+ const branch = "v3.3.0";
+
+ let tarFailed = false;
+
+ // If tar is available, prefer tar-based extraction
+ if (hasTar) {
+ const tempDir = "/tmp/meteor-performance-benchmark-suite";
+ const tarCommand = [
+ `rm -rf ${tempDir}`,
+ `git clone --no-checkout --depth 1 --filter=tree:0 --sparse --progress --branch ${branch} --single-branch ${repoUrl} ${tempDir}`,
+ `cd ${tempDir}`,
+ `git sparse-checkout init --cone`,
+ `git sparse-checkout set scripts`,
+ `git checkout ${branch}`,
+ `mkdir -p ${profilingPath}/scripts`,
+ `tar -czf /tmp/scripts.tar.gz -C ./scripts .`,
+ `tar -xzf /tmp/scripts.tar.gz -C ${profilingPath}/scripts`,
+ `rm -rf ${tempDir}`,
+ `rm -f /tmp/scripts.tar.gz`
+ ].join(" && ");
+
+ const [okTarClone, errTarClone] = await bash`${tarCommand}`;
+ if (!errTarClone) {
+ Console.info("Meteor profiling suite cloned to: " + Console.path(profilingPath));
+ return;
+ } else {
+ Console.warn("Tar-based cloning failed. Will attempt standard git clone...");
+ tarFailed = errTarClone;
+ }
+ } else {
+ Console.warn("Tar not available. Will use standard git clone...");
+ }
+
+ // Fallback to plain git clone
+ const gitCommand = [
+ `mkdir -p ${profilingPath}`,
+ `git clone --no-checkout --depth 1 --filter=tree:0 --sparse --progress --branch ${branch} --single-branch ${repoUrl} ${profilingPath}`,
+ `cd ${profilingPath}`,
+ `git sparse-checkout init --cone`,
+ `git sparse-checkout set scripts`,
+ `git checkout ${branch}`,
+ `find ${profilingPath} -maxdepth 1 -type f -delete`
+ ].join(" && ");
+
+ const [okClone, errClone] = await bash`${gitCommand}`;
+ if (errClone) {
+ let combinedMessage = "Git clone failed.";
+ if (tarFailed) {
+ combinedMessage = `Tar-based cloning also failed:\n${tarFailed}\n\nGit fallback failed:\n${errClone}`;
+ }
+ throw new Error(combinedMessage);
+ }
+
+ // Remove .git folder if present
+ await files.rm_recursive_async(files.pathJoin(profilingPath, ".git"));
+
+ Console.info("Meteor profiling suite cloned to: " + Console.path(profilingPath));
+};
+
+async function doBenchmarkCommand(options) {
+ const isWindows = process.platform === "win32";
+ if (isWindows) {
+ throw new Error('Profiling is not supported on Windows');
+ }
+
+ const args = process.argv.slice(3);
+ var projectContext = new projectContextModule.ProjectContext({
+ projectDir: options.appDir,
+ allowIncompatibleUpdate: options['allow-incompatible-update'],
+ lintAppAndLocalPackages: !options['no-lint'],
+ });
+ const profilingPath = `${projectContext.projectDir}/node_modules/.cache/meteor/performance`;
+ await setupBenchmarkSuite(profilingPath);
+
+ const meteorSizeEnvs = [
+ !!options['size-only'] && 'METEOR_BUNDLE_SIZE_ONLY=true',
+ !!options['size'] && 'METEOR_BUNDLE_SIZE=true',
+ !!options['build'] && 'METEOR_BUNDLE_BUILD=true',
+ ].filter(Boolean);
+ const meteorOptions = args.filter(arg => !['--size-only', '--size', '--build'].includes(arg));
+
+ const profilingCommand = [
+ `${meteorSizeEnvs.join(' ')} ${profilingPath}/scripts/monitor-bundler.sh ${projectContext.projectDir} ${new Date().getTime()} ${meteorOptions.join(' ')}`.trim(),
+ ].join(" && ");
+ const [, errBenchmark] = await bashLive`${profilingCommand}`;
+ if (errBenchmark) {
+ throw new Error(errBenchmark);
+ }
+}
+
+main.registerCommand(
+{
+ name: 'profile',
+ maxArgs: Infinity,
+ options: {
+ ...buildCommands.options || {},
+ ...runCommandOptions.options || {},
+ 'size': { type: Boolean },
+ 'size-only': { type: Boolean },
+ 'build': { type: Boolean },
+ },
+ catalogRefresh: new catalog.Refresh.Never(),
+}, doBenchmarkCommand);
diff --git a/tools/cli/dev-bundle-bin-commands.js b/tools/cli/dev-bundle-bin-commands.js
index a3b980533b..d7f0722ce8 100644
--- a/tools/cli/dev-bundle-bin-commands.js
+++ b/tools/cli/dev-bundle-bin-commands.js
@@ -31,9 +31,10 @@ async function getChildProcess({ isFirstTry }) {
return null;
}
- const child = require("child_process").spawn(cmd, args, {
- stdio: "inherit",
- env: env
+ const child = require('child_process').spawn(cmd, args, {
+ stdio: 'inherit',
+ env: env,
+ shell: process.platform === 'win32' && ['.cmd', '.bat'].some(_extension => cmd.endsWith(_extension)),
});
require("./flush-buffers-on-exit-in-windows");
child.on("error", function (error) {
diff --git a/tools/cli/help.txt b/tools/cli/help.txt
index 20d5dc5f84..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.
@@ -492,7 +493,7 @@ Note that you must have mongosh installed to use this option.
Options:
--url, -U return a Mongo database URL
--verbose, -v to show the errors that have occurred while connecting to the
- database
+ database
Currently, this feature can only be used when developing locally.
The opened Mongo shell connects to the current project's local
@@ -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
@@ -816,7 +819,16 @@ Usage: meteor admin [args]
Rarely used commands for administering official Meteor services.
Commands:
-{{commands}}
+
+ make-bootstrap-tarballs
+ recommend-release
+ change-homepage
+ set-unmigrated
+ set-banners
+ list-organizations
+ members
+ set-latest-readme
+ get-machine
See 'meteor help admin ' for details on an admin command.
@@ -875,12 +887,12 @@ for replacing the names, we offer $$PascalName$$, $$camelName$$, $$name$$.
This is a MeteorJS project command.
Options:
- --help Show help.
- --path The path to the folder where the files will be generated. Default is the current folder.
- --templatePath Path to the template file check https://docs.meteor.com/commandline.html#meteorgenerate-templating for more info.
- --replaceFn Replace function to replace the names in the template. Check https://docs.meteor.com/commandline.html#meteorgenerate-templating for more info.
- --methods Generate methods.
- --publications Generate publications.
+ --help Show help.
+ --path The path to the folder where the files will be generated. Default is the current folder.
+ --templatePath Path to the template file check https://docs.meteor.com/commandline.html#meteorgenerate-templating for more info.
+ --replaceFn Replace function to replace the names in the template. Check https://docs.meteor.com/commandline.html#meteorgenerate-templating for more info.
+ --methods Generate methods.
+ --publications Generate publications.
>>> publish-release
@@ -1151,3 +1163,30 @@ To see the requirements for this compilation step, consult the
platform requirements for 'node-gyp':
https://github.com/nodejs/node-gyp
+
+>>> profile
+Run performance profiling for the Meteor application.
+Usage: meteor profile [...]
+
+This command runs a performance profile for the Meteor application, monitoring the
+bundler process and tracking key performance metrics to help analyze the build and
+bundling performance.
+
+Use METEOR_IDLE_TIMEOUT= to set a timeout for profiling. The default time (90s)
+is usually enough for each build step to complete. If you encounter errors due to
+early exits, adjust the environment variable accordingly.
+
+Use METEOR_CLIENT_ENTRYPOINT= to set a custom client entrypoint, and
+METEOR_SERVER_ENTRYPOINT= to set a custom server entrypoint. By default,
+it uses the server and client entrypoints specified in your package.json.
+
+Use METEOR_LOG_DIR= to set a custom log directory.
+
+Options:
+ --size monitor both bundle runtime and size
+ --size-only monitor only the bundle size
+ --build monitor build time
+
+The rest of options for this command are the same as those for the meteor run command.
+You can pass typical runtime options (such as --settings, --exclude-archs, etc.)
+to customize the profiling process.
diff --git a/tools/cli/main.js b/tools/cli/main.js
index 998e2bfc63..b73e2b7868 100644
--- a/tools/cli/main.js
+++ b/tools/cli/main.js
@@ -287,12 +287,16 @@ main.captureAndExit = async function (header, title, f) {
// NB: files required up to this point may not define commands
+const { initMeteorConfig } = require('../tool-env/meteor-config');
require('./commands.js');
require('./commands-packages.js');
require('./commands-packages-query.js');
require('./commands-cordova.js');
require('./commands-aliases.js');
+// Initialize meteorConfig globally
+initMeteorConfig();
+
///////////////////////////////////////////////////////////////////////////////
// Record all the top-level commands as JSON
///////////////////////////////////////////////////////////////////////////////
@@ -865,6 +869,9 @@ makeGlobalAsyncLocalStorage().run({}, async function () {
var appDir = files.findAppDir();
if (appDir) {
appDir = files.pathResolve(appDir);
+
+ // Renitialize meteorConfig globally when having appDir context
+ initMeteorConfig(appDir);
}
await require('../tool-env/isopackets.js').ensureIsopacketsLoadable();
@@ -1543,6 +1550,9 @@ makeGlobalAsyncLocalStorage().run({}, async function () {
});
}
+ // Set the currentCommand in the global object to spread context
+ global.currentCommand = { name: command.name, options };
+
var ret = await command.func(options, { rawOptions });
} catch (e) {
diff --git a/tools/cordova/index.js b/tools/cordova/index.js
index 1230faba92..e1acabacb6 100644
--- a/tools/cordova/index.js
+++ b/tools/cordova/index.js
@@ -13,11 +13,11 @@ export const CORDOVA_ARCH = "web.cordova";
export const CORDOVA_PLATFORMS = ['ios', 'android'];
-const CORDOVA_ANDROID_VERSION = "13.0.0";
+const CORDOVA_ANDROID_VERSION = "14.0.1";
export const CORDOVA_DEV_BUNDLE_VERSIONS = {
- 'cordova-lib': '12.0.1',
- 'cordova-common': '5.0.0',
+ 'cordova-lib': '12.0.2',
+ 'cordova-common': '5.0.1',
'cordova-create': '2.0.0',
'cordova-registry-mapper': '1.1.15',
'cordova-android': CORDOVA_ANDROID_VERSION,
diff --git a/tools/cordova/project.js b/tools/cordova/project.js
index 001871cf70..27a6891bfd 100644
--- a/tools/cordova/project.js
+++ b/tools/cordova/project.js
@@ -11,6 +11,7 @@ import { Profile } from '../tool-env/profile';
import buildmessage from '../utils/buildmessage.js';
import main from '../cli/main.js';
import { execFileAsync } from '../utils/processes';
+var meteorNpm = require('../isobuild/meteor-npm');
import { cordova as cordova_lib, events as cordova_events, CordovaError }
from 'cordova-lib';
@@ -22,6 +23,8 @@ import { CORDOVA_PLATFORMS, CORDOVA_PLATFORM_VERSIONS, displayNameForPlatform, d
newPluginId, convertPluginVersions, convertToGitUrl } from './index.js';
import { CordovaBuilder } from './builder.js';
+const cordovaPackagesFile = 'cordova-packages.json';
+
cordova_events.on('verbose', logIfVerbose);
cordova_events.on('log', logIfVerbose);
cordova_events.on('info', logIfVerbose);
@@ -429,13 +432,15 @@ to build apps for ${displayNameForPlatform(platform)}.`);
}
async installedVersionForPlatform(platform) {
- const command = files.convertToOSPath(files.pathJoin(
+ const file = files.convertToOSPath(files.pathJoin(
this.projectRoot, 'platforms', platform, 'cordova', 'version'));
+ const command = process.platform === "win32" ? process.execPath : file;
+ const args = process.platform === "win32" ? [file] : [];
// Make sure the command exists before trying to execute it
if (files.exists(command)) {
return this.runCommands(
`getting installed version for platform ${platform} in Cordova project`,
- execFileAsync(command, {
+ execFileAsync(command, args, {
env: this.defaultEnvWithPathsAdded(),
cwd: this.projectRoot}), null, null);
} else {
@@ -458,30 +463,41 @@ to Cordova project`, async () => {
let platformSpec = version ? `${platform}@${version}` : platform;
await cordova_lib.platform('add', platformSpec, this.defaultOptions);
+ const installedPlugins = this.listInstalledPluginVersions();
+
// As per Npm 8, we need now do inject a package.json file
// with the dependencies so that when running any npm command
// it keeps the dependencies installed.
- const packageLock = JSON.parse(files.readFile(
- files.pathJoin(self.projectRoot, 'node_modules/.package-lock.json')
- ));
+ const packageLock = files.exists('node_modules/.package-lock.json') ? JSON.parse(files.readFile(
+ files.pathJoin(self.projectRoot, 'node_modules/.package-lock.json')
+ )) : { packages: { [`cordova-${platform}`]: { version } } };
+ // Accumulated dependencies from plugins
+ const cordovaPackagesPath = files.pathJoin(self.projectRoot, cordovaPackagesFile);
+ const cordovaPackageLock = files.exists(cordovaPackagesPath) ? JSON.parse(files.readFile(cordovaPackagesPath)) : {};
+
+ // Ensure all packages are kept installed
+ const packages = { ...(cordovaPackageLock?.packages || {}), ...(packageLock?.packages || {}) };
const getPackageName = (pkgPath) => {
const split = pkgPath.split("node_modules/");
return split[split.length - 1];
};
- const packageJsonObj = Object.entries(packageLock.packages).reduce((acc, [key, value]) => {
+ const packageJsonObj = Object.entries(packages).reduce((acc, [key, value]) => {
const name = getPackageName(key);
+ const originalPluginVersion = installedPlugins[name];
return ({
dependencies: {
...acc.dependencies,
- [name]: value.version,
+ [name]: originalPluginVersion || value.version,
}
});
- }, { dependencies: {} });
+ }, { dependencies: { [`cordova-${platform}`]: version } });
files.writeFile(
files.pathJoin(self.projectRoot, "package.json"),
JSON.stringify(packageJsonObj, null, 2) + "\n"
);
+
+ await meteorNpm.runNpmCommand(["install"], self.projectRoot);
});
}
@@ -625,6 +641,26 @@ from Cordova project`, async () => {
await this.runCommands(`adding plugin ${target} \
to Cordova project`, cordova_lib.plugin.bind(undefined, 'add', [target],
commandOptions));
+
+ // Accumulate dependencies from plugins to later installation
+ const cordovaPackagesPath = files.pathJoin(this.projectRoot, cordovaPackagesFile);
+ const packageLock = JSON.parse(files.readFile(
+ files.pathJoin(this.projectRoot, 'node_modules/.package-lock.json')
+ ));
+ const existingPackageLock = files.exists(cordovaPackagesPath) ?
+ JSON.parse(files.readFile(cordovaPackagesPath))
+ : {};
+ const accumulatedCordovaPackages = {
+ packages: {
+ ...existingPackageLock.packages,
+ ...packageLock.packages,
+ }
+ };
+ files.writeFile(
+ cordovaPackagesPath,
+ JSON.stringify(accumulatedCordovaPackages, null, 2) + "\n"
+ );
+
} catch (error) {
if (retry && utils.isUrlWithSha(version)) {
Console.warn(`Cordova plugin add for ${id} failed with plugin id
diff --git a/tools/fs/files.ts b/tools/fs/files.ts
index cc9e0c8ee7..1babe78416 100644
--- a/tools/fs/files.ts
+++ b/tools/fs/files.ts
@@ -358,6 +358,20 @@ export const rm_recursive = Profile("files.rm_recursive", async (path: string) =
}
});
+export const rm_recursive_deferred = Profile("files.rm_recursive_deferred", async (path: string) => {
+ // Generate a temp path name for the old build directory
+ const oldBuildPath = path + '.old-' + Math.floor(Math.random() * 999999);
+ // If the original buildPath exists, rename it first
+ if (exists(path)) {
+ await rename(path, oldBuildPath);
+ // Start deletion of old directory asynchronously without awaiting
+ rm_recursive(oldBuildPath).catch(e => {
+ // Log error but don't fail the build
+ console.error(`Error removing old build directory ${oldBuildPath}:`, e);
+ });
+ }
+});
+
// Returns the base64 SHA256 of the given file.
export function fileHash(filename: string) {
const crypto = require('crypto');
diff --git a/tools/fs/safe-watcher-legacy.ts b/tools/fs/safe-watcher-legacy.ts
new file mode 100644
index 0000000000..f9f108afc8
--- /dev/null
+++ b/tools/fs/safe-watcher-legacy.ts
@@ -0,0 +1,476 @@
+import { FSWatcher, Stats, BigIntStats } from "fs";
+import { Profile } from "../tool-env/profile";
+import {
+ statOrNull,
+ convertToOSPath,
+ watchFile,
+ unwatchFile,
+ toPosixPath,
+ pathRelative
+} from "./files";
+import {
+ join as nativeJoin
+} from 'path';
+import nsfw from 'vscode-nsfw';
+
+const pathwatcher = require('pathwatcher');
+
+// Default to prioritizing changed files, but disable that behavior (and
+// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED
+// is explicitly set to a string that parses to a falsy value.
+var PRIORITIZE_CHANGED = true;
+if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED &&
+ ! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) {
+ PRIORITIZE_CHANGED = false;
+}
+
+var DEFAULT_POLLING_INTERVAL =
+ +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000);
+
+var NO_WATCHER_POLLING_INTERVAL =
+ +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500);
+
+// This may seems like a long time to wait before actually closing the
+// file watchers, but it's to our advantage if they survive restarts.
+const WATCHER_CLEANUP_DELAY_MS = 30000;
+
+// Since linux doesn't have recursive file watching, nsfw has to walk the
+// watched folder and create a separate watcher for each subfolder. Until it has a
+// way for us to filter which folders it walks we will continue to use
+// pathwatcher to avoid having too many watchers.
+let watcherLibrary = process.env.METEOR_WATCHER_LIBRARY ||
+ (process.platform === 'linux' ? 'pathwatcher' : 'nsfw');
+
+// Pathwatcher complains (using console.error, ugh) if you try to watch
+// two files with the same stat.ino number but different paths on linux, so we have
+// to deduplicate files by ino.
+const DEDUPLICATE_BY_INO = watcherLibrary === 'pathwatcher';
+// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to
+// force the use of files.watchFile instead of watchLibrary.watch.
+let watcherEnabled = ! JSON.parse(
+ process.env.METEOR_WATCH_FORCE_POLLING || "false"
+);
+
+const entriesByIno = new Map;
+
+export type SafeWatcher = {
+ close: () => void;
+}
+
+type EntryCallback = (event: string) => void;
+
+interface Entry extends SafeWatcher {
+ callbacks: Set;
+ rewatch: () => void;
+ release: (callback: EntryCallback) => void;
+ _fire: (event: string) => void;
+}
+
+const entries: Record = Object.create(null);
+
+// Folders that are watched recursively
+let watchRoots = new Set();
+
+// Set of paths for which a change event has been fired, watched with
+// watchLibrary.watch if available. This could be an LRU cache, but in
+// practice it should never grow large enough for that to matter.
+const changedPaths = new Set;
+
+function hasPriority(absPath: string) {
+ // If we're not prioritizing changed files, then all files have
+ // priority, which means they should be watched with native file
+ // watchers if the platform supports them. If we are prioritizing
+ // changed files, then only changed files get priority.
+ return PRIORITIZE_CHANGED
+ ? changedPaths.has(absPath)
+ : true;
+}
+
+function acquireWatcher(absPath: string, callback: EntryCallback) {
+ const entry = entries[absPath] || (
+ entries[absPath] = startNewWatcher(absPath));
+
+ // Watches successfully established in the past may have become invalid
+ // because the watched file was deleted or renamed, so we need to make
+ // sure we're still watching every time we call safeWatcher.watch.
+ entry.rewatch();
+
+ // The size of the entry.callbacks Set also serves as a reference count
+ // for this watcher.
+ entry.callbacks.add(callback);
+
+ return entry;
+}
+
+function startNewWatcher(absPath: string): Entry {
+ let stat: Stats | BigIntStats | null | undefined = null;
+
+ if (DEDUPLICATE_BY_INO) {
+ stat = statOrNull(absPath);
+ if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) {
+ const entry = entriesByIno.get(stat.ino);
+ if (entries[absPath] === entry) {
+ return entry;
+ }
+ }
+ } else {
+ let entry = entries[absPath];
+ if (entry) {
+ return entry;
+ }
+ }
+
+ function safeUnwatch() {
+ if (watcher) {
+ watcher.close();
+ watcher = null;
+ if (stat && stat.ino > 0) {
+ entriesByIno.delete(stat.ino);
+ }
+ }
+ }
+
+ let lastWatcherEventTime = Date.now();
+ const callbacks = new Set();
+ let watcherCleanupTimer: ReturnType | null = null;
+ let watcher: FSWatcher | null = null;
+
+ // Determines the polling interval to be used for the fs.watchFile-based
+ // safety net that works on all platforms and file systems.
+ function getPollingInterval() {
+ if (hasPriority(absPath)) {
+ // Regardless of whether we have a native file watcher and it works
+ // correctly on this file system, poll prioritized files (that is,
+ // files that have been changed at least once) at a higher frequency
+ // (every 500ms by default).
+ return NO_WATCHER_POLLING_INTERVAL;
+ }
+
+ if (watcherEnabled || PRIORITIZE_CHANGED) {
+ // As long as native file watching is enabled (even if it doesn't
+ // work correctly) and the developer hasn't explicitly opted out of
+ // the file watching priority system, poll unchanged files at a
+ // lower frequency (every 5000ms by default).
+ return DEFAULT_POLLING_INTERVAL;
+ }
+
+ // If native file watching is disabled and the developer has
+ // explicitly opted out of the priority system, poll everything at the
+ // higher frequency (every 500ms by default). Note that this leads to
+ // higher idle CPU usage, so the developer may want to adjust the
+ // METEOR_WATCH_POLLING_INTERVAL_MS environment variable.
+ return NO_WATCHER_POLLING_INTERVAL;
+ }
+
+ function fire(event: string) {
+ if (event !== "change") {
+ // When we receive a "delete" or "rename" event, the watcher is
+ // probably not going to generate any more notifications for this
+ // file, so we close and nullify the watcher to ensure that
+ // entry.rewatch() will attempt to reestablish the watcher the next
+ // time we call safeWatcher.watch.
+ safeUnwatch();
+
+ // Make sure we don't throttle the watchFile callback after a
+ // "delete" or "rename" event, since it is now our only reliable
+ // source of file change notifications.
+ lastWatcherEventTime = 0;
+
+ } else {
+ changedPaths.add(absPath);
+ rewatch();
+ }
+
+ callbacks.forEach(cb => cb(event));
+ }
+
+ function watchWrapper(event: string) {
+ lastWatcherEventTime = Date.now();
+ fire(event);
+
+ // It's tempting to call unwatchFile(absPath, watchFileWrapper) here,
+ // but previous watcher success is no guarantee of future watcher
+ // reliability. For example, watchLibrary.watch works just fine when file
+ // changes originate from within a Vagrant VM, but changes to shared
+ // files made outside the VM are invisible to watcher, so our only
+ // hope of catching them is to continue polling.
+ }
+
+ function rewatch() {
+ if (hasPriority(absPath)) {
+ if (watcher) {
+ // Already watching; nothing to do.
+ return;
+ }
+ watcher = watchLibraryWatch(absPath, watchWrapper);
+ } else if (watcher) {
+ safeUnwatch();
+ }
+
+ // Since we're about to restart the stat-based file watcher, we don't
+ // want to miss any of its events because of the lastWatcherEventTime
+ // throttling that it attempts to do.
+ lastWatcherEventTime = 0;
+
+ // We use files.watchFile in addition to watcher.watch as a fail-safe
+ // to detect file changes even on network file systems. However
+ // (unless the user disabled watcher or this watcher call failed), we
+ // use a relatively long default polling interval of 5000ms to save
+ // CPU cycles.
+ statWatch(absPath, getPollingInterval(), watchFileWrapper);
+ }
+
+ function watchFileWrapper(newStat: Stats, oldStat: Stats) {
+ if (newStat.ino === 0 &&
+ oldStat.ino === 0 &&
+ +newStat.mtime === +oldStat.mtime) {
+ // Node calls the watchFile listener once with bogus identical stat
+ // objects, which should not trigger a file change event.
+ return;
+ }
+
+ // If a watcher event fired in the last polling interval, ignore
+ // this event.
+ if (Date.now() - lastWatcherEventTime > getPollingInterval()) {
+ fire("change");
+ }
+ }
+
+ const entry = {
+ callbacks,
+ rewatch,
+
+ release(callback: EntryCallback) {
+ if (! entries[absPath]) {
+ return;
+ }
+
+ callbacks.delete(callback);
+ if (callbacks.size > 0) {
+ return;
+ }
+
+ // Once there are no more callbacks in the Set, close both watchers
+ // and nullify the shared data.
+ if (watcherCleanupTimer) {
+ clearTimeout(watcherCleanupTimer);
+ }
+
+ watcherCleanupTimer = setTimeout(() => {
+ if (callbacks.size > 0) {
+ // If another callback was added while the timer was pending, we
+ // can avoid tearing anything down.
+ return;
+ }
+ entry.close();
+ }, WATCHER_CLEANUP_DELAY_MS);
+ },
+
+ close() {
+ if (entries[absPath] !== entry) return;
+ entries[absPath] = null;
+
+ if (watcherCleanupTimer) {
+ clearTimeout(watcherCleanupTimer);
+ watcherCleanupTimer = null;
+ }
+
+ safeUnwatch();
+
+ unwatchFile(absPath, watchFileWrapper);
+ },
+ _fire: fire
+ };
+
+ if (stat && stat.ino > 0) {
+ entriesByIno.set(stat.ino, entry);
+ }
+
+ return entry;
+}
+
+export function closeAllWatchers() {
+ Object.keys(entries).forEach(absPath => {
+ const entry = entries[absPath];
+ if (entry) {
+ entry.close();
+ }
+ });
+}
+
+const statWatchers = Object.create(null);
+
+function statWatch(
+ absPath: string,
+ interval: number,
+ callback: (current: Stats, previous: Stats) => void,
+) {
+ let statWatcher = statWatchers[absPath];
+
+ if (!statWatcher) {
+ statWatcher = {
+ interval,
+ changeListeners: [],
+ stat: null
+ };
+ statWatchers[absPath] = statWatcher;
+ }
+
+ // If the interval needs to be changed, replace the watcher.
+ // Node will only recreate the watcher with the new interval if all old
+ // watchers are stopped (which unwatchFile does when not passed a
+ // specific listener)
+ if (statWatcher.interval !== interval && statWatcher.stat) {
+ // This stops all stat watchers for the file, not just those created by
+ // statWatch
+ unwatchFile(absPath);
+ statWatcher.stat = null;
+ statWatcher.interval = interval;
+ }
+
+ if (!statWatcher.changeListeners.includes(callback)) {
+ statWatcher.changeListeners.push(callback);
+ }
+
+ if (!statWatcher.stat) {
+ const newStat = watchFile(absPath, {
+ persistent: false, // never persistent
+ interval,
+ }, (newStat, oldStat) => {
+ statWatcher.changeListeners.forEach((
+ listener: (newStat: Stats, oldStat: Stats) => void
+ ) => {
+ listener(newStat, oldStat);
+ });
+ });
+
+ newStat.on("stop", () => {
+ if (statWatchers[absPath] === statWatch) {
+ delete statWatchers[absPath];
+ }
+ });
+
+ statWatcher.stat = newStat;
+ }
+
+ return statWatcher;
+}
+
+function watchLibraryWatch(absPath: string, callback: EntryCallback) {
+ if (watcherEnabled && watcherLibrary === 'pathwatcher') {
+ try {
+ return pathwatcher.watch(convertToOSPath(absPath), callback);
+ } catch (e: any) {
+ maybeSuggestRaisingWatchLimit(e);
+ // ... ignore the error. We'll still have watchFile, which is good
+ // enough.
+ }
+ }
+
+ return null;
+}
+
+let suggestedRaisingWatchLimit = false;
+
+function maybeSuggestRaisingWatchLimit(error: Error & { errno: number }) {
+ var constants = require('constants');
+ var archinfo = require('../utils/archinfo');
+ if (! suggestedRaisingWatchLimit &&
+ // Note: the not-super-documented require('constants') maps from
+ // strings to SYSTEM errno values. System errno values aren't the same
+ // as the numbers used internally by libuv! Once we're upgraded
+ // to Node 0.12, we'll have the system errno as a string (on 'code'),
+ // but the support for that wasn't in Node 0.10's uv.
+ // See our PR https://github.com/atom/node-pathwatcher/pull/53
+ // (and make sure to read the final commit message, not the original
+ // proposed PR, which had a slightly different interface).
+ error.errno === constants.ENOSPC &&
+ // The only suggestion we currently have is for Linux.
+ archinfo.matches(archinfo.host(), 'os.linux')) {
+
+ // Check suggestedRaisingWatchLimit again because archinfo.host() may
+ // have yielded.
+ if (suggestedRaisingWatchLimit) return;
+ suggestedRaisingWatchLimit = true;
+
+ var Console = require('../console/console.js').Console;
+ if (! Console.isHeadless()) {
+ Console.arrowWarn(
+ "It looks like a simple tweak to your system's configuration will " +
+ "make many tools (including this Meteor command) more efficient. " +
+ "To learn more, see " +
+ Console.url("https://github.com/meteor/docs/blob/master/long-form/file-change-watcher-efficiency.md"));
+ }
+ }
+}
+
+export const watch = Profile(
+ "safeWatcher.watchLegacy",
+ (absPath: string, callback: EntryCallback) => {
+ const entry = acquireWatcher(absPath, callback);
+ return {
+ close() {
+ entry.release(callback);
+ }
+ } as SafeWatcher;
+ }
+);
+
+const fireNames = {
+ [nsfw.actions.CREATED]: 'change',
+ [nsfw.actions.MODIFIED]: 'change',
+ [nsfw.actions.DELETED]: 'delete'
+}
+
+export function addWatchRoot(absPath: string) {
+ if (watchRoots.has(absPath) || watcherLibrary !== 'nsfw' || !watcherEnabled) {
+ return;
+ }
+
+ watchRoots.add(absPath);
+
+ // If there already is a watcher for a parent directory, there is no need
+ // to create this watcher.
+ for (const path of watchRoots) {
+ let relativePath = pathRelative(path, absPath);
+ if (
+ path !== absPath &&
+ !relativePath.startsWith('..') &&
+ !relativePath.startsWith('/')
+ ) {
+ return;
+ }
+ }
+
+ // TODO: check if there are any existing watchers that are children of this
+ // watcher and stop them
+
+ nsfw(
+ convertToOSPath(absPath),
+ (events) => {
+ events.forEach(event => {
+ if(event.action === nsfw.actions.RENAMED) {
+ let oldPath = nativeJoin(event.directory, event.oldFile);
+ let oldEntry = entries[toPosixPath(oldPath)];
+ if (oldEntry) {
+ oldEntry._fire('rename');
+ }
+
+ let path = nativeJoin(event.newDirectory, event.newFile);
+ let newEntry = entries[toPosixPath(path)];
+ if (newEntry) {
+ newEntry._fire('change');
+ }
+ } else {
+ let path = nativeJoin(event.directory, event.file);
+ let entry = entries[toPosixPath(path)];
+ if (entry) {
+ entry._fire(fireNames[event.action]);
+ }
+ }
+ })
+ }
+ ).then(watcher => {
+ watcher.start()
+ });
+}
diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts
index cea3d74d7b..07fe605fb7 100644
--- a/tools/fs/safe-watcher.ts
+++ b/tools/fs/safe-watcher.ts
@@ -1,476 +1,624 @@
-import { FSWatcher, Stats, BigIntStats } from "fs";
+import { Stats } from 'fs';
+import ParcelWatcher from "@parcel/watcher";
+import { watch as watchLegacy, addWatchRoot as addWatchRootLegacy, closeAllWatchers as closeAllWatchersLegacy } from './safe-watcher-legacy';
+
import { Profile } from "../tool-env/profile";
-import {
- statOrNull,
- convertToOSPath,
- watchFile,
- unwatchFile,
- toPosixPath,
- pathRelative
-} from "./files";
-import {
- join as nativeJoin
-} from 'path';
-import nsfw from 'vscode-nsfw';
+import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname } from "./files";
+import { getMeteorConfig } from "../tool-env/meteor-config";
-const pathwatcher = require('pathwatcher');
+// Register process exit handlers to ensure subscriptions are properly cleaned up
+const registerExitHandlers = () => {
+
+ // For SIGINT and SIGTERM, we need to handle the async cleanup before the process exits
+ const cleanupAndExit = (signal: string) => {
+ // Clear the timeout if cleanup completes successfully
+ closeAllWatchers().then(() => {
+ process.exit(0);
+ }).catch(err => {
+ console.error(`Error closing watchers on ${signal}:`, err);
+ process.exit(1);
+ });
+ };
+
+ // Handle SIGINT (Ctrl+C)
+ process.on('SIGINT', () => cleanupAndExit('SIGINT'));
+
+ // Handle SIGTERM
+ process.on('SIGTERM', () => cleanupAndExit('SIGTERM'));
+
+ // Handle 'exit' event
+ process.on('exit', () => {
+ try {
+ for (const root of Array.from(watchRoots)) {
+ const sub = dirSubscriptions.get(root);
+ if (sub) {
+ sub.unsubscribe();
+ dirSubscriptions.delete(root);
+ watchRoots.delete(root);
+ }
+ }
+ } catch (err) {
+ console.error('Error during synchronous cleanup on exit:', err);
+ }
+ });
+};
+
+export type SafeWatcher = { close: () => void; };
+
+type ChangeCallback = (event: string) => void;
+
+interface Entry extends SafeWatcher {
+ callbacks: Set;
+ _fire(event: string): void;
+}
+
+// Registry mapping normalized absolute paths to their watcher entry.
+const entries = new Map();
+
+// Registry mapping normalized absolute paths to their polling watchers.
+// Each path can have multiple callbacks, but only one active watcher.
+interface PollingWatcherInfo {
+ callbacks: Set;
+ pollCallback: (curr: Stats, prev: Stats) => void;
+}
+const pollingWatchers = new Map();
+
+function getEntry(path: string): Entry | null | undefined {
+ return entries.get(path);
+}
+
+function setEntry(path: string, entry: Entry | null): void {
+ entries.set(path, entry);
+}
+
+function deleteEntry(path: string): void {
+ entries.delete(path);
+}
+
+function findNearestEntry(startPath: string): Entry | null {
+ let currentPath = pathResolve(startPath);
+
+ while (true) {
+ const entry = getEntry(currentPath);
+ if (entry) {
+ return entry; // Found it!
+ }
+
+ const parentPath = pathDirname(currentPath);
+ if (parentPath === currentPath) {
+ // Reached root
+ break;
+ }
+
+ currentPath = parentPath;
+ }
+
+ return null;
+}
+
+// Watch roots are directories for which we have an active ParcelWatcher subscription.
+const watchRoots = new Set();
+// For each watch root, store its active subscription.
+const dirSubscriptions = new Map();
+// A set of roots that are known to be unwatchable.
+const ignoredWatchRoots = new Set();
+
+// A set of roots that are known to be symbolic links.
+const symlinkRoots = new Set();
+
+// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to
+// force the use of files.watchFile instead of ParcelWatcher.
+let watcherEnabled = !JSON.parse(process.env.METEOR_WATCH_FORCE_POLLING || "false");
+
+/**
+ * Polling fallback globals.
+ * The legacy Meteor strategy used polling for cases where native watchers failed.
+ * We keep track of files that changed, so that we can poll them faster.
+ */
+var DEFAULT_POLLING_INTERVAL =
+ +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000);
+
+var NO_WATCHER_POLLING_INTERVAL =
+ +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500);
-// Default to prioritizing changed files, but disable that behavior (and
-// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED
-// is explicitly set to a string that parses to a falsy value.
var PRIORITIZE_CHANGED = true;
if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED &&
! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) {
PRIORITIZE_CHANGED = false;
}
-var DEFAULT_POLLING_INTERVAL =
- +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000);
-
-var NO_WATCHER_POLLING_INTERVAL =
- +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500);
-
-// This may seems like a long time to wait before actually closing the
-// file watchers, but it's to our advantage if they survive restarts.
-const WATCHER_CLEANUP_DELAY_MS = 30000;
-
-// Since linux doesn't have recursive file watching, nsfw has to walk the
-// watched folder and create a separate watcher for each subfolder. Until it has a
-// way for us to filter which folders it walks we will continue to use
-// pathwatcher to avoid having too many watchers.
-let watcherLibrary = process.env.METEOR_WATCHER_LIBRARY ||
- (process.platform === 'linux' ? 'pathwatcher' : 'nsfw');
-
-// Pathwatcher complains (using console.error, ugh) if you try to watch
-// two files with the same stat.ino number but different paths on linux, so we have
-// to deduplicate files by ino.
-const DEDUPLICATE_BY_INO = watcherLibrary === 'pathwatcher';
-// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to
-// force the use of files.watchFile instead of watchLibrary.watch.
-let watcherEnabled = ! JSON.parse(
- process.env.METEOR_WATCH_FORCE_POLLING || "false"
-);
-
-const entriesByIno = new Map;
-
-export type SafeWatcher = {
- close: () => void;
-}
-
-type EntryCallback = (event: string) => void;
-
-interface Entry extends SafeWatcher {
- callbacks: Set;
- rewatch: () => void;
- release: (callback: EntryCallback) => void;
- _fire: (event: string) => void;
-}
-
-const entries: Record = Object.create(null);
-
-// Folders that are watched recursively
-let watchRoots = new Set();
-
// Set of paths for which a change event has been fired, watched with
-// watchLibrary.watch if available. This could be an LRU cache, but in
-// practice it should never grow large enough for that to matter.
+// watchLibrary.watch if available.
const changedPaths = new Set;
+function shouldIgnorePath(absPath: string): boolean {
+ const posixPath = toPosixPath(absPath);
+ const parts = posixPath.split('/');
+
+ const cwd = toPosixPath(process.cwd());
+ const isWithinCwd = absPath.startsWith(cwd);
+
+ if (isWithinCwd && absPath.includes(`${cwd}/.meteor/local`)) {
+ return true;
+ }
+
+ // Check for .meteor: allow the .meteor directory itself,
+ // but ignore its "local" subdirectory (or any immediate child folder that indicates cache).
+ const meteorIndex = parts.indexOf(".meteor");
+ if (meteorIndex !== -1) {
+ const nextPart = parts[meteorIndex + 1];
+ if (nextPart && nextPart === "local") {
+ // Ignore anything inside .meteor/local
+ return true;
+ }
+ // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc).
+ }
+
+ // For project node_modules: check if it's a direct node_modules/
+ if (isWithinCwd) {
+ // Check if it's the project node_modules
+ if (absPath.includes(`${cwd}/node_modules`)) {
+ // Check if it's a direct node_modules/ path
+ const relPath = absPath.substring(cwd.length + 1); // +1 for the slash
+ const relParts = relPath.split('/');
+ if (relParts.length >= 2 && relParts[0] === 'node_modules') {
+ // If it's a direct node_modules/, check if it's a symlink
+ // We'll return false here (don't ignore) so that the code can later decide to use polling
+ // based on isSymbolicLink check in the watch function
+ if (relParts.length === 2 && isSymbolicLink(absPath)) {
+ return false;
+ }
+ // Check if it's within a symlink root to not ignore
+ if (isWithinSymlinkRoot(absPath)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ // Otherwise, don't ignore non-npm node_modules
+ return false;
+ }
+ }
+
+ // For external node_modules: check if it's a direct node_modules/
+ const nmIndex = parts.indexOf("node_modules");
+ if (nmIndex !== -1) {
+ // Don't ignore node_modules within .npm/package/ paths
+ const npmPackageIndex = parts.indexOf(".npm");
+ if (npmPackageIndex !== -1 && parts[npmPackageIndex + 1] === "package" &&
+ nmIndex > npmPackageIndex && parts[nmIndex - 1] === "package") {
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Check if a path is a symbolic link.
+ *
+ * Symbolic links are not supported natively in some operating systems,
+ * so we need to use polling for them to ensure they are properly watched.
+ * This function is used to determine if a path is a symbolic link,
+ * so we can use polling instead of native watching for it.
+ *
+ * If a path is a symbolic link, its root is added to the symlinkRoots set.
+ */
+function isSymbolicLink(absPath: string, addToRoots = true): boolean {
+ try {
+ const osPath = convertToOSPath(absPath);
+ const stat = lstat(osPath);
+ if (stat?.isSymbolicLink()) {
+ if (addToRoots) {
+ // Add the directory containing the symlink to the symlinkRoots set
+ const symlinkRoot = toPosixPath(absPath);
+ symlinkRoots.add(symlinkRoot);
+ // Rewatch using polling any existing watchers under this symlink root
+ rewatchPolling(symlinkRoot);
+ }
+ return true;
+ }
+ return false;
+ } catch (e) {
+ // If we can't stat the file, assume it's not a symlink
+ return false;
+ }
+}
+
+/**
+ * Check if a path is within any symlink root.
+ *
+ * This is used to determine if a path should use polling instead of native watching,
+ * even if it's not a symlink itself.
+ */
+function isWithinSymlinkRoot(absPath: string): boolean {
+ for (const root of symlinkRoots) {
+ // Check if absPath starts with root + '/'
+ if (absPath === root || (absPath.startsWith(root) && absPath.charAt(root.length) === '/')) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Ensure that the given directory is being watched by @parcel/watcher.
+ * If it is not a directory or is unwatchable, it is immediately added to an ignore set.
+ */
+async function ensureWatchRoot(dirPath: string): Promise {
+ if (!watcherEnabled || watchRoots.has(dirPath) || ignoredWatchRoots.has(dirPath)) {
+ return;
+ }
+
+ // If an ancestor is already watched, skip this one.
+ for (const root of watchRoots) {
+ const rel = pathRelative(root, dirPath);
+ if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) {
+ return;
+ }
+ }
+
+ // Remove any existing roots that are now encompassed by the new one.
+ for (const root of Array.from(watchRoots)) {
+ const rel = pathRelative(dirPath, root);
+ if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) {
+ const sub = dirSubscriptions.get(root);
+ if (sub) {
+ try {
+ await sub.unsubscribe();
+ } catch (_) {
+ /* ignore errors */
+ }
+ }
+ dirSubscriptions.delete(root);
+ watchRoots.delete(root);
+ }
+ }
+
+ const osDirPath = convertToOSPath(dirPath);
+ // Check that osDirPath is indeed a directory.
+ try {
+ const stats = statOrNull(osDirPath);
+ if (!stats?.isDirectory()) {
+ console.warn(`Skipping watcher for ${osDirPath}: not a directory`);
+ ignoredWatchRoots.add(dirPath);
+ return;
+ }
+ } catch (e) {
+ console.error(`Failed to stat ${osDirPath}:`, e);
+ ignoredWatchRoots.add(dirPath);
+ return;
+ }
+
+ // Set up ignore patterns to skip node_modules and .meteor/local cache
+ const cwd = toPosixPath(process.cwd());
+ const isWithinCwd = dirPath.startsWith(cwd);
+ const ignPrefix = isWithinCwd ? "" : "**/";
+ const ignorePatterns = [`${ignPrefix}node_modules/**`, `${ignPrefix}.meteor/local/**`];
+ try {
+ watchRoots.add(dirPath);
+ const subscription = await ParcelWatcher.subscribe(
+ osDirPath,
+ (err, events) => {
+ if (err) {
+ if (/Events were dropped/.test(err.message)) {
+ return;
+ }
+ console.error(`Parcel watcher error on ${osDirPath}:`, err);
+ // Only disable native watching for critical errors (like ENOSPC).
+ // @ts-ignore
+ if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) {
+ fallbackToPolling();
+ }
+ watchRoots.delete(dirPath);
+ return;
+ }
+ // Dispatch each event to any registered entries.
+ for (const event of events) {
+ const changedPath = toPosixPath(event.path);
+ const entry = findNearestEntry(changedPath);
+ if (!entry) continue;
+ // In Meteor's safe-watcher API, both create/update trigger "change" events.
+ const evtType = event.type === "delete" ? "delete" : "change";
+ entry._fire(evtType);
+ }
+ },
+ { ignore: ignorePatterns }
+ );
+ dirSubscriptions.set(dirPath, subscription);
+ } catch (e: any) {
+ if (
+ e &&
+ (e.code === "ENOTDIR" ||
+ /Not a directory/.test(e.message) ||
+ e.code === "EBADF" ||
+ /Bad file descriptor/.test(e.message))
+ ) {
+ console.warn(`Skipping watcher for ${osDirPath}: not a directory`);
+ ignoredWatchRoots.add(dirPath);
+ } else {
+ console.error(`Failed to start watcher for ${osDirPath}:`, e);
+ if (e.code === "ENOSPC" || e.errno === require("constants").ENOSPC) {
+ fallbackToPolling();
+ }
+ }
+ watchRoots.delete(dirPath);
+ }
+}
+
+/**
+ * Creates a new watch entry for a specific file (or directory) and
+ * holds its registered callbacks.
+ */
+function startNewEntry(absPath: string): Entry {
+ const callbacks = new Set();
+ let closed = false;
+ const entry: Entry = {
+ callbacks,
+ close() {
+ if (closed) return;
+ closed = true;
+ deleteEntry(absPath);
+ },
+ _fire(event: string) {
+ callbacks.forEach(cb => {
+ try {
+ cb(event);
+ } catch (e) {
+ // Ignore callback errors.
+ }
+ });
+ }
+ };
+ return entry;
+}
+
+/**
+ * The primary API function to watch a file or directory.
+ * This registers the callback on the internally managed entry and
+ * ensures that a Parcel watcher is subscribed to a covering directory.
+ */
+export function watch (absPath: string, callback: ChangeCallback): SafeWatcher {
+ // @ts-ignore
+ if (!getMeteorConfig()?.modern?.watcher) {
+ // @ts-ignore
+ return watchLegacy(absPath, callback);
+ }
+ // @ts-ignore
+ return watchModern(absPath, callback);
+};
+
+const watchModern =
+ Profile(
+ "safeWatcher.watchModern",
+ (
+ absPath: string,
+ callback: ChangeCallback
+ ): SafeWatcher => {
+ absPath = toPosixPath(absPath);
+
+ // If the path should be ignored, immediately return a noop SafeWatcher.
+ if (shouldIgnorePath(absPath)) {
+ return { close() {} };
+ }
+ // If native watching is disabled, the path is a symbolic link, or the path is within a symlink root,
+ // use the polling strategy. Symbolic links are not supported natively in some operating systems,
+ // and paths within symlink roots should also use polling for consistency.
+ if (!watcherEnabled || isWithinSymlinkRoot(absPath) || isSymbolicLink(absPath)) {
+ return startPolling(absPath, callback);
+ }
+ // Try to reuse an existing entry if one was created before.
+ let entry = getEntry(absPath);
+ if (!entry) {
+ entry = startNewEntry(absPath);
+ setEntry(absPath, entry);
+ // Determine the directory that should be watched.
+ let watchTarget: string;
+ try {
+ const st = statOrNull(convertToOSPath(absPath));
+ watchTarget = st?.isDirectory() ? absPath : toPosixPath(pathDirname(convertToOSPath(absPath)));
+ } catch (e) {
+ watchTarget = toPosixPath(pathDirname(convertToOSPath(absPath)));
+ }
+ // Set up a watcher on the parent directory (or the directory itself) if not already active.
+ ensureWatchRoot(watchTarget);
+ }
+ // Register the callback for this file.
+ entry.callbacks.add(callback);
+ return {
+ close() {
+ const entry = getEntry(absPath);
+ if (entry) {
+ entry.callbacks.delete(callback);
+ if (entry.callbacks.size === 0) {
+ entry.close();
+ }
+ }
+ }
+ };
+});
+
+/**
+ * Externally force a directory to be watched.
+ * If the provided path is a file, its parent directory is used.
+ */
+export function addWatchRoot(absPath: string) {
+ // @ts-ignore
+ if (!getMeteorConfig()?.modern?.watcher) {
+ // @ts-ignore
+ return addWatchRootLegacy(absPath);
+ }
+
+ absPath = toPosixPath(absPath);
+ let watchTarget = absPath;
+ try {
+ const st = statOrNull(convertToOSPath(absPath));
+ if (!st?.isDirectory()) {
+ watchTarget = toPosixPath(pathDirname(convertToOSPath(absPath)));
+ }
+ } catch (e) {
+ watchTarget = toPosixPath(pathDirname(convertToOSPath(absPath)));
+ }
+ ensureWatchRoot(watchTarget);
+}
+
+async function safeUnsubscribeSub(root: string) {
+ const sub = dirSubscriptions.get(root);
+ if (!sub) return; // Already unsubscribed.
+ // Remove from our maps immediately to prevent further unsubscribe calls.
+ dirSubscriptions.delete(root);
+ watchRoots.delete(root);
+ try {
+ await sub.unsubscribe();
+ } catch (e) {
+ console.error(`Error during unsubscribe for ${root}:`, e);
+ }
+}
+
+export async function closeAllWatchers() {
+ // @ts-ignore
+ if (!getMeteorConfig()?.modern?.watcher) {
+ // @ts-ignore
+ return closeAllWatchersLegacy();
+ }
+ for (const root of Array.from(watchRoots)) {
+ await safeUnsubscribeSub(root);
+ }
+}
+
function hasPriority(absPath: string) {
// If we're not prioritizing changed files, then all files have
// priority, which means they should be watched with native file
// watchers if the platform supports them. If we are prioritizing
// changed files, then only changed files get priority.
return PRIORITIZE_CHANGED
- ? changedPaths.has(absPath)
- : true;
+ ? changedPaths.has(absPath)
+ : true;
}
-function acquireWatcher(absPath: string, callback: EntryCallback) {
- const entry = entries[absPath] || (
- entries[absPath] = startNewWatcher(absPath));
-
- // Watches successfully established in the past may have become invalid
- // because the watched file was deleted or renamed, so we need to make
- // sure we're still watching every time we call safeWatcher.watch.
- entry.rewatch();
-
- // The size of the entry.callbacks Set also serves as a reference count
- // for this watcher.
- entry.callbacks.add(callback);
-
- return entry;
-}
-
-function startNewWatcher(absPath: string): Entry {
- let stat: Stats | BigIntStats | null | undefined = null;
-
- if (DEDUPLICATE_BY_INO) {
- stat = statOrNull(absPath);
- if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) {
- const entry = entriesByIno.get(stat.ino);
- if (entries[absPath] === entry) {
- return entry;
- }
- }
- } else {
- let entry = entries[absPath];
- if (entry) {
- return entry;
- }
- }
-
- function safeUnwatch() {
- if (watcher) {
- watcher.close();
- watcher = null;
- if (stat && stat.ino > 0) {
- entriesByIno.delete(stat.ino);
- }
- }
- }
-
- let lastWatcherEventTime = Date.now();
- const callbacks = new Set();
- let watcherCleanupTimer: ReturnType | null = null;
- let watcher: FSWatcher | null = null;
-
- // Determines the polling interval to be used for the fs.watchFile-based
- // safety net that works on all platforms and file systems.
- function getPollingInterval() {
- if (hasPriority(absPath)) {
- // Regardless of whether we have a native file watcher and it works
- // correctly on this file system, poll prioritized files (that is,
- // files that have been changed at least once) at a higher frequency
- // (every 500ms by default).
- return NO_WATCHER_POLLING_INTERVAL;
- }
-
- if (watcherEnabled || PRIORITIZE_CHANGED) {
- // As long as native file watching is enabled (even if it doesn't
- // work correctly) and the developer hasn't explicitly opted out of
- // the file watching priority system, poll unchanged files at a
- // lower frequency (every 5000ms by default).
- return DEFAULT_POLLING_INTERVAL;
- }
-
- // If native file watching is disabled and the developer has
- // explicitly opted out of the priority system, poll everything at the
- // higher frequency (every 500ms by default). Note that this leads to
- // higher idle CPU usage, so the developer may want to adjust the
- // METEOR_WATCH_POLLING_INTERVAL_MS environment variable.
+// Determines the polling interval to be used for the fs.watchFile-based
+// safety net that works on all platforms and file systems.
+function getPollingInterval(absPath: string): number {
+ if (hasPriority(absPath)) {
+ // Regardless of whether we have a native file watcher and it works
+ // correctly on this file system, poll prioritized files (that is,
+ // files that have been changed at least once) at a higher frequency
+ // (every 500ms by default).
return NO_WATCHER_POLLING_INTERVAL;
}
- function fire(event: string) {
- if (event !== "change") {
- // When we receive a "delete" or "rename" event, the watcher is
- // probably not going to generate any more notifications for this
- // file, so we close and nullify the watcher to ensure that
- // entry.rewatch() will attempt to reestablish the watcher the next
- // time we call safeWatcher.watch.
- safeUnwatch();
-
- // Make sure we don't throttle the watchFile callback after a
- // "delete" or "rename" event, since it is now our only reliable
- // source of file change notifications.
- lastWatcherEventTime = 0;
-
- } else {
- changedPaths.add(absPath);
- rewatch();
- }
-
- callbacks.forEach(cb => cb(event));
+ if (watcherEnabled || PRIORITIZE_CHANGED) {
+ // As long as native file watching is enabled (even if it doesn't
+ // work correctly) and the developer hasn't explicitly opted out of
+ // the file watching priority system, poll unchanged files at a
+ // lower frequency (every 5000ms by default).
+ return DEFAULT_POLLING_INTERVAL;
}
- function watchWrapper(event: string) {
- lastWatcherEventTime = Date.now();
- fire(event);
+ // If native file watching is disabled and the developer has
+ // explicitly opted out of the priority system, poll everything at the
+ // higher frequency (every 500ms by default). Note that this leads to
+ // higher idle CPU usage, so the developer may want to adjust the
+ // METEOR_WATCH_POLLING_INTERVAL_MS environment variable.
+ return NO_WATCHER_POLLING_INTERVAL;
+}
- // It's tempting to call unwatchFile(absPath, watchFileWrapper) here,
- // but previous watcher success is no guarantee of future watcher
- // reliability. For example, watchLibrary.watch works just fine when file
- // changes originate from within a Vagrant VM, but changes to shared
- // files made outside the VM are invisible to watcher, so our only
- // hope of catching them is to continue polling.
- }
+function startPolling(absPath: string, callback: ChangeCallback): SafeWatcher {
+ const osPath = convertToOSPath(absPath);
+ // Initial polling interval.
+ let interval = getPollingInterval(absPath);
- function rewatch() {
- if (hasPriority(absPath)) {
- if (watcher) {
- // Already watching; nothing to do.
- return;
- }
- watcher = watchLibraryWatch(absPath, watchWrapper);
- } else if (watcher) {
- safeUnwatch();
- }
+ // Check if we already have a polling watcher for this path
+ let watcherInfo = pollingWatchers.get(absPath);
- // Since we're about to restart the stat-based file watcher, we don't
- // want to miss any of its events because of the lastWatcherEventTime
- // throttling that it attempts to do.
- lastWatcherEventTime = 0;
-
- // We use files.watchFile in addition to watcher.watch as a fail-safe
- // to detect file changes even on network file systems. However
- // (unless the user disabled watcher or this watcher call failed), we
- // use a relatively long default polling interval of 5000ms to save
- // CPU cycles.
- statWatch(absPath, getPollingInterval(), watchFileWrapper);
- }
-
- function watchFileWrapper(newStat: Stats, oldStat: Stats) {
- if (newStat.ino === 0 &&
- oldStat.ino === 0 &&
- +newStat.mtime === +oldStat.mtime) {
- // Node calls the watchFile listener once with bogus identical stat
- // objects, which should not trigger a file change event.
- return;
- }
-
- // If a watcher event fired in the last polling interval, ignore
- // this event.
- if (Date.now() - lastWatcherEventTime > getPollingInterval()) {
- fire("change");
- }
- }
-
- const entry = {
- callbacks,
- rewatch,
-
- release(callback: EntryCallback) {
- if (! entries[absPath]) {
- return;
- }
-
- callbacks.delete(callback);
- if (callbacks.size > 0) {
- return;
- }
-
- // Once there are no more callbacks in the Set, close both watchers
- // and nullify the shared data.
- if (watcherCleanupTimer) {
- clearTimeout(watcherCleanupTimer);
- }
-
- watcherCleanupTimer = setTimeout(() => {
- if (callbacks.size > 0) {
- // If another callback was added while the timer was pending, we
- // can avoid tearing anything down.
- return;
+ if (watcherInfo) {
+ // Add this callback to the existing watcher
+ watcherInfo.callbacks.add(callback);
+ } else {
+ // Create a new polling watcher
+ const pollCallback = (curr: Stats, prev: Stats) => {
+ // Compare modification times to detect a change.
+ if (+curr.mtime !== +prev.mtime) {
+ changedPaths.add(absPath);
+ // Notify all callbacks registered for this path
+ const info = pollingWatchers.get(absPath);
+ if (info) {
+ for (const cb of info.callbacks) {
+ cb("change");
+ }
}
- entry.close();
- }, WATCHER_CLEANUP_DELAY_MS);
- },
-
- close() {
- if (entries[absPath] !== entry) return;
- entries[absPath] = null;
-
- if (watcherCleanupTimer) {
- clearTimeout(watcherCleanupTimer);
- watcherCleanupTimer = null;
}
-
- safeUnwatch();
-
- unwatchFile(absPath, watchFileWrapper);
- },
- _fire: fire
- };
-
- if (stat && stat.ino > 0) {
- entriesByIno.set(stat.ino, entry);
- }
-
- return entry;
-}
-
-export function closeAllWatchers() {
- Object.keys(entries).forEach(absPath => {
- const entry = entries[absPath];
- if (entry) {
- entry.close();
- }
- });
-}
-
-const statWatchers = Object.create(null);
-
-function statWatch(
- absPath: string,
- interval: number,
- callback: (current: Stats, previous: Stats) => void,
-) {
- let statWatcher = statWatchers[absPath];
-
- if (!statWatcher) {
- statWatcher = {
- interval,
- changeListeners: [],
- stat: null
};
- statWatchers[absPath] = statWatcher;
+
+ watchFile(osPath, { interval }, pollCallback);
+
+ // Store the new watcher info
+ watcherInfo = {
+ callbacks: new Set([callback]),
+ pollCallback
+ };
+ pollingWatchers.set(absPath, watcherInfo);
}
- // If the interval needs to be changed, replace the watcher.
- // Node will only recreate the watcher with the new interval if all old
- // watchers are stopped (which unwatchFile does when not passed a
- // specific listener)
- if (statWatcher.interval !== interval && statWatcher.stat) {
- // This stops all stat watchers for the file, not just those created by
- // statWatch
- unwatchFile(absPath);
- statWatcher.stat = null;
- statWatcher.interval = interval;
- }
+ return {
+ close() {
+ const info = pollingWatchers.get(absPath);
+ if (info) {
+ // Remove this callback
+ info.callbacks.delete(callback);
- if (!statWatcher.changeListeners.includes(callback)) {
- statWatcher.changeListeners.push(callback);
- }
-
- if (!statWatcher.stat) {
- const newStat = watchFile(absPath, {
- persistent: false, // never persistent
- interval,
- }, (newStat, oldStat) => {
- statWatcher.changeListeners.forEach((
- listener: (newStat: Stats, oldStat: Stats) => void
- ) => {
- listener(newStat, oldStat);
- });
- });
-
- newStat.on("stop", () => {
- if (statWatchers[absPath] === statWatch) {
- delete statWatchers[absPath];
- }
- });
-
- statWatcher.stat = newStat;
- }
-
- return statWatcher;
-}
-
-function watchLibraryWatch(absPath: string, callback: EntryCallback) {
- if (watcherEnabled && watcherLibrary === 'pathwatcher') {
- try {
- return pathwatcher.watch(convertToOSPath(absPath), callback);
- } catch (e: any) {
- maybeSuggestRaisingWatchLimit(e);
- // ... ignore the error. We'll still have watchFile, which is good
- // enough.
- }
- }
-
- return null;
-}
-
-let suggestedRaisingWatchLimit = false;
-
-function maybeSuggestRaisingWatchLimit(error: Error & { errno: number }) {
- var constants = require('constants');
- var archinfo = require('../utils/archinfo');
- if (! suggestedRaisingWatchLimit &&
- // Note: the not-super-documented require('constants') maps from
- // strings to SYSTEM errno values. System errno values aren't the same
- // as the numbers used internally by libuv! Once we're upgraded
- // to Node 0.12, we'll have the system errno as a string (on 'code'),
- // but the support for that wasn't in Node 0.10's uv.
- // See our PR https://github.com/atom/node-pathwatcher/pull/53
- // (and make sure to read the final commit message, not the original
- // proposed PR, which had a slightly different interface).
- error.errno === constants.ENOSPC &&
- // The only suggestion we currently have is for Linux.
- archinfo.matches(archinfo.host(), 'os.linux')) {
-
- // Check suggestedRaisingWatchLimit again because archinfo.host() may
- // have yielded.
- if (suggestedRaisingWatchLimit) return;
- suggestedRaisingWatchLimit = true;
-
- var Console = require('../console/console.js').Console;
- if (! Console.isHeadless()) {
- Console.arrowWarn(
- "It looks like a simple tweak to your system's configuration will " +
- "make many tools (including this Meteor command) more efficient. " +
- "To learn more, see " +
- Console.url("https://github.com/meteor/docs/blob/master/long-form/file-change-watcher-efficiency.md"));
- }
- }
-}
-
-export const watch = Profile(
- "safeWatcher.watch",
- (absPath: string, callback: EntryCallback) => {
- const entry = acquireWatcher(absPath, callback);
- return {
- close() {
- entry.release(callback);
- }
- } as SafeWatcher;
- }
-);
-
-const fireNames = {
- [nsfw.actions.CREATED]: 'change',
- [nsfw.actions.MODIFIED]: 'change',
- [nsfw.actions.DELETED]: 'delete'
-}
-
-export function addWatchRoot(absPath: string) {
- if (watchRoots.has(absPath) || watcherLibrary !== 'nsfw' || !watcherEnabled) {
- return;
- }
-
- watchRoots.add(absPath);
-
- // If there already is a watcher for a parent directory, there is no need
- // to create this watcher.
- for (const path of watchRoots) {
- let relativePath = pathRelative(path, absPath);
- if (
- path !== absPath &&
- !relativePath.startsWith('..') &&
- !relativePath.startsWith('/')
- ) {
- return;
- }
- }
-
- // TODO: check if there are any existing watchers that are children of this
- // watcher and stop them
-
- nsfw(
- convertToOSPath(absPath),
- (events) => {
- events.forEach(event => {
- if(event.action === nsfw.actions.RENAMED) {
- let oldPath = nativeJoin(event.directory, event.oldFile);
- let oldEntry = entries[toPosixPath(oldPath)];
- if (oldEntry) {
- oldEntry._fire('rename');
- }
-
- let path = nativeJoin(event.newDirectory, event.newFile);
- let newEntry = entries[toPosixPath(path)];
- if (newEntry) {
- newEntry._fire('change');
- }
- } else {
- let path = nativeJoin(event.directory, event.file);
- let entry = entries[toPosixPath(path)];
- if (entry) {
- entry._fire(fireNames[event.action]);
- }
+ // If no callbacks remain, remove the watcher
+ if (info.callbacks.size === 0) {
+ unwatchFile(osPath, info.pollCallback);
+ pollingWatchers.delete(absPath);
+ changedPaths.delete(absPath);
}
- })
+ }
}
- ).then(watcher => {
- watcher.start()
- });
+ };
}
+
+/**
+ * Rewatch entries under a symlink root from native watchers to polling watchers.
+ * This is called when a new symlink root is discovered.
+ */
+function rewatchPolling(root: string) {
+ for (const [watchedPath, entry] of entries) {
+ // if it lives under the new symlink root...
+ if (watchedPath === root ||
+ (watchedPath.startsWith(root) && watchedPath.charAt(root.length) === '/')) {
+ // Skip if entry is null or already closed
+ if (!entry) continue;
+
+ // Store the callbacks before closing the entry
+ const callbacks = Array.from(entry.callbacks);
+
+ // Tear down the old native watcher
+ entry.close();
+
+ // Remove it from the map
+ entries.delete(watchedPath);
+
+ // Re-watch via polling for each callback
+ for (const cb of callbacks) {
+ startPolling(watchedPath, cb);
+ }
+ }
+ }
+}
+
+/**
+ * Fall back to polling. If a critical error occurs,
+ * we disable native watching and close all existing native watchers.
+ */
+function fallbackToPolling() {
+ if (watcherEnabled) {
+ console.error("Critical native watcher error encountered. Falling back to polling for all entries.");
+ watcherEnabled = false;
+ closeAllWatchers();
+ }
+}
+
+// Register exit handlers to ensure proper cleanup of subscriptions
+registerExitHandlers();
diff --git a/tools/index.js b/tools/index.js
index cbb86ac7d1..82876cae67 100644
--- a/tools/index.js
+++ b/tools/index.js
@@ -1,23 +1,25 @@
-const { getChildProcess } = require('./cli/dev-bundle-bin-commands')
+const { getChildProcess } = require("./cli/dev-bundle-bin-commands");
-getChildProcess({ isFirstTry: true }).then((child) => {
- if (! child) {
- // Use process.nextTick here to prevent the Promise from swallowing
- // errors from the rest of the setup code.
- process.nextTick(continueSetup);
+getChildProcess({ isFirstTry: true }).then(
+ (child) => {
+ if (!child) {
+ // Use process.nextTick here to prevent the Promise from swallowing
+ // errors from the rest of the setup code.
+ process.nextTick(continueSetup);
+ }
+ // If we spawned a process to handle a dev_bundle/bin command like
+ // `meteor npm` or `meteor node`, then don't run any other tool code.
+ },
+ (error) => {
+ process.nextTick(function () {
+ throw error;
+ });
}
- // If we spawned a process to handle a dev_bundle/bin command like
- // `meteor npm` or `meteor node`, then don't run any other tool code.
-}, (error) => {
- process.nextTick(function () {
- throw error;
- });
-});
+);
function continueSetup() {
// Set up the Babel transpiler
- require('./tool-env/install-babel');
+ require("./tool-env/install-babel");
// Run the Meteor command line tool
- require('./cli/main');
+ require("./cli/main");
}
-
diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js
index 563d47bf01..8472f15f16 100644
--- a/tools/isobuild/builder.js
+++ b/tools/isobuild/builder.js
@@ -1,7 +1,7 @@
import assert from "assert";
import {WatchSet, readAndWatchFile, sha1} from '../fs/watch';
import files, {
- symlinkWithOverwrite, realpath,
+ symlinkWithOverwrite, realpath, rm_recursive_deferred,
} from '../fs/files';
import NpmDiscards from './npm-discards';
import {Profile} from '../tool-env/profile';
@@ -126,7 +126,8 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}`
async init() {
// Build the output from scratch
if (this.resetBuildPath) {
- await files.rm_recursive(this.buildPath);
+ await files.rm_recursive_deferred(this.buildPath);
+ // Create the new build directory immediately without waiting for deletion
await files.mkdir_p(this.buildPath, 0o755);
}
this.watchSet = new WatchSet();
@@ -881,7 +882,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}`
removed[path] = true;
} else {
// directory
- await files.rm_recursive(absPath);
+ await files.rm_recursive_deferred(absPath);
// mark all sub-paths as removed, too
paths.forEach((anotherPath) => {
@@ -904,7 +905,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}`
// Delete the partially-completed bundle. Do not disturb outputPath.
abort() {
- return files.rm_recursive(this.buildPath);
+ return files.rm_recursive_deferred(this.buildPath);
}
// Returns a WatchSet representing all files that were read from disk by the
@@ -936,7 +937,7 @@ async function atomicallyRewriteFile(path, data, options) {
// replacing a directory with a file; this is rare (so it can
// be a slow path) but can legitimately happen if e.g. a developer
// puts a file where there used to be a directory in their app.
- await files.rm_recursive(path);
+ await files.rm_recursive_deferred(path);
files.rename(rpath, path);
} else {
throw e;
diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js
index d70f31c6a5..5d42c1b61e 100644
--- a/tools/isobuild/bundler.js
+++ b/tools/isobuild/bundler.js
@@ -1223,13 +1223,16 @@ class Target {
continue;
}
+ var data = await resource.data;
+ var hash = await resource.hash;
+
const fileOptions = {
info: 'unbuild ' + resource,
arch: this.arch,
- data: await resource.data,
+ data,
cacheable: false,
- hash: await resource.hash,
- skipSri: !!await resource.hash
+ hash,
+ skipSri: !!hash
};
const file = new File(fileOptions);
@@ -1290,15 +1293,17 @@ class Target {
continue;
}
+ var data = await resource.data;
+ var hash = await resource.hash;
let sourcePath;
- if ((await resource.data) && resource.sourceRoot && resource.sourcePath) {
+ if (data && resource.sourceRoot && resource.sourcePath) {
sourcePath = files.pathJoin(resource.sourceRoot, resource.sourcePath);
}
const f = new File({
info: 'resource ' + resource.servePath,
arch: this.arch,
- data: await resource.data,
- hash: await resource.hash,
+ data,
+ hash,
cacheable: false,
replaceable: resource.type === 'js' && sourceBatch.hmrAvailable,
sourcePath
@@ -1323,14 +1328,15 @@ class Target {
});
}
+ var sourceMap = await resource.sourceMap;
// Both CSS and JS files can have source maps
- if (await resource.sourceMap) {
+ if (sourceMap) {
// XXX we used to set sourceMapRoot to
// files.pathDirname(relPath) but it's unclear why. With the
// currently generated source map file names, it works without it
// and doesn't work well with it... maybe? we were getting
// 'packages/packages/foo/bar.js'
- f.setSourceMap(await resource.sourceMap, null);
+ f.setSourceMap(sourceMap, null);
}
this[resource.type].push(f);
@@ -1540,6 +1546,9 @@ class Target {
// with the original sources.
rewriteSourceMaps() {
const rewriteSourceMap = function (sm) {
+ if (!sm.sources) {
+ return sm;
+ }
sm.sources = sm.sources.map(function (path) {
const prefix = SOURCE_URL_PREFIX;
if (path.slice(0, prefix.length) === prefix) {
diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js
index 6db7ef7062..6eaa2f332c 100644
--- a/tools/isobuild/compiler-plugin.js
+++ b/tools/isobuild/compiler-plugin.js
@@ -1720,21 +1720,21 @@ export class PackageSourceBatch {
const fileHashes = [];
const cacheKeyPrefix = sha1(JSON.stringify({
linkerOptions,
- files: await jsResources.reduce(async (acc, inputFile) => {
- const resolvedAcc = await acc;
-
- fileHashes.push(await inputFile.hash);
- return [...resolvedAcc, {
- meteorInstallOptions: inputFile.meteorInstallOptions,
- absModuleId: inputFile.absModuleId,
- sourceMap: !! await inputFile.sourceMap,
- mainModule: inputFile.mainModule,
- imported: inputFile.imported,
- alias: inputFile.alias,
- lazy: inputFile.lazy,
- bare: inputFile.bare,
- }];
- }, Promise.resolve([]))
+ files: await Promise.all(
+ jsResources.map(async (inputFile) => {
+ fileHashes.push(await inputFile.hash);
+ return {
+ meteorInstallOptions: inputFile.meteorInstallOptions,
+ absModuleId: inputFile.absModuleId,
+ sourceMap: !!(await inputFile.sourceMap),
+ mainModule: inputFile.mainModule,
+ imported: inputFile.imported,
+ alias: inputFile.alias,
+ lazy: inputFile.lazy,
+ bare: inputFile.bare,
+ };
+ })
+ )
}));
const cacheKeySuffix = sha1(JSON.stringify({
LINKER_CACHE_SALT,
@@ -1832,7 +1832,7 @@ export class PackageSourceBatch {
if (cacheFilename) {
// Write asynchronously.
try {
- await files.rm_recursive(wildcardCacheFilename);
+ await files.rm_recursive_deferred(wildcardCacheFilename);
} finally {
await files.writeFileAtomically(cacheFilename, retAsJSON);
}
diff --git a/tools/isobuild/compiler.js b/tools/isobuild/compiler.js
index 66cb49e2b0..8e03f3d8ca 100644
--- a/tools/isobuild/compiler.js
+++ b/tools/isobuild/compiler.js
@@ -150,6 +150,7 @@ compiler.compile = Profile(function (packageSource, options) {
// Isopack#initFromPath).
var isobuildFeatures = [];
packageSource.architectures.forEach((sourceArch) => {
+ if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(sourceArch.arch)) return;
sourceArch.uses.forEach((use) => {
if (!use.weak && isIsobuildFeaturePackage(use.package) &&
isobuildFeatures.indexOf(use.package) === -1) {
@@ -181,6 +182,7 @@ compiler.compile = Profile(function (packageSource, options) {
if (architecture.arch === 'web.cordova' && ! includeCordovaUnibuild) {
continue;
}
+ if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(architecture.arch)) continue;
// TODO -> Maybe this withCache will bring some problems in other commands.
await files.withCache(async () => {
@@ -226,6 +228,7 @@ compiler.lint = Profile(function (packageSource, options) {
&& architecture.arch === 'web.cordova') {
continue;
}
+ if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(architecture.arch)) continue;
const unibuildWarnings = await lintUnibuild({
isopack: options.isopack,
@@ -246,6 +249,8 @@ compiler.getMinifiers = async function (packageSource, options) {
var minifiers = [];
for (const architecture of packageSource.architectures) {
+ if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(architecture.arch)) continue;
+
var activePluginPackages = await getActivePluginPackages(options.isopack, {
isopackCache: options.isopackCache,
uses: architecture.uses
@@ -791,8 +796,9 @@ async function runLinters({inputSourceArch, isopackCache, sources,
const absPath = files.pathResolve(inputSourceArch.sourceRoot, relPath);
const hash = optimisticHashOrNull(absPath);
- const contents = optimisticReadFile(absPath);
- watchSet.addFile(absPath, hash);
+ if (!watchSet.hasFile(absPath)) {
+ watchSet.addFile(absPath, hash);
+ }
if (classification.type === "meteor-ignore") {
// Return after watching .meteorignore files but before adding them
@@ -801,6 +807,7 @@ async function runLinters({inputSourceArch, isopackCache, sources,
return;
}
+ const contents = optimisticReadFile(absPath);
const wrappedSource = {
relPath, contents, hash, fileOptions,
arch: inputSourceArch.arch,
diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts
index fe6d9639bc..7aff1ee0e6 100644
--- a/tools/isobuild/import-scanner.ts
+++ b/tools/isobuild/import-scanner.ts
@@ -45,6 +45,7 @@ import {
import { wrap } from "optimism";
const { compile: reifyCompile } = require("@meteorjs/reify/lib/compiler");
+const { parse: reifyAcornParse } = require("@meteorjs/reify/lib/parsers/acorn");
const { parse: reifyBabelParse } = require("@meteorjs/reify/lib/parsers/babel");
import Resolver, { Resolution } from "./resolver";
@@ -87,14 +88,32 @@ const reifyCompileWithCache = Profile("reifyCompileWithCache", wrap(function (
}
const isLegacy = isLegacyArch(bundleArch);
- let result = reifyCompile(stripHashBang(source), {
- parse: reifyBabelParse,
+ const reifyOptions = {
generateLetDeclarations: !isLegacy,
avoidModernSyntax: isLegacy,
enforceStrictMode: false,
dynamicImport: true,
ast: false,
- }).code;
+ };
+
+ let result;
+ try {
+ // First attempt: use Acorn
+ result = reifyCompile(stripHashBang(source), {
+ ...reifyOptions,
+ parse: reifyAcornParse,
+ }).code;
+ } catch (acornError) {
+ // Fallback: use Babel parser
+ // acorn may throw SyntaxError due to the lack of support for
+ // some features, but babel should still be able to parse the file
+ // For example, acorn don’t support JSX, only with acorn-jsx,
+ // but it isn’t included in Reify.
+ result = reifyCompile(stripHashBang(source), {
+ ...reifyOptions,
+ parse: reifyBabelParse,
+ }).code;
+ }
if (cacheFilePath) {
Promise.resolve().then(
@@ -977,7 +996,7 @@ export default class ImportScanner {
private async findImportedModuleIdentifiers(
file: File,
): Promise> {
- const fileHash = file.hash;
+ const fileHash = file.hash instanceof Promise ? await file.hash : file.hash;
if (IMPORT_SCANNER_CACHE.has(fileHash)) {
return IMPORT_SCANNER_CACHE.get(fileHash) as Record;
}
@@ -988,8 +1007,8 @@ export default class ImportScanner {
);
// there should always be file.hash, but better safe than sorry
- if (file.hash) {
- IMPORT_SCANNER_CACHE.set(file.hash, result);
+ if (fileHash) {
+ IMPORT_SCANNER_CACHE.set(fileHash, result);
}
return result;
diff --git a/tools/isobuild/isopack-cache.js b/tools/isobuild/isopack-cache.js
index 3ae3104dab..850db04166 100644
--- a/tools/isobuild/isopack-cache.js
+++ b/tools/isobuild/isopack-cache.js
@@ -83,19 +83,19 @@ export class IsopackCache {
// Wipe specific packages.
for (const packageName of packages) {
if (self.cacheDir) {
- await files.rm_recursive(self._isopackDir(packageName));
+ await files.rm_recursive_deferred(self._isopackDir(packageName));
}
if (self._pluginCacheDirRoot) {
- await files.rm_recursive(self._pluginCacheDirForPackage(packageName));
+ await files.rm_recursive_deferred(self._pluginCacheDirForPackage(packageName));
}
}
} else {
// Wipe all packages.
if (self.cacheDir) {
- await files.rm_recursive(self.cacheDir);
+ await files.rm_recursive_deferred(self.cacheDir);
}
if (self._pluginCacheDirRoot) {
- await files.rm_recursive(self._pluginCacheDirRoot);
+ await files.rm_recursive_deferred(self._pluginCacheDirRoot);
}
}
}
@@ -351,7 +351,7 @@ export class IsopackCache {
} else {
// Nope! Compile it again. Give it a fresh plugin cache.
if (pluginCacheDir) {
- await files.rm_recursive(pluginCacheDir);
+ await files.rm_recursive_deferred(pluginCacheDir);
files.mkdir_p(pluginCacheDir);
}
diff --git a/tools/isobuild/isopack.js b/tools/isobuild/isopack.js
index 99549c5d25..aa397e0a80 100644
--- a/tools/isobuild/isopack.js
+++ b/tools/isobuild/isopack.js
@@ -1,3 +1,5 @@
+import { getMeteorConfig } from "../tool-env/meteor-config";
+
var compiler = require('./compiler.js');
var archinfo = require('../utils/archinfo');
var _ = require('underscore');
@@ -514,6 +516,9 @@ Object.assign(Isopack.prototype, {
var Plugin = {
name: pluginName,
+ // Share the meteorConfig object as part of plugin API
+ getMeteorConfig: getMeteorConfig,
+
// 'extension' is a file extension without the separation dot
// (eg 'js', 'coffee', 'coffee.md')
//
diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js
index 35a929c083..2de961be63 100644
--- a/tools/isobuild/js-analyze.js
+++ b/tools/isobuild/js-analyze.js
@@ -4,6 +4,7 @@ import LRUCache from "lru-cache";
import { Profile } from '../tool-env/profile';
import Visitor from "@meteorjs/reify/lib/visitor.js";
import { findPossibleIndexes } from "@meteorjs/reify/lib/utils.js";
+import acorn from 'acorn';
const hasOwn = Object.prototype.hasOwnProperty;
const objToStr = Object.prototype.toString
@@ -15,7 +16,9 @@ function isRegExp(value) {
var AST_CACHE = new LRUCache({
max: Math.pow(2, 12),
length(ast) {
- return ast.loc.end.line;
+ // Estimate cached lines based on average length per character
+ const avgCharsPerLine = 40;
+ return Math.ceil(ast.end / avgCharsPerLine);
}
});
@@ -28,20 +31,32 @@ function tryToParse(source, hash) {
let ast;
try {
Profile.time('jsAnalyze.parse', () => {
- ast = parse(source, {
- strictMode: false,
- sourceType: 'module',
- allowImportExportEverywhere: true,
- allowReturnOutsideFunction: true,
- allowUndeclaredExports: true,
- plugins: [
- // Only plugins for stage 3 features are enabled
- // Enabling some plugins significantly affects parser performance
- 'importAttributes',
- 'explicitResourceManagement',
- 'decorators'
- ]
- });
+ try {
+ ast = acorn.parse(source, {
+ ecmaVersion: 'latest',
+ sourceType: 'script',
+ allowAwaitOutsideFunction: true,
+ allowImportExportEverywhere: true,
+ allowReturnOutsideFunction: true,
+ allowHashBang: true,
+ checkPrivateFields: false,
+ });
+ } catch (error) {
+ ast = parse(source, {
+ strictMode: false,
+ sourceType: 'module',
+ allowImportExportEverywhere: true,
+ allowReturnOutsideFunction: true,
+ allowUndeclaredExports: true,
+ plugins: [
+ // Only plugins for stage 3 features are enabled
+ // Enabling some plugins significantly affects parser performance
+ 'importAttributes',
+ 'explicitResourceManagement',
+ 'decorators'
+ ]
+ });
+ }
});
} catch (e) {
if (typeof e.loc === 'object') {
diff --git a/tools/isobuild/meteor-npm.js b/tools/isobuild/meteor-npm.js
index f051abc634..d1ebe42341 100644
--- a/tools/isobuild/meteor-npm.js
+++ b/tools/isobuild/meteor-npm.js
@@ -87,7 +87,7 @@ meteorNpm.updateDependencies = async function (packageName,
// It didn't exist, which is exactly what we wanted.
return false;
}
- await files.rm_recursive(newPackageNpmDir);
+ await files.rm_recursive_deferred(newPackageNpmDir);
return false;
}
@@ -102,7 +102,7 @@ meteorNpm.updateDependencies = async function (packageName,
// proceed.
if (files.exists(packageNpmDir) &&
! files.exists(files.pathJoin(packageNpmDir, 'npm-shrinkwrap.json'))) {
- await files.rm_recursive(packageNpmDir);
+ await files.rm_recursive_deferred(packageNpmDir);
}
// with the changes on npm 8, where there were changes to how the packages metadata is given
@@ -114,7 +114,7 @@ meteorNpm.updateDependencies = async function (packageName,
files.pathJoin(packageNpmDir, 'npm-shrinkwrap.json')
));
if (shrinkwrap.lockfileVersion !== LOCK_FILE_VERSION) {
- await files.rm_recursive(packageNpmDir);
+ await files.rm_recursive_deferred(packageNpmDir);
}
} catch (e) {}
}
@@ -143,7 +143,7 @@ meteorNpm.updateDependencies = async function (packageName,
throw e;
} finally {
if (files.exists(newPackageNpmDir)) {
- await files.rm_recursive(newPackageNpmDir);
+ await files.rm_recursive_deferred(newPackageNpmDir);
}
tmpDirs = _.without(tmpDirs, newPackageNpmDir);
}
@@ -384,7 +384,7 @@ Profile("meteorNpm.rebuildIfNonPortable", async function (nodeModulesDir) {
const rebuildResult = await runNpmCommand(getRebuildArgs(), tempDir);
if (! rebuildResult.success) {
buildmessage.error(rebuildResult.error);
- await files.rm_recursive(tempDir);
+ await files.rm_recursive_deferred(tempDir);
return false;
}
@@ -420,7 +420,7 @@ Profile("meteorNpm.rebuildIfNonPortable", async function (nodeModulesDir) {
await files.renameDirAlmostAtomically(tempPkgDirs[pkgPath], pkgPath);
}
- await files.rm_recursive(tempDir);
+ await files.rm_recursive_deferred(tempDir);
return true;
});
@@ -644,7 +644,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
}
if (oldNodeVersion !== currentNodeCompatibilityVersion()) {
- await files.rm_recursive(nodeModulesDir);
+ await files.rm_recursive_deferred(nodeModulesDir);
}
}
diff --git a/tools/meteor-services/stats.js b/tools/meteor-services/stats.js
index f324df7a34..c9c8439d72 100644
--- a/tools/meteor-services/stats.js
+++ b/tools/meteor-services/stats.js
@@ -22,7 +22,7 @@ var packageList = function (projectContext) {
name: name,
version: info.version,
local: info.kind === 'local',
- direct: !! projectContext.projectConstraintsFile.getConstraint(name)
+ direct: !!projectContext.projectConstraintsFile.getConstraint(name)
});
});
return versions;
@@ -39,7 +39,8 @@ var recordPackages = async function (options) {
// Before doing anything, look at the app's dependencies to see if the
// opt-out package is there; if present, we don't record any stats.
var packages = packageList(options.projectContext);
- if (_.findWhere(packages, { name: OPT_OUT_PACKAGE_NAME })) {
+ if (_.findWhere(packages, { name: OPT_OUT_PACKAGE_NAME }) ||
+ process.env.DO_NOT_TRACK) {
// Print some output for the 'report-stats' self-test.
if (process.env.METEOR_PACKAGE_STATS_TEST_OUTPUT) {
process.stdout.write("PACKAGE STATS NOT SENT\n");
@@ -86,9 +87,9 @@ var recordPackages = async function (options) {
}
var result = await conn.call("recordAppPackages",
- appIdentifier,
- packages,
- details);
+ appIdentifier,
+ packages,
+ details);
// If the stats server sent us a new session, save it for use on
// subsequent requests.
@@ -112,7 +113,7 @@ var recordPackages = async function (options) {
var logErrorIfInCheckout = function (err) {
if ((Console.isInteractive() && files.inCheckout())
- || process.env.METEOR_PACKAGE_STATS_TEST_OUTPUT) {
+ || process.env.METEOR_PACKAGE_STATS_TEST_OUTPUT) {
Console.warn("Failed to record package usage.");
Console.warn(
"(This error is hidden when you are not running Meteor from a",
@@ -145,8 +146,8 @@ var getPackagesForAppIdInTest = function (projectContext) {
var connectToPackagesStatsServer = async function () {
const sc = new ServiceConnection(
- config.getPackageStatsServerUrl(),
- {_dontPrintErrors: true}
+ config.getPackageStatsServerUrl(),
+ { _dontPrintErrors: true }
);
await sc.init();
diff --git a/tools/node-process-warnings.js b/tools/node-process-warnings.js
new file mode 100644
index 0000000000..9dbc7919dd
--- /dev/null
+++ b/tools/node-process-warnings.js
@@ -0,0 +1,22 @@
+const originalEmitWarning = process.emitWarning;
+
+process.emitWarning = function (message) {
+ /*
+ * A warning was introduced in Node 22:
+ *
+ * "The `punycode` module is deprecated. Please use a userland alternative instead."
+ *
+ * The problem is that punycode is deeply integrated in the Node system. It's not a
+ * simple direct dependency.
+ *
+ * Check these issues for more details:
+ * https://github.com/mathiasbynens/punycode.js/issues/137
+ * https://stackoverflow.com/questions/68774489/punycode-is-deprecated-in-npm-what-should-i-replace-it-with/78946745
+ *
+ * This warning was, besides being annoying, breaking our tests.
+ */
+ if (message.includes("punycode")) {
+ return;
+ }
+ return originalEmitWarning(message);
+};
diff --git a/tools/packaging/catalog/catalog-remote.js b/tools/packaging/catalog/catalog-remote.js
index e40be5086c..85b0fefc12 100644
--- a/tools/packaging/catalog/catalog-remote.js
+++ b/tools/packaging/catalog/catalog-remote.js
@@ -441,9 +441,9 @@ Object.assign(Table.prototype, {
return "(" + _.times(n, function () { return "?" }).join(",") + ")";
},
- find: async function (txn, id) {
+ find: async function (db, id) {
var self = this;
- var rows = await txn.query(self._selectQuery, [ id ]);
+ var rows = await db._query(self._selectQuery, [ id ]);
if (rows.length !== 0) {
if (rows.length !== 1) {
throw new Error("Corrupt database (PK violation)");
@@ -813,23 +813,23 @@ Object.assign(RemoteCatalog.prototype, {
// track, sorted by their orderKey. Returns the empty array if the release
// track does not exist or does not have any recommended versions.
getSortedRecommendedReleaseRecords: async function (track, laterThanOrderKey) {
- var self = this;
- // XXX releaseVersions content objects are kinda big; if we put
- // 'recommended' and 'orderKey' in their own columns this could be faster
- var result = await self._contentQuery(
- "SELECT content FROM releaseVersions WHERE track=?", track);
+ const hasMinKey = laterThanOrderKey != null;
- var recommended = _.filter(result, function (v) {
- if (!v.recommended)
- return false;
- return !laterThanOrderKey || v.orderKey > laterThanOrderKey;
- });
+ // Always use JSON1 to filter & sort directly in SQL
+ const sql = `
+ SELECT content
+ FROM releaseVersions
+ WHERE track = ?
+ AND json_extract(content, '$.recommended') = 1
+ ${hasMinKey ? "AND json_extract(content, '$.orderKey') > ?" : ""}
+ ORDER BY json_extract(content, '$.orderKey') DESC
+ `;
+ const params = hasMinKey
+ ? [track, laterThanOrderKey]
+ : [track];
- var recSort = _.sortBy(recommended, function (rec) {
- return rec.orderKey;
- });
- recSort.reverse();
- return recSort;
+ // _contentQuery will JSON.parse(content) for you
+ return this._contentQuery(sql, params);
},
// Given a release track, returns all version records for this track.
@@ -893,9 +893,7 @@ Object.assign(RemoteCatalog.prototype, {
// No JSON parsing is performed.
_columnsQuery: async function (query, params) {
var self = this;
- var rows = await self.db.runInTransaction(function (txn) {
- return txn.query(query, params);
- });
+ var rows = await self.db._query(query, params);
return rows;
},
@@ -946,9 +944,7 @@ Object.assign(RemoteCatalog.prototype, {
getMetadata: async function(key) {
var self = this;
- var row = await self.db.runInTransaction(function (txn) {
- return self.tableMetadata.find(txn, key);
- });
+ var row = await self.tableMetadata.find(self.db, key);
if (row) {
return JSON.parse(row['content']);
}
@@ -970,9 +966,7 @@ Object.assign(RemoteCatalog.prototype, {
shouldShowBanner: async function (releaseName, bannerDate) {
var self = this;
- var row = await self.db.runInTransaction(function (txn) {
- return self.tableBannersShown.find(txn, releaseName);
- });
+ var row = await self.tableBannersShown.find(self.db, releaseName);
// We've never printed a banner for this release.
if (! row)
return true;
diff --git a/tools/packaging/package-client.js b/tools/packaging/package-client.js
index 0a10007e06..c79a2463df 100644
--- a/tools/packaging/package-client.js
+++ b/tools/packaging/package-client.js
@@ -466,7 +466,7 @@ exports.handlePackageServerConnectionError = function (error) {
};
-// Update the package metdata in the server catalog. Chane the docs,
+// Update the package metadata in the server catalog. Change the docs,
// descriptions and the Git URL to new values.
//
// options:
@@ -537,7 +537,7 @@ exports.updatePackageMetadata = async function (options) {
// Upload the new Readme.
await buildmessage.enterJob('uploading documentation', async function () {
- var readmePath = saveReadmeToTmp(readmeInfo);
+ var readmePath = await saveReadmeToTmp(readmeInfo);
var uploadInfo =
await callPackageServerBM(conn, "createReadme", versionIdentifier);
if (!uploadInfo) {
diff --git a/tools/project-context.js b/tools/project-context.js
index 60f5ae794a..f114f3a38a 100644
--- a/tools/project-context.js
+++ b/tools/project-context.js
@@ -1,3 +1,4 @@
+import { normalizeModernConfig, setMeteorConfig } from "./tool-env/meteor-config";
var assert = require("assert");
var _ = require('underscore');
@@ -318,7 +319,7 @@ Object.assign(ProjectContext.prototype, {
*
* @return {Promise<*|undefined>}
*/
- readProjectMetadata: function () {
+ readProjectMetadata: async function () {
// don't generate a profiling report for this stage (Profile.run),
// because all we do here is read a handful of files.
return this._completeStagesThrough(STAGE.READ_PROJECT_METADATA);
@@ -328,7 +329,7 @@ Object.assign(ProjectContext.prototype, {
* @return {Promise<*|undefined>}
*/
initializeCatalog: function () {
- return Profile.run('ProjectContext initializeCatalog', () => {
+ return Profile.run('ProjectContext initializeCatalog', async () => {
return this._completeStagesThrough(STAGE.INITIALIZE_CATALOG);
});
},
@@ -337,7 +338,7 @@ Object.assign(ProjectContext.prototype, {
* @return {Promise<*|undefined>}
*/
resolveConstraints: function () {
- return Profile.run('ProjectContext resolveConstraints', () => {
+ return Profile.run('ProjectContext resolveConstraints', async () => {
return this._completeStagesThrough(STAGE.RESOLVE_CONSTRAINTS);
});
},
@@ -347,7 +348,7 @@ Object.assign(ProjectContext.prototype, {
* @return {Promise<*|undefined>}
*/
downloadMissingPackages: function () {
- return Profile.run('ProjectContext downloadMissingPackages', () => {
+ return Profile.run('ProjectContext downloadMissingPackages', async () => {
return this._completeStagesThrough(STAGE.DOWNLOAD_MISSING_PACKAGES);
});
},
@@ -356,7 +357,7 @@ Object.assign(ProjectContext.prototype, {
* @return {Promise<*|undefined>}
*/
buildLocalPackages: function () {
- return Profile.run('ProjectContext buildLocalPackages', () => {
+ return Profile.run('ProjectContext buildLocalPackages', async () => {
return this._completeStagesThrough(STAGE.BUILD_LOCAL_PACKAGES);
});
},
@@ -365,7 +366,7 @@ Object.assign(ProjectContext.prototype, {
* @return {Promise<*|undefined>}
*/
saveChangedMetadata: function () {
- return Profile.run('ProjectContext saveChangedMetadata', () => {
+ return Profile.run('ProjectContext saveChangedMetadata', async () => {
return this._completeStagesThrough(STAGE.SAVE_CHANGED_METADATA);
});
},
@@ -376,7 +377,7 @@ Object.assign(ProjectContext.prototype, {
prepareProjectForBuild: function () {
// This is the same as saveChangedMetadata, but if we insert stages after
// that one it will continue to mean "fully finished".
- return Profile.run('ProjectContext prepareProjectForBuild', () => {
+ return Profile.run('ProjectContext prepareProjectForBuild', async () => {
return this._completeStagesThrough(STAGE.SAVE_CHANGED_METADATA);
});
},
@@ -492,6 +493,8 @@ Object.assign(ProjectContext.prototype, {
self.meteorConfig = new MeteorConfig({
appDirectory: self.projectDir,
});
+ self.meteorConfig._ensureInitialized();
+
if (buildmessage.jobHasMessages()) {
return;
}
@@ -1217,7 +1220,16 @@ Object.assign(exports.ProjectConstraintsFile.prototype, {
constraint: constraintToAdd,
trailingSpaceAndComment: ''
};
- self._constraintLines.push(lineRecord);
+ if (constraintToAdd.package === 'npm-mongo-legacy') {
+ const mongoIdx = self._constraintLines.findIndex(lr => lr.constraint && lr.constraint.package === 'mongo');
+ if (mongoIdx > -1) {
+ self._constraintLines.splice(mongoIdx, 0, lineRecord);
+ } else {
+ self._constraintLines.push(lineRecord);
+ }
+ } else {
+ self._constraintLines.push(lineRecord);
+ }
self._constraintMap[constraintToAdd.package] = lineRecord;
self._modified = true;
return;
@@ -1664,7 +1676,7 @@ Object.assign(exports.ReleaseFile.prototype, {
if (this.isCheckout()) {
// Only create .meteor/local/dev_bundle if .meteor/release refers to
// an actual release, and remove it otherwise.
- await files.rm_recursive(devBundleLink);
+ await files.rm_recursive_deferred(devBundleLink);
return;
}
@@ -1811,6 +1823,13 @@ export class MeteorConfig {
},
}),
} : this._config;
+ const modernForced = JSON.parse(process.env.METEOR_MODERN || "false");
+ // Reinitialize meteorConfig globally for project context
+ // Updates config when package.json changes trigger rebuilds
+ setMeteorConfig({
+ ...(this._config || {}),
+ modern: normalizeModernConfig(modernForced || this._config?.modern || false),
+ });
return this._config;
}
@@ -1818,16 +1837,14 @@ export class MeteorConfig {
// General utility for querying the "meteor" section of package.json.
// TODO Implement an API for setting these values?
get(...keys) {
- let config = this._ensureInitialized();
- if (config) {
- keys.every(key => {
- if (config && _.has(config, key)) {
- config = config[key];
- return true;
- }
- });
- return config;
- }
+ const config = this._ensureInitialized();
+ if (!config) return undefined;
+
+ return keys.reduce((cur, key) => {
+ return (cur != null && _.has(cur, key))
+ ? cur[key]
+ : undefined;
+ }, config);
}
getNodeModulesToRecompileByArch() {
diff --git a/tools/runners/run-app.js b/tools/runners/run-app.js
index 9f5bb79ecd..9d92b57780 100644
--- a/tools/runners/run-app.js
+++ b/tools/runners/run-app.js
@@ -1,4 +1,3 @@
-var _ = require('underscore');
var files = require('../fs/files');
var watch = require('../fs/watch');
var bundler = require('../isobuild/bundler.js');
@@ -11,15 +10,15 @@ var Profile = require('../tool-env/profile').Profile;
var release = require('../packaging/release.js');
import { pluginVersionsFromStarManifest } from '../cordova/index.js';
import { closeAllWatchers } from "../fs/safe-watcher";
-import { eachline } from "../utils/eachline";
import { loadIsopackage } from '../tool-env/isopackets.js';
+import { eachline } from "../utils/eachline";
// Parse out s as if it were a bash command line.
var bashParse = function (s) {
if (s.search("\"") !== -1 || s.search("'") !== -1) {
throw new Error("Meteor cannot currently handle quoted SERVER_NODE_OPTIONS");
}
- return _.without(s.split(/\s+/), '');
+ return s.split(/\s+/).filter(Boolean);
};
var getNodeOptionsFromEnvironment = function () {
@@ -238,7 +237,7 @@ Object.assign(AppProcess.prototype, {
files.pathJoin(self.bundlePath, 'main.js'));
// Setting options
- var opts = _.clone(self.nodeOptions);
+ var opts = JSON.parse(JSON.stringify(self.nodeOptions));
if (self.inspect) {
// Always use --inspect rather than --inspect-brk, even when
@@ -251,6 +250,8 @@ Object.assign(AppProcess.prototype, {
opts.push("--inspect=" + self.inspect.port);
}
+ opts.push(`--require=${files.convertToOSPath(files.pathJoin(__dirname, '../node-process-warnings.js'))}`)
+
opts.push(entryPoint);
// Call node
@@ -575,7 +576,7 @@ Object.assign(AppRunner.prototype, {
if (self.recordPackageUsage) {
// Maybe this doesn't need to be awaited for?
- await stats.recordPackages({
+ stats.recordPackages({
what: "sdk.run",
projectContext: self.projectContext
});
@@ -912,7 +913,7 @@ Object.assign(AppRunner.prototype, {
var oldPromise = self.runPromise = self._makePromise("run");
- await refreshClient();
+ refreshClient();
// Establish a watcher on the new files.
setupClientWatcher();
diff --git a/tools/runners/run-mongo.js b/tools/runners/run-mongo.js
index ea319eb0de..c9a7de7334 100644
--- a/tools/runners/run-mongo.js
+++ b/tools/runners/run-mongo.js
@@ -1,3 +1,4 @@
+import { loadIsopackage } from '../tool-env/isopackets.js';
import { MongoExitCodes } from '../utils/mongo-exit-codes';
var files = require('../fs/files');
var utils = require('../utils/utils.js');
@@ -5,7 +6,6 @@ var fiberHelpers = require('../utils/fiber-helpers.js');
var runLog = require('./run-log.js');
var child_process = require('child_process');
var _ = require('underscore');
-import { loadIsopackage } from '../tool-env/isopackets.js';
var Console = require('../console/console.js').Console;
// Given a Mongo URL, open an interactive Mongo shell on this terminal
@@ -28,6 +28,17 @@ function spawnMongod(mongodPath, port, dbPath, replSetName) {
mongodPath = files.convertToOSPath(mongodPath);
dbPath = files.convertToOSPath(dbPath);
+ // Because we're spawning the process with shell: true on win32, the
+ // command string and args array are effectively concatenated to
+ // a single string and passed into the shell. If any of those paths
+ // contain spaces, these will get broken up on white-spaces and treated
+ // as separate tokens. If they are quoted, they will be parsed
+ // correctly by the shell.
+ if (process.platform === 'win32') {
+ mongodPath = '"' + mongodPath + '"'
+ dbPath = '"' + dbPath + '"'
+ }
+
let args = [
// nb: cli-test.sh and findMongoPids make strong assumptions about the
// order of the arguments! Check them before changing any arguments.
@@ -51,11 +62,6 @@ function spawnMongod(mongodPath, port, dbPath, replSetName) {
args.push('--storageEngine', 'mmapv1', '--smallfiles');
}
- // run with rosetta on mac m1
- if (process.platform === 'darwin' && process.arch === 'arm64') {
- args = ['-x86_64', mongodPath, ...args];
- mongodPath = 'arch';
- }
return child_process.spawn(mongodPath, args, {
// Apparently in some contexts, Mongo crashes if your locale isn't set up
// right. I wasn't able to reproduce it, but many people on #4019
@@ -448,11 +454,7 @@ var launchMongo = async function(options) {
var yieldingMethod = async function(object, methodName, ...args) {
return await Promise.race([
stopPromise,
- new Promise((resolve, reject) => {
- object[methodName](...args, (err, res) => {
- err ? reject(err) : resolve(res);
- });
- }),
+ object[methodName](...args),
]);
};
@@ -472,7 +474,7 @@ var launchMongo = async function(options) {
if (options.multiple) {
// This is only for testing, so we're OK with incurring the replset
// setup on each startup.
- await files.rm_recursive(dbPath);
+ await files.rm_recursive_deferred(dbPath);
files.mkdir_p(dbPath, 0o755);
} else if (portFile) {
var portFileExists = false;
diff --git a/tools/static-assets/server/boot.js b/tools/static-assets/server/boot.js
index e6d0a7eb89..43c2668bde 100644
--- a/tools/static-assets/server/boot.js
+++ b/tools/static-assets/server/boot.js
@@ -498,12 +498,10 @@ var runMain = Profile("Run main()", async function () {
global.__METEOR_ASYNC_LOCAL_STORAGE = new AsyncLocalStorage();
}
- await Profile.run('Server startup', function() {
- return global.__METEOR_ASYNC_LOCAL_STORAGE.run({}, async () => {
- await loadServerBundles();
- await callStartupHooks();
- await runMain();
- });
+ await Profile.run('Server startup', async function() {
+ await loadServerBundles();
+ await callStartupHooks();
+ await runMain();
});
})().catch(e => {
console.log('error on boot.js', e )
diff --git a/tools/static-assets/server/npm-rebuild.js b/tools/static-assets/server/npm-rebuild.js
index e0a5624d3c..ca1a3be7bf 100644
--- a/tools/static-assets/server/npm-rebuild.js
+++ b/tools/static-assets/server/npm-rebuild.js
@@ -28,11 +28,10 @@ var binDir = path.dirname(process.execPath);
process.env.PATH = binDir + path.delimiter + process.env.PATH;
var npmCmd = "npm";
+var shell = false;
if (process.platform === "win32") {
- var npmCmdPath = path.join(binDir, "npm.cmd");
- if (fs.existsSync(npmCmdPath)) {
- npmCmd = npmCmdPath;
- }
+ npmCmd = "npm.cmd";
+ shell = true;
}
function rebuild(i) {
@@ -41,7 +40,8 @@ function rebuild(i) {
if (! dir) {
// Print Node/V8/etc. versions for diagnostic purposes.
spawn(npmCmd, ["version", "--json"], {
- stdio: "inherit"
+ stdio: "inherit",
+ shell,
});
return;
@@ -49,7 +49,8 @@ function rebuild(i) {
spawn(npmCmd, rebuildArgs, {
cwd: path.join(__dirname, dir),
- stdio: "inherit"
+ stdio: "inherit",
+ shell,
}).on("exit", function (code) {
if (code !== 0) {
process.exit(code);
diff --git a/tools/static-assets/server/runtime.js b/tools/static-assets/server/runtime.js
index 2b0f800940..3c15a6f9d1 100644
--- a/tools/static-assets/server/runtime.js
+++ b/tools/static-assets/server/runtime.js
@@ -47,14 +47,27 @@ module.exports = function enable ({ cachePath, createLoader = true } = {}) {
};
const reifyVersion = require("@meteorjs/reify/package.json").version;
+ const reifyAcornParse = require("@meteorjs/reify/lib/parsers/acorn").parse;
const reifyBabelParse = require("@meteorjs/reify/lib/parsers/babel").parse;
const reifyCompile = require("@meteorjs/reify/lib/compiler").compile;
function compileContent (content) {
let identical = true;
+ let result;
try {
- const result = reifyCompile(content, {
+ result = reifyCompile(content, {
+ parse: reifyAcornParse,
+ generateLetDeclarations: false,
+ ast: false,
+ });
+ if (!result.identical) {
+ identical = false;
+ content = result.code;
+ }
+ } catch (acornError) {
+ // Fallback: Babel
+ result = reifyCompile(content, {
parse: reifyBabelParse,
generateLetDeclarations: false,
ast: false,
@@ -63,9 +76,9 @@ module.exports = function enable ({ cachePath, createLoader = true } = {}) {
identical = false;
content = result.code;
}
- } finally {
- return { content, identical };
}
+
+ return { content, identical };
}
const _compile = Mp._compile;
diff --git a/tools/static-assets/skel-apollo/package.json b/tools/static-assets/skel-apollo/package.json
index 9bf478f474..8af983a7cb 100644
--- a/tools/static-assets/skel-apollo/package.json
+++ b/tools/static-assets/skel-apollo/package.json
@@ -11,8 +11,9 @@
"@apollo/client": "^3.9.2",
"@apollo/server": "^4.10.0",
"@babel/runtime": "^7.23.9",
+ "@swc/helpers": "^0.5.17",
"graphql": "^16.8.1",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -21,6 +22,7 @@
"client": "client/main.jsx",
"server": "server/main.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-bare/package.json b/tools/static-assets/skel-bare/package.json
index 7db49f3411..f2cb0b71a7 100644
--- a/tools/static-assets/skel-bare/package.json
+++ b/tools/static-assets/skel-bare/package.json
@@ -6,6 +6,6 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
}
}
diff --git a/tools/static-assets/skel-blaze/package.json b/tools/static-assets/skel-blaze/package.json
index 2761af2093..e197964537 100644
--- a/tools/static-assets/skel-blaze/package.json
+++ b/tools/static-assets/skel-blaze/package.json
@@ -9,14 +9,16 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
+ "@swc/helpers": "^0.5.17",
"jquery": "^3.7.1",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-chakra-ui/package.json b/tools/static-assets/skel-chakra-ui/package.json
index ce630e2918..99b3e13d76 100644
--- a/tools/static-assets/skel-chakra-ui/package.json
+++ b/tools/static-assets/skel-chakra-ui/package.json
@@ -9,13 +9,14 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
+ "@swc/helpers": "^0.5.17",
"@chakra-ui/icons": "^1.1.7",
"@chakra-ui/react": "^1.8.8",
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@react-icons/all-files": "^4.1.0",
"framer-motion": "^6.4.2",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -24,6 +25,7 @@
"client": "client/main.jsx",
"server": "server/main.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-full/package.json b/tools/static-assets/skel-full/package.json
index 28d1d84d2e..1abdbdd683 100644
--- a/tools/static-assets/skel-full/package.json
+++ b/tools/static-assets/skel-full/package.json
@@ -7,10 +7,18 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
+ "@swc/helpers": "^0.5.17",
"jquery": "^3.7.1",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"devDependencies": {
"chai": "^4.2.0"
+ },
+ "meteor": {
+ "mainModule": {
+ "client": "client/main.js",
+ "server": "server/main.js"
+ },
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-minimal/.meteor/packages b/tools/static-assets/skel-minimal/.meteor/packages
index d0998cd7ad..9f6770b9e9 100644
--- a/tools/static-assets/skel-minimal/.meteor/packages
+++ b/tools/static-assets/skel-minimal/.meteor/packages
@@ -13,6 +13,7 @@ ecmascript # Enable ECMAScript2015+ syntax in app code
typescript # Enable TypeScript syntax in .ts and .tsx modules
shell-server # Server-side component of the `meteor shell` command
webapp # Serves a Meteor app over HTTP
+ddp # The protocol and client/server libraries that Meteor uses to send data
server-render # Support for server-side rendering
hot-module-replacement # Rebuilds the client if there is a change on the client without restarting the server
~prototype~
diff --git a/tools/static-assets/skel-minimal/package.json b/tools/static-assets/skel-minimal/package.json
index 187111707d..74d5a9b271 100644
--- a/tools/static-assets/skel-minimal/package.json
+++ b/tools/static-assets/skel-minimal/package.json
@@ -9,13 +9,15 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10"
+ "@swc/helpers": "^0.5.17",
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json
index 9f5e7f9bc8..ee33ff775c 100644
--- a/tools/static-assets/skel-react/package.json
+++ b/tools/static-assets/skel-react/package.json
@@ -9,7 +9,8 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "@swc/helpers": "^0.5.17",
+ "meteor-node-stubs": "^1.2.12",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -18,6 +19,7 @@
"client": "client/main.jsx",
"server": "server/main.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-solid/.meteor/packages b/tools/static-assets/skel-solid/.meteor/packages
index f00629b1a1..665679a90c 100644
--- a/tools/static-assets/skel-solid/.meteor/packages
+++ b/tools/static-assets/skel-solid/.meteor/packages
@@ -19,4 +19,4 @@ hot-module-replacement # Update client in development without reloading the pag
~prototype~
static-html # Define static page content in .html files
-jorgenvatle:vite-bundler
+jorgenvatle:vite
diff --git a/tools/static-assets/skel-solid/client/entry-meteor.js b/tools/static-assets/skel-solid/client/entry-meteor.js
new file mode 100644
index 0000000000..0ec3ddc121
--- /dev/null
+++ b/tools/static-assets/skel-solid/client/entry-meteor.js
@@ -0,0 +1,14 @@
+/**
+ * Entrypoint for the Meteor client
+ *
+ * Generally, this file can be left empty. Vite will add imports for
+ * lazy-loaded Meteor packages to this file to ensure they aren't omitted from
+ * the final production bundle.
+ *
+ * Use ./main.js as the primary entrypoint for your client code to take full
+ * advantage of Vite's plugin and build system.
+ *
+ * This can also be a good place to put code that you don't want Vite to
+ * process, for example, if you run into a compatibility issue or need to use
+ * nested imports which Vite doesn't support.
+ */
\ No newline at end of file
diff --git a/tools/static-assets/skel-solid/client/main.js b/tools/static-assets/skel-solid/client/main.js
new file mode 100644
index 0000000000..44bcc64d55
--- /dev/null
+++ b/tools/static-assets/skel-solid/client/main.js
@@ -0,0 +1 @@
+import '../imports/ui/main';
\ No newline at end of file
diff --git a/tools/static-assets/skel-solid/client/main.jsx b/tools/static-assets/skel-solid/client/main.jsx
deleted file mode 100644
index 97d382a9bd..0000000000
--- a/tools/static-assets/skel-solid/client/main.jsx
+++ /dev/null
@@ -1 +0,0 @@
-// main entry point is in imports/ui/main.jsx
diff --git a/tools/static-assets/skel-solid/client/main.css b/tools/static-assets/skel-solid/imports/ui/main.css
similarity index 100%
rename from tools/static-assets/skel-solid/client/main.css
rename to tools/static-assets/skel-solid/imports/ui/main.css
diff --git a/tools/static-assets/skel-solid/imports/ui/main.jsx b/tools/static-assets/skel-solid/imports/ui/main.jsx
index 99eb6c43d7..044f1aee69 100644
--- a/tools/static-assets/skel-solid/imports/ui/main.jsx
+++ b/tools/static-assets/skel-solid/imports/ui/main.jsx
@@ -2,6 +2,7 @@
import { render } from 'solid-js/web';
import { App } from './App';
import { Meteor } from "meteor/meteor";
+import './main.css';
Meteor.startup(() => {
render(() => , document.getElementById('root'));
diff --git a/tools/static-assets/skel-solid/package.json b/tools/static-assets/skel-solid/package.json
index 7b575928e5..17401d9c1b 100644
--- a/tools/static-assets/skel-solid/package.json
+++ b/tools/static-assets/skel-solid/package.json
@@ -9,21 +9,24 @@
},
"dependencies": {
"@babel/runtime": "^7.23.9",
- "meteor-node-stubs": "^1.2.10",
- "solid-js": "^1.8.15"
+ "@swc/helpers": "^0.5.17",
+ "meteor-node-stubs": "^1.2.12",
+ "picocolors": "^1.1.1",
+ "solid-js": "^1.9.4"
},
"meteor": {
"mainModule": {
- "client": "client/main.jsx",
- "server": "server/main.js"
+ "client": "client/entry-meteor.js",
+ "server": "server/entry-meteor.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
},
"devDependencies": {
"babel-preset-solid": "^1.8.15",
- "meteor-vite": "^1.10.2",
- "vite": "^4.5.2",
- "vite-plugin-solid": "^2.10.1",
- "vite-plugin-solid-svg": "^0.8.0"
+ "meteor-vite": "^3.2.1",
+ "vite": "^6.0.11",
+ "vite-plugin-solid": "^2.11.0",
+ "vite-plugin-solid-svg": "^0.8.1"
}
}
diff --git a/tools/static-assets/skel-solid/server/entry-meteor.js b/tools/static-assets/skel-solid/server/entry-meteor.js
new file mode 100644
index 0000000000..8a066f8e94
--- /dev/null
+++ b/tools/static-assets/skel-solid/server/entry-meteor.js
@@ -0,0 +1,12 @@
+/**
+ * Entrypoint for the Meteor server
+ * Generally, this file can be left empty. Vite will add imports for your app's
+ * server bundle here during both development and production build.
+ *
+ * Use ./main.js as the primary entrypoint for your app to take full advantage
+ * of Vite's plugin and build system.
+ *
+ * This can also be a good place to put code that you don't want Vite to
+ * process, for example, if you run into a compatibility issue or need to use
+ * nested imports.
+ */
\ No newline at end of file
diff --git a/tools/static-assets/skel-solid/vite.config.js b/tools/static-assets/skel-solid/vite.config.js
deleted file mode 100644
index c49fa70b16..0000000000
--- a/tools/static-assets/skel-solid/vite.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from 'vite';
-import solidPlugin from 'vite-plugin-solid';
-import solidSvg from "vite-plugin-solid-svg";
-
-export default defineConfig({
- plugins: [solidPlugin(), solidSvg({
- defaultExport: 'component',
- })],
- meteor: {
- clientEntry: 'imports/ui/main.jsx',
- },
-});
diff --git a/tools/static-assets/skel-solid/vite.config.mjs b/tools/static-assets/skel-solid/vite.config.mjs
new file mode 100644
index 0000000000..e0215dbb10
--- /dev/null
+++ b/tools/static-assets/skel-solid/vite.config.mjs
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite';
+import solidPlugin from 'vite-plugin-solid';
+import solidSvg from "vite-plugin-solid-svg";
+import { meteor } from 'meteor-vite/plugin';
+
+export default defineConfig({
+ plugins: [
+ solidPlugin(),
+ solidSvg({ defaultExport: 'component' }),
+ meteor({
+ clientEntry: 'client/main.js',
+ serverEntry: 'server/main.js',
+ enableExperimentalFeatures: true,
+ stubValidation: {
+ ignorePackages: ['meteor/mongo'],
+ },
+ }),
+ ],
+});
diff --git a/tools/static-assets/skel-svelte/package.json b/tools/static-assets/skel-svelte/package.json
index 1be6779d00..0929124049 100644
--- a/tools/static-assets/skel-svelte/package.json
+++ b/tools/static-assets/skel-svelte/package.json
@@ -9,7 +9,8 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "@swc/helpers": "^0.5.17",
+ "meteor-node-stubs": "^1.2.12",
"svelte": "^3.59.2"
},
"devDependencies": {
@@ -27,6 +28,7 @@
]
}
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-tailwind/package.json b/tools/static-assets/skel-tailwind/package.json
index e738792743..73971202ba 100644
--- a/tools/static-assets/skel-tailwind/package.json
+++ b/tools/static-assets/skel-tailwind/package.json
@@ -9,8 +9,9 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
+ "@swc/helpers": "^0.5.17",
"autoprefixer": "^10.4.4",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"postcss": "^8.4.12",
"postcss-load-config": "^3.1.4",
"react": "^17.0.2",
@@ -22,6 +23,7 @@
"client": "client/main.jsx",
"server": "server/main.js"
},
- "testModule": "tests/main.js"
+ "testModule": "tests/main.js",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json
index 0746e8368a..77ff7934bf 100644
--- a/tools/static-assets/skel-typescript/package.json
+++ b/tools/static-assets/skel-typescript/package.json
@@ -9,13 +9,14 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "@swc/helpers": "^0.5.17",
+ "meteor-node-stubs": "^1.2.12",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/mocha": "^8.2.3",
- "@types/node": "^18.16.5",
+ "@types/node": "^22.10.6",
"@types/react": "^18.2.5",
"@types/react-dom": "^18.2.4",
"typescript": "^5.4.5"
@@ -25,6 +26,7 @@
"client": "client/main.tsx",
"server": "server/main.ts"
},
- "testModule": "tests/main.ts"
+ "testModule": "tests/main.ts",
+ "modern": true
}
}
diff --git a/tools/static-assets/skel-vue/.meteor/packages b/tools/static-assets/skel-vue/.meteor/packages
index 3ae6a18b5f..b5e90db564 100644
--- a/tools/static-assets/skel-vue/.meteor/packages
+++ b/tools/static-assets/skel-vue/.meteor/packages
@@ -18,6 +18,5 @@ shell-server # Server-side component of the `meteor shell` com
hot-module-replacement # Update client in development without reloading the page
static-html # Define static page content in .html files
-jorgenvatle:vite-bundler
+jorgenvatle:vite
~prototype~
-
diff --git a/tools/static-assets/skel-vue/client/entry-meteor.js b/tools/static-assets/skel-vue/client/entry-meteor.js
new file mode 100644
index 0000000000..0ec3ddc121
--- /dev/null
+++ b/tools/static-assets/skel-vue/client/entry-meteor.js
@@ -0,0 +1,14 @@
+/**
+ * Entrypoint for the Meteor client
+ *
+ * Generally, this file can be left empty. Vite will add imports for
+ * lazy-loaded Meteor packages to this file to ensure they aren't omitted from
+ * the final production bundle.
+ *
+ * Use ./main.js as the primary entrypoint for your client code to take full
+ * advantage of Vite's plugin and build system.
+ *
+ * This can also be a good place to put code that you don't want Vite to
+ * process, for example, if you run into a compatibility issue or need to use
+ * nested imports which Vite doesn't support.
+ */
\ No newline at end of file
diff --git a/tools/static-assets/skel-vue/client/main.css b/tools/static-assets/skel-vue/client/main.css
deleted file mode 100644
index b5c61c9567..0000000000
--- a/tools/static-assets/skel-vue/client/main.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
diff --git a/tools/static-assets/skel-vue/client/main.js b/tools/static-assets/skel-vue/client/main.js
index 97d382a9bd..403d8a2d2a 100644
--- a/tools/static-assets/skel-vue/client/main.js
+++ b/tools/static-assets/skel-vue/client/main.js
@@ -1 +1 @@
-// main entry point is in imports/ui/main.jsx
+import '../imports/ui/main'
\ No newline at end of file
diff --git a/tools/static-assets/skel-vue/imports/ui/App.vue b/tools/static-assets/skel-vue/imports/ui/App.vue
index 7a775391cb..19a68a1ea1 100644
--- a/tools/static-assets/skel-vue/imports/ui/App.vue
+++ b/tools/static-assets/skel-vue/imports/ui/App.vue
@@ -1,5 +1,5 @@
diff --git a/tools/static-assets/skel-vue/imports/ui/AppMenu.vue b/tools/static-assets/skel-vue/imports/ui/components/AppMenu.vue
similarity index 100%
rename from tools/static-assets/skel-vue/imports/ui/AppMenu.vue
rename to tools/static-assets/skel-vue/imports/ui/components/AppMenu.vue
diff --git a/tools/static-assets/skel-vue/imports/ui/Hello.vue b/tools/static-assets/skel-vue/imports/ui/components/Hello.vue
similarity index 100%
rename from tools/static-assets/skel-vue/imports/ui/Hello.vue
rename to tools/static-assets/skel-vue/imports/ui/components/Hello.vue
diff --git a/tools/static-assets/skel-vue/imports/ui/Info.vue b/tools/static-assets/skel-vue/imports/ui/components/Info.vue
similarity index 100%
rename from tools/static-assets/skel-vue/imports/ui/Info.vue
rename to tools/static-assets/skel-vue/imports/ui/components/Info.vue
diff --git a/tools/static-assets/skel-vue/imports/ui/main.css b/tools/static-assets/skel-vue/imports/ui/main.css
new file mode 100644
index 0000000000..a461c505f1
--- /dev/null
+++ b/tools/static-assets/skel-vue/imports/ui/main.css
@@ -0,0 +1 @@
+@import "tailwindcss";
\ No newline at end of file
diff --git a/tools/static-assets/skel-vue/imports/ui/main.js b/tools/static-assets/skel-vue/imports/ui/main.js
index e3500841ea..f568d94ebf 100644
--- a/tools/static-assets/skel-vue/imports/ui/main.js
+++ b/tools/static-assets/skel-vue/imports/ui/main.js
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'
import { createApp } from 'vue'
import { VueMeteor } from 'vue-meteor-tracker'
+import './main.css'
import App from './App.vue'
import { router } from './router'
diff --git a/tools/static-assets/skel-vue/imports/ui/router.js b/tools/static-assets/skel-vue/imports/ui/router.js
index b271e02d29..00ed2a51e1 100644
--- a/tools/static-assets/skel-vue/imports/ui/router.js
+++ b/tools/static-assets/skel-vue/imports/ui/router.js
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
-import Home from './Home.vue'
-import About from './About.vue'
+import Home from './views/Home.vue'
+import About from './views/About.vue'
export const router = createRouter({
history: createWebHistory(),
diff --git a/tools/static-assets/skel-vue/imports/ui/About.vue b/tools/static-assets/skel-vue/imports/ui/views/About.vue
similarity index 100%
rename from tools/static-assets/skel-vue/imports/ui/About.vue
rename to tools/static-assets/skel-vue/imports/ui/views/About.vue
diff --git a/tools/static-assets/skel-vue/imports/ui/Home.vue b/tools/static-assets/skel-vue/imports/ui/views/Home.vue
similarity index 60%
rename from tools/static-assets/skel-vue/imports/ui/Home.vue
rename to tools/static-assets/skel-vue/imports/ui/views/Home.vue
index 0473845661..38e3688bee 100644
--- a/tools/static-assets/skel-vue/imports/ui/Home.vue
+++ b/tools/static-assets/skel-vue/imports/ui/views/Home.vue
@@ -1,6 +1,6 @@
diff --git a/tools/static-assets/skel-vue/package.json b/tools/static-assets/skel-vue/package.json
index 3c1bedf120..e77cf9ea19 100644
--- a/tools/static-assets/skel-vue/package.json
+++ b/tools/static-assets/skel-vue/package.json
@@ -10,25 +10,26 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "@swc/helpers": "^0.5.17",
+ "meteor-node-stubs": "^1.2.12",
"vue": "^3.3.9",
"vue-meteor-tracker": "^3.0.0-beta.7",
"vue-router": "^4.2.5"
},
"meteor": {
+ "modern": true,
"mainModule": {
- "client": "client/main.js",
- "server": "server/main.js"
+ "client": "client/entry-meteor.js",
+ "server": "server/entry-meteor.js"
},
"testModule": "tests/main.js"
},
"devDependencies": {
"@types/meteor": "^2.9.7",
- "@vitejs/plugin-vue": "^3.2.0",
- "autoprefixer": "^10.4.16",
- "meteor-vite": "^1.10.3",
- "postcss": "^8.4.31",
- "tailwindcss": "^3.3.5",
- "vite": "^3.2.7"
+ "@tailwindcss/vite": "^4.1.11",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "meteor-vite": "^3.2.1",
+ "tailwindcss": "^4.1.11",
+ "vite": "^6.0.11"
}
}
diff --git a/tools/static-assets/skel-vue/postcss.config.js b/tools/static-assets/skel-vue/postcss.config.js
deleted file mode 100644
index 33ad091d26..0000000000
--- a/tools/static-assets/skel-vue/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
diff --git a/tools/static-assets/skel-vue/server/entry-meteor.js b/tools/static-assets/skel-vue/server/entry-meteor.js
new file mode 100644
index 0000000000..8a066f8e94
--- /dev/null
+++ b/tools/static-assets/skel-vue/server/entry-meteor.js
@@ -0,0 +1,12 @@
+/**
+ * Entrypoint for the Meteor server
+ * Generally, this file can be left empty. Vite will add imports for your app's
+ * server bundle here during both development and production build.
+ *
+ * Use ./main.js as the primary entrypoint for your app to take full advantage
+ * of Vite's plugin and build system.
+ *
+ * This can also be a good place to put code that you don't want Vite to
+ * process, for example, if you run into a compatibility issue or need to use
+ * nested imports.
+ */
\ No newline at end of file
diff --git a/tools/static-assets/skel-vue/tailwind.config.js b/tools/static-assets/skel-vue/tailwind.config.js
deleted file mode 100644
index 72c950fc84..0000000000
--- a/tools/static-assets/skel-vue/tailwind.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ['./imports/ui/**/*.{vue,js,ts,jsx,tsx}', './client/*.html'],
- theme: {
- extend: {},
- },
- plugins: [],
-}
diff --git a/tools/static-assets/skel-vue/vite.config.js b/tools/static-assets/skel-vue/vite.config.js
deleted file mode 100644
index d3aeaa9aba..0000000000
--- a/tools/static-assets/skel-vue/vite.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
-
-export default defineConfig({
- plugins: [vue()],
- meteor: {
- clientEntry: 'imports/ui/main.js',
- },
- optimizeDeps: {
- exclude: ['vue-meteor-tracker'],
- },
-})
diff --git a/tools/static-assets/skel-vue/vite.config.mjs b/tools/static-assets/skel-vue/vite.config.mjs
new file mode 100644
index 0000000000..f955234c6a
--- /dev/null
+++ b/tools/static-assets/skel-vue/vite.config.mjs
@@ -0,0 +1,22 @@
+import vue from '@vitejs/plugin-vue';
+import tailwindcss from '@tailwindcss/vite';
+import { defineConfig } from 'vite';
+import { meteor } from 'meteor-vite/plugin';
+
+export default defineConfig({
+ plugins: [
+ vue(),
+ tailwindcss(),
+ meteor({
+ clientEntry: 'client/main.js',
+ serverEntry: 'server/main.js',
+ enableExperimentalFeatures: true,
+ stubValidation: {
+ ignorePackages: ['meteor/mongo'],
+ },
+ }),
+ ],
+ optimizeDeps: {
+ exclude: ['vue-meteor-tracker'],
+ },
+});
diff --git a/tools/tests/app-config.js b/tools/tests/app-config.js
index 7a7cd3832b..1ff476a01e 100644
--- a/tools/tests/app-config.js
+++ b/tools/tests/app-config.js
@@ -199,3 +199,71 @@ selftest.define("testModule", async function () {
await run.stop();
});
+
+async function writeModernConfig(s, run, modernConfig, errorPattern) {
+ const json = JSON.parse(s.read("package.json"));
+
+ json.meteor = {
+ // Make sure the tests.js module is always loaded eagerly.
+ testModule: "tests.js"
+ };
+
+ if (typeof modernConfig === "undefined") {
+ delete json.meteor.modern;
+ } else {
+ json.meteor.modern = modernConfig;
+ }
+
+ s.write("package.json", JSON.stringify(json, null, 2) + "\n");
+
+ run.waitSecs(10);
+
+ if (errorPattern instanceof RegExp) {
+ await run.match(errorPattern);
+ } else {
+ run.forbid(" 0 passing ");
+ await run.match("SERVER FAILURES: 0");
+ await run.match("CLIENT FAILURES: 0");
+ }
+}
+
+selftest.define("modernConfig", async function () {
+ const s = new Sandbox();
+ await s.init();
+
+ await s.createApp("app-config-modernConfig", "app-config");
+ await s.cd("app-config-modernConfig");
+
+ // For meteortesting:mocha to work we must set test broswer driver
+ // See https://github.com/meteortesting/meteor-mocha
+ s.set("TEST_BROWSER_DRIVER", "puppeteer");
+
+ const run = s.run(
+ "test",
+ "--full-app",
+ "--driver-package", "meteortesting:mocha"
+ );
+
+ run.waitSecs(60);
+ await run.match("App running at");
+
+ function check(modernConfig) {
+ return writeModernConfig(s, run, modernConfig);
+ }
+
+ // Test with modern disabled
+ await check(false);
+
+ // Test with modern enabled
+ await check(true);
+
+ // Test with combined options
+ await check({
+ transpiler: true,
+ watcher: true,
+ webArchOnly: true,
+ minifier: true,
+ });
+
+ await run.stop();
+});
diff --git a/tools/tests/apps/app-config/package.json b/tools/tests/apps/app-config/package.json
index dc5c3aaf75..65c8babb5e 100644
--- a/tools/tests/apps/app-config/package.json
+++ b/tools/tests/apps/app-config/package.json
@@ -6,7 +6,7 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"puppeteer": "^2.1.1"
},
"meteor": {
diff --git a/tools/tests/apps/app-prints-pid/package.json b/tools/tests/apps/app-prints-pid/package.json
index c3c3ba64b1..1a2249fa4a 100644
--- a/tools/tests/apps/app-prints-pid/package.json
+++ b/tools/tests/apps/app-prints-pid/package.json
@@ -3,7 +3,7 @@
"private": true,
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": {
diff --git a/tools/tests/apps/client-refresh/package.json b/tools/tests/apps/client-refresh/package.json
index 98588b9b2f..d79be809f3 100644
--- a/tools/tests/apps/client-refresh/package.json
+++ b/tools/tests/apps/client-refresh/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": {
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages
index a049f97cef..aa8a211d5f 100644
--- a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages
@@ -8,4 +8,3 @@ standard-minifier-css
standard-minifier-js
shell-server
dynamic-import
-underscore
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/packages b/tools/tests/apps/compiler-plugin-static-html/.meteor/packages
index a049f97cef..aa8a211d5f 100644
--- a/tools/tests/apps/compiler-plugin-static-html/.meteor/packages
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/packages
@@ -8,4 +8,3 @@ standard-minifier-css
standard-minifier-js
shell-server
dynamic-import
-underscore
diff --git a/tools/tests/apps/css-injection-test/package.json b/tools/tests/apps/css-injection-test/package.json
index c9acaecfb9..7778b8ccd5 100644
--- a/tools/tests/apps/css-injection-test/package.json
+++ b/tools/tests/apps/css-injection-test/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@babel/runtime": "^7.23.5",
"jquery": "^3.7.1",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": "css-injection-test.js"
diff --git a/tools/tests/apps/custom-minifier/package.json b/tools/tests/apps/custom-minifier/package.json
index bc4cc26b3a..1bf5b6c401 100644
--- a/tools/tests/apps/custom-minifier/package.json
+++ b/tools/tests/apps/custom-minifier/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@babel/runtime": "^7.23.5",
"jquery": "^3.7.1",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": "code.js"
diff --git a/tools/tests/apps/dev-bundle-bin-commands/package.json b/tools/tests/apps/dev-bundle-bin-commands/package.json
index 427753ffe6..6e4a415585 100644
--- a/tools/tests/apps/dev-bundle-bin-commands/package.json
+++ b/tools/tests/apps/dev-bundle-bin-commands/package.json
@@ -8,6 +8,6 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
}
}
diff --git a/tools/tests/apps/dynamic-import/package-lock.json b/tools/tests/apps/dynamic-import/package-lock.json
index 312d25c6fd..de6bd3e63a 100644
--- a/tools/tests/apps/dynamic-import/package-lock.json
+++ b/tools/tests/apps/dynamic-import/package-lock.json
@@ -1133,9 +1133,9 @@
}
},
"regenerator-runtime": {
- "version": "0.13.5",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
- "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"rimraf": {
"version": "2.7.1",
diff --git a/tools/tests/apps/dynamic-import/package.json b/tools/tests/apps/dynamic-import/package.json
index 679219d5ed..853e7e6133 100644
--- a/tools/tests/apps/dynamic-import/package.json
+++ b/tools/tests/apps/dynamic-import/package.json
@@ -10,7 +10,7 @@
"acorn": "^7.4.1",
"arson": "^0.2.6",
"jquery": "^3.7.1",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"moment": "^2.29.4",
"optimism": "^0.11.5",
"private": "^0.1.8",
diff --git a/tools/tests/apps/dynamic-import/tests.js b/tools/tests/apps/dynamic-import/tests.js
index 7280cf5ed7..d6ac7a1925 100644
--- a/tools/tests/apps/dynamic-import/tests.js
+++ b/tools/tests/apps/dynamic-import/tests.js
@@ -143,7 +143,7 @@ describe("dynamic import(...)", function () {
assert.strictEqual(lazy.name, requiredName);
}),
- import("meteor/lazy-test-package/dynamic").then(dynamic => {
+ import("meteor/lazy-test-package/dynamic").then(async dynamic => {
assert.strictEqual(
dynamic.name,
"/node_modules/meteor/lazy-test-package/dynamic.js"
@@ -152,7 +152,7 @@ describe("dynamic import(...)", function () {
// Now the synchronous dynamic require succeeds because the module
// has been fetched dynamically.
assert.strictEqual(
- require(dynamicId).name,
+ (await require(dynamicId)).name,
dynamic.name
);
})
diff --git a/tools/tests/apps/ecmascript-regression/package.json b/tools/tests/apps/ecmascript-regression/package.json
index 59c305eeda..bc67fd9207 100644
--- a/tools/tests/apps/ecmascript-regression/package.json
+++ b/tools/tests/apps/ecmascript-regression/package.json
@@ -8,7 +8,7 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"puppeteer": "^10.4.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
diff --git a/tools/tests/apps/extra-packages-option/.meteor/packages b/tools/tests/apps/extra-packages-option/.meteor/packages
index eafeae86c3..556e8e155a 100644
--- a/tools/tests/apps/extra-packages-option/.meteor/packages
+++ b/tools/tests/apps/extra-packages-option/.meteor/packages
@@ -4,5 +4,4 @@
# but you can also edit it by hand.
meteor-base
-underscore
extra-package-2@=0.0.1
diff --git a/tools/tests/apps/git-commit-hash/package.json b/tools/tests/apps/git-commit-hash/package.json
index a08e85969b..dfd798be69 100644
--- a/tools/tests/apps/git-commit-hash/package.json
+++ b/tools/tests/apps/git-commit-hash/package.json
@@ -7,7 +7,7 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10",
+ "meteor-node-stubs": "^1.2.12",
"puppeteer": "^2.1.1"
},
"meteor": {
diff --git a/tools/tests/apps/link-config-npm-package/package.json b/tools/tests/apps/link-config-npm-package/package.json
index f174884643..7f00cdf583 100644
--- a/tools/tests/apps/link-config-npm-package/package.json
+++ b/tools/tests/apps/link-config-npm-package/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@babel/runtime": "^7.23.5",
"config": "file:../config-package",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": {
diff --git a/tools/tests/apps/linked-external-npm-package/package.json b/tools/tests/apps/linked-external-npm-package/package.json
index 96ae9fcaa4..7735413880 100644
--- a/tools/tests/apps/linked-external-npm-package/package.json
+++ b/tools/tests/apps/linked-external-npm-package/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@babel/runtime": "^7.23.5",
"external-package": "file:../external-package",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
},
"meteor": {
"mainModule": {
diff --git a/tools/tests/apps/meteor-ignore/package.json b/tools/tests/apps/meteor-ignore/package.json
index c5723f52b3..8ab2a13628 100644
--- a/tools/tests/apps/meteor-ignore/package.json
+++ b/tools/tests/apps/meteor-ignore/package.json
@@ -6,6 +6,6 @@
},
"dependencies": {
"@babel/runtime": "^7.23.5",
- "meteor-node-stubs": "^1.2.10"
+ "meteor-node-stubs": "^1.2.12"
}
}
diff --git a/tools/tests/apps/modern/.gitignore b/tools/tests/apps/modern/.gitignore
new file mode 100644
index 0000000000..40b878db5b
--- /dev/null
+++ b/tools/tests/apps/modern/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/tools/tests/apps/modern/.meteor/.finished-upgraders b/tools/tests/apps/modern/.meteor/.finished-upgraders
new file mode 100644
index 0000000000..c07b6ff75a
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/.finished-upgraders
@@ -0,0 +1,19 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
+1.4.0-remove-old-dev-bundle-link
+1.4.1-add-shell-server-package
+1.4.3-split-account-service-packages
+1.5-add-dynamic-import-package
+1.7-split-underscore-from-meteor-base
+1.8.3-split-jquery-from-blaze
diff --git a/tools/tests/apps/modern/.meteor/.gitignore b/tools/tests/apps/modern/.meteor/.gitignore
new file mode 100644
index 0000000000..4083037423
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/tools/tests/apps/modern/.meteor/.id b/tools/tests/apps/modern/.meteor/.id
new file mode 100644
index 0000000000..b92c6c71e9
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+xh4qomttjyd.znxg6je45aan
diff --git a/tools/tests/apps/modern/.meteor/packages b/tools/tests/apps/modern/.meteor/packages
new file mode 100644
index 0000000000..341bbfc93a
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/packages
@@ -0,0 +1,15 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+meteor # Shared foundation for all Meteor packages
+static-html # Define static page content in .html files
+standard-minifier-css # CSS minifier run for production mode
+standard-minifier-js # JS minifier run for production mode
+es5-shim # ECMAScript 5 compatibility for older browsers
+ecmascript # Enable ECMAScript2015+ syntax in app code
+shell-server # Server-side component of the `meteor shell` command
+webapp # Serves a Meteor app over HTTP
+server-render # Support for server-side rendering
diff --git a/tools/tests/apps/modern/.meteor/platforms b/tools/tests/apps/modern/.meteor/platforms
new file mode 100644
index 0000000000..efeba1b50c
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/tools/tests/apps/modern/.meteor/release b/tools/tests/apps/modern/.meteor/release
new file mode 100644
index 0000000000..621e94f0ec
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/release
@@ -0,0 +1 @@
+none
diff --git a/tools/tests/apps/modern/.meteor/versions b/tools/tests/apps/modern/.meteor/versions
new file mode 100644
index 0000000000..91b9af05b0
--- /dev/null
+++ b/tools/tests/apps/modern/.meteor/versions
@@ -0,0 +1,39 @@
+babel-compiler@7.2.4
+babel-runtime@1.3.0
+base64@1.0.11
+blaze-tools@1.0.10
+boilerplate-generator@1.6.0
+caching-compiler@1.2.1
+caching-html-compiler@1.1.3
+dynamic-import@0.5.1
+ecmascript@0.12.4
+ecmascript-runtime@0.7.0
+ecmascript-runtime-client@0.8.0
+ecmascript-runtime-server@0.7.1
+ejson@1.1.0
+es5-shim@4.8.0
+fetch@0.1.0
+html-tools@1.0.11
+htmljs@1.0.11
+inter-process-messaging@0.1.0
+logging@1.1.20
+meteor@1.9.3
+minifier-css@1.4.1
+minifier-js@2.4.0
+modern-browsers@0.1.4-beta181.16
+modules@0.13.0
+modules-runtime@0.10.3
+promise@0.11.2
+random@1.1.0
+routepolicy@1.1.0
+server-render@0.3.1
+shell-server@0.4.0
+spacebars-compiler@1.1.3
+standard-minifier-css@1.5.2
+standard-minifier-js@2.4.0
+static-html@1.2.2
+templating-tools@1.1.2
+tracker@1.2.1
+underscore@1.0.11
+webapp@1.7.3-beta181.16
+webapp-hashing@1.0.9
diff --git a/tools/tests/apps/modern/.swcrc b/tools/tests/apps/modern/.swcrc
new file mode 100644
index 0000000000..e859928b67
--- /dev/null
+++ b/tools/tests/apps/modern/.swcrc
@@ -0,0 +1,8 @@
+{
+ "jsc": {
+ "baseUrl": "./",
+ "paths": {
+ "@swcAlias/*": ["swcAlias/*"]
+ }
+ }
+}
diff --git a/tools/tests/apps/modern/client/main.css b/tools/tests/apps/modern/client/main.css
new file mode 100644
index 0000000000..7f354f0fa7
--- /dev/null
+++ b/tools/tests/apps/modern/client/main.css
@@ -0,0 +1,4 @@
+body {
+ padding: 10px;
+ font-family: sans-serif;
+}
diff --git a/tools/tests/apps/modern/client/main.html b/tools/tests/apps/modern/client/main.html
new file mode 100644
index 0000000000..19789db4de
--- /dev/null
+++ b/tools/tests/apps/modern/client/main.html
@@ -0,0 +1,21 @@
+
+ Minimal Meteor app
+
+
+
+ Minimal Meteor app
+
+ This Meteor app uses as few Meteor packages as possible, to keep the
+ client JavaScript bundle as small as possible.
+
+
+
+
+ Learn Meteor!
+
+
diff --git a/tools/tests/apps/modern/client/main.js b/tools/tests/apps/modern/client/main.js
new file mode 100644
index 0000000000..ce4d5b3750
--- /dev/null
+++ b/tools/tests/apps/modern/client/main.js
@@ -0,0 +1 @@
+Meteor.startup(() => {});
diff --git a/tools/tests/apps/modern/package-lock.json b/tools/tests/apps/modern/package-lock.json
new file mode 100644
index 0000000000..b21affb1c2
--- /dev/null
+++ b/tools/tests/apps/modern/package-lock.json
@@ -0,0 +1,1119 @@
+{
+ "name": "modern",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
+ "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
+ "requires": {
+ "regenerator-runtime": "^0.14.0"
+ }
+ },
+ "@types/mime-types": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
+ "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM="
+ },
+ "agent-base": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
+ "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
+ },
+ "async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "debug": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "extract-zip": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
+ "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
+ "requires": {
+ "concat-stream": "^1.6.2",
+ "debug": "^2.6.9",
+ "mkdirp": "^0.5.4",
+ "yauzl": "^2.10.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
+ "requires": {
+ "pend": "~1.2.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "https-proxy-agent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
+ "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
+ "requires": {
+ "agent-base": "5",
+ "debug": "4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "meteor-node-stubs": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.2.9.tgz",
+ "integrity": "sha512-EKRezc1/PblYtYiK4BOT3h5geWDo9AFBBSYNamPNh8AC5msUbVCcg8kekzAa7r7JPzBX8nZWaXEQVar4t8q/Hg==",
+ "requires": {
+ "@meteorjs/crypto-browserify": "^3.12.1",
+ "assert": "^2.1.0",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^5.7.1",
+ "console-browserify": "^1.2.0",
+ "constants-browserify": "^1.0.0",
+ "domain-browser": "^4.23.0",
+ "elliptic": "^6.5.4",
+ "events": "^3.3.0",
+ "https-browserify": "^1.0.0",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "^1.0.1",
+ "process": "^0.11.10",
+ "punycode": "^1.4.1",
+ "querystring-es3": "^0.2.1",
+ "readable-stream": "^3.6.2",
+ "stream-browserify": "^3.0.0",
+ "stream-http": "^3.2.0",
+ "string_decoder": "^1.3.0",
+ "timers-browserify": "^2.0.12",
+ "tty-browserify": "0.0.1",
+ "url": "^0.11.3",
+ "util": "^0.12.5",
+ "vm-browserify": "^1.1.2"
+ },
+ "dependencies": {
+ "@meteorjs/crypto-browserify": {
+ "version": "3.12.1",
+ "bundled": true,
+ "requires": {
+ "browserify-cipher": "^1.0.1",
+ "browserify-sign": "^4.2.3",
+ "create-ecdh": "^4.0.4",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "diffie-hellman": "^5.0.3",
+ "hash-base": "~3.0.4",
+ "inherits": "^2.0.4",
+ "pbkdf2": "^3.1.2",
+ "public-encrypt": "^4.0.3",
+ "randombytes": "^2.1.0",
+ "randomfill": "^1.0.4"
+ },
+ "dependencies": {
+ "hash-base": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ }
+ }
+ },
+ "asn1.js": {
+ "version": "4.10.1",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "bundled": true
+ }
+ }
+ },
+ "assert": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "is-nan": "^1.3.2",
+ "object-is": "^1.1.5",
+ "object.assign": "^4.1.4",
+ "util": "^0.12.5"
+ }
+ },
+ "available-typed-arrays": {
+ "version": "1.0.5",
+ "bundled": true
+ },
+ "base64-js": {
+ "version": "1.5.1",
+ "bundled": true
+ },
+ "bn.js": {
+ "version": "5.2.0",
+ "bundled": true
+ },
+ "brorand": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "browserify-aes": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "browserify-des": {
+ "version": "1.0.2",
+ "bundled": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "browserify-rsa": {
+ "version": "4.1.0",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "browserify-sign": {
+ "version": "4.2.3",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.5",
+ "hash-base": "~3.0",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.7",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "5.2.1",
+ "bundled": true
+ },
+ "hash-base": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.8",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true
+ }
+ }
+ }
+ }
+ },
+ "browserify-zlib": {
+ "version": "0.2.0",
+ "bundled": true,
+ "requires": {
+ "pako": "~1.0.5"
+ }
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "bundled": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "buffer-xor": {
+ "version": "1.0.3",
+ "bundled": true
+ },
+ "builtin-status-codes": {
+ "version": "3.0.0",
+ "bundled": true
+ },
+ "call-bind": {
+ "version": "1.0.5",
+ "bundled": true,
+ "requires": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ }
+ },
+ "cipher-base": {
+ "version": "1.0.4",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "console-browserify": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "constants-browserify": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "core-util-is": {
+ "version": "1.0.3",
+ "bundled": true
+ },
+ "create-ecdh": {
+ "version": "4.0.4",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "bundled": true
+ }
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "bundled": true,
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "define-data-property": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ }
+ },
+ "define-properties": {
+ "version": "1.2.1",
+ "bundled": true,
+ "requires": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "des.js": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "bundled": true
+ }
+ }
+ },
+ "domain-browser": {
+ "version": "4.23.0",
+ "bundled": true
+ },
+ "elliptic": {
+ "version": "6.5.5",
+ "bundled": true,
+ "requires": {
+ "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"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "bundled": true
+ }
+ }
+ },
+ "events": {
+ "version": "3.3.0",
+ "bundled": true
+ },
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "bundled": true,
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "for-each": {
+ "version": "0.3.3",
+ "bundled": true,
+ "requires": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "function-bind": {
+ "version": "1.1.2",
+ "bundled": true
+ },
+ "get-intrinsic": {
+ "version": "1.2.2",
+ "bundled": true,
+ "requires": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ }
+ },
+ "gopd": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "has-property-descriptors": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "get-intrinsic": "^1.2.2"
+ }
+ },
+ "has-proto": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "bundled": true
+ },
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "bundled": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "hash-base": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ }
+ },
+ "hash.js": {
+ "version": "1.1.7",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "hasown": {
+ "version": "2.0.0",
+ "bundled": true,
+ "requires": {
+ "function-bind": "^1.1.2"
+ }
+ },
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "https-browserify": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "bundled": true
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "bundled": true
+ },
+ "is-arguments": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.7",
+ "bundled": true
+ },
+ "is-generator-function": {
+ "version": "1.0.10",
+ "bundled": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-nan": {
+ "version": "1.3.2",
+ "bundled": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "is-typed-array": {
+ "version": "1.1.12",
+ "bundled": true,
+ "requires": {
+ "which-typed-array": "^1.1.11"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "md5.js": {
+ "version": "1.3.5",
+ "bundled": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "miller-rabin": {
+ "version": "4.0.1",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "bundled": true
+ }
+ }
+ },
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "object-inspect": {
+ "version": "1.13.1",
+ "bundled": true
+ },
+ "object-is": {
+ "version": "1.1.5",
+ "bundled": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "bundled": true
+ },
+ "object.assign": {
+ "version": "4.1.4",
+ "bundled": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "os-browserify": {
+ "version": "0.3.0",
+ "bundled": true
+ },
+ "pako": {
+ "version": "1.0.11",
+ "bundled": true
+ },
+ "parse-asn1": {
+ "version": "5.1.7",
+ "bundled": true,
+ "requires": {
+ "asn1.js": "^4.10.1",
+ "browserify-aes": "^1.2.0",
+ "evp_bytestokey": "^1.0.3",
+ "hash-base": "~3.0",
+ "pbkdf2": "^3.1.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "dependencies": {
+ "hash-base": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ }
+ }
+ },
+ "path-browserify": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "pbkdf2": {
+ "version": "3.1.2",
+ "bundled": true,
+ "requires": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "process": {
+ "version": "0.11.10",
+ "bundled": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "bundled": true
+ },
+ "public-encrypt": {
+ "version": "4.0.3",
+ "bundled": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "bundled": true
+ }
+ }
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "bundled": true
+ },
+ "qs": {
+ "version": "6.11.2",
+ "bundled": true,
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
+ "querystring-es3": {
+ "version": "0.2.1",
+ "bundled": true
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "randomfill": {
+ "version": "1.0.4",
+ "bundled": true,
+ "requires": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "readable-stream": {
+ "version": "3.6.2",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "ripemd160": {
+ "version": "2.0.2",
+ "bundled": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "bundled": true
+ },
+ "set-function-length": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "define-data-property": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ }
+ },
+ "setimmediate": {
+ "version": "1.0.5",
+ "bundled": true
+ },
+ "sha.js": {
+ "version": "2.4.11",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "bundled": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "stream-browserify": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
+ "stream-http": {
+ "version": "3.2.0",
+ "bundled": true,
+ "requires": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "timers-browserify": {
+ "version": "2.0.12",
+ "bundled": true,
+ "requires": {
+ "setimmediate": "^1.0.4"
+ }
+ },
+ "tty-browserify": {
+ "version": "0.0.1",
+ "bundled": true
+ },
+ "url": {
+ "version": "0.11.3",
+ "bundled": true,
+ "requires": {
+ "punycode": "^1.4.1",
+ "qs": "^6.11.2"
+ }
+ },
+ "util": {
+ "version": "0.12.5",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "vm-browserify": {
+ "version": "1.1.2",
+ "bundled": true
+ },
+ "which-typed-array": {
+ "version": "1.1.13",
+ "bundled": true,
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.4",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "bundled": true
+ }
+ }
+ },
+ "mime": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
+ "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA=="
+ },
+ "mime-db": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
+ "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
+ },
+ "mime-types": {
+ "version": "2.1.27",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
+ "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
+ "requires": {
+ "mime-db": "1.44.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
+ },
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "puppeteer": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2.1.1.tgz",
+ "integrity": "sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg==",
+ "requires": {
+ "@types/mime-types": "^2.1.0",
+ "debug": "^4.1.0",
+ "extract-zip": "^1.6.6",
+ "https-proxy-agent": "^4.0.0",
+ "mime": "^2.0.3",
+ "mime-types": "^2.1.25",
+ "progress": "^2.0.1",
+ "proxy-from-env": "^1.0.0",
+ "rimraf": "^2.6.1",
+ "ws": "^6.1.0"
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "ws": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
+ "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
+ },
+ "yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+ "requires": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ }
+ }
+}
diff --git a/tools/tests/apps/modern/package.json b/tools/tests/apps/modern/package.json
new file mode 100644
index 0000000000..2b1fe578bf
--- /dev/null
+++ b/tools/tests/apps/modern/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "modern",
+ "private": true,
+ "scripts": {
+ "start": "meteor run"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.23.5",
+ "config": "file:../config-package",
+ "meteor-node-stubs": "^1.2.12",
+ "react": "^18.3.1"
+ },
+ "meteor": {
+ "mainModule": {
+ "client": "client/main.js",
+ "server": "server/main.js"
+ },
+ "testModule": "tests/main.js"
+ }
+}
diff --git a/tools/tests/apps/modern/server/alias.js b/tools/tests/apps/modern/server/alias.js
new file mode 100644
index 0000000000..acf0a92d2a
--- /dev/null
+++ b/tools/tests/apps/modern/server/alias.js
@@ -0,0 +1 @@
+import '@swcAlias/main';
diff --git a/tools/tests/apps/modern/server/custom-component.js b/tools/tests/apps/modern/server/custom-component.js
new file mode 100644
index 0000000000..ac52d8c243
--- /dev/null
+++ b/tools/tests/apps/modern/server/custom-component.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+console.log('custom-component.js');
+
+export default <>>;
diff --git a/tools/tests/apps/modern/server/javascript-component.jsx b/tools/tests/apps/modern/server/javascript-component.jsx
new file mode 100644
index 0000000000..6b3976b857
--- /dev/null
+++ b/tools/tests/apps/modern/server/javascript-component.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+console.log('javascript-component.jsx');
+
+export default <>>;
diff --git a/tools/tests/apps/modern/server/javascript.js b/tools/tests/apps/modern/server/javascript.js
new file mode 100644
index 0000000000..23fb2816d6
--- /dev/null
+++ b/tools/tests/apps/modern/server/javascript.js
@@ -0,0 +1,3 @@
+console.log('javascript.js');
+
+export default {};
diff --git a/tools/tests/apps/modern/server/main.js b/tools/tests/apps/modern/server/main.js
new file mode 100644
index 0000000000..12ffcf4012
--- /dev/null
+++ b/tools/tests/apps/modern/server/main.js
@@ -0,0 +1,10 @@
+import { Meteor } from "meteor/meteor";
+
+Meteor.startup(() => {});
+
+try {
+ const errorStack = new Error().stack;
+ console.log(errorStack);
+} catch (e) {
+ console.log(e);
+}
diff --git a/tools/tests/apps/modern/server/typescript-component.tsx b/tools/tests/apps/modern/server/typescript-component.tsx
new file mode 100644
index 0000000000..9535cc475d
--- /dev/null
+++ b/tools/tests/apps/modern/server/typescript-component.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+console.log('typescript-component.tsx');
+
+export default <>>;
diff --git a/tools/tests/apps/modern/server/typescript.ts b/tools/tests/apps/modern/server/typescript.ts
new file mode 100644
index 0000000000..05ed0e8932
--- /dev/null
+++ b/tools/tests/apps/modern/server/typescript.ts
@@ -0,0 +1,3 @@
+console.log('typescript.ts');
+
+export default {};
diff --git a/tools/tests/apps/modern/swcAlias/main.js b/tools/tests/apps/modern/swcAlias/main.js
new file mode 100644
index 0000000000..65ebe218b3
--- /dev/null
+++ b/tools/tests/apps/modern/swcAlias/main.js
@@ -0,0 +1 @@
+console.log('swcAlias/main.js alias resolved');
diff --git a/tools/tests/apps/modern/tests/main.js b/tools/tests/apps/modern/tests/main.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tools/tests/apps/modules-modern/.gitignore b/tools/tests/apps/modules-modern/.gitignore
new file mode 100644
index 0000000000..07e6e472cc
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.gitignore
@@ -0,0 +1 @@
+/node_modules
diff --git a/tools/tests/apps/modules-modern/.meteor/.finished-upgraders b/tools/tests/apps/modules-modern/.meteor/.finished-upgraders
new file mode 100644
index 0000000000..bc5b50f7cd
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.meteor/.finished-upgraders
@@ -0,0 +1,20 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
+1.3.5-remove-old-dev-bundle-link
+1.4.0-remove-old-dev-bundle-link
+1.4.1-add-shell-server-package
+1.4.3-split-account-service-packages
+1.5-add-dynamic-import-package
+1.7-split-underscore-from-meteor-base
+1.8.3-split-jquery-from-blaze
diff --git a/tools/tests/apps/modules-modern/.meteor/.gitignore b/tools/tests/apps/modules-modern/.meteor/.gitignore
new file mode 100644
index 0000000000..501f92e4b5
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.meteor/.gitignore
@@ -0,0 +1,2 @@
+dev_bundle
+local
diff --git a/tools/tests/apps/modules-modern/.meteor/.id b/tools/tests/apps/modules-modern/.meteor/.id
new file mode 100644
index 0000000000..be64c639e9
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+dpey3716n44ug18al7gu
diff --git a/tools/tests/apps/modules-modern/.meteor/packages b/tools/tests/apps/modules-modern/.meteor/packages
new file mode 100644
index 0000000000..bcccad6a5f
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.meteor/packages
@@ -0,0 +1,29 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+meteor-base@1.5.2-beta300.6 # Packages every Meteor app needs to have
+mobile-experience@1.1.1-beta300.6 # Packages for a great mobile UX
+mongo@2.0.0-beta300.6 # The database Meteor supports right now
+blaze-html-templates # Compile .html files into Meteor Blaze views
+session@1.2.2-beta300.6 # Client-side reactive dictionary for your app
+jquery # Helpful client-side library
+tracker@1.3.3-beta300.6 # Meteor's client-side reactive programming library
+
+es5-shim@4.8.1-beta300.6 # ECMAScript 5 compatibility for older browsers.
+ecmascript@0.16.8-beta300.6 # Enable ECMAScript2015+ syntax in app code
+
+coffeescript
+modules-test-package
+standard-minifier-css@1.9.3-beta300.6
+standard-minifier-js@3.0.0-beta300.6
+client-only-ecmascript
+shell-server@0.6.0-beta300.6
+dynamic-import@0.7.4-beta300.6
+import-local-json-module
+dummy-compiler
+typescript@5.3.3-beta300.6
+meteortesting:mocha@3.0.0
+meteortesting:mocha-core@8.2.0
diff --git a/tools/tests/apps/modules-modern/.meteor/platforms b/tools/tests/apps/modules-modern/.meteor/platforms
new file mode 100644
index 0000000000..efeba1b50c
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/tools/tests/apps/modules-modern/.meteor/release b/tools/tests/apps/modules-modern/.meteor/release
new file mode 100644
index 0000000000..ecc41d1d5a
--- /dev/null
+++ b/tools/tests/apps/modules-modern/.meteor/release
@@ -0,0 +1 @@
+METEOR@3.0-beta.6
diff --git a/tools/tests/apps/modules-modern/README.md b/tools/tests/apps/modules-modern/README.md
new file mode 100644
index 0000000000..323c3fb58b
--- /dev/null
+++ b/tools/tests/apps/modules-modern/README.md
@@ -0,0 +1,10 @@
+# modules
+
+To run the tests, first install the npm dependencies, then run the app:
+
+```sh
+npm install
+npm test # just does `meteor run`
+```
+
+then visit [localhost:3000](//localhost:3000) in your browser.
diff --git a/tools/tests/apps/modules-modern/client/compatibility/bare.js b/tools/tests/apps/modules-modern/client/compatibility/bare.js
new file mode 100644
index 0000000000..f8ac4fa752
--- /dev/null
+++ b/tools/tests/apps/modules-modern/client/compatibility/bare.js
@@ -0,0 +1,5 @@
+// Because this file is contained by a client/compatibility directory, it
+// should be evaluated as a bare file instead of as a module, so this
+// variable declaration should take effect in the scope of the app, and
+// thus be visible to other modules in the app.
+var topLevelVariable = 1234;
diff --git a/tools/tests/apps/modules-modern/client/only.js b/tools/tests/apps/modules-modern/client/only.js
new file mode 100644
index 0000000000..3e3d4356d3
--- /dev/null
+++ b/tools/tests/apps/modules-modern/client/only.js
@@ -0,0 +1 @@
+export default module.id;
diff --git a/tools/tests/apps/modules-modern/eager-coffee.coffee b/tools/tests/apps/modules-modern/eager-coffee.coffee
new file mode 100644
index 0000000000..2dfecc17ed
--- /dev/null
+++ b/tools/tests/apps/modules-modern/eager-coffee.coffee
@@ -0,0 +1,2 @@
+exports.extension = ".coffee";
+require("./imports/shared").default[module.id] = "eager coffee";
diff --git a/tools/tests/apps/modules-modern/eager-jsx.jsx b/tools/tests/apps/modules-modern/eager-jsx.jsx
new file mode 100644
index 0000000000..45e2dfa842
--- /dev/null
+++ b/tools/tests/apps/modules-modern/eager-jsx.jsx
@@ -0,0 +1,3 @@
+import shared from "./imports/shared";
+export const extension = ".jsx";
+shared[module.id] = "eager jsx";
diff --git a/tools/tests/apps/modules-modern/eager.css b/tools/tests/apps/modules-modern/eager.css
new file mode 100644
index 0000000000..3295fdf0cf
--- /dev/null
+++ b/tools/tests/apps/modules-modern/eager.css
@@ -0,0 +1,7 @@
+.app-eager-css {
+ display: none;
+}
+
+.app-lazy-css {
+ padding: 0;
+}
diff --git a/tools/tests/apps/modules-modern/imports/.babelrc b/tools/tests/apps/modules-modern/imports/.babelrc
new file mode 100644
index 0000000000..b18c977ecb
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/.babelrc
@@ -0,0 +1,14 @@
+{
+ "env": {
+ "development": {
+ "plugins": [
+ "proposal-do-expressions",
+ ]
+ },
+ "production": {
+ "plugins": [
+ "proposal-do-expressions",
+ ]
+ }
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/babel-env.js b/tools/tests/apps/modules-modern/imports/babel-env.js
new file mode 100644
index 0000000000..834320ebb4
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/babel-env.js
@@ -0,0 +1,8 @@
+export function check(y) {
+ // If the transform-do-expressions plugin is loaded correctly, there
+ // will be no errors during the compilation of this file. Without the
+ // plugin, the error will be: "SyntaxError: Unexpected token do".
+ return do {
+ y + y;
+ };
+}
diff --git a/tools/tests/apps/modules-modern/imports/chaining.js b/tools/tests/apps/modules-modern/imports/chaining.js
new file mode 100644
index 0000000000..5f0073997a
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/chaining.js
@@ -0,0 +1,3 @@
+export function check(obj) {
+ return obj?.foo?.bar?.baz.qux;
+}
diff --git a/tools/tests/apps/modules-modern/imports/dir/index.js b/tools/tests/apps/modules-modern/imports/dir/index.js
new file mode 100644
index 0000000000..e90dd19200
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/dir/index.js
@@ -0,0 +1 @@
+exports.fellBackTo = module.id;
diff --git a/tools/tests/apps/modules-modern/imports/dir/package.json b/tools/tests/apps/modules-modern/imports/dir/package.json
new file mode 100644
index 0000000000..9c9f954a06
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/dir/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "package-json-with-no-main",
+ "version": "1.2.3",
+ "description": "https://github.com/meteor/meteor/issues/6589"
+}
diff --git a/tools/tests/apps/modules-modern/imports/imported.tests.js b/tools/tests/apps/modules-modern/imports/imported.tests.js
new file mode 100644
index 0000000000..5db10f22c0
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/imported.tests.js
@@ -0,0 +1,9 @@
+import assert from "assert";
+
+export const name = module.id.split("/").pop();
+
+describe(name, () => {
+ it("should be imported", () => {
+ assert.strictEqual(name, "imported.tests.js");
+ });
+});
diff --git a/tools/tests/apps/modules-modern/imports/lazy.css b/tools/tests/apps/modules-modern/imports/lazy.css
new file mode 100644
index 0000000000..f59e326e70
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/lazy.css
@@ -0,0 +1,3 @@
+.app-lazy-css {
+ padding: 10px;
+}
diff --git a/tools/tests/apps/modules-modern/imports/lazy.html b/tools/tests/apps/modules-modern/imports/lazy.html
new file mode 100644
index 0000000000..df3233bbc2
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/lazy.html
@@ -0,0 +1,7 @@
+
+ {{> lazy}}
+
+
+
+ lazy
+
diff --git a/tools/tests/apps/modules-modern/imports/lazy1.js b/tools/tests/apps/modules-modern/imports/lazy1.js
new file mode 100644
index 0000000000..2f9b2be9cf
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/lazy1.js
@@ -0,0 +1,9 @@
+import shared from "./shared";
+import "./lazy2";
+
+shared[module.id] = 1;
+
+export function reset() {
+ delete shared[module.id];
+ delete module.exports;
+}
diff --git a/tools/tests/apps/modules-modern/imports/lazy2.js b/tools/tests/apps/modules-modern/imports/lazy2.js
new file mode 100644
index 0000000000..68e5d569f6
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/lazy2.js
@@ -0,0 +1,9 @@
+import shared from "../imports/shared";
+import "./lazy1";
+
+shared[module.id] = 2;
+
+export function reset() {
+ delete shared[module.id];
+ delete module.exports;
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/.npmignore b/tools/tests/apps/modules-modern/imports/links/acorn/.npmignore
new file mode 100644
index 0000000000..0e574439e0
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/.npmignore
@@ -0,0 +1,3 @@
+.tern-*
+/rollup.config.*
+/src
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/CHANGELOG.md b/tools/tests/apps/modules-modern/imports/links/acorn/CHANGELOG.md
new file mode 100644
index 0000000000..93837a91a1
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/CHANGELOG.md
@@ -0,0 +1,580 @@
+## 6.4.1 (2020-03-09)
+
+### Bug fixes
+
+More carefully check for valid UTF16 surrogate pairs in regexp validator.
+
+## 7.1.1 (2020-03-01)
+
+### Bug fixes
+
+Treat `\8` and `\9` as invalid escapes in template strings.
+
+Allow unicode escapes in property names that are keywords.
+
+Don't error on an exponential operator expression as argument to `await`.
+
+More carefully check for valid UTF16 surrogate pairs in regexp validator.
+
+## 7.1.0 (2019-09-24)
+
+### Bug fixes
+
+Disallow trailing object literal commas when ecmaVersion is less than 5.
+
+### New features
+
+Add a static `acorn` property to the `Parser` class that contains the entire module interface, to allow plugins to access the instance of the library that they are acting on.
+
+## 7.0.0 (2019-08-13)
+
+### Breaking changes
+
+Changes the node format for dynamic imports to use the `ImportExpression` node type, as defined in [ESTree](https://github.com/estree/estree/blob/master/es2020.md#importexpression).
+
+Makes 10 (ES2019) the default value for the `ecmaVersion` option.
+
+## 6.3.0 (2019-08-12)
+
+### New features
+
+`sourceType: "module"` can now be used even when `ecmaVersion` is less than 6, to parse module-style code that otherwise conforms to an older standard.
+
+## 6.2.1 (2019-07-21)
+
+### Bug fixes
+
+Fix bug causing Acorn to treat some characters as identifier characters that shouldn't be treated as such.
+
+Fix issue where setting the `allowReserved` option to `"never"` allowed reserved words in some circumstances.
+
+## 6.2.0 (2019-07-04)
+
+### Bug fixes
+
+Improve valid assignment checking in `for`/`in` and `for`/`of` loops.
+
+Disallow binding `let` in patterns.
+
+### New features
+
+Support bigint syntax with `ecmaVersion` >= 11.
+
+Support dynamic `import` syntax with `ecmaVersion` >= 11.
+
+Upgrade to Unicode version 12.
+
+## 6.1.1 (2019-02-27)
+
+### Bug fixes
+
+Fix bug that caused parsing default exports of with names to fail.
+
+## 6.1.0 (2019-02-08)
+
+### Bug fixes
+
+Fix scope checking when redefining a `var` as a lexical binding.
+
+### New features
+
+Split up `parseSubscripts` to use an internal `parseSubscript` method to make it easier to extend with plugins.
+
+## 6.0.7 (2019-02-04)
+
+### Bug fixes
+
+Check that exported bindings are defined.
+
+Don't treat `\u180e` as a whitespace character.
+
+Check for duplicate parameter names in methods.
+
+Don't allow shorthand properties when they are generators or async methods.
+
+Forbid binding `await` in async arrow function's parameter list.
+
+## 6.0.6 (2019-01-30)
+
+### Bug fixes
+
+The content of class declarations and expressions is now always parsed in strict mode.
+
+Don't allow `let` or `const` to bind the variable name `let`.
+
+Treat class declarations as lexical.
+
+Don't allow a generator function declaration as the sole body of an `if` or `else`.
+
+Ignore `"use strict"` when after an empty statement.
+
+Allow string line continuations with special line terminator characters.
+
+Treat `for` bodies as part of the `for` scope when checking for conflicting bindings.
+
+Fix bug with parsing `yield` in a `for` loop initializer.
+
+Implement special cases around scope checking for functions.
+
+## 6.0.5 (2019-01-02)
+
+### Bug fixes
+
+Fix TypeScript type for `Parser.extend` and add `allowAwaitOutsideFunction` to options type.
+
+Don't treat `let` as a keyword when the next token is `{` on the next line.
+
+Fix bug that broke checking for parentheses around an object pattern in a destructuring assignment when `preserveParens` was on.
+
+## 6.0.4 (2018-11-05)
+
+### Bug fixes
+
+Further improvements to tokenizing regular expressions in corner cases.
+
+## 6.0.3 (2018-11-04)
+
+### Bug fixes
+
+Fix bug in tokenizing an expression-less return followed by a function followed by a regular expression.
+
+Remove stray symlink in the package tarball.
+
+## 6.0.2 (2018-09-26)
+
+### Bug fixes
+
+Fix bug where default expressions could fail to parse inside an object destructuring assignment expression.
+
+## 6.0.1 (2018-09-14)
+
+### Bug fixes
+
+Fix wrong value in `version` export.
+
+## 6.0.0 (2018-09-14)
+
+### Bug fixes
+
+Better handle variable-redefinition checks for catch bindings and functions directly under if statements.
+
+Forbid `new.target` in top-level arrow functions.
+
+Fix issue with parsing a regexp after `yield` in some contexts.
+
+### New features
+
+The package now comes with TypeScript definitions.
+
+### Breaking changes
+
+The default value of the `ecmaVersion` option is now 9 (2018).
+
+Plugins work differently, and will have to be rewritten to work with this version.
+
+The loose parser and walker have been moved into separate packages (`acorn-loose` and `acorn-walk`).
+
+## 5.7.3 (2018-09-10)
+
+### Bug fixes
+
+Fix failure to tokenize regexps after expressions like `x.of`.
+
+Better error message for unterminated template literals.
+
+## 5.7.2 (2018-08-24)
+
+### Bug fixes
+
+Properly handle `allowAwaitOutsideFunction` in for statements.
+
+Treat function declarations at the top level of modules like let bindings.
+
+Don't allow async function declarations as the only statement under a label.
+
+## 5.7.0 (2018-06-15)
+
+### New features
+
+Upgraded to Unicode 11.
+
+## 5.6.0 (2018-05-31)
+
+### New features
+
+Allow U+2028 and U+2029 in string when ECMAVersion >= 10.
+
+Allow binding-less catch statements when ECMAVersion >= 10.
+
+Add `allowAwaitOutsideFunction` option for parsing top-level `await`.
+
+## 5.5.3 (2018-03-08)
+
+### Bug fixes
+
+A _second_ republish of the code in 5.5.1, this time with yarn, to hopefully get valid timestamps.
+
+## 5.5.2 (2018-03-08)
+
+### Bug fixes
+
+A republish of the code in 5.5.1 in an attempt to solve an issue with the file timestamps in the npm package being 0.
+
+## 5.5.1 (2018-03-06)
+
+### Bug fixes
+
+Fix misleading error message for octal escapes in template strings.
+
+## 5.5.0 (2018-02-27)
+
+### New features
+
+The identifier character categorization is now based on Unicode version 10.
+
+Acorn will now validate the content of regular expressions, including new ES9 features.
+
+## 5.4.0 (2018-02-01)
+
+### Bug fixes
+
+Disallow duplicate or escaped flags on regular expressions.
+
+Disallow octal escapes in strings in strict mode.
+
+### New features
+
+Add support for async iteration.
+
+Add support for object spread and rest.
+
+## 5.3.0 (2017-12-28)
+
+### Bug fixes
+
+Fix parsing of floating point literals with leading zeroes in loose mode.
+
+Allow duplicate property names in object patterns.
+
+Don't allow static class methods named `prototype`.
+
+Disallow async functions directly under `if` or `else`.
+
+Parse right-hand-side of `for`/`of` as an assignment expression.
+
+Stricter parsing of `for`/`in`.
+
+Don't allow unicode escapes in contextual keywords.
+
+### New features
+
+Parsing class members was factored into smaller methods to allow plugins to hook into it.
+
+## 5.2.1 (2017-10-30)
+
+### Bug fixes
+
+Fix a token context corruption bug.
+
+## 5.2.0 (2017-10-30)
+
+### Bug fixes
+
+Fix token context tracking for `class` and `function` in property-name position.
+
+Make sure `%*` isn't parsed as a valid operator.
+
+Allow shorthand properties `get` and `set` to be followed by default values.
+
+Disallow `super` when not in callee or object position.
+
+### New features
+
+Support [`directive` property](https://github.com/estree/estree/compare/b3de58c9997504d6fba04b72f76e6dd1619ee4eb...1da8e603237144f44710360f8feb7a9977e905e0) on directive expression statements.
+
+## 5.1.2 (2017-09-04)
+
+### Bug fixes
+
+Disable parsing of legacy HTML-style comments in modules.
+
+Fix parsing of async methods whose names are keywords.
+
+## 5.1.1 (2017-07-06)
+
+### Bug fixes
+
+Fix problem with disambiguating regexp and division after a class.
+
+## 5.1.0 (2017-07-05)
+
+### Bug fixes
+
+Fix tokenizing of regexps in an object-desctructuring `for`/`of` loop and after `yield`.
+
+Parse zero-prefixed numbers with non-octal digits as decimal.
+
+Allow object/array patterns in rest parameters.
+
+Don't error when `yield` is used as a property name.
+
+Allow `async` as a shorthand object property.
+
+### New features
+
+Implement the [template literal revision proposal](https://github.com/tc39/proposal-template-literal-revision) for ES9.
+
+## 5.0.3 (2017-04-01)
+
+### Bug fixes
+
+Fix spurious duplicate variable definition errors for named functions.
+
+## 5.0.2 (2017-03-30)
+
+### Bug fixes
+
+A binary operator after a parenthesized arrow expression is no longer incorrectly treated as an error.
+
+## 5.0.0 (2017-03-28)
+
+### Bug fixes
+
+Raise an error for duplicated lexical bindings.
+
+Fix spurious error when an assignement expression occurred after a spread expression.
+
+Accept regular expressions after `of` (in `for`/`of`), `yield` (in a generator), and braced arrow functions.
+
+Allow labels in front or `var` declarations, even in strict mode.
+
+### Breaking changes
+
+Parse declarations following `export default` as declaration nodes, not expressions. This means that class and function declarations nodes can now have `null` as their `id`.
+
+## 4.0.11 (2017-02-07)
+
+### Bug fixes
+
+Allow all forms of member expressions to be parenthesized as lvalue.
+
+## 4.0.10 (2017-02-07)
+
+### Bug fixes
+
+Don't expect semicolons after default-exported functions or classes, even when they are expressions.
+
+Check for use of `'use strict'` directives in non-simple parameter functions, even when already in strict mode.
+
+## 4.0.9 (2017-02-06)
+
+### Bug fixes
+
+Fix incorrect error raised for parenthesized simple assignment targets, so that `(x) = 1` parses again.
+
+## 4.0.8 (2017-02-03)
+
+### Bug fixes
+
+Solve spurious parenthesized pattern errors by temporarily erring on the side of accepting programs that our delayed errors don't handle correctly yet.
+
+## 4.0.7 (2017-02-02)
+
+### Bug fixes
+
+Accept invalidly rejected code like `(x).y = 2` again.
+
+Don't raise an error when a function _inside_ strict code has a non-simple parameter list.
+
+## 4.0.6 (2017-02-02)
+
+### Bug fixes
+
+Fix exponential behavior (manifesting itself as a complete hang for even relatively small source files) introduced by the new 'use strict' check.
+
+## 4.0.5 (2017-02-02)
+
+### Bug fixes
+
+Disallow parenthesized pattern expressions.
+
+Allow keywords as export names.
+
+Don't allow the `async` keyword to be parenthesized.
+
+Properly raise an error when a keyword contains a character escape.
+
+Allow `"use strict"` to appear after other string literal expressions.
+
+Disallow labeled declarations.
+
+## 4.0.4 (2016-12-19)
+
+### Bug fixes
+
+Fix crash when `export` was followed by a keyword that can't be
+exported.
+
+## 4.0.3 (2016-08-16)
+
+### Bug fixes
+
+Allow regular function declarations inside single-statement `if` branches in loose mode. Forbid them entirely in strict mode.
+
+Properly parse properties named `async` in ES2017 mode.
+
+Fix bug where reserved words were broken in ES2017 mode.
+
+## 4.0.2 (2016-08-11)
+
+### Bug fixes
+
+Don't ignore period or 'e' characters after octal numbers.
+
+Fix broken parsing for call expressions in default parameter values of arrow functions.
+
+## 4.0.1 (2016-08-08)
+
+### Bug fixes
+
+Fix false positives in duplicated export name errors.
+
+## 4.0.0 (2016-08-07)
+
+### Breaking changes
+
+The default `ecmaVersion` option value is now 7.
+
+A number of internal method signatures changed, so plugins might need to be updated.
+
+### Bug fixes
+
+The parser now raises errors on duplicated export names.
+
+`arguments` and `eval` can now be used in shorthand properties.
+
+Duplicate parameter names in non-simple argument lists now always produce an error.
+
+### New features
+
+The `ecmaVersion` option now also accepts year-style version numbers
+(2015, etc).
+
+Support for `async`/`await` syntax when `ecmaVersion` is >= 8.
+
+Support for trailing commas in call expressions when `ecmaVersion` is >= 8.
+
+## 3.3.0 (2016-07-25)
+
+### Bug fixes
+
+Fix bug in tokenizing of regexp operator after a function declaration.
+
+Fix parser crash when parsing an array pattern with a hole.
+
+### New features
+
+Implement check against complex argument lists in functions that enable strict mode in ES7.
+
+## 3.2.0 (2016-06-07)
+
+### Bug fixes
+
+Improve handling of lack of unicode regexp support in host
+environment.
+
+Properly reject shorthand properties whose name is a keyword.
+
+### New features
+
+Visitors created with `visit.make` now have their base as _prototype_, rather than copying properties into a fresh object.
+
+## 3.1.0 (2016-04-18)
+
+### Bug fixes
+
+Properly tokenize the division operator directly after a function expression.
+
+Allow trailing comma in destructuring arrays.
+
+## 3.0.4 (2016-02-25)
+
+### Fixes
+
+Allow update expressions as left-hand-side of the ES7 exponential operator.
+
+## 3.0.2 (2016-02-10)
+
+### Fixes
+
+Fix bug that accidentally made `undefined` a reserved word when parsing ES7.
+
+## 3.0.0 (2016-02-10)
+
+### Breaking changes
+
+The default value of the `ecmaVersion` option is now 6 (used to be 5).
+
+Support for comprehension syntax (which was dropped from the draft spec) has been removed.
+
+### Fixes
+
+`let` and `yield` are now “contextual keywords”, meaning you can mostly use them as identifiers in ES5 non-strict code.
+
+A parenthesized class or function expression after `export default` is now parsed correctly.
+
+### New features
+
+When `ecmaVersion` is set to 7, Acorn will parse the exponentiation operator (`**`).
+
+The identifier character ranges are now based on Unicode 8.0.0.
+
+Plugins can now override the `raiseRecoverable` method to override the way non-critical errors are handled.
+
+## 2.7.0 (2016-01-04)
+
+### Fixes
+
+Stop allowing rest parameters in setters.
+
+Disallow `y` rexexp flag in ES5.
+
+Disallow `\00` and `\000` escapes in strict mode.
+
+Raise an error when an import name is a reserved word.
+
+## 2.6.2 (2015-11-10)
+
+### Fixes
+
+Don't crash when no options object is passed.
+
+## 2.6.0 (2015-11-09)
+
+### Fixes
+
+Add `await` as a reserved word in module sources.
+
+Disallow `yield` in a parameter default value for a generator.
+
+Forbid using a comma after a rest pattern in an array destructuring.
+
+### New features
+
+Support parsing stdin in command-line tool.
+
+## 2.5.0 (2015-10-27)
+
+### Fixes
+
+Fix tokenizer support in the command-line tool.
+
+Stop allowing `new.target` outside of functions.
+
+Remove legacy `guard` and `guardedHandler` properties from try nodes.
+
+Stop allowing multiple `__proto__` properties on an object literal in strict mode.
+
+Don't allow rest parameters to be non-identifier patterns.
+
+Check for duplicate paramter names in arrow functions.
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/LICENSE b/tools/tests/apps/modules-modern/imports/links/acorn/LICENSE
new file mode 100644
index 0000000000..2c0632b6a7
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2012-2018 by various contributors (see AUTHORS)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/README.md b/tools/tests/apps/modules-modern/imports/links/acorn/README.md
new file mode 100644
index 0000000000..585f2736fc
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/README.md
@@ -0,0 +1,270 @@
+# Acorn
+
+A tiny, fast JavaScript parser written in JavaScript.
+
+## Community
+
+Acorn is open source software released under an
+[MIT license](https://github.com/acornjs/acorn/blob/master/acorn/LICENSE).
+
+You are welcome to
+[report bugs](https://github.com/acornjs/acorn/issues) or create pull
+requests on [github](https://github.com/acornjs/acorn). For questions
+and discussion, please use the
+[Tern discussion forum](https://discuss.ternjs.net).
+
+## Installation
+
+The easiest way to install acorn is from [`npm`](https://www.npmjs.com/):
+
+```sh
+npm install acorn
+```
+
+Alternately, you can download the source and build acorn yourself:
+
+```sh
+git clone https://github.com/acornjs/acorn.git
+cd acorn
+npm install
+```
+
+## Interface
+
+**parse**`(input, options)` is the main interface to the library. The
+`input` parameter is a string, `options` can be undefined or an object
+setting some of the options listed below. The return value will be an
+abstract syntax tree object as specified by the [ESTree
+spec](https://github.com/estree/estree).
+
+```javascript
+let acorn = require("acorn");
+console.log(acorn.parse("1 + 1"));
+```
+
+When encountering a syntax error, the parser will raise a
+`SyntaxError` object with a meaningful message. The error object will
+have a `pos` property that indicates the string offset at which the
+error occurred, and a `loc` object that contains a `{line, column}`
+object referring to that same position.
+
+Options can be provided by passing a second argument, which should be
+an object containing any of these fields:
+
+- **ecmaVersion**: Indicates the ECMAScript version to parse. Must be
+ either 3, 5, 6 (2015), 7 (2016), 8 (2017), 9 (2018), 10 (2019) or 11
+ (2020, partial support). This influences support for strict mode,
+ the set of reserved words, and support for new syntax features.
+ Default is 10.
+
+ **NOTE**: Only 'stage 4' (finalized) ECMAScript features are being
+ implemented by Acorn. Other proposed new features can be implemented
+ through plugins.
+
+- **sourceType**: Indicate the mode the code should be parsed in. Can be
+ either `"script"` or `"module"`. This influences global strict mode
+ and parsing of `import` and `export` declarations.
+
+ **NOTE**: If set to `"module"`, then static `import` / `export` syntax
+ will be valid, even if `ecmaVersion` is less than 6.
+
+- **onInsertedSemicolon**: If given a callback, that callback will be
+ called whenever a missing semicolon is inserted by the parser. The
+ callback will be given the character offset of the point where the
+ semicolon is inserted as argument, and if `locations` is on, also a
+ `{line, column}` object representing this position.
+
+- **onTrailingComma**: Like `onInsertedSemicolon`, but for trailing
+ commas.
+
+- **allowReserved**: If `false`, using a reserved word will generate
+ an error. Defaults to `true` for `ecmaVersion` 3, `false` for higher
+ versions. When given the value `"never"`, reserved words and
+ keywords can also not be used as property names (as in Internet
+ Explorer's old parser).
+
+- **allowReturnOutsideFunction**: By default, a return statement at
+ the top level raises an error. Set this to `true` to accept such
+ code.
+
+- **allowImportExportEverywhere**: By default, `import` and `export`
+ declarations can only appear at a program's top level. Setting this
+ option to `true` allows them anywhere where a statement is allowed.
+
+- **allowAwaitOutsideFunction**: By default, `await` expressions can
+ only appear inside `async` functions. Setting this option to
+ `true` allows to have top-level `await` expressions. They are
+ still not allowed in non-`async` functions, though.
+
+- **allowHashBang**: When this is enabled (off by default), if the
+ code starts with the characters `#!` (as in a shellscript), the
+ first line will be treated as a comment.
+
+- **locations**: When `true`, each node has a `loc` object attached
+ with `start` and `end` subobjects, each of which contains the
+ one-based line and zero-based column numbers in `{line, column}`
+ form. Default is `false`.
+
+- **onToken**: If a function is passed for this option, each found
+ token will be passed in same format as tokens returned from
+ `tokenizer().getToken()`.
+
+ If array is passed, each found token is pushed to it.
+
+ Note that you are not allowed to call the parser from the
+ callback—that will corrupt its internal state.
+
+- **onComment**: If a function is passed for this option, whenever a
+ comment is encountered the function will be called with the
+ following parameters:
+
+ - `block`: `true` if the comment is a block comment, false if it
+ is a line comment.
+ - `text`: The content of the comment.
+ - `start`: Character offset of the start of the comment.
+ - `end`: Character offset of the end of the comment.
+
+ When the `locations` options is on, the `{line, column}` locations
+ of the comment’s start and end are passed as two additional
+ parameters.
+
+ If array is passed for this option, each found comment is pushed
+ to it as object in Esprima format:
+
+ ```javascript
+ {
+ "type": "Line" | "Block",
+ "value": "comment text",
+ "start": Number,
+ "end": Number,
+ // If `locations` option is on:
+ "loc": {
+ "start": {line: Number, column: Number}
+ "end": {line: Number, column: Number}
+ },
+ // If `ranges` option is on:
+ "range": [Number, Number]
+ }
+ ```
+
+ Note that you are not allowed to call the parser from the
+ callback—that will corrupt its internal state.
+
+- **ranges**: Nodes have their start and end characters offsets
+ recorded in `start` and `end` properties (directly on the node,
+ rather than the `loc` object, which holds line/column data. To also
+ add a
+ [semi-standardized](https://bugzilla.mozilla.org/show_bug.cgi?id=745678)
+ `range` property holding a `[start, end]` array with the same
+ numbers, set the `ranges` option to `true`.
+
+- **program**: It is possible to parse multiple files into a single
+ AST by passing the tree produced by parsing the first file as the
+ `program` option in subsequent parses. This will add the toplevel
+ forms of the parsed file to the "Program" (top) node of an existing
+ parse tree.
+
+- **sourceFile**: When the `locations` option is `true`, you can pass
+ this option to add a `source` attribute in every node’s `loc`
+ object. Note that the contents of this option are not examined or
+ processed in any way; you are free to use whatever format you
+ choose.
+
+- **directSourceFile**: Like `sourceFile`, but a `sourceFile` property
+ will be added (regardless of the `location` option) directly to the
+ nodes, rather than the `loc` object.
+
+- **preserveParens**: If this option is `true`, parenthesized expressions
+ are represented by (non-standard) `ParenthesizedExpression` nodes
+ that have a single `expression` property containing the expression
+ inside parentheses.
+
+**parseExpressionAt**`(input, offset, options)` will parse a single
+expression in a string, and return its AST. It will not complain if
+there is more of the string left after the expression.
+
+**tokenizer**`(input, options)` returns an object with a `getToken`
+method that can be called repeatedly to get the next token, a `{start,
+end, type, value}` object (with added `loc` property when the
+`locations` option is enabled and `range` property when the `ranges`
+option is enabled). When the token's type is `tokTypes.eof`, you
+should stop calling the method, since it will keep returning that same
+token forever.
+
+In ES6 environment, returned result can be used as any other
+protocol-compliant iterable:
+
+```javascript
+for (let token of acorn.tokenizer(str)) {
+ // iterate over the tokens
+}
+
+// transform code to array of tokens:
+var tokens = [...acorn.tokenizer(str)];
+```
+
+**tokTypes** holds an object mapping names to the token type objects
+that end up in the `type` properties of tokens.
+
+**getLineInfo**`(input, offset)` can be used to get a `{line,
+column}` object for a given program string and offset.
+
+### The `Parser` class
+
+Instances of the **`Parser`** class contain all the state and logic
+that drives a parse. It has static methods `parse`,
+`parseExpressionAt`, and `tokenizer` that match the top-level
+functions by the same name.
+
+When extending the parser with plugins, you need to call these methods
+on the extended version of the class. To extend a parser with plugins,
+you can use its static `extend` method.
+
+```javascript
+var acorn = require("acorn");
+var jsx = require("acorn-jsx");
+var JSXParser = acorn.Parser.extend(jsx());
+JSXParser.parse("foo( )");
+```
+
+The `extend` method takes any number of plugin values, and returns a
+new `Parser` class that includes the extra parser logic provided by
+the plugins.
+
+## Command line interface
+
+The `bin/acorn` utility can be used to parse a file from the command
+line. It accepts as arguments its input file and the following
+options:
+
+- `--ecma3|--ecma5|--ecma6|--ecma7|--ecma8|--ecma9|--ecma10`: Sets the ECMAScript version
+ to parse. Default is version 9.
+
+- `--module`: Sets the parsing mode to `"module"`. Is set to `"script"` otherwise.
+
+- `--locations`: Attaches a "loc" object to each node with "start" and
+ "end" subobjects, each of which contains the one-based line and
+ zero-based column numbers in `{line, column}` form.
+
+- `--allow-hash-bang`: If the code starts with the characters #! (as
+ in a shellscript), the first line will be treated as a comment.
+
+- `--compact`: No whitespace is used in the AST output.
+
+- `--silent`: Do not output the AST, just return the exit status.
+
+- `--help`: Print the usage information and quit.
+
+The utility spits out the syntax tree as JSON data.
+
+## Existing plugins
+
+ - [`acorn-jsx`](https://github.com/RReverser/acorn-jsx): Parse [Facebook JSX syntax extensions](https://github.com/facebook/jsx)
+
+Plugins for ECMAScript proposals:
+
+ - [`acorn-stage3`](https://github.com/acornjs/acorn-stage3): Parse most stage 3 proposals, bundling:
+ - [`acorn-class-fields`](https://github.com/acornjs/acorn-class-fields): Parse [class fields proposal](https://github.com/tc39/proposal-class-fields)
+ - [`acorn-import-meta`](https://github.com/acornjs/acorn-import-meta): Parse [import.meta proposal](https://github.com/tc39/proposal-import-meta)
+ - [`acorn-numeric-separator`](https://github.com/acornjs/acorn-numeric-separator): Parse [numeric separator proposal](https://github.com/tc39/proposal-numeric-separator)
+ - [`acorn-private-methods`](https://github.com/acornjs/acorn-private-methods): parse [private methods, getters and setters proposal](https://github.com/tc39/proposal-private-methods)n
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/package.json b/tools/tests/apps/modules-modern/imports/links/acorn/package.json
new file mode 100644
index 0000000000..ad2b4564c2
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "acorn",
+ "description": "ECMAScript parser",
+ "homepage": "https://github.com/acornjs/acorn",
+ "module": "src/index.js",
+ "version": "7.1.1",
+ "engines": {"node": ">=0.4.0"},
+ "maintainers": [
+ {
+ "name": "Marijn Haverbeke",
+ "email": "marijnh@gmail.com",
+ "web": "https://marijnhaverbeke.nl"
+ },
+ {
+ "name": "Ingvar Stepanyan",
+ "email": "me@rreverser.com",
+ "web": "https://rreverser.com/"
+ },
+ {
+ "name": "Adrian Heine",
+ "web": "http://adrianheine.de"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/acornjs/acorn.git"
+ },
+ "license": "MIT"
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/.eslintrc b/tools/tests/apps/modules-modern/imports/links/acorn/src/.eslintrc
new file mode 100644
index 0000000000..181c1b3692
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/.eslintrc
@@ -0,0 +1,34 @@
+{
+ "extends": [
+ "eslint:recommended",
+ "standard",
+ "plugin:import/errors",
+ "plugin:import/warnings"
+ ],
+ "rules": {
+ "curly": "off",
+ "eqeqeq": ["error", "always", { "null": "ignore" }],
+ "indent": ["error", 2, { "SwitchCase": 0, "VariableDeclarator": 2, "CallExpression": { "arguments": "off" } }],
+ "new-parens": "off",
+ "no-case-declarations": "off",
+ "no-cond-assign": "off",
+ "no-fallthrough": "off",
+ "no-labels": "off",
+ "no-mixed-operators": "off",
+ "no-return-assign": "off",
+ "no-unused-labels": "error",
+ "no-var": "error",
+ "object-curly-spacing": ["error", "never"],
+ "one-var": "off",
+ "quotes": ["error", "double"],
+ "semi-spacing": "off",
+ "space-before-function-paren": ["error", "never"]
+ },
+ "globals": {
+ "Packages": false,
+ "BigInt": false
+ },
+ "plugins": [
+ "import"
+ ]
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/bin/.eslintrc b/tools/tests/apps/modules-modern/imports/links/acorn/src/bin/.eslintrc
new file mode 100644
index 0000000000..2598b25f5f
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/bin/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "extends": "../.eslintrc",
+ "rules": {
+ "no-console": "off"
+ }
+}
\ No newline at end of file
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/bin/acorn.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/bin/acorn.js
new file mode 100644
index 0000000000..9a6712c874
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/bin/acorn.js
@@ -0,0 +1,62 @@
+import {basename} from "path"
+import {readFileSync as readFile} from "fs"
+import * as acorn from "acorn"
+
+let infile, forceFile, silent = false, compact = false, tokenize = false
+const options = {}
+
+function help(status) {
+ const print = (status === 0) ? console.log : console.error
+ print("usage: " + basename(process.argv[1]) + " [--ecma3|--ecma5|--ecma6|--ecma7|--ecma8|--ecma9|...|--ecma2015|--ecma2016|--ecma2017|--ecma2018|...]")
+ print(" [--tokenize] [--locations] [---allow-hash-bang] [--compact] [--silent] [--module] [--help] [--] [infile]")
+ process.exit(status)
+}
+
+for (let i = 2; i < process.argv.length; ++i) {
+ const arg = process.argv[i]
+ if ((arg === "-" || arg[0] !== "-") && !infile) infile = arg
+ else if (arg === "--" && !infile && i + 2 === process.argv.length) forceFile = infile = process.argv[++i]
+ else if (arg === "--locations") options.locations = true
+ else if (arg === "--allow-hash-bang") options.allowHashBang = true
+ else if (arg === "--silent") silent = true
+ else if (arg === "--compact") compact = true
+ else if (arg === "--help") help(0)
+ else if (arg === "--tokenize") tokenize = true
+ else if (arg === "--module") options.sourceType = "module"
+ else {
+ let match = arg.match(/^--ecma(\d+)$/)
+ if (match)
+ options.ecmaVersion = +match[1]
+ else
+ help(1)
+ }
+}
+
+function run(code) {
+ let result
+ try {
+ if (!tokenize) {
+ result = acorn.parse(code, options)
+ } else {
+ result = []
+ let tokenizer = acorn.tokenizer(code, options), token
+ do {
+ token = tokenizer.getToken()
+ result.push(token)
+ } while (token.type !== acorn.tokTypes.eof)
+ }
+ } catch (e) {
+ console.error(infile && infile !== "-" ? e.message.replace(/\(\d+:\d+\)$/, m => m.slice(0, 1) + infile + " " + m.slice(1)) : e.message)
+ process.exit(1)
+ }
+ if (!silent) console.log(JSON.stringify(result, null, compact ? null : 2))
+}
+
+if (forceFile || infile && infile !== "-") {
+ run(readFile(infile, "utf8"))
+} else {
+ let code = ""
+ process.stdin.resume()
+ process.stdin.on("data", chunk => code += chunk)
+ process.stdin.on("end", () => run(code))
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/expression.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/expression.js
new file mode 100644
index 0000000000..a50ea7531e
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/expression.js
@@ -0,0 +1,969 @@
+// A recursive descent parser operates by defining functions for all
+// syntactic elements, and recursively calling those, each function
+// advancing the input stream and returning an AST node. Precedence
+// of constructs (for example, the fact that `!x[1]` means `!(x[1])`
+// instead of `(!x)[1]` is handled by the fact that the parser
+// function that parses unary prefix operators is called first, and
+// in turn calls the function that parses `[]` subscripts — that
+// way, it'll receive the node for `x[1]` already parsed, and wraps
+// *that* in the unary operator node.
+//
+// Acorn uses an [operator precedence parser][opp] to handle binary
+// operator precedence, because it is much more compact than using
+// the technique outlined above, which uses different, nesting
+// functions to specify precedence, for all of the ten binary
+// precedence levels that JavaScript defines.
+//
+// [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser
+
+import {types as tt} from "./tokentype"
+import {Parser} from "./state"
+import {DestructuringErrors} from "./parseutil"
+import {lineBreak} from "./whitespace"
+import {functionFlags, SCOPE_ARROW, SCOPE_SUPER, SCOPE_DIRECT_SUPER, BIND_OUTSIDE, BIND_VAR} from "./scopeflags"
+
+const pp = Parser.prototype
+
+// Check if property name clashes with already added.
+// Object/class getters and setters are not allowed to clash —
+// either with each other or with an init property — and in
+// strict mode, init properties are also not allowed to be repeated.
+
+pp.checkPropClash = function(prop, propHash, refDestructuringErrors) {
+ if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement")
+ return
+ if (this.options.ecmaVersion >= 6 && (prop.computed || prop.method || prop.shorthand))
+ return
+ let {key} = prop, name
+ switch (key.type) {
+ case "Identifier": name = key.name; break
+ case "Literal": name = String(key.value); break
+ default: return
+ }
+ let {kind} = prop
+ if (this.options.ecmaVersion >= 6) {
+ if (name === "__proto__" && kind === "init") {
+ if (propHash.proto) {
+ if (refDestructuringErrors) {
+ if (refDestructuringErrors.doubleProto < 0)
+ refDestructuringErrors.doubleProto = key.start
+ // Backwards-compat kludge. Can be removed in version 6.0
+ } else this.raiseRecoverable(key.start, "Redefinition of __proto__ property")
+ }
+ propHash.proto = true
+ }
+ return
+ }
+ name = "$" + name
+ let other = propHash[name]
+ if (other) {
+ let redefinition
+ if (kind === "init") {
+ redefinition = this.strict && other.init || other.get || other.set
+ } else {
+ redefinition = other.init || other[kind]
+ }
+ if (redefinition)
+ this.raiseRecoverable(key.start, "Redefinition of property")
+ } else {
+ other = propHash[name] = {
+ init: false,
+ get: false,
+ set: false
+ }
+ }
+ other[kind] = true
+}
+
+// ### Expression parsing
+
+// These nest, from the most general expression type at the top to
+// 'atomic', nondivisible expression types at the bottom. Most of
+// the functions will simply let the function(s) below them parse,
+// and, *if* the syntactic construct they handle is present, wrap
+// the AST node that the inner parser gave them in another node.
+
+// Parse a full expression. The optional arguments are used to
+// forbid the `in` operator (in for loops initalization expressions)
+// and provide reference for storing '=' operator inside shorthand
+// property assignment in contexts where both object expression
+// and object pattern might appear (so it's possible to raise
+// delayed syntax error at correct position).
+
+pp.parseExpression = function(noIn, refDestructuringErrors) {
+ let startPos = this.start, startLoc = this.startLoc
+ let expr = this.parseMaybeAssign(noIn, refDestructuringErrors)
+ if (this.type === tt.comma) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.expressions = [expr]
+ while (this.eat(tt.comma)) node.expressions.push(this.parseMaybeAssign(noIn, refDestructuringErrors))
+ return this.finishNode(node, "SequenceExpression")
+ }
+ return expr
+}
+
+// Parse an assignment expression. This includes applications of
+// operators like `+=`.
+
+pp.parseMaybeAssign = function(noIn, refDestructuringErrors, afterLeftParse) {
+ if (this.isContextual("yield")) {
+ if (this.inGenerator) return this.parseYield(noIn)
+ // The tokenizer will assume an expression is allowed after
+ // `yield`, but this isn't that kind of yield
+ else this.exprAllowed = false
+ }
+
+ let ownDestructuringErrors = false, oldParenAssign = -1, oldTrailingComma = -1
+ if (refDestructuringErrors) {
+ oldParenAssign = refDestructuringErrors.parenthesizedAssign
+ oldTrailingComma = refDestructuringErrors.trailingComma
+ refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = -1
+ } else {
+ refDestructuringErrors = new DestructuringErrors
+ ownDestructuringErrors = true
+ }
+
+ let startPos = this.start, startLoc = this.startLoc
+ if (this.type === tt.parenL || this.type === tt.name)
+ this.potentialArrowAt = this.start
+ let left = this.parseMaybeConditional(noIn, refDestructuringErrors)
+ if (afterLeftParse) left = afterLeftParse.call(this, left, startPos, startLoc)
+ if (this.type.isAssign) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.operator = this.value
+ node.left = this.type === tt.eq ? this.toAssignable(left, false, refDestructuringErrors) : left
+ if (!ownDestructuringErrors) {
+ refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = refDestructuringErrors.doubleProto = -1
+ }
+ if (refDestructuringErrors.shorthandAssign >= node.left.start)
+ refDestructuringErrors.shorthandAssign = -1 // reset because shorthand default was used correctly
+ this.checkLVal(left)
+ this.next()
+ node.right = this.parseMaybeAssign(noIn)
+ return this.finishNode(node, "AssignmentExpression")
+ } else {
+ if (ownDestructuringErrors) this.checkExpressionErrors(refDestructuringErrors, true)
+ }
+ if (oldParenAssign > -1) refDestructuringErrors.parenthesizedAssign = oldParenAssign
+ if (oldTrailingComma > -1) refDestructuringErrors.trailingComma = oldTrailingComma
+ return left
+}
+
+// Parse a ternary conditional (`?:`) operator.
+
+pp.parseMaybeConditional = function(noIn, refDestructuringErrors) {
+ let startPos = this.start, startLoc = this.startLoc
+ let expr = this.parseExprOps(noIn, refDestructuringErrors)
+ if (this.checkExpressionErrors(refDestructuringErrors)) return expr
+ if (this.eat(tt.question)) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.test = expr
+ node.consequent = this.parseMaybeAssign()
+ this.expect(tt.colon)
+ node.alternate = this.parseMaybeAssign(noIn)
+ return this.finishNode(node, "ConditionalExpression")
+ }
+ return expr
+}
+
+// Start the precedence parser.
+
+pp.parseExprOps = function(noIn, refDestructuringErrors) {
+ let startPos = this.start, startLoc = this.startLoc
+ let expr = this.parseMaybeUnary(refDestructuringErrors, false)
+ if (this.checkExpressionErrors(refDestructuringErrors)) return expr
+ return expr.start === startPos && expr.type === "ArrowFunctionExpression" ? expr : this.parseExprOp(expr, startPos, startLoc, -1, noIn)
+}
+
+// Parse binary operators with the operator precedence parsing
+// algorithm. `left` is the left-hand side of the operator.
+// `minPrec` provides context that allows the function to stop and
+// defer further parser to one of its callers when it encounters an
+// operator that has a lower precedence than the set it is parsing.
+
+pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) {
+ let prec = this.type.binop
+ if (prec != null && (!noIn || this.type !== tt._in)) {
+ if (prec > minPrec) {
+ let logical = this.type === tt.logicalOR || this.type === tt.logicalAND
+ let op = this.value
+ this.next()
+ let startPos = this.start, startLoc = this.startLoc
+ let right = this.parseExprOp(this.parseMaybeUnary(null, false), startPos, startLoc, prec, noIn)
+ let node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical)
+ return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn)
+ }
+ }
+ return left
+}
+
+pp.buildBinary = function(startPos, startLoc, left, right, op, logical) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.left = left
+ node.operator = op
+ node.right = right
+ return this.finishNode(node, logical ? "LogicalExpression" : "BinaryExpression")
+}
+
+// Parse unary operators, both prefix and postfix.
+
+pp.parseMaybeUnary = function(refDestructuringErrors, sawUnary) {
+ let startPos = this.start, startLoc = this.startLoc, expr
+ if (this.isContextual("await") && (this.inAsync || (!this.inFunction && this.options.allowAwaitOutsideFunction))) {
+ expr = this.parseAwait()
+ sawUnary = true
+ } else if (this.type.prefix) {
+ let node = this.startNode(), update = this.type === tt.incDec
+ node.operator = this.value
+ node.prefix = true
+ this.next()
+ node.argument = this.parseMaybeUnary(null, true)
+ this.checkExpressionErrors(refDestructuringErrors, true)
+ if (update) this.checkLVal(node.argument)
+ else if (this.strict && node.operator === "delete" &&
+ node.argument.type === "Identifier")
+ this.raiseRecoverable(node.start, "Deleting local variable in strict mode")
+ else sawUnary = true
+ expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression")
+ } else {
+ expr = this.parseExprSubscripts(refDestructuringErrors)
+ if (this.checkExpressionErrors(refDestructuringErrors)) return expr
+ while (this.type.postfix && !this.canInsertSemicolon()) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.operator = this.value
+ node.prefix = false
+ node.argument = expr
+ this.checkLVal(expr)
+ this.next()
+ expr = this.finishNode(node, "UpdateExpression")
+ }
+ }
+
+ if (!sawUnary && this.eat(tt.starstar))
+ return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false), "**", false)
+ else
+ return expr
+}
+
+// Parse call, dot, and `[]`-subscript expressions.
+
+pp.parseExprSubscripts = function(refDestructuringErrors) {
+ let startPos = this.start, startLoc = this.startLoc
+ let expr = this.parseExprAtom(refDestructuringErrors)
+ if (expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")")
+ return expr
+ let result = this.parseSubscripts(expr, startPos, startLoc)
+ if (refDestructuringErrors && result.type === "MemberExpression") {
+ if (refDestructuringErrors.parenthesizedAssign >= result.start) refDestructuringErrors.parenthesizedAssign = -1
+ if (refDestructuringErrors.parenthesizedBind >= result.start) refDestructuringErrors.parenthesizedBind = -1
+ }
+ return result
+}
+
+pp.parseSubscripts = function(base, startPos, startLoc, noCalls) {
+ let maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" &&
+ this.lastTokEnd === base.end && !this.canInsertSemicolon() && base.end - base.start === 5 &&
+ this.potentialArrowAt === base.start
+ while (true) {
+ let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow)
+ if (element === base || element.type === "ArrowFunctionExpression") return element
+ base = element
+ }
+}
+
+pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) {
+ let computed = this.eat(tt.bracketL)
+ if (computed || this.eat(tt.dot)) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.object = base
+ node.property = computed ? this.parseExpression() : this.parseIdent(this.options.allowReserved !== "never")
+ node.computed = !!computed
+ if (computed) this.expect(tt.bracketR)
+ base = this.finishNode(node, "MemberExpression")
+ } else if (!noCalls && this.eat(tt.parenL)) {
+ let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
+ this.yieldPos = 0
+ this.awaitPos = 0
+ this.awaitIdentPos = 0
+ let exprList = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors)
+ if (maybeAsyncArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
+ this.checkPatternErrors(refDestructuringErrors, false)
+ this.checkYieldAwaitInDefaultParams()
+ if (this.awaitIdentPos > 0)
+ this.raise(this.awaitIdentPos, "Cannot use 'await' as identifier inside an async function")
+ this.yieldPos = oldYieldPos
+ this.awaitPos = oldAwaitPos
+ this.awaitIdentPos = oldAwaitIdentPos
+ return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList, true)
+ }
+ this.checkExpressionErrors(refDestructuringErrors, true)
+ this.yieldPos = oldYieldPos || this.yieldPos
+ this.awaitPos = oldAwaitPos || this.awaitPos
+ this.awaitIdentPos = oldAwaitIdentPos || this.awaitIdentPos
+ let node = this.startNodeAt(startPos, startLoc)
+ node.callee = base
+ node.arguments = exprList
+ base = this.finishNode(node, "CallExpression")
+ } else if (this.type === tt.backQuote) {
+ let node = this.startNodeAt(startPos, startLoc)
+ node.tag = base
+ node.quasi = this.parseTemplate({isTagged: true})
+ base = this.finishNode(node, "TaggedTemplateExpression")
+ }
+ return base
+}
+
+// Parse an atomic expression — either a single token that is an
+// expression, an expression started by a keyword like `function` or
+// `new`, or an expression wrapped in punctuation like `()`, `[]`,
+// or `{}`.
+
+pp.parseExprAtom = function(refDestructuringErrors) {
+ // If a division operator appears in an expression position, the
+ // tokenizer got confused, and we force it to read a regexp instead.
+ if (this.type === tt.slash) this.readRegexp()
+
+ let node, canBeArrow = this.potentialArrowAt === this.start
+ switch (this.type) {
+ case tt._super:
+ if (!this.allowSuper)
+ this.raise(this.start, "'super' keyword outside a method")
+ node = this.startNode()
+ this.next()
+ if (this.type === tt.parenL && !this.allowDirectSuper)
+ this.raise(node.start, "super() call outside constructor of a subclass")
+ // The `super` keyword can appear at below:
+ // SuperProperty:
+ // super [ Expression ]
+ // super . IdentifierName
+ // SuperCall:
+ // super ( Arguments )
+ if (this.type !== tt.dot && this.type !== tt.bracketL && this.type !== tt.parenL)
+ this.unexpected()
+ return this.finishNode(node, "Super")
+
+ case tt._this:
+ node = this.startNode()
+ this.next()
+ return this.finishNode(node, "ThisExpression")
+
+ case tt.name:
+ let startPos = this.start, startLoc = this.startLoc, containsEsc = this.containsEsc
+ let id = this.parseIdent(false)
+ if (this.options.ecmaVersion >= 8 && !containsEsc && id.name === "async" && !this.canInsertSemicolon() && this.eat(tt._function))
+ return this.parseFunction(this.startNodeAt(startPos, startLoc), 0, false, true)
+ if (canBeArrow && !this.canInsertSemicolon()) {
+ if (this.eat(tt.arrow))
+ return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], false)
+ if (this.options.ecmaVersion >= 8 && id.name === "async" && this.type === tt.name && !containsEsc) {
+ id = this.parseIdent(false)
+ if (this.canInsertSemicolon() || !this.eat(tt.arrow))
+ this.unexpected()
+ return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], true)
+ }
+ }
+ return id
+
+ case tt.regexp:
+ let value = this.value
+ node = this.parseLiteral(value.value)
+ node.regex = {pattern: value.pattern, flags: value.flags}
+ return node
+
+ case tt.num: case tt.string:
+ return this.parseLiteral(this.value)
+
+ case tt._null: case tt._true: case tt._false:
+ node = this.startNode()
+ node.value = this.type === tt._null ? null : this.type === tt._true
+ node.raw = this.type.keyword
+ this.next()
+ return this.finishNode(node, "Literal")
+
+ case tt.parenL:
+ let start = this.start, expr = this.parseParenAndDistinguishExpression(canBeArrow)
+ if (refDestructuringErrors) {
+ if (refDestructuringErrors.parenthesizedAssign < 0 && !this.isSimpleAssignTarget(expr))
+ refDestructuringErrors.parenthesizedAssign = start
+ if (refDestructuringErrors.parenthesizedBind < 0)
+ refDestructuringErrors.parenthesizedBind = start
+ }
+ return expr
+
+ case tt.bracketL:
+ node = this.startNode()
+ this.next()
+ node.elements = this.parseExprList(tt.bracketR, true, true, refDestructuringErrors)
+ return this.finishNode(node, "ArrayExpression")
+
+ case tt.braceL:
+ return this.parseObj(false, refDestructuringErrors)
+
+ case tt._function:
+ node = this.startNode()
+ this.next()
+ return this.parseFunction(node, 0)
+
+ case tt._class:
+ return this.parseClass(this.startNode(), false)
+
+ case tt._new:
+ return this.parseNew()
+
+ case tt.backQuote:
+ return this.parseTemplate()
+
+ case tt._import:
+ if (this.options.ecmaVersion >= 11) {
+ return this.parseExprImport()
+ } else {
+ return this.unexpected()
+ }
+
+ default:
+ this.unexpected()
+ }
+}
+
+pp.parseExprImport = function() {
+ const node = this.startNode()
+ this.next() // skip `import`
+ switch (this.type) {
+ case tt.parenL:
+ return this.parseDynamicImport(node)
+ default:
+ this.unexpected()
+ }
+}
+
+pp.parseDynamicImport = function(node) {
+ this.next() // skip `(`
+
+ // Parse node.source.
+ node.source = this.parseMaybeAssign()
+
+ // Verify ending.
+ if (!this.eat(tt.parenR)) {
+ const errorPos = this.start
+ if (this.eat(tt.comma) && this.eat(tt.parenR)) {
+ this.raiseRecoverable(errorPos, "Trailing comma is not allowed in import()")
+ } else {
+ this.unexpected(errorPos)
+ }
+ }
+
+ return this.finishNode(node, "ImportExpression")
+}
+
+pp.parseLiteral = function(value) {
+ let node = this.startNode()
+ node.value = value
+ node.raw = this.input.slice(this.start, this.end)
+ if (node.raw.charCodeAt(node.raw.length - 1) === 110) node.bigint = node.raw.slice(0, -1)
+ this.next()
+ return this.finishNode(node, "Literal")
+}
+
+pp.parseParenExpression = function() {
+ this.expect(tt.parenL)
+ let val = this.parseExpression()
+ this.expect(tt.parenR)
+ return val
+}
+
+pp.parseParenAndDistinguishExpression = function(canBeArrow) {
+ let startPos = this.start, startLoc = this.startLoc, val, allowTrailingComma = this.options.ecmaVersion >= 8
+ if (this.options.ecmaVersion >= 6) {
+ this.next()
+
+ let innerStartPos = this.start, innerStartLoc = this.startLoc
+ let exprList = [], first = true, lastIsComma = false
+ let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, spreadStart
+ this.yieldPos = 0
+ this.awaitPos = 0
+ // Do not save awaitIdentPos to allow checking awaits nested in parameters
+ while (this.type !== tt.parenR) {
+ first ? first = false : this.expect(tt.comma)
+ if (allowTrailingComma && this.afterTrailingComma(tt.parenR, true)) {
+ lastIsComma = true
+ break
+ } else if (this.type === tt.ellipsis) {
+ spreadStart = this.start
+ exprList.push(this.parseParenItem(this.parseRestBinding()))
+ if (this.type === tt.comma) this.raise(this.start, "Comma is not permitted after the rest element")
+ break
+ } else {
+ exprList.push(this.parseMaybeAssign(false, refDestructuringErrors, this.parseParenItem))
+ }
+ }
+ let innerEndPos = this.start, innerEndLoc = this.startLoc
+ this.expect(tt.parenR)
+
+ if (canBeArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
+ this.checkPatternErrors(refDestructuringErrors, false)
+ this.checkYieldAwaitInDefaultParams()
+ this.yieldPos = oldYieldPos
+ this.awaitPos = oldAwaitPos
+ return this.parseParenArrowList(startPos, startLoc, exprList)
+ }
+
+ if (!exprList.length || lastIsComma) this.unexpected(this.lastTokStart)
+ if (spreadStart) this.unexpected(spreadStart)
+ this.checkExpressionErrors(refDestructuringErrors, true)
+ this.yieldPos = oldYieldPos || this.yieldPos
+ this.awaitPos = oldAwaitPos || this.awaitPos
+
+ if (exprList.length > 1) {
+ val = this.startNodeAt(innerStartPos, innerStartLoc)
+ val.expressions = exprList
+ this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc)
+ } else {
+ val = exprList[0]
+ }
+ } else {
+ val = this.parseParenExpression()
+ }
+
+ if (this.options.preserveParens) {
+ let par = this.startNodeAt(startPos, startLoc)
+ par.expression = val
+ return this.finishNode(par, "ParenthesizedExpression")
+ } else {
+ return val
+ }
+}
+
+pp.parseParenItem = function(item) {
+ return item
+}
+
+pp.parseParenArrowList = function(startPos, startLoc, exprList) {
+ return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList)
+}
+
+// New's precedence is slightly tricky. It must allow its argument to
+// be a `[]` or dot subscript expression, but not a call — at least,
+// not without wrapping it in parentheses. Thus, it uses the noCalls
+// argument to parseSubscripts to prevent it from consuming the
+// argument list.
+
+const empty = []
+
+pp.parseNew = function() {
+ if (this.containsEsc) this.raiseRecoverable(this.start, "Escape sequence in keyword new")
+ let node = this.startNode()
+ let meta = this.parseIdent(true)
+ if (this.options.ecmaVersion >= 6 && this.eat(tt.dot)) {
+ node.meta = meta
+ let containsEsc = this.containsEsc
+ node.property = this.parseIdent(true)
+ if (node.property.name !== "target" || containsEsc)
+ this.raiseRecoverable(node.property.start, "The only valid meta property for new is new.target")
+ if (!this.inNonArrowFunction())
+ this.raiseRecoverable(node.start, "new.target can only be used in functions")
+ return this.finishNode(node, "MetaProperty")
+ }
+ let startPos = this.start, startLoc = this.startLoc, isImport = this.type === tt._import
+ node.callee = this.parseSubscripts(this.parseExprAtom(), startPos, startLoc, true)
+ if (isImport && node.callee.type === "ImportExpression") {
+ this.raise(startPos, "Cannot use new with import()")
+ }
+ if (this.eat(tt.parenL)) node.arguments = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false)
+ else node.arguments = empty
+ return this.finishNode(node, "NewExpression")
+}
+
+// Parse template expression.
+
+pp.parseTemplateElement = function({isTagged}) {
+ let elem = this.startNode()
+ if (this.type === tt.invalidTemplate) {
+ if (!isTagged) {
+ this.raiseRecoverable(this.start, "Bad escape sequence in untagged template literal")
+ }
+ elem.value = {
+ raw: this.value,
+ cooked: null
+ }
+ } else {
+ elem.value = {
+ raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"),
+ cooked: this.value
+ }
+ }
+ this.next()
+ elem.tail = this.type === tt.backQuote
+ return this.finishNode(elem, "TemplateElement")
+}
+
+pp.parseTemplate = function({isTagged = false} = {}) {
+ let node = this.startNode()
+ this.next()
+ node.expressions = []
+ let curElt = this.parseTemplateElement({isTagged})
+ node.quasis = [curElt]
+ while (!curElt.tail) {
+ if (this.type === tt.eof) this.raise(this.pos, "Unterminated template literal")
+ this.expect(tt.dollarBraceL)
+ node.expressions.push(this.parseExpression())
+ this.expect(tt.braceR)
+ node.quasis.push(curElt = this.parseTemplateElement({isTagged}))
+ }
+ this.next()
+ return this.finishNode(node, "TemplateLiteral")
+}
+
+pp.isAsyncProp = function(prop) {
+ return !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" &&
+ (this.type === tt.name || this.type === tt.num || this.type === tt.string || this.type === tt.bracketL || this.type.keyword || (this.options.ecmaVersion >= 9 && this.type === tt.star)) &&
+ !lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
+}
+
+// Parse an object literal or binding pattern.
+
+pp.parseObj = function(isPattern, refDestructuringErrors) {
+ let node = this.startNode(), first = true, propHash = {}
+ node.properties = []
+ this.next()
+ while (!this.eat(tt.braceR)) {
+ if (!first) {
+ this.expect(tt.comma)
+ if (this.options.ecmaVersion >= 5 && this.afterTrailingComma(tt.braceR)) break
+ } else first = false
+
+ const prop = this.parseProperty(isPattern, refDestructuringErrors)
+ if (!isPattern) this.checkPropClash(prop, propHash, refDestructuringErrors)
+ node.properties.push(prop)
+ }
+ return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression")
+}
+
+pp.parseProperty = function(isPattern, refDestructuringErrors) {
+ let prop = this.startNode(), isGenerator, isAsync, startPos, startLoc
+ if (this.options.ecmaVersion >= 9 && this.eat(tt.ellipsis)) {
+ if (isPattern) {
+ prop.argument = this.parseIdent(false)
+ if (this.type === tt.comma) {
+ this.raise(this.start, "Comma is not permitted after the rest element")
+ }
+ return this.finishNode(prop, "RestElement")
+ }
+ // To disallow parenthesized identifier via `this.toAssignable()`.
+ if (this.type === tt.parenL && refDestructuringErrors) {
+ if (refDestructuringErrors.parenthesizedAssign < 0) {
+ refDestructuringErrors.parenthesizedAssign = this.start
+ }
+ if (refDestructuringErrors.parenthesizedBind < 0) {
+ refDestructuringErrors.parenthesizedBind = this.start
+ }
+ }
+ // Parse argument.
+ prop.argument = this.parseMaybeAssign(false, refDestructuringErrors)
+ // To disallow trailing comma via `this.toAssignable()`.
+ if (this.type === tt.comma && refDestructuringErrors && refDestructuringErrors.trailingComma < 0) {
+ refDestructuringErrors.trailingComma = this.start
+ }
+ // Finish
+ return this.finishNode(prop, "SpreadElement")
+ }
+ if (this.options.ecmaVersion >= 6) {
+ prop.method = false
+ prop.shorthand = false
+ if (isPattern || refDestructuringErrors) {
+ startPos = this.start
+ startLoc = this.startLoc
+ }
+ if (!isPattern)
+ isGenerator = this.eat(tt.star)
+ }
+ let containsEsc = this.containsEsc
+ this.parsePropertyName(prop)
+ if (!isPattern && !containsEsc && this.options.ecmaVersion >= 8 && !isGenerator && this.isAsyncProp(prop)) {
+ isAsync = true
+ isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star)
+ this.parsePropertyName(prop, refDestructuringErrors)
+ } else {
+ isAsync = false
+ }
+ this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc)
+ return this.finishNode(prop, "Property")
+}
+
+pp.parsePropertyValue = function(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) {
+ if ((isGenerator || isAsync) && this.type === tt.colon)
+ this.unexpected()
+
+ if (this.eat(tt.colon)) {
+ prop.value = isPattern ? this.parseMaybeDefault(this.start, this.startLoc) : this.parseMaybeAssign(false, refDestructuringErrors)
+ prop.kind = "init"
+ } else if (this.options.ecmaVersion >= 6 && this.type === tt.parenL) {
+ if (isPattern) this.unexpected()
+ prop.kind = "init"
+ prop.method = true
+ prop.value = this.parseMethod(isGenerator, isAsync)
+ } else if (!isPattern && !containsEsc &&
+ this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" &&
+ (prop.key.name === "get" || prop.key.name === "set") &&
+ (this.type !== tt.comma && this.type !== tt.braceR)) {
+ if (isGenerator || isAsync) this.unexpected()
+ prop.kind = prop.key.name
+ this.parsePropertyName(prop)
+ prop.value = this.parseMethod(false)
+ let paramCount = prop.kind === "get" ? 0 : 1
+ if (prop.value.params.length !== paramCount) {
+ let start = prop.value.start
+ if (prop.kind === "get")
+ this.raiseRecoverable(start, "getter should have no params")
+ else
+ this.raiseRecoverable(start, "setter should have exactly one param")
+ } else {
+ if (prop.kind === "set" && prop.value.params[0].type === "RestElement")
+ this.raiseRecoverable(prop.value.params[0].start, "Setter cannot use rest params")
+ }
+ } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") {
+ if (isGenerator || isAsync) this.unexpected()
+ this.checkUnreserved(prop.key)
+ if (prop.key.name === "await" && !this.awaitIdentPos)
+ this.awaitIdentPos = startPos
+ prop.kind = "init"
+ if (isPattern) {
+ prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key)
+ } else if (this.type === tt.eq && refDestructuringErrors) {
+ if (refDestructuringErrors.shorthandAssign < 0)
+ refDestructuringErrors.shorthandAssign = this.start
+ prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key)
+ } else {
+ prop.value = prop.key
+ }
+ prop.shorthand = true
+ } else this.unexpected()
+}
+
+pp.parsePropertyName = function(prop) {
+ if (this.options.ecmaVersion >= 6) {
+ if (this.eat(tt.bracketL)) {
+ prop.computed = true
+ prop.key = this.parseMaybeAssign()
+ this.expect(tt.bracketR)
+ return prop.key
+ } else {
+ prop.computed = false
+ }
+ }
+ return prop.key = this.type === tt.num || this.type === tt.string ? this.parseExprAtom() : this.parseIdent(this.options.allowReserved !== "never")
+}
+
+// Initialize empty function node.
+
+pp.initFunction = function(node) {
+ node.id = null
+ if (this.options.ecmaVersion >= 6) node.generator = node.expression = false
+ if (this.options.ecmaVersion >= 8) node.async = false
+}
+
+// Parse object or class method.
+
+pp.parseMethod = function(isGenerator, isAsync, allowDirectSuper) {
+ let node = this.startNode(), oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
+
+ this.initFunction(node)
+ if (this.options.ecmaVersion >= 6)
+ node.generator = isGenerator
+ if (this.options.ecmaVersion >= 8)
+ node.async = !!isAsync
+
+ this.yieldPos = 0
+ this.awaitPos = 0
+ this.awaitIdentPos = 0
+ this.enterScope(functionFlags(isAsync, node.generator) | SCOPE_SUPER | (allowDirectSuper ? SCOPE_DIRECT_SUPER : 0))
+
+ this.expect(tt.parenL)
+ node.params = this.parseBindingList(tt.parenR, false, this.options.ecmaVersion >= 8)
+ this.checkYieldAwaitInDefaultParams()
+ this.parseFunctionBody(node, false, true)
+
+ this.yieldPos = oldYieldPos
+ this.awaitPos = oldAwaitPos
+ this.awaitIdentPos = oldAwaitIdentPos
+ return this.finishNode(node, "FunctionExpression")
+}
+
+// Parse arrow function expression with given parameters.
+
+pp.parseArrowExpression = function(node, params, isAsync) {
+ let oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
+
+ this.enterScope(functionFlags(isAsync, false) | SCOPE_ARROW)
+ this.initFunction(node)
+ if (this.options.ecmaVersion >= 8) node.async = !!isAsync
+
+ this.yieldPos = 0
+ this.awaitPos = 0
+ this.awaitIdentPos = 0
+
+ node.params = this.toAssignableList(params, true)
+ this.parseFunctionBody(node, true, false)
+
+ this.yieldPos = oldYieldPos
+ this.awaitPos = oldAwaitPos
+ this.awaitIdentPos = oldAwaitIdentPos
+ return this.finishNode(node, "ArrowFunctionExpression")
+}
+
+// Parse function body and check parameters.
+
+pp.parseFunctionBody = function(node, isArrowFunction, isMethod) {
+ let isExpression = isArrowFunction && this.type !== tt.braceL
+ let oldStrict = this.strict, useStrict = false
+
+ if (isExpression) {
+ node.body = this.parseMaybeAssign()
+ node.expression = true
+ this.checkParams(node, false)
+ } else {
+ let nonSimple = this.options.ecmaVersion >= 7 && !this.isSimpleParamList(node.params)
+ if (!oldStrict || nonSimple) {
+ useStrict = this.strictDirective(this.end)
+ // If this is a strict mode function, verify that argument names
+ // are not repeated, and it does not try to bind the words `eval`
+ // or `arguments`.
+ if (useStrict && nonSimple)
+ this.raiseRecoverable(node.start, "Illegal 'use strict' directive in function with non-simple parameter list")
+ }
+ // Start a new scope with regard to labels and the `inFunction`
+ // flag (restore them to their old value afterwards).
+ let oldLabels = this.labels
+ this.labels = []
+ if (useStrict) this.strict = true
+
+ // Add the params to varDeclaredNames to ensure that an error is thrown
+ // if a let/const declaration in the function clashes with one of the params.
+ this.checkParams(node, !oldStrict && !useStrict && !isArrowFunction && !isMethod && this.isSimpleParamList(node.params))
+ // Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval'
+ if (this.strict && node.id) this.checkLVal(node.id, BIND_OUTSIDE)
+ node.body = this.parseBlock(false, undefined, useStrict && !oldStrict)
+ node.expression = false
+ this.adaptDirectivePrologue(node.body.body)
+ this.labels = oldLabels
+ }
+ this.exitScope()
+}
+
+pp.isSimpleParamList = function(params) {
+ for (let param of params)
+ if (param.type !== "Identifier") return false
+ return true
+}
+
+// Checks function params for various disallowed patterns such as using "eval"
+// or "arguments" and duplicate parameters.
+
+pp.checkParams = function(node, allowDuplicates) {
+ let nameHash = {}
+ for (let param of node.params)
+ this.checkLVal(param, BIND_VAR, allowDuplicates ? null : nameHash)
+}
+
+// Parses a comma-separated list of expressions, and returns them as
+// an array. `close` is the token type that ends the list, and
+// `allowEmpty` can be turned on to allow subsequent commas with
+// nothing in between them to be parsed as `null` (which is needed
+// for array literals).
+
+pp.parseExprList = function(close, allowTrailingComma, allowEmpty, refDestructuringErrors) {
+ let elts = [], first = true
+ while (!this.eat(close)) {
+ if (!first) {
+ this.expect(tt.comma)
+ if (allowTrailingComma && this.afterTrailingComma(close)) break
+ } else first = false
+
+ let elt
+ if (allowEmpty && this.type === tt.comma)
+ elt = null
+ else if (this.type === tt.ellipsis) {
+ elt = this.parseSpread(refDestructuringErrors)
+ if (refDestructuringErrors && this.type === tt.comma && refDestructuringErrors.trailingComma < 0)
+ refDestructuringErrors.trailingComma = this.start
+ } else {
+ elt = this.parseMaybeAssign(false, refDestructuringErrors)
+ }
+ elts.push(elt)
+ }
+ return elts
+}
+
+pp.checkUnreserved = function({start, end, name}) {
+ if (this.inGenerator && name === "yield")
+ this.raiseRecoverable(start, "Cannot use 'yield' as identifier inside a generator")
+ if (this.inAsync && name === "await")
+ this.raiseRecoverable(start, "Cannot use 'await' as identifier inside an async function")
+ if (this.keywords.test(name))
+ this.raise(start, `Unexpected keyword '${name}'`)
+ if (this.options.ecmaVersion < 6 &&
+ this.input.slice(start, end).indexOf("\\") !== -1) return
+ const re = this.strict ? this.reservedWordsStrict : this.reservedWords
+ if (re.test(name)) {
+ if (!this.inAsync && name === "await")
+ this.raiseRecoverable(start, "Cannot use keyword 'await' outside an async function")
+ this.raiseRecoverable(start, `The keyword '${name}' is reserved`)
+ }
+}
+
+// Parse the next token as an identifier. If `liberal` is true (used
+// when parsing properties), it will also convert keywords into
+// identifiers.
+
+pp.parseIdent = function(liberal, isBinding) {
+ let node = this.startNode()
+ if (this.type === tt.name) {
+ node.name = this.value
+ } else if (this.type.keyword) {
+ node.name = this.type.keyword
+
+ // To fix https://github.com/acornjs/acorn/issues/575
+ // `class` and `function` keywords push new context into this.context.
+ // But there is no chance to pop the context if the keyword is consumed as an identifier such as a property name.
+ // If the previous token is a dot, this does not apply because the context-managing code already ignored the keyword
+ if ((node.name === "class" || node.name === "function") &&
+ (this.lastTokEnd !== this.lastTokStart + 1 || this.input.charCodeAt(this.lastTokStart) !== 46)) {
+ this.context.pop()
+ }
+ } else {
+ this.unexpected()
+ }
+ this.next(!!liberal)
+ this.finishNode(node, "Identifier")
+ if (!liberal) {
+ this.checkUnreserved(node)
+ if (node.name === "await" && !this.awaitIdentPos)
+ this.awaitIdentPos = node.start
+ }
+ return node
+}
+
+// Parses yield expression inside generator.
+
+pp.parseYield = function(noIn) {
+ if (!this.yieldPos) this.yieldPos = this.start
+
+ let node = this.startNode()
+ this.next()
+ if (this.type === tt.semi || this.canInsertSemicolon() || (this.type !== tt.star && !this.type.startsExpr)) {
+ node.delegate = false
+ node.argument = null
+ } else {
+ node.delegate = this.eat(tt.star)
+ node.argument = this.parseMaybeAssign(noIn)
+ }
+ return this.finishNode(node, "YieldExpression")
+}
+
+pp.parseAwait = function() {
+ if (!this.awaitPos) this.awaitPos = this.start
+
+ let node = this.startNode()
+ this.next()
+ node.argument = this.parseMaybeUnary(null, false)
+ return this.finishNode(node, "AwaitExpression")
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/identifier.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/identifier.js
new file mode 100644
index 0000000000..0d7c3d1364
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/identifier.js
@@ -0,0 +1,87 @@
+// Reserved word lists for various dialects of the language
+
+export const reservedWords = {
+ 3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile",
+ 5: "class enum extends super const export import",
+ 6: "enum",
+ strict: "implements interface let package private protected public static yield",
+ strictBind: "eval arguments"
+}
+
+// And the keywords
+
+const ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this"
+
+export const keywords = {
+ 5: ecma5AndLessKeywords,
+ "5module": ecma5AndLessKeywords + " export import",
+ 6: ecma5AndLessKeywords + " const class extends export import super"
+}
+
+export const keywordRelationalOperator = /^in(stanceof)?$/
+
+// ## Character categories
+
+// Big ugly regular expressions that match characters in the
+// whitespace, identifier, and identifier-start categories. These
+// are only applied when a character is found to actually have a
+// code point above 128.
+// Generated by `bin/generate-identifier-regex.js`.
+let nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u08a0-\u08b4\u08b6-\u08c7\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c88\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\u9ffc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7bf\ua7c2-\ua7ca\ua7f5-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc"
+let nonASCIIidentifierChars = "\u200c\u200d\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08d3-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf\u1ac0\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1df9\u1dfb-\u1dff\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f"
+
+const nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]")
+const nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]")
+
+nonASCIIidentifierStartChars = nonASCIIidentifierChars = null
+
+// These are a run-length and offset encoded representation of the
+// >0xffff code points that are a valid part of identifiers. The
+// offset starts at 0x10000, and each pair of numbers represents an
+// offset to the next range, and then a size of the range. They were
+// generated by bin/generate-identifier-regex.js
+
+// eslint-disable-next-line comma-spacing
+const astralIdentifierStartCodes = [0,11,2,25,2,18,2,1,2,14,3,13,35,122,70,52,268,28,4,48,48,31,14,29,6,37,11,29,3,35,5,7,2,4,43,157,19,35,5,35,5,39,9,51,157,310,10,21,11,7,153,5,3,0,2,43,2,1,4,0,3,22,11,22,10,30,66,18,2,1,11,21,11,25,71,55,7,1,65,0,16,3,2,2,2,28,43,28,4,28,36,7,2,27,28,53,11,21,11,18,14,17,111,72,56,50,14,50,14,35,349,41,7,1,79,28,11,0,9,21,107,20,28,22,13,52,76,44,33,24,27,35,30,0,3,0,9,34,4,0,13,47,15,3,22,0,2,0,36,17,2,24,85,6,2,0,2,3,2,14,2,9,8,46,39,7,3,1,3,21,2,6,2,1,2,4,4,0,19,0,13,4,159,52,19,3,21,2,31,47,21,1,2,0,185,46,42,3,37,47,21,0,60,42,14,0,72,26,230,43,117,63,32,7,3,0,3,7,2,1,2,23,16,0,2,0,95,7,3,38,17,0,2,0,29,0,11,39,8,0,22,0,12,45,20,0,35,56,264,8,2,36,18,0,50,29,113,6,2,1,2,37,22,0,26,5,2,1,2,31,15,0,328,18,190,0,80,921,103,110,18,195,2749,1070,4050,582,8634,568,8,30,114,29,19,47,17,3,32,20,6,18,689,63,129,74,6,0,67,12,65,1,2,0,29,6135,9,1237,43,8,8952,286,50,2,18,3,9,395,2309,106,6,12,4,8,8,9,5991,84,2,70,2,1,3,0,3,1,3,3,2,11,2,0,2,6,2,64,2,3,3,7,2,6,2,27,2,3,2,4,2,0,4,6,2,339,3,24,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,7,2357,44,11,6,17,0,370,43,1301,196,60,67,8,0,1205,3,2,26,2,1,2,0,3,0,2,9,2,3,2,0,2,0,7,0,5,0,2,0,2,0,2,2,2,1,2,0,3,0,2,0,2,0,2,0,2,0,2,1,2,0,3,3,2,6,2,3,2,3,2,0,2,9,2,16,6,2,2,4,2,16,4421,42717,35,4148,12,221,3,5761,15,7472,3104,541,1507,4938]
+
+// eslint-disable-next-line comma-spacing
+const astralIdentifierCodes = [509,0,227,0,150,4,294,9,1368,2,2,1,6,3,41,2,5,0,166,1,574,3,9,9,370,1,154,10,176,2,54,14,32,9,16,3,46,10,54,9,7,2,37,13,2,9,6,1,45,0,13,2,49,13,9,3,2,11,83,11,7,0,161,11,6,9,7,3,56,1,2,6,3,1,3,2,10,0,11,1,3,6,4,4,193,17,10,9,5,0,82,19,13,9,214,6,3,8,28,1,83,16,16,9,82,12,9,9,84,14,5,9,243,14,166,9,71,5,2,1,3,3,2,0,2,1,13,9,120,6,3,6,4,0,29,9,41,6,2,3,9,0,10,10,47,15,406,7,2,7,17,9,57,21,2,13,123,5,4,0,2,1,2,6,2,0,9,9,49,4,2,1,2,4,9,9,330,3,19306,9,135,4,60,6,26,9,1014,0,2,54,8,3,82,0,12,1,19628,1,5319,4,4,5,9,7,3,6,31,3,149,2,1418,49,513,54,5,49,9,0,15,0,23,4,2,14,1361,6,2,16,3,6,2,1,2,4,262,6,10,9,419,13,1495,6,110,6,6,9,4759,9,787719,239]
+
+// This has a complexity linear to the value of the code. The
+// assumption is that looking up astral identifier characters is
+// rare.
+function isInAstralSet(code, set) {
+ let pos = 0x10000
+ for (let i = 0; i < set.length; i += 2) {
+ pos += set[i]
+ if (pos > code) return false
+ pos += set[i + 1]
+ if (pos >= code) return true
+ }
+}
+
+// Test whether a given character code starts an identifier.
+
+export function isIdentifierStart(code, astral) {
+ if (code < 65) return code === 36
+ if (code < 91) return true
+ if (code < 97) return code === 95
+ if (code < 123) return true
+ if (code <= 0xffff) return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code))
+ if (astral === false) return false
+ return isInAstralSet(code, astralIdentifierStartCodes)
+}
+
+// Test whether a given character is part of an identifier.
+
+export function isIdentifierChar(code, astral) {
+ if (code < 48) return code === 36
+ if (code < 58) return true
+ if (code < 65) return false
+ if (code < 91) return true
+ if (code < 97) return code === 95
+ if (code < 123) return true
+ if (code <= 0xffff) return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code))
+ if (astral === false) return false
+ return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes)
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/index.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/index.js
new file mode 100644
index 0000000000..f466a3b9fc
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/index.js
@@ -0,0 +1,102 @@
+// Acorn is a tiny, fast JavaScript parser written in JavaScript.
+//
+// Acorn was written by Marijn Haverbeke, Ingvar Stepanyan, and
+// various contributors and released under an MIT license.
+//
+// Git repositories for Acorn are available at
+//
+// http://marijnhaverbeke.nl/git/acorn
+// https://github.com/acornjs/acorn.git
+//
+// Please use the [github bug tracker][ghbt] to report issues.
+//
+// [ghbt]: https://github.com/acornjs/acorn/issues
+//
+// [walk]: util/walk.js
+
+import {Parser} from "./state"
+import "./parseutil"
+import "./statement"
+import "./lval"
+import "./expression"
+import "./location"
+import "./scope"
+
+import {defaultOptions} from "./options"
+import {Position, SourceLocation, getLineInfo} from "./locutil"
+import {Node} from "./node"
+import {TokenType, types as tokTypes, keywords as keywordTypes} from "./tokentype"
+import {TokContext, types as tokContexts} from "./tokencontext"
+import {isIdentifierChar, isIdentifierStart} from "./identifier"
+import {Token} from "./tokenize"
+import {isNewLine, lineBreak, lineBreakG, nonASCIIwhitespace} from "./whitespace"
+
+export const version = "7.1.0"
+export {
+ Parser,
+ defaultOptions,
+ Position,
+ SourceLocation,
+ getLineInfo,
+ Node,
+ TokenType,
+ tokTypes,
+ keywordTypes,
+ TokContext,
+ tokContexts,
+ isIdentifierChar,
+ isIdentifierStart,
+ Token,
+ isNewLine,
+ lineBreak,
+ lineBreakG,
+ nonASCIIwhitespace
+}
+
+Parser.acorn = {
+ Parser,
+ version,
+ defaultOptions,
+ Position,
+ SourceLocation,
+ getLineInfo,
+ Node,
+ TokenType,
+ tokTypes,
+ keywordTypes,
+ TokContext,
+ tokContexts,
+ isIdentifierChar,
+ isIdentifierStart,
+ Token,
+ isNewLine,
+ lineBreak,
+ lineBreakG,
+ nonASCIIwhitespace
+}
+
+// The main exported interface (under `self.acorn` when in the
+// browser) is a `parse` function that takes a code string and
+// returns an abstract syntax tree as specified by [Mozilla parser
+// API][api].
+//
+// [api]: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API
+
+export function parse(input, options) {
+ return Parser.parse(input, options)
+}
+
+// This function tries to parse a single expression at a given
+// offset in a string. Useful for parsing mixed-language formats
+// that embed JavaScript expressions.
+
+export function parseExpressionAt(input, pos, options) {
+ return Parser.parseExpressionAt(input, pos, options)
+}
+
+// Acorn is organized as a tokenizer and a recursive-descent parser.
+// The `tokenizer` export provides an interface to the tokenizer.
+
+export function tokenizer(input, options) {
+ return Parser.tokenizer(input, options)
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/location.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/location.js
new file mode 100644
index 0000000000..99e71234cc
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/location.js
@@ -0,0 +1,26 @@
+import {Parser} from "./state"
+import {Position, getLineInfo} from "./locutil"
+
+const pp = Parser.prototype
+
+// This function is used to raise exceptions on parse errors. It
+// takes an offset integer (into the current `input`) to indicate
+// the location of the error, attaches the position to the end
+// of the error message, and then raises a `SyntaxError` with that
+// message.
+
+pp.raise = function(pos, message) {
+ let loc = getLineInfo(this.input, pos)
+ message += " (" + loc.line + ":" + loc.column + ")"
+ let err = new SyntaxError(message)
+ err.pos = pos; err.loc = loc; err.raisedAt = this.pos
+ throw err
+}
+
+pp.raiseRecoverable = pp.raise
+
+pp.curPosition = function() {
+ if (this.options.locations) {
+ return new Position(this.curLine, this.pos - this.lineStart)
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/locutil.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/locutil.js
new file mode 100644
index 0000000000..5a9465e017
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/locutil.js
@@ -0,0 +1,42 @@
+import {lineBreakG} from "./whitespace"
+
+// These are used when `options.locations` is on, for the
+// `startLoc` and `endLoc` properties.
+
+export class Position {
+ constructor(line, col) {
+ this.line = line
+ this.column = col
+ }
+
+ offset(n) {
+ return new Position(this.line, this.column + n)
+ }
+}
+
+export class SourceLocation {
+ constructor(p, start, end) {
+ this.start = start
+ this.end = end
+ if (p.sourceFile !== null) this.source = p.sourceFile
+ }
+}
+
+// The `getLineInfo` function is mostly useful when the
+// `locations` option is off (for performance reasons) and you
+// want to find the line/column position for a given character
+// offset. `input` should be the code string that the offset refers
+// into.
+
+export function getLineInfo(input, offset) {
+ for (let line = 1, cur = 0;;) {
+ lineBreakG.lastIndex = cur
+ let match = lineBreakG.exec(input)
+ if (match && match.index < offset) {
+ ++line
+ cur = match.index + match[0].length
+ } else {
+ return new Position(line, offset - cur)
+ }
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/lval.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/lval.js
new file mode 100644
index 0000000000..8ad4b26390
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/lval.js
@@ -0,0 +1,239 @@
+import {types as tt} from "./tokentype"
+import {Parser} from "./state"
+import {has} from "./util"
+import {BIND_NONE, BIND_OUTSIDE, BIND_LEXICAL} from "./scopeflags"
+
+const pp = Parser.prototype
+
+// Convert existing expression atom to assignable pattern
+// if possible.
+
+pp.toAssignable = function(node, isBinding, refDestructuringErrors) {
+ if (this.options.ecmaVersion >= 6 && node) {
+ switch (node.type) {
+ case "Identifier":
+ if (this.inAsync && node.name === "await")
+ this.raise(node.start, "Cannot use 'await' as identifier inside an async function")
+ break
+
+ case "ObjectPattern":
+ case "ArrayPattern":
+ case "RestElement":
+ break
+
+ case "ObjectExpression":
+ node.type = "ObjectPattern"
+ if (refDestructuringErrors) this.checkPatternErrors(refDestructuringErrors, true)
+ for (let prop of node.properties) {
+ this.toAssignable(prop, isBinding)
+ // Early error:
+ // AssignmentRestProperty[Yield, Await] :
+ // `...` DestructuringAssignmentTarget[Yield, Await]
+ //
+ // It is a Syntax Error if |DestructuringAssignmentTarget| is an |ArrayLiteral| or an |ObjectLiteral|.
+ if (
+ prop.type === "RestElement" &&
+ (prop.argument.type === "ArrayPattern" || prop.argument.type === "ObjectPattern")
+ ) {
+ this.raise(prop.argument.start, "Unexpected token")
+ }
+ }
+ break
+
+ case "Property":
+ // AssignmentProperty has type === "Property"
+ if (node.kind !== "init") this.raise(node.key.start, "Object pattern can't contain getter or setter")
+ this.toAssignable(node.value, isBinding)
+ break
+
+ case "ArrayExpression":
+ node.type = "ArrayPattern"
+ if (refDestructuringErrors) this.checkPatternErrors(refDestructuringErrors, true)
+ this.toAssignableList(node.elements, isBinding)
+ break
+
+ case "SpreadElement":
+ node.type = "RestElement"
+ this.toAssignable(node.argument, isBinding)
+ if (node.argument.type === "AssignmentPattern")
+ this.raise(node.argument.start, "Rest elements cannot have a default value")
+ break
+
+ case "AssignmentExpression":
+ if (node.operator !== "=") this.raise(node.left.end, "Only '=' operator can be used for specifying default value.")
+ node.type = "AssignmentPattern"
+ delete node.operator
+ this.toAssignable(node.left, isBinding)
+ // falls through to AssignmentPattern
+
+ case "AssignmentPattern":
+ break
+
+ case "ParenthesizedExpression":
+ this.toAssignable(node.expression, isBinding, refDestructuringErrors)
+ break
+
+ case "MemberExpression":
+ if (!isBinding) break
+
+ default:
+ this.raise(node.start, "Assigning to rvalue")
+ }
+ } else if (refDestructuringErrors) this.checkPatternErrors(refDestructuringErrors, true)
+ return node
+}
+
+// Convert list of expression atoms to binding list.
+
+pp.toAssignableList = function(exprList, isBinding) {
+ let end = exprList.length
+ for (let i = 0; i < end; i++) {
+ let elt = exprList[i]
+ if (elt) this.toAssignable(elt, isBinding)
+ }
+ if (end) {
+ let last = exprList[end - 1]
+ if (this.options.ecmaVersion === 6 && isBinding && last && last.type === "RestElement" && last.argument.type !== "Identifier")
+ this.unexpected(last.argument.start)
+ }
+ return exprList
+}
+
+// Parses spread element.
+
+pp.parseSpread = function(refDestructuringErrors) {
+ let node = this.startNode()
+ this.next()
+ node.argument = this.parseMaybeAssign(false, refDestructuringErrors)
+ return this.finishNode(node, "SpreadElement")
+}
+
+pp.parseRestBinding = function() {
+ let node = this.startNode()
+ this.next()
+
+ // RestElement inside of a function parameter must be an identifier
+ if (this.options.ecmaVersion === 6 && this.type !== tt.name)
+ this.unexpected()
+
+ node.argument = this.parseBindingAtom()
+
+ return this.finishNode(node, "RestElement")
+}
+
+// Parses lvalue (assignable) atom.
+
+pp.parseBindingAtom = function() {
+ if (this.options.ecmaVersion >= 6) {
+ switch (this.type) {
+ case tt.bracketL:
+ let node = this.startNode()
+ this.next()
+ node.elements = this.parseBindingList(tt.bracketR, true, true)
+ return this.finishNode(node, "ArrayPattern")
+
+ case tt.braceL:
+ return this.parseObj(true)
+ }
+ }
+ return this.parseIdent()
+}
+
+pp.parseBindingList = function(close, allowEmpty, allowTrailingComma) {
+ let elts = [], first = true
+ while (!this.eat(close)) {
+ if (first) first = false
+ else this.expect(tt.comma)
+ if (allowEmpty && this.type === tt.comma) {
+ elts.push(null)
+ } else if (allowTrailingComma && this.afterTrailingComma(close)) {
+ break
+ } else if (this.type === tt.ellipsis) {
+ let rest = this.parseRestBinding()
+ this.parseBindingListItem(rest)
+ elts.push(rest)
+ if (this.type === tt.comma) this.raise(this.start, "Comma is not permitted after the rest element")
+ this.expect(close)
+ break
+ } else {
+ let elem = this.parseMaybeDefault(this.start, this.startLoc)
+ this.parseBindingListItem(elem)
+ elts.push(elem)
+ }
+ }
+ return elts
+}
+
+pp.parseBindingListItem = function(param) {
+ return param
+}
+
+// Parses assignment pattern around given atom if possible.
+
+pp.parseMaybeDefault = function(startPos, startLoc, left) {
+ left = left || this.parseBindingAtom()
+ if (this.options.ecmaVersion < 6 || !this.eat(tt.eq)) return left
+ let node = this.startNodeAt(startPos, startLoc)
+ node.left = left
+ node.right = this.parseMaybeAssign()
+ return this.finishNode(node, "AssignmentPattern")
+}
+
+// Verify that a node is an lval — something that can be assigned
+// to.
+// bindingType can be either:
+// 'var' indicating that the lval creates a 'var' binding
+// 'let' indicating that the lval creates a lexical ('let' or 'const') binding
+// 'none' indicating that the binding should be checked for illegal identifiers, but not for duplicate references
+
+pp.checkLVal = function(expr, bindingType = BIND_NONE, checkClashes) {
+ switch (expr.type) {
+ case "Identifier":
+ if (bindingType === BIND_LEXICAL && expr.name === "let")
+ this.raiseRecoverable(expr.start, "let is disallowed as a lexically bound name")
+ if (this.strict && this.reservedWordsStrictBind.test(expr.name))
+ this.raiseRecoverable(expr.start, (bindingType ? "Binding " : "Assigning to ") + expr.name + " in strict mode")
+ if (checkClashes) {
+ if (has(checkClashes, expr.name))
+ this.raiseRecoverable(expr.start, "Argument name clash")
+ checkClashes[expr.name] = true
+ }
+ if (bindingType !== BIND_NONE && bindingType !== BIND_OUTSIDE) this.declareName(expr.name, bindingType, expr.start)
+ break
+
+ case "MemberExpression":
+ if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression")
+ break
+
+ case "ObjectPattern":
+ for (let prop of expr.properties)
+ this.checkLVal(prop, bindingType, checkClashes)
+ break
+
+ case "Property":
+ // AssignmentProperty has type === "Property"
+ this.checkLVal(expr.value, bindingType, checkClashes)
+ break
+
+ case "ArrayPattern":
+ for (let elem of expr.elements) {
+ if (elem) this.checkLVal(elem, bindingType, checkClashes)
+ }
+ break
+
+ case "AssignmentPattern":
+ this.checkLVal(expr.left, bindingType, checkClashes)
+ break
+
+ case "RestElement":
+ this.checkLVal(expr.argument, bindingType, checkClashes)
+ break
+
+ case "ParenthesizedExpression":
+ this.checkLVal(expr.expression, bindingType, checkClashes)
+ break
+
+ default:
+ this.raise(expr.start, (bindingType ? "Binding" : "Assigning to") + " rvalue")
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/node.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/node.js
new file mode 100644
index 0000000000..76b5b09825
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/node.js
@@ -0,0 +1,50 @@
+import {Parser} from "./state"
+import {SourceLocation} from "./locutil"
+
+export class Node {
+ constructor(parser, pos, loc) {
+ this.type = ""
+ this.start = pos
+ this.end = 0
+ if (parser.options.locations)
+ this.loc = new SourceLocation(parser, loc)
+ if (parser.options.directSourceFile)
+ this.sourceFile = parser.options.directSourceFile
+ if (parser.options.ranges)
+ this.range = [pos, 0]
+ }
+}
+
+// Start an AST node, attaching a start offset.
+
+const pp = Parser.prototype
+
+pp.startNode = function() {
+ return new Node(this, this.start, this.startLoc)
+}
+
+pp.startNodeAt = function(pos, loc) {
+ return new Node(this, pos, loc)
+}
+
+// Finish an AST node, adding `type` and `end` properties.
+
+function finishNodeAt(node, type, pos, loc) {
+ node.type = type
+ node.end = pos
+ if (this.options.locations)
+ node.loc.end = loc
+ if (this.options.ranges)
+ node.range[1] = pos
+ return node
+}
+
+pp.finishNode = function(node, type) {
+ return finishNodeAt.call(this, node, type, this.lastTokEnd, this.lastTokEndLoc)
+}
+
+// Finish node at given position
+
+pp.finishNodeAt = function(node, type, pos, loc) {
+ return finishNodeAt.call(this, node, type, pos, loc)
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/options.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/options.js
new file mode 100644
index 0000000000..807251fdfd
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/options.js
@@ -0,0 +1,130 @@
+import {has, isArray} from "./util"
+import {SourceLocation} from "./locutil"
+
+// A second optional argument can be given to further configure
+// the parser process. These options are recognized:
+
+export const defaultOptions = {
+ // `ecmaVersion` indicates the ECMAScript version to parse. Must be
+ // either 3, 5, 6 (2015), 7 (2016), 8 (2017), 9 (2018), or 10
+ // (2019). This influences support for strict mode, the set of
+ // reserved words, and support for new syntax features. The default
+ // is 10.
+ ecmaVersion: 10,
+ // `sourceType` indicates the mode the code should be parsed in.
+ // Can be either `"script"` or `"module"`. This influences global
+ // strict mode and parsing of `import` and `export` declarations.
+ sourceType: "script",
+ // `onInsertedSemicolon` can be a callback that will be called
+ // when a semicolon is automatically inserted. It will be passed
+ // the position of the comma as an offset, and if `locations` is
+ // enabled, it is given the location as a `{line, column}` object
+ // as second argument.
+ onInsertedSemicolon: null,
+ // `onTrailingComma` is similar to `onInsertedSemicolon`, but for
+ // trailing commas.
+ onTrailingComma: null,
+ // By default, reserved words are only enforced if ecmaVersion >= 5.
+ // Set `allowReserved` to a boolean value to explicitly turn this on
+ // an off. When this option has the value "never", reserved words
+ // and keywords can also not be used as property names.
+ allowReserved: null,
+ // When enabled, a return at the top level is not considered an
+ // error.
+ allowReturnOutsideFunction: false,
+ // When enabled, import/export statements are not constrained to
+ // appearing at the top of the program.
+ allowImportExportEverywhere: false,
+ // When enabled, await identifiers are allowed to appear at the top-level scope,
+ // but they are still not allowed in non-async functions.
+ allowAwaitOutsideFunction: false,
+ // When enabled, hashbang directive in the beginning of file
+ // is allowed and treated as a line comment.
+ allowHashBang: false,
+ // When `locations` is on, `loc` properties holding objects with
+ // `start` and `end` properties in `{line, column}` form (with
+ // line being 1-based and column 0-based) will be attached to the
+ // nodes.
+ locations: false,
+ // A function can be passed as `onToken` option, which will
+ // cause Acorn to call that function with object in the same
+ // format as tokens returned from `tokenizer().getToken()`. Note
+ // that you are not allowed to call the parser from the
+ // callback—that will corrupt its internal state.
+ onToken: null,
+ // A function can be passed as `onComment` option, which will
+ // cause Acorn to call that function with `(block, text, start,
+ // end)` parameters whenever a comment is skipped. `block` is a
+ // boolean indicating whether this is a block (`/* */`) comment,
+ // `text` is the content of the comment, and `start` and `end` are
+ // character offsets that denote the start and end of the comment.
+ // When the `locations` option is on, two more parameters are
+ // passed, the full `{line, column}` locations of the start and
+ // end of the comments. Note that you are not allowed to call the
+ // parser from the callback—that will corrupt its internal state.
+ onComment: null,
+ // Nodes have their start and end characters offsets recorded in
+ // `start` and `end` properties (directly on the node, rather than
+ // the `loc` object, which holds line/column data. To also add a
+ // [semi-standardized][range] `range` property holding a `[start,
+ // end]` array with the same numbers, set the `ranges` option to
+ // `true`.
+ //
+ // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678
+ ranges: false,
+ // It is possible to parse multiple files into a single AST by
+ // passing the tree produced by parsing the first file as
+ // `program` option in subsequent parses. This will add the
+ // toplevel forms of the parsed file to the `Program` (top) node
+ // of an existing parse tree.
+ program: null,
+ // When `locations` is on, you can pass this to record the source
+ // file in every node's `loc` object.
+ sourceFile: null,
+ // This value, if given, is stored in every node, whether
+ // `locations` is on or off.
+ directSourceFile: null,
+ // When enabled, parenthesized expressions are represented by
+ // (non-standard) ParenthesizedExpression nodes
+ preserveParens: false
+}
+
+// Interpret and default an options object
+
+export function getOptions(opts) {
+ let options = {}
+
+ for (let opt in defaultOptions)
+ options[opt] = opts && has(opts, opt) ? opts[opt] : defaultOptions[opt]
+
+ if (options.ecmaVersion >= 2015)
+ options.ecmaVersion -= 2009
+
+ if (options.allowReserved == null)
+ options.allowReserved = options.ecmaVersion < 5
+
+ if (isArray(options.onToken)) {
+ let tokens = options.onToken
+ options.onToken = (token) => tokens.push(token)
+ }
+ if (isArray(options.onComment))
+ options.onComment = pushComment(options, options.onComment)
+
+ return options
+}
+
+function pushComment(options, array) {
+ return function(block, text, start, end, startLoc, endLoc) {
+ let comment = {
+ type: block ? "Block" : "Line",
+ value: text,
+ start: start,
+ end: end
+ }
+ if (options.locations)
+ comment.loc = new SourceLocation(this, startLoc, endLoc)
+ if (options.ranges)
+ comment.range = [start, end]
+ array.push(comment)
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/parseutil.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/parseutil.js
new file mode 100644
index 0000000000..98e35237f5
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/parseutil.js
@@ -0,0 +1,151 @@
+import {types as tt} from "./tokentype"
+import {Parser} from "./state"
+import {lineBreak, skipWhiteSpace} from "./whitespace"
+
+const pp = Parser.prototype
+
+// ## Parser utilities
+
+const literal = /^(?:'((?:\\.|[^'])*?)'|"((?:\\.|[^"])*?)")/
+pp.strictDirective = function(start) {
+ for (;;) {
+ // Try to find string literal.
+ skipWhiteSpace.lastIndex = start
+ start += skipWhiteSpace.exec(this.input)[0].length
+ let match = literal.exec(this.input.slice(start))
+ if (!match) return false
+ if ((match[1] || match[2]) === "use strict") {
+ skipWhiteSpace.lastIndex = start + match[0].length
+ let spaceAfter = skipWhiteSpace.exec(this.input), end = spaceAfter.index + spaceAfter[0].length
+ let next = this.input.charAt(end)
+ return next === ";" || next === "}" ||
+ (lineBreak.test(spaceAfter[0]) &&
+ !(/[(`.[+\-/*%<>=,?^&]/.test(next) || next === "!" && this.input.charAt(end + 1) === "="))
+ }
+ start += match[0].length
+
+ // Skip semicolon, if any.
+ skipWhiteSpace.lastIndex = start
+ start += skipWhiteSpace.exec(this.input)[0].length
+ if (this.input[start] === ";")
+ start++
+ }
+}
+
+// Predicate that tests whether the next token is of the given
+// type, and if yes, consumes it as a side effect.
+
+pp.eat = function(type) {
+ if (this.type === type) {
+ this.next()
+ return true
+ } else {
+ return false
+ }
+}
+
+// Tests whether parsed token is a contextual keyword.
+
+pp.isContextual = function(name) {
+ return this.type === tt.name && this.value === name && !this.containsEsc
+}
+
+// Consumes contextual keyword if possible.
+
+pp.eatContextual = function(name) {
+ if (!this.isContextual(name)) return false
+ this.next()
+ return true
+}
+
+// Asserts that following token is given contextual keyword.
+
+pp.expectContextual = function(name) {
+ if (!this.eatContextual(name)) this.unexpected()
+}
+
+// Test whether a semicolon can be inserted at the current position.
+
+pp.canInsertSemicolon = function() {
+ return this.type === tt.eof ||
+ this.type === tt.braceR ||
+ lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
+}
+
+pp.insertSemicolon = function() {
+ if (this.canInsertSemicolon()) {
+ if (this.options.onInsertedSemicolon)
+ this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc)
+ return true
+ }
+}
+
+// Consume a semicolon, or, failing that, see if we are allowed to
+// pretend that there is a semicolon at this position.
+
+pp.semicolon = function() {
+ if (!this.eat(tt.semi) && !this.insertSemicolon()) this.unexpected()
+}
+
+pp.afterTrailingComma = function(tokType, notNext) {
+ if (this.type === tokType) {
+ if (this.options.onTrailingComma)
+ this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc)
+ if (!notNext)
+ this.next()
+ return true
+ }
+}
+
+// Expect a token of a given type. If found, consume it, otherwise,
+// raise an unexpected token error.
+
+pp.expect = function(type) {
+ this.eat(type) || this.unexpected()
+}
+
+// Raise an unexpected token error.
+
+pp.unexpected = function(pos) {
+ this.raise(pos != null ? pos : this.start, "Unexpected token")
+}
+
+export function DestructuringErrors() {
+ this.shorthandAssign =
+ this.trailingComma =
+ this.parenthesizedAssign =
+ this.parenthesizedBind =
+ this.doubleProto =
+ -1
+}
+
+pp.checkPatternErrors = function(refDestructuringErrors, isAssign) {
+ if (!refDestructuringErrors) return
+ if (refDestructuringErrors.trailingComma > -1)
+ this.raiseRecoverable(refDestructuringErrors.trailingComma, "Comma is not permitted after the rest element")
+ let parens = isAssign ? refDestructuringErrors.parenthesizedAssign : refDestructuringErrors.parenthesizedBind
+ if (parens > -1) this.raiseRecoverable(parens, "Parenthesized pattern")
+}
+
+pp.checkExpressionErrors = function(refDestructuringErrors, andThrow) {
+ if (!refDestructuringErrors) return false
+ let {shorthandAssign, doubleProto} = refDestructuringErrors
+ if (!andThrow) return shorthandAssign >= 0 || doubleProto >= 0
+ if (shorthandAssign >= 0)
+ this.raise(shorthandAssign, "Shorthand property assignments are valid only in destructuring patterns")
+ if (doubleProto >= 0)
+ this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property")
+}
+
+pp.checkYieldAwaitInDefaultParams = function() {
+ if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos))
+ this.raise(this.yieldPos, "Yield expression cannot be a default value")
+ if (this.awaitPos)
+ this.raise(this.awaitPos, "Await expression cannot be a default value")
+}
+
+pp.isSimpleAssignTarget = function(expr) {
+ if (expr.type === "ParenthesizedExpression")
+ return this.isSimpleAssignTarget(expr.expression)
+ return expr.type === "Identifier" || expr.type === "MemberExpression"
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/regexp.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/regexp.js
new file mode 100644
index 0000000000..605bce5203
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/regexp.js
@@ -0,0 +1,1046 @@
+import {isIdentifierStart, isIdentifierChar} from "./identifier.js"
+import {Parser} from "./state.js"
+import UNICODE_PROPERTY_VALUES from "./unicode-property-data.js"
+import {has} from "./util.js"
+
+const pp = Parser.prototype
+
+export class RegExpValidationState {
+ constructor(parser) {
+ this.parser = parser
+ this.validFlags = `gim${parser.options.ecmaVersion >= 6 ? "uy" : ""}${parser.options.ecmaVersion >= 9 ? "s" : ""}`
+ this.unicodeProperties = UNICODE_PROPERTY_VALUES[parser.options.ecmaVersion >= 11 ? 11 : parser.options.ecmaVersion]
+ this.source = ""
+ this.flags = ""
+ this.start = 0
+ this.switchU = false
+ this.switchN = false
+ this.pos = 0
+ this.lastIntValue = 0
+ this.lastStringValue = ""
+ this.lastAssertionIsQuantifiable = false
+ this.numCapturingParens = 0
+ this.maxBackReference = 0
+ this.groupNames = []
+ this.backReferenceNames = []
+ }
+
+ reset(start, pattern, flags) {
+ const unicode = flags.indexOf("u") !== -1
+ this.start = start | 0
+ this.source = pattern + ""
+ this.flags = flags
+ this.switchU = unicode && this.parser.options.ecmaVersion >= 6
+ this.switchN = unicode && this.parser.options.ecmaVersion >= 9
+ }
+
+ raise(message) {
+ this.parser.raiseRecoverable(this.start, `Invalid regular expression: /${this.source}/: ${message}`)
+ }
+
+ // If u flag is given, this returns the code point at the index (it combines a surrogate pair).
+ // Otherwise, this returns the code unit of the index (can be a part of a surrogate pair).
+ at(i) {
+ const s = this.source
+ const l = s.length
+ if (i >= l) {
+ return -1
+ }
+ const c = s.charCodeAt(i)
+ if (!this.switchU || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l) {
+ return c
+ }
+ const next = s.charCodeAt(i + 1)
+ return next >= 0xDC00 && next <= 0xDFFF ? (c << 10) + next - 0x35FDC00 : c
+ }
+
+ nextIndex(i) {
+ const s = this.source
+ const l = s.length
+ if (i >= l) {
+ return l
+ }
+ let c = s.charCodeAt(i), next
+ if (!this.switchU || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l ||
+ (next = s.charCodeAt(i + 1)) < 0xDC00 || next > 0xDFFF) {
+ return i + 1
+ }
+ return i + 2
+ }
+
+ current() {
+ return this.at(this.pos)
+ }
+
+ lookahead() {
+ return this.at(this.nextIndex(this.pos))
+ }
+
+ advance() {
+ this.pos = this.nextIndex(this.pos)
+ }
+
+ eat(ch) {
+ if (this.current() === ch) {
+ this.advance()
+ return true
+ }
+ return false
+ }
+}
+
+function codePointToString(ch) {
+ if (ch <= 0xFFFF) return String.fromCharCode(ch)
+ ch -= 0x10000
+ return String.fromCharCode((ch >> 10) + 0xD800, (ch & 0x03FF) + 0xDC00)
+}
+
+/**
+ * Validate the flags part of a given RegExpLiteral.
+ *
+ * @param {RegExpValidationState} state The state to validate RegExp.
+ * @returns {void}
+ */
+pp.validateRegExpFlags = function(state) {
+ const validFlags = state.validFlags
+ const flags = state.flags
+
+ for (let i = 0; i < flags.length; i++) {
+ const flag = flags.charAt(i)
+ if (validFlags.indexOf(flag) === -1) {
+ this.raise(state.start, "Invalid regular expression flag")
+ }
+ if (flags.indexOf(flag, i + 1) > -1) {
+ this.raise(state.start, "Duplicate regular expression flag")
+ }
+ }
+}
+
+/**
+ * Validate the pattern part of a given RegExpLiteral.
+ *
+ * @param {RegExpValidationState} state The state to validate RegExp.
+ * @returns {void}
+ */
+pp.validateRegExpPattern = function(state) {
+ this.regexp_pattern(state)
+
+ // The goal symbol for the parse is |Pattern[~U, ~N]|. If the result of
+ // parsing contains a |GroupName|, reparse with the goal symbol
+ // |Pattern[~U, +N]| and use this result instead. Throw a *SyntaxError*
+ // exception if _P_ did not conform to the grammar, if any elements of _P_
+ // were not matched by the parse, or if any Early Error conditions exist.
+ if (!state.switchN && this.options.ecmaVersion >= 9 && state.groupNames.length > 0) {
+ state.switchN = true
+ this.regexp_pattern(state)
+ }
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-Pattern
+pp.regexp_pattern = function(state) {
+ state.pos = 0
+ state.lastIntValue = 0
+ state.lastStringValue = ""
+ state.lastAssertionIsQuantifiable = false
+ state.numCapturingParens = 0
+ state.maxBackReference = 0
+ state.groupNames.length = 0
+ state.backReferenceNames.length = 0
+
+ this.regexp_disjunction(state)
+
+ if (state.pos !== state.source.length) {
+ // Make the same messages as V8.
+ if (state.eat(0x29 /* ) */)) {
+ state.raise("Unmatched ')'")
+ }
+ if (state.eat(0x5D /* ] */) || state.eat(0x7D /* } */)) {
+ state.raise("Lone quantifier brackets")
+ }
+ }
+ if (state.maxBackReference > state.numCapturingParens) {
+ state.raise("Invalid escape")
+ }
+ for (const name of state.backReferenceNames) {
+ if (state.groupNames.indexOf(name) === -1) {
+ state.raise("Invalid named capture referenced")
+ }
+ }
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-Disjunction
+pp.regexp_disjunction = function(state) {
+ this.regexp_alternative(state)
+ while (state.eat(0x7C /* | */)) {
+ this.regexp_alternative(state)
+ }
+
+ // Make the same message as V8.
+ if (this.regexp_eatQuantifier(state, true)) {
+ state.raise("Nothing to repeat")
+ }
+ if (state.eat(0x7B /* { */)) {
+ state.raise("Lone quantifier brackets")
+ }
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-Alternative
+pp.regexp_alternative = function(state) {
+ while (state.pos < state.source.length && this.regexp_eatTerm(state))
+ ;
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Term
+pp.regexp_eatTerm = function(state) {
+ if (this.regexp_eatAssertion(state)) {
+ // Handle `QuantifiableAssertion Quantifier` alternative.
+ // `state.lastAssertionIsQuantifiable` is true if the last eaten Assertion
+ // is a QuantifiableAssertion.
+ if (state.lastAssertionIsQuantifiable && this.regexp_eatQuantifier(state)) {
+ // Make the same message as V8.
+ if (state.switchU) {
+ state.raise("Invalid quantifier")
+ }
+ }
+ return true
+ }
+
+ if (state.switchU ? this.regexp_eatAtom(state) : this.regexp_eatExtendedAtom(state)) {
+ this.regexp_eatQuantifier(state)
+ return true
+ }
+
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Assertion
+pp.regexp_eatAssertion = function(state) {
+ const start = state.pos
+ state.lastAssertionIsQuantifiable = false
+
+ // ^, $
+ if (state.eat(0x5E /* ^ */) || state.eat(0x24 /* $ */)) {
+ return true
+ }
+
+ // \b \B
+ if (state.eat(0x5C /* \ */)) {
+ if (state.eat(0x42 /* B */) || state.eat(0x62 /* b */)) {
+ return true
+ }
+ state.pos = start
+ }
+
+ // Lookahead / Lookbehind
+ if (state.eat(0x28 /* ( */) && state.eat(0x3F /* ? */)) {
+ let lookbehind = false
+ if (this.options.ecmaVersion >= 9) {
+ lookbehind = state.eat(0x3C /* < */)
+ }
+ if (state.eat(0x3D /* = */) || state.eat(0x21 /* ! */)) {
+ this.regexp_disjunction(state)
+ if (!state.eat(0x29 /* ) */)) {
+ state.raise("Unterminated group")
+ }
+ state.lastAssertionIsQuantifiable = !lookbehind
+ return true
+ }
+ }
+
+ state.pos = start
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-Quantifier
+pp.regexp_eatQuantifier = function(state, noError = false) {
+ if (this.regexp_eatQuantifierPrefix(state, noError)) {
+ state.eat(0x3F /* ? */)
+ return true
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-QuantifierPrefix
+pp.regexp_eatQuantifierPrefix = function(state, noError) {
+ return (
+ state.eat(0x2A /* * */) ||
+ state.eat(0x2B /* + */) ||
+ state.eat(0x3F /* ? */) ||
+ this.regexp_eatBracedQuantifier(state, noError)
+ )
+}
+pp.regexp_eatBracedQuantifier = function(state, noError) {
+ const start = state.pos
+ if (state.eat(0x7B /* { */)) {
+ let min = 0, max = -1
+ if (this.regexp_eatDecimalDigits(state)) {
+ min = state.lastIntValue
+ if (state.eat(0x2C /* , */) && this.regexp_eatDecimalDigits(state)) {
+ max = state.lastIntValue
+ }
+ if (state.eat(0x7D /* } */)) {
+ // SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-term
+ if (max !== -1 && max < min && !noError) {
+ state.raise("numbers out of order in {} quantifier")
+ }
+ return true
+ }
+ }
+ if (state.switchU && !noError) {
+ state.raise("Incomplete quantifier")
+ }
+ state.pos = start
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-Atom
+pp.regexp_eatAtom = function(state) {
+ return (
+ this.regexp_eatPatternCharacters(state) ||
+ state.eat(0x2E /* . */) ||
+ this.regexp_eatReverseSolidusAtomEscape(state) ||
+ this.regexp_eatCharacterClass(state) ||
+ this.regexp_eatUncapturingGroup(state) ||
+ this.regexp_eatCapturingGroup(state)
+ )
+}
+pp.regexp_eatReverseSolidusAtomEscape = function(state) {
+ const start = state.pos
+ if (state.eat(0x5C /* \ */)) {
+ if (this.regexp_eatAtomEscape(state)) {
+ return true
+ }
+ state.pos = start
+ }
+ return false
+}
+pp.regexp_eatUncapturingGroup = function(state) {
+ const start = state.pos
+ if (state.eat(0x28 /* ( */)) {
+ if (state.eat(0x3F /* ? */) && state.eat(0x3A /* : */)) {
+ this.regexp_disjunction(state)
+ if (state.eat(0x29 /* ) */)) {
+ return true
+ }
+ state.raise("Unterminated group")
+ }
+ state.pos = start
+ }
+ return false
+}
+pp.regexp_eatCapturingGroup = function(state) {
+ if (state.eat(0x28 /* ( */)) {
+ if (this.options.ecmaVersion >= 9) {
+ this.regexp_groupSpecifier(state)
+ } else if (state.current() === 0x3F /* ? */) {
+ state.raise("Invalid group")
+ }
+ this.regexp_disjunction(state)
+ if (state.eat(0x29 /* ) */)) {
+ state.numCapturingParens += 1
+ return true
+ }
+ state.raise("Unterminated group")
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedAtom
+pp.regexp_eatExtendedAtom = function(state) {
+ return (
+ state.eat(0x2E /* . */) ||
+ this.regexp_eatReverseSolidusAtomEscape(state) ||
+ this.regexp_eatCharacterClass(state) ||
+ this.regexp_eatUncapturingGroup(state) ||
+ this.regexp_eatCapturingGroup(state) ||
+ this.regexp_eatInvalidBracedQuantifier(state) ||
+ this.regexp_eatExtendedPatternCharacter(state)
+ )
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-InvalidBracedQuantifier
+pp.regexp_eatInvalidBracedQuantifier = function(state) {
+ if (this.regexp_eatBracedQuantifier(state, true)) {
+ state.raise("Nothing to repeat")
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-SyntaxCharacter
+pp.regexp_eatSyntaxCharacter = function(state) {
+ const ch = state.current()
+ if (isSyntaxCharacter(ch)) {
+ state.lastIntValue = ch
+ state.advance()
+ return true
+ }
+ return false
+}
+function isSyntaxCharacter(ch) {
+ return (
+ ch === 0x24 /* $ */ ||
+ ch >= 0x28 /* ( */ && ch <= 0x2B /* + */ ||
+ ch === 0x2E /* . */ ||
+ ch === 0x3F /* ? */ ||
+ ch >= 0x5B /* [ */ && ch <= 0x5E /* ^ */ ||
+ ch >= 0x7B /* { */ && ch <= 0x7D /* } */
+ )
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-PatternCharacter
+// But eat eager.
+pp.regexp_eatPatternCharacters = function(state) {
+ const start = state.pos
+ let ch = 0
+ while ((ch = state.current()) !== -1 && !isSyntaxCharacter(ch)) {
+ state.advance()
+ }
+ return state.pos !== start
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedPatternCharacter
+pp.regexp_eatExtendedPatternCharacter = function(state) {
+ const ch = state.current()
+ if (
+ ch !== -1 &&
+ ch !== 0x24 /* $ */ &&
+ !(ch >= 0x28 /* ( */ && ch <= 0x2B /* + */) &&
+ ch !== 0x2E /* . */ &&
+ ch !== 0x3F /* ? */ &&
+ ch !== 0x5B /* [ */ &&
+ ch !== 0x5E /* ^ */ &&
+ ch !== 0x7C /* | */
+ ) {
+ state.advance()
+ return true
+ }
+ return false
+}
+
+// GroupSpecifier[U] ::
+// [empty]
+// `?` GroupName[?U]
+pp.regexp_groupSpecifier = function(state) {
+ if (state.eat(0x3F /* ? */)) {
+ if (this.regexp_eatGroupName(state)) {
+ if (state.groupNames.indexOf(state.lastStringValue) !== -1) {
+ state.raise("Duplicate capture group name")
+ }
+ state.groupNames.push(state.lastStringValue)
+ return
+ }
+ state.raise("Invalid group")
+ }
+}
+
+// GroupName[U] ::
+// `<` RegExpIdentifierName[?U] `>`
+// Note: this updates `state.lastStringValue` property with the eaten name.
+pp.regexp_eatGroupName = function(state) {
+ state.lastStringValue = ""
+ if (state.eat(0x3C /* < */)) {
+ if (this.regexp_eatRegExpIdentifierName(state) && state.eat(0x3E /* > */)) {
+ return true
+ }
+ state.raise("Invalid capture group name")
+ }
+ return false
+}
+
+// RegExpIdentifierName[U] ::
+// RegExpIdentifierStart[?U]
+// RegExpIdentifierName[?U] RegExpIdentifierPart[?U]
+// Note: this updates `state.lastStringValue` property with the eaten name.
+pp.regexp_eatRegExpIdentifierName = function(state) {
+ state.lastStringValue = ""
+ if (this.regexp_eatRegExpIdentifierStart(state)) {
+ state.lastStringValue += codePointToString(state.lastIntValue)
+ while (this.regexp_eatRegExpIdentifierPart(state)) {
+ state.lastStringValue += codePointToString(state.lastIntValue)
+ }
+ return true
+ }
+ return false
+}
+
+// RegExpIdentifierStart[U] ::
+// UnicodeIDStart
+// `$`
+// `_`
+// `\` RegExpUnicodeEscapeSequence[?U]
+pp.regexp_eatRegExpIdentifierStart = function(state) {
+ const start = state.pos
+ let ch = state.current()
+ state.advance()
+
+ if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state)) {
+ ch = state.lastIntValue
+ }
+ if (isRegExpIdentifierStart(ch)) {
+ state.lastIntValue = ch
+ return true
+ }
+
+ state.pos = start
+ return false
+}
+function isRegExpIdentifierStart(ch) {
+ return isIdentifierStart(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */
+}
+
+// RegExpIdentifierPart[U] ::
+// UnicodeIDContinue
+// `$`
+// `_`
+// `\` RegExpUnicodeEscapeSequence[?U]
+//
+//
+pp.regexp_eatRegExpIdentifierPart = function(state) {
+ const start = state.pos
+ let ch = state.current()
+ state.advance()
+
+ if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state)) {
+ ch = state.lastIntValue
+ }
+ if (isRegExpIdentifierPart(ch)) {
+ state.lastIntValue = ch
+ return true
+ }
+
+ state.pos = start
+ return false
+}
+function isRegExpIdentifierPart(ch) {
+ return isIdentifierChar(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */ || ch === 0x200C /* */ || ch === 0x200D /* */
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-AtomEscape
+pp.regexp_eatAtomEscape = function(state) {
+ if (
+ this.regexp_eatBackReference(state) ||
+ this.regexp_eatCharacterClassEscape(state) ||
+ this.regexp_eatCharacterEscape(state) ||
+ (state.switchN && this.regexp_eatKGroupName(state))
+ ) {
+ return true
+ }
+ if (state.switchU) {
+ // Make the same message as V8.
+ if (state.current() === 0x63 /* c */) {
+ state.raise("Invalid unicode escape")
+ }
+ state.raise("Invalid escape")
+ }
+ return false
+}
+pp.regexp_eatBackReference = function(state) {
+ const start = state.pos
+ if (this.regexp_eatDecimalEscape(state)) {
+ const n = state.lastIntValue
+ if (state.switchU) {
+ // For SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-atomescape
+ if (n > state.maxBackReference) {
+ state.maxBackReference = n
+ }
+ return true
+ }
+ if (n <= state.numCapturingParens) {
+ return true
+ }
+ state.pos = start
+ }
+ return false
+}
+pp.regexp_eatKGroupName = function(state) {
+ if (state.eat(0x6B /* k */)) {
+ if (this.regexp_eatGroupName(state)) {
+ state.backReferenceNames.push(state.lastStringValue)
+ return true
+ }
+ state.raise("Invalid named reference")
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-CharacterEscape
+pp.regexp_eatCharacterEscape = function(state) {
+ return (
+ this.regexp_eatControlEscape(state) ||
+ this.regexp_eatCControlLetter(state) ||
+ this.regexp_eatZero(state) ||
+ this.regexp_eatHexEscapeSequence(state) ||
+ this.regexp_eatRegExpUnicodeEscapeSequence(state) ||
+ (!state.switchU && this.regexp_eatLegacyOctalEscapeSequence(state)) ||
+ this.regexp_eatIdentityEscape(state)
+ )
+}
+pp.regexp_eatCControlLetter = function(state) {
+ const start = state.pos
+ if (state.eat(0x63 /* c */)) {
+ if (this.regexp_eatControlLetter(state)) {
+ return true
+ }
+ state.pos = start
+ }
+ return false
+}
+pp.regexp_eatZero = function(state) {
+ if (state.current() === 0x30 /* 0 */ && !isDecimalDigit(state.lookahead())) {
+ state.lastIntValue = 0
+ state.advance()
+ return true
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-ControlEscape
+pp.regexp_eatControlEscape = function(state) {
+ const ch = state.current()
+ if (ch === 0x74 /* t */) {
+ state.lastIntValue = 0x09 /* \t */
+ state.advance()
+ return true
+ }
+ if (ch === 0x6E /* n */) {
+ state.lastIntValue = 0x0A /* \n */
+ state.advance()
+ return true
+ }
+ if (ch === 0x76 /* v */) {
+ state.lastIntValue = 0x0B /* \v */
+ state.advance()
+ return true
+ }
+ if (ch === 0x66 /* f */) {
+ state.lastIntValue = 0x0C /* \f */
+ state.advance()
+ return true
+ }
+ if (ch === 0x72 /* r */) {
+ state.lastIntValue = 0x0D /* \r */
+ state.advance()
+ return true
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-ControlLetter
+pp.regexp_eatControlLetter = function(state) {
+ const ch = state.current()
+ if (isControlLetter(ch)) {
+ state.lastIntValue = ch % 0x20
+ state.advance()
+ return true
+ }
+ return false
+}
+function isControlLetter(ch) {
+ return (
+ (ch >= 0x41 /* A */ && ch <= 0x5A /* Z */) ||
+ (ch >= 0x61 /* a */ && ch <= 0x7A /* z */)
+ )
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-RegExpUnicodeEscapeSequence
+pp.regexp_eatRegExpUnicodeEscapeSequence = function(state) {
+ const start = state.pos
+
+ if (state.eat(0x75 /* u */)) {
+ if (this.regexp_eatFixedHexDigits(state, 4)) {
+ const lead = state.lastIntValue
+ if (state.switchU && lead >= 0xD800 && lead <= 0xDBFF) {
+ const leadSurrogateEnd = state.pos
+ if (state.eat(0x5C /* \ */) && state.eat(0x75 /* u */) && this.regexp_eatFixedHexDigits(state, 4)) {
+ const trail = state.lastIntValue
+ if (trail >= 0xDC00 && trail <= 0xDFFF) {
+ state.lastIntValue = (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000
+ return true
+ }
+ }
+ state.pos = leadSurrogateEnd
+ state.lastIntValue = lead
+ }
+ return true
+ }
+ if (
+ state.switchU &&
+ state.eat(0x7B /* { */) &&
+ this.regexp_eatHexDigits(state) &&
+ state.eat(0x7D /* } */) &&
+ isValidUnicode(state.lastIntValue)
+ ) {
+ return true
+ }
+ if (state.switchU) {
+ state.raise("Invalid unicode escape")
+ }
+ state.pos = start
+ }
+
+ return false
+}
+function isValidUnicode(ch) {
+ return ch >= 0 && ch <= 0x10FFFF
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-IdentityEscape
+pp.regexp_eatIdentityEscape = function(state) {
+ if (state.switchU) {
+ if (this.regexp_eatSyntaxCharacter(state)) {
+ return true
+ }
+ if (state.eat(0x2F /* / */)) {
+ state.lastIntValue = 0x2F /* / */
+ return true
+ }
+ return false
+ }
+
+ const ch = state.current()
+ if (ch !== 0x63 /* c */ && (!state.switchN || ch !== 0x6B /* k */)) {
+ state.lastIntValue = ch
+ state.advance()
+ return true
+ }
+
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalEscape
+pp.regexp_eatDecimalEscape = function(state) {
+ state.lastIntValue = 0
+ let ch = state.current()
+ if (ch >= 0x31 /* 1 */ && ch <= 0x39 /* 9 */) {
+ do {
+ state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */)
+ state.advance()
+ } while ((ch = state.current()) >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */)
+ return true
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClassEscape
+pp.regexp_eatCharacterClassEscape = function(state) {
+ const ch = state.current()
+
+ if (isCharacterClassEscape(ch)) {
+ state.lastIntValue = -1
+ state.advance()
+ return true
+ }
+
+ if (
+ state.switchU &&
+ this.options.ecmaVersion >= 9 &&
+ (ch === 0x50 /* P */ || ch === 0x70 /* p */)
+ ) {
+ state.lastIntValue = -1
+ state.advance()
+ if (
+ state.eat(0x7B /* { */) &&
+ this.regexp_eatUnicodePropertyValueExpression(state) &&
+ state.eat(0x7D /* } */)
+ ) {
+ return true
+ }
+ state.raise("Invalid property name")
+ }
+
+ return false
+}
+function isCharacterClassEscape(ch) {
+ return (
+ ch === 0x64 /* d */ ||
+ ch === 0x44 /* D */ ||
+ ch === 0x73 /* s */ ||
+ ch === 0x53 /* S */ ||
+ ch === 0x77 /* w */ ||
+ ch === 0x57 /* W */
+ )
+}
+
+// UnicodePropertyValueExpression ::
+// UnicodePropertyName `=` UnicodePropertyValue
+// LoneUnicodePropertyNameOrValue
+pp.regexp_eatUnicodePropertyValueExpression = function(state) {
+ const start = state.pos
+
+ // UnicodePropertyName `=` UnicodePropertyValue
+ if (this.regexp_eatUnicodePropertyName(state) && state.eat(0x3D /* = */)) {
+ const name = state.lastStringValue
+ if (this.regexp_eatUnicodePropertyValue(state)) {
+ const value = state.lastStringValue
+ this.regexp_validateUnicodePropertyNameAndValue(state, name, value)
+ return true
+ }
+ }
+ state.pos = start
+
+ // LoneUnicodePropertyNameOrValue
+ if (this.regexp_eatLoneUnicodePropertyNameOrValue(state)) {
+ const nameOrValue = state.lastStringValue
+ this.regexp_validateUnicodePropertyNameOrValue(state, nameOrValue)
+ return true
+ }
+ return false
+}
+pp.regexp_validateUnicodePropertyNameAndValue = function(state, name, value) {
+ if (!has(state.unicodeProperties.nonBinary, name))
+ state.raise("Invalid property name")
+ if (!state.unicodeProperties.nonBinary[name].test(value))
+ state.raise("Invalid property value")
+}
+pp.regexp_validateUnicodePropertyNameOrValue = function(state, nameOrValue) {
+ if (!state.unicodeProperties.binary.test(nameOrValue))
+ state.raise("Invalid property name")
+}
+
+// UnicodePropertyName ::
+// UnicodePropertyNameCharacters
+pp.regexp_eatUnicodePropertyName = function(state) {
+ let ch = 0
+ state.lastStringValue = ""
+ while (isUnicodePropertyNameCharacter(ch = state.current())) {
+ state.lastStringValue += codePointToString(ch)
+ state.advance()
+ }
+ return state.lastStringValue !== ""
+}
+function isUnicodePropertyNameCharacter(ch) {
+ return isControlLetter(ch) || ch === 0x5F /* _ */
+}
+
+// UnicodePropertyValue ::
+// UnicodePropertyValueCharacters
+pp.regexp_eatUnicodePropertyValue = function(state) {
+ let ch = 0
+ state.lastStringValue = ""
+ while (isUnicodePropertyValueCharacter(ch = state.current())) {
+ state.lastStringValue += codePointToString(ch)
+ state.advance()
+ }
+ return state.lastStringValue !== ""
+}
+function isUnicodePropertyValueCharacter(ch) {
+ return isUnicodePropertyNameCharacter(ch) || isDecimalDigit(ch)
+}
+
+// LoneUnicodePropertyNameOrValue ::
+// UnicodePropertyValueCharacters
+pp.regexp_eatLoneUnicodePropertyNameOrValue = function(state) {
+ return this.regexp_eatUnicodePropertyValue(state)
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClass
+pp.regexp_eatCharacterClass = function(state) {
+ if (state.eat(0x5B /* [ */)) {
+ state.eat(0x5E /* ^ */)
+ this.regexp_classRanges(state)
+ if (state.eat(0x5D /* ] */)) {
+ return true
+ }
+ // Unreachable since it threw "unterminated regular expression" error before.
+ state.raise("Unterminated character class")
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-ClassRanges
+// https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRanges
+// https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRangesNoDash
+pp.regexp_classRanges = function(state) {
+ while (this.regexp_eatClassAtom(state)) {
+ const left = state.lastIntValue
+ if (state.eat(0x2D /* - */) && this.regexp_eatClassAtom(state)) {
+ const right = state.lastIntValue
+ if (state.switchU && (left === -1 || right === -1)) {
+ state.raise("Invalid character class")
+ }
+ if (left !== -1 && right !== -1 && left > right) {
+ state.raise("Range out of order in character class")
+ }
+ }
+ }
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtom
+// https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtomNoDash
+pp.regexp_eatClassAtom = function(state) {
+ const start = state.pos
+
+ if (state.eat(0x5C /* \ */)) {
+ if (this.regexp_eatClassEscape(state)) {
+ return true
+ }
+ if (state.switchU) {
+ // Make the same message as V8.
+ const ch = state.current()
+ if (ch === 0x63 /* c */ || isOctalDigit(ch)) {
+ state.raise("Invalid class escape")
+ }
+ state.raise("Invalid escape")
+ }
+ state.pos = start
+ }
+
+ const ch = state.current()
+ if (ch !== 0x5D /* ] */) {
+ state.lastIntValue = ch
+ state.advance()
+ return true
+ }
+
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassEscape
+pp.regexp_eatClassEscape = function(state) {
+ const start = state.pos
+
+ if (state.eat(0x62 /* b */)) {
+ state.lastIntValue = 0x08 /* */
+ return true
+ }
+
+ if (state.switchU && state.eat(0x2D /* - */)) {
+ state.lastIntValue = 0x2D /* - */
+ return true
+ }
+
+ if (!state.switchU && state.eat(0x63 /* c */)) {
+ if (this.regexp_eatClassControlLetter(state)) {
+ return true
+ }
+ state.pos = start
+ }
+
+ return (
+ this.regexp_eatCharacterClassEscape(state) ||
+ this.regexp_eatCharacterEscape(state)
+ )
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassControlLetter
+pp.regexp_eatClassControlLetter = function(state) {
+ const ch = state.current()
+ if (isDecimalDigit(ch) || ch === 0x5F /* _ */) {
+ state.lastIntValue = ch % 0x20
+ state.advance()
+ return true
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence
+pp.regexp_eatHexEscapeSequence = function(state) {
+ const start = state.pos
+ if (state.eat(0x78 /* x */)) {
+ if (this.regexp_eatFixedHexDigits(state, 2)) {
+ return true
+ }
+ if (state.switchU) {
+ state.raise("Invalid escape")
+ }
+ state.pos = start
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalDigits
+pp.regexp_eatDecimalDigits = function(state) {
+ const start = state.pos
+ let ch = 0
+ state.lastIntValue = 0
+ while (isDecimalDigit(ch = state.current())) {
+ state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */)
+ state.advance()
+ }
+ return state.pos !== start
+}
+function isDecimalDigit(ch) {
+ return ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigits
+pp.regexp_eatHexDigits = function(state) {
+ const start = state.pos
+ let ch = 0
+ state.lastIntValue = 0
+ while (isHexDigit(ch = state.current())) {
+ state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch)
+ state.advance()
+ }
+ return state.pos !== start
+}
+function isHexDigit(ch) {
+ return (
+ (ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */) ||
+ (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) ||
+ (ch >= 0x61 /* a */ && ch <= 0x66 /* f */)
+ )
+}
+function hexToInt(ch) {
+ if (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) {
+ return 10 + (ch - 0x41 /* A */)
+ }
+ if (ch >= 0x61 /* a */ && ch <= 0x66 /* f */) {
+ return 10 + (ch - 0x61 /* a */)
+ }
+ return ch - 0x30 /* 0 */
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-LegacyOctalEscapeSequence
+// Allows only 0-377(octal) i.e. 0-255(decimal).
+pp.regexp_eatLegacyOctalEscapeSequence = function(state) {
+ if (this.regexp_eatOctalDigit(state)) {
+ const n1 = state.lastIntValue
+ if (this.regexp_eatOctalDigit(state)) {
+ const n2 = state.lastIntValue
+ if (n1 <= 3 && this.regexp_eatOctalDigit(state)) {
+ state.lastIntValue = n1 * 64 + n2 * 8 + state.lastIntValue
+ } else {
+ state.lastIntValue = n1 * 8 + n2
+ }
+ } else {
+ state.lastIntValue = n1
+ }
+ return true
+ }
+ return false
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-OctalDigit
+pp.regexp_eatOctalDigit = function(state) {
+ const ch = state.current()
+ if (isOctalDigit(ch)) {
+ state.lastIntValue = ch - 0x30 /* 0 */
+ state.advance()
+ return true
+ }
+ state.lastIntValue = 0
+ return false
+}
+function isOctalDigit(ch) {
+ return ch >= 0x30 /* 0 */ && ch <= 0x37 /* 7 */
+}
+
+// https://www.ecma-international.org/ecma-262/8.0/#prod-Hex4Digits
+// https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigit
+// And HexDigit HexDigit in https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence
+pp.regexp_eatFixedHexDigits = function(state, length) {
+ const start = state.pos
+ state.lastIntValue = 0
+ for (let i = 0; i < length; ++i) {
+ const ch = state.current()
+ if (!isHexDigit(ch)) {
+ state.pos = start
+ return false
+ }
+ state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch)
+ state.advance()
+ }
+ return true
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/scope.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/scope.js
new file mode 100644
index 0000000000..b0b9866611
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/scope.js
@@ -0,0 +1,95 @@
+import {Parser} from "./state"
+import {SCOPE_VAR, SCOPE_FUNCTION, SCOPE_TOP, SCOPE_ARROW, SCOPE_SIMPLE_CATCH, BIND_LEXICAL, BIND_SIMPLE_CATCH, BIND_FUNCTION} from "./scopeflags"
+
+const pp = Parser.prototype
+
+class Scope {
+ constructor(flags) {
+ this.flags = flags
+ // A list of var-declared names in the current lexical scope
+ this.var = []
+ // A list of lexically-declared names in the current lexical scope
+ this.lexical = []
+ // A list of lexically-declared FunctionDeclaration names in the current lexical scope
+ this.functions = []
+ }
+}
+
+// The functions in this module keep track of declared variables in the current scope in order to detect duplicate variable names.
+
+pp.enterScope = function(flags) {
+ this.scopeStack.push(new Scope(flags))
+}
+
+pp.exitScope = function() {
+ this.scopeStack.pop()
+}
+
+// The spec says:
+// > At the top level of a function, or script, function declarations are
+// > treated like var declarations rather than like lexical declarations.
+pp.treatFunctionsAsVarInScope = function(scope) {
+ return (scope.flags & SCOPE_FUNCTION) || !this.inModule && (scope.flags & SCOPE_TOP)
+}
+
+pp.declareName = function(name, bindingType, pos) {
+ let redeclared = false
+ if (bindingType === BIND_LEXICAL) {
+ const scope = this.currentScope()
+ redeclared = scope.lexical.indexOf(name) > -1 || scope.functions.indexOf(name) > -1 || scope.var.indexOf(name) > -1
+ scope.lexical.push(name)
+ if (this.inModule && (scope.flags & SCOPE_TOP))
+ delete this.undefinedExports[name]
+ } else if (bindingType === BIND_SIMPLE_CATCH) {
+ const scope = this.currentScope()
+ scope.lexical.push(name)
+ } else if (bindingType === BIND_FUNCTION) {
+ const scope = this.currentScope()
+ if (this.treatFunctionsAsVar)
+ redeclared = scope.lexical.indexOf(name) > -1
+ else
+ redeclared = scope.lexical.indexOf(name) > -1 || scope.var.indexOf(name) > -1
+ scope.functions.push(name)
+ } else {
+ for (let i = this.scopeStack.length - 1; i >= 0; --i) {
+ const scope = this.scopeStack[i]
+ if (scope.lexical.indexOf(name) > -1 && !((scope.flags & SCOPE_SIMPLE_CATCH) && scope.lexical[0] === name) ||
+ !this.treatFunctionsAsVarInScope(scope) && scope.functions.indexOf(name) > -1) {
+ redeclared = true
+ break
+ }
+ scope.var.push(name)
+ if (this.inModule && (scope.flags & SCOPE_TOP))
+ delete this.undefinedExports[name]
+ if (scope.flags & SCOPE_VAR) break
+ }
+ }
+ if (redeclared) this.raiseRecoverable(pos, `Identifier '${name}' has already been declared`)
+}
+
+pp.checkLocalExport = function(id) {
+ // scope.functions must be empty as Module code is always strict.
+ if (this.scopeStack[0].lexical.indexOf(id.name) === -1 &&
+ this.scopeStack[0].var.indexOf(id.name) === -1) {
+ this.undefinedExports[id.name] = id
+ }
+}
+
+pp.currentScope = function() {
+ return this.scopeStack[this.scopeStack.length - 1]
+}
+
+pp.currentVarScope = function() {
+ for (let i = this.scopeStack.length - 1;; i--) {
+ let scope = this.scopeStack[i]
+ if (scope.flags & SCOPE_VAR) return scope
+ }
+}
+
+// Could be useful for `this`, `new.target`, `super()`, `super.property`, and `super[property]`.
+pp.currentThisScope = function() {
+ for (let i = this.scopeStack.length - 1;; i--) {
+ let scope = this.scopeStack[i]
+ if (scope.flags & SCOPE_VAR && !(scope.flags & SCOPE_ARROW)) return scope
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/scopeflags.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/scopeflags.js
new file mode 100644
index 0000000000..ccb16ecc48
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/scopeflags.js
@@ -0,0 +1,24 @@
+// Each scope gets a bitset that may contain these flags
+export const
+ SCOPE_TOP = 1,
+ SCOPE_FUNCTION = 2,
+ SCOPE_VAR = SCOPE_TOP | SCOPE_FUNCTION,
+ SCOPE_ASYNC = 4,
+ SCOPE_GENERATOR = 8,
+ SCOPE_ARROW = 16,
+ SCOPE_SIMPLE_CATCH = 32,
+ SCOPE_SUPER = 64,
+ SCOPE_DIRECT_SUPER = 128
+
+export function functionFlags(async, generator) {
+ return SCOPE_FUNCTION | (async ? SCOPE_ASYNC : 0) | (generator ? SCOPE_GENERATOR : 0)
+}
+
+// Used in checkLVal and declareName to determine the type of a binding
+export const
+ BIND_NONE = 0, // Not a binding
+ BIND_VAR = 1, // Var-style binding
+ BIND_LEXICAL = 2, // Let- or const-style binding
+ BIND_FUNCTION = 3, // Function declaration
+ BIND_SIMPLE_CATCH = 4, // Simple (identifier pattern) catch binding
+ BIND_OUTSIDE = 5 // Special case for function names as bound inside the function
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/state.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/state.js
new file mode 100644
index 0000000000..eb4e0e1208
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/state.js
@@ -0,0 +1,124 @@
+import {reservedWords, keywords} from "./identifier"
+import {types as tt} from "./tokentype"
+import {lineBreak} from "./whitespace"
+import {getOptions} from "./options"
+import {wordsRegexp} from "./util"
+import {SCOPE_TOP, SCOPE_FUNCTION, SCOPE_ASYNC, SCOPE_GENERATOR, SCOPE_SUPER, SCOPE_DIRECT_SUPER} from "./scopeflags"
+
+export class Parser {
+ constructor(options, input, startPos) {
+ this.options = options = getOptions(options)
+ this.sourceFile = options.sourceFile
+ this.keywords = wordsRegexp(keywords[options.ecmaVersion >= 6 ? 6 : options.sourceType === "module" ? "5module" : 5])
+ let reserved = ""
+ if (options.allowReserved !== true) {
+ for (let v = options.ecmaVersion;; v--)
+ if (reserved = reservedWords[v]) break
+ if (options.sourceType === "module") reserved += " await"
+ }
+ this.reservedWords = wordsRegexp(reserved)
+ let reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict
+ this.reservedWordsStrict = wordsRegexp(reservedStrict)
+ this.reservedWordsStrictBind = wordsRegexp(reservedStrict + " " + reservedWords.strictBind)
+ this.input = String(input)
+
+ // Used to signal to callers of `readWord1` whether the word
+ // contained any escape sequences. This is needed because words with
+ // escape sequences must not be interpreted as keywords.
+ this.containsEsc = false
+
+ // Set up token state
+
+ // The current position of the tokenizer in the input.
+ if (startPos) {
+ this.pos = startPos
+ this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1
+ this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length
+ } else {
+ this.pos = this.lineStart = 0
+ this.curLine = 1
+ }
+
+ // Properties of the current token:
+ // Its type
+ this.type = tt.eof
+ // For tokens that include more information than their type, the value
+ this.value = null
+ // Its start and end offset
+ this.start = this.end = this.pos
+ // And, if locations are used, the {line, column} object
+ // corresponding to those offsets
+ this.startLoc = this.endLoc = this.curPosition()
+
+ // Position information for the previous token
+ this.lastTokEndLoc = this.lastTokStartLoc = null
+ this.lastTokStart = this.lastTokEnd = this.pos
+
+ // The context stack is used to superficially track syntactic
+ // context to predict whether a regular expression is allowed in a
+ // given position.
+ this.context = this.initialContext()
+ this.exprAllowed = true
+
+ // Figure out if it's a module code.
+ this.inModule = options.sourceType === "module"
+ this.strict = this.inModule || this.strictDirective(this.pos)
+
+ // Used to signify the start of a potential arrow function
+ this.potentialArrowAt = -1
+
+ // Positions to delayed-check that yield/await does not exist in default parameters.
+ this.yieldPos = this.awaitPos = this.awaitIdentPos = 0
+ // Labels in scope.
+ this.labels = []
+ // Thus-far undefined exports.
+ this.undefinedExports = {}
+
+ // If enabled, skip leading hashbang line.
+ if (this.pos === 0 && options.allowHashBang && this.input.slice(0, 2) === "#!")
+ this.skipLineComment(2)
+
+ // Scope tracking for duplicate variable names (see scope.js)
+ this.scopeStack = []
+ this.enterScope(SCOPE_TOP)
+
+ // For RegExp validation
+ this.regexpState = null
+ }
+
+ parse() {
+ let node = this.options.program || this.startNode()
+ this.nextToken()
+ return this.parseTopLevel(node)
+ }
+
+ get inFunction() { return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0 }
+ get inGenerator() { return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0 }
+ get inAsync() { return (this.currentVarScope().flags & SCOPE_ASYNC) > 0 }
+ get allowSuper() { return (this.currentThisScope().flags & SCOPE_SUPER) > 0 }
+ get allowDirectSuper() { return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0 }
+ get treatFunctionsAsVar() { return this.treatFunctionsAsVarInScope(this.currentScope()) }
+
+ // Switch to a getter for 7.0.0.
+ inNonArrowFunction() { return (this.currentThisScope().flags & SCOPE_FUNCTION) > 0 }
+
+ static extend(...plugins) {
+ let cls = this
+ for (let i = 0; i < plugins.length; i++) cls = plugins[i](cls)
+ return cls
+ }
+
+ static parse(input, options) {
+ return new this(options, input).parse()
+ }
+
+ static parseExpressionAt(input, pos, options) {
+ let parser = new this(options, input, pos)
+ parser.nextToken()
+ return parser.parseExpression()
+ }
+
+ static tokenizer(input, options) {
+ return new this(options, input)
+ }
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/statement.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/statement.js
new file mode 100644
index 0000000000..8c7e171a9f
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/statement.js
@@ -0,0 +1,877 @@
+import {types as tt} from "./tokentype"
+import {Parser} from "./state"
+import {lineBreak, skipWhiteSpace} from "./whitespace"
+import {isIdentifierStart, isIdentifierChar, keywordRelationalOperator} from "./identifier"
+import {has} from "./util"
+import {DestructuringErrors} from "./parseutil"
+import {functionFlags, SCOPE_SIMPLE_CATCH, BIND_SIMPLE_CATCH, BIND_LEXICAL, BIND_VAR, BIND_FUNCTION} from "./scopeflags"
+
+const pp = Parser.prototype
+
+// ### Statement parsing
+
+// Parse a program. Initializes the parser, reads any number of
+// statements, and wraps them in a Program node. Optionally takes a
+// `program` argument. If present, the statements will be appended
+// to its body instead of creating a new node.
+
+pp.parseTopLevel = function(node) {
+ let exports = {}
+ if (!node.body) node.body = []
+ while (this.type !== tt.eof) {
+ let stmt = this.parseStatement(null, true, exports)
+ node.body.push(stmt)
+ }
+ if (this.inModule)
+ for (let name of Object.keys(this.undefinedExports))
+ this.raiseRecoverable(this.undefinedExports[name].start, `Export '${name}' is not defined`)
+ this.adaptDirectivePrologue(node.body)
+ this.next()
+ node.sourceType = this.options.sourceType
+ return this.finishNode(node, "Program")
+}
+
+const loopLabel = {kind: "loop"}, switchLabel = {kind: "switch"}
+
+pp.isLet = function(context) {
+ if (this.options.ecmaVersion < 6 || !this.isContextual("let")) return false
+ skipWhiteSpace.lastIndex = this.pos
+ let skip = skipWhiteSpace.exec(this.input)
+ let next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next)
+ // For ambiguous cases, determine if a LexicalDeclaration (or only a
+ // Statement) is allowed here. If context is not empty then only a Statement
+ // is allowed. However, `let [` is an explicit negative lookahead for
+ // ExpressionStatement, so special-case it first.
+ if (nextCh === 91) return true // '['
+ if (context) return false
+
+ if (nextCh === 123) return true // '{'
+ if (isIdentifierStart(nextCh, true)) {
+ let pos = next + 1
+ while (isIdentifierChar(this.input.charCodeAt(pos), true)) ++pos
+ let ident = this.input.slice(next, pos)
+ if (!keywordRelationalOperator.test(ident)) return true
+ }
+ return false
+}
+
+// check 'async [no LineTerminator here] function'
+// - 'async /*foo*/ function' is OK.
+// - 'async /*\n*/ function' is invalid.
+pp.isAsyncFunction = function() {
+ if (this.options.ecmaVersion < 8 || !this.isContextual("async"))
+ return false
+
+ skipWhiteSpace.lastIndex = this.pos
+ let skip = skipWhiteSpace.exec(this.input)
+ let next = this.pos + skip[0].length
+ return !lineBreak.test(this.input.slice(this.pos, next)) &&
+ this.input.slice(next, next + 8) === "function" &&
+ (next + 8 === this.input.length || !isIdentifierChar(this.input.charAt(next + 8)))
+}
+
+// Parse a single statement.
+//
+// If expecting a statement and finding a slash operator, parse a
+// regular expression literal. This is to handle cases like
+// `if (foo) /blah/.exec(foo)`, where looking at the previous token
+// does not help.
+
+pp.parseStatement = function(context, topLevel, exports) {
+ let starttype = this.type, node = this.startNode(), kind
+
+ if (this.isLet(context)) {
+ starttype = tt._var
+ kind = "let"
+ }
+
+ // Most types of statements are recognized by the keyword they
+ // start with. Many are trivial to parse, some require a bit of
+ // complexity.
+
+ switch (starttype) {
+ case tt._break: case tt._continue: return this.parseBreakContinueStatement(node, starttype.keyword)
+ case tt._debugger: return this.parseDebuggerStatement(node)
+ case tt._do: return this.parseDoStatement(node)
+ case tt._for: return this.parseForStatement(node)
+ case tt._function:
+ // Function as sole body of either an if statement or a labeled statement
+ // works, but not when it is part of a labeled statement that is the sole
+ // body of an if statement.
+ if ((context && (this.strict || context !== "if" && context !== "label")) && this.options.ecmaVersion >= 6) this.unexpected()
+ return this.parseFunctionStatement(node, false, !context)
+ case tt._class:
+ if (context) this.unexpected()
+ return this.parseClass(node, true)
+ case tt._if: return this.parseIfStatement(node)
+ case tt._return: return this.parseReturnStatement(node)
+ case tt._switch: return this.parseSwitchStatement(node)
+ case tt._throw: return this.parseThrowStatement(node)
+ case tt._try: return this.parseTryStatement(node)
+ case tt._const: case tt._var:
+ kind = kind || this.value
+ if (context && kind !== "var") this.unexpected()
+ return this.parseVarStatement(node, kind)
+ case tt._while: return this.parseWhileStatement(node)
+ case tt._with: return this.parseWithStatement(node)
+ case tt.braceL: return this.parseBlock(true, node)
+ case tt.semi: return this.parseEmptyStatement(node)
+ case tt._export:
+ case tt._import:
+ if (this.options.ecmaVersion > 10 && starttype === tt._import) {
+ skipWhiteSpace.lastIndex = this.pos
+ let skip = skipWhiteSpace.exec(this.input)
+ let next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next)
+ if (nextCh === 40) // '('
+ return this.parseExpressionStatement(node, this.parseExpression())
+ }
+
+ if (!this.options.allowImportExportEverywhere) {
+ if (!topLevel)
+ this.raise(this.start, "'import' and 'export' may only appear at the top level")
+ if (!this.inModule)
+ this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'")
+ }
+ return starttype === tt._import ? this.parseImport(node) : this.parseExport(node, exports)
+
+ // If the statement does not start with a statement keyword or a
+ // brace, it's an ExpressionStatement or LabeledStatement. We
+ // simply start parsing an expression, and afterwards, if the
+ // next token is a colon and the expression was a simple
+ // Identifier node, we switch to interpreting it as a label.
+ default:
+ if (this.isAsyncFunction()) {
+ if (context) this.unexpected()
+ this.next()
+ return this.parseFunctionStatement(node, true, !context)
+ }
+
+ let maybeName = this.value, expr = this.parseExpression()
+ if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon))
+ return this.parseLabeledStatement(node, maybeName, expr, context)
+ else return this.parseExpressionStatement(node, expr)
+ }
+}
+
+pp.parseBreakContinueStatement = function(node, keyword) {
+ let isBreak = keyword === "break"
+ this.next()
+ if (this.eat(tt.semi) || this.insertSemicolon()) node.label = null
+ else if (this.type !== tt.name) this.unexpected()
+ else {
+ node.label = this.parseIdent()
+ this.semicolon()
+ }
+
+ // Verify that there is an actual destination to break or
+ // continue to.
+ let i = 0
+ for (; i < this.labels.length; ++i) {
+ let lab = this.labels[i]
+ if (node.label == null || lab.name === node.label.name) {
+ if (lab.kind != null && (isBreak || lab.kind === "loop")) break
+ if (node.label && isBreak) break
+ }
+ }
+ if (i === this.labels.length) this.raise(node.start, "Unsyntactic " + keyword)
+ return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement")
+}
+
+pp.parseDebuggerStatement = function(node) {
+ this.next()
+ this.semicolon()
+ return this.finishNode(node, "DebuggerStatement")
+}
+
+pp.parseDoStatement = function(node) {
+ this.next()
+ this.labels.push(loopLabel)
+ node.body = this.parseStatement("do")
+ this.labels.pop()
+ this.expect(tt._while)
+ node.test = this.parseParenExpression()
+ if (this.options.ecmaVersion >= 6)
+ this.eat(tt.semi)
+ else
+ this.semicolon()
+ return this.finishNode(node, "DoWhileStatement")
+}
+
+// Disambiguating between a `for` and a `for`/`in` or `for`/`of`
+// loop is non-trivial. Basically, we have to parse the init `var`
+// statement or expression, disallowing the `in` operator (see
+// the second parameter to `parseExpression`), and then check
+// whether the next token is `in` or `of`. When there is no init
+// part (semicolon immediately after the opening parenthesis), it
+// is a regular `for` loop.
+
+pp.parseForStatement = function(node) {
+ this.next()
+ let awaitAt = (this.options.ecmaVersion >= 9 && (this.inAsync || (!this.inFunction && this.options.allowAwaitOutsideFunction)) && this.eatContextual("await")) ? this.lastTokStart : -1
+ this.labels.push(loopLabel)
+ this.enterScope(0)
+ this.expect(tt.parenL)
+ if (this.type === tt.semi) {
+ if (awaitAt > -1) this.unexpected(awaitAt)
+ return this.parseFor(node, null)
+ }
+ let isLet = this.isLet()
+ if (this.type === tt._var || this.type === tt._const || isLet) {
+ let init = this.startNode(), kind = isLet ? "let" : this.value
+ this.next()
+ this.parseVar(init, true, kind)
+ this.finishNode(init, "VariableDeclaration")
+ if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) {
+ if (this.options.ecmaVersion >= 9) {
+ if (this.type === tt._in) {
+ if (awaitAt > -1) this.unexpected(awaitAt)
+ } else node.await = awaitAt > -1
+ }
+ return this.parseForIn(node, init)
+ }
+ if (awaitAt > -1) this.unexpected(awaitAt)
+ return this.parseFor(node, init)
+ }
+ let refDestructuringErrors = new DestructuringErrors
+ let init = this.parseExpression(true, refDestructuringErrors)
+ if (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) {
+ if (this.options.ecmaVersion >= 9) {
+ if (this.type === tt._in) {
+ if (awaitAt > -1) this.unexpected(awaitAt)
+ } else node.await = awaitAt > -1
+ }
+ this.toAssignable(init, false, refDestructuringErrors)
+ this.checkLVal(init)
+ return this.parseForIn(node, init)
+ } else {
+ this.checkExpressionErrors(refDestructuringErrors, true)
+ }
+ if (awaitAt > -1) this.unexpected(awaitAt)
+ return this.parseFor(node, init)
+}
+
+pp.parseFunctionStatement = function(node, isAsync, declarationPosition) {
+ this.next()
+ return this.parseFunction(node, FUNC_STATEMENT | (declarationPosition ? 0 : FUNC_HANGING_STATEMENT), false, isAsync)
+}
+
+pp.parseIfStatement = function(node) {
+ this.next()
+ node.test = this.parseParenExpression()
+ // allow function declarations in branches, but only in non-strict mode
+ node.consequent = this.parseStatement("if")
+ node.alternate = this.eat(tt._else) ? this.parseStatement("if") : null
+ return this.finishNode(node, "IfStatement")
+}
+
+pp.parseReturnStatement = function(node) {
+ if (!this.inFunction && !this.options.allowReturnOutsideFunction)
+ this.raise(this.start, "'return' outside of function")
+ this.next()
+
+ // In `return` (and `break`/`continue`), the keywords with
+ // optional arguments, we eagerly look for a semicolon or the
+ // possibility to insert one.
+
+ if (this.eat(tt.semi) || this.insertSemicolon()) node.argument = null
+ else { node.argument = this.parseExpression(); this.semicolon() }
+ return this.finishNode(node, "ReturnStatement")
+}
+
+pp.parseSwitchStatement = function(node) {
+ this.next()
+ node.discriminant = this.parseParenExpression()
+ node.cases = []
+ this.expect(tt.braceL)
+ this.labels.push(switchLabel)
+ this.enterScope(0)
+
+ // Statements under must be grouped (by label) in SwitchCase
+ // nodes. `cur` is used to keep the node that we are currently
+ // adding statements to.
+
+ let cur
+ for (let sawDefault = false; this.type !== tt.braceR;) {
+ if (this.type === tt._case || this.type === tt._default) {
+ let isCase = this.type === tt._case
+ if (cur) this.finishNode(cur, "SwitchCase")
+ node.cases.push(cur = this.startNode())
+ cur.consequent = []
+ this.next()
+ if (isCase) {
+ cur.test = this.parseExpression()
+ } else {
+ if (sawDefault) this.raiseRecoverable(this.lastTokStart, "Multiple default clauses")
+ sawDefault = true
+ cur.test = null
+ }
+ this.expect(tt.colon)
+ } else {
+ if (!cur) this.unexpected()
+ cur.consequent.push(this.parseStatement(null))
+ }
+ }
+ this.exitScope()
+ if (cur) this.finishNode(cur, "SwitchCase")
+ this.next() // Closing brace
+ this.labels.pop()
+ return this.finishNode(node, "SwitchStatement")
+}
+
+pp.parseThrowStatement = function(node) {
+ this.next()
+ if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start)))
+ this.raise(this.lastTokEnd, "Illegal newline after throw")
+ node.argument = this.parseExpression()
+ this.semicolon()
+ return this.finishNode(node, "ThrowStatement")
+}
+
+// Reused empty array added for node fields that are always empty.
+
+const empty = []
+
+pp.parseTryStatement = function(node) {
+ this.next()
+ node.block = this.parseBlock()
+ node.handler = null
+ if (this.type === tt._catch) {
+ let clause = this.startNode()
+ this.next()
+ if (this.eat(tt.parenL)) {
+ clause.param = this.parseBindingAtom()
+ let simple = clause.param.type === "Identifier"
+ this.enterScope(simple ? SCOPE_SIMPLE_CATCH : 0)
+ this.checkLVal(clause.param, simple ? BIND_SIMPLE_CATCH : BIND_LEXICAL)
+ this.expect(tt.parenR)
+ } else {
+ if (this.options.ecmaVersion < 10) this.unexpected()
+ clause.param = null
+ this.enterScope(0)
+ }
+ clause.body = this.parseBlock(false)
+ this.exitScope()
+ node.handler = this.finishNode(clause, "CatchClause")
+ }
+ node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null
+ if (!node.handler && !node.finalizer)
+ this.raise(node.start, "Missing catch or finally clause")
+ return this.finishNode(node, "TryStatement")
+}
+
+pp.parseVarStatement = function(node, kind) {
+ this.next()
+ this.parseVar(node, false, kind)
+ this.semicolon()
+ return this.finishNode(node, "VariableDeclaration")
+}
+
+pp.parseWhileStatement = function(node) {
+ this.next()
+ node.test = this.parseParenExpression()
+ this.labels.push(loopLabel)
+ node.body = this.parseStatement("while")
+ this.labels.pop()
+ return this.finishNode(node, "WhileStatement")
+}
+
+pp.parseWithStatement = function(node) {
+ if (this.strict) this.raise(this.start, "'with' in strict mode")
+ this.next()
+ node.object = this.parseParenExpression()
+ node.body = this.parseStatement("with")
+ return this.finishNode(node, "WithStatement")
+}
+
+pp.parseEmptyStatement = function(node) {
+ this.next()
+ return this.finishNode(node, "EmptyStatement")
+}
+
+pp.parseLabeledStatement = function(node, maybeName, expr, context) {
+ for (let label of this.labels)
+ if (label.name === maybeName)
+ this.raise(expr.start, "Label '" + maybeName + "' is already declared")
+ let kind = this.type.isLoop ? "loop" : this.type === tt._switch ? "switch" : null
+ for (let i = this.labels.length - 1; i >= 0; i--) {
+ let label = this.labels[i]
+ if (label.statementStart === node.start) {
+ // Update information about previous labels on this node
+ label.statementStart = this.start
+ label.kind = kind
+ } else break
+ }
+ this.labels.push({name: maybeName, kind, statementStart: this.start})
+ node.body = this.parseStatement(context ? context.indexOf("label") === -1 ? context + "label" : context : "label")
+ this.labels.pop()
+ node.label = expr
+ return this.finishNode(node, "LabeledStatement")
+}
+
+pp.parseExpressionStatement = function(node, expr) {
+ node.expression = expr
+ this.semicolon()
+ return this.finishNode(node, "ExpressionStatement")
+}
+
+// Parse a semicolon-enclosed block of statements, handling `"use
+// strict"` declarations when `allowStrict` is true (used for
+// function bodies).
+
+pp.parseBlock = function(createNewLexicalScope = true, node = this.startNode(), exitStrict) {
+ node.body = []
+ this.expect(tt.braceL)
+ if (createNewLexicalScope) this.enterScope(0)
+ while (this.type !== tt.braceR) {
+ let stmt = this.parseStatement(null)
+ node.body.push(stmt)
+ }
+ if (exitStrict) this.strict = false
+ this.next()
+ if (createNewLexicalScope) this.exitScope()
+ return this.finishNode(node, "BlockStatement")
+}
+
+// Parse a regular `for` loop. The disambiguation code in
+// `parseStatement` will already have parsed the init statement or
+// expression.
+
+pp.parseFor = function(node, init) {
+ node.init = init
+ this.expect(tt.semi)
+ node.test = this.type === tt.semi ? null : this.parseExpression()
+ this.expect(tt.semi)
+ node.update = this.type === tt.parenR ? null : this.parseExpression()
+ this.expect(tt.parenR)
+ node.body = this.parseStatement("for")
+ this.exitScope()
+ this.labels.pop()
+ return this.finishNode(node, "ForStatement")
+}
+
+// Parse a `for`/`in` and `for`/`of` loop, which are almost
+// same from parser's perspective.
+
+pp.parseForIn = function(node, init) {
+ const isForIn = this.type === tt._in
+ this.next()
+
+ if (
+ init.type === "VariableDeclaration" &&
+ init.declarations[0].init != null &&
+ (
+ !isForIn ||
+ this.options.ecmaVersion < 8 ||
+ this.strict ||
+ init.kind !== "var" ||
+ init.declarations[0].id.type !== "Identifier"
+ )
+ ) {
+ this.raise(
+ init.start,
+ `${
+ isForIn ? "for-in" : "for-of"
+ } loop variable declaration may not have an initializer`
+ )
+ } else if (init.type === "AssignmentPattern") {
+ this.raise(init.start, "Invalid left-hand side in for-loop")
+ }
+ node.left = init
+ node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign()
+ this.expect(tt.parenR)
+ node.body = this.parseStatement("for")
+ this.exitScope()
+ this.labels.pop()
+ return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement")
+}
+
+// Parse a list of variable declarations.
+
+pp.parseVar = function(node, isFor, kind) {
+ node.declarations = []
+ node.kind = kind
+ for (;;) {
+ let decl = this.startNode()
+ this.parseVarId(decl, kind)
+ if (this.eat(tt.eq)) {
+ decl.init = this.parseMaybeAssign(isFor)
+ } else if (kind === "const" && !(this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of")))) {
+ this.unexpected()
+ } else if (decl.id.type !== "Identifier" && !(isFor && (this.type === tt._in || this.isContextual("of")))) {
+ this.raise(this.lastTokEnd, "Complex binding patterns require an initialization value")
+ } else {
+ decl.init = null
+ }
+ node.declarations.push(this.finishNode(decl, "VariableDeclarator"))
+ if (!this.eat(tt.comma)) break
+ }
+ return node
+}
+
+pp.parseVarId = function(decl, kind) {
+ decl.id = this.parseBindingAtom()
+ this.checkLVal(decl.id, kind === "var" ? BIND_VAR : BIND_LEXICAL, false)
+}
+
+const FUNC_STATEMENT = 1, FUNC_HANGING_STATEMENT = 2, FUNC_NULLABLE_ID = 4
+
+// Parse a function declaration or literal (depending on the
+// `statement & FUNC_STATEMENT`).
+
+// Remove `allowExpressionBody` for 7.0.0, as it is only called with false
+pp.parseFunction = function(node, statement, allowExpressionBody, isAsync) {
+ this.initFunction(node)
+ if (this.options.ecmaVersion >= 9 || this.options.ecmaVersion >= 6 && !isAsync) {
+ if (this.type === tt.star && (statement & FUNC_HANGING_STATEMENT))
+ this.unexpected()
+ node.generator = this.eat(tt.star)
+ }
+ if (this.options.ecmaVersion >= 8)
+ node.async = !!isAsync
+
+ if (statement & FUNC_STATEMENT) {
+ node.id = (statement & FUNC_NULLABLE_ID) && this.type !== tt.name ? null : this.parseIdent()
+ if (node.id && !(statement & FUNC_HANGING_STATEMENT))
+ // If it is a regular function declaration in sloppy mode, then it is
+ // subject to Annex B semantics (BIND_FUNCTION). Otherwise, the binding
+ // mode depends on properties of the current scope (see
+ // treatFunctionsAsVar).
+ this.checkLVal(node.id, (this.strict || node.generator || node.async) ? this.treatFunctionsAsVar ? BIND_VAR : BIND_LEXICAL : BIND_FUNCTION)
+ }
+
+ let oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
+ this.yieldPos = 0
+ this.awaitPos = 0
+ this.awaitIdentPos = 0
+ this.enterScope(functionFlags(node.async, node.generator))
+
+ if (!(statement & FUNC_STATEMENT))
+ node.id = this.type === tt.name ? this.parseIdent() : null
+
+ this.parseFunctionParams(node)
+ this.parseFunctionBody(node, allowExpressionBody, false)
+
+ this.yieldPos = oldYieldPos
+ this.awaitPos = oldAwaitPos
+ this.awaitIdentPos = oldAwaitIdentPos
+ return this.finishNode(node, (statement & FUNC_STATEMENT) ? "FunctionDeclaration" : "FunctionExpression")
+}
+
+pp.parseFunctionParams = function(node) {
+ this.expect(tt.parenL)
+ node.params = this.parseBindingList(tt.parenR, false, this.options.ecmaVersion >= 8)
+ this.checkYieldAwaitInDefaultParams()
+}
+
+// Parse a class declaration or literal (depending on the
+// `isStatement` parameter).
+
+pp.parseClass = function(node, isStatement) {
+ this.next()
+
+ // ecma-262 14.6 Class Definitions
+ // A class definition is always strict mode code.
+ const oldStrict = this.strict
+ this.strict = true
+
+ this.parseClassId(node, isStatement)
+ this.parseClassSuper(node)
+ let classBody = this.startNode()
+ let hadConstructor = false
+ classBody.body = []
+ this.expect(tt.braceL)
+ while (this.type !== tt.braceR) {
+ const element = this.parseClassElement(node.superClass !== null)
+ if (element) {
+ classBody.body.push(element)
+ if (element.type === "MethodDefinition" && element.kind === "constructor") {
+ if (hadConstructor) this.raise(element.start, "Duplicate constructor in the same class")
+ hadConstructor = true
+ }
+ }
+ }
+ this.strict = oldStrict
+ this.next()
+ node.body = this.finishNode(classBody, "ClassBody")
+ return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression")
+}
+
+pp.parseClassElement = function(constructorAllowsSuper) {
+ if (this.eat(tt.semi)) return null
+
+ let method = this.startNode()
+ const tryContextual = (k, noLineBreak = false) => {
+ const start = this.start, startLoc = this.startLoc
+ if (!this.eatContextual(k)) return false
+ if (this.type !== tt.parenL && (!noLineBreak || !this.canInsertSemicolon())) return true
+ if (method.key) this.unexpected()
+ method.computed = false
+ method.key = this.startNodeAt(start, startLoc)
+ method.key.name = k
+ this.finishNode(method.key, "Identifier")
+ return false
+ }
+
+ method.kind = "method"
+ method.static = tryContextual("static")
+ let isGenerator = this.eat(tt.star)
+ let isAsync = false
+ if (!isGenerator) {
+ if (this.options.ecmaVersion >= 8 && tryContextual("async", true)) {
+ isAsync = true
+ isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star)
+ } else if (tryContextual("get")) {
+ method.kind = "get"
+ } else if (tryContextual("set")) {
+ method.kind = "set"
+ }
+ }
+ if (!method.key) this.parsePropertyName(method)
+ let {key} = method
+ let allowsDirectSuper = false
+ if (!method.computed && !method.static && (key.type === "Identifier" && key.name === "constructor" ||
+ key.type === "Literal" && key.value === "constructor")) {
+ if (method.kind !== "method") this.raise(key.start, "Constructor can't have get/set modifier")
+ if (isGenerator) this.raise(key.start, "Constructor can't be a generator")
+ if (isAsync) this.raise(key.start, "Constructor can't be an async method")
+ method.kind = "constructor"
+ allowsDirectSuper = constructorAllowsSuper
+ } else if (method.static && key.type === "Identifier" && key.name === "prototype") {
+ this.raise(key.start, "Classes may not have a static property named prototype")
+ }
+ this.parseClassMethod(method, isGenerator, isAsync, allowsDirectSuper)
+ if (method.kind === "get" && method.value.params.length !== 0)
+ this.raiseRecoverable(method.value.start, "getter should have no params")
+ if (method.kind === "set" && method.value.params.length !== 1)
+ this.raiseRecoverable(method.value.start, "setter should have exactly one param")
+ if (method.kind === "set" && method.value.params[0].type === "RestElement")
+ this.raiseRecoverable(method.value.params[0].start, "Setter cannot use rest params")
+ return method
+}
+
+pp.parseClassMethod = function(method, isGenerator, isAsync, allowsDirectSuper) {
+ method.value = this.parseMethod(isGenerator, isAsync, allowsDirectSuper)
+ return this.finishNode(method, "MethodDefinition")
+}
+
+pp.parseClassId = function(node, isStatement) {
+ if (this.type === tt.name) {
+ node.id = this.parseIdent()
+ if (isStatement)
+ this.checkLVal(node.id, BIND_LEXICAL, false)
+ } else {
+ if (isStatement === true)
+ this.unexpected()
+ node.id = null
+ }
+}
+
+pp.parseClassSuper = function(node) {
+ node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null
+}
+
+// Parses module export declaration.
+
+pp.parseExport = function(node, exports) {
+ this.next()
+ // export * from '...'
+ if (this.eat(tt.star)) {
+ if (this.options.ecmaVersion >= 11) {
+ if (this.eatContextual("as")) {
+ node.exported = this.parseIdent(true)
+ this.checkExport(exports, node.exported.name, this.lastTokStart)
+ } else {
+ node.exported = null
+ }
+ }
+ this.expectContextual("from")
+ if (this.type !== tt.string) this.unexpected()
+ node.source = this.parseExprAtom()
+ this.semicolon()
+ return this.finishNode(node, "ExportAllDeclaration")
+ }
+ if (this.eat(tt._default)) { // export default ...
+ this.checkExport(exports, "default", this.lastTokStart)
+ let isAsync
+ if (this.type === tt._function || (isAsync = this.isAsyncFunction())) {
+ let fNode = this.startNode()
+ this.next()
+ if (isAsync) this.next()
+ node.declaration = this.parseFunction(fNode, FUNC_STATEMENT | FUNC_NULLABLE_ID, false, isAsync)
+ } else if (this.type === tt._class) {
+ let cNode = this.startNode()
+ node.declaration = this.parseClass(cNode, "nullableID")
+ } else {
+ node.declaration = this.parseMaybeAssign()
+ this.semicolon()
+ }
+ return this.finishNode(node, "ExportDefaultDeclaration")
+ }
+ // export var|const|let|function|class ...
+ if (this.shouldParseExportStatement()) {
+ node.declaration = this.parseStatement(null)
+ if (node.declaration.type === "VariableDeclaration")
+ this.checkVariableExport(exports, node.declaration.declarations)
+ else
+ this.checkExport(exports, node.declaration.id.name, node.declaration.id.start)
+ node.specifiers = []
+ node.source = null
+ } else { // export { x, y as z } [from '...']
+ node.declaration = null
+ node.specifiers = this.parseExportSpecifiers(exports)
+ if (this.eatContextual("from")) {
+ if (this.type !== tt.string) this.unexpected()
+ node.source = this.parseExprAtom()
+ } else {
+ for (let spec of node.specifiers) {
+ // check for keywords used as local names
+ this.checkUnreserved(spec.local)
+ // check if export is defined
+ this.checkLocalExport(spec.local)
+ }
+
+ node.source = null
+ }
+ this.semicolon()
+ }
+ return this.finishNode(node, "ExportNamedDeclaration")
+}
+
+pp.checkExport = function(exports, name, pos) {
+ if (!exports) return
+ if (has(exports, name))
+ this.raiseRecoverable(pos, "Duplicate export '" + name + "'")
+ exports[name] = true
+}
+
+pp.checkPatternExport = function(exports, pat) {
+ let type = pat.type
+ if (type === "Identifier")
+ this.checkExport(exports, pat.name, pat.start)
+ else if (type === "ObjectPattern")
+ for (let prop of pat.properties)
+ this.checkPatternExport(exports, prop)
+ else if (type === "ArrayPattern")
+ for (let elt of pat.elements) {
+ if (elt) this.checkPatternExport(exports, elt)
+ }
+ else if (type === "Property")
+ this.checkPatternExport(exports, pat.value)
+ else if (type === "AssignmentPattern")
+ this.checkPatternExport(exports, pat.left)
+ else if (type === "RestElement")
+ this.checkPatternExport(exports, pat.argument)
+ else if (type === "ParenthesizedExpression")
+ this.checkPatternExport(exports, pat.expression)
+}
+
+pp.checkVariableExport = function(exports, decls) {
+ if (!exports) return
+ for (let decl of decls)
+ this.checkPatternExport(exports, decl.id)
+}
+
+pp.shouldParseExportStatement = function() {
+ return this.type.keyword === "var" ||
+ this.type.keyword === "const" ||
+ this.type.keyword === "class" ||
+ this.type.keyword === "function" ||
+ this.isLet() ||
+ this.isAsyncFunction()
+}
+
+// Parses a comma-separated list of module exports.
+
+pp.parseExportSpecifiers = function(exports) {
+ let nodes = [], first = true
+ // export { x, y as z } [from '...']
+ this.expect(tt.braceL)
+ while (!this.eat(tt.braceR)) {
+ if (!first) {
+ this.expect(tt.comma)
+ if (this.afterTrailingComma(tt.braceR)) break
+ } else first = false
+
+ let node = this.startNode()
+ node.local = this.parseIdent(true)
+ node.exported = this.eatContextual("as") ? this.parseIdent(true) : node.local
+ this.checkExport(exports, node.exported.name, node.exported.start)
+ nodes.push(this.finishNode(node, "ExportSpecifier"))
+ }
+ return nodes
+}
+
+// Parses import declaration.
+
+pp.parseImport = function(node) {
+ this.next()
+ // import '...'
+ if (this.type === tt.string) {
+ node.specifiers = empty
+ node.source = this.parseExprAtom()
+ } else {
+ node.specifiers = this.parseImportSpecifiers()
+ this.expectContextual("from")
+ node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected()
+ }
+ this.semicolon()
+ return this.finishNode(node, "ImportDeclaration")
+}
+
+// Parses a comma-separated list of module imports.
+
+pp.parseImportSpecifiers = function() {
+ let nodes = [], first = true
+ if (this.type === tt.name) {
+ // import defaultObj, { x, y as z } from '...'
+ let node = this.startNode()
+ node.local = this.parseIdent()
+ this.checkLVal(node.local, BIND_LEXICAL)
+ nodes.push(this.finishNode(node, "ImportDefaultSpecifier"))
+ if (!this.eat(tt.comma)) return nodes
+ }
+ if (this.type === tt.star) {
+ let node = this.startNode()
+ this.next()
+ this.expectContextual("as")
+ node.local = this.parseIdent()
+ this.checkLVal(node.local, BIND_LEXICAL)
+ nodes.push(this.finishNode(node, "ImportNamespaceSpecifier"))
+ return nodes
+ }
+ this.expect(tt.braceL)
+ while (!this.eat(tt.braceR)) {
+ if (!first) {
+ this.expect(tt.comma)
+ if (this.afterTrailingComma(tt.braceR)) break
+ } else first = false
+
+ let node = this.startNode()
+ node.imported = this.parseIdent(true)
+ if (this.eatContextual("as")) {
+ node.local = this.parseIdent()
+ } else {
+ this.checkUnreserved(node.imported)
+ node.local = node.imported
+ }
+ this.checkLVal(node.local, BIND_LEXICAL)
+ nodes.push(this.finishNode(node, "ImportSpecifier"))
+ }
+ return nodes
+}
+
+// Set `ExpressionStatement#directive` property for directive prologues.
+pp.adaptDirectivePrologue = function(statements) {
+ for (let i = 0; i < statements.length && this.isDirectiveCandidate(statements[i]); ++i) {
+ statements[i].directive = statements[i].expression.raw.slice(1, -1)
+ }
+}
+pp.isDirectiveCandidate = function(statement) {
+ return (
+ statement.type === "ExpressionStatement" &&
+ statement.expression.type === "Literal" &&
+ typeof statement.expression.value === "string" &&
+ // Reject parenthesized strings.
+ (this.input[statement.start] === "\"" || this.input[statement.start] === "'")
+ )
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/tokencontext.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/tokencontext.js
new file mode 100644
index 0000000000..1ff5556879
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/tokencontext.js
@@ -0,0 +1,149 @@
+// The algorithm used to determine whether a regexp can appear at a
+// given point in the program is loosely based on sweet.js' approach.
+// See https://github.com/mozilla/sweet.js/wiki/design
+
+import {Parser} from "./state"
+import {types as tt} from "./tokentype"
+import {lineBreak} from "./whitespace"
+
+export class TokContext {
+ constructor(token, isExpr, preserveSpace, override, generator) {
+ this.token = token
+ this.isExpr = !!isExpr
+ this.preserveSpace = !!preserveSpace
+ this.override = override
+ this.generator = !!generator
+ }
+}
+
+export const types = {
+ b_stat: new TokContext("{", false),
+ b_expr: new TokContext("{", true),
+ b_tmpl: new TokContext("${", false),
+ p_stat: new TokContext("(", false),
+ p_expr: new TokContext("(", true),
+ q_tmpl: new TokContext("`", true, true, p => p.tryReadTemplateToken()),
+ f_stat: new TokContext("function", false),
+ f_expr: new TokContext("function", true),
+ f_expr_gen: new TokContext("function", true, false, null, true),
+ f_gen: new TokContext("function", false, false, null, true)
+}
+
+const pp = Parser.prototype
+
+pp.initialContext = function() {
+ return [types.b_stat]
+}
+
+pp.braceIsBlock = function(prevType) {
+ let parent = this.curContext()
+ if (parent === types.f_expr || parent === types.f_stat)
+ return true
+ if (prevType === tt.colon && (parent === types.b_stat || parent === types.b_expr))
+ return !parent.isExpr
+
+ // The check for `tt.name && exprAllowed` detects whether we are
+ // after a `yield` or `of` construct. See the `updateContext` for
+ // `tt.name`.
+ if (prevType === tt._return || prevType === tt.name && this.exprAllowed)
+ return lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
+ if (prevType === tt._else || prevType === tt.semi || prevType === tt.eof || prevType === tt.parenR || prevType === tt.arrow)
+ return true
+ if (prevType === tt.braceL)
+ return parent === types.b_stat
+ if (prevType === tt._var || prevType === tt._const || prevType === tt.name)
+ return false
+ return !this.exprAllowed
+}
+
+pp.inGeneratorContext = function() {
+ for (let i = this.context.length - 1; i >= 1; i--) {
+ let context = this.context[i]
+ if (context.token === "function")
+ return context.generator
+ }
+ return false
+}
+
+pp.updateContext = function(prevType) {
+ let update, type = this.type
+ if (type.keyword && prevType === tt.dot)
+ this.exprAllowed = false
+ else if (update = type.updateContext)
+ update.call(this, prevType)
+ else
+ this.exprAllowed = type.beforeExpr
+}
+
+// Token-specific context update code
+
+tt.parenR.updateContext = tt.braceR.updateContext = function() {
+ if (this.context.length === 1) {
+ this.exprAllowed = true
+ return
+ }
+ let out = this.context.pop()
+ if (out === types.b_stat && this.curContext().token === "function") {
+ out = this.context.pop()
+ }
+ this.exprAllowed = !out.isExpr
+}
+
+tt.braceL.updateContext = function(prevType) {
+ this.context.push(this.braceIsBlock(prevType) ? types.b_stat : types.b_expr)
+ this.exprAllowed = true
+}
+
+tt.dollarBraceL.updateContext = function() {
+ this.context.push(types.b_tmpl)
+ this.exprAllowed = true
+}
+
+tt.parenL.updateContext = function(prevType) {
+ let statementParens = prevType === tt._if || prevType === tt._for || prevType === tt._with || prevType === tt._while
+ this.context.push(statementParens ? types.p_stat : types.p_expr)
+ this.exprAllowed = true
+}
+
+tt.incDec.updateContext = function() {
+ // tokExprAllowed stays unchanged
+}
+
+tt._function.updateContext = tt._class.updateContext = function(prevType) {
+ if (prevType.beforeExpr && prevType !== tt.semi && prevType !== tt._else &&
+ !(prevType === tt._return && lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) &&
+ !((prevType === tt.colon || prevType === tt.braceL) && this.curContext() === types.b_stat))
+ this.context.push(types.f_expr)
+ else
+ this.context.push(types.f_stat)
+ this.exprAllowed = false
+}
+
+tt.backQuote.updateContext = function() {
+ if (this.curContext() === types.q_tmpl)
+ this.context.pop()
+ else
+ this.context.push(types.q_tmpl)
+ this.exprAllowed = false
+}
+
+tt.star.updateContext = function(prevType) {
+ if (prevType === tt._function) {
+ let index = this.context.length - 1
+ if (this.context[index] === types.f_expr)
+ this.context[index] = types.f_expr_gen
+ else
+ this.context[index] = types.f_gen
+ }
+ this.exprAllowed = true
+}
+
+tt.name.updateContext = function(prevType) {
+ let allowed = false
+ if (this.options.ecmaVersion >= 6 && prevType !== tt.dot) {
+ if (this.value === "of" && !this.exprAllowed ||
+ this.value === "yield" && this.inGeneratorContext())
+ allowed = true
+ }
+ this.exprAllowed = allowed
+}
diff --git a/tools/tests/apps/modules-modern/imports/links/acorn/src/tokenize.js b/tools/tests/apps/modules-modern/imports/links/acorn/src/tokenize.js
new file mode 100644
index 0000000000..835fdcd962
--- /dev/null
+++ b/tools/tests/apps/modules-modern/imports/links/acorn/src/tokenize.js
@@ -0,0 +1,727 @@
+import {isIdentifierStart, isIdentifierChar} from "./identifier"
+import {types as tt, keywords as keywordTypes} from "./tokentype"
+import {Parser} from "./state"
+import {SourceLocation} from "./locutil"
+import {RegExpValidationState} from "./regexp"
+import {lineBreak, lineBreakG, isNewLine, nonASCIIwhitespace} from "./whitespace"
+
+// Object type used to represent tokens. Note that normally, tokens
+// simply exist as properties on the parser object. This is only
+// used for the onToken callback and the external tokenizer.
+
+export class Token {
+ constructor(p) {
+ this.type = p.type
+ this.value = p.value
+ this.start = p.start
+ this.end = p.end
+ if (p.options.locations)
+ this.loc = new SourceLocation(p, p.startLoc, p.endLoc)
+ if (p.options.ranges)
+ this.range = [p.start, p.end]
+ }
+}
+
+// ## Tokenizer
+
+const pp = Parser.prototype
+
+// Move to the next token
+
+pp.next = function(ignoreEscapeSequenceInKeyword) {
+ if (!ignoreEscapeSequenceInKeyword && this.type.keyword && this.containsEsc)
+ this.raiseRecoverable(this.start, "Escape sequence in keyword " + this.type.keyword)
+ if (this.options.onToken)
+ this.options.onToken(new Token(this))
+
+ this.lastTokEnd = this.end
+ this.lastTokStart = this.start
+ this.lastTokEndLoc = this.endLoc
+ this.lastTokStartLoc = this.startLoc
+ this.nextToken()
+}
+
+pp.getToken = function() {
+ this.next()
+ return new Token(this)
+}
+
+// If we're in an ES6 environment, make parsers iterable
+if (typeof Symbol !== "undefined")
+ pp[Symbol.iterator] = function() {
+ return {
+ next: () => {
+ let token = this.getToken()
+ return {
+ done: token.type === tt.eof,
+ value: token
+ }
+ }
+ }
+ }
+
+// Toggle strict mode. Re-reads the next number or string to please
+// pedantic tests (`"use strict"; 010;` should fail).
+
+pp.curContext = function() {
+ return this.context[this.context.length - 1]
+}
+
+// Read a single token, updating the parser object's token-related
+// properties.
+
+pp.nextToken = function() {
+ let curContext = this.curContext()
+ if (!curContext || !curContext.preserveSpace) this.skipSpace()
+
+ this.start = this.pos
+ if (this.options.locations) this.startLoc = this.curPosition()
+ if (this.pos >= this.input.length) return this.finishToken(tt.eof)
+
+ if (curContext.override) return curContext.override(this)
+ else this.readToken(this.fullCharCodeAtPos())
+}
+
+pp.readToken = function(code) {
+ // Identifier or keyword. '\uXXXX' sequences are allowed in
+ // identifiers, so '\' also dispatches to that.
+ if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */)
+ return this.readWord()
+
+ return this.getTokenFromCode(code)
+}
+
+pp.fullCharCodeAtPos = function() {
+ let code = this.input.charCodeAt(this.pos)
+ if (code <= 0xd7ff || code >= 0xe000) return code
+ let next = this.input.charCodeAt(this.pos + 1)
+ return (code << 10) + next - 0x35fdc00
+}
+
+pp.skipBlockComment = function() {
+ let startLoc = this.options.onComment && this.curPosition()
+ let start = this.pos, end = this.input.indexOf("*/", this.pos += 2)
+ if (end === -1) this.raise(this.pos - 2, "Unterminated comment")
+ this.pos = end + 2
+ if (this.options.locations) {
+ lineBreakG.lastIndex = start
+ let match
+ while ((match = lineBreakG.exec(this.input)) && match.index < this.pos) {
+ ++this.curLine
+ this.lineStart = match.index + match[0].length
+ }
+ }
+ if (this.options.onComment)
+ this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos,
+ startLoc, this.curPosition())
+}
+
+pp.skipLineComment = function(startSkip) {
+ let start = this.pos
+ let startLoc = this.options.onComment && this.curPosition()
+ let ch = this.input.charCodeAt(this.pos += startSkip)
+ while (this.pos < this.input.length && !isNewLine(ch)) {
+ ch = this.input.charCodeAt(++this.pos)
+ }
+ if (this.options.onComment)
+ this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos,
+ startLoc, this.curPosition())
+}
+
+// Called at the start of the parse and after every token. Skips
+// whitespace and comments, and.
+
+pp.skipSpace = function() {
+ loop: while (this.pos < this.input.length) {
+ let ch = this.input.charCodeAt(this.pos)
+ switch (ch) {
+ case 32: case 160: // ' '
+ ++this.pos
+ break
+ case 13:
+ if (this.input.charCodeAt(this.pos + 1) === 10) {
+ ++this.pos
+ }
+ case 10: case 8232: case 8233:
+ ++this.pos
+ if (this.options.locations) {
+ ++this.curLine
+ this.lineStart = this.pos
+ }
+ break
+ case 47: // '/'
+ switch (this.input.charCodeAt(this.pos + 1)) {
+ case 42: // '*'
+ this.skipBlockComment()
+ break
+ case 47:
+ this.skipLineComment(2)
+ break
+ default:
+ break loop
+ }
+ break
+ default:
+ if (ch > 8 && ch < 14 || ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) {
+ ++this.pos
+ } else {
+ break loop
+ }
+ }
+ }
+}
+
+// Called at the end of every token. Sets `end`, `val`, and
+// maintains `context` and `exprAllowed`, and skips the space after
+// the token, so that the next one's `start` will point at the
+// right position.
+
+pp.finishToken = function(type, val) {
+ this.end = this.pos
+ if (this.options.locations) this.endLoc = this.curPosition()
+ let prevType = this.type
+ this.type = type
+ this.value = val
+
+ this.updateContext(prevType)
+}
+
+// ### Token reading
+
+// This is the function that is called to fetch the next token. It
+// is somewhat obscure, because it works in character codes rather
+// than characters, and because operator parsing has been inlined
+// into it.
+//
+// All in the name of speed.
+//
+pp.readToken_dot = function() {
+ let next = this.input.charCodeAt(this.pos + 1)
+ if (next >= 48 && next <= 57) return this.readNumber(true)
+ let next2 = this.input.charCodeAt(this.pos + 2)
+ if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.'
+ this.pos += 3
+ return this.finishToken(tt.ellipsis)
+ } else {
+ ++this.pos
+ return this.finishToken(tt.dot)
+ }
+}
+
+pp.readToken_slash = function() { // '/'
+ let next = this.input.charCodeAt(this.pos + 1)
+ if (this.exprAllowed) { ++this.pos; return this.readRegexp() }
+ if (next === 61) return this.finishOp(tt.assign, 2)
+ return this.finishOp(tt.slash, 1)
+}
+
+pp.readToken_mult_modulo_exp = function(code) { // '%*'
+ let next = this.input.charCodeAt(this.pos + 1)
+ let size = 1
+ let tokentype = code === 42 ? tt.star : tt.modulo
+
+ // exponentiation operator ** and **=
+ if (this.options.ecmaVersion >= 7 && code === 42 && next === 42) {
+ ++size
+ tokentype = tt.starstar
+ next = this.input.charCodeAt(this.pos + 2)
+ }
+
+ if (next === 61) return this.finishOp(tt.assign, size + 1)
+ return this.finishOp(tokentype, size)
+}
+
+pp.readToken_pipe_amp = function(code) { // '|&'
+ let next = this.input.charCodeAt(this.pos + 1)
+ if (next === code) return this.finishOp(code === 124 ? tt.logicalOR : tt.logicalAND, 2)
+ if (next === 61) return this.finishOp(tt.assign, 2)
+ return this.finishOp(code === 124 ? tt.bitwiseOR : tt.bitwiseAND, 1)
+}
+
+pp.readToken_caret = function() { // '^'
+ let next = this.input.charCodeAt(this.pos + 1)
+ if (next === 61) return this.finishOp(tt.assign, 2)
+ return this.finishOp(tt.bitwiseXOR, 1)
+}
+
+pp.readToken_plus_min = function(code) { // '+-'
+ let next = this.input.charCodeAt(this.pos + 1)
+ if (next === code) {
+ if (next === 45 && !this.inModule && this.input.charCodeAt(this.pos + 2) === 62 &&
+ (this.lastTokEnd === 0 || lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))) {
+ // A `-->` line comment
+ this.skipLineComment(3)
+ this.skipSpace()
+ return this.nextToken()
+ }
+ return this.finishOp(tt.incDec, 2)
+ }
+ if (next === 61) return this.finishOp(tt.assign, 2)
+ return this.finishOp(tt.plusMin, 1)
+}
+
+pp.readToken_lt_gt = function(code) { // '<>'
+ let next = this.input.charCodeAt(this.pos + 1)
+ let size = 1
+ if (next === code) {
+ size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2
+ if (this.input.charCodeAt(this.pos + size) === 61) return this.finishOp(tt.assign, size + 1)
+ return this.finishOp(tt.bitShift, size)
+ }
+ if (next === 33 && code === 60 && !this.inModule && this.input.charCodeAt(this.pos + 2) === 45 &&
+ this.input.charCodeAt(this.pos + 3) === 45) {
+ // `
+
+
diff --git a/v3-docs/docs/packages/logging.md b/v3-docs/docs/packages/logging.md
index dd52d78aee..9aee4da917 100644
--- a/v3-docs/docs/packages/logging.md
+++ b/v3-docs/docs/packages/logging.md
@@ -11,29 +11,47 @@ meteor add logging
```
You can then import the utility anywhere in you code like this:
+
```javascript
-import { Log } from 'meteor/logging'
+import { Log } from "meteor/logging";
```
You can then call the logging functions in one of the following ways:
+
```javascript
-Log('starting up') // or Log.info('starting up')
-Log.error('error message')
-Log.warn('warning')
-Log.debug('this will show only in development')
+Log("starting up"); // or Log.info('starting up')
+Log.error("error message");
+Log.warn("warning");
+Log.debug("this will show only in development");
```
Besides passing in strings, you can also pass in objects. This has few exceptions and special functions associated.
First in the root of the object the following keys are not allowed:
+
```javascript
-'time', 'timeInexact', 'level', 'file', 'line', 'program', 'originApp', 'satellite', 'stderr'
+ "time",
+ "timeInexact",
+ "level",
+ "file",
+ "line",
+ "program",
+ "originApp",
+ "satellite",
+ "stderr";
```
On the other hand there is `message` and `app`, which are also reserved, but they will display in more prominent manner:
+
```javascript
-Log.info({message: 'warning', app: 'DESKTOP', error: { property1: 'foo', property2: 'bar', property3: { foo: 'bar' }} })
+Log.info({
+ message: "warning",
+ app: "DESKTOP",
+ error: { property1: "foo", property2: "bar", property3: { foo: "bar" } },
+});
```
+
will turn into:
+
```shell
E20200519-17:57:41.655(9) [DESKTOP] (main.js:36) warning {"error":{"property1":"foo","property2":"bar","property3":{"foo":"bar"}}}
```
@@ -41,4 +59,24 @@ E20200519-17:57:41.655(9) [DESKTOP] (main.js:36) warning {"error":{"property1":"
The display of each log is color coded. Info is `blue`, warn is `magenta`, debug is `green` and error is in `red`.
### Log.debug
+
The `Log.debug()` logging is different from the other calls as these messages will not be displayed in production.
+
+### Options
+
+You can set the following options:
+
+```javascript
+// Either 'json' or 'colored-text'. Defaults to 'json'.
+//
+// When this is set to 'json', print JSON documents to the console.
+//
+// When this is set to 'colored-text', call 'Log.format' before printing.
+// This should be used for logging to the console in a human-readable format.
+Log.outputFormat = "json";
+
+// Boolean. Defaults to true.
+// for cloud environments is interesting to leave it false as most of them have the timestamp in the console.
+// Only works in server with colored-text output format.
+Log.showTime = true;
+```
diff --git a/v3-docs/docs/packages/modern-browsers.md b/v3-docs/docs/packages/modern-browsers.md
new file mode 100644
index 0000000000..9ef729c855
--- /dev/null
+++ b/v3-docs/docs/packages/modern-browsers.md
@@ -0,0 +1,32 @@
+# Modern-browsers
+
+API for defining the boundary between modern and legacy JavaScript clients.
+
+You can use this package to define the minimum browser versions for which
+a browser engine will be considered modern. All browsers that do not meet
+the threshold will receive the legacy bundle. This way you can easily keep
+on using modern features that you need.
+
+You can read more about this in [Meteor 1.7 announcement blog](https://blog.meteor.com/meteor-1-7-and-the-evergreen-dream-a8c1270b0901).
+
+
+
+
+
+
+
+
+
+## Configure Unknown Browsers to default to Modern
+
+Browsers not explicitly listed in `setMinimumBrowserVersions` are considered "legacy" by default.
+
+To change this and treat unknown browsers as "modern," update the relevant option in your settings file:
+
+```js
+Meteor.settings.packages = {
+ "modern-browsers": {
+ unknownBrowsersAssumedModern: true,
+ },
+};
+```
diff --git a/v3-docs/docs/packages/react-meteor-data.md b/v3-docs/docs/packages/react-meteor-data.md
new file mode 100644
index 0000000000..50717c82b1
--- /dev/null
+++ b/v3-docs/docs/packages/react-meteor-data.md
@@ -0,0 +1,491 @@
+# react-meteor-data
+
+This package provides an integration between React and [`Tracker`](https://atmospherejs.com/meteor/tracker), Meteor's reactive data system.
+
+## Table of Contents
+
+[[toc]]
+
+## Install
+
+::: tip
+
+This package is included with `meteor create` on react options. No need to install it manually.
+
+:::
+
+To install the package, use `meteor add`:
+
+```bash
+meteor add react-meteor-data
+```
+
+You'll also need to install `react` if you have not already:
+
+```bash
+meteor npm install react
+```
+
+### Changelog
+
+[check recent changes here](https://github.com/meteor/react-packages/blob/master/packages/react-meteor-data/CHANGELOG.md)
+
+## Usage
+
+This package provides two ways to use Tracker reactive data in your React components:
+
+- a hook: `useTracker` (v2 only, requires React `^16.8`)
+- a higher-order component (HOC): `withTracker` (v1 and v2).
+
+The `useTracker` hook, introduced in version 2.0.0, embraces the [benefits of hooks](https://reactjs.org/docs/hooks-faq.html). Like all React hooks, it can only be used in function components, not in class components.
+
+The `withTracker` HOC can be used with all components, function or class based.
+
+It is not necessary to rewrite existing applications to use the `useTracker` hook instead of the existing `withTracker` HOC.
+
+### `useTracker(reactiveFn)`
+
+You can use the `useTracker` hook to get the value of a Tracker reactive function in your React "function components." The reactive function will get re-run whenever its reactive inputs change, and the component will re-render with the new value.
+
+`useTracker` manages its own state, and causes re-renders when necessary. There is no need to call React state setters from inside your `reactiveFn`. Instead, return the values from your `reactiveFn` and assign those to variables directly. When the `reactiveFn` updates, the variables will be updated, and the React component will re-render.
+
+Arguments:
+
+- `reactiveFn`: A Tracker reactive function (receives the current computation).
+
+The basic way to use `useTracker` is to simply pass it a reactive function, with no further fuss. This is the preferred configuration in many cases.
+
+#### `useTracker(reactiveFn, deps)`
+
+You can pass an optional deps array as a second value. When provided, the computation will be retained, and reactive updates after the first run will run asynchronously from the react render execution frame. This array typically includes all variables from the outer scope "captured" in the closure passed as the 1st argument. For example, the value of a prop used in a subscription or a minimongo query; see example below.
+
+This should be considered a low level optimization step for cases where your computations are somewhat long running - like a complex minimongo query. In many cases it's safe and even preferred to omit deps and allow the computation to run synchronously with render.
+
+Arguments:
+
+- `reactiveFn`
+- `deps`: An optional array of "dependencies" of the reactive function. This is very similar to how the `deps` argument for [React's built-in `useEffect`, `useCallback` or `useMemo` hooks](https://reactjs.org/docs/hooks-reference.html) work.
+
+```jsx
+import { useTracker } from 'meteor/react-meteor-data';
+
+// React function component.
+function Foo({ listId }) {
+ // This computation uses no value from the outer scope,
+ // and thus does not needs to pass a 'deps' argument.
+ // However, we can optimize the use of the computation
+ // by providing an empty deps array. With it, the
+ // computation will be retained instead of torn down and
+ // rebuilt on every render. useTracker will produce the
+ // same results either way.
+ const currentUser = useTracker(() => Meteor.user(), []);
+
+ // The following two computations both depend on the
+ // listId prop. When deps are specified, the computation
+ // will be retained.
+ const listLoading = useTracker(() => {
+ // Note that this subscription will get cleaned up
+ // when your component is unmounted or deps change.
+ const handle = Meteor.subscribe('todoList', listId);
+ return !handle.ready();
+ }, [listId]);
+ const tasks = useTracker(() => Tasks.find({ listId }).fetch(), [listId]);
+
+ return (
+ Hello {currentUser.username}
+ {listLoading ? (
+ Loading
+ ) : (
+
+ Here is the Todo list {listId}:
+
+ {tasks.map(task => (
+ {task.label}
+ ))}
+
+
+ )}
+ );
+}
+```
+
+**Note:** the [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) package provides ESLint hints to help detect missing values in the `deps` argument of React built-in hooks. It can be configured to also validate the `deps` argument of the `useTracker` hook or some other hooks, with the following `eslintrc` config:
+
+```json
+"react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useTracker|useSomeOtherHook|..." }]
+```
+
+#### `useTracker(reactiveFn, deps, skipUpdate)` or `useTracker(reactiveFn, skipUpdate)`
+
+You may optionally pass a function as a second or third argument. The `skipUpdate` function can evaluate the return value of `reactiveFn` for changes, and control re-renders in sensitive cases. _Note:_ This is not meant to be used with a deep compare (even fast-deep-equals), as in many cases that may actually lead to worse performance than allowing React to do it's thing. But as an example, you could use this to compare an `updatedAt` field between updates, or a subset of specific fields, if you aren't using the entire document in a subscription. As always with any optimization, measure first, then optimize second. Make sure you really need this before implementing it.
+
+Arguments:
+
+- `reactiveFn`
+- `deps?` - optional - you may omit this, or pass a "falsy" value.
+- `skipUpdate` - A function which receives two arguments: `(prev, next) => (prev === next)`. `prev` and `next` will match the type or data shape as that returned by `reactiveFn`. Note: A return value of `true` means the update will be "skipped". `false` means re-render will occur as normal. So the function should be looking for equivalence.
+
+```jsx
+import { useTracker } from 'meteor/react-meteor-data';
+
+// React function component.
+function Foo({ listId }) {
+ const tasks = useTracker(
+ () => Tasks.find({ listId }).fetch(), [listId],
+ (prev, next) => {
+ // prev and next will match the type returned by the reactiveFn
+ return prev.every((doc, i) => (
+ doc._id === next[i] && doc.updatedAt === next[i]
+ )) && prev.length === next.length;
+ }
+ );
+
+ return (
+ Hello {currentUser.username}
+
+ Here is the Todo list {listId}:
+
+ {tasks.map(task => (
+ {task.label}
+ ))}
+
+
+ );
+}
+```
+
+### `withTracker(reactiveFn)`
+
+You can use the `withTracker` HOC to wrap your components and pass them additional props values from a Tracker reactive function. The reactive function will get re-run whenever its reactive inputs change, and the wrapped component will re-render with the new values for the additional props.
+
+Arguments:
+
+- `reactiveFn`: a Tracker reactive function, getting the props as a parameter, and returning an object of additional props to pass to the wrapped component.
+
+```jsx
+import { withTracker } from 'meteor/react-meteor-data';
+
+// React component (function or class).
+function Foo({ listId, currentUser, listLoading, tasks }) {
+ return (
+ Hello {currentUser.username}
+ {listLoading ?
+ Loading
:
+
+ Here is the Todo list {listId}:
+
{tasks.map(task => {task.label} )}
+
{
+ // Do all your reactive data access in this function.
+ // Note that this subscription will get cleaned up when your component is unmounted
+ const handle = Meteor.subscribe('todoList', listId);
+
+ return {
+ currentUser: Meteor.user(),
+ listLoading: !handle.ready(),
+ tasks: Tasks.find({ listId }).fetch(),
+ };
+})(Foo);
+```
+
+The returned component will, when rendered, render `Foo` (the "lower-order" component) with its provided props in addition to the result of the reactive function. So `Foo` will receive `{ listId }` (provided by its parent) as well as `{ currentUser, listLoading, tasks }` (added by the `withTracker` HOC).
+
+For more information, see the [React article](http://guide.meteor.com/react.html) in the Meteor Guide.
+
+### `withTracker({ reactiveFn, pure, skipUpdate })`
+
+The `withTracker` HOC can receive a config object instead of a simple reactive function.
+
+- `getMeteorData` - The `reactiveFn`.
+- `pure` - `true` by default. Causes the resulting Container to be wrapped with React's `memo()`.
+- `skipUpdate` - A function which receives two arguments: `(prev, next) => (prev === next)`. `prev` and `next` will match the type or data shape as that returned by `reactiveFn`. Note: A return value of `true` means the update will be "skipped". `false` means re-render will occur as normal. So the function should be looking for equivalence.
+
+```jsx
+import { withTracker } from 'meteor/react-meteor-data';
+
+// React component (function or class).
+function Foo({ listId, currentUser, listLoading, tasks }) {
+ return (
+ Hello {currentUser.username}
+ {listLoading ?
+ Loading
:
+
+ Here is the Todo list {listId}:
+
{tasks.map(task => {task.label} )}
+
(
+ doc._id === next[i] && doc.updatedAt === next[i]
+ ))
+ && prev.tasks.length === next.tasks.length
+ );
+ }
+})(Foo);
+```
+
+### `useSubscribe(subName, ...args)`
+
+`useSubscribe` is a convenient short hand for setting up a subscription. It is particularly useful when working with `useFind`, which should NOT be used for setting up subscriptions. At its core, it is a very simple wrapper around `useTracker` (with no deps) to create the subscription in a safe way, and allows you to avoid some of the ceremony around defining a factory and defining deps. Just pass the name of your subscription, and your arguments.
+
+`useSubscribe` returns an `isLoading` function. You can call `isLoading()` to react to changes in the subscription's loading state. The `isLoading` function will both return the loading state of the subscription, and set up a reactivity for the loading state change. If you don't call this function, no re-render will occur when the loading state changes.
+
+```jsx
+// Note: isLoading is a function!
+const isLoading = useSubscribe("posts", groupId);
+const posts = useFind(() => Posts.find({ groupId }), [groupId]);
+
+if (isLoading()) {
+ return ;
+} else {
+ return (
+
+ {posts.map((post) => (
+ {post.title}
+ ))}
+
+ );
+}
+```
+
+If you want to conditionally subscribe, you can set the `name` field (the first argument) to a falsy value to bypass the subscription.
+
+```jsx
+const needsData = false;
+const isLoading = useSubscribe(needsData ? "my-pub" : null);
+
+// When a subscription is not used, isLoading() will always return false
+```
+
+### `useFind(cursorFactory, deps)`
+
+The `useFind` hook can substantially speed up the rendering (and rerendering) of lists coming from mongo queries (subscriptions). It does this by controlling document object references. By providing a highly tailored cursor management within the hook, using the `Cursor.observe` API, `useFind` carefully updates only the object references changed during a DDP update. This approach allows a tighter use of core React tools and philosophies to turbo charge your list renders. It is a very different approach from the more general purpose `useTracker`, and it requires a bit more set up. A notable difference is that you should NOT call `.fetch()`. `useFind` requires its factory to return a `Mongo.Cursor` object. You may also return `null`, if you want to conditionally set up the Cursor.
+
+Here is an example in code:
+
+```jsx
+import React, { memo } from "react";
+import { useFind } from "meteor/react-meteor-data";
+import TestDocs from "/imports/api/collections/TestDocs";
+
+// Memoize the list item
+const ListItem = memo(({ doc }) => {
+ return (
+
+ {doc.id},{doc.updated}
+
+ );
+});
+
+const Test = () => {
+ const docs = useFind(() => TestDocs.find(), []);
+ return (
+
+ {docs.map((doc) => (
+
+ ))}
+
+ );
+};
+
+// Later on, update a single document - notice only that single component is updated in the DOM
+TestDocs.update({ id: 2 }, { $inc: { someProp: 1 } });
+```
+
+If you want to conditionally call the find method based on some props configuration or anything else, return `null` from the factory.
+
+```jsx
+const docs = useFind(() => {
+ if (props.skip) {
+ return null;
+ }
+ return TestDocs.find();
+}, []);
+```
+
+### Concurrent Mode, Suspense and Error Boundaries
+
+There are some additional considerations to keep in mind when using Concurrent Mode, Suspense and Error Boundaries, as
+each of these can cause React to cancel and discard (toss) a render, including the result of the first run of your
+reactive function. One of the things React developers often stress is that we should not create "side-effects" directly
+in the render method or in functional components. There are a number of good reasons for this, including allowing the
+React runtime to cancel renders. Limiting the use of side-effects allows features such as concurrent mode, suspense and
+error boundaries to work deterministically, without leaking memory or creating rogue processes. Care should be taken to
+avoid side effects in your reactive function for these reasons. (Note: this caution does not apply to Meteor specific
+side-effects like subscriptions, since those will be automatically cleaned up when `useTracker`'s computation is
+disposed.)
+
+Ideally, side-effects such as creating a Meteor computation would be done in `useEffect`. However, this is problematic
+for Meteor, which mixes an initial data query with setting up the computation to watch those data sources all in one
+initial run. If we wait to do that in `useEffect`, we'll end up rendering a minimum of 2 times (and using hacks for the
+first one) for every component which uses `useTracker` or `withTracker`, or not running at all in the initial render and
+still requiring a minimum of 2 renders, and complicating the API.
+
+To work around this and keep things running fast, we are creating the computation in the render method directly, and
+doing a number of checks later in `useEffect` to make sure we keep that computation fresh and everything up to date,
+while also making sure to clean things up if we detect the render has been tossed. For the most part, this should all be
+transparent.
+
+The important thing to understand is that your reactive function can be initially called more than once for a single
+render, because sometimes the work will be tossed. Additionally, `useTracker` will not call your reactive function
+reactively until the render is committed (until `useEffect` runs). If you have a particularly fast changing data source,
+this is worth understanding. With this very short possible suspension, there are checks in place to make sure the
+eventual result is always up to date with the current state of the reactive function. Once the render is "committed",
+and the component mounted, the computation is kept running, and everything will run as expected.
+
+## Suspendable version of hooks
+
+### `useTracker`
+
+This is a version of `useTracker` that can be used with React Suspense.
+
+For its first argument, a key is necessary, witch is used to identify the computation and to avoid recreating it when the
+component is re-rendered.
+
+Its second argument is a function that can be async and reactive,
+this argument works similar to the original `useTracker` that does not suspend.
+
+For its _optional_ third argument, the dependency array, works similar to the `useTracker` that does not suspend,
+you pass in an array of variables that this tracking function depends upon.
+
+For its _optional_ fourth argument, the options object, works similar to the `useTracker` that does not suspend,
+you pass in a function for when should skip the update.
+
+```jsx
+import { useTracker } from "meteor/react-meteor-data/suspense";
+import { useSubscribe } from "meteor/react-meteor-data/suspense";
+
+function Tasks() {
+ // this component will suspend
+ useSubscribe("tasks");
+ const { username } = useTracker("user", () => Meteor.user()); // Meteor.user() is async meteor 3.0
+ const tasksByUser = useTracker(
+ "tasksByUser",
+ () =>
+ TasksCollection.find(
+ { username },
+ { sort: { createdAt: -1 } }
+ ).fetchAsync() // async call
+ );
+
+ // render the tasks
+}
+```
+
+### Maintaining the reactive context
+
+To maintain a reactive context using the new Meteor Async methods, we are using the new `Tracker.withComputation` API to maintain the reactive context of an
+async call, this is needed because otherwise it would be only called once, and the computation would never run again,
+this way, every time we have a new Link being added, this useTracker is ran.
+
+```jsx
+// needs Tracker.withComputation because otherwise it would be only called once, and the computation would never run again
+const docs = useTracker("name", async (c) => {
+ const placeholders = await fetch(
+ "https://jsonplaceholder.typicode.com/todos"
+ ).then((x) => x.json());
+ console.log(placeholders);
+ return await Tracker.withComputation(c, () =>
+ LinksCollection.find().fetchAsync()
+ );
+});
+```
+
+A rule of thumb is that if you are using a reactive function for example `find` + `fetchAsync`, it is nice to wrap it
+inside `Tracker.withComputation` to make sure that the computation is kept alive, if you are just calling that function
+that is not necessary, like the one bellow, will be always reactive.
+
+```jsx
+const docs = useTracker("name", () => LinksCollection.find().fetchAsync());
+```
+
+### `useSubscribe`
+
+This is a version of `useSubscribe` that can be used with React Suspense.
+It is similar to `useSubscribe`, it throws a promise and suspends the rendering until the promise is resolved.
+It does not return a Meteor Handle to control the subscription
+
+```jsx
+import { useTracker } from "meteor/react-meteor-data/suspense";
+import { useSubscribe } from "meteor/react-meteor-data/suspense";
+
+function Tasks() {
+ // this component will suspend
+ useSubscribe("tasks");
+ const { username } = useTracker("user", () => Meteor.user()); // Meteor.user() is async meteor 3.0
+ const tasksByUser = useTracker(
+ "tasksByUser",
+ () =>
+ TasksCollection.find(
+ { username },
+ { sort: { createdAt: -1 } }
+ ).fetchAsync() // async call
+ );
+
+ // render the tasks
+}
+```
+
+### `useFind`
+
+This is a version of `useFind` that can be used with React Suspense.
+It has a few differences from the `useFind` without suspense, it throws a promise and suspends the rendering until the promise is resolved.
+It returns the result and it is reactive.
+You should pass as the first parameter the collection where is being searched upon and as the second parameter an array with the arguments,
+the same arguments that you would pass to the `find` method of the collection, third parameter is optional, and it is dependency array object.
+It's meant for the SSR, you don't have to use it if you're not interested in SSR.
+
+```jsx
+import { useFind } from "meteor/react-meteor-data/suspense";
+import { useSubscribe } from "meteor/react-meteor-data/suspense";
+
+function Tasks() {
+ // this component will suspend
+ useSubscribe("tasks");
+ const tasksByUser = useFind(TasksCollection, [
+ {},
+ { sort: { createdAt: -1 } },
+ ]);
+
+ // render the tasks
+}
+```
+
+## Version compatibility notes
+
+- `react-meteor-data` v2.x :
+
+ - `useTracker` hook + `withTracker` HOC
+ - Requires React `^16.8`.
+ - Implementation is compatible with "React Suspense", concurrent mode and error boundaries.
+ - The `withTracker` HOC is strictly backwards-compatible with the one provided in v1.x, the major version number is only motivated by the bump of React version requirement. Provided a compatible React version, existing Meteor apps leveraging the `withTracker` HOC can freely upgrade from v1.x to v2.x, and gain compatibility with future React versions.
+ - The previously deprecated `createContainer` has been removed.
+
+- `react-meteor-data` v0.x :
+ - `withTracker` HOC (+ `createContainer`, kept for backwards compatibility with early v0.x releases)
+ - Requires React `^15.3` or `^16.0`.
+ - Implementation relies on React lifecycle methods (`componentWillMount` / `componentWillUpdate`) that are [marked for deprecation in future React versions](https://reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes) ("React Suspense").
diff --git a/v3-docs/docs/packages/roles.md b/v3-docs/docs/packages/roles.md
new file mode 100644
index 0000000000..bf4063400e
--- /dev/null
+++ b/v3-docs/docs/packages/roles.md
@@ -0,0 +1,483 @@
+# Roles
+
+Authorization package for Meteor - compatible with built-in accounts package.
+
+> Available since Meteor 3.1.0 (previously alanning:roles)
+
+## Installation
+
+To add roles to your application, run this command in your terminal:
+
+```bash
+meteor add roles
+```
+
+## Overview
+
+The roles package lets you attach roles to users and then check against those roles when deciding whether to grant access to Meteor methods or publish data. The core concept is simple - you create role assignments for users and then verify those roles later. This package provides helper methods to make the process of adding, removing, and verifying roles easier.
+
+## Concepts
+
+### Roles vs Permissions
+
+Although named "roles", you can define your **roles**, **scopes** or **permissions** however you like. They are essentially tags assigned to users that you can check later.
+
+You can have traditional roles like `admin` or `webmaster`, or more granular permissions like `view-secrets`, `users.view`, or `users.manage`. Often, more granular permissions are better as they handle edge cases without creating many higher-level roles.
+
+### Role Hierarchy
+
+Roles can be organized in a hierarchy:
+
+- Roles can have multiple parents and children (subroles)
+- If a parent role is assigned to a user, all its descendant roles also apply
+- This allows creating "super roles" that aggregate permissions
+
+Example hierarchy setup:
+
+```js
+import { Roles } from "meteor/roles";
+
+// Create base roles
+await Roles.createRoleAsync("user");
+await Roles.createRoleAsync("admin");
+
+// Create permission roles
+await Roles.createRoleAsync("USERS_VIEW");
+await Roles.createRoleAsync("POST_EDIT");
+
+// Set up hierarchy
+await Roles.addRolesToParentAsync("USERS_VIEW", "admin");
+await Roles.addRolesToParentAsync("POST_EDIT", "admin");
+await Roles.addRolesToParentAsync("POST_EDIT", "user");
+```
+
+### Scopes
+
+Scopes allow users to have independent sets of roles. Use cases include:
+
+- Different communities within your app
+- Multiple tenants in a multi-tenant application
+- Different resource groups
+
+Users can have both scoped roles and global roles:
+
+- Global roles apply across all scopes
+- Scoped roles only apply within their specific scope
+- Scopes are independent of each other
+
+Example using scopes:
+
+```js
+// Assign scoped roles
+await Roles.addUsersToRolesAsync(userId, ["manage-team"], "team-a");
+await Roles.addUsersToRolesAsync(userId, ["player"], "team-b");
+
+// Check scoped roles
+await Roles.userIsInRoleAsync(userId, "manage-team", "team-a"); // true
+await Roles.userIsInRoleAsync(userId, "manage-team", "team-b"); // false
+
+// Assign global role
+await Roles.addUsersToRolesAsync(userId, "super-admin", null);
+
+// Global roles work in all scopes
+await Roles.userIsInRoleAsync(userId, ["manage-team", "super-admin"], "team-b"); // true
+```
+
+## Role Management
+
+
+
+Example:
+
+```js
+import { Roles } from "meteor/roles";
+
+// Create a new role
+await Roles.createRoleAsync("admin");
+
+// Create if doesn't exist
+await Roles.createRoleAsync("editor", { unlessExists: true });
+```
+
+### Modifying Roles
+
+
+
+Example:
+
+```js
+// Make 'editor' a child role of 'admin'
+await Roles.addRolesToParentAsync("editor", "admin");
+
+// Add multiple child roles
+await Roles.addRolesToParentAsync(["editor", "moderator"], "admin");
+```
+
+
+
+Example:
+
+```js
+// Remove 'editor' as child role of 'admin'
+await Roles.removeRolesFromParentAsync("editor", "admin");
+```
+
+
+
+Example:
+
+```js
+// Delete role and all its assignments
+await Roles.deleteRoleAsync("temp-role");
+```
+
+
+
+Example:
+
+```js
+// Rename an existing role
+await Roles.renameRoleAsync("editor", "content-editor");
+```
+
+### Assigning Roles
+
+
+
+Example:
+
+```js
+// Add global roles
+await Roles.addUsersToRolesAsync(userId, ["admin", "editor"]);
+
+// Add scoped roles
+await Roles.addUsersToRolesAsync(userId, ["manager"], "department-a");
+
+// Add roles to multiple users
+await Roles.addUsersToRolesAsync([user1Id, user2Id], ["user"]);
+```
+
+
+
+Example:
+
+```js
+// Replace user's global roles
+await Roles.setUserRolesAsync(userId, ["editor"]);
+
+// Replace scoped roles
+await Roles.setUserRolesAsync(userId, ["viewer"], "project-x");
+
+// Clear all roles in scope
+await Roles.setUserRolesAsync(userId, [], "project-x");
+```
+
+
+
+Example:
+
+```js
+// Remove global roles
+await Roles.removeUsersFromRolesAsync(userId, ["admin"]);
+
+// Remove scoped roles
+await Roles.removeUsersFromRolesAsync(userId, ["manager"], "department-a");
+
+// Remove roles from multiple users
+await Roles.removeUsersFromRolesAsync([user1Id, user2Id], ["temp-role"]);
+```
+
+
+
+Example:
+
+```js
+// Rename a scope
+await Roles.renameScopeAsync("department-1", "marketing");
+```
+
+
+
+Example:
+
+```js
+// Remove a scope and all its role assignments
+await Roles.removeScopeAsync("old-department");
+```
+
+
+
+Example:
+
+```js
+// Get all roles sorted by name
+const roles = Roles.getAllRoles({ sort: { _id: 1 } });
+
+// Get roles with custom query
+const customRoles = Roles.getAllRoles({
+ fields: { _id: 1, children: 1 },
+ sort: { _id: -1 },
+});
+```
+
+
+
+Example:
+
+```js
+// returns a cursor of all admin users
+const adminUsersCursor = await Roles.getUsersInRoleAsync("admin");
+
+// Find all admin users
+const adminUsers = await (await Roles.getUsersInRoleAsync("admin")).fetchAsync();
+
+// Find users with specific roles in a scope
+const scopedUsers = await (await Roles.getUsersInRoleAsync(
+ ["editor", "writer"],
+ "blog"
+)).fetchAsync();
+
+// Find users with custom options
+const users = await (await Roles.getUsersInRoleAsync("manager", {
+ scope: "department-a",
+ queryOptions: {
+ sort: { createdAt: -1 },
+ limit: 10,
+ },
+})).fetchAsync();
+```
+
+## Checking Roles
+
+
+
+Example:
+
+```js
+// Check global role
+const isAdmin = await Roles.userIsInRoleAsync(userId, "admin");
+
+// Check any of multiple roles
+const canEdit = await Roles.userIsInRoleAsync(userId, ["editor", "admin"]);
+
+// Check scoped role
+const isManager = await Roles.userIsInRoleAsync(
+ userId,
+ "manager",
+ "department-a"
+);
+
+// Check role in any scope
+const hasRole = await Roles.userIsInRoleAsync(userId, "viewer", {
+ anyScope: true,
+});
+```
+
+
+
+Example:
+
+```js
+// Get user's global roles
+const globalRoles = await Roles.getRolesForUserAsync(userId);
+
+// Get scoped roles
+const deptRoles = await Roles.getRolesForUserAsync(userId, "department-a");
+
+// Get all roles including inherited
+const allRoles = await Roles.getRolesForUserAsync(userId, {
+ anyScope: true,
+ fullObjects: true,
+});
+```
+
+
+
+Example:
+
+```js
+// Check if admin is a parent of editor
+const isParent = await Roles.isParentOfAsync("admin", "editor");
+
+// Can be used to check inheritance chains
+const hasPermission = await Roles.isParentOfAsync("super-admin", "post-edit");
+```
+
+
+
+Example:
+
+```js
+// Get all scopes for user
+const allScopes = await Roles.getScopesForUserAsync(userId);
+
+// Get scopes where user has specific roles
+const editorScopes = await Roles.getScopesForUserAsync(userId, ["editor"]);
+```
+
+## Publishing Roles
+
+Role assignments need to be published to be available on the client. Example publication:
+
+```js
+// Publish user's own roles
+Meteor.publish(null, function () {
+ if (this.userId) {
+ return Meteor.roleAssignment.find({ "user._id": this.userId });
+ }
+ this.ready();
+});
+
+// Publish roles for specific scope
+Meteor.publish("scopeRoles", function (scope) {
+ if (this.userId) {
+ return Meteor.roleAssignment.find({ scope: scope });
+ }
+ this.ready();
+});
+```
+
+## Client only APIs
+
+On the client alongside the async methods, you can use the `sync` versions of the functions:
+
+- `Roles.userIsInRole(userId, roles, scope)`
+- `Roles.getRolesForUser(userId, scope)`
+- `Roles.getScopesForUser(userId)`
+- `Roles.isParentOf(parent, child)`
+- `Roles.getUsersInRole(role, scope)`
+- `Roles.getAllRoles(options)`
+- `Roles.createRole(role, options)`
+- `Roles.addUsersToRoles(userId, roles, scope)`
+- `Roles.setUserRoles(userId, roles, scope)`
+- `Roles.removeUsersFromRoles(userId, roles, scope)`
+- `Roles.addRolesToParent(child, parent)`
+- `Roles.removeRolesFromParent(child, parent)`
+- `Roles.deleteRole(role)`
+- `Roles.renameRole(oldRole, newRole)`
+- `Roles.renameScope(oldScope, newScope)`
+- `Roles.removeScope(scope)`
+
+## Using with Templates
+
+The roles package automatically provides an `isInRole` helper for templates:
+
+```handlebars
+{{#if isInRole "admin"}}
+
+
+
+{{/if}}
+
+{{#if isInRole "editor,writer" "blog"}}
+
+
+
+{{/if}}
+```
+
+## Migration to Core Version
+
+If you are currently using the `alanning:roles` package, follow these steps to migrate to the core version:
+
+1. Make sure you are on version 3.6 of `alanning:roles` first
+2. Run any pending migrations from previous versions
+3. Switch all server-side role operations to use the async versions of the functions:
+ - createRoleAsync
+ - deleteRoleAsync
+ - addUsersToRolesAsync
+ - setUserRolesAsync
+ - removeUsersFromRolesAsync
+ - etc.
+4. Remove `alanning:roles` package:
+ ```bash
+ meteor remove alanning:roles
+ ```
+5. Add the core roles package:
+ ```bash
+ meteor add roles
+ ```
+6. Update imports to use the new package:
+ ```js
+ import { Roles } from "meteor/roles";
+ ```
+
+The sync versions of these functions are still available on the client.
+
+## Security Considerations
+
+1. Client-side role checks are for convenience only - always verify permissions on the server
+2. Publish only the role data that users need
+3. Use scopes to properly isolate role assignments
+4. Validate role names and scopes to prevent injection attacks
+5. Consider using more granular permissions over broad role assignments
+
+## Example Usage
+
+### Method Security
+
+```js
+// server/methods.js
+Meteor.methods({
+ deletePost: async function (postId) {
+ check(postId, String);
+
+ const canDelete = await Roles.userIsInRoleAsync(
+ this.userId,
+ ["admin", "moderator"],
+ "posts"
+ );
+
+ if (!canDelete) {
+ throw new Meteor.Error("unauthorized", "Not authorized to delete posts");
+ }
+
+ Posts.remove(postId);
+ },
+});
+```
+
+### Publication Security
+
+```js
+// server/publications.js
+Meteor.publish("secretDocuments", async function (scope) {
+ check(scope, String);
+
+ const canView = await Roles.userIsInRoleAsync(
+ this.userId,
+ ["view-secrets", "admin"],
+ scope
+ );
+
+ if (canView) {
+ return SecretDocs.find({ scope: scope });
+ }
+
+ this.ready();
+});
+```
+
+### User Management
+
+```js
+// server/users.js
+Meteor.methods({
+ promoteToEditor: async function (userId, scope) {
+ check(userId, String);
+ check(scope, String);
+
+ const canPromote = await Roles.userIsInRoleAsync(
+ this.userId,
+ "admin",
+ scope
+ );
+
+ if (!canPromote) {
+ throw new Meteor.Error("unauthorized");
+ }
+
+ await Roles.addUsersToRolesAsync(userId, ["editor"], scope);
+ },
+});
+```
diff --git a/v3-docs/docs/packages/service-configuration.md b/v3-docs/docs/packages/service-configuration.md
new file mode 100644
index 0000000000..4cf00cd36f
--- /dev/null
+++ b/v3-docs/docs/packages/service-configuration.md
@@ -0,0 +1,456 @@
+# OAuth Services Configuration
+
+Meteor provides built-in support for OAuth authentication with several popular services. This guide will help you set up OAuth providers for your Meteor application.
+
+## Configuration
+
+After registering your application with an OAuth provider, you need to configure the credentials in your Meteor app. There are several ways to do this:
+
+### Using settings.json (Recommended)
+
+In your `settings.json` add:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "facebook": {
+ "appId": "YOUR_APP_ID",
+ "secret": "YOUR_APP_SECRET"
+ },
+ "google": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ },
+ "github": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ },
+ "twitter": {
+ "consumerKey": "YOUR_CONSUMER_KEY",
+ "secret": "YOUR_CONSUMER_SECRET"
+ },
+ "meetup": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ },
+ "weibo": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ },
+ "meteor-developer": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ }
+ }
+ }
+}
+```
+
+> Another optional setting by each service is `loginStyle` which can be set to `redirect` or `popup`.
+
+Then start your app with:
+
+```bash
+meteor --settings settings.json
+```
+
+In addition to the official services, you can also configure community/custom OAuth services through the `service-configuration` package as well.
+
+### Using ServiceConfiguration (Programmatic)
+
+You can also configure OAuth services programmatically in your server code:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+// Configure Facebook
+ServiceConfiguration.configurations.upsertAsync(
+ { service: 'facebook' },
+ {
+ $set: {
+ appId: 'YOUR_APP_ID',
+ secret: 'YOUR_APP_SECRET'
+ }
+ }
+);
+
+// Configure Google
+ServiceConfiguration.configurations.upsertAsync(
+ { service: 'google' },
+ {
+ $set: {
+ clientId: 'YOUR_CLIENT_ID',
+ secret: 'YOUR_CLIENT_SECRET'
+ }
+ }
+);
+```
+
+## Facebook
+
+### Setting up Facebook OAuth
+
+1. Visit [https://developers.facebook.com/apps](https://developers.facebook.com/apps)
+
+2. Click **"Create App"** and fill out the required information.
+
+3. In **Use cases** select **Authenticate and request data from users with Facebook Login**
+
+4. In the app dashboard, click **"Add Product"** and find **"Facebook Login"**, then click **"Set Up"**
+
+5. Select **"Web"** as your platform
+
+6. In the **"Facebook Login > Settings"** from the left sidebar, set **"Valid OAuth Redirect URIs"** to `YOUR_SITE_URL/_oauth/facebook` (e.g., `http://localhost:3000/_oauth/facebook`) and click **"Save Changes"**
+
+7. Go to **"Settings > Basic"** in the left sidebar
+
+8Note down your **"App ID"** and **"App Secret"** (click **"Show"** to reveal the App Secret). You'll need these for configuration
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "facebook": {
+ "appId": "YOUR_APP_ID",
+ "secret": "YOUR_APP_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'facebook' },
+ {
+ $set: {
+ appId: 'YOUR_APP_ID',
+ secret: 'YOUR_APP_SECRET'
+ }
+ }
+);
+```
+
+## Google
+
+### Setting up Google OAuth
+
+1. Visit [https://console.cloud.google.com/](https://console.cloud.google.com/)
+
+2. Create a new project or select an existing one
+
+3. In the left sidebar, go to **"APIs & Services" > "OAuth consent screen"**
+
+4. Configure the consent screen: select **"External"** user type, enter your app name, user support email, and developer contact email, then click **"Save and Continue"**
+
+5. Skip the **"Scopes"** step (or add scopes if needed) and click **"Save and Continue"**
+
+6. Add test users if needed, then click **"Save and Continue"**
+
+7. In the left sidebar, go to **"Credentials"** and click **"Create Credentials" > "OAuth client ID"**
+
+8. Select **"Web application"** as the application type
+
+9. Add your site URL to **"Authorized JavaScript origins"** (e.g., `http://localhost:3000`)
+
+10. Add `YOUR_SITE_URL/_oauth/google` to **"Authorized redirect URIs"** (e.g., `http://localhost:3000/_oauth/google`)
+
+11. Click **"Create"** and note down your **"Client ID"** and **"Client Secret"** from the popup
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "google": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'google' },
+ {
+ $set: {
+ clientId: 'YOUR_CLIENT_ID',
+ secret: 'YOUR_CLIENT_SECRET'
+ }
+ }
+);
+```
+
+## GitHub
+
+### Setting up GitHub OAuth
+
+1. Visit [https://github.com/settings/applications/new](https://github.com/settings/applications/new)
+
+2. Set **Homepage URL** to your site URL (e.g., `http://localhost:3000` for development or `https://yourdomain.com` for production)
+
+3. Set **Authorization callback URL** to `YOUR_SITE_URL/_oauth/github` (e.g., `http://localhost:3000/_oauth/github`)
+
+4. Click **"Register application"**
+
+5. Note down your **Client ID** and **Client Secret**
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "github": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'github' },
+ {
+ $set: {
+ clientId: 'YOUR_CLIENT_ID',
+ secret: 'YOUR_CLIENT_SECRET'
+ }
+ }
+);
+```
+
+## X/Twitter
+
+### Setting up X/Twitter OAuth
+
+1. Visit [https://developer.x.com/en/portal/dashboard](https://developer.x.com/en/portal/dashboard) and sign in
+
+2. Create a new project and app (or select an existing one)
+
+3. In your app settings, click on **"Set up"** under **"User authentication settings"**
+
+4. Enable **"OAuth 1.0a"** (required for Meteor)
+
+5. Set **"Callback URI / Redirect URL"** to `YOUR_SITE_URL/_oauth/twitter` (e.g., `http://localhost:3000/_oauth/twitter`)
+
+6. Set **"Website URL"** to your site URL (e.g., `http://localhost:3000`)
+
+7. Click **"Save"**
+
+8. Go to the **"Keys and tokens"** tab and note down your **"API Key"** (Consumer Key) and **"API Key Secret"** (Consumer Secret)
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "twitter": {
+ "consumerKey": "YOUR_CONSUMER_KEY",
+ "secret": "YOUR_CONSUMER_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'twitter' },
+ {
+ $set: {
+ consumerKey: 'YOUR_CONSUMER_KEY',
+ secret: 'YOUR_CONSUMER_SECRET'
+ }
+ }
+);
+```
+
+## Meetup
+
+### Setting up Meetup OAuth
+
+1. Visit [https://www.meetup.com/api/oauth/list/](https://www.meetup.com/api/oauth/list//) and sign in
+
+2. Click **"Create new client"**
+
+3. Set the **"Client name"** to the name of your application
+
+4. Set the **"Application Website"** to your site URL
+
+5. Set the **"Redirect URI"** to your site URL (e.g., `http://localhost:3000`). **Do not append a path to this URL**
+
+6. Fill out all the other required fields.
+
+7. Click **"Create"** and note down your **"Key"** (Client ID) and **"Secret"** (Client Secret)
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "meetup": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'meetup' },
+ {
+ $set: {
+ clientId: 'YOUR_CLIENT_ID',
+ secret: 'YOUR_CLIENT_SECRET'
+ }
+ }
+);
+```
+
+## Weibo
+
+> Weibo is currently deprecated and the team is looking for maintainers for this package.
+
+### Setting up Weibo OAuth
+
+
+1. Visit [https://open.weibo.com/apps](https://open.weibo.com/apps) and sign in (Google Chrome's automatic translation works well here)
+
+2. Click **"创建应用"** (Create Application) and select **"网页应用"** (Web Application)
+
+3. Complete the registration form with your app details
+
+4. Complete the email verification process
+
+5. In your app dashboard, go to **"应用信息" > "高级信息"** (Application Info > Advanced Information)
+
+6. Set **"OAuth2.0 授权回调页"** (OAuth2.0 Redirect URI) to `YOUR_SITE_URL/_oauth/weibo` (e.g., `http://localhost:3000/_oauth/weibo`)
+
+7. In **"应用信息" > "基本信息"** (Application Info > Basic Information), note down your **"App Key"** (Client ID) and **"App Secret"** (Client Secret)
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "weibo": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'weibo' },
+ {
+ $set: {
+ clientId: 'YOUR_CLIENT_ID',
+ secret: 'YOUR_CLIENT_SECRET'
+ }
+ }
+);
+```
+
+## Meteor Developer Accounts
+
+### Setting up Meteor Developer OAuth
+
+1. Visit [https://beta.galaxycloud.app/](https://beta.galaxycloud.app/) and sign in
+
+2. Go to **Settings** -> **Authorized Domains** and **Add New Domain**
+
+
+3. Set the **"OAuth Redirect URL"** to `YOUR_SITE_URL/_oauth/meteor-developer` (e.g., `http://localhost:3000/_oauth/meteor-developer`)
+
+4. Click **"Create"** and note down your **"Client ID"** and **"Client Secret"**
+
+### Configuration
+
+Add to your `settings.json`:
+
+```json
+{
+ "packages": {
+ "service-configuration": {
+ "meteor-developer": {
+ "clientId": "YOUR_CLIENT_ID",
+ "secret": "YOUR_CLIENT_SECRET"
+ }
+ }
+ }
+}
+```
+
+Or configure programmatically:
+
+```javascript
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+await ServiceConfiguration.configurations.upsertAsync(
+ { service: 'meteor-developer' },
+ {
+ $set: {
+ clientId: 'YOUR_CLIENT_ID',
+ secret: 'YOUR_CLIENT_SECRET'
+ }
+ }
+);
+```
+
+## Community services
+
+You can add settings for community OAuth providers in the same manner as above. Just check with their documentation for the naming of the keys and any other exceptional behavior.
+If those providers have `*-config-ui` you can find the instructions there.
\ No newline at end of file
diff --git a/v3-docs/docs/packages/standard-minifier-css.md b/v3-docs/docs/packages/standard-minifier-css.md
index f3d55e8ea0..ace7a833e8 100644
--- a/v3-docs/docs/packages/standard-minifier-css.md
+++ b/v3-docs/docs/packages/standard-minifier-css.md
@@ -24,7 +24,7 @@ module.exports = {
> The example config enables the `autoprefixer` postcss plugin. You can install the plugin by running `meteor npm install -D autoprefixer`.
-Learn more about [configuring postcss](https://github.com/postcss/postcss-load-config#packagejson) or find a list of [available plugins](https://www.postcss.parts/).
+Learn more about [configuring postcss](https://github.com/postcss/postcss-load-config#packagejson) or find a list of [available plugins](https://postcss.org/docs/postcss-plugins).
After making changes to the PostCSS Config, `meteor` must be restarted for it to use the new config.
diff --git a/v3-docs/docs/packages/underscore.md b/v3-docs/docs/packages/underscore.md
deleted file mode 100644
index b82b2cb28b..0000000000
--- a/v3-docs/docs/packages/underscore.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Underscore
-
-[Underscore](http://underscorejs.org/) is a utility-belt library for
-JavaScript that provides support for functional programming. It is
-invaluable for writing clear, concise JavaScript in a functional style.
-
-The `underscore` package defines the `_` namespace on both the client
-and the server.
-
-::: warning
-We have slightly modified the way Underscore differentiates between
-objects and arrays in [collection functions](http://underscorejs.org/#each).
-The original Underscore logic is to treat any object with a numeric `length`
-property as an array (which helps it work properly on
-[`NodeList`s](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)).
-In Meteor's version of Underscore, objects with a numeric `length` property
-are treated as objects if they have no prototype (specifically, if
-`x.constructor === Object`.
-:::
diff --git a/v3-docs/docs/performance/websocket-compression.md b/v3-docs/docs/performance/websocket-compression.md
new file mode 100644
index 0000000000..e7c117f155
--- /dev/null
+++ b/v3-docs/docs/performance/websocket-compression.md
@@ -0,0 +1,104 @@
+# Websocket Compression in Meteor
+
+::: warning
+Modifying websocket compression settings without understanding your application's DDP messaging patterns can negatively impact performance. Before changing these settings, you should:
+- Use [Meteor DevTools Evolved](https://chromewebstore.google.com/detail/meteor-devtools-evolved/ibniinmoafhgbifjojidlagmggecmpgf) or your browser's Network tab to monitor WebSocket traffic
+- Analyze your DDP message frequency and payload sizes
+- Test changes in a staging environment with realistic data and user load
+:::
+
+Meteor's stream server uses the permessage-deflate extension for websocket compression by default. While compression can help reduce bandwidth usage, it may impact performance in reactivity-intensive applications due to the computational overhead of compressing numerous DDP messages.
+
+## Configuration
+
+### Disabling Compression
+
+You can disable websocket compression by setting the `SERVER_WEBSOCKET_COMPRESSION` environment variable to `false`:
+
+```bash
+SERVER_WEBSOCKET_COMPRESSION=false
+```
+
+### Custom Compression Settings
+
+To customize compression settings, set `SERVER_WEBSOCKET_COMPRESSION` to a JSON string with your desired configuration:
+
+```bash
+# Example with custom settings
+SERVER_WEBSOCKET_COMPRESSION='{"threshold": 2048, "level": 1}'
+```
+
+Available configuration options:
+
+- `threshold`: Minimum message size (in bytes) before compression is applied (default: 1024)
+- `level`: Compression level (0-9, where 0=none, 1=fastest, 9=best compression)
+- `memLevel`: Memory level (1-9, lower uses less memory)
+- `noContextTakeover`: When true, compressor resets for each message (default: true)
+- `maxWindowBits`: Window size for compression (9-15, lower uses less memory)
+
+## Configuration Examples
+
+Here are recommended configurations for different types of applications:
+
+### High-Frequency Updates / Real-Time Dashboard
+
+For applications with frequent small updates (e.g., real-time dashboards, trading platforms):
+
+```bash
+# Disable compression for optimal performance with small, frequent updates
+SERVER_WEBSOCKET_COMPRESSION=false
+```
+
+### Large Data Transfers
+
+For applications transferring large datasets (e.g., file sharing, data visualization):
+
+```bash
+# Optimize for large data transfers
+SERVER_WEBSOCKET_COMPRESSION='{"threshold": 1024, "level": 6, "memLevel": 8}'
+```
+
+### Memory-Constrained Environment
+
+For deployments with limited memory resources:
+
+```bash
+# Minimize memory usage while maintaining compression
+SERVER_WEBSOCKET_COMPRESSION='{"threshold": 2048, "level": 1, "memLevel": 1, "maxWindowBits": 9}'
+```
+
+### Balanced Configuration
+
+For typical applications with mixed message sizes:
+
+```bash
+# Balance between compression and performance
+SERVER_WEBSOCKET_COMPRESSION='{"threshold": 1536, "level": 3, "memLevel": 4}'
+```
+
+## Verifying Compression Status
+
+You can check if compression is enabled through the Meteor shell:
+
+```javascript
+Meteor.server.stream_server.server.options.faye_server_options.extensions
+```
+
+Results interpretation:
+- `[]` (empty array): Compression is disabled
+- `[{}]` (array with object): Compression is enabled
+
+## Performance Considerations
+
+- For apps with high message throughput or frequent small updates, disabling compression may improve performance
+- Large message payloads may benefit from compression, especially over slower network connections
+- Consider monitoring CPU usage and response times when adjusting compression settings
+
+## Default Configuration
+
+When enabled, the default configuration uses:
+- Compression threshold: 1024 bytes
+- Compression level: Z_BEST_SPEED (fastest)
+- Memory level: Z_MIN_MEMLEVEL (minimum memory usage)
+- Context takeover: Disabled
+- Window bits: Z_MIN_WINDOWBITS (minimum window size)
diff --git a/v3-docs/docs/public/logo.png b/v3-docs/docs/public/logo.png
index c333537981..8644c2a46e 100644
Binary files a/v3-docs/docs/public/logo.png and b/v3-docs/docs/public/logo.png differ
diff --git a/v3-docs/docs/public/meteor-blue.png b/v3-docs/docs/public/meteor-blue.png
index c91f7a9718..0934189a3e 100644
Binary files a/v3-docs/docs/public/meteor-blue.png and b/v3-docs/docs/public/meteor-blue.png differ
diff --git a/v3-docs/docs/public/meteor-logo.png b/v3-docs/docs/public/meteor-logo.png
index ca7bb89de5..3b848982ca 100644
Binary files a/v3-docs/docs/public/meteor-logo.png and b/v3-docs/docs/public/meteor-logo.png differ
diff --git a/v3-docs/docs/troubleshooting/expired-certificate.md b/v3-docs/docs/troubleshooting/expired-certificate.md
index 5678a59e04..fa8e9cea12 100644
--- a/v3-docs/docs/troubleshooting/expired-certificate.md
+++ b/v3-docs/docs/troubleshooting/expired-certificate.md
@@ -69,7 +69,7 @@ sudo certbot certonly --manual --preferred-chain "ISRG Root X1" --preferred-chal
More info can be obtained [here](https://letsencrypt.org/certificates).
-If you are using Galaxy, you need to follow the requirements and steps [here](https://galaxy-guide.meteor.com/encryption.html#Custom%20certificate) after generating the certificate. Galaxy only accepts custom certs in `.pem` format, the same as nginx uses.
+If you are using Galaxy, you need to follow the requirements and steps [here](https://galaxy-support.meteor.com/en/article/encryption-pt8wbl/) after generating the certificate. Galaxy only accepts custom certs in `.pem` format, the same as nginx uses.
This is not a Meteor or Galaxy issue, but it's a change in the Let's Encrypt certificate you are using.
diff --git a/v3-docs/docs/troubleshooting/known-issues.md b/v3-docs/docs/troubleshooting/known-issues.md
deleted file mode 100644
index f533d80a76..0000000000
--- a/v3-docs/docs/troubleshooting/known-issues.md
+++ /dev/null
@@ -1,39 +0,0 @@
-
-# Known issues in 2.13
-
-Troubleshooting in Meteor 2.13
-
-## Cannot extract version of meteor tool {#cannot-extract-meteor-tool}
-
-
-For some users, the `meteor update` to version 2.13 command may fail with the following error or similar:
-
-```shell
-Error: incorrect data check
- at Zlib.zlibOnError [as onerror] (zlib.js:187:17)
- => awaited here:
- ...
- at /tools/cli/main.js:1165:7 {
- errno: -3,
- code: 'Z_DATA_ERROR'
- }
-
-```
-
-## The issue {#the-issue}
-
-It seems related to [our first ESM version of Node.js v14.21.4](https://github.com/meteor/node-v14-esm) and the `zlib` package.
-We have been able to reproduce this issue only in Mac Intel.
-
-You can follow along with the [GitHub issue](https://github.com/meteor/meteor/issues/12731) for updates.
-
-## Solution {#solution}
-
-The solution for this issue is running the following command in your terminal:
-
-```shell
-
-curl https://install.meteor.com/\?release\=2.13.3 | sh
-
-```
-
diff --git a/v3-docs/docs/troubleshooting/mongodb-connection.md b/v3-docs/docs/troubleshooting/mongodb-connection.md
new file mode 100644
index 0000000000..90bbd0aaef
--- /dev/null
+++ b/v3-docs/docs/troubleshooting/mongodb-connection.md
@@ -0,0 +1,17 @@
+# Topology Closed Error
+
+One possible scenario we've seen is that after migrating to Meteor 3, some apps become partially unresponsive and throw `MongoTopologyClosedError: Topology is closed` errors on startup.
+
+In this case, you might consider increasing the server selection timeout for your MongoDB instance, like this:
+
+```json
+{
+ "packages": {
+ "mongo": {
+ "options": {
+ "serverSelectionTimeoutMS": 120000
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/v3-docs/docs/tutorials/application-structure/index.md b/v3-docs/docs/tutorials/application-structure/index.md
new file mode 100644
index 0000000000..c48ae722d9
--- /dev/null
+++ b/v3-docs/docs/tutorials/application-structure/index.md
@@ -0,0 +1,357 @@
+# Application Structure
+
+After reading this article, you'll know:
+
+1. How a Meteor application compares to other types of applications in terms of file structure.
+2. How to organize your application both for small and larger applications.
+3. How to format your code and name the parts of your application in consistent and maintainable ways.
+
+[[toc]]
+
+## Universal JavaScript
+
+Meteor is a *full-stack* framework for building JavaScript applications. This means Meteor applications differ from most applications in that they include code that runs on the client, inside a web browser or Cordova mobile app, code that runs on the server, inside a [Node.js](http://nodejs.org/) container, and _common_ code that runs in both environments. The [Meteor build tool](https://guide.meteor.com/build-tool) allows you to specify what JavaScript code, including any supporting UI templates, CSS rules, and static assets, to run in each environment using a combination of ES2015 `import` and `export` and the Meteor build system [default file load order](#default-file-load-order) rules.
+
+### ES2015 modules
+
+As of version 1.3, Meteor ships with full support for [ES2015 modules](https://developer.mozilla.org/en/docs/web/javascript/reference/statements/import). The ES2015 module standard is the replacement for [CommonJS](http://requirejs.org/docs/commonjs.html) and [AMD](https://github.com/amdjs/amdjs-api), which are commonly used JavaScript module format and loading systems.
+
+In ES2015, you can make variables available outside a file using the `export` keyword. To use the variables somewhere else, you must `import` them using the path to the source. Files that export some variables are called "modules", because they represent a unit of reusable code. Explicitly importing the modules and packages you use helps you write your code in a modular way, avoiding the introduction of global symbols and "action at a distance".
+
+You can read about the module system in detail in the [`modules` package README](https://docs.meteor.com/#/full/modules). This package is automatically included in every new Meteor app as part of the [`ecmascript` meta-package](https://docs.meteor.com/#/full/ecmascript), so most apps won't need to do anything to start using modules right away.
+
+### Introduction to using `import` and `export`
+
+Meteor allows you to `import` not only JavaScript in your application, but also CSS and HTML to control load order:
+
+```js
+import '../../api/lists/methods.js'; // import from relative path
+import '/imports/startup/client'; // import module with index.js from absolute path
+import './loading.html'; // import Blaze compiled HTML from relative path
+import '/imports/ui/style.css'; // import CSS from absolute path
+```
+
+> For more ways to import styles, see the [Build System](https://guide.meteor.com/build-tool#css-importing) article.
+
+Meteor also supports the standard ES2015 modules `export` syntax:
+
+```js
+export const listRenderHold = LaunchScreen.hold(); // named export
+export { Todos }; // named export
+export default Lists; // default export
+export default new Collection('lists'); // default export
+```
+
+### Importing from packages
+
+In Meteor, it is also simple and straightforward to use the `import` syntax to load npm packages on the client or server and access the package's exported symbols as you would with any other module. You can also import from Meteor Atmosphere packages, but the import path must be prefixed with `meteor/` to avoid conflict with the npm package namespace. For example, to import `moment` from npm and `HTTP` from Atmosphere:
+
+```js
+import moment from 'moment'; // default import from npm
+import { HTTP } from 'meteor/http'; // named import from Atmosphere
+```
+
+For more details using `imports` with packages see [Using Packages](../../packages/#using-atmosphere-packages) tutorial.
+
+### Using `require`
+
+In Meteor, `import` statements compile to CommonJS `require` syntax. However, as a convention we encourage you to use `import`.
+
+With that said, in some situations you may need to call out to `require` directly. One notable example is when requiring client or server-only code from a common file. As `import`s must be at the top-level scope, you may not place them within an `if` statement, so you'll need to write code like:
+
+```js
+if (Meteor.isClient) {
+ require('./client-only-file.js');
+}
+```
+
+Note that dynamic calls to `require()` (where the name being required can change at runtime) cannot be analyzed correctly and may result in broken client bundles.
+
+If you need to `require` from an ES2015 module with a `default` export, you can grab the export with `require("package").default`.
+
+### Using CoffeeScript
+
+See the Docs: [CoffeeScript](https://docs.meteor.com/packages/coffeescript.html#coffeescript)
+
+```cs
+// lists.coffee
+
+export Lists = new Collection 'lists'
+```
+
+```cs
+import { Lists } from './lists.coffee'
+```
+
+## File structure
+
+To fully use the module system and ensure that our code only runs when we ask it to, we recommend that all of your application code should be placed inside the `imports/` directory. This means that the Meteor build system will only bundle and include that file if it is referenced from another file using an `import` (also called "lazy evaluation or loading").
+
+Meteor will load all files outside of any directory named `imports/` in the application using the [default file load order](#default-file-load-order) rules (also called "eager evaluation or loading"). It is recommended that you create exactly two eagerly loaded files, `client/main.js` and `server/main.js`, in order to define explicit entry points for both the client and the server. Meteor ensures that any file in any directory named `server/` will only be available on the server, and likewise for files in any directory named `client/`. This also precludes trying to `import` a file to be used on the server from any directory named `client/` even if it is nested in an `imports/` directory and vice versa for importing client files from `server/`.
+
+These `main.js` files won't do anything themselves, but they should import some _startup_ modules which will run immediately, on client and server respectively, when the app loads. These modules should do any configuration necessary for the packages you are using in your app, and import the rest of your app's code.
+
+### Example directory layout
+
+To start, one can have a look to the [example applications](https://github.com/meteor/examples) provided. They are great examples to follow when structuring your app.
+
+Below is an overview of an exemple directory structure. You can generate a new app with this structure using the command `meteor create appName --full`. The default frontend is Blaze, but you can change it later. Or use [another create option](https://docs.meteor.com/cli/#meteorcreate)
+
+
+```sh
+imports/
+ startup/
+ both/
+ index.js # single entry point to import isomorphic modules for client and server
+ client/
+ index.js # import client startup through a single index entry point
+ routes.js # set up all routes in the app
+ server/
+ fixtures.js # fill the DB with example data on startup
+ index.js # import server startup through a single index entry point
+ register-api.js # dedicated file to import server code for api
+
+ api/
+ lists/ # a unit of domain logic
+ server/
+ publications.js # all list-related publications
+ publications.tests.js # tests for the list publications
+ lists.js # definition of the Lists collection
+ lists.tests.js # tests for the behavior of that collection
+ methods.js # methods related to lists
+ methods.tests.js # tests for those methods
+
+ ui/
+ components/ # all reusable components in the application
+ # can be split by domain if there are many
+ layouts/ # wrapper components for behaviour and visuals
+ pages/ # entry points for rendering used by the router
+ stylesheets/ # global stylesheets
+
+private/ # to store private assets for the server
+public/ # to store public assets (pictures)
+
+client/
+ main.js # client entry point, imports all client code
+
+server/
+ main.js # server entry point, imports all server code
+```
+
+### Structuring imports
+
+Now that we have placed all files inside the `imports/` directory, let's think about how best to organize our code using modules. It makes sense to put all code that runs when your app starts in an `imports/startup` directory. Another good idea is splitting data and business logic from UI rendering code. We suggest using directories called `imports/api` and `imports/ui` for this logical split.
+
+Within the `imports/api` directory, it's sensible to split the code into directories based on the domain that the code is providing an API for --- typically this corresponds to the collections you've defined in your app. For instance in the Todos example app, we have the `imports/api/lists` and `imports/api/todos` domains. Inside each directory we define the collections, publications and methods used to manipulate the relevant domain data. Each API folder typically has different files for isomorphic code and server-specific code. To ensure good isolation, server-specific code is put into a `server` folder.
+
+> Note: in a larger application, given that the todos themselves are a part of a list, it might make sense to group both of these domains into a single larger "list" module. The Todos example is small enough that we need to separate these only to demonstrate modularity.
+
+Within the `imports/ui` directory it typically makes sense to group files into directories based on the type of UI side code they define, i.e. top level `pages`, wrapping `layouts`, or reusable `components`.
+
+For each module defined above, it makes sense to co-locate the various auxiliary files with the base JavaScript file. For instance, a Blaze UI component should have its template HTML, JavaScript logic, and CSS rules in the same directory. A JavaScript module with some business logic should be co-located with the unit tests for that module.
+
+### Startup files
+
+Some of your code isn't going to be a unit of business logic or UI, it's some setup or configuration code that needs to run in the context of the app when it starts up. In the above example, the `imports/startup/client/routes.js` configures all the routes and then imports *all* other code that is required on the client:
+
+```js
+import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
+
+// Import needed templates
+import '../../ui/layouts/body/body.js';
+import '../../ui/pages/home/home.js';
+import '../../ui/pages/not-found/not-found.js';
+```
+
+We then import both of these files in `imports/startup/client/index.js`:
+
+```js
+import './routes.js';
+```
+
+This makes it easy to then import all the client startup code with a single import as a module from our main eagerly loaded client entry point `client/main.js`:
+
+```js
+import '/imports/startup/client';
+```
+
+On the server, we use the same technique of importing all the startup code in `imports/startup/server/index.js`:
+
+```js
+import './fixtures.js';
+import './register-api.js';
+```
+
+Our main server entry point `server/main.js` then imports this startup module. You can see that here we don't actually import any variables from these files - we import them so that they execute in this order.
+
+### Importing Meteor "pseudo-globals"
+
+For backwards compatibility Meteor still provides Meteor's global namespacing for the Meteor core package as well as for other Meteor packages you include in your application. You can also still directly call functions such as [`Meteor.publish`](https://docs.meteor.com/api/Meteor#Meteor-publish), as in previous versions of Meteor, without first importing them. However, it is recommended best practice that you first load all the Meteor "pseudo-globals" using the `import { Name } from 'meteor/package'` syntax before using them. For instance:
+
+```js
+import { Meteor } from 'meteor/meteor';
+import { EJSON } from 'meteor/ejson';
+```
+
+## Default file load order
+
+Even though it is recommended that you write your application to use ES2015 modules and the `imports/` directory, Meteor continues to support eager evaluation of files, using these default load order rules, to provide backwards compatibility with applications written for Meteor 1.2 and earlier. For a description of the difference between eager evaluation, lazy evaluation, and lazy loading, please see this Stack Overflow [article](https://stackoverflow.com/a/51158735/219238).
+
+You may combine both eager evaluation and lazy loading using `import` in a single app. Any import statements are evaluated in the order they are listed in a file when that file is loaded and evaluated using these rules.
+
+There are several load order rules. They are *applied sequentially* to all applicable files in the application, in the priority given below:
+
+1. HTML template files are **always** loaded before everything else
+2. Files beginning with `main.` are loaded **last**
+3. Files inside **any** `lib/` directory are loaded next
+4. Files with deeper paths are loaded next
+5. Files are then loaded in alphabetical order of the entire path
+
+```js
+ nav.html
+ main.html
+ client/lib/methods.js
+ client/lib/styles.js
+ lib/feature/styles.js
+ lib/collections.js
+ client/feature-y.js
+ feature-x.js
+ client/main.js
+```
+
+For example, the files above are arranged in the correct load order. `main.html` is loaded second because HTML templates are always loaded first, even if it begins with `main.`, since rule 1 has priority over rule 2. However, it will be loaded after `nav.html` because rule 2 has priority over rule 5.
+
+`client/lib/styles.js` and `lib/feature/styles.js` have identical load order up to rule 4; however, since `client` comes before `lib` alphabetically, it will be loaded first.
+
+> You can also use [Meteor.startup](https://docs.meteor.com/api/Meteor#Meteor-startup) to control when run code is run on both the server and the client.
+
+### Special directories
+
+By default, any JavaScript files in your Meteor application folder are bundled and loaded on both the client and the server. However, the names of the files and directories inside your project can affect their load order, where they are loaded, and some other characteristics. Here is a list of file and directory names that are treated specially by Meteor:
+
+- **imports**
+
+ Any directory named `imports/` is not loaded anywhere and files must be imported using `import`.
+
+- **node_modules**
+
+ Any directory named `node_modules/` is not loaded anywhere. node.js packages installed into `node_modules` directories must be imported using `import` or by using `Npm.depends` in `package.js`.
+
+- **client**
+
+ Any directory named `client/` is not loaded on the server. Similar to wrapping your code in `if (Meteor.isClient) { ... }`. All files loaded on the client are automatically concatenated and minified when in production mode. In development mode, JavaScript and CSS files are not minified, to make debugging easier. CSS files are still combined into a single file for consistency between production and development, because changing the CSS file's URL affects how URLs in it are processed.
+
+ > HTML files in a Meteor application are treated quite a bit differently from a server-side framework. Meteor scans all the HTML files in your directory for three top-level elements: ``, ``, and ``. The head and body sections are separately concatenated into a single head and body, which are transmitted to the client on initial page load.
+
+- **server**
+
+ Any directory named `server/` is not loaded on the client. Similar to wrapping your code in `if (Meteor.isServer) { ... }`, except the client never even receives the code. Any sensitive code that you don't want served to the client, such as code containing passwords or authentication mechanisms, should be kept in the `server/` directory.
+
+ Meteor gathers all your JavaScript files, excluding anything under the `client`, `public`, and `private` subdirectories, and loads them into a Node.js server instance. In Meteor, your server code runs in a single thread per request, not in the asynchronous callback style typical of Node.
+
+- **public**
+
+ All files inside a top-level directory called `public/` are served as-is to the client. When referencing these assets, do not include `public/` in the URL, write the URL as if they were all in the top level. For example, reference `public/bg.png` as ` `. This is the best place for `favicon.ico`, `robots.txt`, and similar files.
+
+- **private**
+
+ All files inside a top-level directory called `private/` are only accessible from server code and can be loaded via the [`Assets`](https://docs.meteor.com/api/Assets#Assets-getTextAsync) API. This can be used for private data files and any files that are in your project directory that you don't want to be accessible from the outside.
+
+- **client/compatibility**
+
+ This folder is for compatibility with JavaScript libraries that rely on variables declared with var at the top level being exported as globals. Files in this directory are executed without being wrapped in a new variable scope. These files are executed before other client-side JavaScript files.
+
+ > It is recommended to use npm for 3rd party JavaScript libraries and use `import` to control when files are loaded.
+
+- **tests**
+
+ Any directory named `tests/` is not loaded anywhere. Use this for any test code you want to run using a test runner outside of [Meteor's built-in test tools](https://guide.meteor.com/testing.html).
+
+The following directories are also not loaded as part of your app code:
+
+- Files/directories whose names start with a dot, like `.meteor` and `.git`
+- `packages/`: Used for local packages
+- `cordova-build-override/`: Used for [advanced mobile build customizations](https://guide.meteor.com/mobile.html#advanced-build)
+- `programs`: For legacy reasons
+
+### Files outside special directories
+
+All JavaScript files outside special directories are loaded on both the client and the server. Meteor provides the variables [`Meteor.isClient`](https://docs.meteor.com/api/Meteor#Meteor-isClient) and [`Meteor.isServer`](https://docs.meteor.com/api/Meteor#Meteor-isServer) so that your code can alter its behavior depending on whether it's running on the client or the server.
+
+CSS and HTML files outside special directories are loaded on the client only and cannot be used from server code.
+
+## Splitting into multiple apps
+
+If you are writing a sufficiently complex system, there can come a time where it makes sense to split your code up into multiple applications. For example you may want to create a separate application for the administration UI (rather than checking permissions all through the admin part of your site, you can check once), or separate the code for the mobile and desktop versions of your app.
+
+Another very common use case is splitting a worker process away from your main application so that expensive jobs do not impact the user experience of your visitors by locking up a single web server.
+
+There are some advantages of splitting your application in this way:
+
+- Your client JavaScript bundle can be significantly smaller if you separate out code that a specific type of user will never use.
+
+- You can deploy the different applications with different scaling setups and secure them differently (for instance you might restrict access to your admin application to users behind a firewall).
+
+- You can allow different teams at your organization to work on the different applications independently.
+
+However there are some challenges to splitting your code in this way that should be considered before jumping in.
+
+### Sharing code
+
+The primary challenge is properly sharing code between the different applications you are building. The simplest approach to deal with this issue is to deploy the *same* application on different web servers, controlling the behavior via different [settings](https://guide.meteor.com/deployment.html#environment). This approach allows you to deploy different versions with different scaling behavior but doesn't enjoy most of the other advantages stated above.
+
+If you want to create Meteor applications with separate code, you'll have some modules that you'd like to share between them. If those modules are something the wider world could use, you should consider [publishing them to a package system](../../packages/#writing-atmosphere-packages), either npm or Atmosphere, depending on whether the code is Meteor-specific or otherwise.
+
+If the code is private, or of no interest to others, it typically makes sense to include the same module in both applications (you *can* do this with [private npm modules](https://docs.npmjs.com/about-private-packages)). There are several ways to do this:
+
+- a straightforward approach is to include the common code as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) of both applications.
+
+- alternatively, if you include both applications in a single repository, you can use symbolic links to include the common module inside both apps.
+
+### Sharing data
+
+Another important consideration is how you'll share the data between your different applications.
+
+The simplest approach is to point both applications at the same `MONGO_URL` and allow both applications to read and write from the database directly. This works well thanks to Meteor's support for reactivity through the database. When one app changes some data in MongoDB, users of any other app connected to the database will see the changes immediately thanks to Meteor's livequery.
+
+However, in some cases it's better to allow one application to be the master and control access to the data for other applications via an API. This can help if you want to deploy the different applications on different schedules and need to be conservative about how the data changes.
+
+The simplest way to provide a server-server API is to use Meteor's built-in DDP protocol directly. This is the same way your Meteor client gets data from your server, but you can also use it to communicate between different applications. You can use [`DDP.connect()`](https://docs.meteor.com/api/meteor#DDP-connect) to connect from a "client" server to the master server, and then use the connection object returned to make method calls and read from publications.
+
+If you need a more traditional API, you can use the bundled `express` available in Meteor. See the [documentation of WebApp](https://docs.meteor.com/packages/webapp#webapp)
+
+```js
+import { Meteor } from "meteor/meteor";
+import { WebApp } from "meteor/webapp";
+import bodyParser from "body-parser";
+
+const app = WebApp.handlers;
+app.use(bodyParser.json());
+app.use("/hello", (req, res, next) => {
+ res.writeHead(200);
+ res.end(`Hello world from: ${Meteor.release}`);
+});
+```
+
+### Sharing accounts
+
+If you have two servers that access the same database and you want authenticated users to make DDP calls across the both of them, you can use the *resume token* set on one connection to login on the other.
+
+If your user has connected to server A, then you can use `DDP.connect()` to open a connection to server B, and pass in server A's resume token to authenticate on server B. As both servers are using the same DB, the same server token will work in both cases. The code to authenticate looks like this:
+
+```js
+// This is server A's token as the default `Accounts` points at our server
+const token = Accounts._storedLoginToken();
+
+// We create a *second* accounts client pointing at server B
+const app2 = DDP.connect('url://of.server.b');
+const accounts2 = new AccountsClient({ connection: app2 });
+
+// Now we can login with the token. Further calls to `accounts2` will be authenticated
+accounts2.loginWithToken(token);
+```
+
+You can see a proof of concept of this architecture in an [example repository](https://github.com/tmeasday/multi-app-accounts).
+
+
+Another pattern is to have another app which acts as identity provider for your apps. The [leaonline:oauth2-server](https://atmospherejs.com/leaonline/oauth2-server) package can be used to create an OAuth2 server in one Meteor app, and then other apps can use the standard `accounts-oauth` packages to authenticate users against it.
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 }}
+
+
+
+
+
+
+
+ {{#each tasks}}
+ {{> task}}
+ {{/each}}
+
+
+
+
+
+ {{text}}
+
+```
+:::
+
+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 ``.
+
+Everything inside any `` tags is added to the head section of the HTML sent to the client, and everything inside `` tags is added to the body section, just like in a regular HTML file.
+
+Everything inside `` tags is compiled into Meteor templates, which can be included inside HTML with {{> templateName}} or referenced in your JavaScript with `Template.templateName`.
+
+Also, the `body` section can be referenced in your JavaScript with `Template.body`. Think of it as a special “parent” template, that can include the other child templates.
+
+All of the code in your HTML files will be compiled with [Meteor’s Spacebars compiler](http://blazejs.org/api/spacebars.html). Spacebars uses statements surrounded by double curly braces such as {{#each}} and {{#if}} to let you add logic and data to your views.
+
+You can pass data into templates from your JavaScript code by defining helpers. In the code above, we defined a helper called `tasks` on `Template.mainContainer` that returns an array. Inside the template tag of the HTML, we can use {{#each tasks}} to iterate over the array and insert a task template for each value. Inside the #each block, we can display the text property of each array item using {{text}} .
+
+### 1.6: Mobile Look
+
+Let’s see how your app is looking on mobile. You can simulate a mobile environment by `right clicking` your app in the browser (we are assuming you are using Google Chrome, as it is the most popular browser) and then `inspect`, this will open a new window inside your browser called `Dev Tools`. In the `Dev Tools` you have a small icon showing a Mobile device and a Tablet:
+
+
+
+Click on it and then select the phone that you want to simulate and in the top nav bar.
+
+> You can also check your app in your personal cellphone. To do so, connect to your App using your local IP in the navigation browser of your mobile browser.
+>
+> This command should print your local IP for you on Unix systems
+> `ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}'`
+>
+> On Microsoft Windows try this in a command prompt
+> `ipconfig | findstr "IPv4 Address"`
+
+You should see the following:
+
+
+
+As you can see, everything is small, as we are not adjusting the view port for mobile devices. You can fix this and other similar issues by adding these lines to your `client/main.html` file, inside the `head` tag, after the `title`.
+
+::: code-group
+
+```html [client/main.html]
+...
+
+
+
+
+
+...
+```
+:::
+
+Now your app should look like this:
+
+
+
+
+### 1.7: Hot Module Replacement
+
+By default, when using Blaze with Meteor, a package called [hot-module-replacement](https://docs.meteor.com/packages/hot-module-replacement) is already added for you. This package updates the javascript modules in a running app that were modified during a rebuild. Reduces the feedback cycle while developing, so you can view and test changes quicker (it even updates the app before the build has finished). You are also not going to lose the state, your app code will be updated, and your state will be the same.
+
+> You can read more about packages [here](https://docs.meteor.com/packages/).
+
+You should also add the package [dev-error-overlay](https://atmospherejs.com/meteor/dev-error-overlay) at this point, so you can see the errors in your web browser.
+
+```shell
+meteor add dev-error-overlay
+```
+
+You can try to make some mistakes and then you are going to see the errors in the browser and not only in the console.
+
+In the next step we are going to work with our MongoDB database to be able to store our tasks.
diff --git a/v3-docs/docs/tutorials/blaze/2.collections.md b/v3-docs/docs/tutorials/blaze/2.collections.md
new file mode 100644
index 0000000000..c605f2fdf4
--- /dev/null
+++ b/v3-docs/docs/tutorials/blaze/2.collections.md
@@ -0,0 +1,156 @@
+## 2: Collections
+
+Meteor already sets up MongoDB for you. In order to use our database, we need to create a _collection_, which is where we will store our _documents_, in our case our `tasks`.
+
+> You can read more about collections [here](https://v3-docs.meteor.com/api/collections.html).
+
+In this step we will implement all the necessary code to have a basic collection for our tasks up and running.
+
+### 2.1: Create Tasks Collection {#create-tasks-collection}
+
+Create a new directory in `imports/api` if it doesn't exist already. We can create a new collection to store our tasks by creating a new file at `imports/api/TasksCollection.js` which instantiates a new Mongo collection and exports it.
+
+::: code-group
+
+```js [imports/api/TasksCollection.js]
+import { Mongo } from "meteor/mongo";
+
+export const TasksCollection = new Mongo.Collection("tasks");
+```
+:::
+
+Notice that we stored the file in the `imports/api` directory, which is a place to store API-related code, like publications and methods. You can name this folder as you want, this is just a choice.
+
+> You can read more about app structure and imports/exports [here](http://guide.meteor.com/structure.html).
+
+### 2.2: Initialize Tasks Collection {#initialize-tasks-collection}
+
+For our collection to work, you need to import it in the server so it sets some plumbing up.
+
+You can either use `import "/imports/api/TasksCollection"` or `import { TasksCollection } from "/imports/api/TasksCollection"` if you are going to use on the same file, but make sure it is imported.
+
+Now it is easy to check if there is data or not in our collection, otherwise, we can insert some sample data easily as well.
+
+You don't need to keep the old content of `server/main.js`.
+
+::: code-group
+
+```js [server/main.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "/imports/api/TasksCollection";
+
+const insertTask = (taskText) =>
+ TasksCollection.insertAsync({ text: taskText });
+
+Meteor.startup(async () => {
+ if ((await TasksCollection.find().countAsync()) === 0) {
+ [
+ "First Task",
+ "Second Task",
+ "Third Task",
+ "Fourth Task",
+ "Fifth Task",
+ "Sixth Task",
+ "Seventh Task",
+ ].forEach(insertTask);
+ }
+});
+```
+:::
+
+So you are importing the `TasksCollection` and adding a few tasks to it iterating over an array of strings and for each string calling a function to insert this string as our `text` field in our `task` document.
+
+### 2.3: Render Tasks Collection {#render-tasks-collection}
+
+Now comes the fun part, you will render the tasks with Blaze. That will be pretty simple.
+
+In your `App.js` file, import the `TasksCollection` file and, instead of returning a static array, return the tasks saved in the database:
+
+::: code-group
+
+```javascript [imports/ui/App.js]
+import { Template } from 'meteor/templating';
+import { TasksCollection } from "../api/TasksCollection";
+import './App.html';
+
+Template.mainContainer.helpers({
+ tasks() {
+ return TasksCollection.find({});
+ },
+});
+```
+:::
+
+But wait! Something is missing. If you run your app now, you'll see that you don't render any tasks.
+
+That's because we need to publish our data to the client.
+
+> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub).
+
+Meteor doesn't need REST calls. It instead relies on synchronizing the MongoDB on the server with a MiniMongoDB on the client. It does this by first publishing collections on the server and then subscribing to them on the client.
+
+First, create a publication for our tasks:
+
+::: code-group
+
+```javascript [imports/api/TasksPublications.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.publish("tasks", function () {
+ return TasksCollection.find();
+});
+```
+:::
+
+Now, we need to import this file in our server:
+
+::: code-group
+
+```js [server/main.js]
+...
+import { TasksCollection } from '/imports/api/TasksCollection';
+
+import "../imports/api/TasksPublications"; // [!code highlight]
+
+const insertTask = taskText => TasksCollection.insertAsync({ text: taskText });
+...
+```
+:::
+
+The only thing left is subscribe to this publication:
+
+::: code-group
+
+```javascript [imports/ui/App.js]
+...
+
+Template.mainContainer.onCreated(function mainContainerOnCreated() {
+ Meteor.subscribe('tasks');
+});
+
+...
+```
+:::
+
+See how your app should look like now:
+
+
+
+You can change your data on MongoDB in the server and your app will react and re-render for you.
+
+You can connect to your MongoDB running `meteor mongo` in the terminal from your app folder or using a Mongo UI client, like [NoSQLBooster](https://nosqlbooster.com/downloads). Your embedded MongoDB is running in port `3001`.
+
+See how to connect:
+
+
+
+See your database:
+
+
+
+You can double-click your collection to see the documents stored on it:
+
+
+
+In the next step, we are going to create tasks using a form.
diff --git a/v3-docs/docs/tutorials/blaze/3.forms-and-events.md b/v3-docs/docs/tutorials/blaze/3.forms-and-events.md
new file mode 100644
index 0000000000..053aafe0c3
--- /dev/null
+++ b/v3-docs/docs/tutorials/blaze/3.forms-and-events.md
@@ -0,0 +1,205 @@
+## 3: Forms and Events
+
+All apps need to allow the user to perform some sort of interaction with the data that is stored. In our case, the first type of interaction is to insert new tasks. Without it, our To-Do app wouldn't be very helpful.
+
+One of the main ways in which a user can insert or edit data on a website is through forms. In most cases, it is a good idea to use the `
+ );
+};
+```
+:::
+
+### 4.2: Toggle Checkbox
+
+Now you can update your task document by toggling its `isChecked` field.
+
+First, create a new method called `tasks.toggleChecked` to update the `isChecked` property.
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.methods({
+ ...
+ "tasks.toggleChecked"({ _id, isChecked }) {
+ return TasksCollection.updateAsync(_id, {
+ $set: { isChecked: !isChecked },
+ });
+ },
+});
+```
+:::
+
+In `Task.jsx` (as shown above), we've added an `onChange` handler that calls the method to toggle the checked state.
+
+Toggling checkboxes should now persist in the DB even if you refresh the web browser.
+
+Your app should look like this:
+
+