diff --git a/.eslintrc.js b/.eslintrc.js index 0e8ad007..5db9f815 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,7 @@ /* This file is automatically added by @npmcli/template-oss. Do not edit. */ +'use strict' + const { readdirSync: readdir } = require('fs') const localConfigs = readdir(__dirname) diff --git a/.eslintrc.local.js b/.eslintrc.local.js new file mode 100644 index 00000000..5b7c98ea --- /dev/null +++ b/.eslintrc.local.js @@ -0,0 +1,18 @@ +'use strict' + +module.exports = { + overrides: [ + { + files: ['bin/**', 'classes/**', 'functions/**', 'internal/**', 'ranges/**'], + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: false, + }, + ], + 'import/no-nodejs-modules': ['error'], + }, + }, + ], +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 96d8eafb..8da2a452 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ version: 2 updates: - package-ecosystem: npm - directory: "/" + directory: / schedule: interval: daily allow: diff --git a/.github/matchers/tap.json b/.github/matchers/tap.json new file mode 100644 index 00000000..2c81ea98 --- /dev/null +++ b/.github/matchers/tap.json @@ -0,0 +1,32 @@ +{ + "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", + "problemMatcher": [ + { + "owner": "tap", + "pattern": [ + { + "regexp": "^\\s*not ok \\d+ - (.*)", + "message": 1 + }, + { + "regexp": "^\\s*---" + }, + { + "regexp": "^\\s*at:" + }, + { + "regexp": "^\\s*line:\\s*(\\d+)", + "line": 1 + }, + { + "regexp": "^\\s*column:\\s*(\\d+)", + "column": 1 + }, + { + "regexp": "^\\s*file:\\s*(.*)", + "file": 1 + } + ] + } + ] +} diff --git a/.github/settings.yml b/.github/settings.yml index 1019e26f..adbef7e6 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,2 +1,52 @@ ---- -_extends: '.github:npm-cli/settings.yml' +# This file is automatically added by @npmcli/template-oss. Do not edit. + +repository: + allow_merge_commit: false + allow_rebase_merge: true + allow_squash_merge: true + squash_merge_commit_title: PR_TITLE + squash_merge_commit_message: PR_BODY + delete_branch_on_merge: true + enable_automated_security_fixes: true + enable_vulnerability_alerts: true + +branches: + - name: main + protection: + required_status_checks: null + enforce_admins: true + required_pull_request_reviews: + required_approving_review_count: 1 + require_code_owner_reviews: true + require_last_push_approval: true + dismiss_stale_reviews: true + restrictions: + apps: [] + users: [] + teams: [ "cli-team" ] + - name: latest + protection: + required_status_checks: null + enforce_admins: true + required_pull_request_reviews: + required_approving_review_count: 1 + require_code_owner_reviews: true + require_last_push_approval: true + dismiss_stale_reviews: true + restrictions: + apps: [] + users: [] + teams: [ "cli-team" ] + - name: release/v* + protection: + required_status_checks: null + enforce_admins: true + required_pull_request_reviews: + required_approving_review_count: 1 + require_code_owner_reviews: true + require_last_push_approval: true + dismiss_stale_reviews: true + restrictions: + apps: [] + users: [] + teams: [ "cli-team" ] diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 03dcd937..8b8f3748 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -5,23 +5,35 @@ name: Audit on: workflow_dispatch: schedule: - # "At 01:00 on Monday" https://crontab.guru/#0_1_*_*_1 - - cron: "0 1 * * 1" + # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 + - cron: "0 8 * * 1" jobs: audit: + name: Audit Dependencies + if: github.repository_owner == 'npm' runs-on: ubuntu-latest + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v3 - - name: Setup git user + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User run: | - git config --global user.email "ops+npm-cli@npmjs.com" - git config --global user.name "npm cli ops bot" - - uses: actions/setup-node@v3 + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: 16.x - - name: Update npm to latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v - - run: npm i --ignore-scripts --no-audit --no-fund --package-lock - - run: npm audit + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund --package-lock + - name: Run Production Audit + run: npm audit --omit=dev + - name: Run Full Audit + run: npm audit --audit-level=none diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml new file mode 100644 index 00000000..98b70866 --- /dev/null +++ b/.github/workflows/ci-release.yml @@ -0,0 +1,216 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI - Release + +on: + workflow_dispatch: + inputs: + ref: + required: true + type: string + default: main + workflow_call: + inputs: + ref: + required: true + type: string + check-sha: + required: true + type: string + +jobs: + lint-all: + name: Lint All + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + if: inputs.check-sha + id: check-output + env: + JOB_NAME: "Lint All" + MATRIX_NAME: "" + with: + script: | + const { owner, repo } = context.repo + + const { data } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: context.runId, + per_page: 100 + }) + + const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME + const job = data.jobs.find(j => j.name.endsWith(jobName)) + const jobUrl = job?.html_url + + const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}` + + let summary = `This check is assosciated with ${shaUrl}\n\n` + + if (jobUrl) { + summary += `For run logs, click here: ${jobUrl}` + } else { + summary += `Run logs could not be found for a job with name: "${jobName}"` + } + + return { summary } + - name: Create Check + uses: LouisBrunner/checks-action@v1.6.0 + id: check + if: inputs.check-sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Lint All + sha: ${{ inputs.check-sha }} + output: ${{ steps.check-output.outputs.result }} + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Lint + run: npm run lint --ignore-scripts + - name: Post Lint + run: npm run postlint --ignore-scripts + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.6.0 + if: steps.check.outputs.check_id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check_id: ${{ steps.check.outputs.check_id }} + + test-all: + name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + if: github.repository_owner == 'npm' + strategy: + fail-fast: false + matrix: + platform: + - name: Linux + os: ubuntu-latest + shell: bash + - name: macOS + os: macos-latest + shell: bash + - name: Windows + os: windows-latest + shell: cmd + node-version: + - 10.0.0 + - 10.x + - 12.x + - 14.x + - 16.x + - 18.x + runs-on: ${{ matrix.platform.os }} + defaults: + run: + shell: ${{ matrix.platform.shell }} + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + if: inputs.check-sha + id: check-output + env: + JOB_NAME: "Test All" + MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" + with: + script: | + const { owner, repo } = context.repo + + const { data } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: context.runId, + per_page: 100 + }) + + const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME + const job = data.jobs.find(j => j.name.endsWith(jobName)) + const jobUrl = job?.html_url + + const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}` + + let summary = `This check is assosciated with ${shaUrl}\n\n` + + if (jobUrl) { + summary += `For run logs, click here: ${jobUrl}` + } else { + summary += `Run logs could not be found for a job with name: "${jobName}"` + } + + return { summary } + - name: Create Check + uses: LouisBrunner/checks-action@v1.6.0 + id: check + if: inputs.check-sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + sha: ${{ inputs.check-sha }} + output: ${{ steps.check-output.outputs.result }} + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Install npm@7 + if: startsWith(matrix.node-version, '10.') + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Install npm@8 + if: ${{ !startsWith(matrix.node-version, '10.') }} + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Add Problem Matcher + run: echo "::add-matcher::.github/matchers/tap.json" + - name: Test + run: npm test --ignore-scripts + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.6.0 + if: steps.check.outputs.check_id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check_id: ${{ steps.check.outputs.check_id }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 538e46a0..bb473086 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,65 +5,84 @@ name: CI on: workflow_dispatch: pull_request: - branches: - - '*' push: branches: - main - latest + - release/v* schedule: - # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1 - - cron: "0 2 * * 1" + # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" jobs: lint: + name: Lint + if: github.repository_owner == 'npm' runs-on: ubuntu-latest + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v3 - - name: Setup git user + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User run: | - git config --global user.email "ops+npm-cli@npmjs.com" - git config --global user.name "npm cli ops bot" - - uses: actions/setup-node@v3 + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: 16.x - - name: Update npm to latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v - - run: npm i --ignore-scripts --no-audit --no-fund - - run: npm run lint + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Lint + run: npm run lint --ignore-scripts + - name: Post Lint + run: npm run postlint --ignore-scripts test: + name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + if: github.repository_owner == 'npm' strategy: fail-fast: false matrix: + platform: + - name: Linux + os: ubuntu-latest + shell: bash + - name: macOS + os: macos-latest + shell: bash + - name: Windows + os: windows-latest + shell: cmd node-version: - 10.0.0 - 10.x - 12.x - 14.x - 16.x - platform: - - os: ubuntu-latest - shell: bash - - os: macos-latest - shell: bash - - os: windows-latest - shell: cmd + - 18.x runs-on: ${{ matrix.platform.os }} defaults: run: shell: ${{ matrix.platform.shell }} steps: - - uses: actions/checkout@v3 - - name: Setup git user + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User run: | - git config --global user.email "ops+npm-cli@npmjs.com" - git config --global user.name "npm cli ops bot" - - uses: actions/setup-node@v3 + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - - name: Update to workable npm (windows) + - name: Update Windows npm # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) run: | @@ -73,13 +92,17 @@ jobs: node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz cd .. rmdir /s /q package - - name: Update npm to 7 - # If we do test on npm 10 it needs npm7 + - name: Install npm@7 if: startsWith(matrix.node-version, '10.') run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Update npm to latest + - name: Install npm@8 if: ${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v - - run: npm i --ignore-scripts --no-audit --no-fund - - run: npm test --ignore-scripts + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Add Problem Matcher + run: echo "::add-matcher::.github/matchers/tap.json" + - name: Test + run: npm test --ignore-scripts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 56cd7b9c..21244879 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,20 +1,21 @@ # This file is automatically added by @npmcli/template-oss. Do not edit. -name: "CodeQL" +name: CodeQL on: push: branches: - main - latest + - release/v* pull_request: - # The branches below must be a subset of the branches above branches: - main - latest + - release/v* schedule: - # "At 03:00 on Monday" https://crontab.guru/#0_3_*_*_1 - - cron: "0 3 * * 1" + # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 + - cron: "0 10 * * 1" jobs: analyze: @@ -24,21 +25,16 @@ jobs: actions: read contents: read security-events: write - - strategy: - fail-fast: false - matrix: - language: [ javascript ] - steps: - - uses: actions/checkout@v3 - - name: Setup git user + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User run: | - git config --global user.email "ops+npm-cli@npmjs.com" - git config --global user.name "npm cli ops bot" + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: - languages: ${{ matrix.language }} + languages: javascript - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml index 0372a7a9..03c85681 100644 --- a/.github/workflows/post-dependabot.yml +++ b/.github/workflows/post-dependabot.yml @@ -1,43 +1,121 @@ # This file is automatically added by @npmcli/template-oss. Do not edit. -name: Post Dependabot Actions +name: Post Dependabot on: pull_request -# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions permissions: contents: write jobs: - template-oss-apply: + template-oss: + name: template-oss + if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v3 - - name: Setup git user + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + - name: Setup Git User run: | - git config --global user.email "ops+npm-cli@npmjs.com" - git config --global user.name "npm cli ops bot" - - uses: actions/setup-node@v3 + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: 16.x - - name: Update npm to latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v - - name: Dependabot metadata + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Fetch Dependabot Metadata id: metadata - uses: dependabot/fetch-metadata@v1.1.1 + uses: dependabot/fetch-metadata@v1 with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - name: npm install and commit + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Dependabot can update multiple directories so we output which directory + # it is acting on so we can run the command for the correct root or workspace + - name: Get Dependabot Directory if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') + id: flags + run: | + dependabot_dir="${{ steps.metadata.outputs.directory }}" + if [[ "$dependabot_dir" == "/" ]]; then + echo "workspace=-iwr" >> $GITHUB_OUTPUT + else + # strip leading slash from directory so it works as a + # a path to the workspace flag + echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT + fi + + - name: Apply Changes + if: steps.flags.outputs.workspace + id: apply + run: | + npm run template-oss-apply ${{ steps.flags.outputs.workspace }} + if [[ `git status --porcelain` ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + fi + # This only sets the conventional commit prefix. This workflow can't reliably determine + # what the breaking change is though. If a BREAKING CHANGE message is required then + # this PR check will fail and the commit will be amended with stafftools + if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then + prefix='feat!' + else + prefix='chore' + fi + echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT + + # This step will fail if template-oss has made any workflow updates. It is impossible + # for a workflow to update other workflows. In the case it does fail, we continue + # and then try to apply only a portion of the changes in the next step + - name: Push All Changes + if: steps.apply.outputs.changes + id: push + continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh pr checkout ${{ github.event.pull_request.number }} - npm install --ignore-scripts --no-audit --no-fund - npm run template-oss-apply - git add . - git commit -am "chore: postinstall for dependabot template-oss PR" + git commit -am "${{ steps.apply.outputs.message }}" git push - npm run lint + + # If the previous step failed, then reset the commit and remove any workflow changes + # and attempt to commit and push again. This is helpful because we will have a commit + # with the correct prefix that we can then --amend with @npmcli/stafftools later. + - name: Push All Changes Except Workflows + if: steps.apply.outputs.changes && steps.push.outcome == 'failure' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git reset HEAD~ + git checkout HEAD -- .github/workflows/ + git clean -fd .github/workflows/ + git commit -am "${{ steps.apply.outputs.message }}" + git push + + # Check if all the necessary template-oss changes were applied. Since we continued + # on errors in one of the previous steps, this check will fail if our follow up + # only applied a portion of the changes and we need to followup manually. + # + # Note that this used to run `lint` and `postlint` but that will fail this action + # if we've also shipped any linting changes separate from template-oss. We do + # linting in another action, so we want to fail this one only if there are + # template-oss changes that could not be applied. + - name: Check Changes + if: steps.apply.outputs.changes + run: | + npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check + + - name: Fail on Breaking Change + if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') + run: | + echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" + echo "for more information on how to fix this with a BREAKING CHANGE footer." + exit 1 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 93a5c3c9..da5779df 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,6 +1,6 @@ # This file is automatically added by @npmcli/template-oss. Do not edit. -name: Pull Request Linting +name: Pull Request on: pull_request: @@ -11,28 +11,40 @@ on: - synchronize jobs: - check: - name: Check PR Title or Commits + commitlint: + name: Lint Commits + if: github.repository_owner == 'npm' runs-on: ubuntu-latest + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup git user + - name: Setup Git User run: | - git config --global user.email "ops+npm-cli@npmjs.com" - git config --global user.name "npm cli ops bot" - - uses: actions/setup-node@v3 + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: 16.x - - name: Update npm to latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v - - name: Install deps - run: npm i -D @commitlint/cli @commitlint/config-conventional - - name: Check commits OR PR title + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Run Commitlint on Commits + id: commit + continue-on-error: true + run: | + npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} + - name: Run Commitlint on PR Title + if: steps.commit.outcome == 'failure' env: PR_TITLE: ${{ github.event.pull_request.title }} run: | - npx --offline commitlint -V --from origin/main --to ${{ github.event.pull_request.head.sha }} \ - || echo $PR_TITLE | npx --offline commitlint -V + echo '$PR_TITLE' | npx --offline commitlint -V diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index ab3a9105..00000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: Release Please - -on: - push: - branches: - - main - - latest - -jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: google-github-actions/release-please-action@v3 - id: release - with: - release-type: node - changelog-types: > - [ - {"type":"feat","section":"Features","hidden":false}, - {"type":"fix","section":"Bug Fixes","hidden":false}, - {"type":"docs","section":"Documentation","hidden":false}, - {"type":"deps","section":"Dependencies","hidden":false}, - {"type":"chore","hidden":true} - ] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b086b0a5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,399 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release + +on: + workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string + push: + branches: + - main + - latest + - release/v* + +permissions: + contents: write + pull-requests: write + checks: write + +jobs: + release: + outputs: + pr: ${{ steps.release.outputs.pr }} + release: ${{ steps.release.outputs.release }} + releases: ${{ steps.release.outputs.releases }} + branch: ${{ steps.release.outputs.pr-branch }} + pr-number: ${{ steps.release.outputs.pr-number }} + comment-id: ${{ steps.pr-comment.outputs.result }} + check-id: ${{ steps.check.outputs.check_id }} + name: Release + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Release Please + id: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx --offline template-oss-release-please "${{ github.ref_name }}" "${{ inputs.release-pr }}" + - name: Post Pull Request Comment + if: steps.release.outputs.pr-number + uses: actions/github-script@v6 + id: pr-comment + env: + PR_NUMBER: ${{ steps.release.outputs.pr-number }} + REF_NAME: ${{ github.ref_name }} + with: + script: | + const { REF_NAME, PR_NUMBER: issue_number } = process.env + const { runId, repo: { owner, repo } } = context + + const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) + + let body = '## Release Manager\n\n' + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id + + body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n` + body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`${REF_NAME}\`. ` + body += `To force CI to update this PR, run this command:\n\n` + body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\`` + + if (commentId) { + await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) + } else { + const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body }) + commentId = comment?.id + } + + return commentId + - name: Get Workflow Job + uses: actions/github-script@v6 + if: steps.release.outputs.pr-sha + id: check-output + env: + JOB_NAME: "Release" + MATRIX_NAME: "" + with: + script: | + const { owner, repo } = context.repo + + const { data } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: context.runId, + per_page: 100 + }) + + const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME + const job = data.jobs.find(j => j.name.endsWith(jobName)) + const jobUrl = job?.html_url + + const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}` + + let summary = `This check is assosciated with ${shaUrl}\n\n` + + if (jobUrl) { + summary += `For run logs, click here: ${jobUrl}` + } else { + summary += `Run logs could not be found for a job with name: "${jobName}"` + } + + return { summary } + - name: Create Check + uses: LouisBrunner/checks-action@v1.6.0 + id: check + if: steps.release.outputs.pr-sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Release + sha: ${{ steps.release.outputs.pr-sha }} + output: ${{ steps.check-output.outputs.result }} + + update: + needs: release + outputs: + sha: ${{ steps.commit.outputs.sha }} + check-id: ${{ steps.check.outputs.check_id }} + name: Update - Release + if: github.repository_owner == 'npm' && needs.release.outputs.pr + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ needs.release.outputs.branch }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@8 + run: npm i --prefer-online --no-fund --no-audit -g npm@8 + - name: npm Version + run: npm -v + - name: Install Dependencies + run: npm i --ignore-scripts --no-audit --no-fund + - name: Run Post Pull Request Actions + env: + RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }} + RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npm exec --offline -- template-oss-release-manager --lockfile=false --publish=true + npm run rp-pull-request --ignore-scripts --if-present + - name: Commit + id: commit + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git commit --all --amend --no-edit || true + git push --force-with-lease + echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + - name: Get Workflow Job + uses: actions/github-script@v6 + if: steps.commit.outputs.sha + id: check-output + env: + JOB_NAME: "Update - Release" + MATRIX_NAME: "" + with: + script: | + const { owner, repo } = context.repo + + const { data } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: context.runId, + per_page: 100 + }) + + const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME + const job = data.jobs.find(j => j.name.endsWith(jobName)) + const jobUrl = job?.html_url + + const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}` + + let summary = `This check is assosciated with ${shaUrl}\n\n` + + if (jobUrl) { + summary += `For run logs, click here: ${jobUrl}` + } else { + summary += `Run logs could not be found for a job with name: "${jobName}"` + } + + return { summary } + - name: Create Check + uses: LouisBrunner/checks-action@v1.6.0 + id: check + if: steps.commit.outputs.sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Release + sha: ${{ steps.commit.outputs.sha }} + output: ${{ steps.check-output.outputs.result }} + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.6.0 + if: needs.release.outputs.check-id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check_id: ${{ needs.release.outputs.check-id }} + + ci: + name: CI - Release + needs: [ release, update ] + if: needs.release.outputs.pr + uses: ./.github/workflows/ci-release.yml + with: + ref: ${{ needs.release.outputs.branch }} + check-sha: ${{ needs.update.outputs.sha }} + + post-ci: + needs: [ release, update, ci ] + name: Post CI - Release + if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get Needs Result + id: needs-result + run: | + result="" + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + result="failure" + elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + result="cancelled" + else + result="success" + fi + echo "result=$result" >> $GITHUB_OUTPUT + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.6.0 + if: needs.update.outputs.check-id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ steps.needs-result.outputs.result }} + check_id: ${{ needs.update.outputs.check-id }} + + post-release: + needs: release + name: Post Release - Release + if: github.repository_owner == 'npm' && needs.release.outputs.releases + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Create Release PR Comment + uses: actions/github-script@v6 + env: + RELEASES: ${{ needs.release.outputs.releases }} + with: + script: | + const releases = JSON.parse(process.env.RELEASES) + const { runId, repo: { owner, repo } } = context + const issue_number = releases[0].prNumber + + let body = '## Release Workflow\n\n' + for (const { pkgName, version, url } of releases) { + body += `- \`${pkgName}@${version}\` ${url}\n` + } + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) + console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`) + const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at')) + + for (const comment of releaseComments) { + console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`) + await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) + } + + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: `${body}- Workflow run: :arrows_counterclockwise: ${runUrl}`, + }) + + release-integration: + needs: release + name: Release Integration + if: needs.release.outputs.release + runs-on: ubuntu-latest + defaults: + run: + shell: bash + permissions: + deployments: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ fromJSON(needs.release.outputs.release).tagName }} + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@latest + run: | + npm i --prefer-online --no-fund --no-audit -g npm@latest + npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} + - name: Publish + env: + PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} + run: npm publish --provenance + + post-release-integration: + needs: [ release, release-integration ] + name: Post Release Integration - Release + if: github.repository_owner == 'npm' && needs.release.outputs.release && always() + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get Needs Result + id: needs-result + run: | + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + result="x" + elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + result="heavy_multiplication_x" + else + result="white_check_mark" + fi + echo "result=$result" >> $GITHUB_OUTPUT + - name: Update Release PR Comment + uses: actions/github-script@v6 + env: + PR_NUMBER: ${{ fromJSON(needs.release.outputs.release).prNumber }} + RESULT: ${{ steps.needs-result.outputs.result }} + with: + script: | + const { PR_NUMBER: issue_number, RESULT } = process.env + const { runId, repo: { owner, repo } } = context + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + const updateComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## Release Workflow\n\n') && + c.body.includes(runId) + ) + + if (updateComment) { + console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) + let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`) + const tagCodeowner = RESULT !== 'white_check_mark' + if (tagCodeowner) { + body += `\n\n:rotating_light:` + body += ` @npm/cli-team: The post-release workflow failed for this release.` + body += ` Manual steps may need to be taken after examining the workflow output` + body += ` from the above workflow run. :rotating_light:` + } + await github.rest.issues.updateComment({ + owner, + repo, + body, + comment_id: updateComment.id, + }) + } else { + console.log('No matching comments found:', JSON.stringify(comments, null, 2)) + } diff --git a/.gitignore b/.gitignore index dd3ceb93..00bdaf2b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,28 +4,33 @@ /* # keep these -!/.eslintrc.local.* !**/.gitignore -!/docs/ -!/tap-snapshots/ -!/test/ -!/map.js -!/scripts/ -!/README* -!/LICENSE* -!/CHANGELOG* !/.commitlintrc.js !/.eslintrc.js +!/.eslintrc.local.* !/.github/ !/.gitignore !/.npmrc -!/SECURITY.md +!/.release-please-manifest.json !/bin/ +!/CHANGELOG* !/classes/ +!/CODE_OF_CONDUCT.md +!/CONTRIBUTING.md +!/docs/ !/functions/ !/index.js !/internal/ +!/lib/ +!/LICENSE* +!/map.js !/package.json !/preload.js !/range.bnf !/ranges/ +!/README* +!/release-please-config.json +!/scripts/ +!/SECURITY.md +!/tap-snapshots/ +!/test/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..92661b3b --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "7.5.4" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ad0067..232b63ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # Changelog +## [7.5.4](https://github.com/npm/node-semver/compare/v7.5.3...v7.5.4) (2023-07-07) + +### Bug Fixes + +* [`cc6fde2`](https://github.com/npm/node-semver/commit/cc6fde2d34b95cb600d126649d926901bd2a9703) [#588](https://github.com/npm/node-semver/pull/588) trim each range set before parsing (@lukekarrys) +* [`99d8287`](https://github.com/npm/node-semver/commit/99d8287516a1d2abf0286033e2e26eca6b69c09f) [#583](https://github.com/npm/node-semver/pull/583) correctly parse long build ids as valid (#583) (@lukekarrys) + +## [7.5.3](https://github.com/npm/node-semver/compare/v7.5.2...v7.5.3) (2023-06-22) + +### Bug Fixes + +* [`abdd93d`](https://github.com/npm/node-semver/commit/abdd93d55496d22e3c15a454a5cf13f101e48bce) [#571](https://github.com/npm/node-semver/pull/571) set max lengths in regex for numeric and build identifiers (#571) (@lukekarrys) + +### Documentation + +* [`bf53dd8`](https://github.com/npm/node-semver/commit/bf53dd8da15a17eb6b8111115d0d8ef341fea5db) [#569](https://github.com/npm/node-semver/pull/569) add example for `>` comparator (#569) (@mbtools) + +## [7.5.2](https://github.com/npm/node-semver/compare/v7.5.1...v7.5.2) (2023-06-15) + +### Bug Fixes + +* [`58c791f`](https://github.com/npm/node-semver/commit/58c791f40ba8cf4be35a5ca6644353ecd6249edc) [#566](https://github.com/npm/node-semver/pull/566) diff when detecting major change from prerelease (#566) (@lukekarrys) +* [`5c8efbc`](https://github.com/npm/node-semver/commit/5c8efbcb3c6c125af10746d054faff13e8c33fbd) [#565](https://github.com/npm/node-semver/pull/565) preserve build in raw after inc (#565) (@lukekarrys) +* [`717534e`](https://github.com/npm/node-semver/commit/717534ee353682f3bcf33e60a8af4292626d4441) [#564](https://github.com/npm/node-semver/pull/564) better handling of whitespace (#564) (@lukekarrys) + +## [7.5.1](https://github.com/npm/node-semver/compare/v7.5.0...v7.5.1) (2023-05-12) + +### Bug Fixes + +* [`d30d25a`](https://github.com/npm/node-semver/commit/d30d25a5c1fb963c3cc9178cb1769fe45e4a3cab) [#559](https://github.com/npm/node-semver/pull/559) show type on invalid semver error (#559) (@tjenkinson) + +## [7.5.0](https://github.com/npm/node-semver/compare/v7.4.0...v7.5.0) (2023-04-17) + +### Features + +* [`503a4e5`](https://github.com/npm/node-semver/commit/503a4e52fe2b1c6ed1400d33149f7733c8361eed) [#548](https://github.com/npm/node-semver/pull/548) allow identifierBase to be false (#548) (@lsvalina) + +### Bug Fixes + +* [`e219bb4`](https://github.com/npm/node-semver/commit/e219bb454036a0c23e34407591f921c8edb688e7) [#552](https://github.com/npm/node-semver/pull/552) throw on bad version with correct error message (#552) (@wraithgar) +* [`fc2f3df`](https://github.com/npm/node-semver/commit/fc2f3df0b5d25253b3580607e111a9a280d888ca) [#546](https://github.com/npm/node-semver/pull/546) incorrect results from diff sometimes with prerelease versions (#546) (@tjenkinson) +* [`2781767`](https://github.com/npm/node-semver/commit/27817677794f592b592bf6181a80a4824ff762b2) [#547](https://github.com/npm/node-semver/pull/547) avoid re-instantiating SemVer during diff compare (#547) (@macno) + +## [7.4.0](https://github.com/npm/node-semver/compare/v7.3.8...v7.4.0) (2023-04-10) + +### Features + +* [`113f513`](https://github.com/npm/node-semver/commit/113f51312a1a6b6aa50d4f9486b4fde21782c1f5) [#532](https://github.com/npm/node-semver/pull/532) identifierBase parameter for .inc (#532) (@wraithgar, @b-bly) +* [`48d8f8f`](https://github.com/npm/node-semver/commit/48d8f8fa63bf6e35db70ff840b6da1a51596a5a8) [#530](https://github.com/npm/node-semver/pull/530) export new RELEASE_TYPES constant (@hcharley) + +### Bug Fixes + +* [`940723d`](https://github.com/npm/node-semver/commit/940723d22bca824993627c45ac30dd3d2854b8cd) [#538](https://github.com/npm/node-semver/pull/538) intersects with v0.0.0 and v0.0.0-0 (#538) (@wraithgar) +* [`aa516b5`](https://github.com/npm/node-semver/commit/aa516b50b32f5a144017d8fc1b9efe0540963c91) [#535](https://github.com/npm/node-semver/pull/535) faster parse options (#535) (@H4ad) +* [`61e6ea1`](https://github.com/npm/node-semver/commit/61e6ea1e9b7af01baf19ab0c0a63c8e3ebfac97c) [#536](https://github.com/npm/node-semver/pull/536) faster cache key factory for range (#536) (@H4ad) +* [`f8b8b61`](https://github.com/npm/node-semver/commit/f8b8b619e71746a47852a9d301f3087ab311444f) [#541](https://github.com/npm/node-semver/pull/541) optimistic parse (#541) (@H4ad) +* [`796cbe2`](https://github.com/npm/node-semver/commit/796cbe29b06d102e1b16f3ed78eaba210ece951e) [#533](https://github.com/npm/node-semver/pull/533) semver.diff prerelease to release recognition (#533) (@wraithgar, @dominique-blockchain) +* [`3f222b1`](https://github.com/npm/node-semver/commit/3f222b144033525ca9f8a2ce5bc6e02f0401881f) [#537](https://github.com/npm/node-semver/pull/537) reuse comparators on subset (#537) (@H4ad) +* [`f66cc45`](https://github.com/npm/node-semver/commit/f66cc45c6e82eebb4b5b51af73e7b8dcaeda7e21) [#539](https://github.com/npm/node-semver/pull/539) faster diff (#539) (@H4ad) + +### Documentation + +* [`c5d29df`](https://github.com/npm/node-semver/commit/c5d29df6f75741fea27fffe3b88c9c3b28e3ca73) [#530](https://github.com/npm/node-semver/pull/530) Add "Constants" section to README (@hcharley) + +## [7.3.8](https://github.com/npm/node-semver/compare/v7.3.7...v7.3.8) (2022-10-04) + +### Bug Fixes + +* [`d8ef32c`](https://github.com/npm/node-semver/commit/d8ef32cee7a7e34310838f32451c9bcf52956b64) [#383](https://github.com/npm/node-semver/pull/383) add support for node.js esm auto exports (#383) (@MylesBorins) + +### Documentation + +* [`7209b14`](https://github.com/npm/node-semver/commit/7209b14ccd7ca35b9a1077a0b67d9ce884fe6d00) [#477](https://github.com/npm/node-semver/pull/477) update range.js comments to clarify the caret ranges examples (#477) (@amitse) + ### [7.3.7](https://github.com/npm/node-semver/compare/v7.3.6...v7.3.7) (2022-04-11) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..167043c2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ + + +All interactions in this repo are covered by the [npm Code of +Conduct](https://docs.npmjs.com/policies/conduct) + +The npm cli team may, at its own discretion, moderate, remove, or edit +any interactions such as pull requests, issues, and comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..69e88788 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ + + +# Contributing + +## Code of Conduct + +All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). + +## Reporting Bugs + +Before submitting a new bug report please search for an existing or similar report. + +Use one of our existing issue templates if you believe you've come across a unique problem. + +Duplicate issues, or issues that don't use one of our templates may get closed without a response. + +## Pull Request Conventions + +### Commits + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). + +When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: + + - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. + - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. + - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. + - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). + +### Test Coverage + +Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. + +Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. + +### Linting + +Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. + +Please make sure linting passes before submitting a PR. + +## What _not_ to contribute? + +### Dependencies + +It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. + +### Tools/Automation + +Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. diff --git a/README.md b/README.md index df54e7a0..53ea9b52 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,9 @@ Options: -l --loose Interpret versions and ranges loosely +-n <0|1> + This is the base to be used for the prerelease identifier. + -p --include-prerelease Always include prerelease versions in range matching @@ -156,7 +159,9 @@ of primitive `operators` is: For example, the comparator `>=1.2.7` would match the versions `1.2.7`, `1.2.8`, `2.5.3`, and `1.3.9`, but not the versions `1.2.6` -or `1.1.0`. +or `1.1.0`. The comparator `>1` is equivalent to `>=2.0.0` and +would match the versions `2.0.0` and `3.1.0`, but not the versions +`1.0.1` or `1.1.0`. Comparators can be joined by whitespace to form a `comparator set`, which is satisfied by the **intersection** of all of the comparators @@ -232,6 +237,35 @@ $ semver 1.2.4-beta.0 -i prerelease 1.2.4-beta.1 ``` +#### Prerelease Identifier Base + +The method `.inc` takes an optional parameter 'identifierBase' string +that will let you let your prerelease number as zero-based or one-based. +Set to `false` to omit the prerelease number altogether. +If you do not specify this parameter, it will default to zero-based. + +```javascript +semver.inc('1.2.3', 'prerelease', 'beta', '1') +// '1.2.4-beta.1' +``` + +```javascript +semver.inc('1.2.3', 'prerelease', 'beta', false) +// '1.2.4-beta' +``` + +command-line example: + +```bash +$ semver 1.2.3 -i prerelease --preid beta -n 1 +1.2.4-beta.1 +``` + +```bash +$ semver 1.2.3 -i prerelease --preid beta -n false +1.2.4-beta +``` + ### Advanced Range Syntax Advanced range syntax desugars to primitive comparators in @@ -513,6 +547,40 @@ ex. * `s.clean(' 2.1.5 ')`: `'2.1.5'` * `s.clean('~1.0.0')`: `null` +## Constants + +As a convenience, helper constants are exported to provide information about what `node-semver` supports: + +### `RELEASE_TYPES` + +- major +- premajor +- minor +- preminor +- patch +- prepatch +- prerelease + +``` +const semver = require('semver'); + +if (semver.RELEASE_TYPES.includes(arbitraryUserInput)) { + console.log('This is a valid release type!'); +} else { + console.warn('This is NOT a valid release type!'); +} +``` + +### `SEMVER_SPEC_VERSION` + +2.0.0 + +``` +const semver = require('semver'); + +console.log('We are currently using the semver specification version:', semver.SEMVER_SPEC_VERSION); +``` + ## Exported Modules -Please send vulnerability reports through [hackerone](https://hackerone.com/github). +GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). + +If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. + +If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). + +If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Thanks for helping make GitHub safe for everyone. diff --git a/bin/semver.js b/bin/semver.js index 8d1b5572..242b7ade 100755 --- a/bin/semver.js +++ b/bin/semver.js @@ -23,7 +23,10 @@ let rtl = false let identifier +let identifierBase + const semver = require('../') +const parseOptions = require('../internal/parse-options') let reverse = false @@ -71,6 +74,12 @@ const main = () => { case '-r': case '--range': range.push(argv.shift()) break + case '-n': + identifierBase = argv.shift() + if (identifierBase === 'false') { + identifierBase = false + } + break case '-c': case '--coerce': coerce = true break @@ -88,7 +97,7 @@ const main = () => { } } - options = { loose: loose, includePrerelease: includePrerelease, rtl: rtl } + options = parseOptions({ loose, includePrerelease, rtl }) versions = versions.map((v) => { return coerce ? (semver.coerce(v, options) || { version: v }).version : v @@ -127,7 +136,7 @@ const success = () => { }).map((v) => { return semver.clean(v, options) }).map((v) => { - return inc ? semver.inc(v, inc, options, identifier) : v + return inc ? semver.inc(v, inc, options, identifier, identifierBase) : v }).forEach((v, i, _) => { console.log(v) }) @@ -172,6 +181,11 @@ Options: --ltr Coerce version strings left to right (default) +-n + Base number to be used for the prerelease identifier. + Can be either 0 or 1, or false to omit the number altogether. + Defaults to 0. + Program exits successfully if any valid version satisfies all supplied ranges, and prints all satisfying versions. diff --git a/classes/comparator.js b/classes/comparator.js index 62cd204d..3d39c0ee 100644 --- a/classes/comparator.js +++ b/classes/comparator.js @@ -16,6 +16,7 @@ class Comparator { } } + comp = comp.trim().split(/\s+/).join(' ') debug('comparator', comp, options) this.options = options this.loose = !!options.loose @@ -78,13 +79,6 @@ class Comparator { throw new TypeError('a Comparator is required') } - if (!options || typeof options !== 'object') { - options = { - loose: !!options, - includePrerelease: false, - } - } - if (this.operator === '') { if (this.value === '') { return true @@ -97,39 +91,50 @@ class Comparator { return new Range(this.value, options).test(comp.semver) } - const sameDirectionIncreasing = - (this.operator === '>=' || this.operator === '>') && - (comp.operator === '>=' || comp.operator === '>') - const sameDirectionDecreasing = - (this.operator === '<=' || this.operator === '<') && - (comp.operator === '<=' || comp.operator === '<') - const sameSemVer = this.semver.version === comp.semver.version - const differentDirectionsInclusive = - (this.operator === '>=' || this.operator === '<=') && - (comp.operator === '>=' || comp.operator === '<=') - const oppositeDirectionsLessThan = - cmp(this.semver, '<', comp.semver, options) && - (this.operator === '>=' || this.operator === '>') && - (comp.operator === '<=' || comp.operator === '<') - const oppositeDirectionsGreaterThan = - cmp(this.semver, '>', comp.semver, options) && - (this.operator === '<=' || this.operator === '<') && - (comp.operator === '>=' || comp.operator === '>') - - return ( - sameDirectionIncreasing || - sameDirectionDecreasing || - (sameSemVer && differentDirectionsInclusive) || - oppositeDirectionsLessThan || - oppositeDirectionsGreaterThan - ) + options = parseOptions(options) + + // Special cases where nothing can possibly be lower + if (options.includePrerelease && + (this.value === '<0.0.0-0' || comp.value === '<0.0.0-0')) { + return false + } + if (!options.includePrerelease && + (this.value.startsWith('<0.0.0') || comp.value.startsWith('<0.0.0'))) { + return false + } + + // Same direction increasing (> or >=) + if (this.operator.startsWith('>') && comp.operator.startsWith('>')) { + return true + } + // Same direction decreasing (< or <=) + if (this.operator.startsWith('<') && comp.operator.startsWith('<')) { + return true + } + // same SemVer and both sides are inclusive (<= or >=) + if ( + (this.semver.version === comp.semver.version) && + this.operator.includes('=') && comp.operator.includes('=')) { + return true + } + // opposite directions less than + if (cmp(this.semver, '<', comp.semver, options) && + this.operator.startsWith('>') && comp.operator.startsWith('<')) { + return true + } + // opposite directions greater than + if (cmp(this.semver, '>', comp.semver, options) && + this.operator.startsWith('<') && comp.operator.startsWith('>')) { + return true + } + return false } } module.exports = Comparator const parseOptions = require('../internal/parse-options') -const { re, t } = require('../internal/re') +const { safeRe: re, t } = require('../internal/re') const cmp = require('../functions/cmp') const debug = require('../internal/debug') const SemVer = require('./semver') diff --git a/classes/range.js b/classes/range.js index 7dc24bc7..7e7c4141 100644 --- a/classes/range.js +++ b/classes/range.js @@ -26,9 +26,16 @@ class Range { this.loose = !!options.loose this.includePrerelease = !!options.includePrerelease - // First, split based on boolean or || + // First reduce all whitespace as much as possible so we do not have to rely + // on potentially slow regexes like \s*. This is then stored and used for + // future error messages as well. this.raw = range - this.set = range + .trim() + .split(/\s+/) + .join(' ') + + // First, split on || + this.set = this.raw .split('||') // map the range to a 2d array of comparators .map(r => this.parseRange(r.trim())) @@ -38,7 +45,7 @@ class Range { .filter(c => c.length) if (!this.set.length) { - throw new TypeError(`Invalid SemVer Range: ${range}`) + throw new TypeError(`Invalid SemVer Range: ${this.raw}`) } // if we have any that are not the null set, throw out null sets. @@ -64,9 +71,7 @@ class Range { format () { this.range = this.set - .map((comps) => { - return comps.join(' ').trim() - }) + .map((comps) => comps.join(' ').trim()) .join('||') .trim() return this.range @@ -77,12 +82,12 @@ class Range { } parseRange (range) { - range = range.trim() - // memoize range parsing for performance. // this is a very hot path, and fully deterministic. - const memoOpts = Object.keys(this.options).join(',') - const memoKey = `parseRange:${memoOpts}:${range}` + const memoOpts = + (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | + (this.options.loose && FLAG_LOOSE) + const memoKey = memoOpts + ':' + range const cached = cache.get(memoKey) if (cached) { return cached @@ -93,18 +98,18 @@ class Range { const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] range = range.replace(hr, hyphenReplace(this.options.includePrerelease)) debug('hyphen replace', range) + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) debug('comparator trim', range) // `~ 1.2.3` => `~1.2.3` range = range.replace(re[t.TILDETRIM], tildeTrimReplace) + debug('tilde trim', range) // `^ 1.2.3` => `^1.2.3` range = range.replace(re[t.CARETTRIM], caretTrimReplace) - - // normalize spaces - range = range.split(/\s+/).join(' ') + debug('caret trim', range) // At this point, the range is completely trimmed and // ready to be split into comparators. @@ -190,6 +195,7 @@ class Range { return false } } + module.exports = Range const LRU = require('lru-cache') @@ -200,12 +206,13 @@ const Comparator = require('./comparator') const debug = require('../internal/debug') const SemVer = require('./semver') const { - re, + safeRe: re, t, comparatorTrimReplace, tildeTrimReplace, caretTrimReplace, } = require('../internal/re') +const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants') const isNullSet = c => c.value === '<0.0.0-0' const isAny = c => c.value === '' @@ -252,10 +259,14 @@ const isX = id => !id || id.toLowerCase() === 'x' || id === '*' // ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0-0 // ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0-0 // ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0-0 -const replaceTildes = (comp, options) => - comp.trim().split(/\s+/).map((c) => { - return replaceTilde(c, options) - }).join(' ') +// ~0.0.1 --> >=0.0.1 <0.1.0-0 +const replaceTildes = (comp, options) => { + return comp + .trim() + .split(/\s+/) + .map((c) => replaceTilde(c, options)) + .join(' ') +} const replaceTilde = (comp, options) => { const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] @@ -291,10 +302,15 @@ const replaceTilde = (comp, options) => { // ^1.2, ^1.2.x --> >=1.2.0 <2.0.0-0 // ^1.2.3 --> >=1.2.3 <2.0.0-0 // ^1.2.0 --> >=1.2.0 <2.0.0-0 -const replaceCarets = (comp, options) => - comp.trim().split(/\s+/).map((c) => { - return replaceCaret(c, options) - }).join(' ') +// ^0.0.1 --> >=0.0.1 <0.0.2-0 +// ^0.1.0 --> >=0.1.0 <0.2.0-0 +const replaceCarets = (comp, options) => { + return comp + .trim() + .split(/\s+/) + .map((c) => replaceCaret(c, options)) + .join(' ') +} const replaceCaret = (comp, options) => { debug('caret', comp, options) @@ -351,9 +367,10 @@ const replaceCaret = (comp, options) => { const replaceXRanges = (comp, options) => { debug('replaceXRanges', comp, options) - return comp.split(/\s+/).map((c) => { - return replaceXRange(c, options) - }).join(' ') + return comp + .split(/\s+/) + .map((c) => replaceXRange(c, options)) + .join(' ') } const replaceXRange = (comp, options) => { @@ -436,12 +453,15 @@ const replaceXRange = (comp, options) => { const replaceStars = (comp, options) => { debug('replaceStars', comp, options) // Looseness is ignored here. star is always as loose as it gets! - return comp.trim().replace(re[t.STAR], '') + return comp + .trim() + .replace(re[t.STAR], '') } const replaceGTE0 = (comp, options) => { debug('replaceGTE0', comp, options) - return comp.trim() + return comp + .trim() .replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], '') } @@ -479,7 +499,7 @@ const hyphenReplace = incPr => ($0, to = `<=${to}` } - return (`${from} ${to}`).trim() + return `${from} ${to}`.trim() } const testSet = (set, version, options) => { diff --git a/classes/semver.js b/classes/semver.js index af629551..84e84590 100644 --- a/classes/semver.js +++ b/classes/semver.js @@ -1,6 +1,6 @@ const debug = require('../internal/debug') const { MAX_LENGTH, MAX_SAFE_INTEGER } = require('../internal/constants') -const { re, t } = require('../internal/re') +const { safeRe: re, t } = require('../internal/re') const parseOptions = require('../internal/parse-options') const { compareIdentifiers } = require('../internal/identifiers') @@ -16,7 +16,7 @@ class SemVer { version = version.version } } else if (typeof version !== 'string') { - throw new TypeError(`Invalid Version: ${version}`) + throw new TypeError(`Invalid version. Must be a string. Got type "${typeof version}".`) } if (version.length > MAX_LENGTH) { @@ -175,36 +175,36 @@ class SemVer { // preminor will bump the version up to the next minor release, and immediately // down to pre-release. premajor and prepatch work the same way. - inc (release, identifier) { + inc (release, identifier, identifierBase) { switch (release) { case 'premajor': this.prerelease.length = 0 this.patch = 0 this.minor = 0 this.major++ - this.inc('pre', identifier) + this.inc('pre', identifier, identifierBase) break case 'preminor': this.prerelease.length = 0 this.patch = 0 this.minor++ - this.inc('pre', identifier) + this.inc('pre', identifier, identifierBase) break case 'prepatch': // If this is already a prerelease, it will bump to the next version // drop any prereleases that might already exist, since they are not // relevant at this point. this.prerelease.length = 0 - this.inc('patch', identifier) - this.inc('pre', identifier) + this.inc('patch', identifier, identifierBase) + this.inc('pre', identifier, identifierBase) break // If the input is a non-prerelease version, this acts the same as // prepatch. case 'prerelease': if (this.prerelease.length === 0) { - this.inc('patch', identifier) + this.inc('patch', identifier, identifierBase) } - this.inc('pre', identifier) + this.inc('pre', identifier, identifierBase) break case 'major': @@ -246,9 +246,15 @@ class SemVer { break // This probably shouldn't be used publicly. // 1.0.0 'pre' would become 1.0.0-0 which is the wrong direction. - case 'pre': + case 'pre': { + const base = Number(identifierBase) ? 1 : 0 + + if (!identifier && identifierBase === false) { + throw new Error('invalid increment argument: identifier is empty') + } + if (this.prerelease.length === 0) { - this.prerelease = [0] + this.prerelease = [base] } else { let i = this.prerelease.length while (--i >= 0) { @@ -259,27 +265,36 @@ class SemVer { } if (i === -1) { // didn't increment anything - this.prerelease.push(0) + if (identifier === this.prerelease.join('.') && identifierBase === false) { + throw new Error('invalid increment argument: identifier already exists') + } + this.prerelease.push(base) } } if (identifier) { // 1.2.0-beta.1 bumps to 1.2.0-beta.2, // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + let prerelease = [identifier, base] + if (identifierBase === false) { + prerelease = [identifier] + } if (compareIdentifiers(this.prerelease[0], identifier) === 0) { if (isNaN(this.prerelease[1])) { - this.prerelease = [identifier, 0] + this.prerelease = prerelease } } else { - this.prerelease = [identifier, 0] + this.prerelease = prerelease } } break - + } default: throw new Error(`invalid increment argument: ${release}`) } - this.format() - this.raw = this.version + this.raw = this.format() + if (this.build.length) { + this.raw += `+${this.build.join('.')}` + } return this } } diff --git a/functions/coerce.js b/functions/coerce.js index 2e01452f..febbff9c 100644 --- a/functions/coerce.js +++ b/functions/coerce.js @@ -1,6 +1,6 @@ const SemVer = require('../classes/semver') const parse = require('./parse') -const { re, t } = require('../internal/re') +const { safeRe: re, t } = require('../internal/re') const coerce = (version, options) => { if (version instanceof SemVer) { diff --git a/functions/diff.js b/functions/diff.js index 87200ef3..fc224e30 100644 --- a/functions/diff.js +++ b/functions/diff.js @@ -1,23 +1,65 @@ -const parse = require('./parse') -const eq = require('./eq') +const parse = require('./parse.js') const diff = (version1, version2) => { - if (eq(version1, version2)) { + const v1 = parse(version1, null, true) + const v2 = parse(version2, null, true) + const comparison = v1.compare(v2) + + if (comparison === 0) { return null - } else { - const v1 = parse(version1) - const v2 = parse(version2) - const hasPre = v1.prerelease.length || v2.prerelease.length - const prefix = hasPre ? 'pre' : '' - const defaultResult = hasPre ? 'prerelease' : '' - for (const key in v1) { - if (key === 'major' || key === 'minor' || key === 'patch') { - if (v1[key] !== v2[key]) { - return prefix + key - } - } + } + + const v1Higher = comparison > 0 + const highVersion = v1Higher ? v1 : v2 + const lowVersion = v1Higher ? v2 : v1 + const highHasPre = !!highVersion.prerelease.length + const lowHasPre = !!lowVersion.prerelease.length + + if (lowHasPre && !highHasPre) { + // Going from prerelease -> no prerelease requires some special casing + + // If the low version has only a major, then it will always be a major + // Some examples: + // 1.0.0-1 -> 1.0.0 + // 1.0.0-1 -> 1.1.1 + // 1.0.0-1 -> 2.0.0 + if (!lowVersion.patch && !lowVersion.minor) { + return 'major' + } + + // Otherwise it can be determined by checking the high version + + if (highVersion.patch) { + // anything higher than a patch bump would result in the wrong version + return 'patch' + } + + if (highVersion.minor) { + // anything higher than a minor bump would result in the wrong version + return 'minor' } - return defaultResult // may be undefined + + // bumping major/minor/patch all have same result + return 'major' + } + + // add the `pre` prefix if we are going to a prerelease version + const prefix = highHasPre ? 'pre' : '' + + if (v1.major !== v2.major) { + return prefix + 'major' + } + + if (v1.minor !== v2.minor) { + return prefix + 'minor' + } + + if (v1.patch !== v2.patch) { + return prefix + 'patch' } + + // high and low are preleases + return 'prerelease' } + module.exports = diff diff --git a/functions/inc.js b/functions/inc.js index 62d1da2c..7670b1be 100644 --- a/functions/inc.js +++ b/functions/inc.js @@ -1,7 +1,8 @@ const SemVer = require('../classes/semver') -const inc = (version, release, options, identifier) => { +const inc = (version, release, options, identifier, identifierBase) => { if (typeof (options) === 'string') { + identifierBase = identifier identifier = options options = undefined } @@ -10,7 +11,7 @@ const inc = (version, release, options, identifier) => { return new SemVer( version instanceof SemVer ? version.version : version, options - ).inc(release, identifier).version + ).inc(release, identifier, identifierBase).version } catch (er) { return null } diff --git a/functions/parse.js b/functions/parse.js index a66663aa..459b3b17 100644 --- a/functions/parse.js +++ b/functions/parse.js @@ -1,32 +1,15 @@ -const { MAX_LENGTH } = require('../internal/constants') -const { re, t } = require('../internal/re') const SemVer = require('../classes/semver') - -const parseOptions = require('../internal/parse-options') -const parse = (version, options) => { - options = parseOptions(options) - +const parse = (version, options, throwErrors = false) => { if (version instanceof SemVer) { return version } - - if (typeof version !== 'string') { - return null - } - - if (version.length > MAX_LENGTH) { - return null - } - - const r = options.loose ? re[t.LOOSE] : re[t.FULL] - if (!r.test(version)) { - return null - } - try { return new SemVer(version, options) } catch (er) { - return null + if (!throwErrors) { + return null + } + throw er } } diff --git a/index.js b/index.js index 57e2ae64..86d42ac1 100644 --- a/index.js +++ b/index.js @@ -1,48 +1,89 @@ // just pre-load all the stuff that index.js lazily exports const internalRe = require('./internal/re') +const constants = require('./internal/constants') +const SemVer = require('./classes/semver') +const identifiers = require('./internal/identifiers') +const parse = require('./functions/parse') +const valid = require('./functions/valid') +const clean = require('./functions/clean') +const inc = require('./functions/inc') +const diff = require('./functions/diff') +const major = require('./functions/major') +const minor = require('./functions/minor') +const patch = require('./functions/patch') +const prerelease = require('./functions/prerelease') +const compare = require('./functions/compare') +const rcompare = require('./functions/rcompare') +const compareLoose = require('./functions/compare-loose') +const compareBuild = require('./functions/compare-build') +const sort = require('./functions/sort') +const rsort = require('./functions/rsort') +const gt = require('./functions/gt') +const lt = require('./functions/lt') +const eq = require('./functions/eq') +const neq = require('./functions/neq') +const gte = require('./functions/gte') +const lte = require('./functions/lte') +const cmp = require('./functions/cmp') +const coerce = require('./functions/coerce') +const Comparator = require('./classes/comparator') +const Range = require('./classes/range') +const satisfies = require('./functions/satisfies') +const toComparators = require('./ranges/to-comparators') +const maxSatisfying = require('./ranges/max-satisfying') +const minSatisfying = require('./ranges/min-satisfying') +const minVersion = require('./ranges/min-version') +const validRange = require('./ranges/valid') +const outside = require('./ranges/outside') +const gtr = require('./ranges/gtr') +const ltr = require('./ranges/ltr') +const intersects = require('./ranges/intersects') +const simplifyRange = require('./ranges/simplify') +const subset = require('./ranges/subset') module.exports = { + parse, + valid, + clean, + inc, + diff, + major, + minor, + patch, + prerelease, + compare, + rcompare, + compareLoose, + compareBuild, + sort, + rsort, + gt, + lt, + eq, + neq, + gte, + lte, + cmp, + coerce, + Comparator, + Range, + satisfies, + toComparators, + maxSatisfying, + minSatisfying, + minVersion, + validRange, + outside, + gtr, + ltr, + intersects, + simplifyRange, + subset, + SemVer, re: internalRe.re, src: internalRe.src, tokens: internalRe.t, - SEMVER_SPEC_VERSION: require('./internal/constants').SEMVER_SPEC_VERSION, - SemVer: require('./classes/semver'), - compareIdentifiers: require('./internal/identifiers').compareIdentifiers, - rcompareIdentifiers: require('./internal/identifiers').rcompareIdentifiers, - parse: require('./functions/parse'), - valid: require('./functions/valid'), - clean: require('./functions/clean'), - inc: require('./functions/inc'), - diff: require('./functions/diff'), - major: require('./functions/major'), - minor: require('./functions/minor'), - patch: require('./functions/patch'), - prerelease: require('./functions/prerelease'), - compare: require('./functions/compare'), - rcompare: require('./functions/rcompare'), - compareLoose: require('./functions/compare-loose'), - compareBuild: require('./functions/compare-build'), - sort: require('./functions/sort'), - rsort: require('./functions/rsort'), - gt: require('./functions/gt'), - lt: require('./functions/lt'), - eq: require('./functions/eq'), - neq: require('./functions/neq'), - gte: require('./functions/gte'), - lte: require('./functions/lte'), - cmp: require('./functions/cmp'), - coerce: require('./functions/coerce'), - Comparator: require('./classes/comparator'), - Range: require('./classes/range'), - satisfies: require('./functions/satisfies'), - toComparators: require('./ranges/to-comparators'), - maxSatisfying: require('./ranges/max-satisfying'), - minSatisfying: require('./ranges/min-satisfying'), - minVersion: require('./ranges/min-version'), - validRange: require('./ranges/valid'), - outside: require('./ranges/outside'), - gtr: require('./ranges/gtr'), - ltr: require('./ranges/ltr'), - intersects: require('./ranges/intersects'), - simplifyRange: require('./ranges/simplify'), - subset: require('./ranges/subset'), + SEMVER_SPEC_VERSION: constants.SEMVER_SPEC_VERSION, + RELEASE_TYPES: constants.RELEASE_TYPES, + compareIdentifiers: identifiers.compareIdentifiers, + rcompareIdentifiers: identifiers.rcompareIdentifiers, } diff --git a/internal/constants.js b/internal/constants.js index 4f0de59b..94be1c57 100644 --- a/internal/constants.js +++ b/internal/constants.js @@ -9,9 +9,27 @@ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || // Max safe segment length for coercion. const MAX_SAFE_COMPONENT_LENGTH = 16 +// Max safe length for a build identifier. The max length minus 6 characters for +// the shortest version with a build 0.0.0+BUILD. +const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6 + +const RELEASE_TYPES = [ + 'major', + 'premajor', + 'minor', + 'preminor', + 'patch', + 'prepatch', + 'prerelease', +] + module.exports = { - SEMVER_SPEC_VERSION, MAX_LENGTH, - MAX_SAFE_INTEGER, MAX_SAFE_COMPONENT_LENGTH, + MAX_SAFE_BUILD_LENGTH, + MAX_SAFE_INTEGER, + RELEASE_TYPES, + SEMVER_SPEC_VERSION, + FLAG_INCLUDE_PRERELEASE: 0b001, + FLAG_LOOSE: 0b010, } diff --git a/internal/parse-options.js b/internal/parse-options.js index bbd9ec77..10d64ce0 100644 --- a/internal/parse-options.js +++ b/internal/parse-options.js @@ -1,11 +1,15 @@ -// parse out just the options we care about so we always get a consistent -// obj with keys in a consistent order. -const opts = ['includePrerelease', 'loose', 'rtl'] -const parseOptions = options => - !options ? {} - : typeof options !== 'object' ? { loose: true } - : opts.filter(k => options[k]).reduce((o, k) => { - o[k] = true - return o - }, {}) +// parse out just the options we care about +const looseOption = Object.freeze({ loose: true }) +const emptyOpts = Object.freeze({ }) +const parseOptions = options => { + if (!options) { + return emptyOpts + } + + if (typeof options !== 'object') { + return looseOption + } + + return options +} module.exports = parseOptions diff --git a/internal/re.js b/internal/re.js index ed88398a..21150b3e 100644 --- a/internal/re.js +++ b/internal/re.js @@ -1,19 +1,49 @@ -const { MAX_SAFE_COMPONENT_LENGTH } = require('./constants') +const { + MAX_SAFE_COMPONENT_LENGTH, + MAX_SAFE_BUILD_LENGTH, + MAX_LENGTH, +} = require('./constants') const debug = require('./debug') exports = module.exports = {} // The actual regexps go on exports.re const re = exports.re = [] +const safeRe = exports.safeRe = [] const src = exports.src = [] const t = exports.t = {} let R = 0 +const LETTERDASHNUMBER = '[a-zA-Z0-9-]' + +// Replace some greedy regex tokens to prevent regex dos issues. These regex are +// used internally via the safeRe object since all inputs in this library get +// normalized first to trim and collapse all extra whitespace. The original +// regexes are exported for userland consumption and lower level usage. A +// future breaking change could export the safer regex only with a note that +// all input should have extra whitespace removed. +const safeRegexReplacements = [ + ['\\s', 1], + ['\\d', MAX_LENGTH], + [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH], +] + +const makeSafeRegex = (value) => { + for (const [token, max] of safeRegexReplacements) { + value = value + .split(`${token}*`).join(`${token}{0,${max}}`) + .split(`${token}+`).join(`${token}{1,${max}}`) + } + return value +} + const createToken = (name, value, isGlobal) => { + const safe = makeSafeRegex(value) const index = R++ debug(name, index, value) t[name] = index src[index] = value re[index] = new RegExp(value, isGlobal ? 'g' : undefined) + safeRe[index] = new RegExp(safe, isGlobal ? 'g' : undefined) } // The following Regular Expressions can be used for tokenizing, @@ -23,13 +53,13 @@ const createToken = (name, value, isGlobal) => { // A single `0`, or a non-zero digit followed by zero or more digits. createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*') -createToken('NUMERICIDENTIFIERLOOSE', '[0-9]+') +createToken('NUMERICIDENTIFIERLOOSE', '\\d+') // ## Non-numeric Identifier // Zero or more digits, followed by a letter or hyphen, and then zero or // more letters, digits, or hyphens. -createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*') +createToken('NONNUMERICIDENTIFIER', `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`) // ## Main Version // Three dot-separated numeric identifiers. @@ -64,7 +94,7 @@ createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE] // ## Build Metadata Identifier // Any combination of digits, letters, or hyphens. -createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+') +createToken('BUILDIDENTIFIER', `${LETTERDASHNUMBER}+`) // ## Build Metadata // Plus sign, followed by one or more period-separated build metadata diff --git a/package.json b/package.json index 7898f590..c145eca2 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,20 @@ { "name": "semver", - "version": "7.3.7", + "version": "7.5.4", "description": "The semantic version parser used by npm.", "main": "index.js", "scripts": { "test": "tap", "snap": "tap", - "preversion": "npm test", - "postversion": "npm publish", - "postpublish": "git push origin --follow-tags", "lint": "eslint \"**/*.js\"", "postlint": "template-oss-check", "lintfix": "npm run lint -- --fix", - "prepublishOnly": "git push origin --follow-tags", "posttest": "npm run lint", "template-oss-apply": "template-oss-apply --force" }, "devDependencies": { - "@npmcli/eslint-config": "^3.0.1", - "@npmcli/template-oss": "3.3.2", + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.17.0", "tap": "^16.0.0" }, "license": "ISC", @@ -31,6 +27,7 @@ }, "files": [ "bin/", + "lib/", "classes/", "functions/", "internal/", @@ -40,8 +37,12 @@ "range.bnf" ], "tap": { - "check-coverage": true, - "coverage-map": "map.js" + "timeout": 30, + "coverage-map": "map.js", + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] }, "engines": { "node": ">=10" @@ -52,17 +53,18 @@ "author": "GitHub Inc.", "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "3.3.2", + "version": "4.17.0", "engines": ">=10", "ciVersions": [ "10.0.0", "10.x", "12.x", "14.x", - "16.x" + "16.x", + "18.x" ], + "npmSpec": "8", "distPaths": [ - "bin/", "classes/", "functions/", "internal/", @@ -70,6 +72,16 @@ "index.js", "preload.js", "range.bnf" - ] + ], + "allowPaths": [ + "/classes/", + "/functions/", + "/internal/", + "/ranges/", + "/index.js", + "/preload.js", + "/range.bnf" + ], + "publish": "true" } } diff --git a/ranges/intersects.js b/ranges/intersects.js index 3d1a6f31..e0e9b7ce 100644 --- a/ranges/intersects.js +++ b/ranges/intersects.js @@ -2,6 +2,6 @@ const Range = require('../classes/range') const intersects = (r1, r2, options) => { r1 = new Range(r1, options) r2 = new Range(r2, options) - return r1.intersects(r2) + return r1.intersects(r2, options) } module.exports = intersects diff --git a/ranges/subset.js b/ranges/subset.js index e0dea43c..1e5c2683 100644 --- a/ranges/subset.js +++ b/ranges/subset.js @@ -68,6 +68,9 @@ const subset = (sub, dom, options = {}) => { return true } +const minimumVersionWithPreRelease = [new Comparator('>=0.0.0-0')] +const minimumVersion = [new Comparator('>=0.0.0')] + const simpleSubset = (sub, dom, options) => { if (sub === dom) { return true @@ -77,9 +80,9 @@ const simpleSubset = (sub, dom, options) => { if (dom.length === 1 && dom[0].semver === ANY) { return true } else if (options.includePrerelease) { - sub = [new Comparator('>=0.0.0-0')] + sub = minimumVersionWithPreRelease } else { - sub = [new Comparator('>=0.0.0')] + sub = minimumVersion } } @@ -87,7 +90,7 @@ const simpleSubset = (sub, dom, options) => { if (options.includePrerelease) { return true } else { - dom = [new Comparator('>=0.0.0')] + dom = minimumVersion } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..73d1e353 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,36 @@ +{ + "exclude-packages-from-root": true, + "group-pull-request-title-pattern": "chore: release ${version}", + "pull-request-title-pattern": "chore: release${component} ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "docs", + "section": "Documentation", + "hidden": false + }, + { + "type": "deps", + "section": "Dependencies", + "hidden": false + }, + { + "type": "chore", + "hidden": true + } + ], + "packages": { + ".": { + "package-name": "" + } + } +} diff --git a/tap-snapshots/test/bin/semver.js.test.cjs b/tap-snapshots/test/bin/semver.js.test.cjs index 4093fdab..e820ca47 100644 --- a/tap-snapshots/test/bin/semver.js.test.cjs +++ b/tap-snapshots/test/bin/semver.js.test.cjs @@ -93,6 +93,11 @@ Object { --ltr Coerce version strings left to right (default) + -n + Base number to be used for the prerelease identifier. + Can be either 0 or 1, or false to omit the number altogether. + Defaults to 0. + Program exits successfully if any valid version satisfies all supplied ranges, and prints all satisfying versions. @@ -149,6 +154,11 @@ Object { --ltr Coerce version strings left to right (default) + -n + Base number to be used for the prerelease identifier. + Can be either 0 or 1, or false to omit the number altogether. + Defaults to 0. + Program exits successfully if any valid version satisfies all supplied ranges, and prints all satisfying versions. @@ -205,6 +215,11 @@ Object { --ltr Coerce version strings left to right (default) + -n + Base number to be used for the prerelease identifier. + Can be either 0 or 1, or false to omit the number altogether. + Defaults to 0. + Program exits successfully if any valid version satisfies all supplied ranges, and prints all satisfying versions. @@ -261,6 +276,11 @@ Object { --ltr Coerce version strings left to right (default) + -n + Base number to be used for the prerelease identifier. + Can be either 0 or 1, or false to omit the number altogether. + Defaults to 0. + Program exits successfully if any valid version satisfies all supplied ranges, and prints all satisfying versions. @@ -301,6 +321,24 @@ Object { } ` +exports[`test/bin/semver.js TAP inc tests > -i premajor 1.0.0 --preid=beta -n 1 1`] = ` +Object { + "code": 0, + "err": "", + "out": "2.0.0-beta.1\\n", + "signal": null, +} +` + +exports[`test/bin/semver.js TAP inc tests > -i premajor 1.0.0 --preid=beta -n false 1`] = ` +Object { + "code": 0, + "err": "", + "out": "2.0.0-beta\\n", + "signal": null, +} +` + exports[`test/bin/semver.js TAP inc tests > -i premajor 1.0.0 --preid=beta 1`] = ` Object { "code": 0, diff --git a/test/bin/semver.js b/test/bin/semver.js index 04333fd5..262ca380 100644 --- a/test/bin/semver.js +++ b/test/bin/semver.js @@ -29,6 +29,8 @@ t.test('inc tests', t => Promise.all([ ['-i', 'major', '1.0.0'], ['-i', 'major', '1.0.0', '1.0.1'], ['-i', 'premajor', '1.0.0', '--preid=beta'], + ['-i', 'premajor', '1.0.0', '--preid=beta', '-n', '1'], + ['-i', 'premajor', '1.0.0', '--preid=beta', '-n', 'false'], ['-i', '1.2.3'], ].map(args => t.resolveMatchSnapshot(run(args), args.join(' '))))) diff --git a/test/classes/comparator.js b/test/classes/comparator.js index 6c2e1215..209a024b 100644 --- a/test/classes/comparator.js +++ b/test/classes/comparator.js @@ -22,17 +22,18 @@ test('tostrings', (t) => { test('intersect comparators', (t) => { t.plan(comparatorIntersection.length) - comparatorIntersection.forEach(([c0, c1, expect]) => t.test(`${c0} ${c1} ${expect}`, t => { - const comp0 = new Comparator(c0) - const comp1 = new Comparator(c1) - - t.equal(comp0.intersects(comp1, false), expect, - `${c0} intersects ${c1}`) - - t.equal(comp1.intersects(comp0, { loose: false }), expect, - `${c1} intersects ${c0}`) - t.end() - })) + comparatorIntersection.forEach(([c0, c1, expect, includePrerelease]) => + t.test(`${c0} ${c1} ${expect}`, t => { + const comp0 = new Comparator(c0) + const comp1 = new Comparator(c1) + + t.equal(comp0.intersects(comp1, { includePrerelease }), expect, + `${c0} intersects ${c1}`) + + t.equal(comp1.intersects(comp0, { includePrerelease }), expect, + `${c1} intersects ${c0}`) + t.end() + })) }) test('intersect demands another comparator', t => { diff --git a/test/classes/semver.js b/test/classes/semver.js index 50619c0f..85a0ec31 100644 --- a/test/classes/semver.js +++ b/test/classes/semver.js @@ -62,15 +62,19 @@ test('really big numeric prerelease value', (t) => { }) test('invalid version numbers', (t) => { - ['1.2.3.4', - 'NOT VALID', - 1.2, - null, - 'Infinity.NaN.Infinity', - ].forEach((v) => { - t.throws(() => { - new SemVer(v) // eslint-disable-line no-new - }, { name: 'TypeError', message: `Invalid Version: ${v}` }) + ['1.2.3.4', 'NOT VALID', 1.2, null, 'Infinity.NaN.Infinity'].forEach((v) => { + t.throws( + () => { + new SemVer(v) // eslint-disable-line no-new + }, + { + name: 'TypeError', + message: + typeof v === 'string' + ? `Invalid Version: ${v}` + : `Invalid version. Must be a string. Got type "${typeof v}".`, + } + ) }) t.end() @@ -84,12 +88,20 @@ test('incrementing', t => { expect, options, id, + base, ]) => t.test(`${version} ${inc} ${id || ''}`.trim(), t => { - t.plan(1) if (expect === null) { - t.throws(() => new SemVer(version, options).inc(inc, id)) + t.plan(1) + t.throws(() => new SemVer(version, options).inc(inc, id, base)) } else { - t.equal(new SemVer(version, options).inc(inc, id).version, expect) + t.plan(2) + const incremented = new SemVer(version, options).inc(inc, id, base) + t.equal(incremented.version, expect) + if (incremented.build.length) { + t.equal(incremented.raw, `${expect}+${incremented.build.join('.')}`) + } else { + t.equal(incremented.raw, expect) + } } })) }) @@ -111,21 +123,6 @@ test('compare main vs pre', (t) => { t.end() }) -test('invalid version numbers', (t) => { - ['1.2.3.4', - 'NOT VALID', - 1.2, - null, - 'Infinity.NaN.Infinity', - ].forEach((v) => { - t.throws(() => { - new SemVer(v) // eslint-disable-line no-new - }, { name: 'TypeError', message: `Invalid Version: ${v}` }) - }) - - t.end() -}) - test('compareBuild', (t) => { const noBuild = new SemVer('1.0.0') const build0 = new SemVer('1.0.0+0') diff --git a/test/fixtures/comparator-intersection.js b/test/fixtures/comparator-intersection.js index 5f24acce..1d777d38 100644 --- a/test/fixtures/comparator-intersection.js +++ b/test/fixtures/comparator-intersection.js @@ -1,4 +1,4 @@ -// c0, c1, expected intersection +// c0, c1, expected intersection, includePrerelease module.exports = [ // One is a Version ['1.3.0', '>=1.3.0', true], @@ -33,4 +33,10 @@ module.exports = [ ['', '', true], ['', '>1.0.0', true], ['<=2.0.0', '', true], + ['<0.0.0', '<0.1.0', false], + ['<0.1.0', '<0.0.0', false], + ['<0.0.0-0', '<0.1.0', false], + ['<0.1.0', '<0.0.0-0', false], + ['<0.0.0-0', '<0.1.0', false, true], + ['<0.1.0', '<0.0.0-0', false, true], ] diff --git a/test/fixtures/increments.js b/test/fixtures/increments.js index 6a998b5f..65e9530b 100644 --- a/test/fixtures/increments.js +++ b/test/fixtures/increments.js @@ -1,5 +1,5 @@ -// [version, inc, result, options, identifier] -// inc(version, inc) -> result +// [version, inc, result, options, identifier, identifierBase] +// inc(version, inc, options, identifier, identifierBase) -> result module.exports = [ ['1.2.3', 'major', '2.0.0'], ['1.2.3', 'minor', '1.3.0'], @@ -79,12 +79,49 @@ module.exports = [ ['1.2.3-1', 'preminor', '1.3.0-dev.0', false, 'dev'], ['1.2.0', 'premajor', '2.0.0-dev.0', false, 'dev'], ['1.2.3-1', 'premajor', '2.0.0-dev.0', false, 'dev'], + ['1.2.3-1', 'premajor', '2.0.0-dev.1', false, 'dev', 1], ['1.2.0-1', 'minor', '1.2.0', false, 'dev'], ['1.0.0-1', 'major', '1.0.0', 'dev'], ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev.0', false, 'dev'], - ['1.2.3-0', 'prerelease', '1.2.3-1.0', false, '1'], ['1.2.3-1.0', 'prerelease', '1.2.3-1.1', false, '1'], ['1.2.3-1.1', 'prerelease', '1.2.3-1.2', false, '1'], ['1.2.3-1.1', 'prerelease', '1.2.3-2.0', false, '2'], + + // [version, inc, result, identifierIndex, loose, identifier] + ['1.2.0-1', 'prerelease', '1.2.0-alpha.0', false, 'alpha', '0'], + ['1.2.1', 'prerelease', '1.2.2-alpha.0', false, 'alpha', '0'], + ['0.2.0', 'prerelease', '0.2.1-alpha.0', false, 'alpha', '0'], + ['1.2.2', 'prerelease', '1.2.3-alpha.1', false, 'alpha', '1'], + ['1.2.3', 'prerelease', '1.2.4-alpha.1', false, 'alpha', '1'], + ['1.2.4', 'prerelease', '1.2.5-alpha.1', false, 'alpha', '1'], + ['1.2.0', 'prepatch', '1.2.1-dev.1', false, 'dev', '1'], + ['1.2.0-1', 'prepatch', '1.2.1-dev.1', false, 'dev', '1'], + ['1.2.0', 'premajor', '2.0.0-dev.0', false, 'dev', '0'], + ['1.2.3-1', 'premajor', '2.0.0-dev.0', false, 'dev', '0'], + ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev.0', false, 'dev', '0'], + ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev.1', false, 'dev', '1'], + ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev.bar.0', false, '', '0'], + ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev.bar.1', false, '', '1'], + ['1.2.0', 'preminor', '1.3.0-dev.1', false, 'dev', '1'], + ['1.2.3-1', 'preminor', '1.3.0-dev.0', false, 'dev'], + ['1.2.0', 'prerelease', '1.2.1-1', false, '', '1'], + + ['1.2.0-1', 'prerelease', '1.2.0-alpha', false, 'alpha', false], + ['1.2.1', 'prerelease', '1.2.2-alpha', false, 'alpha', false], + ['1.2.2', 'prerelease', '1.2.3-alpha', false, 'alpha', false], + ['1.2.0', 'prepatch', '1.2.1-dev', false, 'dev', false], + ['1.2.0-1', 'prepatch', '1.2.1-dev', false, 'dev', false], + ['1.2.0', 'premajor', '2.0.0-dev', false, 'dev', false], + ['1.2.3-1', 'premajor', '2.0.0-dev', false, 'dev', false], + ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev', false, 'dev', false], + ['1.2.3-dev.bar', 'prerelease', '1.2.3-dev.baz', false, 'dev.baz', false], + ['1.2.0', 'preminor', '1.3.0-dev', false, 'dev', false], + ['1.2.3-1', 'preminor', '1.3.0-dev', false, 'dev', false], + ['1.2.3-dev', 'prerelease', null, false, 'dev', false], + ['1.2.0-dev', 'premajor', '2.0.0-dev', false, 'dev', false], + ['1.2.0-dev', 'preminor', '1.3.0-beta', false, 'beta', false], + ['1.2.0-dev', 'prepatch', '1.2.1-dev', false, 'dev', false], + ['1.2.0', 'prerelease', null, false, '', false], + ['1.0.0-rc.1+build.4', 'prerelease', '1.0.0-rc.2', 'rc', false], ] diff --git a/test/fixtures/range-exclude.js b/test/fixtures/range-exclude.js index 9e829445..2789148a 100644 --- a/test/fixtures/range-exclude.js +++ b/test/fixtures/range-exclude.js @@ -80,7 +80,6 @@ module.exports = [ ['^1.0.0', '1.0.0-rc1', { includePrerelease: true }], ['^1.0.0', '2.0.0-rc1', { includePrerelease: true }], ['^1.2.3-rc2', '2.0.0', { includePrerelease: true }], - ['^1.0.0', '2.0.0-rc1', { includePrerelease: true }], ['^1.0.0', '2.0.0-rc1'], ['1 - 2', '3.0.0-pre', { includePrerelease: true }], @@ -103,4 +102,6 @@ module.exports = [ ['>=1.0.0 <1.1.0', '1.1.0', { includePrerelease: true }], ['>=1.0.0 <1.1.0', '1.1.0-pre'], ['>=1.0.0 <1.1.0-pre', '1.1.0-pre'], + + ['== 1.0.0 || foo', '2.0.0', { loose: true }], ] diff --git a/test/fixtures/range-include.js b/test/fixtures/range-include.js index da20f6ce..cdb7034b 100644 --- a/test/fixtures/range-include.js +++ b/test/fixtures/range-include.js @@ -84,7 +84,6 @@ module.exports = [ ['~1.2.1 1.2.3', '1.2.3'], ['~1.2.1 >=1.2.3 1.2.3', '1.2.3'], ['~1.2.1 1.2.3 >=1.2.3', '1.2.3'], - ['~1.2.1 1.2.3', '1.2.3'], ['>=1.2.1 1.2.3', '1.2.3'], ['1.2.3 >=1.2.1', '1.2.3'], ['>=1.2.3 >=1.2.1', '1.2.3'], diff --git a/test/fixtures/range-parse.js b/test/fixtures/range-parse.js index 83adaff8..dcafc6b5 100644 --- a/test/fixtures/range-parse.js +++ b/test/fixtures/range-parse.js @@ -14,14 +14,10 @@ module.exports = [ ['>=*', '*'], ['', '*'], ['*', '*'], - ['*', '*'], ['>=1.0.0', '>=1.0.0'], ['>1.0.0', '>1.0.0'], ['<=2.0.0', '<=2.0.0'], ['1', '>=1.0.0 <2.0.0-0'], - ['<=2.0.0', '<=2.0.0'], - ['<=2.0.0', '<=2.0.0'], - ['<2.0.0', '<2.0.0'], ['<2.0.0', '<2.0.0'], ['>= 1.0.0', '>=1.0.0'], ['>= 1.0.0', '>=1.0.0'], @@ -34,25 +30,19 @@ module.exports = [ ['< 2.0.0', '<2.0.0'], ['<\t2.0.0', '<2.0.0'], ['>=0.1.97', '>=0.1.97'], - ['>=0.1.97', '>=0.1.97'], ['0.1.20 || 1.2.4', '0.1.20||1.2.4'], ['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'], - ['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'], - ['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'], ['||', '*'], ['2.x.x', '>=2.0.0 <3.0.0-0'], ['1.2.x', '>=1.2.0 <1.3.0-0'], ['1.2.x || 2.x', '>=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0'], - ['1.2.x || 2.x', '>=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0'], ['x', '*'], ['2.*.*', '>=2.0.0 <3.0.0-0'], ['1.2.*', '>=1.2.0 <1.3.0-0'], ['1.2.* || 2.*', '>=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0'], - ['*', '*'], ['2', '>=2.0.0 <3.0.0-0'], ['2.3', '>=2.3.0 <2.4.0-0'], ['~2.4', '>=2.4.0 <2.5.0-0'], - ['~2.4', '>=2.4.0 <2.5.0-0'], ['~>3.2.1', '>=3.2.1 <3.3.0-0'], ['~1', '>=1.0.0 <2.0.0-0'], ['~>1', '>=1.0.0 <2.0.0-0'], @@ -75,7 +65,6 @@ module.exports = [ ['>= 1', '>=1.0.0'], ['<1.2', '<1.2.0-0'], ['< 1.2', '<1.2.0-0'], - ['1', '>=1.0.0 <2.0.0-0'], ['>01.02.03', '>1.2.3', true], ['>01.02.03', null], ['~1.2.3beta', '>=1.2.3-beta <1.3.0-0', { loose: true }], diff --git a/test/fixtures/version-gt-range.js b/test/fixtures/version-gt-range.js index c30055e0..51049924 100644 --- a/test/fixtures/version-gt-range.js +++ b/test/fixtures/version-gt-range.js @@ -34,7 +34,6 @@ module.exports = [ ['=0.7.x', '0.8.0'], ['=0.7.x', '0.8.0-asdf'], ['<0.7.x', '0.7.0'], - ['~1.2.2', '1.3.0'], ['1.0.0 - 2.0.0', '2.2.3'], ['1.0.0', '1.0.1'], ['<=2.0.0', '3.0.0'], @@ -46,17 +45,13 @@ module.exports = [ ['1.2.x', '1.3.3'], ['1.2.x || 2.x', '3.1.3'], ['2.*.*', '3.1.3'], - ['1.2.*', '1.3.3'], ['1.2.* || 2.*', '3.1.3'], ['2', '3.1.2'], ['2.3', '2.4.1'], - ['~2.4', '2.5.0'], // >=2.4.0 <2.5.0 ['~>3.2.1', '3.3.2'], // >=3.2.1 <3.3.0 - ['~1', '2.2.3'], // >=1.0.0 <2.0.0 ['~>1', '2.2.3'], ['~1.0', '1.1.0'], // >=1.0.0 <1.1.0 ['<1', '1.0.0'], - ['1', '2.0.0beta', true], ['<1', '1.0.0beta', true], ['< 1', '1.0.0beta', true], ['=0.7.x', '0.8.2'], diff --git a/test/fixtures/version-lt-range.js b/test/fixtures/version-lt-range.js index 5482dbf2..55fe5c17 100644 --- a/test/fixtures/version-lt-range.js +++ b/test/fixtures/version-lt-range.js @@ -30,29 +30,22 @@ module.exports = [ ['> 1.2', '1.2.1'], ['1', '0.0.0beta', true], ['~v0.5.4-pre', '0.5.4-alpha'], - ['~v0.5.4-pre', '0.5.4-alpha'], ['=0.7.x', '0.6.0'], ['=0.7.x', '0.6.0-asdf'], ['>=0.7.x', '0.6.0'], - ['~1.2.2', '1.2.1'], ['1.0.0 - 2.0.0', '0.2.3'], ['1.0.0', '0.0.1'], ['>=2.0.0', '1.0.0'], ['>=2.0.0', '1.9999.9999'], - ['>=2.0.0', '1.2.9'], - ['>2.0.0', '2.0.0'], ['>2.0.0', '1.2.9'], ['2.x.x', '1.1.3'], ['1.2.x', '1.1.3'], ['1.2.x || 2.x', '1.1.3'], ['2.*.*', '1.1.3'], - ['1.2.*', '1.1.3'], ['1.2.* || 2.*', '1.1.3'], ['2', '1.9999.9999'], ['2.3', '2.2.1'], - ['~2.4', '2.3.0'], // >=2.4.0 <2.5.0 ['~>3.2.1', '2.3.2'], // >=3.2.1 <3.3.0 - ['~1', '0.2.3'], // >=1.0.0 <2.0.0 ['~>1', '0.2.3'], ['~1.0', '0.0.0'], // >=1.0.0 <1.1.0 ['>1', '1.0.0'], diff --git a/test/fixtures/version-not-gt-range.js b/test/fixtures/version-not-gt-range.js index 15e7b6af..1f40738e 100644 --- a/test/fixtures/version-not-gt-range.js +++ b/test/fixtures/version-not-gt-range.js @@ -48,8 +48,6 @@ module.exports = [ ['1.2.*', '1.2.3'], ['1.2.* || 2.*', '2.1.3'], ['1.2.* || 2.*', '1.2.3'], - ['1.2.* || 2.*', '1.2.3'], - ['*', '1.2.3'], ['2', '2.1.2'], ['2.3', '2.3.1'], ['~2.4', '2.4.0'], // >=2.4.0 <2.5.0 diff --git a/test/fixtures/version-not-lt-range.js b/test/fixtures/version-not-lt-range.js index 35954f7a..aac7c254 100644 --- a/test/fixtures/version-not-lt-range.js +++ b/test/fixtures/version-not-lt-range.js @@ -48,8 +48,6 @@ module.exports = [ ['1.2.*', '1.2.3'], ['1.2.* || 2.*', '2.1.3'], ['1.2.* || 2.*', '1.2.3'], - ['1.2.* || 2.*', '1.2.3'], - ['*', '1.2.3'], ['2', '2.1.2'], ['2.3', '2.3.1'], ['~2.4', '2.4.0'], // >=2.4.0 <2.5.0 diff --git a/test/functions/diff.js b/test/functions/diff.js index 2369c98f..720e159b 100644 --- a/test/functions/diff.js +++ b/test/functions/diff.js @@ -4,19 +4,36 @@ const diff = require('../../functions/diff') test('diff versions test', (t) => { // [version1, version2, result] // diff(version1, version2) -> result - [['1.2.3', '0.2.3', 'major'], + [ + ['1.2.3', '0.2.3', 'major'], + ['0.2.3', '1.2.3', 'major'], ['1.4.5', '0.2.3', 'major'], ['1.2.3', '2.0.0-pre', 'premajor'], + ['2.0.0-pre', '1.2.3', 'premajor'], ['1.2.3', '1.3.3', 'minor'], ['1.0.1', '1.1.0-pre', 'preminor'], ['1.2.3', '1.2.4', 'patch'], ['1.2.3', '1.2.4-pre', 'prepatch'], - ['0.0.1', '0.0.1-pre', 'prerelease'], - ['0.0.1', '0.0.1-pre-2', 'prerelease'], - ['1.1.0', '1.1.0-pre', 'prerelease'], + ['0.0.1', '0.0.1-pre', 'patch'], + ['0.0.1', '0.0.1-pre-2', 'patch'], + ['1.1.0', '1.1.0-pre', 'minor'], ['1.1.0-pre-1', '1.1.0-pre-2', 'prerelease'], ['1.0.0', '1.0.0', null], - + ['1.0.0-1', '1.0.0-1', null], + ['0.0.2-1', '0.0.2', 'patch'], + ['0.0.2-1', '0.0.3', 'patch'], + ['0.0.2-1', '0.1.0', 'minor'], + ['0.0.2-1', '1.0.0', 'major'], + ['0.1.0-1', '0.1.0', 'minor'], + ['1.0.0-1', '1.0.0', 'major'], + ['1.0.0-1', '1.1.1', 'major'], + ['1.0.0-1', '2.1.1', 'major'], + ['1.0.1-1', '1.0.1', 'patch'], + ['0.0.0-1', '0.0.0', 'major'], + ['1.0.0-1', '2.0.0', 'major'], + ['1.0.0-1', '2.0.0-1', 'premajor'], + ['1.0.0-1', '1.1.0-1', 'preminor'], + ['1.0.0-1', '1.0.1-1', 'prepatch'], ].forEach((v) => { const version1 = v[0] const version2 = v[1] @@ -28,3 +45,13 @@ test('diff versions test', (t) => { t.end() }) + +test('throws on bad version', (t) => { + t.throws(() => { + diff('bad', '1.2.3') + }, { + message: 'Invalid Version: bad', + name: 'TypeError', + }) + t.end() +}) diff --git a/test/functions/inc.js b/test/functions/inc.js index 909debdf..2f6f9bb4 100644 --- a/test/functions/inc.js +++ b/test/functions/inc.js @@ -4,20 +4,28 @@ const parse = require('../../functions/parse') const increments = require('../fixtures/increments.js') test('increment versions test', (t) => { - increments.forEach(([pre, what, wanted, options, id]) => { - const found = inc(pre, what, options, id) - const cmd = `inc(${pre}, ${what}, ${id})` + increments.forEach(([pre, what, wanted, options, id, base]) => { + const found = inc(pre, what, options, id, base) + const cmd = `inc(${pre}, ${what}, ${id}, ${base})` t.equal(found, wanted, `${cmd} === ${wanted}`) const parsed = parse(pre, options) const parsedAsInput = parse(pre, options) if (wanted) { - parsed.inc(what, id) + parsed.inc(what, id, base) t.equal(parsed.version, wanted, `${cmd} object version updated`) - t.equal(parsed.raw, wanted, `${cmd} object raw field updated`) + if (parsed.build.length) { + t.equal( + parsed.raw, + `${wanted}+${parsed.build.join('.')}`, + `${cmd} object raw field updated with build` + ) + } else { + t.equal(parsed.raw, wanted, `${cmd} object raw field updated`) + } const preIncObject = JSON.stringify(parsedAsInput) - inc(parsedAsInput, what, options, id) + inc(parsedAsInput, what, options, id, base) const postIncObject = JSON.stringify(parsedAsInput) t.equal( postIncObject, @@ -26,7 +34,7 @@ test('increment versions test', (t) => { ) } else if (parsed) { t.throws(() => { - parsed.inc(what, id) + parsed.inc(what, id, base) }) } else { t.equal(parsed, null) diff --git a/test/functions/parse.js b/test/functions/parse.js index 16183dc0..dd091e94 100644 --- a/test/functions/parse.js +++ b/test/functions/parse.js @@ -9,6 +9,22 @@ t.test('returns null instead of throwing when presented with garbage', t => { t.equal(parse(v, opts), null, msg)) }) +t.test('throw errors if asked to', t => { + t.throws(() => { + parse('bad', null, true) + }, { + name: 'TypeError', + message: 'Invalid Version: bad', + }) + t.throws(() => { + parse([], null, true) + }, { + name: 'TypeError', + message: 'Invalid version. Must be a string. Got type "object".', + }) + t.end() +}) + t.test('parse a version into a SemVer object', t => { t.match(parse('1.2.3'), new SemVer('1.2.3')) const s = new SemVer('4.5.6') diff --git a/test/functions/valid.js b/test/functions/valid.js index ab51fed3..33399ed7 100644 --- a/test/functions/valid.js +++ b/test/functions/valid.js @@ -2,6 +2,7 @@ const t = require('tap') const valid = require('../../functions/valid') const SemVer = require('../../classes/semver') const invalidVersions = require('../fixtures/invalid-versions') +const { MAX_SAFE_INTEGER } = require('../../internal/constants') t.test('returns null instead of throwing when presented with garbage', t => { t.plan(invalidVersions.length) @@ -17,3 +18,12 @@ t.test('validate a version into a SemVer object', t => { t.equal(valid('4.2.0foo', { loose: true }), '4.2.0-foo', 'looseness as an option') t.end() }) + +t.test('long build id', t => { + const longBuild = '-928490632884417731e7af463c92b034d6a78268fc993bcb88a57944' + const shortVersion = '1.1.1' + const longVersion = `${MAX_SAFE_INTEGER}.${MAX_SAFE_INTEGER}.${MAX_SAFE_INTEGER}` + t.equal(valid(shortVersion + longBuild), shortVersion + longBuild) + t.equal(valid(longVersion + longBuild), longVersion + longBuild) + t.end() +}) diff --git a/test/integration/whitespace.js b/test/integration/whitespace.js new file mode 100644 index 00000000..a3541325 --- /dev/null +++ b/test/integration/whitespace.js @@ -0,0 +1,49 @@ +const { test } = require('tap') +const Range = require('../../classes/range') +const SemVer = require('../../classes/semver') +const Comparator = require('../../classes/comparator') +const validRange = require('../../ranges/valid') +const minVersion = require('../../ranges/min-version') +const minSatisfying = require('../../ranges/min-satisfying') +const maxSatisfying = require('../../ranges/max-satisfying') + +const wsMedium = ' '.repeat(125) +const wsLarge = ' '.repeat(500000) +const zeroLarge = '0'.repeat(500000) + +test('range with whitespace', (t) => { + // a range with these extra characters would take a few minutes to process if + // any redos susceptible regexes were used. there is a global tap timeout per + // file set in the package.json that will error if this test takes too long. + const r = `1.2.3 ${wsLarge} <1.3.0` + t.equal(new Range(r).range, '1.2.3 <1.3.0') + t.equal(validRange(r), '1.2.3 <1.3.0') + t.equal(minVersion(r).version, '1.2.3') + t.equal(minSatisfying(['1.2.3'], r), '1.2.3') + t.equal(maxSatisfying(['1.2.3'], r), '1.2.3') + t.end() +}) + +test('range with 0', (t) => { + const r = `1.2.3 ${zeroLarge} <1.3.0` + t.throws(() => new Range(r).range) + t.equal(validRange(r), null) + t.throws(() => minVersion(r).version) + t.equal(minSatisfying(['1.2.3'], r), null) + t.equal(maxSatisfying(['1.2.3'], r), null) + t.end() +}) + +test('semver version', (t) => { + const v = `${wsMedium}1.2.3${wsMedium}` + const tooLong = `${wsLarge}1.2.3${wsLarge}` + t.equal(new SemVer(v).version, '1.2.3') + t.throws(() => new SemVer(tooLong)) + t.end() +}) + +test('comparator', (t) => { + const comparator = `${wsLarge}<${wsLarge}1.2.3${wsLarge}` + t.equal(new Comparator(comparator).value, '<1.2.3') + t.end() +}) diff --git a/test/internal/constants.js b/test/internal/constants.js index 1b72d870..a8f6ab2d 100644 --- a/test/internal/constants.js +++ b/test/internal/constants.js @@ -2,8 +2,9 @@ const t = require('tap') const constants = require('../../internal/constants') t.match(constants, { - SEMVER_SPEC_VERSION: String, MAX_LENGTH: Number, - MAX_SAFE_INTEGER: Number, MAX_SAFE_COMPONENT_LENGTH: Number, -}, 'got some numbers exported') + MAX_SAFE_INTEGER: Number, + RELEASE_TYPES: Array, + SEMVER_SPEC_VERSION: String, +}, 'got appropriate data types exported') diff --git a/test/internal/parse-options.js b/test/internal/parse-options.js index 6213423d..2400537d 100644 --- a/test/internal/parse-options.js +++ b/test/internal/parse-options.js @@ -18,12 +18,24 @@ t.test('truthy non-objects always loose mode, for backwards comp', t => { t.end() }) -t.test('objects only include truthy flags we know about, set to true', t => { - t.strictSame(parseOptions(/asdf/), {}) - t.strictSame(parseOptions(new Error('hello')), {}) - t.strictSame(parseOptions({ loose: true, a: 1, rtl: false }), { loose: true }) +t.test('any object passed is returned', t => { + t.strictSame(parseOptions(/asdf/), /asdf/) + t.strictSame(parseOptions(new Error('hello')), new Error('hello')) + t.strictSame(parseOptions({ loose: true, a: 1, rtl: false }), { loose: true, a: 1, rtl: false }) t.strictSame(parseOptions({ loose: 1, rtl: 2, includePrerelease: 10 }), { + loose: 1, + rtl: 2, + includePrerelease: 10, + }) + t.strictSame(parseOptions({ loose: true }), { loose: true }) + t.strictSame(parseOptions({ rtl: true }), { rtl: true }) + t.strictSame(parseOptions({ includePrerelease: true }), { includePrerelease: true }) + t.strictSame(parseOptions({ loose: true, rtl: true }), { loose: true, rtl: true }) + t.strictSame(parseOptions({ loose: true, includePrerelease: true }), { loose: true, + includePrerelease: true, + }) + t.strictSame(parseOptions({ rtl: true, includePrerelease: true }), { rtl: true, includePrerelease: true, }) diff --git a/test/internal/re.js b/test/internal/re.js index 1aad22ba..2851b325 100644 --- a/test/internal/re.js +++ b/test/internal/re.js @@ -1,5 +1,5 @@ const { test } = require('tap') -const { src, re } = require('../../internal/re') +const { src, re, safeRe } = require('../../internal/re') const semver = require('../../') test('has a list of src, re, and tokens', (t) => { @@ -13,5 +13,11 @@ test('has a list of src, re, and tokens', (t) => { for (const i in semver.tokens) { t.match(semver.tokens[i], Number, 'tokens are numbers') } + + safeRe.forEach(r => { + t.notMatch(r.source, '\\s+', 'safe regex do not contain greedy whitespace') + t.notMatch(r.source, '\\s*', 'safe regex do not contain greedy whitespace') + }) + t.end() }) diff --git a/test/map.js b/test/map.js index aded2454..5c36eb7d 100644 --- a/test/map.js +++ b/test/map.js @@ -1,48 +1,46 @@ const t = require('tap') +const { resolve, join, relative, extname, dirname, basename } = require('path') +const { statSync, readdirSync } = require('fs') +const map = require('../map.js') +const pkg = require('../package.json') -// ensure that the coverage map maps all coverage -const ignore = [ - '.git', - '.github', - '.commitlintrc.js', - '.eslintrc.js', - 'node_modules', - 'coverage', - 'tap-snapshots', - 'test', - 'fixtures', -] +const ROOT = resolve(__dirname, '..') +const TEST = join(ROOT, 'test') +const IGNORE_DIRS = ['fixtures', 'integration'] -const { statSync, readdirSync } = require('fs') -const find = (folder, set = [], root = true) => { - const ent = readdirSync(folder) - set.push(...ent.filter(f => !ignore.includes(f) && /\.m?js$/.test(f)).map(f => folder + '/' + f)) - for (const e of ent.filter(f => !ignore.includes(f) && !/\.m?js$/.test(f))) { - if (statSync(folder + '/' + e).isDirectory()) { - find(folder + '/' + e, set, false) +const getFile = (f) => { + try { + if (statSync(f).isFile()) { + return extname(f) === '.js' ? [f] : [] } + } catch { + return [] } - if (!root) { - return - } - return set.map(f => f.slice(folder.length + 1) - .replace(/\\/g, '/')) - .sort((a, b) => a.localeCompare(b)) } -const { resolve } = require('path') -const root = resolve(__dirname, '..') +const walk = (item, res = []) => getFile(item) || readdirSync(item) + .map(f => join(item, f)) + .reduce((acc, f) => acc.concat(statSync(f).isDirectory() ? walk(f, res) : getFile(f)), []) + .filter(Boolean) -const sut = find(root) -const tests = find(root + '/test') -t.strictSame(sut, tests, 'test files should match system files') -const map = require('../map.js') +const walkAll = (items, relativeTo) => items + .reduce((acc, f) => acc.concat(walk(join(ROOT, f))), []) + .map((f) => relative(relativeTo, f)) + .sort() -for (const testFile of tests) { - t.test(testFile, t => { - t.plan(1) - // cast to an array, since map() can return a string or array - const systemFiles = [].concat(map(testFile)) - t.ok(systemFiles.some(sys => sut.includes(sys)), 'test covers a file') - }) -} +t.test('tests match system', t => { + const sut = walkAll([pkg.tap['coverage-map'], ...pkg.files], ROOT) + const tests = walkAll([basename(TEST)], TEST) + .filter(f => !IGNORE_DIRS.includes(dirname(f))) + + t.strictSame(sut, tests, 'test files should match system files') + + for (const f of tests) { + t.test(f, t => { + t.plan(1) + t.ok(sut.includes(map(f)), 'test covers a file') + }) + } + + t.end() +}) diff --git a/test/ranges/intersects.js b/test/ranges/intersects.js index e93492b7..b23ad03d 100644 --- a/test/ranges/intersects.js +++ b/test/ranges/intersects.js @@ -7,24 +7,24 @@ const rangeIntersection = require('../fixtures/range-intersection.js') test('intersect comparators', t => { t.plan(comparatorIntersection.length) - comparatorIntersection.forEach(([c0, c1, expect]) => t.test(`${c0} ${c1} ${expect}`, t => { - const comp0 = new Comparator(c0) - const comp1 = new Comparator(c1) + comparatorIntersection.forEach(([c0, c1, expect, includePrerelease]) => + t.test(`${c0} ${c1} ${expect}`, t => { + const opts = { loose: false, includePrerelease } + const comp0 = new Comparator(c0) + const comp1 = new Comparator(c1) - t.equal(intersects(comp0, comp1), expect, `${c0} intersects ${c1} objects`) - t.equal(intersects(comp1, comp0), expect, `${c1} intersects ${c0} objects`) - t.equal(intersects(comp0, comp1, true), expect, - `${c0} intersects ${c1} loose, objects`) - t.equal(intersects(comp1, comp0, true), expect, - `${c1} intersects ${c0} loose, objects`) - t.equal(intersects(c0, c1), expect, `${c0} intersects ${c1}`) - t.equal(intersects(c1, c0), expect, `${c1} intersects ${c0}`) - t.equal(intersects(c0, c1, true), expect, - `${c0} intersects ${c1} loose`) - t.equal(intersects(c1, c0, true), expect, - `${c1} intersects ${c0} loose`) - t.end() - })) + t.equal(intersects(comp0, comp1, opts), expect, `${c0} intersects ${c1} objects`) + t.equal(intersects(comp1, comp0, opts), expect, `${c1} intersects ${c0} objects`) + t.equal(intersects(c0, c1, opts), expect, `${c0} intersects ${c1}`) + t.equal(intersects(c1, c0, opts), expect, `${c1} intersects ${c0}`) + + opts.loose = true + t.equal(intersects(comp0, comp1, opts), expect, `${c0} intersects ${c1} loose, objects`) + t.equal(intersects(comp1, comp0, opts), expect, `${c1} intersects ${c0} loose, objects`) + t.equal(intersects(c0, c1, opts), expect, `${c0} intersects ${c1} loose`) + t.equal(intersects(c1, c0, opts), expect, `${c1} intersects ${c0} loose`) + t.end() + })) }) test('ranges intersect', (t) => {