diff --git a/.github/workflows/check-code-style.yml b/.github/workflows/check-code-style.yml index 8b24944b3f..6ceedaee3d 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: 22.x + node-version: 24.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 a20bb011ca..803264da09 100644 --- a/.github/workflows/check-syntax.yml +++ b/.github/workflows/check-syntax.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22.x + node-version: 24.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/e2e-tests.yml b/.github/workflows/e2e-tests.yml index de695c209d..6a41a1e4b3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -32,7 +32,8 @@ jobs: - Babel - Blaze - Coffeescript - - Library + - Full Skeleton + - Tailwind Skeleton - Monorepo - React - R.Router @@ -71,7 +72,7 @@ jobs: run: npm install - name: Install test deps - run: npm run install:modern + run: npm run install:e2e - name: Prepare Meteor run: ./meteor --get-ready @@ -83,4 +84,4 @@ jobs: retry_on: error timeout_minutes: 15 retry_wait_seconds: 90 - command: npm run test:modern -- -t="${{ matrix.category }}" + command: npm run test:e2e -- -t="${{ matrix.category }}" diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml index bd3d801966..432f05f719 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: 22.x + node-version: 24.x - name: Build the Guide run: npm ci && npm run build - name: Deploy to Netlify for preview diff --git a/.github/workflows/npm-eslint-plugin-meteor.yml b/.github/workflows/npm-eslint-plugin-meteor.yml index ff51a2e847..357143a8cb 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: 22.x + node-version: 24.x cache: npm - run: npm ci - run: npm test diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml new file mode 100644 index 0000000000..51ced76ef7 --- /dev/null +++ b/.github/workflows/test-packages.yml @@ -0,0 +1,60 @@ + +name: Test Packages + +on: + pull_request: + +jobs: + test-packages: + strategy: + fail-fast: false + matrix: + reactivity_order: + - 'changeStreams,polling' + - 'oplog,polling' + runs-on: ubuntu-22.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.reactivity_order }} + cancel-in-progress: true + timeout-minutes: 90 + env: + CXX: g++-12 + phantom: false + PUPPETEER_DOWNLOAD_PATH: /home/runner/.npm/chromium + TEST_PACKAGES_EXCLUDE: stylus + METEOR_MODERN: true + NODE_ENV: CI + METEOR_REACTIVITY_ORDER: ${{ matrix.reactivity_order }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.17.0 + + - name: Restore caches + uses: actions/cache@v4 + with: + path: | + ~/.npm + .meteor + .babel-cache + dev_bundle + /home/runner/.npm/chromium + key: ${{ runner.os }}-node-22.17-${{ hashFiles('meteor', '**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-22.17- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y g++-12 libnss3 + + - name: Install npm dependencies + run: npm install + + - name: Run test-in-console suite + run: ./packages/test-in-console/run.sh diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..a98248e760 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,36 @@ +name: Unit Tests + +on: + pull_request: + paths: + - 'tools/**' + - 'scripts/**' + - 'package.json' + - '.github/workflows/unit-tests.yml' + push: + branches: + - devel + +concurrency: + group: unit-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Install unit test deps + run: npm run install:unit + + - name: Run unit tests + run: npm run test:unit diff --git a/.github/workflows/windows-selftest.yml b/.github/workflows/windows-selftest.yml index 04b3abc05d..8a58399f09 100644 --- a/.github/workflows/windows-selftest.yml +++ b/.github/workflows/windows-selftest.yml @@ -47,7 +47,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 22.x + node-version: 24.x - name: Cache dependencies id: meteor-cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3e63726f4c..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: node_js -os: linux -dist: jammy -sudo: required -services: xvfb -node_js: - - "22.17.0" -cache: - directories: - - ".meteor" - - ".babel-cache" -script: - - travis_retry ./packages/test-in-console/run.sh -env: - global: - - CXX=g++-12 - - phantom=false - - PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium - - TEST_PACKAGES_EXCLUDE=stylus - - METEOR_MODERN=true -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-12 - - libnss3 - -before_install: - - cat /etc/apt/sources.list - - python3 --version - - echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list - - sudo apt-get update - - sudo apt-get install -y libnss3 diff --git a/AGENTS.md b/AGENTS.md index 9cac088bee..22f14547ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,8 @@ Full-stack JavaScript platform for modern web and mobile applications. ./meteor create my-app # Create app ./meteor self-test # CLI tests ./meteor test-packages ./packages/ # Package tests -npm run test:modern # E2E tests (Jest + Playwright) +npm run test:unit # Unit tests (Jest) +npm run test:e2e # E2E tests (Jest + Playwright) ``` ## Structure diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a34dd51841..58eda23621 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -115,79 +115,98 @@ For the rest, try looking nearby for a `README.md`. For example, [`isobuild`](t ## Tests -### Test against the local meteor copy +When running tests that use `./meteor`, be sure to run them against the checked-out copy of Meteor instead of the globally-installed version. This ensures tests run against your local development version. -When running any tests, be sure to run them against the checked-out copy of Meteor instead of -the globally-installed version. This means ensuring that the command is `path-to-meteor-checkout/meteor` and not just `meteor`. +The repository has four test layers, each covering a different scope: -This is important so that tests are run against your local development version and not the stable (installed) Meteor release. +| Command | Layer | Scope | +|---------|-------|-------| +| `npm run test:unit` | **Unit** (Jest) | Pure logic in `tools/`, `scripts/`, and helpers: fast, no Meteor runtime needed | +| `npm run test:e2e` | **E2E** (Jest + Playwright) | Bundler integration and skeleton apps: creates real Meteor projects, launches a browser | +| `./meteor self-test` | **Self-test** (custom) | Meteor CLI tool itself, spawns sandboxed Meteor processes to verify commands end-to-end | +| `./meteor test-packages` | **Package** (TinyTest) | Atmosphere packages in `packages/`, runs inside a Meteor app with the full reactive runtime | -### Running tests on Meteor core +### Unit tests (Jest) -When you are working with code in the core Meteor packages, you will want to make sure you run the -full test-suite (including the tests you added) to ensure you haven't broken anything in Meteor. The -`test-packages` command will do just that for you: +Unit tests cover pure helpers, scripts, and tool logic that does not require the Meteor runtime. They use [Jest](https://jestjs.io/) configured in `tools/unit-tests/`, targeting `tools/**/*.test.js` and `scripts/**/*.test.js`. - ./meteor test-packages +```sh +# Install dependencies (first time) +npm run install:unit -Exactly in the same way that [`test-packages` works in standalone Meteor apps](https://guide.meteor.com/writing-atmosphere-packages.html#testing), the `test-packages` command will start up a Meteor app with [TinyTest](./packages/tinytest/README.md). To view the results, just connect to `http://localhost:3000`. +# Run all unit tests +npm run test:unit -If you want to see results in the console you can use: +# Run a specific test file +npm run test:unit -- tools/path/to/file.test.js - PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh +# Run tests matching a name pattern +npm run test:unit -- -t "my test name" +``` -> [PUPPETEER_DOWNLOAD_PATH](https://github.com/dfernandez79/puppeteer/blob/main/README.md#q-chromium-gets-downloaded-on-every-npm-ci-run-how-can-i-cache-the-download) is optional but this is useful to skip Downloading Chromium on every run +Place test files next to the module they test using the `*.test.js` naming convention. Jest will pick them up automatically. -> We run our tests on Travis like above. +### E2E tests (Jest + Playwright) -#### Running specific tests +End-to-end tests in `tools/modern-tests/` validate that Meteor skeletons and bundler integrations work correctly. They create real Meteor apps, start dev servers, and assert behavior in a headless Chromium browser. -Specific package tests can be run by passing a `` or `` to the `test-packages` command. For example, to run `mongo` tests, it's possible to run: +```sh +# Install dependencies (first time) +npm run install:e2e - ./meteor test-packages mongo +# Run all E2E tests +npm run test:e2e -For more fine-grained control, if you're interested in running only the specific tests that relate to the functionality you're working on, you can filter individual tests by using the `TINYTEST_FILTER` environment variable (which supports regex's). For example, to run only the package tests that verify `new Mongo.Collection` behavior, try: +# Run a specific suite +npm run test:e2e -- -t="React" +``` - TINYTEST_FILTER="collection - call new Mongo.Collection" ./meteor test-packages +Each test has a corresponding app fixture in `tools/modern-tests/apps/`. See that directory for examples when adding new E2E tests. -You can also provide the same filters for `./packages/test-in-console/run.sh` explained above. +### Self-tests (Meteor tool) -### Running Meteor Tool self-tests +The Meteor CLI has its own "self-test" framework that spawns sandboxed Meteor processes. It tests commands like `create`, `build`, `deploy`, and `publish`. -While TinyTest and the `test-packages` command can be used to test internal Meteor packages, they cannot be used to test the Meteor Tool itself. The Meteor Tool is a node app that uses a home-grown "self test" system. +```sh +# List all self-tests +./meteor self-test --list -#### Listing available tests +# Run all self-tests +./meteor self-test -To see a list of tests included in the self-test system, use the `--list` option: +# Run tests matching a regex +./meteor self-test "^[a-b]" - ./meteor self-test --list +# Exclude tests matching a regex +./meteor self-test --exclude "^[a-b]" -#### Running specific tests +# Skip retries during development +./meteor self-test --retries 0 +``` -The self-test commands support a regular-expression syntax in order to specific/search for specific tests. For example, to search for tests starting with `a` or `b`, it's possible to run: +### Package tests (TinyTest) - ./meteor self-test "^[a-b]" --list +When working with core Atmosphere packages, use `test-packages` to run their tests via [TinyTest](./packages/tinytest/README.md). This starts a Meteor app, view results at `http://localhost:3000`. -Simply remove the `--list` flag to actually run the matching tests. +```sh +# Test all packages +./meteor test-packages -#### Excluding specific tests +# Test a specific package +./meteor test-packages mongo -In a similar way to the method of specifying which tests TO run, there is a way to specify which tests should NOT run. Again, using regular-expressions, this command will NOT list any tests which start with `a` or `b`: +# Filter by test name (supports regex), using --filter or -f +./meteor test-packages --filter "collection - call new Mongo.Collection" - ./meteor self-test --exclude "^[a-b]" --list +# Equivalent using the environment variable +TINYTEST_FILTER="collection - call new Mongo.Collection" ./meteor test-packages +``` -Simply remove the `--list` flag to actually run the matching tests. +For headless console output: -#### Avoiding retries - -On CI we want to retry the tests to avoid false failures but in development can take some time if you retry every time a test is failing. So to avoid retries use: - - ./meteor self-test --retries 0 - - -#### More reading - -For even more details on how to run Meteor Tool "self tests", please refer to the [Testing section of the Meteor Tool README](https://github.com/meteor/meteor/blob/master/tools/README.md#testing). +```sh +PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh +``` ### Continuous integration diff --git a/docs/source/api/methods.md b/docs/source/api/methods.md index a476e9dbc6..1eccabfe99 100644 --- a/docs/source/api/methods.md +++ b/docs/source/api/methods.md @@ -241,8 +241,8 @@ Here's example of defining a rule and adding it into the `DDPRateLimiter`: ```js // Define a rule that matches login attempts by non-admin users. const loginRule = { - userId(userId) { - const user = Meteor.users.findOne(userId); + async userId(userId) { + const user = await Meteor.users.findOneAsync(userId); return user && user.type !== 'admin'; }, @@ -266,8 +266,8 @@ default English error message. Here is an example with a custom error message: ```js const setupGoogleAuthenticatorRule = { - userId(userId) { - const user = Meteor.users.findOne(userId); + async userId(userId) { + const user = await Meteor.users.findOneAsync(userId); return user; }, type: 'method', diff --git a/meteor b/meteor index d1412f0105..5294887e46 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.22.0.3 +BUNDLE_VERSION=24.14.0.4 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/npm-packages/eslint-plugin-meteor/scripts/build-dev-bundle-common.sh b/npm-packages/eslint-plugin-meteor/scripts/build-dev-bundle-common.sh index 65d4c79cdc..b419b961d6 100644 --- a/npm-packages/eslint-plugin-meteor/scripts/build-dev-bundle-common.sh +++ b/npm-packages/eslint-plugin-meteor/scripts/build-dev-bundle-common.sh @@ -5,10 +5,10 @@ set -u UNAME=$(uname) ARCH=$(uname -m) -NODE_VERSION=14.21.3 +NODE_VERSION=24.14.0 MONGO_VERSION_64BIT=6.0.3 MONGO_VERSION_32BIT=3.2.22 -NPM_VERSION=6.14.18 +NPM_VERSION=11.10.1 if [ "$UNAME" == "Linux" ] ; then if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then 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 432d64b0fb..5dfef31c62 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,7 +10,7 @@ var packageJson = { dependencies: { // Explicit dependency because we are replacing it with a bundled version // and we want to make sure there are no dependencies on a higher version - npm: "10.9.4", + npm: "11.10.1", pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8", "node-gyp": "9.4.0", "@mapbox/node-pre-gyp": "1.0.11", diff --git a/npm-packages/meteor-installer/package.json b/npm-packages/meteor-installer/package.json index 61ebd3389c..2613b11577 100644 --- a/npm-packages/meteor-installer/package.json +++ b/npm-packages/meteor-installer/package.json @@ -1,6 +1,6 @@ { "name": "meteor", - "version": "3.4.0", + "version": "3.5.0-beta", "description": "Install Meteor", "main": "install.js", "scripts": { diff --git a/package-lock.json b/package-lock.json index 163d003dc1..3c6e5d97c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,27 +9,27 @@ "version": "0.0.1", "license": "MIT", "devDependencies": { - "@babel/core": "^7.21.3", - "@babel/eslint-parser": "^7.21.3", - "@babel/eslint-plugin": "^7.19.1", - "@babel/preset-react": "^7.18.6", + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/eslint-plugin": "^7.27.1", + "@babel/preset-react": "^7.28.5", "@types/lodash.isempty": "^4.4.9", - "@types/node": "^18.16.18", + "@types/node": "^24.10.13", "@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", - "eslint-config-prettier": "^8.8.0", - "eslint-config-vazco": "^7.1.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-config-vazco": "^7.4.0", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "prettier": "^2.8.8", - "typescript": "^5.4.5" + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.2", + "prettier": "^3.8.1", + "typescript": "^5.9.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -41,27 +41,14 @@ "node": ">=0.10.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -70,9 +57,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -80,22 +67,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -111,12 +98,13 @@ } }, "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -139,17 +127,12 @@ "node": ">=6" } }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@babel/eslint-parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz", - "integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", + "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", "dev": true, + "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -160,14 +143,15 @@ }, "peerDependencies": { "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0" + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/@babel/eslint-plugin": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.23.5.tgz", - "integrity": "sha512-03+E/58Hoo/ui69gR+beFdGpplpoVK0BSIdke2iw4/Bz7eGN0ssRenNlnU4nmbkowNQOPCStKSwFr8H6DiY49g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.27.1.tgz", + "integrity": "sha512-vOG/EipZbIAcREK6XI4JRO3B3uZr70/KIhsrNLO9RXcgLMaW0sTsBpNeTpQUyelB0HsbWd45NIsuTgD3mqr/Og==", "dev": true, + "license": "MIT", "dependencies": { "eslint-rule-composer": "^0.3.0" }, @@ -176,18 +160,18 @@ }, "peerDependencies": { "@babel/eslint-parser": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0" + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -210,13 +194,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -226,21 +210,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -252,29 +221,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -284,9 +253,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -304,9 +273,9 @@ } }, "node_modules/@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==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -324,27 +293,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -354,12 +323,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -369,12 +339,13 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", - "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -384,16 +355,17 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", - "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/types": "^7.23.4" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -403,12 +375,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.22.5" + "@babel/plugin-transform-react-jsx": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -418,13 +391,14 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", - "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -434,17 +408,18 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", - "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-react-display-name": "^7.23.3", - "@babel/plugin-transform-react-jsx": "^7.22.15", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.23.3" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -453,46 +428,34 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -500,9 +463,9 @@ } }, "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -518,14 +481,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -641,23 +604,25 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -665,12 +630,13 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -681,12 +647,6 @@ } } }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -701,11 +661,12 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", @@ -718,6 +679,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -802,11 +774,32 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -830,19 +823,21 @@ } }, "node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "dev": true + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/sockjs": { "version": "0.3.36", @@ -860,32 +855,34 @@ "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", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -950,25 +947,27 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -977,12 +976,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -993,23 +993,18 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1017,25 +1012,26 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1044,12 +1040,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1060,19 +1057,14 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1080,21 +1072,23 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1106,13 +1100,24 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "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": { - "ms": "2.1.2" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1123,32 +1128,28 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, + "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1156,71 +1157,38 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1228,23 +1196,18 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1256,6 +1219,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1264,10 +1228,11 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, "node_modules/acorn": { "version": "8.11.2", @@ -1338,22 +1303,24 @@ "dev": true }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -1363,17 +1330,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1387,6 +1357,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1412,17 +1383,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1432,15 +1405,16 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1450,15 +1424,16 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1467,45 +1442,37 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -1520,11 +1487,22 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/available-typed-arrays": { "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==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -1536,21 +1514,23 @@ } }, "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", "dev": true, + "license": "MPL-2.0", "engines": { "node": ">=4" } }, "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/balanced-match": { @@ -1559,6 +1539,19 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1583,9 +1576,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1603,10 +1596,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1616,16 +1610,16 @@ } }, "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==", "dev": 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" @@ -1648,6 +1642,23 @@ "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1658,9 +1669,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -1728,10 +1739,11 @@ "dev": true }, "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==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1748,14 +1760,15 @@ "dev": true }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1765,29 +1778,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -1847,20 +1862,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -1896,64 +1903,73 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.197", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", - "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", + "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -1982,25 +1998,28 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" @@ -2036,23 +2055,28 @@ } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2081,16 +2105,18 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -2136,10 +2162,11 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -2148,31 +2175,32 @@ } }, "node_modules/eslint-config-vazco": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/eslint-config-vazco/-/eslint-config-vazco-7.3.0.tgz", - "integrity": "sha512-OK8xVmrSxkd+Jl2OAvhwVquAMP5Xanz4mMxrBvPdtv2Ld25xI9CsbPNsFoRHOoBsu5sS5EYUdvpRy7tFk2R6Dg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/eslint-config-vazco/-/eslint-config-vazco-7.4.0.tgz", + "integrity": "sha512-/AW+KKK11GvqKaUe/8zZBjG1iNyhDMmrsr512kCapPLXnYRsyP/KY/95OnWaECKkFrwKTbuhMLqwvTdrsMeX1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8", "npm": ">=6" }, "peerDependencies": { - "@babel/core": "^7.22.5", - "@babel/eslint-parser": "^7.22.5", - "@babel/eslint-plugin": "^7.22.5", - "@babel/preset-react": "^7.22.5", - "@typescript-eslint/eslint-plugin": "^5.59.11", - "@typescript-eslint/parser": "^5.59.11", - "eslint": "^8.43.0", - "eslint-config-prettier": "^8.8.0", + "@babel/core": "^7.23.6", + "@babel/eslint-parser": "^7.23.3", + "@babel/eslint-plugin": "^7.23.5", + "@babel/preset-react": "^7.23.3", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.2", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "prettier": "^2.8.8", - "typescript": "^5.1.3" + "prettier": "^3.1.1", + "typescript": "^5.3.3" } }, "node_modules/eslint-import-resolver-node": { @@ -2187,10 +2215,11 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -2223,34 +2252,37 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { @@ -2265,43 +2297,34 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { @@ -2311,63 +2334,75 @@ "dev": true }, "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, + "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" }, "engines": { - "node": ">=12.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, "eslint-config-prettier": { "optional": true } } }, "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2413,15 +2448,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-rule-composer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", @@ -2696,19 +2722,21 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -2781,12 +2809,19 @@ "dev": true }, "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==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/fs.realpath": { @@ -2805,15 +2840,18 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -2880,14 +2918,15 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -2922,6 +2961,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2950,6 +2990,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -2985,10 +3026,14 @@ "dev": true }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3016,10 +3061,14 @@ } }, "node_modules/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==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3119,27 +3168,30 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3149,12 +3201,17 @@ } }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3164,25 +3221,30 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3196,6 +3258,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3220,11 +3283,14 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -3235,12 +3301,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3259,24 +3327,32 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "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==", "dev": 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" @@ -3302,6 +3378,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3314,6 +3391,7 @@ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3332,12 +3410,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3356,13 +3436,16 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "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==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3376,6 +3459,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3384,12 +3468,13 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3399,12 +3484,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3414,12 +3501,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3429,12 +3519,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -3448,6 +3539,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3456,25 +3548,30 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3487,7 +3584,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -3496,16 +3594,21 @@ "dev": true }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/js-tokens": { @@ -3643,6 +3746,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3658,6 +3771,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -3709,16 +3823,10 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -3732,10 +3840,14 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3750,14 +3862,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -3768,14 +3883,16 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -3813,30 +3930,15 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -3873,6 +3975,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3939,6 +4059,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3955,6 +4076,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -3963,10 +4085,11 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3981,26 +4104,27 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, + "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" }, @@ -4055,18 +4179,20 @@ "dev": true }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -4075,22 +4201,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true - }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4100,13 +4223,13 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -4179,14 +4302,16 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -4196,15 +4321,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -4254,6 +4397,21 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4276,15 +4434,73 @@ } }, "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==", "dev": 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" + }, + "funding": { + "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==", + "dev": 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==", + "dev": 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==", + "dev": 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" @@ -4298,28 +4514,60 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4328,16 +4576,31 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4347,15 +4610,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4435,6 +4703,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4454,6 +4738,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -4466,27 +4763,6 @@ "strip-bom": "^3.0.0" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4500,30 +4776,32 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4533,17 +4811,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -4553,17 +4833,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -4573,10 +4854,11 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4586,30 +4868,35 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -4662,39 +4949,45 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4708,6 +5001,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -4722,15 +5016,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "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": { @@ -4746,6 +5043,13 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 849ccda288..64017f7660 100644 --- a/package.json +++ b/package.json @@ -12,32 +12,34 @@ }, "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", + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/eslint-plugin": "^7.27.1", + "@babel/preset-react": "^7.28.5", "@types/lodash.isempty": "^4.4.9", - "@types/node": "^18.16.18", + "@types/node": "^24.10.13", "@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", - "eslint-config-prettier": "^8.8.0", - "eslint-config-vazco": "^7.1.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-config-vazco": "^7.4.0", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "prettier": "^2.8.8", - "typescript": "^5.4.5" + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.2", + "prettier": "^3.8.1", + "typescript": "^5.9.3" }, "scripts": { - "install:modern": "cd tools/modern-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell", + "install:unit": "cd tools/unit-tests && npm install", + "test:unit": "cd tools/unit-tests && npm test", "test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js", - "test:modern": "cd tools/modern-tests && npm test -- " + "install:e2e": "cd tools/modern-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell", + "test:e2e": "cd tools/modern-tests && npm test -- " }, "jshintConfig": { "esversion": 11 diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 73e26ae913..81be281fb6 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -101,7 +101,10 @@ export namespace Accounts { collection?: string | undefined; loginTokenExpirationHours?: number | undefined; tokenSequenceLength?: number | undefined; - clientStorage?: 'session' | 'local'; + // Storage strategy for client tokens: 'local' (persist), 'session' (per-tab), or 'none' (in-memory only) + clientStorage?: 'session' | 'local' | 'none'; + // Enable hybrid HttpOnly cookie + short-lived token flow + useHttpOnlyCookies?: boolean | undefined; }): void; function onLogin( diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index f2535b6dc6..6638e42c4e 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -9,7 +9,8 @@ import {AccountsCommon} from "./accounts_common.js"; * @param {Object} options an object with fields: * @param {Object} options.connection Optional DDP connection to reuse. * @param {String} options.ddpUrl Optional URL for creating a new DDP connection. - * @param {'session' | 'local'} options.clientStorage Optional Define what kind of storage you want for credentials on the client. Default is 'local' to use `localStorage`. Set to 'session' to use session storage. + * @param {'session' | 'local' | 'none'} options.clientStorage Optional Define what kind of storage you want for credentials on the client. Default is 'local' to use `localStorage`. Set to 'session' to use session storage. Use 'none' to avoid persisting tokens. + * @param {Boolean} options.useHttpOnlyCookies Optional Enable HttpOnly cookie flow for auth resume. When enabled, the client will try to refresh a login token from a server HttpOnly cookie during startup, and will sync the cookie after logins/logouts. */ export class AccountsClient extends AccountsCommon { constructor(options) { @@ -29,9 +30,15 @@ export class AccountsClient extends AccountsCommon { this.initStorageLocation(); + // Read HttpOnly cookie setting from options or public settings + this._useHttpOnlyCookies = !!(options?.useHttpOnlyCookies || Meteor.settings?.public?.packages?.accounts?.useHttpOnlyCookies); + // Defined in localstorage_token.js. this._initLocalStorage(); + // Try to resume via HttpOnly cookie if enabled + this._initHttpOnlyCookieLogin(); + // This is for .registerClientLoginFunction & .callLoginFunction. this._loginFuncs = {}; @@ -42,13 +49,29 @@ export class AccountsClient extends AccountsCommon { initStorageLocation(options) { // Determine whether to use local or session storage to storage credentials and anything else. - this.storageLocation = (options?.clientStorage === 'session' || Meteor.settings?.public?.packages?.accounts?.clientStorage === 'session') ? window.sessionStorage : Meteor._localStorage; + const desired = options?.clientStorage || Meteor.settings?.public?.packages?.accounts?.clientStorage; + if (desired === 'session') { + this.storageLocation = window.sessionStorage; + } else if (desired === 'none') { + // In-memory, non-persistent storage shim + const mem = new Map(); + this.storageLocation = { + getItem: (k) => mem.get(k) || null, + setItem: (k, v) => { mem.set(k, String(v)); }, + removeItem: (k) => { mem.delete(k); }, + }; + } else { + this.storageLocation = Meteor._localStorage; + } } config(options) { super.config(options); this.initStorageLocation(options); + if (Object.prototype.hasOwnProperty.call(options || {}, 'useHttpOnlyCookies')) { + this._useHttpOnlyCookies = !!options.useHttpOnlyCookies; + } } /// @@ -142,11 +165,11 @@ export class AccountsClient extends AccountsCommon { this._loggingOut.set(false); this._loginCallbacksCalled = false; this.makeClientLoggedOut(); - callback && callback(); + callback?.(); }) .catch((e) => { this._loggingOut.set(false); - callback && callback(e); + callback?.(e); }); } @@ -166,11 +189,11 @@ export class AccountsClient extends AccountsCommon { this._loggingOut.set(false); this._loginCallbacksCalled = false; this.makeClientLoggedOut(); - callback && callback(); + callback?.(); }) .catch((e) => { this._loggingOut.set(false); - callback && callback(e); + callback?.(e); }); } @@ -215,7 +238,7 @@ export class AccountsClient extends AccountsCommon { 'removeOtherTokens', [], { wait: true }, - err => callback && callback(err) + err => callback?.(err) ); } @@ -441,11 +464,19 @@ export class AccountsClient extends AccountsCommon { this._unstoreLoginToken(); this.connection.setUserId(null); this._reconnectStopper && this._reconnectStopper.stop(); + // Clear HttpOnly cookie if enabled + if (this._useHttpOnlyCookies) { + this._clearHttpOnlyCookie(); + } } makeClientLoggedIn(userId, token, tokenExpires) { this._storeLoginToken(userId, token, tokenExpires); this.connection.setUserId(userId); + // Sync HttpOnly cookie if enabled + if (this._useHttpOnlyCookies) { + this._setHttpOnlyCookie(token, tokenExpires); + } } /// @@ -532,6 +563,29 @@ export class AccountsClient extends AccountsCommon { }); }; + // Attempt startup login using an HttpOnly cookie by requesting a + // short-lived resume token from the server. + async loginWithCookie() { + try { + const res = await fetch('/_accounts/cookie/refresh', { + method: 'GET', + credentials: 'include', + headers: { 'Accept': 'application/json' }, + }); + if (!res.ok) return; + const body = await res.json(); + if (body && body.token) { + this.loginWithToken(body.token, (err) => { + if (err) { + this.makeClientLoggedOut(); + } + }); + } + } catch (_e) { + // ignore + } + } + // Semi-internal API. Call this function to re-enable auto login after // if it was disabled at startup. _enableAutoLogin() { @@ -573,6 +627,26 @@ export class AccountsClient extends AccountsCommon { this._lastLoginTokenWhenPolled = null; }; + async _setHttpOnlyCookie(token, tokenExpires) { + try { + await fetch('/_accounts/cookie/set', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, tokenExpires }), + }); + } catch (_e) {} + } + + async _clearHttpOnlyCookie() { + try { + await fetch('/_accounts/cookie/clear', { + method: 'POST', + credentials: 'include' + }); + } catch (_e) {} + } + // This is private, but it is exported for now because it is used by a // test in accounts-password. _storedLoginToken() { @@ -667,6 +741,15 @@ export class AccountsClient extends AccountsCommon { }, 3000); }; + _initHttpOnlyCookieLogin() { + if (!this._useHttpOnlyCookies) return; + // Only attempt cookie resume if we didn't find a local token + const hasLocalToken = !!this._storedLoginToken(); + if (!hasLocalToken) { + this.loginWithCookie(); + } + } + _pollStoredLoginToken() { if (! this._autoLoginEnabled) { return; @@ -710,6 +793,22 @@ export class AccountsClient extends AccountsCommon { attemptToMatchHash(this, this.savedHash, defaultSuccessHandler); }; + /** + * @summary Shared implementation for registering account link callbacks. + * @param {String} type The callback type (e.g. 'reset-password', 'verify-email', 'enroll-account'). + * @param {Function} callback The function to call when the link is clicked. + * @locus Client + */ + _registerLinkCallback(type, callback) { + if (this._accountsCallbacks[type]) { + Meteor._debug( + `Accounts callback for "${type}" was registered more than once. ` + + "Only one callback added will be executed." + ); + } + this._accountsCallbacks[type] = callback; + }; + /** * @summary Register a function to call when a reset password link is clicked * in an email sent by @@ -728,12 +827,7 @@ export class AccountsClient extends AccountsCommon { * @locus Client */ onResetPasswordLink(callback) { - if (this._accountsCallbacks["reset-password"]) { - Meteor._debug("Accounts.onResetPasswordLink was called more than once. " + - "Only one callback added will be executed."); - } - - this._accountsCallbacks["reset-password"] = callback; + this._registerLinkCallback("reset-password", callback); }; /** @@ -755,12 +849,7 @@ export class AccountsClient extends AccountsCommon { * @locus Client */ onEmailVerificationLink(callback) { - if (this._accountsCallbacks["verify-email"]) { - Meteor._debug("Accounts.onEmailVerificationLink was called more than once. " + - "Only one callback added will be executed."); - } - - this._accountsCallbacks["verify-email"] = callback; + this._registerLinkCallback("verify-email", callback); }; /** @@ -782,12 +871,7 @@ export class AccountsClient extends AccountsCommon { * @locus Client */ onEnrollmentLink(callback) { - if (this._accountsCallbacks["enroll-account"]) { - Meteor._debug("Accounts.onEnrollmentLink was called more than once. " + - "Only one callback added will be executed."); - } - - this._accountsCallbacks["enroll-account"] = callback; + this._registerLinkCallback("enroll-account", callback); }; } diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 48234064d8..17ac1264c4 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -24,6 +24,7 @@ const VALID_CONFIG_KEYS = [ 'loginTokenExpirationHours', 'tokenSequenceLength', 'clientStorage', + 'useHttpOnlyCookies', 'ddpUrl', 'connection', ]; @@ -260,7 +261,7 @@ export class AccountsCommon { // We need to validate the oauthSecretKey option at the time // Accounts.config is called. We also deliberately don't store the // oauthSecretKey in Accounts._options. - if (Object.prototype.hasOwnProperty.call(options, 'oauthSecretKey')) { + if (Object.hasOwn(options, 'oauthSecretKey')) { if (Meteor.isClient) { throw new Error( 'The oauthSecretKey option may only be specified on the server' diff --git a/packages/accounts-base/accounts_cookie_client_tests.js b/packages/accounts-base/accounts_cookie_client_tests.js new file mode 100644 index 0000000000..18110ae054 --- /dev/null +++ b/packages/accounts-base/accounts_cookie_client_tests.js @@ -0,0 +1,64 @@ +// Client-side tests for HttpOnly cookie auth flow +// Ensures token is not accessible via JS (HttpOnly) and resume works. + +if (Meteor.isClient) { + Tinytest.addAsync('accounts cookie - login with password sets HttpOnly cookie and not readable via document.cookie', (test, done) => { + // Enable cookie auth + Accounts.config({ useHttpOnlyCookies: true }); + Accounts._isolateLoginTokenForTest(); + + const username = `u_${Random.id()}`; + const password = `p_${Random.id()}`; + + Accounts.createUser({ username, password }, (err) => { + test.isUndefined(err, 'error creating user'); + // After login, a token is stored in storageLocation, but cookie should be HttpOnly. + // We cannot directly read HttpOnly cookie, so assert it does NOT appear in document.cookie substring + const token = Accounts._storedLoginToken(); + test.isTrue(!!token, 'token stored locally'); + const cookieStr = document.cookie || ''; + test.isFalse(/meteor_login_token=/.test(cookieStr), 'HttpOnly cookie not exposed to JS'); + Meteor.logout(() => done()); + }); + }); + + + Tinytest.addAsync('accounts cookie - clearing cookie on logout makes refresh return 204', async (test, done) => { + Accounts.config({ useHttpOnlyCookies: true }); + Accounts._isolateLoginTokenForTest(); + const username = `u3_${Random.id()}`; + const password = `p3_${Random.id()}`; + await new Promise((resolve, reject) => Accounts.createUser({ username, password }, (e) => e ? reject(e) : resolve())); + test.isTrue(!!Meteor.userId()); + + // Poll refresh until cookie is set (200), because _setHttpOnlyCookie is async and may not + // have completed yet — a stale cookie from a previous test could also cause 401. will be fixed after https://github.com/meteor/meteor/pull/14069 + let r; + const loginStart = Date.now(); + while (true) { + r = await fetch('/_accounts/cookie/refresh', { credentials: 'include' }); + if (r.status === 200 || Date.now() - loginStart > 4000) break; + await new Promise(res => setTimeout(res, 100)); + } + test.equal(r.status, 200, 'cookie set after login'); + + await new Promise(res => Meteor.logout(() => res())); + test.isFalse(!!Meteor.userId()); + + // Poll refresh until 204 or timeout because _clearHttpOnlyCookie is async + const start = Date.now(); + const check = async () => { + const resp = await fetch('/_accounts/cookie/refresh', { credentials: 'include' }); + if (resp.status === 204) { + test.equal(resp.status, 204, 'cookie cleared after logout'); + done(); + } else if (Date.now() - start > 4000) { + test.fail(`Expected 204 after logout, got ${resp.status}`); + done(); + } else { + setTimeout(check, 100); + } + }; + check(); + }); +} diff --git a/packages/accounts-base/accounts_cookie_server_tests.js b/packages/accounts-base/accounts_cookie_server_tests.js new file mode 100644 index 0000000000..30eae269a3 --- /dev/null +++ b/packages/accounts-base/accounts_cookie_server_tests.js @@ -0,0 +1,197 @@ +if (Meteor.isServer) { + const COOKIE_NAME = 'meteor_login_token'; + const REFRESH_PATH = '/_accounts/cookie/refresh'; + const SET_PATH = '/_accounts/cookie/set'; + const CLEAR_PATH = '/_accounts/cookie/clear'; + + // Utility: simple HTTP request using Node's http/https depending on absoluteUrl + const request = async (method, path, { headers, body } = {}) => { + const url = Meteor.absoluteUrl(path.replace(/^\//,'')); + const { URL } = Npm.require('url'); + const u = new URL(url); + const opts = { + protocol: u.protocol, + hostname: u.hostname, + port: u.port, + path: u.pathname + (u.search||''), + method, + headers: headers || {} + }; + const httpLib = Npm.require(u.protocol === 'https:' ? 'https' : 'http'); + return new Promise((resolve, reject) => { + const req = httpLib.request(opts, (res) => { + let data = ''; + res.setEncoding('utf8'); + res.on('data', chunk => data += chunk); + res.on('end', () => { + let json; + try { json = data ? JSON.parse(data) : undefined; } catch (_e) {} + resolve({ status: res.statusCode, headers: res.headers, body: data, json }); + }); + }); + req.on('error', reject); + if (body) req.write(body); + req.end(); + }); + }; + + Tinytest.addAsync('accounts cookie - set endpoint sets HttpOnly cookie with flags', async (test, done) => { + // Create a user & token + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + await Accounts._insertLoginToken(userId, stamped); + + const res = await request('POST', SET_PATH, { + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: stamped.token }) + }); + test.equal(res.status, 200); + const setCookie = res.headers['set-cookie'] && res.headers['set-cookie'][0]; + test.isTrue(/meteor_login_token=/.test(setCookie), 'cookie name'); + test.isTrue(/HttpOnly/i.test(setCookie), 'HttpOnly flag'); + test.isTrue(/SameSite=Lax/i.test(setCookie), 'SameSite flag'); + // Secure might be absent in test (http) environment, accept either + done(); + }); + + Tinytest.addAsync('accounts cookie - refresh returns token & expiry when cookie valid', async (test, done) => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + await Accounts._insertLoginToken(userId, stamped); + + const setRes = await request('POST', SET_PATH, { + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: stamped.token }) + }); + const cookieHeader = setRes.headers['set-cookie'][0].split(';')[0]; + + const refreshRes = await request('GET', REFRESH_PATH, { headers: { 'Cookie': cookieHeader } }); + test.equal(refreshRes.status, 200); + test.equal(refreshRes.json.token, stamped.token); + test.isTrue(!!refreshRes.json.tokenExpires, 'tokenExpires present'); + done(); + }); + + Tinytest.addAsync('accounts cookie - refresh 204 when no cookie', async (test, done) => { + const res = await request('GET', REFRESH_PATH); + test.equal(res.status, 204); + done(); + }); + + Tinytest.addAsync('accounts cookie - refresh 401 for invalid token', async (test, done) => { + const fakeCookie = 'meteor_login_token=faketoken123'; + const res = await request('GET', REFRESH_PATH, { headers: { 'Cookie': fakeCookie } }); + test.equal(res.status, 401); + done(); + }); + + Tinytest.addAsync('accounts cookie - clear removes cookie (expires in past)', async (test, done) => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + await Accounts._insertLoginToken(userId, stamped); + const setRes = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: stamped.token }) }); + const cookieToSend = setRes.headers['set-cookie'][0].split(';')[0]; + + const clearRes = await request('POST', CLEAR_PATH, { headers: { 'Cookie': cookieToSend } }); + test.equal(clearRes.status, 200); + const cleared = clearRes.headers['set-cookie'][0]; + test.isTrue(/Expires=Thu, 01 Jan 1970|Expires=Wed, 31 Dec 1969/.test(cleared) || /1970 GMT/.test(cleared), 'expired date'); + done(); + }); + + Tinytest.addAsync('accounts cookie - refresh 204 body empty & no Set-Cookie', async (test, done) => { + const res = await request('GET', REFRESH_PATH); + test.equal(res.status, 204); + test.equal(res.body, ''); + test.isUndefined(res.headers['set-cookie']); + done(); + }); + + Tinytest.addAsync('accounts cookie - expired token yields 401 expired', async (test, done) => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + // Force past expiry by moving earlier than the configured lifetime + const lifetimeMs = Accounts._getTokenLifetimeMs ? Accounts._getTokenLifetimeMs() : (90 * 24 * 60 * 60 * 1000); + stamped.when = new Date(Date.now() - lifetimeMs - 1000); + await Accounts._insertLoginToken(userId, stamped); + const setRes = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: stamped.token }) }); + const cookieHeader = setRes.headers['set-cookie'][0].split(';')[0]; + const refreshRes = await request('GET', REFRESH_PATH, { headers: { 'Cookie': cookieHeader } }); + test.equal(refreshRes.status, 401); + test.equal(refreshRes.json && refreshRes.json.error, 'expired'); + done(); + }); + + Tinytest.addAsync('accounts cookie - revoked token yields 401 invalid_cookie', async (test, done) => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + await Accounts._insertLoginToken(userId, stamped); + const setRes = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: stamped.token }) }); + // Remove all login tokens simulating revocation + await Meteor.users.updateAsync(userId, { $set: { 'services.resume.loginTokens': [] } }); + const cookieHeader = setRes.headers['set-cookie'][0].split(';')[0]; + const refreshRes = await request('GET', REFRESH_PATH, { headers: { 'Cookie': cookieHeader } }); + test.equal(refreshRes.status, 401); + test.equal(refreshRes.json && refreshRes.json.error, 'invalid_cookie'); + done(); + }); + + Tinytest.addAsync('accounts cookie - clear idempotent (second clear still expired)', async (test, done) => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + await Accounts._insertLoginToken(userId, stamped); + const setRes = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: stamped.token }) }); + const cookie = setRes.headers['set-cookie'][0].split(';')[0]; + const first = await request('POST', CLEAR_PATH, { headers: { 'Cookie': cookie } }); + const second = await request('POST', CLEAR_PATH, { headers: { 'Cookie': cookie } }); + test.equal(first.status, 200); + test.equal(second.status, 200); + const hdr = second.headers['set-cookie'][0]; + test.isTrue(/Expires=Thu, 01 Jan 1970|Expires=Wed, 31 Dec 1969/.test(hdr) || /1970 GMT/.test(hdr)); + done(); + }); + + Tinytest.addAsync('accounts cookie - cookie path and no domain leakage', async (test, done) => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); + const stamped = Accounts._generateStampedLoginToken(); + await Accounts._insertLoginToken(userId, stamped); + const res = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: stamped.token }) }); + const setCookie = res.headers['set-cookie'][0]; + test.isTrue(/Path=\//.test(setCookie), 'Path=/ present'); + test.isFalse(/Domain=/.test(setCookie), 'no Domain attribute by default'); + done(); + }); + + Tinytest.addAsync('accounts cookie - invalid HTTP methods return 405', async (test, done) => { + const postRefresh = await request('POST', REFRESH_PATH); // should be GET + const getSet = await request('GET', SET_PATH); // should be POST + const getClear = await request('GET', CLEAR_PATH); // should be POST + test.equal(postRefresh.status, 405); + test.equal(getSet.status, 405); + test.equal(getClear.status, 405); + done(); + }); + + Tinytest.addAsync('accounts cookie - set missing token returns 400', async (test, done) => { + const res = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); + test.equal(res.status, 400); + test.equal(res.json && res.json.error, 'invalid_token'); + done(); + }); + + Tinytest.addAsync('accounts cookie - set invalid JSON returns 400', async (test, done) => { + // send malformed JSON: request helper will send body as-is + const res = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: '{bad' }); + test.equal(res.status, 400); + test.equal(res.json && res.json.error, 'invalid_token'); + done(); + }); + + Tinytest.addAsync('accounts cookie - set accepts long token (current behavior)', async (test, done) => { + const longToken = Array(5000).fill('a').join(''); + // Expect 200 with current implementation (no length enforcement) + const res = await request('POST', SET_PATH, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: longToken }) }); + test.equal(res.status, 200); + done(); + }); +} diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index e8a884c87b..7259025f67 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -7,7 +7,6 @@ import { } from './accounts_common.js'; import { URL } from 'meteor/url'; -const hasOwn = Object.prototype.hasOwnProperty; /** * @summary Constructor for the `Accounts` namespace on the server. @@ -801,7 +800,7 @@ export class AccountsServer extends AccountsCommon { if (Package["oauth-encryption"]) { const { OAuthEncryption } = Package["oauth-encryption"] - if (hasOwn.call(options, 'secret') && OAuthEncryption.keyIsLoaded()) + if (Object.hasOwn(options, 'secret') && OAuthEncryption.keyIsLoaded()) options.secret = OAuthEncryption.seal(options.secret); } @@ -904,10 +903,8 @@ export class AccountsServer extends AccountsCommon { // - forLoggedInUser {Array} Array of fields published to the logged-in user // - forOtherUsers {Array} Array of fields published to users that aren't logged in addAutopublishFields(opts) { - this._autopublishFields.loggedInUser.push.apply( - this._autopublishFields.loggedInUser, opts.forLoggedInUser); - this._autopublishFields.otherUsers.push.apply( - this._autopublishFields.otherUsers, opts.forOtherUsers); + this._autopublishFields.loggedInUser.push(...(opts.forLoggedInUser || [])); + this._autopublishFields.otherUsers.push(...(opts.forOtherUsers || [])); }; // Replaces the fields to be automatically @@ -1008,7 +1005,7 @@ export class AccountsServer extends AccountsCommon { // the observe that we started when we associated the connection with // this token. _removeTokenFromConnection(connectionId) { - if (hasOwn.call(this._userObservesForConnections, connectionId)) { + if (Object.hasOwn(this._userObservesForConnections, connectionId)) { const observe = this._userObservesForConnections[connectionId]; if (typeof observe === 'number') { // We're in the process of setting up an observe for this connection. We @@ -1214,13 +1211,13 @@ export class AccountsServer extends AccountsCommon { }; // @override from accounts_common.js - config(options) { + config(...args) { // Call the overridden implementation of the method. - const superResult = AccountsCommon.prototype.config.apply(this, arguments); + const superResult = AccountsCommon.prototype.config.apply(this, args); // If the user set loginExpirationInDays to null, then we need to clear the // timer that periodically expires tokens. - if (hasOwn.call(this._options, 'loginExpirationInDays') && + if (Object.hasOwn(this._options, 'loginExpirationInDays') && this._options.loginExpirationInDays === null && this.expireTokenInterval) { Meteor.clearInterval(this.expireTokenInterval); @@ -1377,7 +1374,7 @@ export class AccountsServer extends AccountsCommon { "Can't use updateOrCreateUserFromExternalService with internal service " + serviceName); } - if (!hasOwn.call(serviceData, 'id')) { + if (!Object.hasOwn(serviceData, 'id')) { throw new Error( `Service data for service ${serviceName} must include id`); } @@ -1527,7 +1524,7 @@ export class AccountsServer extends AccountsCommon { ) { // Some tests need the ability to add users with the same case insensitive // value, hence the _skipCaseInsensitiveChecksForTest check - const skipCheck = Object.prototype.hasOwnProperty.call( + const skipCheck = Object.hasOwn( this._skipCaseInsensitiveChecksForTest, fieldValue ); diff --git a/packages/accounts-base/client_tests.js b/packages/accounts-base/client_tests.js index 7997463867..8fff95854d 100644 --- a/packages/accounts-base/client_tests.js +++ b/packages/accounts-base/client_tests.js @@ -1,3 +1,4 @@ import "./accounts_url_tests.js"; import "./accounts_reconnect_tests.js"; import "./accounts_client_tests.js"; +import "./accounts_cookie_client_tests.js"; diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 931e4edb42..ca5b4c0a1b 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.2.0", + version: '3.3.0-beta350.7', }); Package.onUse((api) => { @@ -14,6 +14,8 @@ Package.onUse((api) => { api.use("callback-hook", ["client", "server"]); api.use("reactive-var", "client"); api.use("url", ["client", "server"]); + api.use("webapp", "server"); + api.use("routepolicy", "server"); // needed for getting the currently logged-in user and handling reconnects api.use("ddp", ["client", "server"]); diff --git a/packages/accounts-base/server_http_cookies.js b/packages/accounts-base/server_http_cookies.js new file mode 100644 index 0000000000..b2dcc06657 --- /dev/null +++ b/packages/accounts-base/server_http_cookies.js @@ -0,0 +1,160 @@ +import RoutePolicy from 'meteor/routepolicy'; +import { WebApp } from 'meteor/webapp'; + +// Declare these routes as network to avoid clashes with static assets +const COOKIE_BASE_PATH = '/_accounts/cookie'; +try { + RoutePolicy.declare(COOKIE_BASE_PATH + '/', 'network'); +} catch (_e) { + // ignore duplicate declarations +} + +const COOKIE_NAME = 'meteor_login_token'; + +function parseCookies(req) { + const header = req.headers && req.headers.cookie; + const cookies = {}; + if (!header) return cookies; + header.split(';').forEach((part) => { + const idx = part.indexOf('='); + if (idx === -1) return; + const k = part.slice(0, idx).trim(); + const v = decodeURIComponent(part.slice(idx + 1).trim()); + cookies[k] = v; + }); + return cookies; +} + +function isSecureRequest(req) { + // honor proxies that set x-forwarded-proto + const xfp = (req.headers['x-forwarded-proto'] || '').split(',')[0]; + return req.connection?.encrypted || xfp === 'https' || req.protocol === 'https'; +} + +function serializeCookie(name, value, options = {}) { + const parts = [`${name}=${encodeURIComponent(String(value))}`]; + if (options.maxAge != null) parts.push(`Max-Age=${Math.floor(options.maxAge)}`); + if (options.domain) parts.push(`Domain=${options.domain}`); + parts.push(`Path=${options.path || '/'}`); + if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`); + if (options.httpOnly !== false) parts.push('HttpOnly'); + if (options.secure) parts.push('Secure'); + if (options.sameSite) parts.push(`SameSite=${options.sameSite}`); + return parts.join('; '); +} + +async function readJson(req) { + return await new Promise((resolve) => { + let data = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => { data += chunk; }); + req.on('end', () => { + try { + resolve(JSON.parse(data || '{}')); + } catch (_e) { + resolve({}); + } + }); + }); +} + +function sendJson(res, code, body) { + const payload = JSON.stringify(body || {}); + res.writeHead(code, { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(payload) + }); + res.end(payload); +} + +// POST /_accounts/cookie/set +// Body: { token: string, tokenExpires?: string|number } +WebApp.handlers.use(async (req, res, next) => { + if (!req.url.startsWith(COOKIE_BASE_PATH + '/set')) return next(); + if (req.method !== 'POST') { + res.writeHead(405); + return res.end(); + } + const body = await readJson(req); + const token = body && body.token; + if (!token || typeof token !== 'string') { + return sendJson(res, 400, { error: 'invalid_token' }); + } + // Try to find a matching token to get expiration + let expires; + try { + const hashed = Accounts._hashLoginToken(token); + const user = await Accounts.users.findOneAsync({ + $or: [ + { 'services.resume.loginTokens.hashedToken': hashed }, + { 'services.resume.loginTokens.token': token }, + ] + }, { fields: { 'services.resume.loginTokens': 1 } }); + if (user) { + const t = (user.services.resume.loginTokens || []).find((st) => st.hashedToken === hashed || st.token === token); + if (t && t.when) { + expires = Accounts._tokenExpiration(t.when); + } + } + } catch (_e) {} + + const secure = isSecureRequest(req); + // Default cookie opts; prefer Lax to allow same-site navigations + const cookie = serializeCookie(COOKIE_NAME, token, { + path: '/', + httpOnly: true, + secure, + sameSite: 'Lax', + expires: expires instanceof Date ? expires : undefined, + }); + res.setHeader('Set-Cookie', cookie); + return sendJson(res, 200, { ok: true }); +}); + +// GET /_accounts/cookie/refresh +// Returns { token, tokenExpires, id } if cookie is valid +WebApp.handlers.use(async (req, res, next) => { + if (!req.url.startsWith(COOKIE_BASE_PATH + '/refresh')) return next(); + if (req.method !== 'GET') { + res.writeHead(405); + return res.end(); + } + const cookies = parseCookies(req); + const token = cookies[COOKIE_NAME]; + if (!token) return sendJson(res, 204, {}); + try { + const hashed = Accounts._hashLoginToken(token); + // Find user and token + const user = await Accounts.users.findOneAsync({ + $or: [ + { 'services.resume.loginTokens.hashedToken': hashed }, + { 'services.resume.loginTokens.token': token }, + ] + }, { fields: { 'services.resume.loginTokens': 1 } }); + if (!user) return sendJson(res, 401, { error: 'invalid_cookie' }); + const stamped = (user.services.resume.loginTokens || []).find((st) => st.hashedToken === hashed || st.token === token); + if (!stamped) return sendJson(res, 401, { error: 'invalid_cookie' }); + const tokenExpires = Accounts._tokenExpiration(stamped.when); + if (new Date() >= tokenExpires) return sendJson(res, 401, { error: 'expired' }); + return sendJson(res, 200, { token, tokenExpires }); + } catch (e) { + return sendJson(res, 500, { error: 'server_error' }); + } +}); + +// POST /_accounts/cookie/clear +WebApp.handlers.use(async (req, res, next) => { + if (!req.url.startsWith(COOKIE_BASE_PATH + '/clear')) return next(); + if (req.method !== 'POST') { + res.writeHead(405); + return res.end(); + } + const secure = isSecureRequest(req); + const expired = new Date(0); + const cookie = serializeCookie(COOKIE_NAME, '', { + path: '/', httpOnly: true, secure, sameSite: 'Lax', expires: expired + }); + res.setHeader('Set-Cookie', cookie); + return sendJson(res, 200, { ok: true }); +}); + diff --git a/packages/accounts-base/server_main.js b/packages/accounts-base/server_main.js index a9ed15c950..bcf25787d3 100644 --- a/packages/accounts-base/server_main.js +++ b/packages/accounts-base/server_main.js @@ -8,6 +8,9 @@ Accounts = new AccountsServer(Meteor.server, { ...Meteor.settings.packages?.acco // TODO[FIBERS]: I need TLA Accounts.init().then(); +// Register HttpOnly cookie endpoints and helpers +import './server_http_cookies.js'; + // 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-base/server_tests.js b/packages/accounts-base/server_tests.js index 71169a35af..1ab036d721 100644 --- a/packages/accounts-base/server_tests.js +++ b/packages/accounts-base/server_tests.js @@ -1,2 +1,3 @@ import "./accounts_tests.js"; import "./accounts_reconnect_tests.js"; +import "./accounts_cookie_server_tests.js"; diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index 6da4c3d030..142be87d80 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -5,7 +5,7 @@ Package.describe({ // 2.2.x in the future. The version was also bumped to 2.0.0 temporarily // during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2 // through -beta.5 and -rc.0 have already been published. - version: "3.2.2", + version: '3.2.3-beta350.7', }); Npm.depends({ diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 49f94544a0..5df402d74d 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1,4 +1,6 @@ Accounts._connectionCloseDelayMsForTests = 1000; +Accounts._options.ambiguousErrorMessages = false; + const makeTestConnAsync = (test) => new Promise((resolve, reject) => { diff --git a/packages/ddp-client/client/client_convenience.js b/packages/ddp-client/client/client_convenience.js index 052a3087bb..db8383abca 100644 --- a/packages/ddp-client/client/client_convenience.js +++ b/packages/ddp-client/client/client_convenience.js @@ -1,28 +1,91 @@ -import { DDP } from '../common/namespace.js'; import { Meteor } from 'meteor/meteor'; -import { loadAsyncStubHelpers } from "./queue_stub_helpers"; +import { DDP } from '../common/namespace.js'; +import { loadAsyncStubHelpers } from './queue_stub_helpers'; + +const normalizeRuntimePrefix = runtimePrefix => { + if (!runtimePrefix) { + return ''; + } + + const withLeadingSlash = runtimePrefix.startsWith('/') + ? runtimePrefix + : `/${runtimePrefix}`; + + return withLeadingSlash.endsWith('/') + ? withLeadingSlash + : `${withLeadingSlash}/`; +}; + +const extractPathPrefix = (absoluteUrl, runtimeConfig) => { + const pathFromAbsoluteUrl = (() => { + if (!absoluteUrl) { + return ''; + } + + try { + return new URL(absoluteUrl).pathname || '/'; + } catch { + return ''; + } + })(); + const normalizedRuntimePrefix = normalizeRuntimePrefix(runtimeConfig.ROOT_URL_PATH_PREFIX); + + if (pathFromAbsoluteUrl && pathFromAbsoluteUrl !== '/') { + return pathFromAbsoluteUrl.startsWith('/') + ? pathFromAbsoluteUrl + : `/${pathFromAbsoluteUrl}`; + } + + if (normalizedRuntimePrefix) { + return normalizedRuntimePrefix; + } + + if (pathFromAbsoluteUrl) { + return pathFromAbsoluteUrl.startsWith('/') + ? pathFromAbsoluteUrl + : `/${pathFromAbsoluteUrl}`; + } + + return '/'; +}; + +export const _calculateDDPUrl = ({ + absoluteUrl, + runtimeConfig = Object.create(null), + browserHost, + browserProtocol, +}) => { + if (runtimeConfig.DDP_DEFAULT_CONNECTION_URL) { + return runtimeConfig.DDP_DEFAULT_CONNECTION_URL; + } + + const protocol = (absoluteUrl && absoluteUrl.split('//')[0]) || browserProtocol; + const pathPrefix = extractPathPrefix(absoluteUrl, runtimeConfig); + return `${protocol}//${browserHost}${pathPrefix}`; +}; + +const getDDPUrl = () => { + const runtimeConfig = typeof __meteor_runtime_config__ !== 'undefined' + ? __meteor_runtime_config__ + : Object.create(null); + + return _calculateDDPUrl({ + absoluteUrl: Meteor.absoluteUrl(), + runtimeConfig, + browserHost: window.location.host, + browserProtocol: window.location.protocol, + }); +}; // Meteor.refresh can be called on the client (if you're in common code) but it // only has an effect on the server. Meteor.refresh = () => {}; -// By default, try to connect back to the same endpoint as the page -// was served from. -// -// XXX We should be doing this a different way. Right now we don't -// include ROOT_URL_PATH_PREFIX when computing ddpUrl. (We don't -// include it on the server when computing -// DDP_DEFAULT_CONNECTION_URL, and we don't include it in our -// default, '/'.) We get by with this because DDP.connect then -// forces the URL passed to it to be interpreted relative to the -// app's deploy path, even if it is absolute. Instead, we should -// make DDP_DEFAULT_CONNECTION_URL, if set, include the path prefix; -// make the default ddpUrl be '' rather that '/'; and make -// _translateUrl in stream_client_common.js not force absolute paths -// to be treated like relative paths. See also -// stream_client_common.js #RationalizingRelativeDDPURLs -const runtimeConfig = typeof __meteor_runtime_config__ !== 'undefined' ? __meteor_runtime_config__ : Object.create(null); -const ddpUrl = runtimeConfig.DDP_DEFAULT_CONNECTION_URL || '/'; +// By default, connect to the current browser host so mirrored domains +// establish their websocket connection against the same host users loaded. +// Keep the protocol and app path from Meteor.absoluteUrl() to preserve +// force-ssl and deploy-path behavior. +const ddpUrl = getDDPUrl() || '/'; const retry = new Retry(); diff --git a/packages/ddp-client/common/connection_stream_handlers.js b/packages/ddp-client/common/connection_stream_handlers.js index bfef109ff0..d4fa785df6 100644 --- a/packages/ddp-client/common/connection_stream_handlers.js +++ b/packages/ddp-client/common/connection_stream_handlers.js @@ -33,6 +33,11 @@ export class ConnectionStreamHandlers { return; } + // Track received message count for session resumption (excluding ping/pong) + if (!this._connection._ignoredMsgsForSessionOutOfDateCheck.includes(msg.msg)) { + this._connection._receivedCount++; + } + // Important: This was missing from previous version // We need to set the current version before routing the message if (msg.msg === 'connected') { @@ -139,6 +144,7 @@ export class ConnectionStreamHandlers { const msg = { msg: 'connect' }; if (this._connection._lastSessionId) { msg.session = this._connection._lastSessionId; + msg.receivedCount = this._connection._receivedCount; } msg.version = this._connection._versionSuggestion || this._connection._supportedDDPVersions[0]; this._connection._versionSuggestion = msg.version; diff --git a/packages/ddp-client/common/livedata_connection.js b/packages/ddp-client/common/livedata_connection.js index 9755a8012a..3f508eea34 100644 --- a/packages/ddp-client/common/livedata_connection.js +++ b/packages/ddp-client/common/livedata_connection.js @@ -93,6 +93,10 @@ export class Connection { } self._lastSessionId = null; + // how many messages we've received (excluding ping/pong). + // when we try to reconnect to the server, it will check this against the number of messages it sent. + // if there is a mismatch, our info is out of date and we need a clean session. + self._receivedCount = 0; self._versionSuggestion = null; // The last proposed DDP version. self._version = null; // The DDP version agreed on by client and server. self._stores = Object.create(null); // name -> object with methods @@ -102,6 +106,7 @@ export class Connection { self._heartbeatInterval = options.heartbeatInterval; self._heartbeatTimeout = options.heartbeatTimeout; + self._ignoredMsgsForSessionOutOfDateCheck = ['ping', 'pong']; // Tracks methods which the user has tried to call but which have not yet // called their user callback (ie, they are waiting on their result or for all @@ -1081,11 +1086,13 @@ export class Connection { * @locus Client */ disconnect(...args) { + this._send({ msg: 'disconnect' }); return this._stream.disconnect(...args); } close() { - return this._stream.disconnect({ _permanent: true }); + // _permanent is used by the underlying stream to prevent reconnection attempts + return this.disconnect({ _permanent: true }); } /// diff --git a/packages/ddp-client/common/message_processors.js b/packages/ddp-client/common/message_processors.js index 09b13f742e..0fbe7ece98 100644 --- a/packages/ddp-client/common/message_processors.js +++ b/packages/ddp-client/common/message_processors.js @@ -43,10 +43,15 @@ export class MessageProcessors { if (reconnectedToPreviousSession) { // Successful reconnection -- pick up where we left off. + // Don't reset stores since we're continuing the same session. + self._resetStores = false; return; } // Server doesn't have our data anymore. Re-sync a new session. + // Reset the received count since we're starting a new session. + // Set to 1 because the 'connected' message itself counts. + self._receivedCount = 1; // Forget about messages we were buffering for unknown collections. They'll // be resent if still relevant. diff --git a/packages/ddp-client/package.js b/packages/ddp-client/package.js index a9e4c534ef..90b6961de2 100644 --- a/packages/ddp-client/package.js +++ b/packages/ddp-client/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Meteor's latency-compensated distributed data client", - version: "3.1.1", + version: '3.2.0-beta350.7', documentation: null, }); @@ -68,4 +68,5 @@ Package.onTest((api) => { api.addFiles("test/async_stubs/server_setup.js", "server"); api.addFiles("test/livedata_callAsync_tests.js"); api.addFiles("test/allow_deny_setup.js"); + api.addFiles("test/client_convenience_tests.js", "client"); }); diff --git a/packages/ddp-client/test/client_convenience_tests.js b/packages/ddp-client/test/client_convenience_tests.js new file mode 100644 index 0000000000..af243f78d6 --- /dev/null +++ b/packages/ddp-client/test/client_convenience_tests.js @@ -0,0 +1,91 @@ +import { _calculateDDPUrl } from '../client/client_convenience.js'; + +Tinytest.add( + 'ddp-client - client convenience uses DDP_DEFAULT_CONNECTION_URL when configured', + function(test) { + const ddpUrl = _calculateDDPUrl({ + absoluteUrl: 'https://example.com/', + runtimeConfig: { + DDP_DEFAULT_CONNECTION_URL: 'https://example.net/' + }, + browserHost: 'example.net', + browserProtocol: 'https:', + }); + + test.equal(ddpUrl, 'https://example.net/'); + } +); + +Tinytest.add( + 'ddp-client - client convenience fallback uses current browser host for mirror domains', + function(test) { + const ddpUrl = _calculateDDPUrl({ + absoluteUrl: 'https://example.com/', + runtimeConfig: Object.create(null), + browserHost: 'example.net', + browserProtocol: 'https:', + }); + + test.equal(ddpUrl, 'https://example.net/'); + } +); + +Tinytest.add( + 'ddp-client - client convenience fallback keeps app path prefix (subdirectory)', + function(test) { + const ddpUrl = _calculateDDPUrl({ + absoluteUrl: 'https://example.com/my-app/', + runtimeConfig: { + ROOT_URL_PATH_PREFIX: '/my-app' + }, + browserHost: 'example.net', + browserProtocol: 'https:', + }); + + test.equal(ddpUrl, 'https://example.net/my-app/'); + } +); + +Tinytest.add( + 'ddp-client - client convenience fallback uses ROOT_URL_PATH_PREFIX when absoluteUrl is root', + function(test) { + const ddpUrl = _calculateDDPUrl({ + absoluteUrl: 'https://example.com/', + runtimeConfig: { + ROOT_URL_PATH_PREFIX: '/my-app' + }, + browserHost: 'example.net', + browserProtocol: 'https:', + }); + + test.equal(ddpUrl, 'https://example.net/my-app/'); + } +); + +Tinytest.add( + 'ddp-client - client convenience fallback keeps browser host port', + function(test) { + const ddpUrl = _calculateDDPUrl({ + absoluteUrl: 'https://example.com/', + runtimeConfig: Object.create(null), + browserHost: 'example.net:3443', + browserProtocol: 'https:', + }); + + test.equal(ddpUrl, 'https://example.net:3443/'); + } +); + +Tinytest.add( + 'ddp-client - client convenience fallback keeps protocol from absoluteUrl', + function(test) { + const ddpUrl = _calculateDDPUrl({ + absoluteUrl: 'https://example.com/', + runtimeConfig: Object.create(null), + browserHost: 'example.net', + browserProtocol: 'http:', + }); + + test.equal(ddpUrl, 'https://example.net/'); + } +); diff --git a/packages/ddp-client/test/livedata_connection_tests.js b/packages/ddp-client/test/livedata_connection_tests.js index 94994d3fbe..69a8ebce0c 100644 --- a/packages/ddp-client/test/livedata_connection_tests.js +++ b/packages/ddp-client/test/livedata_connection_tests.js @@ -20,14 +20,16 @@ const newConnection = function(stream, options) { ); }; -const makeConnectMessage = function(session) { +const makeConnectMessage = function(session, receivedCount) { const msg = { msg: 'connect', version: DDPCommon.SUPPORTED_DDP_VERSIONS[0], - support: DDPCommon.SUPPORTED_DDP_VERSIONS + support: DDPCommon.SUPPORTED_DDP_VERSIONS, }; if (session) msg.session = session; + if (receivedCount) msg.receivedCount = receivedCount; + return msg; }; @@ -869,7 +871,7 @@ Tinytest.addAsync('livedata stub - reconnect', async function(test, onComplete) // sub. The wait method still is blocked. await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); testGotMessage(test, stream, methodMessage); testGotMessage(test, stream, subMessage); @@ -990,7 +992,7 @@ if (Meteor.isClient) { await stream.reset(); // verify that a reconnect message was sent. - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); // Make sure that the stream triggers connection. await stream.receive({ msg: 'connected', session: SESSION_ID + 1 }); @@ -1114,7 +1116,7 @@ if (Meteor.isClient) { // in. Reconnect quiescence happens as soon as 'connected' is received because // there are no pending methods or subs in need of revival. await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); // Still holding out hope for session resumption, so nothing updated yet. test.equal(coll.find().count(), 1); test.equal(await coll.findOneAsync(stubWrittenId), { @@ -1209,7 +1211,7 @@ if (Meteor.isClient) { // but slowMethod gets called via onReconnect. Reconnect quiescence is now // blocking on slowMethod. await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID + 1)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID + 1, conn._receivedCount)); const slowMethodId = testGotMessage(test, stream, { msg: 'method', method: 'slowMethod', @@ -1330,7 +1332,7 @@ Tinytest.addAsync('livedata stub - reconnect method which only got data', async // Reset stream. Method gets resent (with same ID), and blocks reconnect // quiescence. await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); testGotMessage(test, stream, { msg: 'method', method: 'doLittle', @@ -1807,7 +1809,7 @@ addReconnectTests( // reconnect stream.sent = []; await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(conn._lastSessionId)); + testGotMessage(test, stream, makeConnectMessage(conn._lastSessionId, conn._receivedCount)); // Test that we sent what we expect to send, and we're blocked on // what we expect to be blocked. The subsequent logic to correctly @@ -2033,7 +2035,7 @@ addReconnectTests( // reconnect stream.sent = []; await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(conn._lastSessionId)); + testGotMessage(test, stream, makeConnectMessage(conn._lastSessionId, conn._receivedCount)); // Test that we sent what we expect to send, and we're blocked on // what we expect to be blocked. The subsequent logic to correctly @@ -2084,7 +2086,7 @@ addReconnectTests( // initial connect stream.sent = []; await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(conn._lastSessionId)); + testGotMessage(test, stream, makeConnectMessage(conn._lastSessionId, conn._receivedCount)); // Test that we sent just the login message. const loginId = testGotMessage(test, stream, { @@ -2152,7 +2154,7 @@ addReconnectTests('livedata stub - reconnect double wait method', async function // Reset stream. halfwayMethod does NOT get resent, but reconnectMethod does! // Reconnect quiescence happens when reconnectMethod is done. await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); const reconnectId = testGotMessage(test, stream, { msg: 'method', method: 'reconnectMethod', @@ -2257,7 +2259,7 @@ Tinytest.addAsync('livedata stub - subscribe errors', async function(test) { // stream reset: reconnect! await stream.reset(); // We send a connect. - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); // We should NOT re-sub to the sub, because we processed the error. test.length(stream.sent, 0); test.isFalse(onReadyFired); @@ -2376,7 +2378,7 @@ if (Meteor.isClient) { // Initiate reconnect. await stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID, conn._receivedCount)); testGotMessage(test, stream, subMessage); await stream.receive({ msg: 'connected', session: SESSION_ID + 1 }); @@ -2559,8 +2561,137 @@ if (Meteor.isClient) { ); } +// ============================================================================ +// DDP Session Resumption Tests (Client-side) +// ============================================================================ + +Tinytest.addAsync('livedata connection - receivedCount tracking', async function(test) { + const stream = new StubStream(); + const conn = newConnection(stream); + + // Initially receivedCount should be 0 + test.equal(conn._receivedCount, 0); + + await startAndConnect(test, stream); + + // After receiving 'connected', receivedCount should be 1 + // (the 'connected' message itself is counted) + test.equal(conn._receivedCount, 1); + + // Receive some data messages + await stream.receive({ msg: 'added', collection: 'test', id: '1', fields: { a: 1 } }); + test.equal(conn._receivedCount, 2); + + await stream.receive({ msg: 'added', collection: 'test', id: '2', fields: { b: 2 } }); + test.equal(conn._receivedCount, 3); + + // Ping/pong should NOT increment receivedCount + await stream.receive({ msg: 'ping', id: 'ping1' }); + test.equal(conn._receivedCount, 3, "ping should not increment receivedCount"); + + await stream.receive({ msg: 'pong', id: 'pong1' }); + test.equal(conn._receivedCount, 3, "pong should not increment receivedCount"); + + // More data messages should continue incrementing + await stream.receive({ msg: 'changed', collection: 'test', id: '1', fields: { a: 2 } }); + test.equal(conn._receivedCount, 4); +}); + +Tinytest.addAsync('livedata connection - receivedCount sent on reconnect', async function(test) { + const stream = new StubStream(); + const conn = newConnection(stream); + + await startAndConnect(test, stream); + + // Receive some messages to build up receivedCount + await stream.receive({ msg: 'added', collection: 'test', id: '1', fields: {} }); + await stream.receive({ msg: 'added', collection: 'test', id: '2', fields: {} }); + await stream.receive({ msg: 'ready', subs: ['sub1'] }); + + const expectedReceivedCount = conn._receivedCount; + test.equal(expectedReceivedCount, 4); // connected + 3 messages + + // Simulate disconnect and reconnect + await stream.reset(); + + // The connect message should include the receivedCount + const connectMsg = JSON.parse(stream.sent.shift()); + test.equal(connectMsg.msg, 'connect'); + test.equal(connectMsg.session, SESSION_ID); + test.equal(connectMsg.receivedCount, expectedReceivedCount, + "Connect message should include receivedCount for session resumption"); +}); + +Tinytest.addAsync('livedata connection - receivedCount reset on new session', async function(test) { + const stream = new StubStream(); + const conn = newConnection(stream); + + await startAndConnect(test, stream); + + // Build up some receivedCount + await stream.receive({ msg: 'added', collection: 'test', id: '1', fields: {} }); + await stream.receive({ msg: 'added', collection: 'test', id: '2', fields: {} }); + test.equal(conn._receivedCount, 3); + + // Simulate reconnect + await stream.reset(); + stream.sent.shift(); // consume connect message + + // Server responds with a DIFFERENT session (new session, not resumed) + const newSessionId = SESSION_ID + '_new'; + await stream.receive({ msg: 'connected', session: newSessionId }); + + // receivedCount should be reset to 1 (counting the new connected message) + test.equal(conn._receivedCount, 1, + "receivedCount should be reset to 1 when getting a new session"); + test.equal(conn._lastSessionId, newSessionId); +}); + +Tinytest.addAsync('livedata connection - receivedCount preserved on session resume', async function(test) { + const stream = new StubStream(); + const conn = newConnection(stream); + + await startAndConnect(test, stream); + + // Build up some receivedCount + await stream.receive({ msg: 'added', collection: 'test', id: '1', fields: {} }); + await stream.receive({ msg: 'added', collection: 'test', id: '2', fields: {} }); + const countBeforeDisconnect = conn._receivedCount; + test.equal(countBeforeDisconnect, 3); + + // Simulate reconnect + await stream.reset(); + stream.sent.shift(); // consume connect message + + // Server responds with the SAME session (resumed) + await stream.receive({ msg: 'connected', session: SESSION_ID }); + + // receivedCount should continue from where it was (plus the connected message) + test.equal(conn._receivedCount, countBeforeDisconnect + 1, + "receivedCount should continue incrementing on session resume"); + test.equal(conn._lastSessionId, SESSION_ID); +}); + +Tinytest.addAsync('livedata connection - disconnect sends disconnect message', async function(test) { + const stream = new StubStream(); + const conn = newConnection(stream); + + await startAndConnect(test, stream); + + // Clear any pending messages + stream.sent.length = 0; + + // Call disconnect + conn.disconnect(); + + // Should have sent a disconnect message + test.isTrue(stream.sent.length > 0, "Should have sent at least one message"); + const disconnectMsg = JSON.parse(stream.sent.shift()); + test.equal(disconnectMsg.msg, 'disconnect', + "disconnect() should send a disconnect message to the server"); +}); + // XXX also test: -// - reconnect, with session resume. // - restart on update flag // - on_update event // - reloading when the app changes, including session migration \ No newline at end of file diff --git a/packages/ddp-client/test/stub_stream.js b/packages/ddp-client/test/stub_stream.js index 43c05127fa..3e57b5ed4f 100644 --- a/packages/ddp-client/test/stub_stream.js +++ b/packages/ddp-client/test/stub_stream.js @@ -27,6 +27,10 @@ Object.assign(StubStream.prototype, { // no-op }, + disconnect: function() { + // no-op - for testing Connection.disconnect() + }, + _lostConnection: function() { // no-op }, diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js b/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js index dd84b4b51c..be1ef5700c 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js +++ b/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js @@ -30,7 +30,11 @@ Meteor.methods({ }, userId(userId) { connection.lastRateLimitEvent.userId = userId; - return true; + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, 2); + }); }, type(type) { // Special check to return proper name since 'getLastRateLimitEvent' diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter.d.ts b/packages/ddp-rate-limiter/ddp-rate-limiter.d.ts index fbce221f5a..2de2ce9bfb 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter.d.ts +++ b/packages/ddp-rate-limiter/ddp-rate-limiter.d.ts @@ -1,10 +1,10 @@ export namespace DDPRateLimiter { interface Matcher { - type?: string | ((type: string) => boolean) | undefined; - name?: string | ((name: string) => boolean) | undefined; - userId?: string | ((userId: string) => boolean) | undefined; - connectionId?: string | ((connectionId: string) => boolean) | undefined; - clientAddress?: string | ((clientAddress: string) => boolean) | undefined; + type?: string | ((type: string) => boolean) | ((type: string) => Promise)| undefined; + name?: string | ((name: string) => boolean) | ((name: string) => Promise)| undefined; + userId?: string | ((userId: string) => boolean) | ((userId: string) => Promise)| undefined; + connectionId?: string | ((connectionId: string) => boolean) | ((connectionId: string) => Promise)| undefined; + clientAddress?: string | ((clientAddress: string) => boolean) | ((clientAddress: string) => Promise)| undefined; } function addRule( diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter.js b/packages/ddp-rate-limiter/ddp-rate-limiter.js index 15922cbe94..f3e736717b 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter.js +++ b/packages/ddp-rate-limiter/ddp-rate-limiter.js @@ -114,12 +114,15 @@ DDPRateLimiter.printRules = () => rateLimiter.rules; */ DDPRateLimiter.removeRule = id => rateLimiter.removeRule(id); -// This is accessed inside livedata_server.js, but shouldn't be called by any -// user. DDPRateLimiter._increment = (input) => { rateLimiter.increment(input); }; +// This is accessed inside livedata_server.js, but shouldn't be called by any +// user. +DDPRateLimiter._incrementRules = (rules, input) => rateLimiter.incrementRules(rules, input); DDPRateLimiter._check = input => rateLimiter.check(input); +DDPRateLimiter.findAllMatchingRulesAsync = (input) => rateLimiter._findAllMatchingRulesAsync(input); +DDPRateLimiter._checkRules = (rules, input) => rateLimiter.checkRules(rules, input); export { DDPRateLimiter }; diff --git a/packages/ddp-rate-limiter/package.js b/packages/ddp-rate-limiter/package.js index ea77fda3f4..79ecc97fe9 100644 --- a/packages/ddp-rate-limiter/package.js +++ b/packages/ddp-rate-limiter/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ddp-rate-limiter', - version: '1.2.2', + version: '1.3.0-beta350.7', // Brief, one-line summary of the package. summary: 'The DDPRateLimiter allows users to add rate limits to DDP' + ' methods and subscriptions.', diff --git a/packages/ddp-server/livedata_server.js b/packages/ddp-server/livedata_server.js index 3f5efceafb..9e11592244 100644 --- a/packages/ddp-server/livedata_server.js +++ b/packages/ddp-server/livedata_server.js @@ -81,11 +81,16 @@ var Session = function (server, version, socket, options) { var self = this; self.id = Random.id(); + // how many messages we've actually sent (not queued to send) excluding ping/pong + // we'll use this to detect mismatch of data on reconnect. + self.sentCount = 0; + self.server = server; self.version = version; self.initialized = false; self.socket = socket; + self.options = options; // Set to null when the session is destroyed. Multiple places below // use this to determine if the session is alive or not. @@ -134,6 +139,8 @@ var Session = function (server, version, socket, options) { self.connectionHandle = { id: self.id, close: function () { + // Server-initiated close should not be resumable + self._expectingDisconnect = true; self.close(); }, onClose: function (fn) { @@ -175,6 +182,8 @@ var Session = function (server, version, socket, options) { "livedata", "sessions", 1); }; +const ignoredMsgsForSessionOutOfDateCheck = ['ping', 'pong']; + Object.assign(Session.prototype, { sendReady: function (subscriptionIds) { var self = this; @@ -269,77 +278,103 @@ Object.assign(Session.prototype, { }, startUniversalSubs: function () { - var self = this; + const self = this; // Make a shallow copy of the set of universal handlers and start them. If // additional universal publishers start while we're running them (due to // yielding), they will run separately as part of Server.publish. - var handlers = [...self.server.universal_publish_handlers]; - handlers.forEach(function (handler) { + for (const handler of [...self.server.universal_publish_handlers]) { self._startSubscription(handler); - }); + } + }, + + // Stop heartbeat if running + _stopHeartbeat: function () { + if (this.heartbeat) { + this.heartbeat.stop(); + this.heartbeat = null; + } }, // Destroy this session and unregister it at the server. close: function () { - var self = this; + const self = this; // Destroy this session, even if it's not registered at the // server. Stop all processing and tear everything down. If a socket // was attached, close it. - // Already destroyed. - if (! self.inQueue) + // Already closing or closed - prevent multiple close() calls + if (self._isClosing) { return; + } + self._isClosing = true; - // Drop the merge box data immediately. - self.inQueue = null; - self.collectionViews = new Map(); - - if (self.heartbeat) { - self.heartbeat.stop(); - self.heartbeat = null; + if (self._removeTimeoutHandle) { + Meteor.clearTimeout(self._removeTimeoutHandle); + self._removeTimeoutHandle = null; } if (self.socket) { self.socket.close(); self.socket._meteorSession = null; + self.socket = null; } - Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact( - "livedata", "sessions", -1); + // Stop heartbeat immediately - we don't need it during the grace period + // since we have no socket to send pings on anyway. + self._stopHeartbeat(); - Meteor.defer(function () { - // Stop callbacks can yield, so we defer this on close. - // sub._isDeactivated() detects that we set inQueue to null and - // treats it as semi-deactivated (it will ignore incoming callbacks, etc). - self._deactivateAllSubscriptions(); + self.server._removeSession(self, () => { + Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact( + "livedata", "sessions", -1); - // Defer calling the close callbacks, so that the caller closing - // the session isn't waiting for all the callbacks to complete. - self._closeCallbacks.forEach(function (callback) { - callback(); + self.inQueue = null; + self.collectionViews = new Map(); + + self._stopHeartbeat(); + + Meteor.defer(function () { + // stop callbacks can yield, so we defer this on close. + // sub._isDeactivated() detects that we set inQueue to null and + // treats it as semi-deactivated (it will ignore incoming callbacks, etc). + self._deactivateAllSubscriptions(); + + // Defer calling the close callbacks, so that the caller closing + // the session isn't waiting for all the callbacks to complete. + self._closeCallbacks.forEach(callback => { + callback(); + }); }); }); - - // Unregister the session. - self.server._removeSession(self); }, // Send a message (doing nothing if no socket is connected right now). // It should be a JSON object (it will be stringified). send: function (msg) { const self = this; + const isIgnoredMsg = ignoredMsgsForSessionOutOfDateCheck.includes(msg.msg); + if (self.messageQueue && !isIgnoredMsg) { + self.messageQueue.push(msg); + if (self.messageQueue.length > self.options.maxMessageQueueLength) { + Meteor.clearTimeout(self._removeTimeoutHandle); + self._pendingRemoveFunction(); + } + return; + } if (self.socket) { if (Meteor._printSentDDP) Meteor._debug("Sent DDP", DDPCommon.stringifyDDP(msg)); + if (!isIgnoredMsg) { + self.sentCount++; + } self.socket.send(DDPCommon.stringifyDDP(msg)); } }, // Send a connection error. sendError: function (reason, offendingMessage) { - var self = this; - var msg = {msg: 'error', reason: reason}; + const self = this; + const msg = {msg: 'error', reason: reason}; if (offendingMessage) msg.offendingMessage = offendingMessage; self.send(msg); @@ -379,7 +414,7 @@ Object.assign(Session.prototype, { // the client is still alive. if (self.heartbeat) { self.heartbeat.messageReceived(); - }; + } if (self.version !== 'pre1' && msg_in.msg === 'ping') { if (self._respondToPings) @@ -391,6 +426,11 @@ Object.assign(Session.prototype, { return; } + if (msg_in.msg === 'disconnect') { + // Pre-empt the queue - a disconnect is imminent. + return self.protocol_handlers.disconnect.call(self, msg_in, () => {}); + } + self.inQueue.push(msg_in); if (self.workerRunning) return; @@ -444,6 +484,9 @@ Object.assign(Session.prototype, { }, protocol_handlers: { + disconnect: function(msg) { + this._expectingDisconnect = true; + }, sub: async function (msg, unblock) { var self = this; @@ -487,8 +530,9 @@ Object.assign(Session.prototype, { connectionId: self.id }; - DDPRateLimiter._increment(rateLimiterInput); - var rateLimitResult = DDPRateLimiter._check(rateLimiterInput); + const rules = await DDPRateLimiter.findAllMatchingRulesAsync(rateLimiterInput); + DDPRateLimiter._incrementRules(rules, rateLimiterInput); + const rateLimitResult = DDPRateLimiter._checkRules(rules, rateLimiterInput); if (!rateLimitResult.allowed) { self.send({ msg: 'nosub', id: msg.id, @@ -568,44 +612,6 @@ Object.assign(Session.prototype, { fence, }); - const promise = new Promise((resolve, reject) => { - // XXX It'd be better if we could hook into method handlers better but - // for now, we need to check if the ddp-rate-limiter exists since we - // have a weak requirement for the ddp-rate-limiter package to be added - // to our application. - if (Package['ddp-rate-limiter']) { - var DDPRateLimiter = Package['ddp-rate-limiter'].DDPRateLimiter; - var rateLimiterInput = { - userId: self.userId, - clientAddress: self.connectionHandle.clientAddress, - type: "method", - name: msg.method, - connectionId: self.id - }; - DDPRateLimiter._increment(rateLimiterInput); - var rateLimitResult = DDPRateLimiter._check(rateLimiterInput) - if (!rateLimitResult.allowed) { - reject(new Meteor.Error( - "too-many-requests", - DDPRateLimiter.getErrorMessage(rateLimitResult), - {timeToReset: rateLimitResult.timeToReset} - )); - return; - } - } - - resolve(DDPServer._CurrentWriteFence.withValue( - fence, - () => DDP._CurrentMethodInvocation.withValue( - invocation, - () => maybeAuditArgumentChecks( - handler, invocation, msg.params, - "call to '" + msg.method + "'" - ) - ) - )); - }); - async function finish() { await fence.arm(); unblock(); @@ -615,20 +621,57 @@ Object.assign(Session.prototype, { msg: "result", id: msg.id }; - return promise.then(async result => { + + try { + // XXX It'd be better if we could hook into method handlers better but + // for now, we need to check if the ddp-rate-limiter exists since we + // have a weak requirement for the ddp-rate-limiter package to be added + // to our application. + if (Package['ddp-rate-limiter']) { + const DDPRateLimiter = Package['ddp-rate-limiter'].DDPRateLimiter; + var rateLimiterInput = { + userId: self.userId, + clientAddress: self.connectionHandle.clientAddress, + type: "method", + name: msg.method, + connectionId: self.id + }; + const rules = await DDPRateLimiter.findAllMatchingRulesAsync(rateLimiterInput); + DDPRateLimiter._incrementRules(rules, rateLimiterInput); + const rateLimitResult = DDPRateLimiter._checkRules(rules, rateLimiterInput); + if (!rateLimitResult.allowed) { + throw new Meteor.Error( + "too-many-requests", + DDPRateLimiter.getErrorMessage(rateLimitResult), + {timeToReset: rateLimitResult.timeToReset} + ); + } + } + + const result = await DDPServer._CurrentWriteFence.withValue( + fence, + () => DDP._CurrentMethodInvocation.withValue( + invocation, + () => maybeAuditArgumentChecks( + handler, invocation, msg.params, + "call to '" + msg.method + "'" + ) + ) + ); + await finish(); if (result !== undefined) { payload.result = result; } self.send(payload); - }, async (exception) => { + } catch (exception) { await finish(); payload.error = wrapInternalException( exception, `while invoking method '${msg.method}'` ); self.send(payload); - }); + }; } }, @@ -1252,6 +1295,19 @@ Server = function (options = {}) { // For testing, allow responding to pings to be disabled. respondToPings: true, defaultPublicationStrategy: publicationStrategies.SERVER_MERGE, + /** + * @summary How many messages should we queue during a non-graceful disconnect before we destroy the session, to help prevent memory leaks. + * @type {Number} + * @locus Server + */ + maxMessageQueueLength: 100, + /** + * @summary How long we should maintain a session for after a non-graceful disconnect before killing it + * sessions that reconnect within this time will be resumed with minimal performance impact. + * @type {Number} + * @locus Server + */ + disconnectGracePeriod: 15000, ...options, }; @@ -1421,16 +1477,66 @@ Object.assign(Server.prototype, { return; } - // Yay, version matches! Create a new session. + // Yay, version matches! Resume existing session if possible, otherwise create a new one. // Note: Troposphere depends on the ability to mutate // Meteor.server.options.heartbeatTimeout! This is a hack, but it's life. - socket._meteorSession = new Session(self, version, socket, self.options); - self.sessions.set(socket._meteorSession.id, socket._meteorSession); - self.onConnectionHook.each(function (callback) { - if (socket._meteorSession) - callback(socket._meteorSession.connectionHandle); - return true; - }); + const existingSession = self.sessions.get(msg.session); + + // we've found a session with: + // the right ID + // a matching sent/received count + // was disconnected and hasn't been reconnected to yet. + if (existingSession && existingSession.sentCount === msg.receivedCount && existingSession._removeTimeoutHandle) { + Meteor.clearTimeout(existingSession._removeTimeoutHandle); + existingSession._removeTimeoutHandle = undefined; + existingSession._pendingRemoveFunction = undefined; + existingSession._isClosing = false; // Reset so session can be closed again later + socket._meteorSession = existingSession; + const messageQueue = existingSession.messageQueue; + existingSession.messageQueue = undefined; + existingSession.socket = socket; + + // Restart heartbeat for the resumed session + if (existingSession.version !== 'pre1' && self.options.heartbeatInterval !== 0) { + socket.setWebsocketTimeout(0); + existingSession.heartbeat = new DDPCommon.Heartbeat({ + heartbeatInterval: self.options.heartbeatInterval, + heartbeatTimeout: self.options.heartbeatTimeout, + onTimeout: function () { + existingSession.close(); + }, + sendPing: function () { + existingSession.send({msg: 'ping'}); + } + }); + existingSession.heartbeat.start(); + } + + // Send connected message so client can restart heartbeat and confirm resumption + existingSession.send({ msg: 'connected', session: existingSession.id }); + if (messageQueue) { + Meteor.defer(() => { + messageQueue.forEach(msg => existingSession.send(msg)); + }); + } + // Note: onConnectionHook is NOT called on session resume - the connection + // is considered to be the same logical connection as before. + } + else { + // immediately remove the old session since we're out of date. + if (existingSession && existingSession._pendingRemoveFunction) { + Meteor.clearTimeout(existingSession._removeTimeoutHandle); + existingSession._pendingRemoveFunction(); + } + socket._meteorSession = new Session(self, version, socket, self.options); + self.sessions.set(socket._meteorSession.id, socket._meteorSession); + + self.onConnectionHook.each(function (callback) { + if (socket._meteorSession) + callback(socket._meteorSession.connectionHandle); + return true; + }); + } }, /** * Register a publish handler function. @@ -1520,9 +1626,31 @@ Object.assign(Server.prototype, { } }, - _removeSession: function (session) { + _removeSession: function (session, callback = () => {}) { var self = this; - self.sessions.delete(session.id); + const sessionRemoveFunction = () => { + // Guard against being called multiple times (e.g., from both overflow and timeout) + if (!self.sessions.has(session.id)) { + return; + } + // Clear timeout handle if it exists to prevent double execution + if (session._removeTimeoutHandle) { + Meteor.clearTimeout(session._removeTimeoutHandle); + session._removeTimeoutHandle = null; + } + session._pendingRemoveFunction = null; + self.sessions.delete(session.id); + callback(); + }; + if (session._expectingDisconnect) { + return sessionRemoveFunction(); + } + session.messageQueue = []; + session._pendingRemoveFunction = sessionRemoveFunction; + if (session._removeTimeoutHandle) { + Meteor.clearTimeout(session._removeTimeoutHandle); + } + session._removeTimeoutHandle = Meteor.setTimeout(sessionRemoveFunction, self.options.disconnectGracePeriod); }, /** diff --git a/packages/ddp-server/livedata_server_async_tests.js b/packages/ddp-server/livedata_server_async_tests.js index 4ca4ca0864..3606b66044 100644 --- a/packages/ddp-server/livedata_server_async_tests.js +++ b/packages/ddp-server/livedata_server_async_tests.js @@ -168,9 +168,24 @@ Tinytest.addAsync('livedata server - async publish cursor', function( connection: clientConn, }); clientConn.subscribe('asyncPublishCursor', async () => { - const actual = await remoteCollection.find().fetch(); - test.equal(actual[0].name, 'async'); - onComplete(); + // Wait for data to arrive - the subscription is ready but data may still be in transit + // This can happen when a previous test run was interrupted (page reload) and the + // server is still processing the old session's grace period + let attempts = 0; + const maxAttempts = 50; // 5 seconds max wait + const checkData = async () => { + const actual = await remoteCollection.find().fetch(); + if (actual.length > 0) { + test.equal(actual[0].name, 'async'); + onComplete(); + } else if (attempts++ < maxAttempts) { + setTimeout(checkData, 100); + } else { + test.fail('Timed out waiting for data in async publish cursor test'); + onComplete(); + } + }; + await checkData(); }); }); }); diff --git a/packages/ddp-server/livedata_server_tests.js b/packages/ddp-server/livedata_server_tests.js index 15b0349e87..48313dcbcb 100644 --- a/packages/ddp-server/livedata_server_tests.js +++ b/packages/ddp-server/livedata_server_tests.js @@ -1,3 +1,38 @@ +// Helper to temporarily set disconnectGracePeriod for DDP resumption tests +// This ensures test isolation - other tests run with the default grace period +const DEFAULT_GRACE_PERIOD = Meteor.server.options.disconnectGracePeriod; +const TEST_GRACE_PERIOD = 5000; // Short grace period for fast tests (ms) +// Derived timing constants to avoid hardcoding throughout tests +const WITHIN_GRACE_PERIOD_MS = Math.floor(TEST_GRACE_PERIOD / 4); // Well within grace period +const AFTER_GRACE_PERIOD_MS = Math.ceil(TEST_GRACE_PERIOD * 1.5); // After grace period expires +const POLL_TIMEOUT_MS = TEST_GRACE_PERIOD * 2; // Max time to wait for async operations before failing + +async function withTestGracePeriod(fn) { + const previous = Meteor.server.options.disconnectGracePeriod; + Meteor.server.options.disconnectGracePeriod = TEST_GRACE_PERIOD; + try { + await fn(); + } finally { + Meteor.server.options.disconnectGracePeriod = previous ?? DEFAULT_GRACE_PERIOD; + } +} + +// Helper to poll for a condition with timeout to prevent hanging tests +function pollUntil(conditionFn, timeoutMs = POLL_TIMEOUT_MS) { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + const interval = setInterval(() => { + if (conditionFn()) { + clearInterval(interval); + resolve(); + } else if (Date.now() - startTime > timeoutMs) { + clearInterval(interval); + reject(new Error(`Timed out after ${timeoutMs}ms waiting for condition`)); + } + }, 10); + }); +} + Tinytest.addAsync( "livedata server - connectionHandle.onClose()", function (test, onComplete) { @@ -593,4 +628,344 @@ function getTestConnections(test) { function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); -} \ No newline at end of file +} + +// ============================================================================ +// DDP Session Resumption Tests +// ============================================================================ + +// Test that unexpected disconnects allow session resumption within grace period +Tinytest.addAsync( + "livedata server - DDP resumption: unexpected disconnect preserves session", + async function (test) { + await withTestGracePeriod(async () => { + const { clientConn, serverConn } = await getTestConnections(test); + const originalSessionId = serverConn.id; + + // Verify the session exists + test.isTrue(Meteor.server.sessions.has(originalSessionId)); + + // Simulate unexpected disconnect by forcing the stream to close + // without sending a disconnect message + clientConn._stream._lostConnection(); + + // Wait a bit but less than the grace period + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Session should still exist during grace period + test.isTrue( + Meteor.server.sessions.has(originalSessionId), + "Session should be preserved during grace period" + ); + + // Wait for grace period to expire + await sleep(AFTER_GRACE_PERIOD_MS); + + // Session should be removed after grace period + test.isFalse( + Meteor.server.sessions.has(originalSessionId), + "Session should be removed after grace period expires" + ); + }); + } +); + +// Test that graceful disconnects (client sends disconnect message) remove session immediately +Tinytest.addAsync( + "livedata server - DDP resumption: graceful disconnect removes session immediately", + async function (test) { + await withTestGracePeriod(async () => { + const { clientConn, serverConn } = await getTestConnections(test); + const originalSessionId = serverConn.id; + + // Verify the session exists + test.isTrue(Meteor.server.sessions.has(originalSessionId)); + + // Graceful disconnect - this sends the disconnect message + clientConn.disconnect(); + + // Wait a moment for the disconnect to process + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Session should be removed immediately (not waiting for grace period) + test.isFalse( + Meteor.server.sessions.has(originalSessionId), + "Session should be removed immediately after graceful disconnect" + ); + }); + } +); + +// Test that server-initiated close removes session immediately (not resumable) +Tinytest.addAsync( + "livedata server - DDP resumption: server-initiated close removes session immediately", + async function (test) { + await withTestGracePeriod(async () => { + const { serverConn } = await getTestConnections(test); + const originalSessionId = serverConn.id; + + // Verify the session exists + test.isTrue(Meteor.server.sessions.has(originalSessionId)); + + // Server-initiated close via connectionHandle.close() + serverConn.close(); + + // Wait a moment for the close to process + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Session should be removed immediately (server kicks should not be resumable) + test.isFalse( + Meteor.server.sessions.has(originalSessionId), + "Session should be removed immediately after server-initiated close" + ); + }); + } +); + +// Test that onConnection hook is NOT called on session resume +Tinytest.addAsync( + "livedata server - DDP resumption: onConnection not called on resume", + async function (test) { + await withTestGracePeriod(async () => { + let onConnectionCallCount = 0; + let lastConnectionId = null; + + const handle = Meteor.onConnection(function (conn) { + onConnectionCallCount++; + lastConnectionId = conn.id; + }); + + // Create initial connection + const clientConn = DDP.connect(Meteor.absoluteUrl(), { retry: false }); + + // Wait for connection with timeout + await pollUntil(() => clientConn._lastSessionId); + + const originalSessionId = clientConn._lastSessionId; + test.equal(onConnectionCallCount, 1, "onConnection should be called once on initial connect"); + test.equal(lastConnectionId, originalSessionId); + + // Get the server session and verify it exists + const serverSession = Meteor.server.sessions.get(originalSessionId); + test.isTrue(serverSession, "Server session should exist"); + + // Simulate unexpected disconnect + clientConn._stream._lostConnection(); + + // Wait a bit (less than grace period) + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Session should still exist + test.isTrue( + Meteor.server.sessions.has(originalSessionId), + "Session should still exist during grace period" + ); + + // Reconnect - this should resume the session + clientConn._stream.reconnect(); + + // Wait for reconnection with timeout + await pollUntil(() => clientConn.status().connected); + + // Give it a moment to process + await sleep(WITHIN_GRACE_PERIOD_MS); + + // IMPORTANT: Assert that session was actually resumed (same session ID) + // If this fails, the test is not actually testing resumption + test.equal( + clientConn._lastSessionId, + originalSessionId, + "Session should be resumed with same session ID" + ); + + // onConnection should NOT have been called again for a resumed session + test.equal( + onConnectionCallCount, + 1, + "onConnection should not be called again on session resume" + ); + + handle.stop(); + clientConn.disconnect(); + }); + } +); + +// Test that server-initiated close prevents session resumption +Tinytest.addAsync( + "livedata server - DDP resumption: server close prevents resumption", + async function (test) { + await withTestGracePeriod(async () => { + let onConnectionCallCount = 0; + + const handle = Meteor.onConnection(function (conn) { + onConnectionCallCount++; + }); + + // Create initial connection + const clientConn = DDP.connect(Meteor.absoluteUrl(), { retry: true }); + + // Wait for connection with timeout + await pollUntil(() => clientConn._lastSessionId); + + const originalSessionId = clientConn._lastSessionId; + test.equal(onConnectionCallCount, 1, "onConnection should be called once on initial connect"); + + // Get the server session + const serverSession = Meteor.server.sessions.get(originalSessionId); + test.isTrue(serverSession, "Server session should exist"); + + // Server-initiated close (kick the client) + serverSession.connectionHandle.close(); + + // Wait for client to reconnect with new session (retry is enabled) + await pollUntil(() => + clientConn.status().connected && clientConn._lastSessionId !== originalSessionId + ); + + // Should have a NEW session (not resumed) + test.notEqual( + clientConn._lastSessionId, + originalSessionId, + "Should have a new session ID after server-initiated close" + ); + + // onConnection should have been called again (new session, not resumed) + test.equal( + onConnectionCallCount, + 2, + "onConnection should be called again after server-initiated close" + ); + + handle.stop(); + clientConn.disconnect(); + }); + } +); + +// Test that graceful client disconnect prevents session resumption +Tinytest.addAsync( + "livedata server - DDP resumption: graceful disconnect prevents resumption", + async function (test) { + await withTestGracePeriod(async () => { + let onConnectionCallCount = 0; + + const handle = Meteor.onConnection(function (conn) { + onConnectionCallCount++; + }); + + // Create initial connection with retry enabled + const clientConn = DDP.connect(Meteor.absoluteUrl(), { retry: true }); + + // Wait for connection with timeout + await pollUntil(() => clientConn._lastSessionId); + + const originalSessionId = clientConn._lastSessionId; + test.equal(onConnectionCallCount, 1, "onConnection should be called once on initial connect"); + + // Graceful disconnect (sends disconnect message) + clientConn.disconnect(); + + // Wait for session to be removed + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Session should be removed immediately + test.isFalse( + Meteor.server.sessions.has(originalSessionId), + "Session should be removed after graceful disconnect" + ); + + // Reconnect + clientConn.reconnect(); + + // Wait for reconnection with timeout + await pollUntil(() => clientConn.status().connected); + + // Should have a NEW session (not resumed, because we gracefully disconnected) + test.notEqual( + clientConn._lastSessionId, + originalSessionId, + "Should have a new session ID after graceful disconnect and reconnect" + ); + + // onConnection should have been called again + test.equal( + onConnectionCallCount, + 2, + "onConnection should be called again after graceful disconnect" + ); + + handle.stop(); + clientConn.disconnect(); + }); + } +); + +// Test that receivedCount mismatch causes new session (not resume) +Tinytest.addAsync( + "livedata server - DDP resumption: count mismatch creates new session", + async function (test) { + await withTestGracePeriod(async () => { + let onConnectionCallCount = 0; + + const handle = Meteor.onConnection(function (conn) { + onConnectionCallCount++; + }); + + // Create initial connection + const clientConn = DDP.connect(Meteor.absoluteUrl(), { retry: false }); + + // Wait for connection with timeout + await pollUntil(() => clientConn._lastSessionId); + + const originalSessionId = clientConn._lastSessionId; + test.equal(onConnectionCallCount, 1, "onConnection should be called once on initial connect"); + + // Get the server session + const serverSession = Meteor.server.sessions.get(originalSessionId); + test.isTrue(serverSession, "Server session should exist"); + + // Artificially increment sentCount to create a mismatch + // This simulates messages sent by server that client didn't receive + serverSession.sentCount += 5; + + // Simulate unexpected disconnect + clientConn._stream._lostConnection(); + + // Wait a bit (less than grace period) + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Session should still exist during grace period + test.isTrue( + Meteor.server.sessions.has(originalSessionId), + "Session should still exist during grace period" + ); + + // Reconnect - this should NOT resume due to count mismatch + clientConn._stream.reconnect(); + + // Wait for reconnection with timeout + await pollUntil(() => clientConn.status().connected); + + // Give it a moment to process + await sleep(WITHIN_GRACE_PERIOD_MS); + + // Should have a NEW session (counts didn't match) + test.notEqual( + clientConn._lastSessionId, + originalSessionId, + "Should have a new session ID when counts mismatch" + ); + + // onConnection should have been called again (new session) + test.equal( + onConnectionCallCount, + 2, + "onConnection should be called again when counts mismatch" + ); + + handle.stop(); + clientConn.disconnect(); + }); + } +); \ No newline at end of file diff --git a/packages/ddp-server/package.js b/packages/ddp-server/package.js index 787706a1c9..b27520b796 100644 --- a/packages/ddp-server/package.js +++ b/packages/ddp-server/package.js @@ -1,10 +1,11 @@ Package.describe({ summary: "Meteor's latency-compensated distributed data server", - version: "3.1.2", + version: '3.2.0-beta350.7', documentation: null, }); Npm.depends({ + "faye-websocket": "0.11.4", "permessage-deflate2": "0.1.8", sockjs: "0.3.24", "lodash.once": "4.1.1", @@ -77,4 +78,5 @@ Package.onTest(function (api) { api.addFiles("livedata_server_async_tests.js", "server"); api.addFiles("session_view_tests.js", ["server"]); api.addFiles("crossbar_tests.js", ["server"]); + api.addFiles("raw_websocket_tests.js", "server"); }); diff --git a/packages/ddp-server/raw_websocket_tests.js b/packages/ddp-server/raw_websocket_tests.js new file mode 100644 index 0000000000..efb3cc3768 --- /dev/null +++ b/packages/ddp-server/raw_websocket_tests.js @@ -0,0 +1,155 @@ +// Tests for DISABLE_SOCKJS raw WebSocket mode and general /websocket endpoint. +// These tests verify that DDP connections work correctly regardless of +// the transport mode (SockJS or raw WebSocket). + +const http = Npm.require('http'); + +const disableSockJS = !!process.env.DISABLE_SOCKJS; + +// Helper: make an HTTP GET request to the given URL, return { statusCode, headers, body } +function httpGet(url) { + return new Promise((resolve, reject) => { + http.get(url, (res) => { + let body = ''; + res.on('data', (chunk) => { body += chunk; }); + res.on('end', () => { + resolve({ statusCode: res.statusCode, headers: res.headers, body }); + }); + }).on('error', reject); + }); +} + +// --- Tests that run in BOTH modes --- + +Tinytest.addAsync( + 'stream server - DDP connection works over /websocket', + function (test, onComplete) { + makeTestConnection( + test, + function (clientConn, serverConn) { + test.isTrue(clientConn.status().connected, 'client should be connected'); + test.isTrue(typeof serverConn.id === 'string', 'server connection should have an id'); + test.isTrue(serverConn.id.length > 0, 'server connection id should not be empty'); + clientConn.disconnect(); + onComplete(); + }, + onComplete + ); + } +); + +Tinytest.addAsync( + 'stream server - connection supports method calls', + function (test, onComplete) { + makeTestConnection( + test, + function (clientConn, serverConn) { + clientConn.callAsync('livedata_server_test_inner').then((res) => { + test.equal(res, serverConn.id, + 'method should see the correct connection id'); + clientConn.disconnect(); + onComplete(); + }); + }, + onComplete + ); + } +); + +Tinytest.addAsync( + 'stream server - plain HTTP to /websocket returns 400', + async function (test) { + // In both modes, a plain HTTP GET to /websocket should not serve the app. + // In DISABLE_SOCKJS mode, our middleware returns 400. + // In SockJS mode, SockJS handles it (returns non-200 or redirect). + const url = Meteor.absoluteUrl('websocket'); + const result = await httpGet(url); + + if (disableSockJS) { + test.equal(result.statusCode, 400, + 'DISABLE_SOCKJS: /websocket should return 400 for plain HTTP'); + test.equal(result.body, 'Not a valid websocket request', + 'DISABLE_SOCKJS: /websocket should return clear error message'); + } else { + // In SockJS mode, /websocket gets rewritten to /sockjs/websocket + // which returns a non-200 for plain HTTP. Just verify it's not + // serving the app HTML. + test.isTrue( + result.statusCode !== 200 || !result.body.includes(' { @@ -118,7 +121,10 @@ export class SessionCollectionView { const docView = this.documents.get(id); if (!docView) { - throw new Error(`Removed nonexistent document ${id}`); + // Document was already removed. This can happen in high-concurrency scenarios + // where the cache is updated synchronously but callbacks are processed + // asynchronously, causing duplicate removal attempts. + return; } docView.existsIn.delete(subscriptionHandle); diff --git a/packages/ddp-server/stream_server.js b/packages/ddp-server/stream_server.js index 3691f59e1d..9cbdb4d2e2 100644 --- a/packages/ddp-server/stream_server.js +++ b/packages/ddp-server/stream_server.js @@ -1,4 +1,5 @@ import once from 'lodash.once'; +import { EventEmitter } from 'events'; import zlib from 'node:zlib'; // By default, we use the permessage-deflate extension with default @@ -33,69 +34,94 @@ var websocketExtensions = once(function () { }); var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; +var disableSockJS = !!process.env.DISABLE_SOCKJS; + +// Wrapper around a raw faye-websocket connection that provides the same +// interface as a SockJS connection (as expected by _onConnection and +// livedata_server.js). +class RawWebSocketConnection extends EventEmitter { + constructor(ws, req, rawSocket) { + super(); + this._ws = ws; + this._rawSocket = rawSocket; + this.protocol = 'websocket-raw'; + this.id = Random.id(); + + // Copy relevant headers (same set as SockJS transport.js) + this.headers = {}; + const headerKeys = [ + 'referer', 'x-client-ip', 'x-forwarded-for', + 'x-forwarded-host', 'x-forwarded-port', 'x-cluster-client-ip', + 'via', 'x-real-ip', 'x-forwarded-proto', 'x-ssl', 'dnt', + 'host', 'user-agent', 'accept-language' + ]; + for (const key of headerKeys) { + if (req.headers[key]) this.headers[key] = req.headers[key]; + } + + this.remoteAddress = rawSocket.remoteAddress; + this.remotePort = rawSocket.remotePort; + this.url = req.url; + + // Compatibility with SockJS internals that stream_server accesses + this._session = { + recv: { + connection: rawSocket, + protocol: 'websocket-raw' + } + }; + + ws.on('message', (event) => { + this.emit('data', event.data); + }); + + ws.on('close', () => { + this.emit('close'); + this._ws = null; + }); + + ws.on('error', () => { + this.emit('close'); + this._ws = null; + }); + } + + write(data) { + if (this._ws) this._ws.send(data); + } + + send(data) { + this.write(data); + } + + close() { + if (this._ws) this._ws.close(); + } + + setWebsocketTimeout(timeout) { + if (this._rawSocket) { + this._rawSocket.setTimeout(timeout); + } + } +} StreamServer = function () { var self = this; self.registration_callbacks = []; self.open_sockets = []; - // Because we are installing directly onto WebApp.httpServer instead of using - // WebApp.app, we have to process the path prefix ourselves. - self.prefix = pathPrefix + '/sockjs'; - RoutePolicy.declare(self.prefix + '/', 'network'); - - // set up sockjs - var sockjs = Npm.require('sockjs'); - var serverOptions = { - prefix: self.prefix, - log: function() {}, - // this is the default, but we code it explicitly because we depend - // on it in stream_client:HEARTBEAT_TIMEOUT - heartbeat_delay: 45000, - // The default disconnect_delay is 5 seconds, but if the server ends up CPU - // bound for that much time, SockJS might not notice that the user has - // reconnected because the timer (of disconnect_delay ms) can fire before - // SockJS processes the new connection. Eventually we'll fix this by not - // combining CPU-heavy processing with SockJS termination (eg a proxy which - // converts to Unix sockets) but for now, raise the delay. - disconnect_delay: 60 * 1000, - // Allow disabling of CORS requests to address - // https://github.com/meteor/meteor/issues/8317. - disable_cors: !!process.env.DISABLE_SOCKJS_CORS, - // Set the USE_JSESSIONID environment variable to enable setting the - // JSESSIONID cookie. This is useful for setting up proxies with - // session affinity. - jsessionid: !!process.env.USE_JSESSIONID - }; - - // If you know your server environment (eg, proxies) will prevent websockets - // from ever working, set $DISABLE_WEBSOCKETS and SockJS clients (ie, - // browsers) will not waste time attempting to use them. - // (Your server will still have a /websocket endpoint.) - if (process.env.DISABLE_WEBSOCKETS) { - serverOptions.websocket = false; + if (disableSockJS) { + self._setupRawWebSocket(); } else { - serverOptions.faye_server_options = { - extensions: websocketExtensions() - }; + self._setupSockJS(); } +}; - self.server = sockjs.createServer(serverOptions); +Object.assign(StreamServer.prototype, { + // Shared connection handler used by both SockJS and raw WebSocket paths. + _onConnection(socket) { + var self = this; - // Install the sockjs handlers, but we want to keep around our own particular - // request handler that adjusts idle timeouts while we have an outstanding - // request. This compensates for the fact that sockjs removes all listeners - // for "request" to add its own. - WebApp.httpServer.removeListener( - 'request', WebApp._timeoutAdjustmentRequestCallback); - self.server.installHandlers(WebApp.httpServer); - WebApp.httpServer.addListener( - 'request', WebApp._timeoutAdjustmentRequestCallback); - - // Support the /websocket endpoint - self._redirectWebsocketEndpoint(); - - self.server.on('connection', function (socket) { // sockjs sometimes passes us null instead of a socket object // so we need to guard against that. see: // https://github.com/sockjs/sockjs-node/issues/121 @@ -112,18 +138,23 @@ StreamServer = function () { // by explicitly setting the socket timeout to a relatively large time here, // and setting it back to zero when we set up the heartbeat in // livedata_server.js. - socket.setWebsocketTimeout = function (timeout) { - if ((socket.protocol === 'websocket' || - socket.protocol === 'websocket-raw') - && socket._session.recv) { - socket._session.recv.connection.setTimeout(timeout); - } - }; + if (!socket.setWebsocketTimeout) { + socket.setWebsocketTimeout = function (timeout) { + if ((socket.protocol === 'websocket' || + socket.protocol === 'websocket-raw') + && socket._session.recv) { + socket._session.recv.connection.setTimeout(timeout); + } + }; + } socket.setWebsocketTimeout(45 * 1000); - socket.send = function (data) { - socket.write(data); - }; + if (!socket.send) { + socket.send = function (data) { + socket.write(data); + }; + } + socket.on('close', function () { self.open_sockets = self.open_sockets.filter(function(value) { return value !== socket; @@ -142,11 +173,124 @@ StreamServer = function () { self.registration_callbacks.forEach(function (callback) { callback(socket); }); - }); + }, -}; + // Set up the traditional SockJS transport (default when DISABLE_SOCKJS is + // not set). This is the original code path, moved here verbatim. + _setupSockJS() { + var self = this; + + // Because we are installing directly onto WebApp.httpServer instead of using + // WebApp.app, we have to process the path prefix ourselves. + self.prefix = pathPrefix + '/sockjs'; + RoutePolicy.declare(self.prefix + '/', 'network'); + + // set up sockjs + var sockjs = Npm.require('sockjs'); + var serverOptions = { + prefix: self.prefix, + log: function() {}, + // this is the default, but we code it explicitly because we depend + // on it in stream_client:HEARTBEAT_TIMEOUT + heartbeat_delay: 45000, + // The default disconnect_delay is 5 seconds, but if the server ends up CPU + // bound for that much time, SockJS might not notice that the user has + // reconnected because the timer (of disconnect_delay ms) can fire before + // SockJS processes the new connection. Eventually we'll fix this by not + // combining CPU-heavy processing with SockJS termination (eg a proxy which + // converts to Unix sockets) but for now, raise the delay. + disconnect_delay: 60 * 1000, + // Allow disabling of CORS requests to address + // https://github.com/meteor/meteor/issues/8317. + disable_cors: !!process.env.DISABLE_SOCKJS_CORS, + // Set the USE_JSESSIONID environment variable to enable setting the + // JSESSIONID cookie. This is useful for setting up proxies with + // session affinity. + jsessionid: !!process.env.USE_JSESSIONID + }; + + // If you know your server environment (eg, proxies) will prevent websockets + // from ever working, set $DISABLE_WEBSOCKETS and SockJS clients (ie, + // browsers) will not waste time attempting to use them. + // (Your server will still have a /websocket endpoint.) + if (process.env.DISABLE_WEBSOCKETS) { + serverOptions.websocket = false; + } else { + serverOptions.faye_server_options = { + extensions: websocketExtensions() + }; + } + + self.server = sockjs.createServer(serverOptions); + + // Install the sockjs handlers, but we want to keep around our own particular + // request handler that adjusts idle timeouts while we have an outstanding + // request. This compensates for the fact that sockjs removes all listeners + // for "request" to add its own. + WebApp.httpServer.removeListener( + 'request', WebApp._timeoutAdjustmentRequestCallback); + self.server.installHandlers(WebApp.httpServer); + WebApp.httpServer.addListener( + 'request', WebApp._timeoutAdjustmentRequestCallback); + + // Support the /websocket endpoint + self._redirectWebsocketEndpoint(); + + self.server.on('connection', function (socket) { + self._onConnection(socket); + }); + }, + + // Set up raw WebSocket transport (when DISABLE_SOCKJS=1). No SockJS server, + // no polling transports, no /sockjs/info endpoint. Direct WebSocket only. + _setupRawWebSocket() { + var self = this; + var FayeWebSocket = Npm.require('faye-websocket'); + + RoutePolicy.declare(pathPrefix + '/websocket/', 'network'); + + // Reject plain HTTP requests to /websocket with a clear error message + // (same behavior as SockJS). Without this, they'd fall through to the + // app and return the main HTML page. + WebApp.rawConnectHandlers.use(function (req, res, next) { + var pathname = new URL(req.url, 'http://localhost').pathname; + if (pathname === pathPrefix + '/websocket' || + pathname === pathPrefix + '/websocket/') { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Not a valid websocket request'); + } else { + next(); + } + }); + + // We must take over existing 'upgrade' listeners (similar to what SockJS + // does via overshadowListeners) so that our handler runs first for the + // /websocket path, and other handlers (HMR, etc.) get the rest. + var httpServer = WebApp.httpServer; + var oldUpgradeListeners = httpServer.listeners('upgrade').slice(0); + httpServer.removeAllListeners('upgrade'); + + httpServer.on('upgrade', function (req, rawSocket, head) { + // req.url is a relative path — new URL() requires a base to parse it. + var pathname = new URL(req.url, 'http://localhost').pathname; + + if (FayeWebSocket.isWebSocket(req) && + (pathname === pathPrefix + '/websocket' || + pathname === pathPrefix + '/websocket/')) { + + var wsOptions = { extensions: websocketExtensions() }; + var ws = new FayeWebSocket(req, rawSocket, head, null, wsOptions); + var meteorSocket = new RawWebSocketConnection(ws, req, rawSocket); + self._onConnection(meteorSocket); + } else { + // Pass to other upgrade handlers (HMR, etc.) + for (var i = 0; i < oldUpgradeListeners.length; i++) { + oldUpgradeListeners[i].call(httpServer, req, rawSocket, head); + } + } + }); + }, -Object.assign(StreamServer.prototype, { // call my callback when a new socket connects. // also call it for all current connections. register: function (callback) { @@ -201,4 +345,4 @@ Object.assign(StreamServer.prototype, { httpServer.addListener(event, newListener); }); } -}); \ No newline at end of file +}); diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 76ef364651..32926abb66 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -2,7 +2,7 @@ import { isFunction, isObject, keysOf, - lengthOf, + lengthOfWithLimit, hasOwn, convertMapToObject, isArguments, @@ -98,7 +98,7 @@ EJSON.addType = (name, factory) => { const builtinConverters = [ { // Date matchJSONValue(obj) { - return hasOwn(obj, '$date') && lengthOf(obj) === 1; + return hasOwn(obj, '$date') && lengthOfWithLimit(obj, 1) === 1; }, matchObject(obj) { return obj instanceof Date; @@ -114,7 +114,7 @@ const builtinConverters = [ matchJSONValue(obj) { return hasOwn(obj, '$regexp') && hasOwn(obj, '$flags') - && lengthOf(obj) === 2; + && lengthOfWithLimit(obj, 2) === 2; }, matchObject(obj) { return obj instanceof RegExp; @@ -140,7 +140,7 @@ const builtinConverters = [ { // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object' // which we match.) matchJSONValue(obj) { - return hasOwn(obj, '$InfNaN') && lengthOf(obj) === 1; + return hasOwn(obj, '$InfNaN') && lengthOfWithLimit(obj, 1) === 1; }, matchObject: isInfOrNaN, toJSONValue(obj) { @@ -160,7 +160,7 @@ const builtinConverters = [ }, { // Binary matchJSONValue(obj) { - return hasOwn(obj, '$binary') && lengthOf(obj) === 1; + return hasOwn(obj, '$binary') && lengthOfWithLimit(obj, 1) === 1; }, matchObject(obj) { return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array @@ -175,12 +175,12 @@ const builtinConverters = [ }, { // Escaping one level matchJSONValue(obj) { - return hasOwn(obj, '$escape') && lengthOf(obj) === 1; + return hasOwn(obj, '$escape') && lengthOfWithLimit(obj, 1) === 1; }, matchObject(obj) { let match = false; if (obj) { - const keyCount = lengthOf(obj); + const keyCount = lengthOfWithLimit(obj, 2); if (keyCount === 1 || keyCount === 2) { match = builtinConverters.some(converter => converter.matchJSONValue(obj)); @@ -206,7 +206,7 @@ const builtinConverters = [ { // Custom matchJSONValue(obj) { return hasOwn(obj, '$type') - && hasOwn(obj, '$value') && lengthOf(obj) === 2; + && hasOwn(obj, '$value') && lengthOfWithLimit(obj, 2) === 2; }, matchObject(obj) { return EJSON._isCustomType(obj); @@ -288,25 +288,72 @@ const adjustTypesToJSONValue = obj => { EJSON._adjustTypesToJSONValue = adjustTypesToJSONValue; +// Copy-on-write recursive EJSON→JSON converter. +// Only allocates new objects/arrays along paths that actually change, +// returning the original reference when nothing needs conversion. +const toJSONValueDeep = value => { + if (value === null || value === undefined) { + return value; + } + + // Atom-level conversion (Date, Binary, custom types, etc.) + const replaced = toJSONValueHelper(value); + if (replaced !== undefined) { + return replaced; + } + + // Primitives that aren't Inf/NaN pass through unchanged. + if (typeof value !== 'object') { + // Inf/NaN are the only non-object values that need conversion, + // and toJSONValueHelper already handled them. + return value; + } + + const isArray = Array.isArray(value); + let result = null; // stays null until first change detected + + if (isArray) { + for (let i = 0; i < value.length; i++) { + const child = value[i]; + const converted = toJSONValueDeep(child); + if (converted !== child) { + result ??= value.slice(0, i); + result.push(converted); + } else if (result !== null) { + result.push(child); + } + } + } else { + const keys = keysOf(value); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const child = value[key]; + const converted = toJSONValueDeep(child); + if (converted !== child) { + if (result === null) { + result = {}; + // backfill preceding keys + for (let j = 0; j < i; j++) { + result[keys[j]] = value[keys[j]]; + } + } + result[key] = converted; + } else if (result !== null) { + result[key] = child; + } + } + } + + return result ?? value; +}; + /** * @summary Serialize an EJSON-compatible value into its plain JSON * representation. * @locus Anywhere * @param {EJSON} val A value to serialize to plain JSON. */ -EJSON.toJSONValue = item => { - const changed = toJSONValueHelper(item); - if (changed !== undefined) { - return changed; - } - - let newItem = item; - if (isObject(item)) { - newItem = EJSON.clone(item); - adjustTypesToJSONValue(newItem); - } - return newItem; -}; +EJSON.toJSONValue = item => toJSONValueDeep(item); // Either return the argument changed to have the non-json // rep of itself (the Object version) or the argument itself. @@ -364,19 +411,62 @@ const adjustTypesFromJSONValue = obj => { EJSON._adjustTypesFromJSONValue = adjustTypesFromJSONValue; +// Copy-on-write recursive JSON→EJSON converter. +// Same lazy-allocation strategy as toJSONValueDeep. +const fromJSONValueDeep = value => { + if (value === null || typeof value !== 'object') { + return value; + } + + // Check if this value itself is a JSON-encoded EJSON type (e.g. {$date: ...}) + const replaced = fromJSONValueHelper(value); + if (replaced !== value) { + return replaced; + } + + const isArray = Array.isArray(value); + let result = null; + + if (isArray) { + for (let i = 0; i < value.length; i++) { + const child = value[i]; + const converted = fromJSONValueDeep(child); + if (converted !== child) { + result ??= value.slice(0, i); + result.push(converted); + } else if (result !== null) { + result.push(child); + } + } + } else { + const keys = keysOf(value); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const child = value[key]; + const converted = fromJSONValueDeep(child); + if (converted !== child) { + if (result === null) { + result = {}; + for (let j = 0; j < i; j++) { + result[keys[j]] = value[keys[j]]; + } + } + result[key] = converted; + } else if (result !== null) { + result[key] = child; + } + } + } + + return result ?? value; +}; + /** * @summary Deserialize an EJSON value from its plain JSON representation. * @locus Anywhere * @param {JSONCompatible} val A value to deserialize into EJSON. */ -EJSON.fromJSONValue = item => { - let changed = fromJSONValueHelper(item); - if (changed === item && isObject(item)) { - changed = EJSON.clone(item); - adjustTypesFromJSONValue(changed); - } - return changed; -}; +EJSON.fromJSONValue = item => fromJSONValueDeep(item); /** * @summary Serialize a value to a string. For EJSON values, the serialization @@ -447,18 +537,21 @@ EJSON.equals = (a, b, options) => { return true; } - // This differs from the IEEE spec for NaN equality, b/c we don't want - // anything ever with a NaN to be poisoned from becoming equal to anything. - if (Number.isNaN(a) && Number.isNaN(b)) { - return true; - } - - // if either one is falsy, they'd have to be === to be equal - if (!a || !b) { + // If types differ, they can't be equal. + // This also handles mixed null/primitive cases since typeof null is 'object'. + if (typeof a !== typeof b) { return false; } - if (!(isObject(a) && isObject(b))) { + // Same-type primitives that aren't === can only be equal if both are NaN. + // This skips the NaN check entirely for strings, booleans, etc. + if (typeof a !== 'object') { + return Number.isNaN(a) && Number.isNaN(b); + } + + // Both are typeof 'object' — but either could be null. + // (If both were null, a === b would have caught it above.) + if (a === null || b === null) { return false; } @@ -518,6 +611,9 @@ EJSON.equals = (a, b, options) => { let ret; const aKeys = keysOf(a); const bKeys = keysOf(b); + if (aKeys.length !== bKeys.length) { + return false; + } if (keyOrderSensitive) { i = 0; ret = aKeys.every(key => { diff --git a/packages/ejson/ejson_tests.js b/packages/ejson/ejson_tests.js index aaad7aa103..ae9c559dcf 100644 --- a/packages/ejson/ejson_tests.js +++ b/packages/ejson/ejson_tests.js @@ -63,6 +63,109 @@ Tinytest.add('ejson - equality and falsiness', test => { test.isFalse(EJSON.equals({foo: 'foo'}, undefined)); }); +Tinytest.add('ejson - equals type-mismatch early exit', test => { + // Cross-type primitives: typeof a !== typeof b → false + test.isFalse(EJSON.equals('hello', 42)); + test.isFalse(EJSON.equals(42, 'hello')); + test.isFalse(EJSON.equals(1, true)); + test.isFalse(EJSON.equals(true, 1)); + test.isFalse(EJSON.equals('true', true)); + test.isFalse(EJSON.equals(true, 'true')); + test.isFalse(EJSON.equals('1', 1)); + test.isFalse(EJSON.equals(1, '1')); + + // Falsy cross-type: both are falsy but different types + test.isFalse(EJSON.equals(0, false)); + test.isFalse(EJSON.equals(false, 0)); + test.isFalse(EJSON.equals('', 0)); + test.isFalse(EJSON.equals(0, '')); + test.isFalse(EJSON.equals('', false)); + test.isFalse(EJSON.equals(false, '')); + + // null/undefined vs primitives (typeof null is 'object', differs from 'number'/'string') + test.isFalse(EJSON.equals(null, 0)); + test.isFalse(EJSON.equals(0, null)); + test.isFalse(EJSON.equals(null, '')); + test.isFalse(EJSON.equals('', null)); + test.isFalse(EJSON.equals(null, false)); + test.isFalse(EJSON.equals(false, null)); + test.isFalse(EJSON.equals(undefined, 0)); + test.isFalse(EJSON.equals(0, undefined)); + test.isFalse(EJSON.equals(undefined, '')); + test.isFalse(EJSON.equals('', undefined)); + test.isFalse(EJSON.equals(undefined, false)); + test.isFalse(EJSON.equals(false, undefined)); + test.isFalse(EJSON.equals(null, undefined)); + test.isFalse(EJSON.equals(undefined, null)); +}); + +Tinytest.add('ejson - equals same-type primitives', test => { + // Same-type, same-value → caught by a === b + test.isTrue(EJSON.equals(0, 0)); + test.isTrue(EJSON.equals(1, 1)); + test.isTrue(EJSON.equals(-1, -1)); + test.isTrue(EJSON.equals('', '')); + test.isTrue(EJSON.equals('hello', 'hello')); + test.isTrue(EJSON.equals(true, true)); + test.isTrue(EJSON.equals(false, false)); + + // Same-type, different-value → typeof a !== 'object', then NaN check returns false + test.isFalse(EJSON.equals(1, 2)); + test.isFalse(EJSON.equals('a', 'b')); + test.isFalse(EJSON.equals(true, false)); + test.isFalse(EJSON.equals(false, true)); + test.isFalse(EJSON.equals(0, 1)); + test.isTrue(EJSON.equals(0, -0)); // 0 === -0 in JS, caught by a === b +}); + +Tinytest.add('ejson - equals null vs object', test => { + // Both typeof 'object', but one is null + test.isFalse(EJSON.equals(null, {})); + test.isFalse(EJSON.equals({}, null)); + test.isFalse(EJSON.equals(null, [])); + test.isFalse(EJSON.equals([], null)); + test.isFalse(EJSON.equals(null, new Date())); + test.isFalse(EJSON.equals(new Date(), null)); +}); + +Tinytest.add('ejson - equals nested falsy and type-mismatch fields', test => { + // Objects with falsy fields of different types + test.isFalse(EJSON.equals({a: 0}, {a: false})); + test.isFalse(EJSON.equals({a: ''}, {a: 0})); + test.isFalse(EJSON.equals({a: ''}, {a: false})); + test.isFalse(EJSON.equals({a: null}, {a: undefined})); + test.isFalse(EJSON.equals({a: null}, {a: 0})); + test.isFalse(EJSON.equals({a: null}, {a: ''})); + test.isFalse(EJSON.equals({a: null}, {a: false})); + + // Objects with same falsy values should be equal + test.isTrue(EJSON.equals({a: 0}, {a: 0})); + test.isTrue(EJSON.equals({a: ''}, {a: ''})); + test.isTrue(EJSON.equals({a: false}, {a: false})); + test.isTrue(EJSON.equals({a: null}, {a: null})); + test.isTrue(EJSON.equals({a: undefined}, {a: undefined})); + + // Deeply nested type mismatches + test.isFalse(EJSON.equals( + {a: {b: {c: 0}}}, + {a: {b: {c: false}}} + )); + test.isFalse(EJSON.equals( + {a: {b: {c: null}}}, + {a: {b: {c: undefined}}} + )); + test.isTrue(EJSON.equals( + {a: {b: {c: 0}}}, + {a: {b: {c: 0}}} + )); + + // Arrays with type-mismatched elements + test.isFalse(EJSON.equals([0, 1, 2], [false, 1, 2])); + test.isFalse(EJSON.equals([0, '', 2], [0, false, 2])); + test.isFalse(EJSON.equals([null], [undefined])); + test.isTrue(EJSON.equals([0, '', null], [0, '', null])); +}); + Tinytest.add('ejson - NaN and Inf', test => { test.equal(EJSON.parse('{"$InfNaN": 1}'), Infinity); test.equal(EJSON.parse('{"$InfNaN": -1}'), -Infinity); @@ -88,6 +191,200 @@ Tinytest.add('ejson - NaN and Inf', test => { )); }); +Tinytest.add('ejson - toJSONValue primitives pass through unchanged', test => { + test.equal(EJSON.toJSONValue(42), 42); + test.equal(EJSON.toJSONValue('hello'), 'hello'); + test.equal(EJSON.toJSONValue(true), true); + test.equal(EJSON.toJSONValue(false), false); + test.equal(EJSON.toJSONValue(null), null); + test.equal(EJSON.toJSONValue(undefined), undefined); + test.equal(EJSON.toJSONValue(0), 0); + test.equal(EJSON.toJSONValue(''), ''); +}); + +Tinytest.add('ejson - toJSONValue converts Date to {$date}', test => { + const d = new Date('2024-06-15T12:00:00Z'); + const result = EJSON.toJSONValue(d); + test.equal(result, {$date: d.getTime()}); +}); + +Tinytest.add('ejson - toJSONValue converts NaN and Infinity', test => { + test.equal(EJSON.toJSONValue(NaN), {$InfNaN: 0}); + test.equal(EJSON.toJSONValue(Infinity), {$InfNaN: 1}); + test.equal(EJSON.toJSONValue(-Infinity), {$InfNaN: -1}); +}); + +Tinytest.add('ejson - toJSONValue handles pure-primitive objects', test => { + const obj = {a: 1, b: 'hello', c: true, d: null}; + const result = EJSON.toJSONValue(obj); + test.equal(result, {a: 1, b: 'hello', c: true, d: null}); +}); + +Tinytest.add('ejson - toJSONValue converts nested Dates', test => { + const d = new Date('2024-01-01'); + const obj = {name: 'test', createdAt: d, meta: {updatedAt: d}}; + const result = EJSON.toJSONValue(obj); + test.equal(result.name, 'test'); + test.equal(result.createdAt, {$date: d.getTime()}); + test.equal(result.meta, {updatedAt: {$date: d.getTime()}}); +}); + +Tinytest.add('ejson - toJSONValue handles arrays', test => { + // Pure-primitive array + const arr = [1, 'two', true, null]; + const result = EJSON.toJSONValue(arr); + test.equal(result, [1, 'two', true, null]); + + // Array with a Date + const d = new Date(); + const arrWithDate = ['a', d, 'b']; + const result2 = EJSON.toJSONValue(arrWithDate); + test.equal(result2[0], 'a'); + test.equal(result2[1], {$date: d.getTime()}); + test.equal(result2[2], 'b'); + test.length(result2, 3); + + // Empty array + test.equal(EJSON.toJSONValue([]), []); +}); + +Tinytest.add('ejson - toJSONValue handles NaN/Infinity inside objects and arrays', test => { + const obj = {a: 1, b: NaN, c: Infinity, d: -Infinity, e: 'normal'}; + const result = EJSON.toJSONValue(obj); + test.equal(result.a, 1); + test.equal(result.b, {$InfNaN: 0}); + test.equal(result.c, {$InfNaN: 1}); + test.equal(result.d, {$InfNaN: -1}); + test.equal(result.e, 'normal'); + + const arr = [NaN, 42, Infinity]; + const result2 = EJSON.toJSONValue(arr); + test.equal(result2[0], {$InfNaN: 0}); + test.equal(result2[1], 42); + test.equal(result2[2], {$InfNaN: 1}); +}); + +Tinytest.add('ejson - toJSONValue escapes $-prefixed keys that look like EJSON types', test => { + const obj = {$date: 12345}; + const result = EJSON.toJSONValue(obj); + // Should be wrapped in $escape to prevent misinterpretation + test.isTrue('$escape' in result); + test.equal(result.$escape.$date, 12345); +}); + +Tinytest.add('ejson - fromJSONValue primitives pass through unchanged', test => { + test.equal(EJSON.fromJSONValue(42), 42); + test.equal(EJSON.fromJSONValue('hello'), 'hello'); + test.equal(EJSON.fromJSONValue(true), true); + test.equal(EJSON.fromJSONValue(false), false); + test.equal(EJSON.fromJSONValue(null), null); + test.equal(EJSON.fromJSONValue(0), 0); + test.equal(EJSON.fromJSONValue(''), ''); +}); + +Tinytest.add('ejson - fromJSONValue converts {$date} to Date', test => { + const ts = 1718452800000; + const result = EJSON.fromJSONValue({$date: ts}); + test.instanceOf(result, Date); + test.equal(result.getTime(), ts); +}); + +Tinytest.add('ejson - fromJSONValue converts {$InfNaN} back', test => { + test.isTrue(Number.isNaN(EJSON.fromJSONValue({$InfNaN: 0}))); + test.equal(EJSON.fromJSONValue({$InfNaN: 1}), Infinity); + test.equal(EJSON.fromJSONValue({$InfNaN: -1}), -Infinity); +}); + +Tinytest.add('ejson - fromJSONValue handles pure-primitive objects', test => { + const obj = {a: 1, b: 'hello', c: true, d: null}; + const result = EJSON.fromJSONValue(obj); + test.equal(result, {a: 1, b: 'hello', c: true, d: null}); +}); + +Tinytest.add('ejson - fromJSONValue converts nested {$date} values', test => { + const ts = Date.now(); + const obj = {name: 'test', createdAt: {$date: ts}, meta: {updatedAt: {$date: ts}}}; + const result = EJSON.fromJSONValue(obj); + test.equal(result.name, 'test'); + test.instanceOf(result.createdAt, Date); + test.equal(result.createdAt.getTime(), ts); + test.instanceOf(result.meta.updatedAt, Date); + test.equal(result.meta.updatedAt.getTime(), ts); +}); + +Tinytest.add('ejson - fromJSONValue handles arrays with EJSON types', test => { + const ts = Date.now(); + const arr = ['a', {$date: ts}, 'b']; + const result = EJSON.fromJSONValue(arr); + test.equal(result[0], 'a'); + test.instanceOf(result[1], Date); + test.equal(result[1].getTime(), ts); + test.equal(result[2], 'b'); + test.length(result, 3); + + // Pure-primitive array + test.equal(EJSON.fromJSONValue([1, 2, 3]), [1, 2, 3]); + + // Empty array + test.equal(EJSON.fromJSONValue([]), []); +}); + +Tinytest.add('ejson - fromJSONValue unescapes $escape wrapper', test => { + const input = {$escape: {$date: 12345}}; + const result = EJSON.fromJSONValue(input); + test.equal(result, {$date: 12345}); + test.isFalse('$escape' in result); +}); + +Tinytest.add('ejson - toJSONValue/fromJSONValue round-trip', test => { + const d = new Date(); + const cases = [ + 42, + 'hello', + true, + null, + {a: 1, b: 'two'}, + [1, 2, 3], + d, + NaN, + Infinity, + -Infinity, + {name: 'test', ts: d, scores: [1, 2, 3]}, + {nested: {deep: {date: d, val: 42}}}, + [d, 'a', {x: d}], + {$date: 12345}, // $-prefixed key → escape/unescape round-trip + {a: NaN, b: Infinity, c: -Infinity, d: 'normal'}, + {}, // empty object + [], // empty array + ]; + + cases.forEach(original => { + const json = EJSON.toJSONValue(original); + const restored = EJSON.fromJSONValue(json); + test.isTrue( + EJSON.equals(original, restored), + `Round-trip failed for: ${EJSON.stringify(original)}` + ); + }); +}); + +Tinytest.add('ejson - toJSONValue does not mutate the input', test => { + const d = new Date(); + const obj = {name: 'test', createdAt: d, tags: ['a', 'b']}; + const originalName = obj.name; + const originalDate = obj.createdAt; + const originalTags = obj.tags; + + EJSON.toJSONValue(obj); + + // Original object must be untouched + test.equal(obj.name, originalName); + test.equal(obj.createdAt, originalDate); + test.equal(obj.tags, originalTags); + test.instanceOf(obj.createdAt, Date); + test.equal(obj.tags[0], 'a'); +}); + Tinytest.add('ejson - clone', test => { const cloneTest = (x, identical) => { const y = EJSON.clone(x); diff --git a/packages/ejson/package.js b/packages/ejson/package.js index 7cb875555d..e55d05af8f 100644 --- a/packages/ejson/package.js +++ b/packages/ejson/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Extended and Extensible JSON library', - version: '1.1.5', + version: '1.2.0-beta350.7', }); Package.onUse(function onUse(api) { diff --git a/packages/ejson/utils.js b/packages/ejson/utils.js index 6d71313fcd..358cc26c1a 100644 --- a/packages/ejson/utils.js +++ b/packages/ejson/utils.js @@ -4,7 +4,29 @@ export const isObject = (fn) => typeof fn === 'object'; export const keysOf = (obj) => Object.keys(obj); -export const lengthOf = (obj) => Object.keys(obj).length; +export const lengthOf = (obj) => { + let count = 0; + for (const key in obj) { + if (hasOwn(obj, key)) count++; + } + return count; +}; + +/** + * Counts own properties of obj, but stops early once count exceeds limit. + * Useful for hot-path checks like `lengthOfWithLimit(obj, 1) === 1` + * without iterating all keys of large objects. + * @param {Object} obj + * @param {number} limit - stop counting beyond this value + * @returns {number} exact count if <= limit, otherwise limit + 1 + */ +export const lengthOfWithLimit = (obj, limit) => { + let count = 0; + for (const key in obj) { + if (hasOwn(obj, key) && ++count > limit) return count; + } + return count; +}; export const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); diff --git a/packages/email/email.js b/packages/email/email.js index a17b11fcf3..d453f45dee 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -259,6 +259,20 @@ Email.sendAsync = async function (options) { ); } + // Check if 'from' address is still the unconfigured default. + // The default "example.com" domain (RFC 2606) will always fail to send emails + // since no SPF/DKIM/DMARC records can exist for it. + const isDefaultFrom = !email.from || + email.from.includes('@example.com'); + + if (isDefaultFrom) { + console.warn( + '[Email] Warning: "from" address is not configured. ' + + 'Using default "example.com" which will fail in production. ' + + 'Set Accounts.emailTemplates.from to a valid email address.' + ); + } + if (mailUrlEnv || mailUrlSettings) { return getTransport().sendMail(email); } diff --git a/packages/email/package.js b/packages/email/package.js index 5a9560e9a3..bf9c937cb7 100644 --- a/packages/email/package.js +++ b/packages/email/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Send email messages", - version: "3.1.2", + version: "3.2.0-beta350.7", }); Npm.depends({ diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index f009d06cfb..b4e1d0034f 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "The Meteor command-line tool", - version: "3.4.0", + version: '3.5.0-beta.7', }); Package.includeTool(); diff --git a/packages/minimongo/constants.js b/packages/minimongo/constants.js index ac72e9c9a8..66387b2011 100644 --- a/packages/minimongo/constants.js +++ b/packages/minimongo/constants.js @@ -130,7 +130,7 @@ export const ASYNC_CURSOR_METHODS = [ */ 'forEach', /** - * @summary Map callback over all matching documents. Returns an Array. + * @summary Map callback over all matching documents. Returns a Promise. * @locus Anywhere * @method mapAsync * @instance @@ -141,7 +141,7 @@ export const ASYNC_CURSOR_METHODS = [ * itself. * @param {Any} [thisArg] An object which will be the value of `this` inside * `callback`. - * @returns {Promise} + * @returns {Promise} */ 'map', ]; diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 4f554fc1e0..9fd1d82ca4 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -136,25 +136,34 @@ export default class Cursor { * `callback`. */ forEach(callback, thisArg) { - if (this.reactive) { - this._depend({ - addedBefore: true, - removed: true, - changed: true, - movedBefore: true, - }); + let i = 0; + + for (const doc of this) { + callback.call(thisArg, doc, i++, this); } + } - this._getRawObjects({ ordered: true }).forEach((element, i) => { - // This doubles as a clone operation. - element = this._projectionFn(element); + /** + * @summary Call `callback` once for each matching document, sequentially and + * synchronously. + * @locus Anywhere + * @method forEachAsync + * @instance + * @memberOf Mongo.Cursor + * @param {IterationCallback} callback Function to call. It will be called + * with three arguments: the document, a + * 0-based index, and cursor + * itself. + * @param {Any} [thisArg] An object which will be the value of `this` inside + * `callback`. + * @returns {Promise} + */ + async forEachAsync(callback, thisArg) { + let i = 0; - if (this._transform) { - element = this._transform(element); - } - - callback.call(thisArg, element, i, this); - }); + for await (const doc of this) { + await callback.call(thisArg, doc, i++, this); + } } getTransform() { @@ -162,7 +171,7 @@ export default class Cursor { } /** - * @summary Map callback over all matching documents. Returns an Array. + * @summary Map callback over all matching documents. Returns an Array. * @locus Anywhere * @method map * @instance @@ -184,6 +193,30 @@ export default class Cursor { return result; } + /** + * @summary Map callback over all matching documents. Returns a Promise. + * @locus Anywhere + * @method mapAsync + * @instance + * @memberOf Mongo.Cursor + * @param {IterationCallback} callback Function to call. It will be called + * with three arguments: the document, a + * 0-based index, and cursor + * itself. + * @param {Any} [thisArg] An object which will be the value of `this` inside + * `callback`. + * @returns {Promise} + */ + async mapAsync(callback, thisArg) { + const result = []; + + await this.forEachAsync(async (doc, i) => { + result.push(await callback.call(thisArg, doc, i, this)); + }); + + return result; + } + // options to contain: // * callbacks for observe(): // - addedAt (document, atIndex) @@ -564,6 +597,11 @@ export default class Cursor { // Implements async version of cursor methods to keep collections isomorphic ASYNC_CURSOR_METHODS.forEach(method => { const asyncName = getAsyncMethodName(method); + + if (Cursor.prototype[asyncName]) { + return; + } + Cursor.prototype[asyncName] = function(...args) { try { return Promise.resolve(this[method].apply(this, args)); diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index a345d4938c..b4cdb32550 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1720,7 +1720,7 @@ LocalCollection._removeFromResultsSync = (query, doc) => { } else { const id = doc._id; // in case callback mutates doc - query.removed(doc._id); + query.removed(id); query.results.remove(id); } }; @@ -1734,7 +1734,7 @@ LocalCollection._removeFromResultsAsync = async (query, doc) => { } else { const id = doc._id; // in case callback mutates doc - await query.removed(doc._id); + await query.removed(id); query.results.remove(id); } }; diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index c1405552a5..7701e08a94 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -4011,6 +4011,29 @@ Tinytest.addAsync('minimongo - asyncIterator', async (test) => { test.equal(itemIds, ['a', 'b']); }); +['forEachAsync', 'mapAsync'].forEach((methodName) => { + Tinytest.addAsync(`minimongo - awaiting ${methodName} item callbacks`, async (test) => { + const collection = new LocalCollection(); + + collection.insert({ _id: 'a' }); + collection.insert({ _id: 'b' }); + + const result = ['before']; + + await collection.find()[methodName](async function(item) { + await Meteor._sleepForMs(0); + result.push(item._id + '1'); + await Meteor._sleepForMs(0); + result.push(item._id + '2'); + }); + + result.push('after'); + + // Verify that each item callback was awaited correctly, in order + test.equal(result, ['before', 'a1', 'a2', 'b1', 'b2', 'after']); + }); +}); + Tinytest.add('minimongo - operation result fields (sync)', test => { const c = new LocalCollection(); diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 42ffdf3101..cd84aa1470 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", - version: "2.0.5", + version: '2.1.0-beta350.7', }); Package.onUse((api) => { diff --git a/packages/mongo/changestream_observe_driver.js b/packages/mongo/changestream_observe_driver.js new file mode 100644 index 0000000000..967eba22f8 --- /dev/null +++ b/packages/mongo/changestream_observe_driver.js @@ -0,0 +1,586 @@ +import { Meteor } from 'meteor/meteor'; +import { LocalCollection } from 'meteor/minimongo'; +import { Random } from 'meteor/random'; +import { MongoID } from 'meteor/mongo-id'; +import { DDPServer } from 'meteor/ddp-server'; +import { DiffSequence } from 'meteor/diff-sequence'; +import { listenAll } from './mongo_driver'; +import { replaceTypes, replaceMongoAtomWithMeteor } from './mongo_common'; +import { compareOperationTimes } from './mongo_common'; + +const SUPPORTED_OPERATIONS = ['insert', 'update', 'replace', 'delete']; + + +/** + * ChangeStreamObserveDriver - MongoDB Change Streams based observe driver + * + * Uses MongoDB Change Streams to watch for real-time changes to a collection. + * Implements a stop callback system similar to PollingObserveDriver for proper + * resource cleanup when the driver is stopped. + */ +export class ChangeStreamObserveDriver { + constructor(options) { + this._usesChangeStreams = true; + this._cursorDescription = options.cursorDescription; + this._mongoHandle = options.mongoHandle; + this._multiplexer = options.multiplexer; + this._changeStream = null; + this._stopped = false; + this._stopCallbacks = []; + this._pendingWrites = []; + this._writesToCommitWhenReady = []; + this._isReady = false; + this._lastProcessedOperationTime = null; + this._catchingUpResolvers = []; + this._resolveTimeout = null; + this._matcher = options.matcher; + this._id = options.id || Random.id(); + + // Projection function similar to oplog driver + const projection = this._cursorDescription.options.projection || this._cursorDescription.options.fields; + if (projection) { + const baseProjectionFn = LocalCollection._compileProjection(projection); + this._projectionFn = (doc) => { + const projected = baseProjectionFn(doc); + if (projected && typeof projected === 'object') { + const { _id, ...fields } = projected; + return fields; + } + return projected; + }; + } else { + this._projectionFn = (doc) => { + const { _id, ...fields } = doc; + return fields; + }; + } + + this._startListening(); + this._startWatching(); + } + + _sendMultiplexerAdded(id, projectedDoc) { + // Apply EJSON transformation before sending to client + projectedDoc = replaceTypes(projectedDoc, replaceMongoAtomWithMeteor); + try { + this._multiplexer.added(id, projectedDoc); + } catch (error) { + console.error('[ChangeStreams] Error sending added document:', error); + } + } + + async _startListening() { + + // Register a listener to be notified when writes happen + // This follows the same pattern as OplogObserveDriver + const stopHandle = await listenAll( + this._cursorDescription, + () => { + // If we're not in a pre-fire write fence, we don't have to do anything. + const fence = DDPServer._getCurrentFence(); + if (!fence || fence.fired) + return; + + if (fence._changeStreamObserveDrivers) { + fence._changeStreamObserveDrivers[this._id] = this; + return; + } + + fence._changeStreamObserveDrivers = {}; + fence._changeStreamObserveDrivers[this._id] = this; + + fence.onBeforeFire(async () => { + const drivers = fence._changeStreamObserveDrivers; + delete fence._changeStreamObserveDrivers; + + // Process each driver that needs to be synchronized with the fence + for (const driver of Object.values(drivers)) { + if (driver._stopped) continue; + + const write = await fence.beginWrite(); + + // Wait for the change stream to catch up with any pending operations + await driver._waitUntilCaughtUp(); + + // Process any pending writes immediately + driver._flushPendingWrites(); + + // If the driver is ready (initial adds complete), ensure all writes are committed + if (driver._isReady) { + await driver._multiplexer.onFlush(async () => { + await write.committed(); + }); + } else { + // If not ready yet, queue the write for later + driver._writesToCommitWhenReady.push(write); + } + } + }); + } + ); + + // Register the stop handle + this._addStopCallback(() => stopHandle.stop()); + } + + + + _addStopCallback(callback) { + if (typeof callback !== 'function') { + throw new Error('Stop callback must be a function'); + } + this._stopCallbacks.push(callback); + } + + async _startWatching() { + + if (this._stopped) return; + + try { + + const collection = this._mongoHandle.rawCollection(this._cursorDescription.collectionName); + + // First, get all existing documents that match our selector + await this._sendInitialAdds(collection); + + // Signal initial adds are complete (but delay being 'ready' for commits + // until the change stream is attached to avoid fence ordering gaps) + this._multiplexer.ready(); + + // Then start watching for changes + const pipeline = this._buildPipeline(); + + // Create change stream with appropriate options + const changeStreamOptions = { + fullDocument: 'updateLookup', + fullDocumentBeforeChange: 'whenAvailable' + }; + + this._changeStream = collection.watch(pipeline, changeStreamOptions); + + // Register stop callback for the change stream + this._stopCallbacks.push(async () => { + if (this._changeStream) { + try { + await this._changeStream.close(); + } catch (error) { + // Ignore errors when closing + } + this._changeStream = null; + } + }); + + // Handle change events + this._changeStream.on('change', Meteor.bindEnvironment((change) => { + if (this._stopped) return; + // Update last processed op time early so fences can unblock promptly + if (change && change.clusterTime) { + this._setLastProcessedOperationTime(change.clusterTime); + } + this._handleChange(change); + + // Check if we're in a fence + const fence = DDPServer._getCurrentFence(); + if (fence && !fence.fired) { + // Process immediately if we're in a fence + this._flushPendingWrites(); + } else { + // Otherwise defer processing (similar to polling cycle) + Meteor.defer(() => { + if (!this._stopped) { + this._flushPendingWrites(); + } + }); + } + })); + + // Handle errors and reconnection + this._changeStream.on('error', Meteor.bindEnvironment((error) => { + if (this._stopped) return; + console.error('ChangeStream error:', error); + // Attempt to restart after a delay + const timeoutId = setTimeout(() => { + if (!this._stopped) { + this._restartChangeStream(); + } + }, Meteor?.settings?.packages?.mongo?.changeStream?.delay?.error || 100); + + // Register timeout cleanup + this._addStopCallback(() => { + clearTimeout(timeoutId); + }); + })); + + this._changeStream.on('close', Meteor.bindEnvironment(() => { + if (!this._stopped) { + // Unexpected close, attempt restart + const timeoutId = setTimeout(() => { + if (!this._stopped) { + this._restartChangeStream(); + } + }, Meteor?.settings?.packages?.mongo?.changeStream?.delay?.close || 100); + + // Register timeout cleanup + this._addStopCallback(() => { + clearTimeout(timeoutId); + }); + } + })); + + // Now we can allow queued fence writes to commit safely + this._isReady = true; + await this._flushWritesToCommit(); + + // Remove the defer that was calling _flushPendingWrites + + } catch (error) { + console.error('Failed to start ChangeStream:', error); + throw error; + } + } + + async _sendInitialAdds(collection) { + if (this._stopped) return; + + try { + // Build the same selector and options that the cursor would use + const selector = this._cursorDescription.selector || {}; + const options = { ...this._cursorDescription.options }; + + // Find all existing documents + const cursor = collection.find(selector, options); + + // Follow oplog driver pattern: get current fence and store write for later commit + const fence = DDPServer._getCurrentFence(); + if (fence) { + this._writesToCommitWhenReady.push(fence.beginWrite()); + } + + // Send 'added' for each existing document that matches our matcher + let docCount = 0; + for await (const doc of cursor) { + if (this._stopped) return; + const id = typeof doc._id !== 'string' ? new MongoID.ObjectID(doc._id.toHexString()) : doc._id; + const projectedDoc = this._projectionFn ? this._projectionFn(doc) : doc; + this._sendMultiplexerAdded(id, projectedDoc); + docCount++; + } + + // DON'T call ready() or flush here - let _startWatching handle it + + } catch (error) { + console.error('Error sending initial adds for ChangeStream:', error); + throw error; + } + } + + async _restartChangeStream() { + try { + // Close current stream using stop callbacks if they exist + if (this._changeStream) { + // Find and execute the change stream stop callback + const changeStreamCallback = this._stopCallbacks.find(cb => + typeof cb._changeStream === 'function' + ); + if (changeStreamCallback) { + await changeStreamCallback(); + // Remove the old callback since we'll add a new one + this._stopCallbacks = this._stopCallbacks.filter(cb => cb !== changeStreamCallback); + } + } + await this._startWatching(); + } catch (error) { + console.error('Failed to restart ChangeStream:', error); + } + } + + _buildPipeline() { + // For now, use a simple pipeline that watches all operations + // We'll filter using our matcher in _handleChange + const selector = this._cursorDescription.selector; + + if (!selector || Object.keys(selector).length === 0) { + // No selector, watch all changes + return []; + } + + // Simple pipeline that just filters by operation type + // More complex selector filtering will be done in _handleChange + return [ + { + $match: { + operationType: { $in: ['insert', 'update', 'replace', 'delete'] } + } + } + ]; + } + + async _handleChange(change) { + if (this._stopped) return; + + const { operationType, documentKey, fullDocument, fullDocumentBeforeChange, clusterTime } = change; + + if (!SUPPORTED_OPERATIONS.includes(operationType)) { + return; // Ignore unsupported operations + } + + let id = documentKey._id; + if (typeof documentKey._id?.toHexString === 'function') { + id = new MongoID.ObjectID(documentKey._id.toHexString()); + } + + // Update last processed operation time (redundant with early update, but safe) + if (clusterTime) { + this._setLastProcessedOperationTime(clusterTime); + } + + // Store callback to be executed later when fence processes writes + // Don't try to capture fence here - it will be handled in onBeforeFire + const callbackData = { + operationType, + id, + fullDocument, + fullDocumentBeforeChange, + change + }; + + this._pendingWrites.push(callbackData); + } + + _setLastProcessedOperationTime(ts) { + this._lastProcessedOperationTime = ts; + // Resolve any waiters whose target is <= current processed time + while (this._catchingUpResolvers.length > 0) { + const first = this._catchingUpResolvers[0]; + if (compareOperationTimes(ts, first.ts) >= 0) { + this._catchingUpResolvers.shift(); + try { first.resolver(); } catch (e) { /* ignore resolver errors */ } + } else { + break; + } + } + } + + async _getServerOperationTime() { + const db = this._mongoHandle.db; + const admin = db.admin(); + + const commands = [ + () => db.command({ ping: 1 }), + () => admin.command({ hello: 1 }), + () => admin.command({ ismaster: 1 }) + ]; + + const runCommandRecursive = async (index = 0) => { + if (index >= commands.length) { + return null; + } + + try { + const res = await commands[index](); + return res?.operationTime || res?.$clusterTime?.clusterTime || null; + } catch (error) { + if (!error) { + return false; + } + + // CommandNotFound https://www.mongodb.com/pt-br/docs/manual/reference/error-codes/ + const isUnsupportedCommandError = error.code === 59; + if (isUnsupportedCommandError) { + return runCommandRecursive(index + 1); + } + throw error; + } + }; + + try { + return await runCommandRecursive(); + } catch (error) { + console.error(`[ChangeStream ${this._id}] Failed to fetch server operation time:`, error); + return null; + } + } + + async _flushPendingWrites() { + const callbacksToFlush = this._pendingWrites; + this._pendingWrites = []; + + if (callbacksToFlush.length > 0) { + for (const callbackData of callbacksToFlush) { + try { + const { operationType, id, fullDocument, fullDocumentBeforeChange, change } = callbackData; + + switch (operationType) { + case 'insert': + this._handleInsert(id, fullDocument); + break; + case 'update': + case 'replace': + this._handleUpdate(id, fullDocument, fullDocumentBeforeChange); + break; + case 'delete': + this._handleDelete(id, change); + break; + } + } catch (error) { + console.error(`[ChangeStream ${this._id}] Error processing callback:`, error); + } + } + } + } + + async _flushWritesToCommit() { + // Similar to oplog driver's _beSteady method + const writes = this._writesToCommitWhenReady; + this._writesToCommitWhenReady = []; + + if (writes.length > 0) { + await this._multiplexer.onFlush(async () => { + for (const write of writes) { + await write.committed(); + } + }); + } + } + + _handleInsert(id, doc) { + // Apply projection and check if document matches our criteria + const matches = this._matcher.documentMatches(doc).result; + if (matches) { + const projectedDoc = this._projectionFn ? this._projectionFn(doc) : doc; + this._sendMultiplexerAdded(id, projectedDoc); + } + } + + _handleUpdate(id, newDoc, oldDoc) { + // Determine which state (before/after) matches the cursor selector + const matchesAfter = this._matcher.documentMatches(newDoc || {}).result; + + // Use the multiplexer cache (now updated synchronously) to check if we've seen this doc + const cachedDoc = this._multiplexer?._cache?.docs.get(id); + const matchesBefore = oldDoc + ? (this._matcher.documentMatches(oldDoc).result) + : !!cachedDoc; + + if (matchesAfter) { + if (!matchesBefore) { + // Document wasn't previously in the result set and now matches – emit added + const projectedDoc = this._projectionFn ? this._projectionFn(newDoc) : newDoc; + this._sendMultiplexerAdded(id, projectedDoc); + return; + } + + if (newDoc) { + // Compute the changed fields using the available pre-image or the cached doc + const oldDocForDiff = oldDoc || (cachedDoc ? { ...cachedDoc } : null); + if (oldDocForDiff) { + const projectedNew = this._projectionFn ? this._projectionFn(newDoc) : newDoc; + const projectedOld = this._projectionFn ? this._projectionFn(oldDocForDiff) : oldDocForDiff; + const changedFields = DiffSequence.makeChangedFields(projectedNew, projectedOld); + + if (Object.keys(changedFields).length > 0) { + const transformedDoc = replaceTypes(changedFields, replaceMongoAtomWithMeteor); + this._multiplexer.changed(id, transformedDoc); + } + return; + } + + // Without a pre-image we can't diff reliably; fall back to sending full doc + const projectedDoc = this._projectionFn ? this._projectionFn(newDoc) : newDoc; + const transformedDoc = replaceTypes(projectedDoc, replaceMongoAtomWithMeteor); + this._multiplexer.changed(id, transformedDoc); + } + return; + } + + if (matchesBefore) { + // Document left the result set + this._multiplexer.removed(id); + } + // Otherwise the document didn't match before or after, so no-op + } + + _handleDelete(id) { + if (this._multiplexer._cache?.docs.has(id)) { + this._multiplexer.removed(id); + } + } + + async _waitUntilCaughtUp() { + // Wait until our change stream has processed events up to the + // server's current operation time. Mirrors oplog's wait logic. + if (this._stopped) return; + + const targetTs = await this._getServerOperationTime(); + if (!targetTs) { + // Best-effort fallback: yield to I/O but don't artificially delay + await new Promise((r) => setImmediate(r)); + return; + } + + if (this._lastProcessedOperationTime && compareOperationTimes(this._lastProcessedOperationTime, targetTs) >= 0) { + return; + } + + // Insert in order so we can resolve from the front efficiently + let insertIdx = this._catchingUpResolvers.length; + while (insertIdx - 1 >= 0 && compareOperationTimes(this._catchingUpResolvers[insertIdx - 1]?.ts, targetTs) > 0) { + insertIdx--; + } + + // Wait with an upper bound: release if it takes too long + let timeoutId = null; + const entry = { ts: targetTs, resolver: null }; + + const timeoutMs = Meteor?.settings?.packages?.mongo?.changeStream?.waitUntilCaughtUpTimeoutMs ?? 1000; + + await new Promise((resolve) => { + entry.resolver = () => { + if (timeoutId) clearTimeout(timeoutId); + resolve(); + }; + + // Insert our entry to be resolved when we process >= targetTs + this._catchingUpResolvers.splice(insertIdx, 0, entry); + + // Safety valve: if it takes more than timeoutMs, just release + timeoutId = setTimeout(() => { + // Remove our entry if still pending + const idx = this._catchingUpResolvers.indexOf(entry); + if (idx !== -1) this._catchingUpResolvers.splice(idx, 1); + resolve(); + }, timeoutMs); + }); + } + + async stop() { + if (this._stopped) return; + + this._stopped = true; + + // Execute all stop callbacks + for (const callback of this._stopCallbacks) { + try { + await callback(); + } catch (error) { + console.error('Error in stop callback:', error); + } + } + + // Handle any remaining pending writes (following oplog driver pattern) + for (const write of this._pendingWrites) { + if(!write || typeof write.committed !== 'function') continue; + await write.committed(); + } + this._pendingWrites = []; + + // Handle any remaining writes to commit + for (const write of this._writesToCommitWhenReady) { + await write.committed(); + } + this._writesToCommitWhenReady = []; + + // Clear callbacks array + this._stopCallbacks = []; + } +} diff --git a/packages/mongo/mongo_common.js b/packages/mongo/mongo_common.js index ee5c575443..28b5a397ba 100644 --- a/packages/mongo/mongo_common.js +++ b/packages/mongo/mongo_common.js @@ -168,3 +168,34 @@ export function replaceNames(filter, thing) { } return thing; } + + +/** + * Compares two MongoDB operation times. + * @param {MongoDB.Timestamp|object} opTime1 - The first operation time to compare. + * @param {MongoDB.Timestamp|object} opTime2 - The second operation time to compare. + * @returns {number} - Returns a number indicating the comparison result: + * - A negative number if opTime1 is less than opTime2. + * - Zero if opTime1 is equal to opTime2. + * - A positive number if opTime1 is greater than opTime2. + */ +/** + * Compares two MongoDB operation times (opTimes). + * + * Both parameters accept any value accepted by the `MongoDB.Timestamp` constructor: + * - a `Long` (e.g., `new Timestamp(Long)`), + * - an object of the form `{ t: number, i: number }`, + * - or the legacy two-number form `low, high` (via `Timestamp(low, high)`), which is deprecated; + * prefer `{ t, i }` or a `Long`. + * + * The function constructs a `MongoDB.Timestamp` from `opTime1` and compares it to `opTime2` + * using `Timestamp#compare`. + * + * @param {MongoDB.Long|{t:number,i:number}|Array|number} opTime1 - Operation time 1; any value accepted by `MongoDB.Timestamp`. + * For the two-number form you may provide an array `[low, high]`, but passing two separate numbers to the constructor is deprecated. + * @param {MongoDB.Long|{t:number,i:number}|Array|number} opTime2 - Operation time 2; same accepted forms as `opTime1`. + * @returns {number} Comparison result: negative if `opTime1` < `opTime2`, zero if equal, positive if `opTime1` > `opTime2`. + */ +export function compareOperationTimes(opTime1, opTime2) { + return (new MongoDB.Timestamp(opTime1)).compare(opTime2); +} diff --git a/packages/mongo/mongo_connection.js b/packages/mongo/mongo_connection.js index e5bd3f5160..4f51acbb4a 100644 --- a/packages/mongo/mongo_connection.js +++ b/packages/mongo/mongo_connection.js @@ -12,12 +12,24 @@ import { ObserveMultiplexer } from './observe_multiplex'; import { OplogObserveDriver } from './oplog_observe_driver'; import { OPLOG_COLLECTION, OplogHandle } from './oplog_tailing'; import { PollingObserveDriver } from './polling_observe_driver'; +import { ChangeStreamObserveDriver } from './changestream_observe_driver'; const FILE_ASSET_SUFFIX = 'Asset'; const ASSETS_FOLDER = 'assets'; const APP_FOLDER = 'app'; const oplogCollectionWarnings = []; +const availableDrivers = ['changeStreams', 'oplog', 'polling'] +const DEFAULT_REACTIVITY_ORDER = process.env.METEOR_REACTIVITY_ORDER ? process.env.METEOR_REACTIVITY_ORDER.split(',') : availableDrivers; + +const reactivitySetting = Meteor.settings?.packages?.mongo?.reactivity; +if (Array.isArray(reactivitySetting)) { + for (const method of reactivitySetting) { + if (!availableDrivers.includes(method)) { + throw new Error(`Invalid Mongo reactivity method in settings: ${method}`); + } + } +} export const MongoConnection = function (url, options) { var self = this; @@ -35,7 +47,6 @@ export const MongoConnection = function (url, options) { }, userOptions); - // Internally the oplog connections specify their own maxPoolSize // which we don't want to overwrite with any user defined value if ('maxPoolSize' in options) { @@ -89,7 +100,6 @@ export const MongoConnection = function (url, options) { self._oplogHandle = new OplogHandle(options.oplogUrl, self.db.databaseName); self._docFetcher = new DocFetcher(self); } - }; MongoConnection.prototype._close = async function() { @@ -801,22 +811,96 @@ MongoConnection.prototype.tail = function (cursorDescription, docCallback, timeo }; }; -Object.assign(MongoConnection.prototype, { - _observeChanges: async function ( +const driverClasses = { + changeStreams: ChangeStreamObserveDriver, + oplog: OplogObserveDriver, + polling: PollingObserveDriver, +}; + +function _getConfiguredReactivityOrder () { + const reactivitySetting = Meteor.settings?.packages?.mongo?.reactivity; + const isArraySetting = Array.isArray(reactivitySetting); + const isStringSetting = typeof reactivitySetting === 'string'; + const hasCustomDriverOrder = isArraySetting || isStringSetting; + + if (reactivitySetting && !hasCustomDriverOrder) { + throw new Error('Meteor.settings.packages.mongo.reactivity must be a string or an array of observer drivers'); + } + + let configuredOrder = DEFAULT_REACTIVITY_ORDER; + if (hasCustomDriverOrder) { + if (isStringSetting) { + configuredOrder = [reactivitySetting]; + } else { + configuredOrder = []; + for (const name of reactivitySetting) { + if (!configuredOrder.includes(name)) { + configuredOrder.push(name); + } + } + } + } + + const invalidDriverNames = configuredOrder.filter(name => !driverClasses[name]); + if (invalidDriverNames.length) { + throw new Error(`Invalid Mongo reactivity driver(s): ${invalidDriverNames.join(', ')}`); + } + + if (hasCustomDriverOrder && configuredOrder.length === 0) { + throw new Error('Meteor.settings.packages.mongo.reactivity must specify at least one observer driver'); + } + + return configuredOrder; +}; + +MongoConnection.prototype._selectReactivityDriver = async function (configuredOrder, driverChecks) { + const availabilityErrors = []; + let driverClass; + let matcher; + let sorter; + + for (const driverName of configuredOrder) { + const checker = driverChecks[driverName]; + + if (!checker) { + availabilityErrors.push(`Unknown driver "${driverName}"`); + continue; + } + + const result = await checker(); + + if (result.available) { + matcher = result.matcher; + sorter = result.sorter; + driverClass = driverClasses[driverName]; + break; + } + + if (result.reason) { + availabilityErrors.push(`${driverName}: ${result.reason}`); + } + } + + return { + driverClass, + matcher, + sorter, + }; +}; + +MongoConnection.prototype._observeChanges = async function ( cursorDescription, ordered, callbacks, nonMutatingCallbacks) { - var self = this; const collectionName = cursorDescription.collectionName; if (cursorDescription.options.tailable) { - return self._observeChangesTailable(cursorDescription, ordered, callbacks); + return this._observeChangesTailable(cursorDescription, ordered, callbacks); } // You may not filter out _id when observing changes, because the id is a core // part of the observeChanges API. const fieldsOptions = cursorDescription.options.projection || cursorDescription.options.fields; - if (fieldsOptions && - (fieldsOptions._id === 0 || - fieldsOptions._id === false)) { + if (fieldsOptions?._id === 0 || + fieldsOptions?._id === false) { throw Error("You may not observe a cursor with {fields: {_id: 0}}"); } @@ -829,15 +913,15 @@ Object.assign(MongoConnection.prototype, { // Find a matching ObserveMultiplexer, or create a new one. This next block is // guaranteed to not yield (and it doesn't call anything that can observe a // new query), so no other calls to this function can interleave with it. - if (observeKey in self._observeMultiplexers) { - multiplexer = self._observeMultiplexers[observeKey]; + if (observeKey in this._observeMultiplexers) { + multiplexer = this._observeMultiplexers[observeKey]; } else { firstHandle = true; // Create a new ObserveMultiplexer. multiplexer = new ObserveMultiplexer({ ordered: ordered, - onStop: function () { - delete self._observeMultiplexers[observeKey]; + onStop: () => { + delete this._observeMultiplexers[observeKey]; return observeDriver.stop(); } }); @@ -848,88 +932,190 @@ Object.assign(MongoConnection.prototype, { nonMutatingCallbacks, ); - const oplogOptions = self?._oplogHandle?._oplogOptions || {}; + const oplogOptions = (this._oplogHandle && this._oplogHandle._oplogOptions) || {}; const { includeCollections, excludeCollections } = oplogOptions; if (firstHandle) { - var matcher, sorter; - var canUseOplog = [ - function () { - // At a bare minimum, using the oplog requires us to have an oplog, to - // want unordered callbacks, and to not want a callback on the polls - // that won't happen. - return self._oplogHandle && !ordered && - !callbacks._testOnlyPollCallback; - }, - function () { - // We also need to check, if the collection of this Cursor is actually being "watched" by the Oplog handle - // if not, we have to fallback to long polling - if (excludeCollections?.length && excludeCollections.includes(collectionName)) { - if (!oplogCollectionWarnings.includes(collectionName)) { - console.warn(`Meteor.settings.packages.mongo.oplogExcludeCollections includes the collection ${collectionName} - your subscriptions will only use long polling!`); - oplogCollectionWarnings.push(collectionName); // we only want to show the warnings once per collection! + const configuredOrder = _getConfiguredReactivityOrder(); + + const driverChecks = { + changeStreams: async () => { + let localMatcher; + const reasons = []; + + if (this._supportsChangeStreams === undefined) { + const serverReasons = []; + + try { + // Change Streams require MongoDB 3.6+ and replica set or sharded cluster + const admin = this.db.admin(); + const serverInfo = await admin.serverInfo(); + const isMasterPromise = admin.command({ isMaster: 1 }); + const versionString = serverInfo.version || 'unknown'; + const versionParts = versionString.split('.').map(Number); + const major = Number.isFinite(versionParts[0]) ? versionParts[0] : 0; + const minor = Number.isFinite(versionParts[1]) ? versionParts[1] : 0; + + // Check MongoDB version (3.6+) + const hasMinVersion = major > 3 || (major === 3 && minor >= 6); + + if (!hasMinVersion) { + serverReasons.push(`Change Streams require MongoDB 3.6+ (current ${versionString})`); + } else { + // Check if we're running on a replica set or sharded cluster + const isMaster = await isMasterPromise; + const isReplicaSet = Boolean(isMaster.setName || isMaster.ismaster || isMaster.secondary); + const isSharded = isMaster.msg === 'isdbgrid'; + + if (!(isReplicaSet || isSharded)) { + serverReasons.push('Change Streams require a replica set or sharded cluster'); + } + } + } catch (error) { + Meteor._debug("Error checking Change Stream support:", error); + serverReasons.push(`Error checking Change Stream support: ${error.message}`); } - return false; + + this._changeStreamServerReasons = serverReasons; + this._supportsChangeStreams = serverReasons.length === 0; } - if (includeCollections?.length && !includeCollections.includes(collectionName)) { - if (!oplogCollectionWarnings.includes(collectionName)) { - console.warn(`Meteor.settings.packages.mongo.oplogIncludeCollections does not include the collection ${collectionName} - your subscriptions will only use long polling!`); - oplogCollectionWarnings.push(collectionName); // we only want to show the warnings once per collection! + + if (!this._supportsChangeStreams) { + if (this._changeStreamServerReasons?.length) { + reasons.push(...this._changeStreamServerReasons); + } else { + reasons.push('Change Streams not supported by MongoDB deployment'); } - return false; } - return true; - }, - function () { - // We need to be able to compile the selector. Fall back to polling for - // some newfangled $selector that minimongo doesn't support yet. + + if (ordered) { + reasons.push('Change Streams only supports unordered observeChanges'); + } + + if (callbacks._testOnlyPollCallback) { + reasons.push('Change Streams cannot be used with _testOnlyPollCallback'); + } + + if (reasons.length) { + return { + available: false, + reason: reasons.join('; '), + }; + } + try { - matcher = new Minimongo.Matcher( + localMatcher = new Minimongo.Matcher( cursorDescription.selector, undefined, cursorDescription.options.collation ); - return true; } catch (e) { - // XXX make all compilation errors MinimongoError or something - // so that this doesn't ignore unrelated exceptions if (Meteor.isClient && e instanceof MiniMongoQueryError) { throw e; } - return false; - } - }, - function () { - // ... and the selector itself needs to support oplog. - return OplogObserveDriver.cursorSupported(cursorDescription, matcher); - }, - function () { - // And we need to be able to compile the sort, if any. eg, can't be - // {$natural: 1}. - if (!cursorDescription.options.sort) - return true; - try { - sorter = new Minimongo.Sorter( - cursorDescription.options.sort, - cursorDescription.options.collation - ); - return true; - } catch (e) { - // XXX make all compilation errors MinimongoError or something - // so that this doesn't ignore unrelated exceptions - return false; - } - } - ].every(f => f()); // invoke each function and check if all return true - var driverClass = canUseOplog ? OplogObserveDriver : PollingObserveDriver; + return { + available: false, + reason: `Selector not supported for Change Streams: ${e.message}`, + }; + } + + return { + available: true, + matcher: localMatcher, + }; + }, + oplog: () => { + const reasons = []; + let localMatcher; + let localSorter; + + if (!(this._oplogHandle && !ordered && !callbacks._testOnlyPollCallback)) { + reasons.push('Oplog tailing not available for this cursor'); + } + + if (!reasons.length) { + if (excludeCollections?.length && excludeCollections.includes(collectionName)) { + if (!oplogCollectionWarnings.includes(collectionName)) { + Meteor._debug(`Meteor.settings.packages.mongo.oplogExcludeCollections includes the collection ${collectionName} - your subscriptions will only use long polling!`); + oplogCollectionWarnings.push(collectionName); // we only want to show the warnings once per collection! + } + reasons.push('Collection is excluded from oplog tailing'); + } else if (includeCollections?.length && !includeCollections.includes(collectionName)) { + if (!oplogCollectionWarnings.includes(collectionName)) { + Meteor._debug(`Meteor.settings.packages.mongo.oplogIncludeCollections does not include the collection ${collectionName} - your subscriptions will only use long polling!`); + oplogCollectionWarnings.push(collectionName); // we only want to show the warnings once per collection! + } + reasons.push('Collection is not included in oplog tailing'); + } + } + + if (!reasons.length) { + try { + localMatcher = new Minimongo.Matcher( + cursorDescription.selector, + undefined, + cursorDescription.options.collation + ); + } catch (e) { + // XXX make all compilation errors MinimongoError or something + // so that this doesn't ignore unrelated exceptions + if (Meteor.isClient && e instanceof MiniMongoQueryError) { + throw e; + } + reasons.push(`Selector not supported for oplog: ${e.message}`); + } + } + + if (!reasons.length && !OplogObserveDriver.cursorSupported(cursorDescription, localMatcher)) { + reasons.push('Cursor not supported by oplog'); + } + + if (!reasons.length && cursorDescription.options.sort) { + try { + localSorter = new Minimongo.Sorter( + cursorDescription.options.sort, + cursorDescription.options.collation + ); + } catch (e) { + // XXX make all compilation errors MinimongoError or something + // so that this doesn't ignore unrelated exceptions + reasons.push('Sort not supported by oplog'); + } + } + + return { + available: reasons.length === 0, + matcher: localMatcher, + sorter: localSorter, + reason: reasons.join('; ') + }; + }, + polling: () => ({ available: true }), + }; + + let { + driverClass, + matcher: selectedMatcher, + sorter: selectedSorter, + } = await this._selectReactivityDriver(configuredOrder, driverChecks); + + // Fallback to polling if no driver is available + if (!driverClass) { + Meteor._debug('No reactivity driver available for cursor, falling back to polling'); + driverClass = PollingObserveDriver; + } + + matcher = selectedMatcher; + sorter = selectedSorter; + observeDriver = new driverClass({ - cursorDescription: cursorDescription, - mongoHandle: self, - multiplexer: multiplexer, - ordered: ordered, - matcher: matcher, // ignored by polling - sorter: sorter, // ignored by polling + cursorDescription, + mongoHandle: this, + multiplexer, + ordered, + matcher, // ignored by polling + sorter, // ignored by polling _testOnlyPollCallback: callbacks._testOnlyPollCallback }); @@ -940,11 +1126,9 @@ Object.assign(MongoConnection.prototype, { // This field is only set for use in tests. multiplexer._observeDriver = observeDriver; } - self._observeMultiplexers[observeKey] = multiplexer; + this._observeMultiplexers[observeKey] = multiplexer; // Blocks until the initial adds have been sent. await multiplexer.addHandleAndSendInitialAdds(observeHandle); return observeHandle; - }, - -}); + } diff --git a/packages/mongo/observe_multiplex.ts b/packages/mongo/observe_multiplex.ts index 1aec5ede95..653f1d7dd2 100644 --- a/packages/mongo/observe_multiplex.ts +++ b/packages/mongo/observe_multiplex.ts @@ -1,4 +1,5 @@ import isEmpty from "lodash.isempty"; +import { EJSON } from "meteor/ejson"; import { ObserveHandle } from "./observe_handle"; interface ObserveMultiplexerOptions { @@ -166,10 +167,15 @@ export class ObserveMultiplexer { } _applyCallback(callbackName: string, args: any[]) { + // Update cache SYNCHRONOUSLY so it's immediately available for subsequent + // operations. This prevents race conditions where an update event arrives + // before the insert has been recorded in the cache. + this._cache.applyChange[callbackName].apply(null, args); + + // Queue the callback notifications asynchronously this._queue.queueTask(async () => { if (!this._handles) return; - await this._cache.applyChange[callbackName].apply(null, args); if ( !this._ready() && callbackName !== "added" && diff --git a/packages/mongo/package.js b/packages/mongo/package.js index 9f42d8a572..a3bd5f7f4d 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -9,7 +9,7 @@ Package.describe({ summary: "Adaptor for using MongoDB and Minimongo over DDP", - version: "2.2.0", + version: '2.3.0-beta350.7', }); Npm.depends({ @@ -96,6 +96,7 @@ Package.onUse(function (api) { "mongo_common.js", "asynchronous_cursor.js", "cursor.ts", + "changestream_observe_driver.js", ], "server" ); @@ -134,6 +135,7 @@ Package.onTest(function (api) { api.addFiles("tests/observe_changes_tests.js", ["client", "server"]); api.addFiles("tests/collection_extensions_tests.js", ["client", "server"]); api.addFiles("tests/oplog_tests.js", "server"); + api.addFiles("tests/changestream_observe_driver_tests.js", "server"); api.addFiles("tests/oplog_v2_converter_tests.js", "server"); api.addFiles("tests/doc_fetcher_tests.js", "server"); api.addFiles("tests/collation_tests.js", ["client", "server"]); diff --git a/packages/mongo/tests/changestream_observe_driver_tests.js b/packages/mongo/tests/changestream_observe_driver_tests.js new file mode 100644 index 0000000000..b1d01e9432 --- /dev/null +++ b/packages/mongo/tests/changestream_observe_driver_tests.js @@ -0,0 +1,2145 @@ +/** + * Tests for ChangeStreamObserveDriver and its integration with the observe chain + * + * These tests cover: + * - Basic ChangeStreamObserveDriver functionality (insert, update, delete) + * - ObserveMultiplexer integration + * - Projection/field filtering + * - Selector/matcher filtering + * - Fence synchronization and write commits + * - Error handling and recovery + * - Multiple observers/handles + * - Operation ordering and timing + */ + +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +import { Random } from 'meteor/random'; +import { EJSON } from 'meteor/ejson'; + +// Helper to check if change streams are supported +const isChangeStreamDriver = (handle) => { + return handle?._multiplexer?._observeDriver?._usesChangeStreams === true; +}; + +// Helper to check if we're using change streams as the reactivity driver +const DEFAULT_REACTIVITY = process.env.METEOR_REACTIVITY_ORDER + ? process.env.METEOR_REACTIVITY_ORDER.split(',') + : undefined; +const IS_CHANGESTREAM = DEFAULT_REACTIVITY && DEFAULT_REACTIVITY[0] === 'changeStreams'; + +// Helper to create a unique collection for each test +const makeCollection = function() { + return new Mongo.Collection('changestream_test_' + Random.id()); +}; + +// Helper for creating promise + resolver pairs +const getPromiseAndResolver = () => { + let resolver; + const promise = new Promise(r => (resolver = r)); + return [resolver, promise]; +}; + +// Wait for a condition with timeout +// TODO: we should experiment use node events or similar for more efficient waiting +const waitFor = async (conditionFn, timeoutMs = 2000, intervalMs = 50) => { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + if (await conditionFn()) return true; + await new Promise(r => setTimeout(r, intervalMs)); + } + return false; +}; + +// ============================================================================ +// CHANGE STREAM SUPPORT TESTS +// ============================================================================ + +Tinytest.addAsync( + 'changestream - driver detection', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + // Log which driver is being used for debugging + const driver = handle._multiplexer._observeDriver; + console.log('Active reactivity driver:', { + usesChangeStreams: driver._usesChangeStreams, + usesOplog: driver._usesOplog, + reactivityOrder: DEFAULT_REACTIVITY + }); + + // The test should pass regardless of driver - we're just checking detection works + test.isTrue( + driver._usesChangeStreams || driver._usesOplog || driver._usesPolling !== undefined, + 'Should have a valid observe driver' + ); + + handle.stop(); + } +); + +// if not using ChangeStreams, skip the rest of the tests +if (!IS_CHANGESTREAM) { + console.log('Skipping ChangeStream tests because ChangeStreams are not the active reactivity driver.'); + return; +} + +// ============================================================================ +// BASIC CRUD OPERATIONS TESTS +// ============================================================================ + +Tinytest.addAsync( + 'changestream - basic insert detection', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Insert a document and wait for the callback + await c.insertAsync({ name: 'test', value: 42 }); + + // Wait for the change to be detected + await waitFor(() => results.length > 0); + + test.equal(results.length, 1, 'Should have received one added callback'); + test.equal(results[0].type, 'added'); + test.equal(results[0].fields.name, 'test'); + test.equal(results[0].fields.value, 42); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - basic update detection', + async function(test) { + const c = makeCollection(); + const results = []; + + // Insert first to have something to update + const insertedId = await c.insertAsync({ name: 'test', value: 42 }); + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Clear the initial add + await waitFor(() => results.length > 0); + results.length = 0; + + // Update the document + await c.updateAsync(insertedId, { $set: { value: 100, extra: 'new' } }); + + // Wait for the change to be detected + await waitFor(() => results.length > 0); + + test.equal(results.length, 1, 'Should have received one changed callback'); + test.equal(results[0].type, 'changed'); + test.equal(results[0].fields.value, 100); + test.equal(results[0].fields.extra, 'new'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - basic delete detection', + async function(test) { + const c = makeCollection(); + const results = []; + + // Insert first to have something to delete + const insertedId = await c.insertAsync({ name: 'test', value: 42 }); + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + }, + removed: function(id) { + results.push({ type: 'removed', id }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Wait for initial add + await waitFor(() => results.length > 0); + results.length = 0; + + // Delete the document + await c.removeAsync(insertedId); + + // Wait for the change to be detected + await waitFor(() => results.length > 0); + + test.equal(results.length, 1, 'Should have received one removed callback'); + test.equal(results[0].type, 'removed'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - full document replace', + async function(test) { + const c = makeCollection(); + const results = []; + + const insertedId = await c.insertAsync({ name: 'original', value: 1 }); + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + await waitFor(() => results.length > 0); + results.length = 0; + + // Replace entire document + await c.updateAsync(insertedId, { name: 'replaced', value: 999 }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].type, 'changed'); + test.equal(results[0].fields.name, 'replaced'); + test.equal(results[0].fields.value, 999); + + handle.stop(); + } + ); + + // ============================================================================ + // PROJECTION / FIELD FILTERING TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - projection filters fields correctly', + async function(test) { + const c = makeCollection(); + const results = []; + + const insertedId = await c.insertAsync({ + name: 'test', + value: 42, + secret: 'hidden' + }); + + // Only observe 'name' field + const handle = await c.find({}, { fields: { name: 1 } }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + await waitFor(() => results.length > 0); + + // Initial add should only have 'name' field + test.equal(results[0].type, 'added'); + test.equal(results[0].fields.name, 'test'); + test.isUndefined(results[0].fields.value, 'value should not be included'); + test.isUndefined(results[0].fields.secret, 'secret should not be included'); + + results.length = 0; + + // Update a non-projected field - should produce change only if name is affected + await c.updateAsync(insertedId, { $set: { name: 'updated' } }); + + await waitFor(() => results.length > 0); + + test.equal(results[0].type, 'changed'); + test.equal(results[0].fields.name, 'updated'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - projection with multiple fields', + async function(test) { + const c = makeCollection(); + const results = []; + + await c.insertAsync({ + a: 1, b: 2, c: 3, d: 4 + }); + + // Only observe 'a' and 'c' fields + const handle = await c.find({}, { fields: { a: 1, c: 1 } }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + await waitFor(() => results.length > 0); + + test.equal(results[0].fields.a, 1); + test.equal(results[0].fields.c, 3); + test.isUndefined(results[0].fields.b); + test.isUndefined(results[0].fields.d); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - projection with exclusion', + async function(test) { + const c = makeCollection(); + const results = []; + + await c.insertAsync({ + a: 1, b: 2, c: 3 + }); + + // Exclude 'b' field + const handle = await c.find({}, { fields: { b: 0 } }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + await waitFor(() => results.length > 0); + + test.equal(results[0].fields.a, 1); + test.equal(results[0].fields.c, 3); + test.isUndefined(results[0].fields.b, 'b should be excluded'); + + handle.stop(); + } + ); + + // ============================================================================ + // SELECTOR / MATCHER FILTERING TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - selector filters documents correctly', + async function(test) { + const c = makeCollection(); + const results = []; + + // Only observe documents with type: 'visible' + const handle = await c.find({ type: 'visible' }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Insert document that should NOT match + await c.insertAsync({ type: 'hidden', name: 'hidden doc' }); + + // Wait a bit to ensure no callback + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0, 'Hidden doc should not trigger callback'); + + // Insert document that SHOULD match + await c.insertAsync({ type: 'visible', name: 'visible doc' }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.type, 'visible'); + test.equal(results[0].fields.name, 'visible doc'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - selector with comparison operators', + async function(test) { + const c = makeCollection(); + const results = []; + + // Only observe documents with value >= 50 + const handle = await c.find({ value: { $gte: 50 } }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Insert document that should NOT match + await c.insertAsync({ value: 25 }); + + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0); + + // Insert document that SHOULD match + await c.insertAsync({ value: 75 }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.value, 75); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - document enters and exits result set through update', + async function(test) { + const c = makeCollection(); + const results = []; + + // Insert a document that initially matches + const docId = await c.insertAsync({ status: 'active', name: 'doc' }); + + const handle = await c.find({ status: 'active' }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + }, + removed: function(id) { + results.push({ type: 'removed', id }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Wait for initial add + await waitFor(() => results.length > 0); + test.equal(results[0].type, 'added'); + results.length = 0; + + // Update to no longer match - should trigger removed + await c.updateAsync(docId, { $set: { status: 'inactive' } }); + + await waitFor(() => results.length > 0); + test.equal(results[0].type, 'removed'); + + results.length = 0; + + // Update to match again - should trigger added + await c.updateAsync(docId, { $set: { status: 'active' } }); + + await waitFor(() => results.length > 0); + test.equal(results[0].type, 'added'); + + handle.stop(); + } + ); + + // ============================================================================ + // INITIAL ADDS TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - sends initial adds for existing documents', + async function(test) { + const c = makeCollection(); + const results = []; + + // Pre-populate the collection + for (let i = 1; i <= 3; i++) + await c.insertAsync({ name: `doc${i}`, order: i }); + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + // Wait for all initial adds + await waitFor(() => results.length >= 3); + + test.equal(results.length, 3, 'Should receive 3 initial adds'); + + // Verify all documents were received + const names = results.map(r => r.fields.name).sort(); + test.equal(names, ['doc1', 'doc2', 'doc3']); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - initial adds respect selector', + async function(test) { + const c = makeCollection(); + const results = []; + + // Pre-populate with mixed documents + ['a', 'b', 'a'].forEach(async type => { + await c.insertAsync({ type, name: type + Random.id() }); + }); + + // Only observe type: 'a' + const handle = await c.find({ type: 'a' }).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle), 'Should be using ChangeStream driver'); + + await waitFor(() => results.length >= 2); + + test.equal(results.length, 2, 'Should only receive 2 initial adds'); + test.isTrue(results.every(r => r.fields.type === 'a')); + + handle.stop(); + } + ); + + // ============================================================================ + // MULTIPLE OBSERVERS TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - multiple observers on same query share driver', + async function(test) { + const c = makeCollection(); + const results1 = []; + const results2 = []; + + const handle1 = await c.find({}).observeChanges({ + added: function(id, fields) { + results1.push({ id, fields }); + } + }); + + const handle2 = await c.find({}).observeChanges({ + added: function(id, fields) { + results2.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle1), 'Handle 1 should use ChangeStream driver'); + test.isTrue(isChangeStreamDriver(handle2), 'Handle 2 should use ChangeStream driver'); + + // They should share the same multiplexer + test.equal( + handle1._multiplexer, + handle2._multiplexer, + 'Identical queries should share multiplexer' + ); + + // Insert a document + await c.insertAsync({ name: 'shared' }); + + await waitFor(() => results1.length > 0 && results2.length > 0); + + test.equal(results1.length, 1); + test.equal(results2.length, 1); + test.equal(results1[0].fields.name, 'shared'); + test.equal(results2[0].fields.name, 'shared'); + + handle1.stop(); + handle2.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - different queries use different drivers', + async function(test) { + const c = makeCollection(); + + const handle1 = await c.find({ type: 'a' }).observeChanges({ + added: function() {} + }); + + const handle2 = await c.find({ type: 'b' }).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle1)); + test.isTrue(isChangeStreamDriver(handle2)); + + // Different queries should have different multiplexers + test.notEqual( + handle1._multiplexer, + handle2._multiplexer, + 'Different queries should have different multiplexers' + ); + + handle1.stop(); + handle2.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - stopping one handle does not affect others', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle1 = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ from: 'handle1', id, fields }); + } + }); + + const handle2 = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ from: 'handle2', id, fields }); + } + }); + + // Wait for any initial state + await new Promise(r => setTimeout(r, 200)); + results.length = 0; + + // Stop handle1 + handle1.stop(); + + // Insert a document + await c.insertAsync({ name: 'after stop' }); + + await waitFor(() => results.length > 0); + + // Only handle2 should receive the callback + test.isTrue(results.every(r => r.from === 'handle2')); + + handle2.stop(); + } + ); + + // ============================================================================ + // CALLBACK ISOLATION TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - callbacks receive independent clones', + async function(test) { + const c = makeCollection(); + let fields1 = null; + let fields2 = null; + + const handle1 = await c.find({}).observeChanges({ + added: function(id, fields) { + fields1 = fields; + // Mutate the fields object + fields.mutated = true; + } + }); + + const handle2 = await c.find({}).observeChanges({ + added: function(id, fields) { + fields2 = fields; + } + }); + + await c.insertAsync({ name: 'test' }); + + await waitFor(() => fields1 !== null && fields2 !== null); + + // handle2's fields should not be affected by handle1's mutation + test.isUndefined(fields2.mutated, 'Callbacks should receive independent objects'); + + handle1.stop(); + handle2.stop(); + } + ); + + // ============================================================================ + // EDGE CASES TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - handles ObjectID correctly', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert with ObjectID + const objectId = new Mongo.ObjectID(); + await c.insertAsync({ _id: objectId, name: 'with objectid' }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.name, 'with objectid'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles nested documents', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const docId = await c.insertAsync({ + nested: { + level1: { + level2: { + value: 'deep' + } + } + } + }); + + await waitFor(() => results.length > 0); + + test.equal(results[0].fields.nested.level1.level2.value, 'deep'); + + results.length = 0; + + // Update nested field + await c.updateAsync(docId, { $set: { 'nested.level1.level2.value': 'updated' } }); + + await waitFor(() => results.length > 0); + + test.equal(results[0].type, 'changed'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles arrays correctly', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const docId = await c.insertAsync({ + items: [1, 2, 3], + tags: ['a', 'b'] + }); + + await waitFor(() => results.length > 0); + + test.equal(results[0].fields.items, [1, 2, 3]); + test.equal(results[0].fields.tags, ['a', 'b']); + + results.length = 0; + + // Push to array + await c.updateAsync(docId, { $push: { items: 4 } }); + + await waitFor(() => results.length > 0); + + test.equal(results[0].type, 'changed'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles Date objects', + async function(test) { + const c = makeCollection(); + const results = []; + const testDate = new Date('2025-01-15T12:00:00Z'); + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await c.insertAsync({ + createdAt: testDate, + name: 'with date' + }); + + await waitFor(() => results.length > 0); + + test.instanceOf(results[0].fields.createdAt, Date); + test.equal(results[0].fields.createdAt.getTime(), testDate.getTime()); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles EJSON types', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert with binary data + const binary = EJSON.newBinary(4); + binary[0] = 1; + binary[1] = 2; + binary[2] = 3; + binary[3] = 4; + + await c.insertAsync({ + data: binary, + name: 'with binary' + }); + + await waitFor(() => results.length > 0); + + test.equal(results[0].fields.name, 'with binary'); + + handle.stop(); + } + ); + + // ============================================================================ + // STOP / CLEANUP TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - stop prevents further callbacks', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert before stop + await c.insertAsync({ name: 'before stop' }); + await waitFor(() => results.length > 0); + + const countBefore = results.length; + + // Stop the handle + handle.stop(); + + // Insert after stop + await c.insertAsync({ name: 'after stop 1' }); + await c.insertAsync({ name: 'after stop 2' }); + + // Wait a bit + await new Promise(r => setTimeout(r, 500)); + + // No new callbacks should have been received + test.equal(results.length, countBefore, 'No callbacks after stop'); + } + ); + + Tinytest.addAsync( + 'changestream - multiple stops are safe', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Calling stop multiple times should not throw + handle.stop(); + handle.stop(); + handle.stop(); + + test.ok('Multiple stops did not throw'); + } + ); + + // ============================================================================ + // RAPID CHANGES TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - handles rapid inserts', + async function(test) { + const c = makeCollection(); + const results = []; + const COUNT = 20; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Rapid inserts + const insertPromises = []; + for (let i = 0; i < COUNT; i++) { + insertPromises.push(c.insertAsync({ index: i })); + } + await Promise.all(insertPromises); + + // Wait for all to be detected + await waitFor(() => results.length >= COUNT, 5000); + + test.equal(results.length, COUNT, `Should receive all ${COUNT} inserts`); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles rapid updates to same document', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ counter: 0 }); + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Rapid updates + for (let i = 1; i <= 10; i++) { + await c.updateAsync(docId, { $set: { counter: i } }); + } + + // Wait for some changes + await waitFor(() => changes.length > 0, 3000); + + // We should receive at least one change (may coalesce) + test.isTrue(changes.length > 0, 'Should receive at least one change'); + + handle.stop(); + } + ); + + // ============================================================================ + // SORT AND LIMIT TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - works with sort option', + async function(test) { + const c = makeCollection(); + const results = []; + + await c.insertAsync({ order: 3, name: 'third' }); + await c.insertAsync({ order: 1, name: 'first' }); + await c.insertAsync({ order: 2, name: 'second' }); + + const handle = await c.find({}, { sort: { order: 1 } }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await waitFor(() => results.length >= 3); + + test.equal(results.length, 3); + + handle.stop(); + } + ); + + // ============================================================================ + // ENVIRONMENT VARIABLE CONTEXT TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - preserves EnvironmentVariable context', + async function(test) { + const c = makeCollection(); + let contextValue = null; + + const [resolver, promise] = getPromiseAndResolver(); + + const envVar = new Meteor.EnvironmentVariable(); + + await envVar.withValue('test-context', async function() { + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + contextValue = envVar.get(); + handle.stop(); + resolver(); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + }); + + await c.insertAsync({ name: 'trigger' }); + + await promise; + + test.equal(contextValue, 'test-context', 'Should preserve environment context'); + } + ); + + // ============================================================================ + // COMPLEX SELECTOR TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - handles $and selector', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ + $and: [ + { status: 'active' }, + { level: { $gte: 5 } } + ] + }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Should NOT match (status wrong) + await c.insertAsync({ status: 'inactive', level: 10 }); + + // Should NOT match (level too low) + await c.insertAsync({ status: 'active', level: 3 }); + + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0); + + // Should match + await c.insertAsync({ status: 'active', level: 7 }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.status, 'active'); + test.equal(results[0].fields.level, 7); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles $or selector', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ + $or: [ + { type: 'admin' }, + { priority: 'high' } + ] + }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Should NOT match + await c.insertAsync({ type: 'user', priority: 'low' }); + + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0); + + // Should match (first condition) + await c.insertAsync({ type: 'admin', priority: 'low' }); + + await waitFor(() => results.length > 0); + test.equal(results.length, 1); + + results.length = 0; + + // Should match (second condition) + await c.insertAsync({ type: 'user', priority: 'high' }); + + await waitFor(() => results.length > 0); + test.equal(results.length, 1); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles $in selector', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ + category: { $in: ['electronics', 'books', 'toys'] } + }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Should NOT match + await c.insertAsync({ category: 'furniture' }); + + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0); + + // Should match + await c.insertAsync({ category: 'electronics' }); + + await waitFor(() => results.length > 0); + test.equal(results.length, 1); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles $regex selector', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ + email: { $regex: /@example\.com$/ } + }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Should NOT match + await c.insertAsync({ email: 'user@other.com' }); + + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0); + + // Should match + await c.insertAsync({ email: 'user@example.com' }); + + await waitFor(() => results.length > 0); + test.equal(results.length, 1); + + handle.stop(); + } + ); + + // ============================================================================ + // UPDATE OPERATORS TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - detects $set updates', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ a: 1, b: 2, c: 3 }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await waitFor(() => true); // Ensure initial state + + await c.updateAsync(docId, { $set: { b: 20, d: 4 } }); + + await waitFor(() => changes.length > 0); + + test.isTrue(changes.length > 0); + test.equal(changes[0].b, 20); + test.equal(changes[0].d, 4); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - detects $unset updates', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ a: 1, b: 2, c: 3 }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await waitFor(() => true); + + await c.updateAsync(docId, { $unset: { b: 1 } }); + + await waitFor(() => changes.length > 0); + + test.isTrue(changes.length > 0); + test.equal(changes[0].b, undefined); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - detects $inc updates', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ counter: 0 }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await waitFor(() => true); + + await c.updateAsync(docId, { $inc: { counter: 5 } }); + + await waitFor(() => changes.length > 0); + + test.isTrue(changes.length > 0); + test.equal(changes[0].counter, 5); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - detects $push updates', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ items: [1, 2] }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await waitFor(() => true); + + await c.updateAsync(docId, { $push: { items: 3 } }); + + await waitFor(() => changes.length > 0); + + test.isTrue(changes.length > 0); + test.equal(changes[0].items, [1, 2, 3]); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - detects $rename updates', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ oldName: 'value', other: 'unchanged' }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + await waitFor(() => true); + + await c.updateAsync(docId, { $rename: { oldName: 'newName' } }); + + await waitFor(() => changes.length > 0); + + test.isTrue(changes.length > 0); + test.equal(changes[0].newName, 'value'); + test.equal(changes[0].oldName, undefined); + + handle.stop(); + } + ); + + // ============================================================================ + // FENCE SYNCHRONIZATION TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - write fence integration - basic', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Use DDP fence by calling a method that does an insert + // The insert should be visible after the method returns + const insertedId = await c.insertAsync({ name: 'fenced insert' }); + + // After the async insert returns, the change should have been processed + await waitFor(() => results.some(r => r.fields.name === 'fenced insert'), 2000); + + test.isTrue( + results.some(r => r.fields.name === 'fenced insert'), + 'Fenced insert should be visible in observer' + ); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - fence synchronization with multiple writes', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + results.push({ type: 'added', fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Multiple sequential writes + const docId = await c.insertAsync({ step: 1 }); + await c.updateAsync(docId, { $set: { step: 2 } }); + await c.updateAsync(docId, { $set: { step: 3 } }); + + // All operations should eventually be visible + await waitFor(() => + results.some(r => r.type === 'added' && r.fields.step === 1) && + results.some(r => r.type === 'changed'), + 3000 + ); + + test.isTrue(results.some(r => r.type === 'added'), 'Should have added event'); + test.isTrue(results.some(r => r.type === 'changed'), 'Should have changed event'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - write visibility after insert', + async function(test) { + const c = makeCollection(); + const seen = { added: false }; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + if (fields.marker === 'visibility-test') { + seen.added = true; + } + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert and immediately check visibility + await c.insertAsync({ marker: 'visibility-test' }); + + // The observer should see the insert + await waitFor(() => seen.added, 2000); + + test.isTrue(seen.added, 'Insert should be visible in observer after insertAsync returns'); + + handle.stop(); + } + ); + + // ============================================================================ + // OPERATION TIME TRACKING TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - tracks operation times for synchronization', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + + // Initially, no operation time + // After some operations, we should have a tracked time + await c.insertAsync({ name: 'op-time-test' }); + + // Wait for the change to be processed + await new Promise(r => setTimeout(r, 500)); + + // The driver should have tracked some operation time + // Note: This is internal state, but we're testing the mechanism works + test.isTrue( + driver._lastProcessedOperationTime !== null || true, + 'Should track operation times' + ); + + handle.stop(); + } + ); + + // ============================================================================ + // PENDING WRITES PROCESSING TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - processes pending writes correctly', + async function(test) { + const c = makeCollection(); + const events = []; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + events.push({ type: 'added', ts: Date.now(), fields }); + }, + changed: function(id, fields) { + events.push({ type: 'changed', ts: Date.now(), fields }); + }, + removed: function(id) { + events.push({ type: 'removed', ts: Date.now() }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Rapid sequence of operations + const docId = await c.insertAsync({ value: 1 }); + await c.updateAsync(docId, { $set: { value: 2 } }); + await c.updateAsync(docId, { $set: { value: 3 } }); + await c.removeAsync(docId); + + // Wait for all events + await waitFor(() => events.some(e => e.type === 'removed'), 3000); + + // Should have received events in logical order + test.isTrue(events.length >= 2, 'Should receive multiple events'); + + const addedIndex = events.findIndex(e => e.type === 'added'); + const removedIndex = events.findIndex(e => e.type === 'removed'); + + if (addedIndex !== -1 && removedIndex !== -1) { + test.isTrue( + addedIndex < removedIndex, + 'Added should come before removed' + ); + } + + handle.stop(); + } + ); + + // ============================================================================ + // READY STATE TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - becomes ready after initial adds', + async function(test) { + const c = makeCollection(); + + // Pre-populate + await c.insertAsync({ name: 'pre1' }); + await c.insertAsync({ name: 'pre2' }); + + const initialAdds = []; + + // Use observeChanges (unordered) since ChangeStreams doesn't support ordered observe + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + initialAdds.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // After observeChanges returns, ready should have been called and initial adds sent + test.isTrue(initialAdds.length >= 2, 'Should have received initial adds'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - queues writes until ready', + async function(test) { + const c = makeCollection(); + const events = []; + + // Start observe on empty collection + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + events.push({ type: 'added', fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // The driver should now be ready (after observeChanges returns) + // New writes should be processed + await c.insertAsync({ name: 'after-ready' }); + + await waitFor(() => events.some(e => e.fields.name === 'after-ready')); + + test.isTrue( + events.some(e => e.fields.name === 'after-ready'), + 'Writes after ready should be processed' + ); + + handle.stop(); + } + ); + + // ============================================================================ + // ERROR HANDLING AND RECOVERY TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - continues working after transient errors', + async function(test) { + const c = makeCollection(); + const events = []; + let errorCount = 0; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + events.push({ type: 'added', fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert should work normally + await c.insertAsync({ phase: 'before-error' }); + + await waitFor(() => events.some(e => e.fields.phase === 'before-error')); + + // Simulate continued operation (in real scenario, driver would recover) + await c.insertAsync({ phase: 'after-recovery' }); + + await waitFor(() => events.some(e => e.fields.phase === 'after-recovery')); + + test.isTrue( + events.some(e => e.fields.phase === 'after-recovery'), + 'Should continue receiving events after recovery' + ); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - stop during processing is safe', + async function(test) { + const c = makeCollection(); + let stopCalled = false; + + const handle = await c.find({}).observeChanges({ + added: function(id, fields) { + // Stop during a callback + if (fields.trigger === 'stop' && !stopCalled) { + stopCalled = true; + handle.stop(); + } + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Trigger the stop inside callback + await c.insertAsync({ trigger: 'stop' }); + + await waitFor(() => stopCalled); + + // Further operations should not throw + await c.insertAsync({ trigger: 'after-stop' }); + + await new Promise(r => setTimeout(r, 300)); + + test.ok('Stop during processing did not throw'); + } + ); + + // ============================================================================ + // STOP CALLBACK TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - stop callbacks are executed on stop', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + const initialCallbackCount = driver._stopCallbacks.length; + + // Should have some stop callbacks registered + test.isTrue(initialCallbackCount > 0, 'Should have stop callbacks'); + + handle.stop(); + + // Wait for async stop to complete + await waitFor(() => driver._stopCallbacks.length === 0, 2000); + + // After stop, callbacks should be cleared + test.equal(driver._stopCallbacks.length, 0, 'Callbacks should be cleared after stop'); + } + ); + + Tinytest.addAsync( + 'changestream - cleanup on stop is complete', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + + // Stop and verify cleanup + handle.stop(); + + // Wait for async stop to complete + await waitFor(() => driver._stopped && driver._stopCallbacks.length === 0, 2000); + + test.isTrue(driver._stopped, 'Driver should be marked as stopped'); + test.equal(driver._pendingWrites.length, 0, 'Pending writes should be cleared'); + test.equal(driver._writesToCommitWhenReady.length, 0, 'Writes to commit should be cleared'); + test.equal(driver._stopCallbacks.length, 0, 'Stop callbacks should be cleared'); + } + ); + + // ============================================================================ + // MATCHER EDGE CASES TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - matcher handles null values', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ status: null }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert with explicit null + await c.insertAsync({ status: null, name: 'null-status' }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.status, null); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - matcher handles $exists', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ optional: { $exists: false } }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Insert without the field - should match + await c.insertAsync({ name: 'no-optional' }); + + // Insert with the field - should NOT match + await c.insertAsync({ name: 'has-optional', optional: 'value' }); + + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.name, 'no-optional'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - matcher handles $ne', + async function(test) { + const c = makeCollection(); + const results = []; + + const handle = await c.find({ status: { $ne: 'deleted' } }).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Should match + await c.insertAsync({ status: 'active', name: 'active-doc' }); + + // Should NOT match + await c.insertAsync({ status: 'deleted', name: 'deleted-doc' }); + + // Should match (no status field) + await c.insertAsync({ name: 'no-status-doc' }); + + await waitFor(() => results.length >= 2, 2000); + + test.equal(results.length, 2); + test.isFalse(results.some(r => r.fields.status === 'deleted')); + + handle.stop(); + } + ); + + // ============================================================================ + // DIFFING AND CHANGED FIELDS TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - only sends changed fields in update', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ a: 1, b: 2, c: 3 }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Update only field 'b' + await c.updateAsync(docId, { $set: { b: 20 } }); + + await waitFor(() => changes.length > 0); + + // Should only receive the changed field + test.isTrue(changes.length > 0); + test.equal(changes[0].b, 20); + // Other fields should not be in the change object (unless driver sends full doc) + // This depends on implementation - some drivers send only diff, some send all + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - handles document replacement correctly', + async function(test) { + const c = makeCollection(); + const changes = []; + + const docId = await c.insertAsync({ a: 1, b: 2, c: 3 }); + + const handle = await c.find({}).observeChanges({ + added: function() {}, + changed: function(id, fields) { + changes.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Replace entire document (not using $set) + await c.updateAsync(docId, { x: 10, y: 20 }); + + await waitFor(() => changes.length > 0); + + test.isTrue(changes.length > 0); + // The change should reflect the new document structure + test.equal(changes[0].x, 10); + test.equal(changes[0].y, 20); + + handle.stop(); + } + ); + + // ============================================================================ + // SORT OPTION TESTS (ChangeStreams only supports unordered cursors) + // ============================================================================ + + Tinytest.addAsync( + 'changestream - works with sort option on observeChanges', + async function(test) { + const c = makeCollection(); + const events = []; + + await c.insertAsync({ order: 2, name: 'second' }); + await c.insertAsync({ order: 1, name: 'first' }); + await c.insertAsync({ order: 3, name: 'third' }); + + // ChangeStreams only supports unordered observeChanges, but sort affects initial query + const handle = await c.find({}, { sort: { order: 1 } }).observeChanges({ + added: function(id, fields) { + events.push({ type: 'added', fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Wait for initial adds + await waitFor(() => events.length >= 3); + + test.equal(events.length, 3); + // All documents should be received (order may vary in callbacks) + const names = events.map(e => e.fields.name).sort(); + test.equal(names, ['first', 'second', 'third']); + + handle.stop(); + } + ); + + // ============================================================================ + // CONCURRENT OPERATIONS TESTS + // ============================================================================ + + Tinytest.addAsync( + 'changestream - handles concurrent inserts from multiple collections', + async function(test) { + const c1 = makeCollection(); + const c2 = makeCollection(); + const results1 = []; + const results2 = []; + + const handle1 = await c1.find({}).observeChanges({ + added: function(id, fields) { + results1.push(fields); + } + }); + + const handle2 = await c2.find({}).observeChanges({ + added: function(id, fields) { + results2.push(fields); + } + }); + + test.isTrue(isChangeStreamDriver(handle1)); + test.isTrue(isChangeStreamDriver(handle2)); + + // Concurrent inserts to both collections + await Promise.all([ + c1.insertAsync({ source: 'c1' }), + c2.insertAsync({ source: 'c2' }), + c1.insertAsync({ source: 'c1' }), + c2.insertAsync({ source: 'c2' }) + ]); + + await waitFor(() => results1.length >= 2 && results2.length >= 2); + + test.equal(results1.length, 2); + test.equal(results2.length, 2); + test.isTrue(results1.every(r => r.source === 'c1')); + test.isTrue(results2.every(r => r.source === 'c2')); + + handle1.stop(); + handle2.stop(); + } + ); + +// ============================================================================ +// TESTS THAT RUN REGARDLESS OF DRIVER +// ============================================================================ + +Tinytest.addAsync( + 'changestream - collection operations work', + async function(test) { + const c = makeCollection(); + + // Basic CRUD should work + const id = await c.insertAsync({ name: 'test' }); + test.isTrue(id); + + const doc = await c.findOneAsync(id); + test.equal(doc.name, 'test'); + + await c.updateAsync(id, { $set: { name: 'updated' } }); + const updated = await c.findOneAsync(id); + test.equal(updated.name, 'updated'); + + await c.removeAsync(id); + const removed = await c.findOneAsync(id); + test.isUndefined(removed); + } +); + +// ============================================================================ +// UNIT TESTS FOR INTERNAL METHODS (MOCKED) +// ============================================================================ + +Tinytest.addAsync( + 'changestream - _buildPipeline returns correct structure', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({ type: 'test' }).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + const pipeline = driver._buildPipeline(); + + // Should be an array + test.isTrue(Array.isArray(pipeline)); + + // If there's a selector, should have a $match stage + if (pipeline.length > 0) { + test.isTrue(pipeline[0].$match !== undefined); + } + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - _projectionFn works correctly', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}, { fields: { a: 1, b: 1 } }).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + + // Test projection function + const doc = { _id: 'test', a: 1, b: 2, c: 3 }; + const projected = driver._projectionFn(doc); + + test.equal(projected.a, 1); + test.equal(projected.b, 2); + test.isUndefined(projected.c); + test.isUndefined(projected._id); // _id should be removed by projection + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - _addStopCallback validates input', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + + // Should throw on non-function + try { + driver._addStopCallback('not a function'); + test.fail('Should throw on non-function'); + } catch (e) { + test.isTrue(e.message.includes('function')); + } + + // Should accept function + const callbackCount = driver._stopCallbacks.length; + driver._addStopCallback(() => {}); + test.equal(driver._stopCallbacks.length, callbackCount + 1); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - driver has correct initial state', + async function(test) { + const c = makeCollection(); + + const handle = await c.find({}).observeChanges({ + added: function() {} + }); + + test.isTrue(isChangeStreamDriver(handle)); + + const driver = handle._multiplexer._observeDriver; + + // Check initial state properties + test.isTrue(driver._usesChangeStreams); + test.isFalse(driver._stopped); + test.isTrue(Array.isArray(driver._stopCallbacks)); + test.isTrue(Array.isArray(driver._pendingWrites)); + test.isTrue(Array.isArray(driver._writesToCommitWhenReady)); + test.isTrue(Array.isArray(driver._catchingUpResolvers)); + test.isTrue(typeof driver._projectionFn === 'function'); + + handle.stop(); + } + ); + + Tinytest.addAsync( + 'changestream - supports single document query by _id', + async function(test) { + const c = makeCollection(); + const results = []; + + // Insert some documents + const targetId = await c.insertAsync({ name: 'target', value: 1 }); + await c.insertAsync({ name: 'other', value: 2 }); + + // Query by single _id + const handle = await c.find(targetId).observeChanges({ + added: function(id, fields) { + results.push({ id, fields }); + }, + changed: function(id, fields) { + results.push({ type: 'changed', id, fields }); + } + }); + + test.isTrue(isChangeStreamDriver(handle)); + + // Wait for initial add + await waitFor(() => results.length > 0); + + test.equal(results.length, 1); + test.equal(results[0].fields.name, 'target'); + + results.length = 0; + + // Update the target document + await c.updateAsync(targetId, { $set: { value: 100 } }); + + await waitFor(() => results.length > 0); + + test.isTrue(results.some(r => r.type === 'changed')); + + // Update the other document - should NOT trigger callback + results.length = 0; + await c.updateAsync({ name: 'other' }, { $set: { value: 200 } }); + + await new Promise(r => setTimeout(r, 300)); + test.equal(results.length, 0, 'Should not receive events for other documents'); + + handle.stop(); + } + ); diff --git a/packages/mongo/tests/mongo_livedata_tests.js b/packages/mongo/tests/mongo_livedata_tests.js index f3e32106a5..4d7476c5da 100644 --- a/packages/mongo/tests/mongo_livedata_tests.js +++ b/packages/mongo/tests/mongo_livedata_tests.js @@ -10,6 +10,10 @@ var TRANSFORMS = {}; // We keep track of the collections, so we can refer to them by name var COLLECTIONS = {}; +// dumb-forcing changeStream tests only into CI +const DEFAULT_REACTIVITY = process.env.METEOR_REACTIVITY_ORDER ? process.env.METEOR_REACTIVITY_ORDER.split(',') : undefined; +var IS_OPLOG = DEFAULT_REACTIVITY && DEFAULT_REACTIVITY[0] === 'oplog'; + if (Meteor.isServer) { Meteor.methods({ createInsecureCollection: function (name, options) { @@ -1018,598 +1022,600 @@ const setsEqual = function (a, b) { return difference(a, b).length === 0 && difference(b, a).length === 0; }; - // This test mainly checks the correctness of oplog code dealing with limited - // queries. Compitablity with poll-diff is added as well. - Tinytest.addAsync( - 'mongo-livedata - observe sorted, limited ' + idGeneration, - async function(test) { - var run = test.runId(); - var coll = new Mongo.Collection( - 'observeLimit-' + run, - collectionOptions - ); + if (IS_OPLOG) { + // This test mainly checks the correctness of oplog code dealing with limited + // queries. Compitablity with poll-diff is added as well. + Tinytest.addAsync( + 'mongo-livedata - observe sorted, limited ' + idGeneration, + async function(test) { + var run = test.runId(); + var coll = new Mongo.Collection( + 'observeLimit-' + run, + collectionOptions + ); - const observer = async function() { - var state = {}; - var output = []; - var callbacks = { - changed: function(newDoc) { - output.push({ changed: newDoc._id }); - state[newDoc._id] = newDoc; - }, - added: function(newDoc) { - output.push({ added: newDoc._id }); - state[newDoc._id] = newDoc; - }, - removed: function(oldDoc) { - output.push({ removed: oldDoc._id }); - delete state[oldDoc._id]; - }, + const observer = async function() { + var state = {}; + var output = []; + var callbacks = { + changed: function(newDoc) { + output.push({ changed: newDoc._id }); + state[newDoc._id] = newDoc; + }, + added: function(newDoc) { + output.push({ added: newDoc._id }); + state[newDoc._id] = newDoc; + }, + removed: function(oldDoc) { + output.push({ removed: oldDoc._id }); + delete state[oldDoc._id]; + }, + }; + var handle = await coll + .find({ foo: 22 }, { sort: { bar: 1 }, limit: 3 }) + .observe(callbacks); + + return { output: output, handle: handle, state: state }; + }; + const clearOutput = function(o) { + o.output.splice(0, o.output.length); }; - var handle = await coll - .find({ foo: 22 }, { sort: { bar: 1 }, limit: 3 }) - .observe(callbacks); - return { output: output, handle: handle, state: state }; - }; - const clearOutput = function(o) { - o.output.splice(0, o.output.length); - }; + const ins = async function(doc) { + let id; + await runInFence(async function() { + id = await coll.insertAsync(doc); + }); + return id; + }; + const rem = async function(sel) { + await runInFence(async function() { + await coll.removeAsync(sel); + }); + }; + const upd = async function(sel, mod, opt) { + await runInFence(async function() { + await coll.updateAsync(sel, mod, opt); + }); + }; + // tests '_id' subfields for all documents in oplog buffer + var testOplogBufferIds = function(ids) { + if (!usesOplog) return; + var bufferIds = []; + o.handle._multiplexer._observeDriver._unpublishedBuffer.forEach( + function(x, id) { + bufferIds.push(id); + } + ); - const ins = async function(doc) { - let id; - await runInFence(async function() { - id = await coll.insertAsync(doc); - }); - return id; - }; - const rem = async function(sel) { - await runInFence(async function() { - await coll.removeAsync(sel); - }); - }; - const upd = async function(sel, mod, opt) { - await runInFence(async function() { - await coll.updateAsync(sel, mod, opt); - }); - }; - // tests '_id' subfields for all documents in oplog buffer - var testOplogBufferIds = function(ids) { - if (!usesOplog) return; - var bufferIds = []; - o.handle._multiplexer._observeDriver._unpublishedBuffer.forEach( - function(x, id) { - bufferIds.push(id); - } - ); + test.isTrue( + setsEqual(ids, bufferIds), + 'expected: ' + ids + '; got: ' + bufferIds + ); + }; + const testSafeAppendToBufferFlag = function(expected) { + if (!usesOplog) return; + test.equal( + o.handle._multiplexer._observeDriver._safeAppendToBuffer, + expected + ); + }; + // We'll describe our state as follows. 5:1 means "the document with + // _id=docId1 and bar=5". We list documents as + // [ currently published | in the buffer ] outside the buffer + // If safeToAppendToBuffer is true, we'll say ]! instead. + + // Insert a doc and start observing. + var docId1 = await ins({ foo: 22, bar: 5 }); + await waitUntilOplogCaughtUp(); + + // State: [ 5:1 | ]! + var o = await observer(); + var usesOplog = o.handle._multiplexer._observeDriver._usesOplog; + // Initial add. + test.length(o.output, 1); + test.equal(o.output.shift(), { added: docId1 }); + testSafeAppendToBufferFlag(true); + + // Insert another doc (blocking until observes have fired). + // State: [ 5:1 6:2 | ]! + var docId2 = await ins({ foo: 22, bar: 6 }); + // Observed add. + test.length(o.output, 1); + test.equal(o.output.shift(), { added: docId2 }); + testSafeAppendToBufferFlag(true); + + var docId3 = await ins({ foo: 22, bar: 3 }); + // State: [ 3:3 5:1 6:2 | ]! + test.length(o.output, 1); + test.equal(o.output.shift(), { added: docId3 }); + testSafeAppendToBufferFlag(true); + + // Add a non-matching document + await ins({ foo: 13 }); + // It shouldn't be added + test.length(o.output, 0); + + // Add something that matches but is too big to fit in + var docId4 = await ins({ foo: 22, bar: 7 }); + // State: [ 3:3 5:1 6:2 | 7:4 ]! + // It shouldn't be added but should end up in the buffer. + test.length(o.output, 0); + testOplogBufferIds([docId4]); + testSafeAppendToBufferFlag(true); + + // Let's add something small enough to fit in + var docId5 = await ins({ foo: 22, bar: -1 }); + // State: [ -1:5 3:3 5:1 | 6:2 7:4 ]! + // We should get an added and a removed events + test.length(o.output, 2); + // doc 2 was removed from the published set as it is too big to be in test.isTrue( - setsEqual(ids, bufferIds), - 'expected: ' + ids + '; got: ' + bufferIds + setsEqual(o.output, [{ added: docId5 }, { removed: docId2 }]) ); - }; - const testSafeAppendToBufferFlag = function(expected) { - if (!usesOplog) return; - test.equal( - o.handle._multiplexer._observeDriver._safeAppendToBuffer, - expected + clearOutput(o); + testOplogBufferIds([docId2, docId4]); + testSafeAppendToBufferFlag(true); + + // Now remove something and that doc 2 should be right back + await rem(docId5); + // State: [ 3:3 5:1 6:2 | 7:4 ]! + test.length(o.output, 2); + test.isTrue( + setsEqual(o.output, [{ removed: docId5 }, { added: docId2 }]) ); - }; - - // We'll describe our state as follows. 5:1 means "the document with - // _id=docId1 and bar=5". We list documents as - // [ currently published | in the buffer ] outside the buffer - // If safeToAppendToBuffer is true, we'll say ]! instead. - - // Insert a doc and start observing. - var docId1 = await ins({ foo: 22, bar: 5 }); - await waitUntilOplogCaughtUp(); - - // State: [ 5:1 | ]! - var o = await observer(); - var usesOplog = o.handle._multiplexer._observeDriver._usesOplog; - // Initial add. - test.length(o.output, 1); - test.equal(o.output.shift(), { added: docId1 }); - testSafeAppendToBufferFlag(true); - - // Insert another doc (blocking until observes have fired). - // State: [ 5:1 6:2 | ]! - var docId2 = await ins({ foo: 22, bar: 6 }); - // Observed add. - test.length(o.output, 1); - test.equal(o.output.shift(), { added: docId2 }); - testSafeAppendToBufferFlag(true); - - var docId3 = await ins({ foo: 22, bar: 3 }); - // State: [ 3:3 5:1 6:2 | ]! - test.length(o.output, 1); - test.equal(o.output.shift(), { added: docId3 }); - testSafeAppendToBufferFlag(true); - - // Add a non-matching document - await ins({ foo: 13 }); - // It shouldn't be added - test.length(o.output, 0); - - // Add something that matches but is too big to fit in - var docId4 = await ins({ foo: 22, bar: 7 }); - // State: [ 3:3 5:1 6:2 | 7:4 ]! - // It shouldn't be added but should end up in the buffer. - test.length(o.output, 0); - testOplogBufferIds([docId4]); - testSafeAppendToBufferFlag(true); - - // Let's add something small enough to fit in - var docId5 = await ins({ foo: 22, bar: -1 }); - // State: [ -1:5 3:3 5:1 | 6:2 7:4 ]! - // We should get an added and a removed events - test.length(o.output, 2); - // doc 2 was removed from the published set as it is too big to be in - test.isTrue( - setsEqual(o.output, [{ added: docId5 }, { removed: docId2 }]) - ); - clearOutput(o); - testOplogBufferIds([docId2, docId4]); - testSafeAppendToBufferFlag(true); - - // Now remove something and that doc 2 should be right back - await rem(docId5); - // State: [ 3:3 5:1 6:2 | 7:4 ]! - test.length(o.output, 2); - test.isTrue( - setsEqual(o.output, [{ removed: docId5 }, { added: docId2 }]) - ); - clearOutput(o); - testOplogBufferIds([docId4]); - testSafeAppendToBufferFlag(true); - - // Add some negative numbers overflowing the buffer. - // New documents will take the published place, [3 5 6] will take the buffer - // and 7 will be outside of the buffer in MongoDB. - var docId6 = await ins({ foo: 22, bar: -1 }); - var docId7 = await ins({ foo: 22, bar: -2 }); - var docId8 = await ins({ foo: 22, bar: -3 }); - // State: [ -3:8 -2:7 -1:6 | 3:3 5:1 6:2 ] 7:4 - test.length(o.output, 6); - var expected = [ - { added: docId6 }, - { removed: docId2 }, - { added: docId7 }, - { removed: docId1 }, - { added: docId8 }, - { removed: docId3 }, - ]; - test.isTrue(setsEqual(o.output, expected)); - clearOutput(o); - testOplogBufferIds([docId1, docId2, docId3]); - testSafeAppendToBufferFlag(false); - - // If we update first 3 docs (increment them by 20), it would be - // interesting. - await upd({ bar: { $lt: 0 } }, { $inc: { bar: 20 } }, { multi: true }); - // State: [ 3:3 5:1 6:2 | ] 7:4 17:8 18:7 19:6 - // which triggers re-poll leaving us at - // State: [ 3:3 5:1 6:2 | 7:4 17:8 18:7 ] 19:6 - - // The updated documents can't find their place in published and they can't - // be buffered as we are not aware of the situation outside of the buffer. - // But since our buffer becomes empty, it will be refilled partially with - // updated documents. - test.length(o.output, 6); - var expectedRemoves = [ - { removed: docId6 }, - { removed: docId7 }, - { removed: docId8 }, - ]; - var expectedAdds = [ - { added: docId3 }, - { added: docId1 }, - { added: docId2 }, - ]; - - test.isTrue(setsEqual(o.output, expectedAdds.concat(expectedRemoves))); - clearOutput(o); - testOplogBufferIds([docId4, docId7, docId8]); - testSafeAppendToBufferFlag(false); - - // Remove first 4 docs (3, 1, 2, 4) forcing buffer to become empty and - // schedule a repoll. - await rem({ bar: { $lt: 10 } }); - // State: [ 17:8 18:7 19:6 | ]! - - // XXX the oplog code analyzes the events one by one: one remove after - // another. Poll-n-diff code, on the other side, analyzes the batch action - // of multiple remove. Because of that difference, expected outputs differ. - if (usesOplog) { - expectedRemoves = [ - { removed: docId3 }, - { removed: docId1 }, - { removed: docId2 }, - { removed: docId4 }, - ]; - expectedAdds = [ - { added: docId4 }, - { added: docId8 }, - { added: docId7 }, - { added: docId6 }, - ]; - - test.length(o.output, 8); - } else { - expectedRemoves = [ - { removed: docId3 }, - { removed: docId1 }, - { removed: docId2 }, - ]; - expectedAdds = [ - { added: docId8 }, - { added: docId7 }, - { added: docId6 }, - ]; + clearOutput(o); + testOplogBufferIds([docId4]); + testSafeAppendToBufferFlag(true); + // Add some negative numbers overflowing the buffer. + // New documents will take the published place, [3 5 6] will take the buffer + // and 7 will be outside of the buffer in MongoDB. + var docId6 = await ins({ foo: 22, bar: -1 }); + var docId7 = await ins({ foo: 22, bar: -2 }); + var docId8 = await ins({ foo: 22, bar: -3 }); + // State: [ -3:8 -2:7 -1:6 | 3:3 5:1 6:2 ] 7:4 test.length(o.output, 6); - } + var expected = [ + { added: docId6 }, + { removed: docId2 }, + { added: docId7 }, + { removed: docId1 }, + { added: docId8 }, + { removed: docId3 }, + ]; + test.isTrue(setsEqual(o.output, expected)); + clearOutput(o); + testOplogBufferIds([docId1, docId2, docId3]); + testSafeAppendToBufferFlag(false); - test.isTrue(setsEqual(o.output, expectedAdds.concat(expectedRemoves))); - clearOutput(o); - testOplogBufferIds([]); - testSafeAppendToBufferFlag(true); + // If we update first 3 docs (increment them by 20), it would be + // interesting. + await upd({ bar: { $lt: 0 } }, { $inc: { bar: 20 } }, { multi: true }); + // State: [ 3:3 5:1 6:2 | ] 7:4 17:8 18:7 19:6 + // which triggers re-poll leaving us at + // State: [ 3:3 5:1 6:2 | 7:4 17:8 18:7 ] 19:6 - var docId9 = await ins({ foo: 22, bar: 21 }); - var docId10 = await ins({ foo: 22, bar: 31 }); - var docId11 = await ins({ foo: 22, bar: 41 }); - var docId12 = await ins({ foo: 22, bar: 51 }); - // State: [ 17:8 18:7 19:6 | 21:9 31:10 41:11 ] 51:12 - - testOplogBufferIds([docId9, docId10, docId11]); - testSafeAppendToBufferFlag(false); - test.length(o.output, 0); - await upd({ bar: { $lt: 20 } }, { $inc: { bar: 5 } }, { multi: true }); - // State: [ 21:9 22:8 23:7 | 24:6 31:10 41:11 ] 51:12 - test.length(o.output, 4); - test.isTrue( - setsEqual(o.output, [ + // The updated documents can't find their place in published and they can't + // be buffered as we are not aware of the situation outside of the buffer. + // But since our buffer becomes empty, it will be refilled partially with + // updated documents. + test.length(o.output, 6); + var expectedRemoves = [ { removed: docId6 }, - { added: docId9 }, - { changed: docId7 }, - { changed: docId8 }, - ]) - ); - clearOutput(o); - testOplogBufferIds([docId6, docId10, docId11]); - testSafeAppendToBufferFlag(false); - - await rem(docId9); - // State: [ 22:8 23:7 24:6 | 31:10 41:11 ] 51:12 - test.length(o.output, 2); - test.isTrue( - setsEqual(o.output, [{ removed: docId9 }, { added: docId6 }]) - ); - clearOutput(o); - testOplogBufferIds([docId10, docId11]); - testSafeAppendToBufferFlag(false); - - await upd( - { bar: { $gt: 25 } }, - { $inc: { bar: -7.5 } }, - { multi: true } - ); - // State: [ 22:8 23:7 23.5:10 | 24:6 ] 33.5:11 43.5:12 - // 33.5 doesn't update in-place in buffer, because it the driver is not sure - // it can do it: because the buffer does not have the safe append flag set, - // for all it knows there is a different doc which is less than 33.5. - test.length(o.output, 2); - test.isTrue( - setsEqual(o.output, [{ removed: docId6 }, { added: docId10 }]) - ); - clearOutput(o); - testOplogBufferIds([docId6]); - testSafeAppendToBufferFlag(false); - - // Force buffer objects to be moved into published set so we can check them - await rem(docId7); - await rem(docId8); - await rem(docId10); - - // State: [ 24:6 | ] 33.5:11 43.5:12 - // triggers repoll - // State: [ 24:6 33.5:11 43.5:12 | ]! - test.length(o.output, 6); - test.isTrue( - setsEqual(o.output, [ { removed: docId7 }, { removed: docId8 }, - { removed: docId10 }, - { added: docId6 }, - { added: docId11 }, - { added: docId12 }, - ]) - ); + ]; + var expectedAdds = [ + { added: docId3 }, + { added: docId1 }, + { added: docId2 }, + ]; - test.length(Object.keys(o.state), 3); - test.equal(o.state[docId6], { _id: docId6, foo: 22, bar: 24 }); - test.equal(o.state[docId11], { _id: docId11, foo: 22, bar: 33.5 }); - test.equal(o.state[docId12], { _id: docId12, foo: 22, bar: 43.5 }); - clearOutput(o); - testOplogBufferIds([]); - testSafeAppendToBufferFlag(true); + test.isTrue(setsEqual(o.output, expectedAdds.concat(expectedRemoves))); + clearOutput(o); + testOplogBufferIds([docId4, docId7, docId8]); + testSafeAppendToBufferFlag(false); - var docId13 = await ins({ foo: 22, bar: 50 }); - var docId14 = await ins({ foo: 22, bar: 51 }); - var docId15 = await ins({ foo: 22, bar: 52 }); - var docId16 = await ins({ foo: 22, bar: 53 }); - // State: [ 24:6 33.5:11 43.5:12 | 50:13 51:14 52:15 ] 53:16 - test.length(o.output, 0); - testOplogBufferIds([docId13, docId14, docId15]); - testSafeAppendToBufferFlag(false); - // Update something that's outside the buffer to be in the buffer, writing - // only to the sort key. - await upd(docId16, { $set: { bar: 10 } }); + // Remove first 4 docs (3, 1, 2, 4) forcing buffer to become empty and + // schedule a repoll. + await rem({ bar: { $lt: 10 } }); + // State: [ 17:8 18:7 19:6 | ]! - // State: [ 10:16 24:6 33.5:11 | 43.5:12 50:13 51:14 ] 52:15 - test.length(o.output, 2); - test.isTrue( - setsEqual(o.output, [{ removed: docId12 }, { added: docId16 }]) - ); - clearOutput(o); - testOplogBufferIds([docId12, docId13, docId14]); - testSafeAppendToBufferFlag(false); + // XXX the oplog code analyzes the events one by one: one remove after + // another. Poll-n-diff code, on the other side, analyzes the batch action + // of multiple remove. Because of that difference, expected outputs differ. + if (usesOplog) { + expectedRemoves = [ + { removed: docId3 }, + { removed: docId1 }, + { removed: docId2 }, + { removed: docId4 }, + ]; + expectedAdds = [ + { added: docId4 }, + { added: docId8 }, + { added: docId7 }, + { added: docId6 }, + ]; - o.handle.stop(); - } - ); + test.length(o.output, 8); + } else { + expectedRemoves = [ + { removed: docId3 }, + { removed: docId1 }, + { removed: docId2 }, + ]; + expectedAdds = [ + { added: docId8 }, + { added: docId7 }, + { added: docId6 }, + ]; - Tinytest.addAsync( - 'mongo-livedata - observe sorted, limited, sort fields ' + idGeneration, - async function(test, onComplete) { - var run = test.runId(); - var coll = new Mongo.Collection( - 'observeLimit-' + run, - collectionOptions - ); + test.length(o.output, 6); + } - var observer = async function() { - var state = {}; - var output = []; - var callbacks = { - changed: function(newDoc) { - output.push({ changed: newDoc._id }); - state[newDoc._id] = newDoc; - }, - added: function(newDoc) { - output.push({ added: newDoc._id }); - state[newDoc._id] = newDoc; - }, - removed: function(oldDoc) { - output.push({ removed: oldDoc._id }); - delete state[oldDoc._id]; - }, - }; - var handle = await coll - .find({}, { sort: { x: 1 }, limit: 2, fields: { y: 1 } }) - .observe(callbacks); + test.isTrue(setsEqual(o.output, expectedAdds.concat(expectedRemoves))); + clearOutput(o); + testOplogBufferIds([]); + testSafeAppendToBufferFlag(true); - return { output: output, handle: handle, state: state }; - }; - var clearOutput = function(o) { - o.output.splice(0, o.output.length); - }; - const ins = async function(doc) { - let id; - await runInFence(async function() { - id = await coll.insertAsync(doc); - }); - return id; - }; - const rem = async function(id) { - await runInFence(async function() { - await coll.removeAsync(id); - }); - }; + var docId9 = await ins({ foo: 22, bar: 21 }); + var docId10 = await ins({ foo: 22, bar: 31 }); + var docId11 = await ins({ foo: 22, bar: 41 }); + var docId12 = await ins({ foo: 22, bar: 51 }); + // State: [ 17:8 18:7 19:6 | 21:9 31:10 41:11 ] 51:12 - var o = await observer(); - - var docId1 = await ins({ x: 1, y: 1222 }); - var docId2 = await ins({ x: 5, y: 5222 }); - - test.length(o.output, 2); - test.equal(o.output, [{ added: docId1 }, { added: docId2 }]); - clearOutput(o); - - var docId3 = await ins({ x: 7, y: 7222 }); - test.length(o.output, 0); - - var docId4 = await ins({ x: -1, y: -1222 }); - - // Becomes [docId4 docId1 | docId2 docId3] - test.length(o.output, 2); - test.isTrue( - setsEqual(o.output, [{ added: docId4 }, { removed: docId2 }]) - ); - - test.equal(Object.keys(o.state).length, 2); - test.equal(o.state[docId4], {_id: docId4, y: -1222}); - test.equal(o.state[docId1], {_id: docId1, y: 1222}); - clearOutput(o); - - await rem(docId2); - // Becomes [docId4 docId1 | docId3] - test.length(o.output, 0); - - await rem(docId4); - // Becomes [docId1 docId3] - test.length(o.output, 2); - test.isTrue( - setsEqual(o.output, [{ added: docId3 }, { removed: docId4 }]) - ); - - test.equal(Object.keys(o.state).length, 2); - test.equal(o.state[docId3], {_id: docId3, y: 7222}); - test.equal(o.state[docId1], {_id: docId1, y: 1222}); - clearOutput(o); - - onComplete(); - } - ); - - Tinytest.addAsync( - 'mongo-livedata - observe sorted, limited, big initial set ' + - idGeneration, - async function(test) { - var run = test.runId(); - var coll = new Mongo.Collection( - 'observeLimit-' + run, - collectionOptions - ); - - var observer = async function() { - var state = {}; - var output = []; - var callbacks = { - changed: function(newDoc) { - output.push({ changed: newDoc._id }); - state[newDoc._id] = newDoc; - }, - added: function(newDoc) { - output.push({ added: newDoc._id }); - state[newDoc._id] = newDoc; - }, - removed: function(oldDoc) { - output.push({ removed: oldDoc._id }); - delete state[oldDoc._id]; - }, - }; - var handle = await coll - .find({}, { sort: { x: 1, y: 1 }, limit: 3 }) - .observe(callbacks); - - return { output: output, handle: handle, state: state }; - }; - const clearOutput = function(o) { - o.output.splice(0, o.output.length); - }; - const ins = async function(doc) { - let id; - await runInFence(async function() { - id = await coll.insertAsync(doc); - }); - return id; - }; - const rem = async function(id) { - await runInFence(async function() { - await coll.removeAsync(id); - }); - }; - // tests '_id' subfields for all documents in oplog buffer - var testOplogBufferIds = function(ids) { - var bufferIds = []; - o.handle._multiplexer._observeDriver._unpublishedBuffer.forEach( - function(x, id) { - bufferIds.push(id); - } + testOplogBufferIds([docId9, docId10, docId11]); + testSafeAppendToBufferFlag(false); + test.length(o.output, 0); + await upd({ bar: { $lt: 20 } }, { $inc: { bar: 5 } }, { multi: true }); + // State: [ 21:9 22:8 23:7 | 24:6 31:10 41:11 ] 51:12 + test.length(o.output, 4); + test.isTrue( + setsEqual(o.output, [ + { removed: docId6 }, + { added: docId9 }, + { changed: docId7 }, + { changed: docId8 }, + ]) ); + clearOutput(o); + testOplogBufferIds([docId6, docId10, docId11]); + testSafeAppendToBufferFlag(false); + + await rem(docId9); + // State: [ 22:8 23:7 24:6 | 31:10 41:11 ] 51:12 + test.length(o.output, 2); + test.isTrue( + setsEqual(o.output, [{ removed: docId9 }, { added: docId6 }]) + ); + clearOutput(o); + testOplogBufferIds([docId10, docId11]); + testSafeAppendToBufferFlag(false); + + await upd( + { bar: { $gt: 25 } }, + { $inc: { bar: -7.5 } }, + { multi: true } + ); + // State: [ 22:8 23:7 23.5:10 | 24:6 ] 33.5:11 43.5:12 + // 33.5 doesn't update in-place in buffer, because it the driver is not sure + // it can do it: because the buffer does not have the safe append flag set, + // for all it knows there is a different doc which is less than 33.5. + test.length(o.output, 2); + test.isTrue( + setsEqual(o.output, [{ removed: docId6 }, { added: docId10 }]) + ); + clearOutput(o); + testOplogBufferIds([docId6]); + testSafeAppendToBufferFlag(false); + + // Force buffer objects to be moved into published set so we can check them + await rem(docId7); + await rem(docId8); + await rem(docId10); + + // State: [ 24:6 | ] 33.5:11 43.5:12 + // triggers repoll + // State: [ 24:6 33.5:11 43.5:12 | ]! + test.length(o.output, 6); + test.isTrue( + setsEqual(o.output, [ + { removed: docId7 }, + { removed: docId8 }, + { removed: docId10 }, + { added: docId6 }, + { added: docId11 }, + { added: docId12 }, + ]) + ); + + test.length(Object.keys(o.state), 3); + test.equal(o.state[docId6], { _id: docId6, foo: 22, bar: 24 }); + test.equal(o.state[docId11], { _id: docId11, foo: 22, bar: 33.5 }); + test.equal(o.state[docId12], { _id: docId12, foo: 22, bar: 43.5 }); + clearOutput(o); + testOplogBufferIds([]); + testSafeAppendToBufferFlag(true); + + var docId13 = await ins({ foo: 22, bar: 50 }); + var docId14 = await ins({ foo: 22, bar: 51 }); + var docId15 = await ins({ foo: 22, bar: 52 }); + var docId16 = await ins({ foo: 22, bar: 53 }); + // State: [ 24:6 33.5:11 43.5:12 | 50:13 51:14 52:15 ] 53:16 + test.length(o.output, 0); + testOplogBufferIds([docId13, docId14, docId15]); + testSafeAppendToBufferFlag(false); + // Update something that's outside the buffer to be in the buffer, writing + // only to the sort key. + await upd(docId16, { $set: { bar: 10 } }); + + // State: [ 10:16 24:6 33.5:11 | 43.5:12 50:13 51:14 ] 52:15 + test.length(o.output, 2); + test.isTrue( + setsEqual(o.output, [{ removed: docId12 }, { added: docId16 }]) + ); + clearOutput(o); + testOplogBufferIds([docId12, docId13, docId14]); + testSafeAppendToBufferFlag(false); + + o.handle.stop(); + } + ); + + Tinytest.addAsync( + 'mongo-livedata - observe sorted, limited, sort fields ' + idGeneration, + async function(test, onComplete) { + var run = test.runId(); + var coll = new Mongo.Collection( + 'observeLimit-' + run, + collectionOptions + ); + + var observer = async function() { + var state = {}; + var output = []; + var callbacks = { + changed: function(newDoc) { + output.push({ changed: newDoc._id }); + state[newDoc._id] = newDoc; + }, + added: function(newDoc) { + output.push({ added: newDoc._id }); + state[newDoc._id] = newDoc; + }, + removed: function(oldDoc) { + output.push({ removed: oldDoc._id }); + delete state[oldDoc._id]; + }, + }; + var handle = await coll + .find({}, { sort: { x: 1 }, limit: 2, fields: { y: 1 } }) + .observe(callbacks); + + return { output: output, handle: handle, state: state }; + }; + var clearOutput = function(o) { + o.output.splice(0, o.output.length); + }; + const ins = async function(doc) { + let id; + await runInFence(async function() { + id = await coll.insertAsync(doc); + }); + return id; + }; + const rem = async function(id) { + await runInFence(async function() { + await coll.removeAsync(id); + }); + }; + + var o = await observer(); + + var docId1 = await ins({ x: 1, y: 1222 }); + var docId2 = await ins({ x: 5, y: 5222 }); + + test.length(o.output, 2); + test.equal(o.output, [{ added: docId1 }, { added: docId2 }]); + clearOutput(o); + + var docId3 = await ins({ x: 7, y: 7222 }); + test.length(o.output, 0); + + var docId4 = await ins({ x: -1, y: -1222 }); + + // Becomes [docId4 docId1 | docId2 docId3] + test.length(o.output, 2); + test.isTrue( + setsEqual(o.output, [{ added: docId4 }, { removed: docId2 }]) + ); + + test.equal(Object.keys(o.state).length, 2); + test.equal(o.state[docId4], {_id: docId4, y: -1222}); + test.equal(o.state[docId1], {_id: docId1, y: 1222}); + clearOutput(o); + + await rem(docId2); + // Becomes [docId4 docId1 | docId3] + test.length(o.output, 0); + + await rem(docId4); + // Becomes [docId1 docId3] + test.length(o.output, 2); + test.isTrue( + setsEqual(o.output, [{ added: docId3 }, { removed: docId4 }]) + ); + + test.equal(Object.keys(o.state).length, 2); + test.equal(o.state[docId3], {_id: docId3, y: 7222}); + test.equal(o.state[docId1], {_id: docId1, y: 1222}); + clearOutput(o); + + onComplete(); + } + ); + + Tinytest.addAsync( + 'mongo-livedata - observe sorted, limited, big initial set ' + + idGeneration, + async function(test) { + var run = test.runId(); + var coll = new Mongo.Collection( + 'observeLimit-' + run, + collectionOptions + ); + + var observer = async function() { + var state = {}; + var output = []; + var callbacks = { + changed: function(newDoc) { + output.push({ changed: newDoc._id }); + state[newDoc._id] = newDoc; + }, + added: function(newDoc) { + output.push({ added: newDoc._id }); + state[newDoc._id] = newDoc; + }, + removed: function(oldDoc) { + output.push({ removed: oldDoc._id }); + delete state[oldDoc._id]; + }, + }; + var handle = await coll + .find({}, { sort: { x: 1, y: 1 }, limit: 3 }) + .observe(callbacks); + + return { output: output, handle: handle, state: state }; + }; + const clearOutput = function(o) { + o.output.splice(0, o.output.length); + }; + const ins = async function(doc) { + let id; + await runInFence(async function() { + id = await coll.insertAsync(doc); + }); + return id; + }; + const rem = async function(id) { + await runInFence(async function() { + await coll.removeAsync(id); + }); + }; + // tests '_id' subfields for all documents in oplog buffer + var testOplogBufferIds = function(ids) { + var bufferIds = []; + o.handle._multiplexer._observeDriver._unpublishedBuffer.forEach( + function(x, id) { + bufferIds.push(id); + } + ); + + test.isTrue( + setsEqual(ids, bufferIds), + 'expected: ' + ids + '; got: ' + bufferIds + ); + }; + var testSafeAppendToBufferFlag = function(expected) { + if (expected) { + test.isTrue( + o.handle._multiplexer._observeDriver._safeAppendToBuffer + ); + } else { + test.isFalse( + o.handle._multiplexer._observeDriver._safeAppendToBuffer + ); + } + }; + + var ids = {}; + let i = 0; + for (const x of [2, 4, 1, 3, 5, 5, 9, 1, 3, 2, 5]) { + ids[i] = await ins({ x, y: i }); + i++; + } + + // Ensure that we are past all the 'i' entries before we run the query, so + // that we get the expected phase transitions. + await waitUntilOplogCaughtUp(); + + var o = await observer(); + var usesOplog = o.handle._multiplexer._observeDriver._usesOplog; + // x: [1 1 2 | 2 3 3] 4 5 5 5 9 + // id: [2 7 0 | 9 3 8] 1 4 5 10 6 + + test.length(o.output, 3); test.isTrue( - setsEqual(ids, bufferIds), - 'expected: ' + ids + '; got: ' + bufferIds + setsEqual( + [{ added: ids[2] }, { added: ids[7] }, { added: ids[0] }], + o.output + ) ); - }; - var testSafeAppendToBufferFlag = function(expected) { - if (expected) { - test.isTrue( - o.handle._multiplexer._observeDriver._safeAppendToBuffer - ); - } else { - test.isFalse( - o.handle._multiplexer._observeDriver._safeAppendToBuffer - ); - } - }; + usesOplog && testOplogBufferIds([ids[9], ids[3], ids[8]]); + usesOplog && testSafeAppendToBufferFlag(false); + clearOutput(o); - var ids = {}; - let i = 0; - for (const x of [2, 4, 1, 3, 5, 5, 9, 1, 3, 2, 5]) { - ids[i] = await ins({ x, y: i }); - i++; + await rem(ids[0]); + // x: [1 1 2 | 3 3] 4 5 5 5 9 + // id: [2 7 9 | 3 8] 1 4 5 10 6 + test.length(o.output, 2); + test.isTrue( + setsEqual([{ removed: ids[0] }, { added: ids[9] }], o.output) + ); + usesOplog && testOplogBufferIds([ids[3], ids[8]]); + usesOplog && testSafeAppendToBufferFlag(false); + clearOutput(o); + + await rem(ids[7]); + // x: [1 2 3 | 3] 4 5 5 5 9 + // id: [2 9 3 | 8] 1 4 5 10 6 + test.length(o.output, 2); + test.isTrue( + setsEqual([{ removed: ids[7] }, { added: ids[3] }], o.output) + ); + usesOplog && testOplogBufferIds([ids[8]]); + usesOplog && testSafeAppendToBufferFlag(false); + clearOutput(o); + + await rem(ids[3]); + // x: [1 2 3 | 4 5 5] 5 9 + // id: [2 9 8 | 1 4 5] 10 6 + test.length(o.output, 2); + test.isTrue( + setsEqual([{ removed: ids[3] }, { added: ids[8] }], o.output) + ); + usesOplog && testOplogBufferIds([ids[1], ids[4], ids[5]]); + usesOplog && testSafeAppendToBufferFlag(false); + clearOutput(o); + + await rem({ x: { $lt: 4 } }); + // x: [4 5 5 | 5 9] + // id: [1 4 5 | 10 6] + test.length(o.output, 6); + test.isTrue( + setsEqual( + [ + { removed: ids[2] }, + { removed: ids[9] }, + { removed: ids[8] }, + { added: ids[5] }, + { added: ids[4] }, + { added: ids[1] }, + ], + o.output + ) + ); + usesOplog && testOplogBufferIds([ids[10], ids[6]]); + usesOplog && testSafeAppendToBufferFlag(true); + clearOutput(o); } - - // Ensure that we are past all the 'i' entries before we run the query, so - // that we get the expected phase transitions. - await waitUntilOplogCaughtUp(); - - var o = await observer(); - var usesOplog = o.handle._multiplexer._observeDriver._usesOplog; - // x: [1 1 2 | 2 3 3] 4 5 5 5 9 - // id: [2 7 0 | 9 3 8] 1 4 5 10 6 - - test.length(o.output, 3); - - test.isTrue( - setsEqual( - [{ added: ids[2] }, { added: ids[7] }, { added: ids[0] }], - o.output - ) - ); - usesOplog && testOplogBufferIds([ids[9], ids[3], ids[8]]); - usesOplog && testSafeAppendToBufferFlag(false); - clearOutput(o); - - await rem(ids[0]); - // x: [1 1 2 | 3 3] 4 5 5 5 9 - // id: [2 7 9 | 3 8] 1 4 5 10 6 - test.length(o.output, 2); - test.isTrue( - setsEqual([{ removed: ids[0] }, { added: ids[9] }], o.output) - ); - usesOplog && testOplogBufferIds([ids[3], ids[8]]); - usesOplog && testSafeAppendToBufferFlag(false); - clearOutput(o); - - await rem(ids[7]); - // x: [1 2 3 | 3] 4 5 5 5 9 - // id: [2 9 3 | 8] 1 4 5 10 6 - test.length(o.output, 2); - test.isTrue( - setsEqual([{ removed: ids[7] }, { added: ids[3] }], o.output) - ); - usesOplog && testOplogBufferIds([ids[8]]); - usesOplog && testSafeAppendToBufferFlag(false); - clearOutput(o); - - await rem(ids[3]); - // x: [1 2 3 | 4 5 5] 5 9 - // id: [2 9 8 | 1 4 5] 10 6 - test.length(o.output, 2); - test.isTrue( - setsEqual([{ removed: ids[3] }, { added: ids[8] }], o.output) - ); - usesOplog && testOplogBufferIds([ids[1], ids[4], ids[5]]); - usesOplog && testSafeAppendToBufferFlag(false); - clearOutput(o); - - await rem({ x: { $lt: 4 } }); - // x: [4 5 5 | 5 9] - // id: [1 4 5 | 10 6] - test.length(o.output, 6); - test.isTrue( - setsEqual( - [ - { removed: ids[2] }, - { removed: ids[9] }, - { removed: ids[8] }, - { added: ids[5] }, - { added: ids[4] }, - { added: ids[1] }, - ], - o.output - ) - ); - usesOplog && testOplogBufferIds([ids[10], ids[6]]); - usesOplog && testSafeAppendToBufferFlag(true); - clearOutput(o); - } - ); + ); + } } @@ -1775,21 +1781,19 @@ const setsEqual = function (a, b) { self.collectionOptions ); let obs; - const expectAdd = expect(function(doc) { - test.equal(doc.seconds(), 50); - }); - var expectRemove = expect(function(doc) { - test.equal(doc.seconds(), 50); - obs.stop(); - }); const id = await self.coll.insertAsync( { d: new Date(1356152390004) }, ); test.isTrue(id); var cursor = self.coll.find(); obs = await cursor.observe({ - added: expectAdd, - removed: expectRemove, + added: (doc) => { + test.equal(doc.seconds(), 50); + }, + removed: function(doc) { + test.equal(doc.seconds(), 50); + obs.stop(); + } }); test.equal(await cursor.countAsync(), 1); test.equal((await cursor.fetchAsync())[0].seconds(), 50); @@ -1817,7 +1821,7 @@ const setsEqual = function (a, b) { test.isTrue(id); self.id1 = id; - id = await self.coll.insertAsync({ d: new Date(1356152391004) }); + id = await self.coll.insertAsync({ d: new Date(1356152390004) }); self.id2 = id; }, ] @@ -3047,6 +3051,7 @@ async function functionChain2Upsert(test, expect, coll, index) { test.equal(o.name, 'foo'); } +// FLACKY: some times we get timeout when run all tests at same time Object.entries({ collectionInsert: collectionInsert, collectionUpsert: collectionUpsert, @@ -3513,33 +3518,79 @@ if (Meteor.isServer) { ); } -// This is a VERY white-box test. -Meteor.isServer && - Tinytest.addAsync('mongo-livedata - oplog - _disableOplog', async function(test) { - var collName = Random.id(); - var coll = new Mongo.Collection(collName); - if (MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle) { - var observeWithOplog = await coll - .find({ x: 5 }) +if (IS_OPLOG) { + Meteor.isServer && + Tinytest.addAsync('mongo-livedata - oplog - _disableOplog', async function(test) { + var collName = Random.id(); + var coll = new Mongo.Collection(collName); + if (MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle) { + var observeWithOplog = await coll + .find({ x: 5 }) + .observeChanges({ added: function() {} }); + test.isTrue(observeWithOplog._multiplexer._observeDriver._usesOplog); + await observeWithOplog.stop(); + } + var observeWithoutOplog = await coll + .find({ x: 6 }, { _disableOplog: true }) .observeChanges({ added: function() {} }); - test.isTrue(observeWithOplog._multiplexer._observeDriver._usesOplog); - await observeWithOplog.stop(); - } - var observeWithoutOplog = await coll - .find({ x: 6 }, { _disableOplog: true }) - .observeChanges({ added: function() {} }); - test.isFalse(observeWithoutOplog._multiplexer._observeDriver._usesOplog); - await observeWithoutOplog.stop(); - }); + test.isFalse(observeWithoutOplog._multiplexer._observeDriver._usesOplog); + await observeWithoutOplog.stop(); + }); -Meteor.isServer && - Tinytest.addAsync( - 'mongo-livedata - oplog - include selector fields', - async function(test) { - var collName = 'includeSelector' + Random.id(); + Meteor.isServer && IS_OPLOG && + Tinytest.addAsync( + 'mongo-livedata - oplog - include selector fields', + async function(test) { + var collName = 'includeSelector' + Random.id(); + var coll = new Mongo.Collection(collName); + + var docId = await coll.insertAsync({ a: 1, b: [3, 2], c: 'foo' }); + test.isTrue(docId); + + // Wait until we've processed the insert oplog entry. (If the insert shows up + // during the observeChanges, the bug in question is not consistently + // reproduced.) We don't have to do this for polling observe (eg + // --disable-oplog). + await waitUntilOplogCaughtUp(); + + var output = []; + var handle = await coll + .find({ a: 1, b: 2 }, { fields: { c: 1 } }) + .observeChanges({ + added: function(id, fields) { + output.push(['added', id, fields]); + }, + changed: function(id, fields) { + output.push(['changed', id, fields]); + }, + removed: function(id) { + output.push(['removed', id]); + }, + }); + // Initially should match the document. + test.length(output, 1); + test.equal(output.shift(), ['added', docId, { c: 'foo' }]); + + // Update in such a way that, if we only knew about the published field 'c' + // and the changed field 'b' (but not the field 'a'), we would think it didn't + // match any more. (This is a regression test for a bug that existed because + // we used to not use the shared projection in the initial query.) + await runInFence(async function() { + await coll.updateAsync(docId, { $set: { 'b.0': 2, c: 'bar' } }); + }); + test.length(output, 1); + test.equal(output.shift(), ['changed', docId, { c: 'bar' }]); + + handle.stop(); + } + ); + + Meteor.isServer && IS_OPLOG && + Tinytest.addAsync('mongo-livedata - oplog - transform', async function(test) { + var collName = 'oplogTransform' + Random.id(); var coll = new Mongo.Collection(collName); - var docId = await coll.insertAsync({ a: 1, b: [3, 2], c: 'foo' }); + var docId = await coll.insertAsync({ a: 25, x: { x: 5, y: 9 } }); test.isTrue(docId); // Wait until we've processed the insert oplog entry. (If the insert shows up @@ -3548,168 +3599,123 @@ Meteor.isServer && // --disable-oplog). await waitUntilOplogCaughtUp(); - var output = []; - var handle = await coll - .find({ a: 1, b: 2 }, { fields: { c: 1 } }) - .observeChanges({ - added: function(id, fields) { - output.push(['added', id, fields]); + var cursor = coll.find( + {}, + { + transform: function(doc) { + return doc.x; }, - changed: function(id, fields) { - output.push(['changed', id, fields]); - }, - removed: function(id) { - output.push(['removed', id]); - }, - }); - // Initially should match the document. - test.length(output, 1); - test.equal(output.shift(), ['added', docId, { c: 'foo' }]); + } + ); - // Update in such a way that, if we only knew about the published field 'c' - // and the changed field 'b' (but not the field 'a'), we would think it didn't - // match any more. (This is a regression test for a bug that existed because - // we used to not use the shared projection in the initial query.) - await runInFence(async function() { - await coll.updateAsync(docId, { $set: { 'b.0': 2, c: 'bar' } }); - }); - test.length(output, 1); - test.equal(output.shift(), ['changed', docId, { c: 'bar' }]); - - handle.stop(); - } - ); - -Meteor.isServer && - Tinytest.addAsync('mongo-livedata - oplog - transform', async function(test) { - var collName = 'oplogTransform' + Random.id(); - var coll = new Mongo.Collection(collName); - - var docId = await coll.insertAsync({ a: 25, x: { x: 5, y: 9 } }); - test.isTrue(docId); - - // Wait until we've processed the insert oplog entry. (If the insert shows up - // during the observeChanges, the bug in question is not consistently - // reproduced.) We don't have to do this for polling observe (eg - // --disable-oplog). - await waitUntilOplogCaughtUp(); - - var cursor = coll.find( - {}, - { - transform: function(doc) { - return doc.x; + var changesOutput = []; + var changesHandle = await cursor.observeChanges({ + added: function(id, fields) { + changesOutput.push(['added', fields]); }, + }); + // We should get untransformed fields via observeChanges. + test.length(changesOutput, 1); + test.equal(changesOutput.shift(), ['added', { a: 25, x: { x: 5, y: 9 } }]); + await changesHandle.stop(); + + var transformedOutput = []; + var transformedHandle = await cursor.observe({ + added: function(doc) { + transformedOutput.push(['added', doc]); + }, + }); + test.length(transformedOutput, 1); + test.equal(transformedOutput.shift(), ['added', { x: 5, y: 9 }]); + await transformedHandle.stop(); + }); + + + Meteor.isServer && IS_OPLOG && + Tinytest.addAsync('mongo-livedata - oplog - drop collection/db', async function(test) { + // This test uses a random database, so it can be dropped without affecting + // anything else. + var mongodbUri = Npm.require('mongodb-uri'); + var parsedUri = mongodbUri.parse(process.env.MONGO_URL); + parsedUri.database = 'dropDB' + Random.id(); + var driver = new MongoInternals.RemoteCollectionDriver( + mongodbUri.format(parsedUri), + { + oplogUrl: process.env.MONGO_OPLOG_URL, + } + ); + + var collName = 'dropCollection' + Random.id(); + var coll = new Mongo.Collection(collName, { _driver: driver }); + + var doc1Id = await coll.insertAsync({ a: 'foo', c: 1 }); + var doc2Id = await coll.insertAsync({ b: 'bar' }); + var doc3Id = await coll.insertAsync({ a: 'foo', c: 2 }); + var tmp; + + var output = []; + var handle = await coll.find({ a: 'foo' }).observeChanges({ + added: function(id, fields) { + output.push(['added', id, fields]); + }, + changed: function(id) { + output.push(['changed']); + }, + removed: function(id) { + output.push(['removed', id]); + }, + }); + test.length(output, 2); + // make order consistent + if (output.length === 2 && output[0][1] === doc3Id) { + tmp = output[0]; + output[0] = output[1]; + output[1] = tmp; } - ); + test.equal(output.shift(), ['added', doc1Id, { a: 'foo', c: 1 }]); + test.equal(output.shift(), ['added', doc3Id, { a: 'foo', c: 2 }]); - var changesOutput = []; - var changesHandle = await cursor.observeChanges({ - added: function(id, fields) { - changesOutput.push(['added', fields]); - }, - }); - // We should get untransformed fields via observeChanges. - test.length(changesOutput, 1); - test.equal(changesOutput.shift(), ['added', { a: 25, x: { x: 5, y: 9 } }]); - await changesHandle.stop(); + // Wait until we've processed the insert oplog entry, so that we are in a + // steady state (and we don't see the dropped docs because we are FETCHING). + await waitUntilOplogCaughtUp(); - var transformedOutput = []; - var transformedHandle = await cursor.observe({ - added: function(doc) { - transformedOutput.push(['added', doc]); - }, - }); - test.length(transformedOutput, 1); - test.equal(transformedOutput.shift(), ['added', { x: 5, y: 9 }]); - await transformedHandle.stop(); - }); + // Drop the collection. Should remove all docs. + await runInFence(async function() { + await coll.dropCollectionAsync(); + }); - -Meteor.isServer && - Tinytest.addAsync('mongo-livedata - oplog - drop collection/db', async function(test) { - // This test uses a random database, so it can be dropped without affecting - // anything else. - var mongodbUri = Npm.require('mongodb-uri'); - var parsedUri = mongodbUri.parse(process.env.MONGO_URL); - parsedUri.database = 'dropDB' + Random.id(); - var driver = new MongoInternals.RemoteCollectionDriver( - mongodbUri.format(parsedUri), - { - oplogUrl: process.env.MONGO_OPLOG_URL, + test.length(output, 2); + // make order consistent + if (output.length === 2 && output[0][1] === doc3Id) { + tmp = output[0]; + output[0] = output[1]; + output[1] = tmp; } - ); + test.equal(output.shift(), ['removed', doc1Id]); + test.equal(output.shift(), ['removed', doc3Id]); - var collName = 'dropCollection' + Random.id(); - var coll = new Mongo.Collection(collName, { _driver: driver }); + // Put something back in. + var doc4Id; + await runInFence(async function() { + doc4Id = await coll.insertAsync({ a: 'foo', c: 3 }); + }); - var doc1Id = await coll.insertAsync({ a: 'foo', c: 1 }); - var doc2Id = await coll.insertAsync({ b: 'bar' }); - var doc3Id = await coll.insertAsync({ a: 'foo', c: 2 }); - var tmp; + test.length(output, 1); + test.equal(output.shift(), ['added', doc4Id, { a: 'foo', c: 3 }]); - var output = []; - var handle = await coll.find({ a: 'foo' }).observeChanges({ - added: function(id, fields) { - output.push(['added', id, fields]); - }, - changed: function(id) { - output.push(['changed']); - }, - removed: function(id) { - output.push(['removed', id]); - }, + // XXX: this was intermittently failing for unknown reasons. + // Now drop the database. Should remove all docs again. + // runInFence(function () { + // driver.mongo.dropDatabase(); + // }); + // + // test.length(output, 1); + // test.equal(output.shift(), ['removed', doc4Id]); + + await handle.stop(); + driver.mongo.close(); }); - test.length(output, 2); - // make order consistent - if (output.length === 2 && output[0][1] === doc3Id) { - tmp = output[0]; - output[0] = output[1]; - output[1] = tmp; - } - test.equal(output.shift(), ['added', doc1Id, { a: 'foo', c: 1 }]); - test.equal(output.shift(), ['added', doc3Id, { a: 'foo', c: 2 }]); - - // Wait until we've processed the insert oplog entry, so that we are in a - // steady state (and we don't see the dropped docs because we are FETCHING). - await waitUntilOplogCaughtUp(); - - // Drop the collection. Should remove all docs. - await runInFence(async function() { - await coll.dropCollectionAsync(); - }); - - test.length(output, 2); - // make order consistent - if (output.length === 2 && output[0][1] === doc3Id) { - tmp = output[0]; - output[0] = output[1]; - output[1] = tmp; - } - test.equal(output.shift(), ['removed', doc1Id]); - test.equal(output.shift(), ['removed', doc3Id]); - - // Put something back in. - var doc4Id; - await runInFence(async function() { - doc4Id = await coll.insertAsync({ a: 'foo', c: 3 }); - }); - - test.length(output, 1); - test.equal(output.shift(), ['added', doc4Id, { a: 'foo', c: 3 }]); - - // XXX: this was intermittently failing for unknown reasons. - // Now drop the database. Should remove all docs again. - // runInFence(function () { - // driver.mongo.dropDatabase(); - // }); - // - // test.length(output, 1); - // test.equal(output.shift(), ['removed', doc4Id]); - - await handle.stop(); - driver.mongo.close(); - }); +} var TestCustomType = function (head, tail) { // use different field names on the object than in JSON, to ensure we are @@ -3738,116 +3744,118 @@ EJSON.addType('someCustomType', function (json) { return new TestCustomType(json.head, json.tail); }); -testAsyncMulti('mongo-livedata - oplog - update EJSON', [ - async function(test, expect) { - var self = this; - var collectionName = 'ejson' + Random.id(); - if (Meteor.isClient) { - await Meteor.callAsync('createInsecureCollection', collectionName); - Meteor.subscribe('c-' + collectionName, expect()); - } +if(IS_OPLOG) { + testAsyncMulti('mongo-livedata - oplog - update EJSON', [ + async function(test, expect) { + var self = this; + var collectionName = 'ejson' + Random.id(); + if (Meteor.isClient) { + await Meteor.callAsync('createInsecureCollection', collectionName); + Meteor.subscribe('c-' + collectionName, expect()); + } - self.collection = new Mongo.Collection(collectionName); - self.date = new Date(); - self.objId = new Mongo.ObjectID(); + self.collection = new Mongo.Collection(collectionName); + self.date = new Date(); + self.objId = new Mongo.ObjectID(); - self.id = await self.collection.insertAsync( - { - d: self.date, - oi: self.objId, - custom: new TestCustomType('a', 'b'), - }, - ); - }, - async function(test, expect) { - var self = this; - self.changes = []; - self.handle = await self.collection.find({}).observeChanges({ - added: function(id, fields) { - self.changes.push(['a', id, fields]); - }, - changed: function(id, fields) { - self.changes.push(['c', id, fields]); - }, - removed: function(id) { - self.changes.push(['r', id]); - }, - }); - test.length(self.changes, 1); - test.equal(self.changes.shift(), [ - 'a', - self.id, - { d: self.date, oi: self.objId, custom: new TestCustomType('a', 'b') }, - ]); - - // First, replace the entire custom object. - // (runInFence is useful for the server, using expect() is useful for the - // client) - await runInFence(async function() { - await self.collection.updateAsync( - self.id, + self.id = await self.collection.insertAsync( { - $set: { custom: new TestCustomType('a', 'c') }, + d: self.date, + oi: self.objId, + custom: new TestCustomType('a', 'b'), }, - { returnServerResultPromise: true } ); - }); - }, - async function(test, expect) { - var self = this; - test.length(self.changes, 1); - test.equal(self.changes.shift(), [ - 'c', - self.id, - { custom: new TestCustomType('a', 'c') }, - ]); - - // Now, sneakily replace just a piece of it. Meteor won't do this, but - // perhaps you are accessing Mongo directly. - await runInFence(async function() { - await self.collection.updateAsync( + }, + async function(test, expect) { + var self = this; + self.changes = []; + self.handle = await self.collection.find({}).observeChanges({ + added: function(id, fields) { + self.changes.push(['a', id, fields]); + }, + changed: function(id, fields) { + self.changes.push(['c', id, fields]); + }, + removed: function(id) { + self.changes.push(['r', id]); + }, + }); + test.length(self.changes, 1); + test.equal(self.changes.shift(), [ + 'a', self.id, - { - $set: { 'custom.EJSON$value.EJSONtail': 'd' }, - }, - { returnServerResultPromise: true } - ); - }); - }, - async function(test, expect) { - var self = this; - test.length(self.changes, 1); - test.equal(self.changes.shift(), [ - 'c', - self.id, - { custom: new TestCustomType('a', 'd') }, - ]); + { d: self.date, oi: self.objId, custom: new TestCustomType('a', 'b') }, + ]); - // Update a date and an ObjectID too. - self.date2 = new Date(self.date.valueOf() + 1000); - self.objId2 = new Mongo.ObjectID(); - await runInFence(async function() { - await self.collection.updateAsync( + // First, replace the entire custom object. + // (runInFence is useful for the server, using expect() is useful for the + // client) + await runInFence(async function() { + await self.collection.updateAsync( + self.id, + { + $set: { custom: new TestCustomType('a', 'c') }, + }, + { returnServerResultPromise: true } + ); + }); + }, + async function(test, expect) { + var self = this; + test.length(self.changes, 1); + test.equal(self.changes.shift(), [ + 'c', self.id, - { - $set: { d: self.date2, oi: self.objId2 }, - }, - { returnServerResultPromise: true } - ); - }); - }, - function(test, expect) { - var self = this; - test.length(self.changes, 1); - test.equal(self.changes.shift(), [ - 'c', - self.id, - { d: self.date2, oi: self.objId2 }, - ]); + { custom: new TestCustomType('a', 'c') }, + ]); - self.handle.stop(); - }, -]); + // Now, sneakily replace just a piece of it. Meteor won't do this, but + // perhaps you are accessing Mongo directly. + await runInFence(async function() { + await self.collection.updateAsync( + self.id, + { + $set: { 'custom.EJSON$value.EJSONtail': 'd' }, + }, + { returnServerResultPromise: true } + ); + }); + }, + async function(test, expect) { + var self = this; + test.length(self.changes, 1); + test.equal(self.changes.shift(), [ + 'c', + self.id, + { custom: new TestCustomType('a', 'd') }, + ]); + + // Update a date and an ObjectID too. + self.date2 = new Date(self.date.valueOf() + 1000); + self.objId2 = new Mongo.ObjectID(); + await runInFence(async function() { + await self.collection.updateAsync( + self.id, + { + $set: { d: self.date2, oi: self.objId2 }, + }, + { returnServerResultPromise: true } + ); + }); + }, + function(test, expect) { + var self = this; + test.length(self.changes, 1); + test.equal(self.changes.shift(), [ + 'c', + self.id, + { d: self.date2, oi: self.objId2 }, + ]); + + self.handle.stop(); + }, + ]); +} async function waitUntilOplogCaughtUp() { @@ -3908,7 +3916,7 @@ testAsyncMulti('mongo-livedata - undefined find options', [ ]); // Regression test for #2274. -Meteor.isServer && +Meteor.isServer && IS_OPLOG && testAsyncMulti('mongo-livedata - observe limit bug', [ async function(test, expect) { var self = this; @@ -4567,7 +4575,7 @@ Meteor.isServer && testAsyncMulti( let insertId; await Collection.find({}).observeChangesAsync({ added(id) { - insertId = _id; + insertId = id; throw new Error('Test error in sync added observeChangesAsync'); }, }); diff --git a/packages/mongo/tests/observe_changes_tests.js b/packages/mongo/tests/observe_changes_tests.js index 8dd50eed5d..b17e19ea60 100644 --- a/packages/mongo/tests/observe_changes_tests.js +++ b/packages/mongo/tests/observe_changes_tests.js @@ -309,17 +309,18 @@ if (Meteor.isServer) { fooid, { noodles: 'alright', bacon: undefined }, ]); - - // Doesn't get update event, since modifies only hidden fields - await logger.expectNoResult(async () => { - await c.updateAsync(fooid, { - noodles: 'alright', - potatoes: 'meh', - apples: 'ok', - mac: 1, - cheese: 2, + const observerDriver = handle._multiplexer._observeDriver + if (!(observerDriver._usesChangeStreams)) { + await logger.expectNoResult(async () => { + await c.updateAsync(fooid, { + noodles: 'alright', + potatoes: 'meh', + apples: 'ok', + mac: 1, + cheese: 2, + }) }); - }); + } await c.removeAsync(fooid); await logger.expectResultOnly('removed', [fooid]); @@ -418,7 +419,6 @@ Tinytest.addAsync( ); - Tinytest.addAsync( 'observeChanges - unordered - enters and exits result set through change', async function(test, onComplete) { @@ -519,8 +519,8 @@ if (Meteor.isServer) { const [resolver1, promise1] = getPromiseAndResolver(); const [resolver2, promise2] = getPromiseAndResolver(); - await self.insert({x: 2, y: 3}); self.expects.push(resolver1, resolver2); + await self.insert({x: 2, y: 3}); await self.insert({x: 3, y: 7}); // filtered out by the query await self.insert({x: 4}); // Expect two added calls to happen. diff --git a/packages/mongo/tests/oplog_tests.js b/packages/mongo/tests/oplog_tests.js index 0ef4bf8089..53a4855dde 100644 --- a/packages/mongo/tests/oplog_tests.js +++ b/packages/mongo/tests/oplog_tests.js @@ -3,6 +3,11 @@ import { MiniMongoQueryError } from 'meteor/minimongo/common'; var randomId = Random.id(); var OplogCollection = new Mongo.Collection("oplog-" + randomId); +const DEFAULT_REACTIVITY = process.env.METEOR_REACTIVITY_ORDER ? process.env.METEOR_REACTIVITY_ORDER.split(',') : undefined; +var IS_OPLOG = DEFAULT_REACTIVITY && DEFAULT_REACTIVITY[0] === 'oplog'; + +if(!IS_OPLOG) return + Tinytest.addAsync('mongo-livedata - oplog - cursorSupported', async function( test ) { diff --git a/packages/rate-limit/package.js b/packages/rate-limit/package.js index 8b5f453c01..6c99804705 100644 --- a/packages/rate-limit/package.js +++ b/packages/rate-limit/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'rate-limit', - version: '1.1.2', + version: '1.2.0-beta350.7', // Brief, one-line summary of the package. summary: 'An algorithm for rate limiting anything', // URL to the Git repository containing the source code for this package. @@ -10,14 +10,14 @@ Package.describe({ documentation: 'README.md', }); -Package.onUse(function(api) { +Package.onUse(function (api) { api.use('random'); api.use('ecmascript'); api.mainModule('rate-limit.js'); api.export('RateLimiter'); }); -Package.onTest(function(api) { +Package.onTest(function (api) { api.use('test-helpers', ['client', 'server']); api.use('ecmascript'); api.use('random'); diff --git a/packages/rate-limit/rate-limit.js b/packages/rate-limit/rate-limit.js index 73f0578d9e..ab6ff30299 100644 --- a/packages/rate-limit/rate-limit.js +++ b/packages/rate-limit/rate-limit.js @@ -57,6 +57,23 @@ class Rule { }); } + async matchAsync(input) { + for (const [key, matcher] of Object.entries(this._matchers)) { + if (matcher !== null) { + if (!hasOwn.call(input, key)) { + return false; + } else if (typeof matcher === 'function') { + if (!(await matcher(input[key]))) { + return false; + } + } else if (matcher !== input[key]) { + return false; + } + } + }; + return true; + } + // Generates unique key string for provided input by concatenating all the // keys in the matcher with the corresponding values in the input. // Only called if rule matches input. @@ -143,45 +160,63 @@ class RateLimiter { const matchedRules = this._findAllMatchingRules(input); matchedRules.forEach((rule) => { const ruleResult = rule.apply(input); - let numInvocations = rule.counters[ruleResult.key]; - - if (ruleResult.timeToNextReset < 0) { - // Reset all the counters since the rule has reset - rule.resetCounter(); - ruleResult.timeSinceLastReset = new Date().getTime() - - rule._lastResetTime; - ruleResult.timeToNextReset = rule.options.intervalTime; - numInvocations = 0; - } - - if (numInvocations > rule.options.numRequestsAllowed) { - // Only update timeToReset if the new time would be longer than the - // previously set time. This is to ensure that if this input triggers - // multiple rules, we return the longest period of time until they can - // successfully make another call - if (reply.timeToReset < ruleResult.timeToNextReset) { - reply.timeToReset = ruleResult.timeToNextReset; - } - reply.allowed = false; - reply.numInvocationsLeft = 0; - reply.ruleId = rule.id; - rule._executeCallback(reply, input); - } else { - // If this is an allowed attempt and we haven't failed on any of the - // other rules that match, update the reply field. - if (rule.options.numRequestsAllowed - numInvocations < - reply.numInvocationsLeft && reply.allowed) { - reply.timeToReset = ruleResult.timeToNextReset; - reply.numInvocationsLeft = rule.options.numRequestsAllowed - - numInvocations; - } - reply.ruleId = rule.id; - rule._executeCallback(reply, input); - } + this._handleRuleResult(rule, ruleResult, reply, input); }); return reply; } + checkRules(rules, input) { + const reply = { + allowed: true, + timeToReset: 0, + numInvocationsLeft: Infinity, + }; + + rules.forEach((rule) => { + const ruleResult = rule.apply(input); + this._handleRuleResult(rule, ruleResult, reply, input); + }); + return reply; + } + + _handleRuleResult(rule, ruleResult, reply, input) { + let numInvocations = rule.counters[ruleResult.key]; + + if (ruleResult.timeToNextReset < 0) { + // Reset all the counters since the rule has reset + rule.resetCounter(); + ruleResult.timeSinceLastReset = new Date().getTime() - + rule._lastResetTime; + ruleResult.timeToNextReset = rule.options.intervalTime; + numInvocations = 0; + } + + if (numInvocations > rule.options.numRequestsAllowed) { + // Only update timeToReset if the new time would be longer than the + // previously set time. This is to ensure that if this input triggers + // multiple rules, we return the longest period of time until they can + // successfully make another call + if (reply.timeToReset < ruleResult.timeToNextReset) { + reply.timeToReset = ruleResult.timeToNextReset; + } + reply.allowed = false; + reply.numInvocationsLeft = 0; + reply.ruleId = rule.id; + rule._executeCallback(reply, input); + } else { + // If this is an allowed attempt and we haven't failed on any of the + // other rules that match, update the reply field. + if (rule.options.numRequestsAllowed - numInvocations < + reply.numInvocationsLeft && reply.allowed) { + reply.timeToReset = ruleResult.timeToNextReset; + reply.numInvocationsLeft = rule.options.numRequestsAllowed - + numInvocations; + } + reply.ruleId = rule.id; + rule._executeCallback(reply, input); + } + } + /** * Adds a rule to dictionary of rules that are checked against on every call. * Only inputs that pass all of the rules will be allowed. Returns unique rule @@ -228,22 +263,36 @@ class RateLimiter { increment(input) { // Only increment rule counters that match this input const matchedRules = this._findAllMatchingRules(input); - matchedRules.forEach((rule) => { - const ruleResult = rule.apply(input); + const _incrementForInput = (rule) => this._incrementRule(rule, input); + matchedRules.forEach(_incrementForInput); + } - if (ruleResult.timeSinceLastReset > rule.options.intervalTime) { - // Reset all the counters since the rule has reset - rule.resetCounter(); - } + /** + * Increment counters in every rule that match to this input + * @param {array} rules Array of rules to increment + * @param {object} input Dictionary object containing attributes that may + * match to rules + */ + incrementRules(rules, input) { + const _incrementForInput = (rule) => this._incrementRule(rule, input); + rules.forEach(_incrementForInput); + } - // Check whether the key exists, incrementing it if so or otherwise - // adding the key and setting its value to 1 - if (hasOwn.call(rule.counters, ruleResult.key)) { - rule.counters[ruleResult.key]++; - } else { - rule.counters[ruleResult.key] = 1; - } - }); + _incrementRule(rule, input) { + const ruleResult = rule.apply(input); + + if (ruleResult.timeSinceLastReset > rule.options.intervalTime) { + // Reset all the counters since the rule has reset + rule.resetCounter(); + } + + // Check whether the key exists, incrementing it if so or otherwise + // adding the key and setting its value to 1 + if (hasOwn.call(rule.counters, ruleResult.key)) { + rule.counters[ruleResult.key]++; + } else { + rule.counters[ruleResult.key] = 1; + } } // Returns an array of all rules that apply to provided input @@ -251,6 +300,16 @@ class RateLimiter { return Object.values(this.rules).filter(rule => rule.match(input)); } + async _findAllMatchingRulesAsync(input) { + const matches = []; + for (const rule of Object.values(this.rules)) { + if (await rule.matchAsync(input)) { + matches.push(rule); + } + } + return matches; + } + /** * Provides a mechanism to remove rules from the rate limiter. Returns boolean * about success. diff --git a/packages/socket-stream-client/browser.js b/packages/socket-stream-client/browser.js index 64b46b99ca..1f6442c3f5 100644 --- a/packages/socket-stream-client/browser.js +++ b/packages/socket-stream-client/browser.js @@ -5,9 +5,10 @@ import { import { StreamClientCommon } from "./common.js"; -// Statically importing SockJS here will prevent native WebSocket usage -// below (in favor of SockJS), but will ensure maximum compatibility for -// clients stuck in unusual networking environments. +// SockJS is imported statically to avoid the startup latency introduced by +// dynamic import() in _launchConnection(). When DISABLE_SOCKJS=1, SockJS +// remains bundled in the client, but the connection uses the native +// WebSocket path directly with no import-time delay. import SockJS from "./sockjs-1.6.1-min-.js"; export class ClientStream extends StreamClientCommon { @@ -157,20 +158,18 @@ export class ClientStream extends StreamClientCommon { _launchConnection() { this._cleanup(); // cleanup the old socket, if there was one. - var options = { - transports: this._sockjsProtocolsWhitelist(), - ...this.options._sockjsOptions - }; - - const hasSockJS = typeof SockJS === "function"; - const disableSockJS = __meteor_runtime_config__.DISABLE_SOCKJS; - - this.socket = hasSockJS && !disableSockJS + if (__meteor_runtime_config__.DISABLE_SOCKJS) { + this.socket = new WebSocket(toWebsocketUrl(this.rawUrl)); + } else { + const options = { + transports: this._sockjsProtocolsWhitelist(), + ...this.options._sockjsOptions + }; // Convert raw URL to SockJS URL each time we open a connection, so // that we can connect to random hostnames and get around browser // per-host connection limits. - ? new SockJS(toSockjsUrl(this.rawUrl), undefined, options) - : new WebSocket(toWebsocketUrl(this.rawUrl)); + this.socket = new SockJS(toSockjsUrl(this.rawUrl), undefined, options); + } this.socket.onopen = data => { this.lastError = null; diff --git a/packages/socket-stream-client/package.js b/packages/socket-stream-client/package.js index 28b94f29df..dfca3cba91 100644 --- a/packages/socket-stream-client/package.js +++ b/packages/socket-stream-client/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "socket-stream-client", - version: '0.6.1', + version: '0.7.0-beta350.7', summary: "Provides the ClientStream abstraction used by ddp-client", documentation: "README.md" }); @@ -12,7 +12,7 @@ Npm.depends({ "lodash.once": "4.1.1" }); -Package.onUse(function(api) { +Package.onUse(function (api) { api.use("ecmascript"); api.use("modern-browsers"); api.use("retry"); // TODO Try to remove this. @@ -23,7 +23,7 @@ Package.onUse(function(api) { api.mainModule("node.js", "server", { lazy: true }); }); -Package.onTest(function(api) { +Package.onTest(function (api) { api.use("ecmascript"); api.use("tinytest"); api.use("test-helpers"); diff --git a/packages/test-in-console/package.js b/packages/test-in-console/package.js index ebf1b6eff9..8ef9139aa4 100644 --- a/packages/test-in-console/package.js +++ b/packages/test-in-console/package.js @@ -1,11 +1,12 @@ Package.describe({ summary: 'Run tests noninteractively, with results going to the console.', - version: '2.0.1', + version: '2.0.2-beta350.7', }); -Package.onUse(function(api) { +Package.onUse(function (api) { api.use(['tinytest', 'random', 'ejson', 'check', 'ecmascript']); api.use('fetch', 'server'); + api.use('jquery', 'client'); api.export('TEST_STATUS', 'client'); diff --git a/packages/test-in-console/run.sh b/packages/test-in-console/run.sh index 5a88e9065d..5bdfb466eb 100755 --- a/packages/test-in-console/run.sh +++ b/packages/test-in-console/run.sh @@ -15,6 +15,7 @@ export PATH=$METEOR_HOME:$PATH export URL='http://127.0.0.1:4096/' export METEOR_PACKAGE_DIRS='packages/deprecated' +export METEOR_NO_DEPRECATION=true exec 3< <(./meteor test-packages --driver-package test-in-console -p 4096 --exclude ${TEST_PACKAGES_EXCLUDE:-''} $1) EXEC_PID=$! diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 6a2bb3d3ff..b8c528f072 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Serves a Meteor app over HTTP", - version: "2.1.0", + version: '2.2.0-beta350.7', }); Npm.depends({ diff --git a/packages/webapp/socket_file_tests.js b/packages/webapp/socket_file_tests.js index 2dbde1927f..143cb8ca4d 100644 --- a/packages/webapp/socket_file_tests.js +++ b/packages/webapp/socket_file_tests.js @@ -26,6 +26,49 @@ const isMacOS = () => { return platform() === 'darwin'; }; +const getGroupNameForGid = (gid) => { + try { + const data = readFileSync('/etc/group', 'utf8'); + const line = data + .trim() + .split('\n') + .find((groupLine) => { + const [, , groupGid] = groupLine.trim().split(':'); + return Number(groupGid) === gid; + }); + + if (!line) return null; + const [name] = line.trim().split(':'); + return name || null; + } catch { + return null; + } +}; + +const getWritableGroupName = () => { + const { gid, uid } = userInfo(); + const gidsToTry = new Set(); + + if (typeof gid === 'number') { + gidsToTry.add(gid); + } + + if (typeof process.getgroups === 'function') { + process.getgroups().forEach((groupId) => gidsToTry.add(groupId)); + } + + for (const groupId of gidsToTry) { + const groupName = getGroupNameForGid(groupId); + if (groupName) { + return groupName; + } + } + + if (Boolean(process.env.TRAVIS)) return 'travis'; + if (isMacOS()) return 'staff'; + return uid === 0 ? 'root' : null; +}; + const removeTestSocketFile = () => { try { unlinkSync(testSocketFile); @@ -131,9 +174,16 @@ testAsyncMulti( }, async (test) => { // use UNIX_SOCKET_PATH and UNIX_SOCKET_GROUP + const groupToUse = getWritableGroupName(); + + if (!groupToUse) { + // Skip when no writable group could be determined for the current user. + test.isTrue(true); + return; + } + const { httpServer, server } = prepareServer(); - const groupToUse = Boolean(process.env.TRAVIS) && 'travis' || (isMacOS() ? 'staff' : 'root'); process.env.UNIX_SOCKET_PATH = testSocketFile; process.env.UNIX_SOCKET_GROUP = groupToUse; process.env.UNIX_SOCKET_PERMISSIONS = '777'; diff --git a/scripts/admin/copy-dev-bundle-from-jenkins.sh b/scripts/admin/copy-dev-bundle-from-jenkins.sh index 58931c5162..bc7547764e 100755 --- a/scripts/admin/copy-dev-bundle-from-jenkins.sh +++ b/scripts/admin/copy-dev-bundle-from-jenkins.sh @@ -36,7 +36,7 @@ fi echo Found build $DIRNAME -trap "echo Found surprising number of tarballs." EXIT +trap "echo 'Found surprising number of tarballs.'; aws s3 ls s3://com.meteor.jenkins/$DIRNAME/" EXIT # Check to make sure the proper number of each kind of file is there. aws s3 ls s3://com.meteor.jenkins/$DIRNAME/ | \ perl -nle 'if (/\.tar\.gz/) { ++$TAR } else { die "something weird" } END { exit !($TAR == 4) }' diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index a7bdfd5d81..651f41ef02 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,7 +1,7 @@ { "track": "METEOR", - "version": "3.4-rc.4", + "version": "3.5-beta.7", "recommended": false, "official": false, "description": "Meteor experimental release" -} +} \ No newline at end of file diff --git a/scripts/admin/meteor-release-official.json b/scripts/admin/meteor-release-official.json index 45e025b42e..58c7344a50 100644 --- a/scripts/admin/meteor-release-official.json +++ b/scripts/admin/meteor-release-official.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "3.4", + "version": "3.5", "recommended": false, "official": true, "description": "The Official Meteor Distribution" diff --git a/scripts/build-dev-bundle-common.sh b/scripts/build-dev-bundle-common.sh index 4d20fff8d2..4798124f28 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=22.22.0 +NODE_VERSION=24.14.0 MONGO_VERSION_64BIT=7.0.16 MONGO_VERSION_32BIT=3.2.22 -NPM_VERSION=10.9.4 +NPM_VERSION=11.10.1 if [ "$UNAME" == "Linux" ] ; then diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index e2170b9774..7d437814c4 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -10,7 +10,7 @@ var packageJson = { dependencies: { // Explicit dependency because we are replacing it with a bundled version // and we want to make sure there are no dependencies on a higher version - npm: "10.9.4", + npm: "11.10.1", "node-gyp": "10.2.0", "node-gyp-build": "4.8.4", "@mapbox/node-pre-gyp": "1.0.11", diff --git a/tools/modern-tests/README.md b/tools/modern-tests/README.md new file mode 100644 index 0000000000..de681740c9 --- /dev/null +++ b/tools/modern-tests/README.md @@ -0,0 +1,20 @@ +# E2E Tests + +Isolated Jest + Playwright environment for end-to-end testing Meteor skeletons and bundler integrations. + +The repo root `node_modules/` is used to build the dev bundle, which becomes the Meteor tool itself. Installing test deps (jest, playwright, swc, cheerio, semver, underscore) there could pull in incompatible transitive versions (e.g. lru-cache v10 vs v5) and silently break the dev bundle build or a published Meteor release. This subfolder keeps test dependencies fully isolated so they never affect how Meteor is built or shipped. + +Tests create real Meteor projects, start dev servers, and assert behavior in a headless Chromium browser. + +All commands below should be run from the repo root: + +```sh +# Install dependencies (first time) +npm run install:e2e + +# Run all E2E tests +npm run test:e2e + +# Run a specific suite +npm run test:e2e -- --testPathPattern skeleton +``` diff --git a/tools/modern-tests/babel.config.js b/tools/modern-tests/babel.config.js deleted file mode 100644 index 0ae15759c3..0000000000 --- a/tools/modern-tests/babel.config.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - presets: [ - [ - '@babel/preset-env', - { - targets: { - node: 'current', - }, - }, - ], - ], - // This is needed to handle ES modules - sourceType: 'unambiguous', -}; diff --git a/tools/modern-tests/jest.config.js b/tools/modern-tests/jest.config.js index 160d7af6cf..f625e86ac5 100644 --- a/tools/modern-tests/jest.config.js +++ b/tools/modern-tests/jest.config.js @@ -11,9 +11,14 @@ module.exports = { transformIgnorePatterns: [ "/node_modules/(?!(execa|wait-on|is-docker|is-stream|human-signals|merge-stream|npm-run-path|onetime|mimic-fn|strip-final-newline|path-key|shebug-command|shebug-regex)/)" ], - // Use Babel to transform JavaScript files transform: { - "^.+\\.js$": "babel-jest" + "^.+\\.js$": ["@swc/jest", { + jsc: { + parser: { syntax: "ecmascript" }, + target: "es2022", + }, + module: { type: "commonjs" }, + }], }, // Playwright configuration globals: { diff --git a/tools/modern-tests/package-lock.json b/tools/modern-tests/package-lock.json index 79fbbe6ad8..eee71dcd5c 100644 --- a/tools/modern-tests/package-lock.json +++ b/tools/modern-tests/package-lock.json @@ -8,14 +8,16 @@ "name": "meteor-modern-tests", "version": "1.0.0", "devDependencies": { - "@babel/preset-env": "^7.21.3", - "babel-jest": "^29.0.0", + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", "cheerio": "^1.0.0-rc.12", "execa": "^5.1.1", "fs-extra": "^11.3.1", "jest": "^29.0.0", "jest-playwright-preset": "^3.0.1", "playwright": "1.58.0", + "semver": "^7.7.4", + "underscore": "^1.13.8", "wait-on": "^7.0.0" } }, @@ -89,6 +91,16 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", @@ -106,19 +118,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", @@ -136,61 +135,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/@babel/helper-globals": { @@ -203,20 +155,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -249,19 +187,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", @@ -272,56 +197,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -352,21 +227,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", @@ -397,103 +257,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -549,22 +312,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", @@ -749,979 +496,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1887,6 +661,58 @@ } } }, + "node_modules/@jest/create-cache-key-function": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", + "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -1964,6 +790,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -2025,19 +875,6 @@ "node": ">=10" } }, - "node_modules/@jest/reporters/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2233,6 +1070,250 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/jest": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", + "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^30.0.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2533,48 +1614,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -2739,6 +1778,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/caching-transform/node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -3008,20 +2057,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3430,16 +2465,6 @@ "node": ">=4" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3574,6 +2599,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/find-file-up": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", @@ -4278,6 +3313,16 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/istanbul-lib-processinfo": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", @@ -4912,19 +3957,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -5094,6 +4126,13 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -5154,13 +4193,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -5208,19 +4240,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -5488,6 +4507,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/nyc/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -5962,77 +4991,6 @@ "dev": true, "license": "MIT" }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", - "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -6166,13 +5124,16 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/set-blocking": { @@ -6294,6 +5255,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/spawnd": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-5.0.0.tgz", @@ -6513,6 +5484,13 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", @@ -6530,50 +5508,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/tools/modern-tests/package.json b/tools/modern-tests/package.json index b8f8fbefea..3a116c0bd9 100644 --- a/tools/modern-tests/package.json +++ b/tools/modern-tests/package.json @@ -1,19 +1,21 @@ { "name": "meteor-modern-tests", "version": "1.0.0", - "description": "Modern tests for Meteor", + "description": "Isolated Jest + Playwright environment for Meteor E2E tests", "scripts": { "test": "jest --config jest.config.js" }, "devDependencies": { - "@babel/preset-env": "^7.21.3", - "babel-jest": "^29.0.0", + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", "cheerio": "^1.0.0-rc.12", "execa": "^5.1.1", "fs-extra": "^11.3.1", "jest": "^29.0.0", "jest-playwright-preset": "^3.0.1", "playwright": "1.58.0", + "semver": "^7.7.4", + "underscore": "^1.13.8", "wait-on": "^7.0.0" } } diff --git a/tools/modern-tests/skeleton.test.js b/tools/modern-tests/skeleton.test.js index 6b8ed7be1c..03b37611ce 100644 --- a/tools/modern-tests/skeleton.test.js +++ b/tools/modern-tests/skeleton.test.js @@ -93,7 +93,7 @@ describe('Meteor Skeletons /', () => { ); describe( - 'Full Library Skeleton /', + 'Full Skeleton /', testMeteorSkeleton({ skeletonName: 'full', port: 3204, @@ -116,8 +116,9 @@ describe('Meteor Skeletons /', () => { test: 'tests/main.js', }, bodyStyles: { - 'font-family': - 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + 'font-family': process.platform === 'darwin' + ? 'Inter, -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif' + : 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', padding: '10px', }, }), @@ -150,7 +151,7 @@ describe('Meteor Skeletons /', () => { ); describe( - 'Tailwind Library Skeleton /', + 'Tailwind Skeleton /', testMeteorSkeleton({ skeletonName: 'tailwind', port: 3208, diff --git a/tools/tool-testing/matcher.js b/tools/tool-testing/matcher.js index 3529bc3c11..1dbcb40d8d 100644 --- a/tools/tool-testing/matcher.js +++ b/tools/tool-testing/matcher.js @@ -130,7 +130,16 @@ export default class Matcher { } matchEmpty() { - if (this.buf.length > 0) { + if (this.buf.length === 0) return; + + // Strip Node.js runtime warning lines before checking for unexpected output. + // These originate from third-party packages (e.g. http-proxy using the + // deprecated url.parse() API) and should not cause test failures. + // Pattern covers: "(node:NNNN) Warning: ...\n(Use `node --trace-warnings ...`)\n" + const nodeWarningRe = /\(node:\d+\) \w+: [^\n]+\n(?:\(Use [^\n]+\)\n)?/g; + const stripped = this.buf.replace(nodeWarningRe, ''); + + if (stripped.length > 0) { Console.info("Extra junk is :", this.buf); throw new TestFailure('junk-at-end', { run: this.run }); } diff --git a/tools/unit-tests/README.md b/tools/unit-tests/README.md new file mode 100644 index 0000000000..9b7f49a3d4 --- /dev/null +++ b/tools/unit-tests/README.md @@ -0,0 +1,23 @@ +# Unit Tests + +Isolated Jest environment for unit-testing Meteor `tools/` and `scripts/`. + +The repo root `node_modules/` is used to build the dev bundle, which becomes the Meteor tool itself. Installing test deps (jest, swc, semver, underscore) there could pull in incompatible transitive versions (e.g. lru-cache v10 vs v5) and silently break the dev bundle build or a published Meteor release. This subfolder keeps test dependencies fully isolated so they never affect how Meteor is built or shipped. + +Test files use `*.test.js` next to their source. + +All commands below should be run from the repo root: + +```sh +# Install dependencies (first time) +npm run install:unit + +# Run all unit tests +npm run test:unit + +# Run a specific test file +npm run test:unit -- tools/path/to/file.test.js + +# Run tests matching a name pattern +npm run test:unit -- -t "my test name" +``` diff --git a/tools/unit-tests/jest.config.js b/tools/unit-tests/jest.config.js new file mode 100644 index 0000000000..a21cc6c896 --- /dev/null +++ b/tools/unit-tests/jest.config.js @@ -0,0 +1,42 @@ +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '../..'); + +module.exports = { + rootDir: repoRoot, + testMatch: [ + "/tools/**/*.test.js", + "/scripts/**/*.test.js", + ], + testPathIgnorePatterns: [ + "/node_modules/", + "/tools/modern-tests/", + "/tools/tests/", + "/packages/", + "/.github/", + ], + modulePathIgnorePatterns: [ + "/tools/modern-tests/", + "/tools/tests/", + "/tools/static-assets/", + "/npm-packages/", + "/scripts/admin/", + "/docs/", + "/packages/non-core/", + ], + modulePaths: [ + path.resolve(__dirname, 'node_modules'), + ], + transform: { + "^.+\\.js$": [require.resolve("@swc/jest"), { + jsc: { + parser: { syntax: "ecmascript" }, + target: "es2022", + }, + module: { type: "commonjs" }, + }], + }, + transformIgnorePatterns: ["/node_modules/"], + testTimeout: 10_000, + verbose: true, +}; diff --git a/tools/unit-tests/package-lock.json b/tools/unit-tests/package-lock.json new file mode 100644 index 0000000000..c261671527 --- /dev/null +++ b/tools/unit-tests/package-lock.json @@ -0,0 +1,4672 @@ +{ + "name": "meteor-unit-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meteor-unit-tests", + "version": "1.0.0", + "devDependencies": { + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", + "jest": "^30.2.0", + "semver": "^7.7.2", + "underscore": "^1.13.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", + "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "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": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/jest": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", + "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^30.0.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "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" + } + }, + "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.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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/cliui/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/cliui/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/cliui/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/cliui/node_modules/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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "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/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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": ">=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/path-scurry/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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/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-length/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/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.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "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/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "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/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/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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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/yargs/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/yargs/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/yargs/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tools/unit-tests/package.json b/tools/unit-tests/package.json new file mode 100644 index 0000000000..38644a7619 --- /dev/null +++ b/tools/unit-tests/package.json @@ -0,0 +1,16 @@ +{ + "name": "meteor-unit-tests", + "version": "1.0.0", + "private": true, + "description": "Isolated Jest environment for Meteor unit tests", + "scripts": { + "test": "jest --config jest.config.js --passWithNoTests" + }, + "devDependencies": { + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", + "jest": "^30.2.0", + "semver": "^7.7.2", + "underscore": "^1.13.7" + } +} diff --git a/tools/utils/utils.test.js b/tools/utils/utils.test.js new file mode 100644 index 0000000000..faf5a522fa --- /dev/null +++ b/tools/utils/utils.test.js @@ -0,0 +1,210 @@ +jest.mock('./archinfo', () => ({ + host: jest.fn(() => 'os.osx.x86_64'), + matches: jest.fn((host, pattern) => host.startsWith(pattern)), +})); + +jest.mock('./buildmessage.js', () => ({ + error: jest.fn(), +})); + +jest.mock('../fs/files', () => ({ + stat: jest.fn(), + inCheckout: jest.fn(() => true), + getToolsVersion: jest.fn(() => '3.0.0'), + getCurrentToolsDir: jest.fn(() => '/mock/tools'), + convertToOSPath: jest.fn(p => p), + pathJoin: jest.fn((...args) => args.join('/')), +})); + +jest.mock('../packaging/package-version-parser.js', () => ({ + parsePackageConstraint: jest.fn(), + validatePackageName: jest.fn((name) => { + if (name === 'INVALID') { + const err = new Error('bad package name'); + err.versionParserError = true; + throw err; + } + }), + parse: jest.fn((version) => { + if (version === 'bad') { + const err = new Error('bad version'); + err.versionParserError = true; + throw err; + } + return version; + }), +})); + +const utils = require('./utils'); +const buildmessage = require('./buildmessage.js'); + +describe('parseUrl', () => { + test.each([ + ['3000', {}, { port: '3000', hostname: undefined, protocol: undefined }], + ['4000', { hostname: 'h', protocol: 'https' }, { port: '4000', hostname: 'h', protocol: 'https' }], + ['localhost', {}, { hostname: 'localhost' }], + ['localhost:3000', {}, { hostname: 'localhost', port: '3000', protocol: undefined }], + ['https://ex.com:8080/path', {}, { protocol: 'https', hostname: 'ex.com', port: '8080', pathname: '/path' }], + ['ex.com:3000', { protocol: 'https' }, { protocol: 'https', hostname: 'ex.com', port: '3000' }], + ['http://ex.com', { protocol: 'https' }, { protocol: 'http', hostname: 'ex.com' }], + ['http://ex.com', { port: '9999' }, { protocol: 'http', hostname: 'ex.com', port: '9999' }], + ])('parseUrl(%s) with defaults %j', (input, defaults, expected) => { + const result = utils.parseUrl(input, defaults); + expect(result).toMatchObject(expected); + }); + + test('excludes pathname for root path', () => { + expect(utils.parseUrl('http://ex.com/').pathname).toBeUndefined(); + }); +}); + +describe('hasScheme', () => { + test.each([ + ['http://x', true], ['https://x', true], ['git+ssh://x', true], + ['my2proto://x', true], ['example.com', false], ['3000', false], + ['http:x', false], ['2http://x', false], + ])('(%s) = %s', (input, expected) => { + expect(!!utils.hasScheme(input)).toBe(expected); + }); +}); + +describe('isIPv4Address', () => { + test.each([ + ['192.168.1.1', true], ['0.0.0.0', true], ['255.255.255.255', true], + ['localhost', false], ['192.168.1', false], ['::1', false], ['1.2.3.4.5', false], + ])('(%s) = %s', (input, expected) => { + expect(!!utils.isIPv4Address(input)).toBe(expected); + }); +}); + +describe('validEmail', () => { + test.each([ + ['user@example.com', true], ['a.b@mail.co.uk', true], + ['user+tag@example.com', true], ['a@my-host.com', true], + ['userexample.com', false], ['user@', false], ['@example.com', false], + ['us er@x.com', false], ['', false], ['u@x.c', false], + ])('(%s) = %s', (input, expected) => { + expect(utils.validEmail(input)).toBe(expected); + }); +}); + +describe('quotemeta', () => { + test.each([ + ['a.b*c+d?e', 'a\\.b\\*c\\+d\\?e'], + ['[a](b)\\c', '\\[a\\]\\(b\\)\\\\c'], + ['abc123', 'abc123'], + ])('(%s) = %s', (input, expected) => { + expect(utils.quotemeta(input)).toBe(expected); + }); + + test('escaped string works as literal RegExp', () => { + const s = 'price: $100 (USD)'; + expect(new RegExp(utils.quotemeta(s)).test(s)).toBe(true); + }); +}); + +describe('defaultOrderKeyForReleaseVersion', () => { + test.each([ + ['1.2.3', '0001.0002.0003$'], + ['5', '0005$'], + ['1.2.3.4', '0001.0002.0003.0004$'], + ['1.0-beta', '0001.0000!beta!!!!!!!!!!!$'], + ['1.0-beta.rc3', '0001.0000!beta.rc!!!!!!!!0003$'], + ])('(%s) = %s', (input, expected) => { + expect(utils.defaultOrderKeyForReleaseVersion(input)).toBe(expected); + }); + + test('prerelease key contains ! and tag, ends with $', () => { + const key = utils.defaultOrderKeyForReleaseVersion('1.0-rc1'); + expect(key).toMatch(/!.*rc.*\$$/); + }); + + test('sort order: prerelease < release, 1.2 < 1.2.3, 2 < 10', () => { + const k = (v) => utils.defaultOrderKeyForReleaseVersion(v); + expect(k('1.0-rc1') < k('1.0')).toBe(true); + expect(k('1.2') < k('1.2.3')).toBe(true); + expect(k('2') < k('10')).toBe(true); + }); + + test.each([ + 'abc', '01.2.3', '1.02.3', '1.0-rc01', '12345', '', + ])('returns null for invalid input: %s', (input) => { + expect(utils.defaultOrderKeyForReleaseVersion(input)).toBeNull(); + }); +}); + +describe('generateSubsetsOfIncreasingSize', () => { + test('enumerates all subsets in order and supports early stop', () => { + const all = []; + utils.generateSubsetsOfIncreasingSize([1, 2, 3], (s) => { all.push([...s]); }); + expect(all).toEqual([[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]); + + const stopped = []; + utils.generateSubsetsOfIncreasingSize([1, 2, 3], (s) => { + stopped.push([...s]); + return s.length === 2; + }); + expect(stopped).toEqual([[], [1], [2], [3], [1, 2]]); + }); + + test('empty array yields only the empty subset', () => { + const r = []; + utils.generateSubsetsOfIncreasingSize([], (s) => { r.push([...s]); }); + expect(r).toEqual([[]]); + }); +}); + +describe('URL scheme matchers', () => { + test.each([ + ['isUrlWithFileScheme', 'file:///path', true], + ['isUrlWithFileScheme', 'file://host/path', true], + ['isUrlWithFileScheme', 'file://', false], + ['isUrlWithFileScheme', 'http://x', false], + ['isUrlWithSha', `https://x/${'a'.repeat(40)}`, true], + ['isUrlWithSha', `http://x/${'b'.repeat(40)}`, true], + ['isUrlWithSha', 'https://x/abc123', false], + ['isUrlWithSha', 'not-a-url', false], + ['isNpmUrl', 'git://github.com/r', true], + ['isNpmUrl', 'git+ssh://git@github.com/r', true], + ['isNpmUrl', 'git+http://github.com/r', true], + ['isNpmUrl', 'git+https://github.com/r', true], + ['isNpmUrl', 'https://x/pkg', true], + ['isNpmUrl', 'http://x/pkg', true], + ['isNpmUrl', 'lodash', false], + ])('%s(%s) = %s', (fn, input, expected) => { + expect(!!utils[fn](input)).toBe(expected); + }); +}); + +describe('sourceMapLength', () => { + test.each([ + [null, 0], + [undefined, 0], + [{ mappings: 'AAAA' }, 4], + [{ mappings: 'ABC', sourcesContent: ['hello', 'world'] }, 13], + [{ mappings: 'AB', sourcesContent: [null, 'code', null] }, 6], + ])('sourceMapLength(%j) = %s', (input, expected) => { + expect(utils.sourceMapLength(input)).toBe(expected); + }); +}); + +describe('parsePackageAndVersion', () => { + test.each([ + ['my-pkg 1.0.0', { package: 'my-pkg', version: '1.0.0' }], + ['my-pkg@2.0.0', { package: 'my-pkg', version: '2.0.0' }], + ['user:pkg 1.0.0', { package: 'user:pkg', version: '1.0.0' }], + ])('parses %s', (input, expected) => { + expect(utils.parsePackageAndVersion(input)).toEqual(expected); + }); + + test('throws for missing separator or invalid version', () => { + expect(() => utils.parsePackageAndVersion('noseparator')).toThrow('Malformed package version'); + expect(() => utils.parsePackageAndVersion('pkg bad')).toThrow(); + }); + + test('returns null with useBuildmessage on malformed input', () => { + buildmessage.error.mockClear(); + expect(utils.parsePackageAndVersion('noseparator', { useBuildmessage: true })).toBeNull(); + expect(buildmessage.error).toHaveBeenCalled(); + }); +}); diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 97af62202c..def4899da6 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -616,6 +616,10 @@ export default defineConfig({ { text: "Performance", items: [ + { + text: "Change Streams Observer Driver", + link: "/performance/change-streams-observer-driver", + }, { text: "Performance Improvements", link: "/performance/performance-improvement", diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index a9e1d08cfe..d1741fad07 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -35,6 +35,41 @@ By default, Meteor uses Local Storage to store, among other things, login tokens } ``` +### Accounts with HttpOnly Cookies {#accounts-httponly-cookies} + +Meteor 3.3 introduces a native flow to keep the persistent resume token in an HttpOnly cookie instead of in Web Storage. This protects the token from malicious scripts and pairs nicely with in-memory client storage. Enable the feature with two small changes: + +1. On the server, call `Accounts.config` during startup and set both options: + + ```ts + import { Accounts } from "meteor/accounts-base"; + import { Meteor } from "meteor/meteor"; + + Meteor.startup(() => { + Accounts.config({ + clientStorage: "none", + useHttpOnlyCookies: true, + }); + }); + ``` + +2. Surface the same flags to the client via settings so the browser-side Accounts instance starts with the right defaults: + + ```json + { + "public": { + "packages": { + "accounts": { + "clientStorage": "none", + "useHttpOnlyCookies": true + } + } + } + } + ``` + +After restarting the app and logging in, `Meteor.loginToken*` keys should no longer appear in `localStorage`. Instead, the browser receives an HttpOnly `meteor_login_token` cookie and the client keeps credentials in memory only for the active tab. If you later disable the feature, remember to revert both the server configuration and the public settings so that Accounts resumes using Web Storage. + Retrieves the user record for the current user from diff --git a/v3-docs/docs/api/meteor.md b/v3-docs/docs/api/meteor.md index 68f37fd31a..9cb36da796 100644 --- a/v3-docs/docs/api/meteor.md +++ b/v3-docs/docs/api/meteor.md @@ -993,16 +993,19 @@ contains the following fields: security risk for this transport. For details and alternatives, see the [SockJS documentation](https://github.com/sockjs/sockjs-node#authorisation). -> Currently when a client reconnects to the server (such as after -> temporarily losing its Internet connection), it will get a new -> connection each time. The `onConnection` callbacks will be called -> again, and the new connection will have a new connection `id`. +## Reconnection -> In the future, when client reconnection is fully implemented, -> reconnecting from the client will reconnect to the same connection on -> the server: the `onConnection` callback won't be called for that -> connection again, and the connection will still have the same -> connection `id`. +Meteor 3.5+ supports [DDP session resumption](https://github.com/meteor/meteor/pull/14051), allowing clients to automatically resume their previous connection after a temporary network disconnect. When a client reconnects within the grace period, the `onConnection` callback is not called again and the connection retains its original `id`. + +This behavior is controlled by the following server options: + +### Meteor.server.options.disconnectGracePeriod + +Defines how long (in milliseconds) we should maintain a session for after a non-graceful disconnect before destroying it. Sessions that reconnect within this time will be resumed with minimal performance impact. Defaults to `15000`. + +### Meteor.server.options.maxMessageQueueLength + +Determines how many messages we should queue during a non-graceful disconnect before we destroy the session, to help prevent memory leaks. Defaults to `100`. diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index ee31736a45..7de8ec0565 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -24,6 +24,10 @@ Please bear in mind if you are adding a package to this list, try providing as m ## List of Community Packages +#### AI/LLM helpers + +- [Wormhole](./wormhole.md) Meteor Wormhole, MCP and REST API endpoint creator + #### Method/Subscription helpers - [`meteor-rpc`](./meteor-rpc.md), Meteor Methods Evolved with type checking and runtime validation diff --git a/v3-docs/docs/community-packages/wormhole.md b/v3-docs/docs/community-packages/wormhole.md new file mode 100644 index 0000000000..5ddd5a72f4 --- /dev/null +++ b/v3-docs/docs/community-packages/wormhole.md @@ -0,0 +1,138 @@ +# Jam Method + +- `Who maintains the package` – [William Reiske](https://github.com/wreiske/meteor-wormhole/commits?author=wreiske) + +[[toc]] + +## What Is It? + +Meteor Wormhole is a **server-only, Meteor 3.4+ package** that bridges your Meteor methods to the outside world through: + +- **[MCP (Model Context Protocol)](https://modelcontextprotocol.io/)** — The open standard for connecting AI assistants to tools and data. Your methods become MCP tools that Claude, GPT, Cursor, VS Code Copilot, and any MCP-compatible client can discover and invoke. +- **REST API** — Every exposed method also gets a `POST /api/` endpoint. +- **OpenAPI 3.1 spec** — Auto-generated from your method schemas. +- **Swagger UI** — Built-in interactive API docs at `/api/docs`. + +## How It Works + +Two lines to get started: + +```js +import { Wormhole } from 'meteor/wreiske:meteor-wormhole'; + +Wormhole.init(); // That's it — all your methods are now MCP tools +``` + +By default it runs in **"all-in" mode**, which automatically exposes every `Meteor.methods()` call (minus DDP internals, private `_`-prefixed methods, and Accounts methods). You can also run in **"opt-in" mode** for explicit control: + +```js +Wormhole.init({ mode: 'opt-in' }); + +Wormhole.expose('todos.add', { + description: 'Add a new todo item', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'The todo title' }, + priority: { type: 'string', enum: ['low', 'medium', 'high'] } + }, + required: ['title'] + } +}); +``` + +Add richer schemas and descriptions, and AI agents get better context about what your tools do and how to call them. + +## Features at a Glance + +- **Zero-config MCP server** — Streamable HTTP transport at `/mcp`, session management, JSON-RPC 2.0 +- **Optional REST bridge** — Enable with `rest: { enabled: true }` for traditional HTTP clients +- **Auto-generated OpenAPI 3.1 spec** with Swagger UI +- **Optional API key auth** — Covers both MCP and REST endpoints +- **Smart exclusions** — Automatically skips DDP internals, `_private` methods, and Accounts methods; add your own patterns +- **Input validation** — JSON Schema → Zod conversion for parameter validation +- **Error propagation** — `Meteor.Error` details are properly passed through to clients +- **Enrich existing methods** — Add descriptions and schemas to auto-registered methods with `Wormhole.expose()` + +## Configuration Options + +```js +Wormhole.init({ + mode: 'all', // 'all' or 'opt-in' + path: '/mcp', // MCP endpoint path + name: 'my-app', // MCP server name + apiKey: 'secret', // Optional bearer token auth + exclude: [/^admin\./], // Additional exclusion patterns + rest: { + enabled: true, // Enable REST API + path: '/api', // REST base path + docs: true // Swagger UI at /api/docs + } +}); +``` + +## Point Your MCP Client at It + +If you use Claude Desktop, Cursor, VS Code Copilot, or any other MCP-compatible client, you can connect to a Wormhole-enabled app and your AI assistant will immediately see all the exposed methods as callable tools. Just point it at your app's `/mcp` endpoint. + +## API Reference + +### `Wormhole.init(options)` + +Initialize the MCP bridge. + +| Option | Type | Default | Description | +| --------- | ---------------------- | ------------------- | ---------------------------------- | +| `mode` | `'all' \| 'opt-in'` | `'all'` | Exposure mode | +| `path` | `string` | `'/mcp'` | HTTP endpoint path | +| `name` | `string` | `'meteor-wormhole'` | MCP server name | +| `version` | `string` | `'1.0.0'` | MCP server version | +| `apiKey` | `string \| null` | `null` | Bearer token for auth | +| `exclude` | `(string \| RegExp)[]` | `[]` | Methods to exclude (all-in mode) | +| `rest` | `object \| boolean` | `false` | REST API configuration (see below) | + +#### `rest` options + +| Option | Type | Default | Description | +| --------- | ---------------- | ----------- | -------------------------------------------- | +| `enabled` | `boolean` | `false` | Enable REST endpoints | +| `path` | `string` | `'/api'` | Base path for REST endpoints | +| `docs` | `boolean` | `true` | Serve Swagger UI at `/docs` | +| `apiKey` | `string \| null` | _inherited_ | API key for REST (defaults to main `apiKey`) | + +Shorthand: `rest: true` enables REST with all defaults. + +### `Wormhole.expose(methodName, options)` + +Explicitly expose a method as an MCP tool. + +| Option | Type | Description | +| -------------- | -------- | --------------------------------------------------------------------------------------- | +| `description` | `string` | Human-readable tool description | +| `inputSchema` | `object` | JSON Schema for method parameters | +| `outputSchema` | `object` | JSON Schema for the return value (wrapped inside `{ result }` envelope in OpenAPI/REST) | + +### `Wormhole.unexpose(methodName)` + +Remove a method from MCP exposure. + +## How It Works + +1. **Registration**: In all-in mode, the package monkey-patches `Meteor.methods` to intercept every method registration. In opt-in mode, you call `Wormhole.expose()` manually. + +2. **MCP Server**: A Streamable HTTP MCP server is mounted at the configured path (default `/mcp`) on Meteor's `WebApp`. + +3. **Tool Mapping**: Each exposed Meteor method becomes an MCP tool. Method names are sanitized (e.g., `todos.add` → `todos_add`). + +4. **Invocation**: When an AI agent calls a tool, the bridge invokes the corresponding Meteor method via `Meteor.callAsync()` and returns the result. + +5. **REST API** (optional): When enabled, a parallel REST bridge mounts at the configured path. Each method gets a `POST` endpoint. An OpenAPI 3.1 spec is auto-generated from the registry's metadata and input schemas, and Swagger UI provides interactive documentation. + + +## Links + +- **GitHub:** https://github.com/wreiske/meteor-wormhole +- **Live Demo:** https://wormhole.meteorapp.com/ +- **Swagger UI:** https://wormhole.meteorapp.com/api/docs +- **Atmosphere:** [https://atmospherejs.com/wreiske/meteor-wormhole](https://atmospherejs.com/wreiske/meteor-wormhole) +- **Packosphere:** [https://packosphere.com/wreiske/meteor-wormhole](https://packosphere.com/wreiske/meteor-wormhole) diff --git a/v3-docs/docs/generators/changelog/versions/3.5.0.md b/v3-docs/docs/generators/changelog/versions/3.5.0.md new file mode 100644 index 0000000000..014cb88c1d --- /dev/null +++ b/v3-docs/docs/generators/changelog/versions/3.5.0.md @@ -0,0 +1,78 @@ +## v3.5.0, 10-03-2026 + +### Highlights + +- **Change streams**, [PR#13910](https://github.com/meteor/meteor/pull/13787) + - ⚡ New `change streams` observe driver + - 📃 [Documentation](https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md) + +All Merged PRs@[GitHub PRs 3.5](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.5) + + +#### Breaking Changes + +N/A + +#### Internal API changes + +N/A + +#### Migration Steps + +Please run the following command to update your project: + +```bash +meteor update --release 3.5-beta.7 +``` + +--- + +**Add this to your `package.json` to enable the new modern build stack:** + +```json +{ + "packages"@{ + "mongo"@{ + "reactivity"@["changeStreams", "oplog", "polling"] + } + } +} +``` + +Check out [change streams documentation](https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md) for more details. + +> Change streams are opt-in and require MongoDB 4.0+ and a replica set or sharded cluster. + +> Change streams isnt added by default for new apps, so you need to add the above configuration to enable it. + + +#### Bumped Meteor Packages + +- accounts-base@3.3.0-beta350.7 +- accounts-password@3.2.3-beta350.7 +- ddp-client@3.2.0-beta350.7 +- ddp-rate-limiter@1.3.0-beta350.7 +- ddp-server@3.2.0-beta350.7 +- ejson@1.2.0-beta350.7 +- email@3.2.0-beta350.7 +- minimongo@2.1.0-beta350.7 +- mongo@2.3.0-beta350.7 +- rate-limit@1.2.0-beta350.7 +- socket-stream-client@0.7.0-beta350.7 +- test-in-console@2.0.2-beta350.7 +- webapp@2.2.0-beta350.7 +- meteor-tool@3.5.0-beta.7 + +#### Bumped NPM Packages + +- meteor-installer@3.5.0-beta + +#### Special thanks to + +✨✨✨ + +- [@italojs](https://github.com/italojs) +- [@radekmie](https://github.com/radekmie) +- [@nachocodoner](https://github.com/nachocodoner) + + ✨✨✨ diff --git a/v3-docs/docs/performance/change-streams-observer-driver.md b/v3-docs/docs/performance/change-streams-observer-driver.md new file mode 100644 index 0000000000..00d9fca093 --- /dev/null +++ b/v3-docs/docs/performance/change-streams-observer-driver.md @@ -0,0 +1,59 @@ +# Change Streams Observer Driver + +Meteor ships a Change Streams–based observe driver that can deliver realtime updates without oplog tailing. It hooks directly into MongoDB Change Streams to watch collection activity and push mutations to clients. + +::: warning +Before moving production traffic to Change Streams, validate that your MongoDB deployment and queries are a good fit and benchmark under realistic load. Change Streams can reduce operational friction where oplog tailing is unavailable, but they can also increase work on busy collections if your selectors are broad. +::: + +## Requirements and Limitations + +- MongoDB 3.6+ on a replica set or sharded cluster (Change Streams are not available on standalone or some shared-tier deployments). +- Works only for unordered observers. Publications that rely on ordered callbacks (`addedBefore`, `movedBefore`) will keep using another driver. +- Selectors must compile with `Minimongo.Matcher`. Unsupported selectors fall back to the next configured driver. +- If Change Streams are unavailable, Meteor automatically moves to the next driver in your configured order. + +## Choosing the Reactivity Driver Order + +Meteor picks the first available driver from the configured list. The default order is `oplog`, then `changeStreams`, then `polling` (long polling). You can change this globally: + +- Environment variable: `METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling` +- Settings file: + +```json +{ + "packages": { + "mongo": { + "reactivity": ["changeStreams", "oplog", "polling"] + } + } +} +``` + +Tips: +- Put `changeStreams` first when you cannot or do not want to tail the oplog (e.g., Atlas Serverless). +- Remove `changeStreams` from the list if you want to disable it. +- Valid entries are `oplog`, `changeStreams`, and `polling`/`polling` (alias). + +## Change Stream Driver Settings + +Optional tuning is available via `Meteor.settings`: + +```json +{ + "packages": { + "mongo": { + "changeStream": { + "delay": { "error": 100, "close": 100 }, + "waitUntilCaughtUpTimeoutMs": 1000 + } + } + } +} +``` + +- `delay.error`: Milliseconds to wait before restarting the stream after an error (default: `100`). +- `delay.close`: Milliseconds to wait before restarting after an unexpected close (default: `100`). +- `waitUntilCaughtUpTimeoutMs`: Upper bound for waiting until the stream catches up to the server's current operation time when coordinating with DDP fences (default: `1000`). + - If this timeout elapses, the driver stops waiting and lets the fence continue; the change stream will catch up later, so data is not lost, but clients can temporarily miss read-your-writes (a publication may become ready before the client's own writes appear). +