diff --git a/.eslintrc.js b/.eslintrc.js index 5db9f81..f21d26e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,9 @@ const localConfigs = readdir(__dirname) module.exports = { root: true, + ignorePatterns: [ + 'tap-testdir*/', + ], extends: [ '@npmcli', ...localConfigs, diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 2555c28..908ae16 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -27,51 +27,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') + node-version: 20.x + check-latest: contains('20.x', '.x') - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@8 on Node 12 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index fbfa4d5..0a056a5 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -82,51 +82,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') + node-version: 20.x + check-latest: contains('20.x', '.x') - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@8 on Node 12 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi + + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v @@ -167,6 +152,7 @@ jobs: - 16.x - 18.0.0 - 18.x + - 20.x runs-on: ${{ matrix.platform.os }} defaults: run: @@ -234,7 +220,9 @@ jobs: - name: Update Windows npm if: | matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') + startsWith(steps.node.outputs.node-version, 'v10.') || + startsWith(steps.node.outputs.node-version, 'v12.') || + startsWith(steps.node.outputs.node-version, 'v14.') ) run: | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz @@ -244,34 +232,33 @@ jobs: cd .. rmdir /s /q package - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@8 on Node 12 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e61dee5..984097d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,51 +31,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') + node-version: 20.x + check-latest: contains('20.x', '.x') - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@8 on Node 12 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v @@ -109,6 +94,7 @@ jobs: - 16.x - 18.0.0 - 18.x + - 20.x runs-on: ${{ matrix.platform.os }} defaults: run: @@ -131,7 +117,9 @@ jobs: - name: Update Windows npm if: | matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') + startsWith(steps.node.outputs.node-version, 'v10.') || + startsWith(steps.node.outputs.node-version, 'v12.') || + startsWith(steps.node.outputs.node-version, 'v14.') ) run: | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz @@ -141,34 +129,33 @@ jobs: cd .. rmdir /s /q package - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@8 on Node 12 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml index c889883..9a4b761 100644 --- a/.github/workflows/post-dependabot.yml +++ b/.github/workflows/post-dependabot.yml @@ -28,51 +28,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') - - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + node-version: 20.x + check-latest: contains('20.x', '.x') - - name: Install npm@8 on Node 12 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" + + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index aac7cb6..3418d4c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -31,51 +31,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') + node-version: 20.x + check-latest: contains('20.x', '.x') - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@8 on Node 12 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9543d0..70010ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,51 +44,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') - - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + node-version: 20.x + check-latest: contains('20.x', '.x') - - name: Install npm@8 on Node 12 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" + + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi + + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v @@ -201,51 +186,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') - - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + node-version: 20.x + check-latest: contains('20.x', '.x') - - name: Install npm@8 on Node 12 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi + + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v @@ -411,51 +381,36 @@ jobs: uses: actions/setup-node@v3 id: node with: - node-version: 18.x - check-latest: contains('18.x', '.x') - - # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update Windows npm - if: | - matrix.platform.os == 'windows-latest' && ( - startsWith(steps.node.outputs.node-version, 'v10.') || startsWith(steps.node.outputs.node-version, 'v12.') || startsWith(steps.node.outputs.node-version, 'v14.') - ) - 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 - - # Start on Node 10 because we dont test on anything lower - - name: Install npm@7 on Node 10 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v10.') - id: npm-7 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@7 - echo "updated=true" >> "$GITHUB_OUTPUT" + node-version: 20.x + check-latest: contains('20.x', '.x') - - name: Install npm@8 on Node 12 + - name: Install Latest npm shell: bash - if: startsWith(steps.node.outputs.node-version, 'v12.') - id: npm-8 + env: + NODE_VERSION: ${{ steps.node.outputs.node-version }} run: | - npm i --prefer-online --no-fund --no-audit -g npm@8 - echo "updated=true" >> "$GITHUB_OUTPUT" + MATCH="" + SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") - - name: Install npm@9 on Node 14/16/18.0 - shell: bash - if: startsWith(steps.node.outputs.node-version, 'v14.') || startsWith(steps.node.outputs.node-version, 'v16.') || startsWith(steps.node.outputs.node-version, 'v18.0.') - id: npm-9 - run: | - npm i --prefer-online --no-fund --no-audit -g npm@9 - echo "updated=true" >> "$GITHUB_OUTPUT" + echo "node@$NODE_VERSION" + + for SPEC in ${SPECS[@]}; do + ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') + echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" + + if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then + MATCH=$SPEC + echo "Found compatible version: npm@$MATCH" + break + fi + done + + if [ -z $MATCH ]; then + echo "Could not find a compatible version of npm for node@$NODE_VERSION" + exit 1 + fi - - name: Install npm@latest on Node - if: ${{ !(steps.npm-7.outputs.updated || steps.npm-8.outputs.updated || steps.npm-9.outputs.updated) }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest + npm i --prefer-online --no-fund --no-audit -g npm@$MATCH - name: npm Version run: npm -v diff --git a/.gitignore b/.gitignore index b3eeced..773cada 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # ignore everything in the root /* +# transient test directories +tap-testdir*/ # keep these !**/.gitignore @@ -27,3 +29,4 @@ !/SECURITY.md !/tap-snapshots/ !/test/ +!/tsconfig.json diff --git a/lib/index.js b/lib/index.js index 2b9f3c2..c21dd64 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,55 +1,78 @@ 'use strict' -const hexify = char => { +const INDENT = Symbol.for('indent') +const NEWLINE = Symbol.for('newline') + +const DEFAULT_NEWLINE = '\n' +const DEFAULT_INDENT = ' ' +const BOM = /^\uFEFF/ + +// only respect indentation if we got a line break, otherwise squash it +// things other than objects and arrays aren't indented, so ignore those +// Important: in both of these regexps, the $1 capture group is the newline +// or undefined, and the $2 capture group is the indent, or undefined. +const FORMAT = /^\s*[{[]((?:\r?\n)+)([\s\t]*)/ +const EMPTY = /^(?:\{\}|\[\])((?:\r?\n)+)?$/ + +// Node 20 puts single quotes around the token and a comma after it +const UNEXPECTED_TOKEN = /^Unexpected token '?(.)'?(,)? /i + +const hexify = (char) => { const h = char.charCodeAt(0).toString(16).toUpperCase() - return '0x' + (h.length % 2 ? '0' : '') + h + return `0x${h.length % 2 ? '0' : ''}${h}` } -const parseError = (e, txt, context) => { +// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) +// because the buffer-to-string conversion in `fs.readFileSync()` +// translates it to FEFF, the UTF-16 BOM. +const stripBOM = (txt) => String(txt).replace(BOM, '') + +const makeParsedError = (msg, parsing, position = 0) => ({ + message: `${msg} while parsing ${parsing}`, + position, +}) + +const parseError = (e, txt, context = 20) => { + let msg = e.message + if (!txt) { - return { - message: e.message + ' while parsing empty string', - position: 0, - } + return makeParsedError(msg, 'empty string') } - const badToken = e.message.match(/^Unexpected token (.) .*position\s+(\d+)/i) - const errIdx = badToken ? +badToken[2] - : e.message.match(/^Unexpected end of JSON.*/i) ? txt.length - 1 - : null - const msg = badToken ? e.message.replace(/^Unexpected token ./, `Unexpected token ${ - JSON.stringify(badToken[1]) - } (${hexify(badToken[1])})`) - : e.message + const badTokenMatch = msg.match(UNEXPECTED_TOKEN) + const badIndexMatch = msg.match(/ position\s+(\d+)/i) - if (errIdx !== null && errIdx !== undefined) { - const start = errIdx <= context ? 0 - : errIdx - context + if (badTokenMatch) { + msg = msg.replace( + UNEXPECTED_TOKEN, + `Unexpected token ${JSON.stringify(badTokenMatch[1])} (${hexify(badTokenMatch[1])})$2 ` + ) + } - const end = errIdx + context >= txt.length ? txt.length - : errIdx + context + let errIdx + if (badIndexMatch) { + errIdx = +badIndexMatch[1] + } else if (msg.match(/^Unexpected end of JSON.*/i)) { + errIdx = txt.length - 1 + } - const slice = (start === 0 ? '' : '...') + - txt.slice(start, end) + - (end === txt.length ? '' : '...') + if (errIdx == null) { + return makeParsedError(msg, `'${txt.slice(0, context * 2)}'`) + } - const near = txt === slice ? '' : 'near ' + const start = errIdx <= context ? 0 : errIdx - context + const end = errIdx + context >= txt.length ? txt.length : errIdx + context + const slice = `${start ? '...' : ''}${txt.slice(start, end)}${end === txt.length ? '' : '...'}` - return { - message: msg + ` while parsing ${near}${JSON.stringify(slice)}`, - position: errIdx, - } - } else { - return { - message: msg + ` while parsing '${txt.slice(0, context * 2)}'`, - position: 0, - } - } + return makeParsedError( + msg, + `${txt === slice ? '' : 'near '}${JSON.stringify(slice)}`, + errIdx + ) } class JSONParseError extends SyntaxError { constructor (er, txt, context, caller) { - context = context || 20 const metadata = parseError(er, txt, context) super(metadata.message) Object.assign(this, metadata) @@ -63,67 +86,50 @@ class JSONParseError extends SyntaxError { } set name (n) {} + get [Symbol.toStringTag] () { return this.constructor.name } } -const kIndent = Symbol.for('indent') -const kNewline = Symbol.for('newline') -// only respect indentation if we got a line break, otherwise squash it -// things other than objects and arrays aren't indented, so ignore those -// Important: in both of these regexps, the $1 capture group is the newline -// or undefined, and the $2 capture group is the indent, or undefined. -const formatRE = /^\s*[{[]((?:\r?\n)+)([\s\t]*)/ -const emptyRE = /^(?:\{\}|\[\])((?:\r?\n)+)?$/ - -const parseJson = (txt, reviver, context) => { - const parseText = stripBOM(txt) - context = context || 20 - try { +const parseJson = (txt, reviver) => { + const result = JSON.parse(txt, reviver) + if (result && typeof result === 'object') { // get the indentation so that we can save it back nicely // if the file starts with {" then we have an indent of '', ie, none - // otherwise, pick the indentation of the next line after the first \n - // If the pattern doesn't match, then it means no indentation. - // JSON.stringify ignores symbols, so this is reasonably safe. - // if the string is '{}' or '[]', then use the default 2-space indent. - const [, newline = '\n', indent = ' '] = parseText.match(emptyRE) || - parseText.match(formatRE) || - [null, '', ''] - - const result = JSON.parse(parseText, reviver) - if (result && typeof result === 'object') { - result[kNewline] = newline - result[kIndent] = indent - } - return result + // otherwise, pick the indentation of the next line after the first \n If the + // pattern doesn't match, then it means no indentation. JSON.stringify ignores + // symbols, so this is reasonably safe. if the string is '{}' or '[]', then + // use the default 2-space indent. + const match = txt.match(EMPTY) || txt.match(FORMAT) || [null, '', ''] + result[NEWLINE] = match[1] ?? DEFAULT_NEWLINE + result[INDENT] = match[2] ?? DEFAULT_INDENT + } + return result +} + +const parseJsonError = (raw, reviver, context) => { + const txt = stripBOM(raw) + try { + return parseJson(txt, reviver) } catch (e) { - if (typeof txt !== 'string' && !Buffer.isBuffer(txt)) { - const isEmptyArray = Array.isArray(txt) && txt.length === 0 - throw Object.assign(new TypeError( - `Cannot parse ${isEmptyArray ? 'an empty array' : String(txt)}` - ), { - code: 'EJSONPARSE', - systemError: e, - }) + if (typeof raw !== 'string' && !Buffer.isBuffer(raw)) { + const msg = Array.isArray(raw) && raw.length === 0 ? 'an empty array' : String(raw) + throw Object.assign( + new TypeError(`Cannot parse ${msg}`), + { code: 'EJSONPARSE', systemError: e } + ) } - - throw new JSONParseError(e, parseText, context, parseJson) + throw new JSONParseError(e, txt, context, parseJsonError) } } -// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) -// because the buffer-to-string conversion in `fs.readFileSync()` -// translates it to FEFF, the UTF-16 BOM. -const stripBOM = txt => String(txt).replace(/^\uFEFF/, '') - -module.exports = parseJson -parseJson.JSONParseError = JSONParseError - -parseJson.noExceptions = (txt, reviver) => { +module.exports = parseJsonError +parseJsonError.JSONParseError = JSONParseError +parseJsonError.noExceptions = (raw, reviver) => { try { - return JSON.parse(stripBOM(txt), reviver) - } catch (e) { + return parseJson(stripBOM(raw), reviver) + } catch { // no exceptions } } diff --git a/package.json b/package.json index 96aa52f..97bbc42 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "test": "tap", "snap": "tap", - "lint": "eslint \"**/*.js\"", + "lint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"", "postlint": "template-oss-check", "template-oss-apply": "template-oss-apply --force", "lintfix": "npm run lint -- --fix", @@ -28,7 +28,7 @@ "license": "MIT", "devDependencies": { "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.18.1", + "@npmcli/template-oss": "4.20.0", "tap": "^16.3.0" }, "tap": { @@ -43,6 +43,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.18.1" + "version": "4.20.0" } } diff --git a/test/index.js b/test/index.js index 4b1b562..081096c 100644 --- a/test/index.js +++ b/test/index.js @@ -1,10 +1,39 @@ 'use strict' const t = require('tap') - const parseJson = require('..') -t.test('parses JSON', t => { +const currentNodeMajor = +process.version.split('.')[0].slice(1) + +// Given an object where keys are major versions of node, this will return the +// value where the current major version is >= the latest key. eg: in node 24, +// for the input {20:1, 22:2}, this will return 2 if not match is found it will +// return the value of the `default` key. +const getLatestMatchingNode = ({ default: defaultNode, ...majors }) => { + for (const major of Object.keys(majors).sort((a, b) => b - a)) { + if (currentNodeMajor >= major) { + return majors[major] + } + } + return defaultNode +} + +// This will join all args into a regexp that can be used to assert a match. +// Each argument can be a string, regexp or an object passed to getLatestMatchingNode +const expectMessage = (...args) => new RegExp(args.map((rawValue) => { + const value = rawValue.constructor === Object ? getLatestMatchingNode(rawValue) : rawValue + return value instanceof RegExp ? value.source : value +}).join('')) + +const jsonThrows = (t, data, ...args) => { + let context + if (typeof args[0] === 'number') { + context = args.shift() + } + return t.throws(() => parseJson(data, null, context), ...args) +} + +t.test('parses JSON', (t) => { const cases = Object.entries({ object: { foo: 1, @@ -24,7 +53,7 @@ t.test('parses JSON', t => { } }) -t.test('preserves indentation and newline styles', t => { +t.test('preserves indentation and newline styles', (t) => { const kIndent = Symbol.for('indent') const kNewline = Symbol.for('newline') const object = { name: 'object', version: '1.2.3' } @@ -34,7 +63,7 @@ t.test('preserves indentation and newline styles', t => { for (const [type, obj] of Object.entries({ object, array })) { const n = JSON.stringify({ type, newline, indent }) const txt = JSON.stringify(obj, null, indent).replace(/\n/g, newline) - t.test(n, t => { + t.test(n, (t) => { const res = parseJson(txt) // no newline if no indentation t.equal(res[kNewline], indent && newline, 'preserved newline') @@ -47,7 +76,7 @@ t.test('preserves indentation and newline styles', t => { t.end() }) -t.test('indentation is the default when object/array is empty', t => { +t.test('indentation is the default when object/array is empty', (t) => { const kIndent = Symbol.for('indent') const kNewline = Symbol.for('newline') const obj = '{}' @@ -55,7 +84,7 @@ t.test('indentation is the default when object/array is empty', t => { for (const newline of ['', '\n', '\r\n', '\n\n', '\r\n\r\n']) { const expect = newline || '\n' for (const str of [obj, arr]) { - t.test(JSON.stringify({ str, newline, expect }), t => { + t.test(JSON.stringify({ str, newline, expect }), (t) => { const res = parseJson(str + newline) t.equal(res[kNewline], expect, 'got expected newline') t.equal(res[kIndent], ' ', 'got expected default indentation') @@ -66,7 +95,7 @@ t.test('indentation is the default when object/array is empty', t => { t.end() }) -t.test('parses JSON if it is a Buffer, removing BOM bytes', t => { +t.test('parses JSON if it is a Buffer, removing BOM bytes', (t) => { const str = JSON.stringify({ foo: 1, bar: { @@ -74,131 +103,188 @@ t.test('parses JSON if it is a Buffer, removing BOM bytes', t => { }, }) const data = Buffer.from(str) - const bom = Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), data]) + const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), data]) t.same(parseJson(data), JSON.parse(str)) t.same(parseJson(bom), JSON.parse(str), 'strips the byte order marker') t.end() }) -t.test('better errors when faced with \\b and other malarky', t => { +t.test('better errors when faced with \\b and other malarky', (t) => { const str = JSON.stringify({ foo: 1, bar: { baz: [1, 2, 3, 'four'], }, }) - const data = Buffer.from(str) - const bombom = Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF, 0xEF, 0xBB, 0xBF]), data]) - t.throws(() => parseJson(bombom), { - message: /\(0xFEFF\) in JSON at position 0/, - }, 'only strips a single BOM, not multiple') - const bs = str + '\b\b\b\b\b\b\b\b\b\b\b\b' - t.throws(() => parseJson(bs), { - message: /^Unexpected token "\\b" \(0x08\) in JSON at position.*\\b"$/, + const bombom = Buffer.concat([ + Buffer.from([0xef, 0xbb, 0xbf, 0xef, 0xbb, 0xbf]), + Buffer.from(str), + ]) + + jsonThrows( + t, + bombom, + { + message: /Unexpected token "." \(0xFEFF\)/, + }, + 'only strips a single BOM, not multiple' + ) + + jsonThrows(t, str + '\b\b\b\b\b\b\b\b\b\b\b\b', { + message: expectMessage( + 'Unexpected ', + { + 20: 'non-whitespace character after JSON', + default: /token "\\b" \(0x08\) in JSON/, + }, + / at position.*\\b"/ + ), }) + t.end() }) -t.test('throws SyntaxError for unexpected token', t => { +t.test('throws SyntaxError for unexpected token', (t) => { const data = 'foo' - t.throws( - () => parseJson(data), - { - message: 'Unexpected token "o" (0x6F) in JSON at position 1 while parsing "foo"', - code: 'EJSONPARSE', - position: 1, - name: 'JSONParseError', - systemError: SyntaxError, - } - ) + jsonThrows(t, data, { + message: expectMessage( + /Unexpected token "o" \(0x6F\)/, + { + 20: ', "foo" is not valid JSON', + default: ' in JSON at position 1', + }, + / while parsing .foo./ + ), + code: 'EJSONPARSE', + position: getLatestMatchingNode({ 20: 0, default: 1 }), + name: 'JSONParseError', + systemError: SyntaxError, + }) t.end() }) -t.test('throws SyntaxError for unexpected end of JSON', t => { +t.test('throws SyntaxError for unexpected end of JSON', (t) => { const data = '{"foo: bar}' - t.throws( - () => parseJson(data), - { - message: 'Unexpected end of JSON input while parsing "{\\"foo: bar}"', - code: 'EJSONPARSE', - position: 10, - name: 'JSONParseError', - systemError: SyntaxError, - } - ) + jsonThrows(t, data, { + message: expectMessage( + { + 20: /Unterminated string in JSON at position \d+/, + default: /Unexpected end of JSON input/, + }, + / while parsing "{\\"foo: bar}"/ + ), + code: 'EJSONPARSE', + position: getLatestMatchingNode({ 20: 11, default: 10 }), + name: 'JSONParseError', + systemError: SyntaxError, + }) t.end() }) -t.test('throws SyntaxError for unexpected number', t => { +t.test('throws SyntaxError for unexpected number', (t) => { const data = '[[1,2],{3,3,3,3,3}]' - t.throws( - () => parseJson(data), - { - message: 'Unexpected number in JSON at position 8', - code: 'EJSONPARSE', - position: 0, - name: 'JSONParseError', - systemError: SyntaxError, - } - ) + jsonThrows(t, data, { + message: expectMessage( + { + 20: "Expected property name or '}'", + default: 'Unexpected number', + }, + ' in JSON at position 8' + ), + code: 'EJSONPARSE', + position: 8, + name: 'JSONParseError', + systemError: SyntaxError, + }) t.end() }) -t.test('SyntaxError with less context (limited start)', t => { +t.test('SyntaxError with less context (limited start)', (t) => { const data = '{"6543210' - t.throws( - () => parseJson(data, null, 3), - { - message: 'Unexpected end of JSON input while parsing near "...3210"', - code: 'EJSONPARSE', - position: 8, - name: 'JSONParseError', - systemError: SyntaxError, - }) + jsonThrows(t, data, 3, { + message: expectMessage( + { + 20: 'Unterminated string in JSON at position 9', + default: 'Unexpected end of JSON input', + }, + ' while parsing near "...', + { + 20: '210', + default: '3210', + } + ), + code: 'EJSONPARSE', + position: getLatestMatchingNode({ 20: 9, default: 8 }), + name: 'JSONParseError', + systemError: SyntaxError, + }) t.end() }) -t.test('SyntaxError with less context (limited end)', t => { +t.test('SyntaxError with less context (limited end)', (t) => { const data = 'abcde' - t.throws( - () => parseJson(data, null, 2), - { - message: 'Unexpected token "a" (0x61) in JSON at position 0 while parsing near "ab..."', - code: 'EJSONPARSE', - position: 0, - name: 'JSONParseError', - systemError: SyntaxError, - } - ) + jsonThrows(t, data, 2, { + message: expectMessage( + /Unexpected token "a" \(0x61\)/, + { + 20: ', "abcde" is not valid JSON', + default: ' in JSON at position 0', + }, + ' while parsing ', + { + 20: "'abcd'", + default: 'near "ab..."', + } + ), + code: 'EJSONPARSE', + position: 0, + name: 'JSONParseError', + systemError: SyntaxError, + }) t.end() }) -t.test('throws TypeError for undefined', t => { - t.throws( - () => parseJson(undefined), - new TypeError('Cannot parse undefined') - ) +t.test('throws for end of input', (t) => { + const data = '{"a":1,""' + jsonThrows(t, data, 2, { + message: expectMessage('Unexpected end of JSON input while parsing'), + code: 'EJSONPARSE', + position: 8, + name: 'JSONParseError', + systemError: SyntaxError, + }) t.end() }) -t.test('throws TypeError for non-strings', t => { - t.throws( - () => parseJson(new Map()), - new TypeError('Cannot parse [object Map]') +t[currentNodeMajor >= 20 ? 'test' : 'skip']('coverage on node 20', (t) => { + t.match( + new parseJson.JSONParseError( + { message: `Unexpected token \b at position 2` }, + 'a'.repeat(4), + 1 + ).message, + /Unexpected token/ ) t.end() }) -t.test('throws TypeError for empty arrays', t => { - t.throws( - () => parseJson([]), - new TypeError('Cannot parse an empty array') - ) +t.test('throws TypeError for undefined', (t) => { + jsonThrows(t, undefined, new TypeError('Cannot parse undefined')) t.end() }) -t.test('handles empty string helpfully', t => { - t.throws(() => parseJson(''), { +t.test('throws TypeError for non-strings', (t) => { + jsonThrows(t, new Map(), new TypeError('Cannot parse [object Map]')) + t.end() +}) + +t.test('throws TypeError for empty arrays', (t) => { + jsonThrows(t, [], new TypeError('Cannot parse an empty array')) + t.end() +}) + +t.test('handles empty string helpfully', (t) => { + jsonThrows(t, '', { message: 'Unexpected end of JSON input while parsing empty string', name: 'JSONParseError', position: 0, @@ -208,12 +294,19 @@ t.test('handles empty string helpfully', t => { t.end() }) -t.test('json parse error class', t => { +t.test('json parse error class', (t) => { t.type(parseJson.JSONParseError, 'function') + // we already checked all the various index checking logic above const poop = new Error('poop') + const fooShouldNotShowUpInStackTrace = () => { - return new parseJson.JSONParseError(poop, 'this is some json', undefined, bar) + return new parseJson.JSONParseError( + poop, + 'this is some json', + undefined, + bar + ) } const bar = () => fooShouldNotShowUpInStackTrace() const err1 = bar() @@ -224,6 +317,7 @@ t.test('json parse error class', t => { err1.name = 'something else' t.equal(err1.name, 'JSONParseError') t.notMatch(err1.stack, /fooShouldNotShowUpInStackTrace/) + // calling it directly, tho, it does const fooShouldShowUpInStackTrace = () => { return new parseJson.JSONParseError(poop, 'this is some json') @@ -237,7 +331,7 @@ t.test('json parse error class', t => { t.end() }) -t.test('parse without exception', t => { +t.test('parse without exception', (t) => { const bad = 'this is not json' t.equal(parseJson.noExceptions(bad), undefined, 'does not throw') const obj = { this: 'is json' } @@ -245,7 +339,7 @@ t.test('parse without exception', t => { t.same(parseJson.noExceptions(good), obj, 'parses json string') const buf = Buffer.from(good) t.same(parseJson.noExceptions(buf), obj, 'parses json buffer') - const bom = Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), buf]) + const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), buf]) t.same(parseJson.noExceptions(bom), obj, 'parses json buffer with bom') t.end() })