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/settings.yml b/.github/settings.yml index 1019e26f..107aa0ad 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,2 +1,26 @@ ---- -_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" ] diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 60bb334b..8b8f3748 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -27,11 +27,13 @@ jobs: 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 + - 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 Audit - run: npm audit + - 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 index ccd68e1e..98b70866 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -3,6 +3,12 @@ name: CI - Release on: + workflow_dispatch: + inputs: + ref: + required: true + type: string + default: main workflow_call: inputs: ref: @@ -21,21 +27,49 @@ jobs: 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.3.1 + 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 }} - # XXX: this does not work when using the default GITHUB_TOKEN. - # Instead we post the main job url to the PR as a comment which - # will link to all the other checks. To work around this we would - # need to create a GitHub that would create on-demand tokens. - # https://github.com/LouisBrunner/checks-action/issues/18 - # details_url: + output: ${{ steps.check-output.outputs.result }} - name: Checkout uses: actions/checkout@v3 with: @@ -48,8 +82,8 @@ jobs: 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 + - 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 @@ -59,8 +93,8 @@ jobs: - name: Post Lint run: npm run postlint --ignore-scripts - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: always() + uses: LouisBrunner/checks-action@v1.6.0 + if: steps.check.outputs.check_id && always() with: token: ${{ secrets.GITHUB_TOKEN }} conclusion: ${{ job.status }} @@ -94,21 +128,49 @@ jobs: 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.3.1 + 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 }} - # XXX: this does not work when using the default GITHUB_TOKEN. - # Instead we post the main job url to the PR as a comment which - # will link to all the other checks. To work around this we would - # need to create a GitHub that would create on-demand tokens. - # https://github.com/LouisBrunner/checks-action/issues/18 - # details_url: + output: ${{ steps.check-output.outputs.result }} - name: Checkout uses: actions/checkout@v3 with: @@ -134,9 +196,9 @@ jobs: - 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@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 i --prefer-online --no-fund --no-audit -g npm@8 - name: npm Version run: npm -v - name: Install Dependencies @@ -146,8 +208,8 @@ jobs: - name: Test run: npm test --ignore-scripts - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: always() + uses: LouisBrunner/checks-action@v1.6.0 + if: steps.check.outputs.check_id && always() with: token: ${{ secrets.GITHUB_TOKEN }} conclusion: ${{ job.status }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bc2e2db..90c632b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,54 +14,6 @@ on: - cron: "0 9 * * 1" jobs: - engines: - name: Engines - ${{ matrix.platform.name }} - ${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - node-version: - - 10.0.0 - runs-on: ${{ matrix.platform.os }} - defaults: - run: - shell: ${{ matrix.platform.shell }} - 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: ${{ 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@latest - if: ${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund --engines-strict - lint: name: Lint if: github.repository_owner == 'npm' @@ -80,8 +32,8 @@ jobs: 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 + - 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 @@ -142,9 +94,9 @@ jobs: - 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@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 i --prefer-online --no-fund --no-audit -g npm@8 - name: npm Version run: npm -v - name: Install Dependencies diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml index e854e127..03c85681 100644 --- a/.github/workflows/post-dependabot.yml +++ b/.github/workflows/post-dependabot.yml @@ -19,7 +19,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - ref: ${{ github.ref_name }} + ref: ${{ github.event.pull_request.head.ref }} - name: Setup Git User run: | git config --global user.email "npm-cli+bot@github.com" @@ -28,8 +28,8 @@ jobs: 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 + - 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 @@ -48,11 +48,11 @@ jobs: run: | dependabot_dir="${{ steps.metadata.outputs.directory }}" if [[ "$dependabot_dir" == "/" ]]; then - echo "::set-output name=workspace::-iwr" + echo "workspace=-iwr" >> $GITHUB_OUTPUT else # strip leading slash from directory so it works as a # a path to the workspace flag - echo "::set-output name=workspace::-w ${dependabot_dir#/}" + echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT fi - name: Apply Changes @@ -61,17 +61,17 @@ jobs: run: | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} if [[ `git status --porcelain` ]]; then - echo "::set-output name=changes::true" + 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.dependabot-metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then + if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then prefix='feat!' else - prefix='chore!' + prefix='chore' fi - echo "::set-output name=message::$prefix: postinstall for dependabot template-oss PR" + 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 @@ -90,7 +90,7 @@ jobs: # 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-all.outcome == 'failure' + if: steps.apply.outputs.changes && steps.push.outcome == 'failure' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1a1d1ee8..da5779df 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -31,8 +31,8 @@ jobs: 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 + - 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 @@ -41,8 +41,10 @@ jobs: id: commit continue-on-error: true run: | - npx --offline commitlint -V --from origin/${{ github.base_ref }} --to ${{ github.event.pull_request.head.sha }} + 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: | - echo ${{ github.event.pull_request.title }} | npx --offline commitlint -V + echo '$PR_TITLE' | npx --offline commitlint -V diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01a8d6a9..3b69ae10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,16 @@ 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 @@ -17,8 +23,8 @@ jobs: release: outputs: pr: ${{ steps.release.outputs.pr }} + release: ${{ steps.release.outputs.release }} releases: ${{ steps.release.outputs.releases }} - release-flags: ${{ steps.release.outputs.release-flags }} branch: ${{ steps.release.outputs.pr-branch }} pr-number: ${{ steps.release.outputs.pr-number }} comment-id: ${{ steps.pr-comment.outputs.result }} @@ -40,8 +46,8 @@ jobs: 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 + - 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 @@ -51,49 +57,82 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - npx --offline template-oss-release-please ${{ github.ref_name }} + 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 repo = { owner: context.repo.owner, repo: context.repo.repo } - const issue = { ...repo, issue_number: process.env.PR_NUMBER } + const { REF_NAME, PR_NUMBER: issue_number } = process.env + const { runId, repo: { owner, repo } } = context - const { data: workflow } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: context.runId }) + 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, issue) - let commentId = comments?.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id + 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 \`main\`. ` + 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\`\`\`` - body += `- Release workflow run: ${workflow.html_url}` if (commentId) { - await github.rest.issues.updateComment({ ...repo, comment_id: commentId, body }) + await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) } else { - const { data: comment } = await github.rest.issues.createComment({ ...issue, body }) + 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.3.1 + uses: LouisBrunner/checks-action@v1.6.0 id: check - if: steps.release.outputs.pr-number + if: steps.release.outputs.pr-sha with: token: ${{ secrets.GITHUB_TOKEN }} status: in_progress name: Release sha: ${{ steps.release.outputs.pr-sha }} - # XXX: this does not work when using the default GITHUB_TOKEN. - # Instead we post the main job url to the PR as a comment which - # will link to all the other checks. To work around this we would - # need to create a GitHub that would create on-demand tokens. - # https://github.com/LouisBrunner/checks-action/issues/18 - # details_url: + output: ${{ steps.check-output.outputs.result }} update: needs: release @@ -120,8 +159,8 @@ jobs: 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 + - 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 @@ -132,7 +171,8 @@ jobs: RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - npm run rp-pull-request --ignore-scripts -ws -iwr --if-present + 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: @@ -140,25 +180,53 @@ jobs: run: | git commit --all --amend --no-edit || true git push --force-with-lease - echo "::set-output name=sha::$(git rev-parse HEAD)" + 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.3.1 + 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 }} - # XXX: this does not work when using the default GITHUB_TOKEN. - # Instead we post the main job url to the PR as a comment which - # will link to all the other checks. To work around this we would - # need to create a GitHub that would create on-demand tokens. - # https://github.com/LouisBrunner/checks-action/issues/18 - # details_url: + output: ${{ steps.check-output.outputs.result }} - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: always() + uses: LouisBrunner/checks-action@v1.6.0 + if: needs.release.outputs.check-id && always() with: token: ${{ secrets.GITHUB_TOKEN }} conclusion: ${{ job.status }} @@ -193,10 +261,10 @@ jobs: else result="success" fi - echo "::set-output name=result::$result" + echo "result=$result" >> $GITHUB_OUTPUT - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: always() + 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 }} @@ -210,25 +278,122 @@ jobs: 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 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" + 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 - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Run Post Release Actions + run: | + npm i --prefer-online --no-fund --no-audit -g npm@latest + npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} + - name: Publish env: - RELEASES: ${{ needs.release.outputs.releases }} + 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: | - npm run rp-release --ignore-scripts --if-present ${{ join(fromJSON(needs.release.outputs.release-flags), ' ') }} + 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 3b94e235..00bdaf2b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ !/CHANGELOG* !/classes/ !/CODE_OF_CONDUCT.md +!/CONTRIBUTING.md !/docs/ !/functions/ !/index.js diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c1526ab8..0be3c6ce 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.3.8" + ".": "7.5.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 289bbac0..c08c960c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## [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 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..b52a5eb1 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 @@ -232,6 +235,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 +545,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 a791d912..53c2540f 100644 --- a/classes/range.js +++ b/classes/range.js @@ -26,19 +26,26 @@ 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())) + .map(r => this.parseRange(r)) // throw out any comparator lists that are empty // this generally means that it was not a valid range, which is allowed // in loose mode, but will still throw if the WHOLE range is invalid. .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 @@ -103,9 +108,6 @@ class Range { // `^ 1.2.3` => `^1.2.3` range = range.replace(re[t.CARETTRIM], caretTrimReplace) - // normalize spaces - range = range.split(/\s+/).join(' ') - // At this point, the range is completely trimmed and // ready to be split into comparators. @@ -190,6 +192,7 @@ class Range { return false } } + module.exports = Range const LRU = require('lru-cache') @@ -200,12 +203,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 === '' @@ -253,10 +257,13 @@ const isX = id => !id || id.toLowerCase() === 'x' || id === '*' // ~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 // ~0.0.1 --> >=0.0.1 <0.1.0-0 -const replaceTildes = (comp, options) => - comp.trim().split(/\s+/).map((c) => { - return replaceTilde(c, options) - }).join(' ') +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] @@ -294,10 +301,13 @@ const replaceTilde = (comp, options) => { // ^1.2.0 --> >=1.2.0 <2.0.0-0 // ^0.0.1 --> >=0.0.1 <0.0.2-0 // ^0.1.0 --> >=0.1.0 <0.2.0-0 -const replaceCarets = (comp, options) => - comp.trim().split(/\s+/).map((c) => { - return replaceCaret(c, options) - }).join(' ') +const replaceCarets = (comp, options) => { + return comp + .trim() + .split(/\s+/) + .map((c) => replaceCaret(c, options)) + .join(' ') +} const replaceCaret = (comp, options) => { debug('caret', comp, options) @@ -354,9 +364,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) => { @@ -439,12 +450,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], '') } @@ -482,7 +496,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 4a342c6a..86d42ac1 100644 --- a/index.js +++ b/index.js @@ -83,6 +83,7 @@ module.exports = { src: internalRe.src, tokens: internalRe.t, 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..25fab1ea 100644 --- a/internal/constants.js +++ b/internal/constants.js @@ -9,9 +9,22 @@ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || // Max safe segment length for coercion. const MAX_SAFE_COMPONENT_LENGTH = 16 +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_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..f73ef1aa 100644 --- a/internal/re.js +++ b/internal/re.js @@ -4,16 +4,27 @@ 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 createToken = (name, value, isGlobal) => { + // Replace all greedy whitespace 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 safe = value + .split('\\s*').join('\\s{0,1}') + .split('\\s+').join('\\s') 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, diff --git a/package.json b/package.json index 72d3f66e..7d0aff3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "semver", - "version": "7.3.8", + "version": "7.5.2", "description": "The semantic version parser used by npm.", "main": "index.js", "scripts": { @@ -13,8 +13,8 @@ "template-oss-apply": "template-oss-apply --force" }, "devDependencies": { - "@npmcli/eslint-config": "^3.0.1", - "@npmcli/template-oss": "4.4.4", + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.15.1", "tap": "^16.0.0" }, "license": "ISC", @@ -37,7 +37,7 @@ "range.bnf" ], "tap": { - "check-coverage": true, + "timeout": 30, "coverage-map": "map.js", "nyc-arg": [ "--exclude", @@ -53,9 +53,8 @@ "author": "GitHub Inc.", "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.4.4", + "version": "4.15.1", "engines": ">=10", - "content": "./scripts", "ciVersions": [ "10.0.0", "10.x", @@ -64,6 +63,7 @@ "16.x", "18.x" ], + "npmSpec": "8", "distPaths": [ "classes/", "functions/", @@ -81,6 +81,7 @@ "/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/scripts/_step-test.yml b/scripts/_step-test.yml deleted file mode 100644 index 5b7131bc..00000000 --- a/scripts/_step-test.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" -- name: Test - run: {{ rootNpmPath }} test --ignore-scripts 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..1e4d48f8 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) + } } })) }) @@ -112,15 +124,19 @@ test('compare main vs pre', (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() 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..4b6c5631 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 }], 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/integration/whitespace.js b/test/integration/whitespace.js new file mode 100644 index 00000000..eb9744d9 --- /dev/null +++ b/test/integration/whitespace.js @@ -0,0 +1,39 @@ +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 s = (n = 500000) => ' '.repeat(n) + +test('regex dos via range whitespace', (t) => { + // a range with this much whitespace 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 ${s()} <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('semver version', (t) => { + const v = `${s(125)}1.2.3${s(125)}` + const tooLong = `${s()}1.2.3${s()}` + t.equal(new SemVer(v).version, '1.2.3') + t.throws(() => new SemVer(tooLong)) + t.end() +}) + +test('comparator', (t) => { + const c = `${s()}<${s()}1.2.3${s()}` + t.equal(new Comparator(c).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) => {