From d69cdb179329709c0ab39e8f028251148562cf62 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Fri, 14 Oct 2022 18:18:51 -0700 Subject: [PATCH 01/11] feat!: add @npmcli/template-oss and modernize BREAKING CHANGE: - callback has been removed from the async interface, it is now `Promise` only - `which` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0 - cli now ignores any arguments after `--` --- .commitlintrc.js | 10 + .eslintrc.js | 17 ++ .github/CODEOWNERS | 3 + .github/ISSUE_TEMPLATE/bug.yml | 54 +++++ .github/ISSUE_TEMPLATE/config.yml | 3 + .github/dependabot.yml | 17 ++ .github/matchers/tap.json | 32 +++ .github/workflows/audit.yml | 39 ++++ .github/workflows/ci-release.yml | 216 +++++++++++++++++++ .github/workflows/ci.yml | 107 +++++++++ .github/workflows/codeql-analysis.yml | 38 ++++ .github/workflows/post-dependabot.yml | 121 +++++++++++ .github/workflows/pull-request.yml | 48 +++++ .github/workflows/release.yml | 299 ++++++++++++++++++++++++++ .gitignore | 32 ++- .npmrc | 3 + .release-please-manifest.json | 3 + .travis.yml | 7 - CHANGELOG.md | 3 +- CODE_OF_CONDUCT.md | 7 + CONTRIBUTING.md | 1 - README.md | 22 +- SECURITY.md | 3 + appveyor.yml | 18 -- bin/node-which | 52 ----- bin/which.js | 52 +++++ gen-changelog.sh | 9 - lib/index.js | 103 +++++++++ lib/is-windows.js | 10 + package.json | 45 ++-- release-please-config.json | 36 ++++ test/basic.js | 233 ++++++++------------ test/bin.js | 189 ++++++++-------- test/windows.js | 2 +- which.js | 125 ----------- 35 files changed, 1461 insertions(+), 498 deletions(-) create mode 100644 .commitlintrc.js create mode 100644 .eslintrc.js create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/matchers/tap.json create mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/ci-release.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/post-dependabot.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 .github/workflows/release.yml create mode 100644 .npmrc create mode 100644 .release-please-manifest.json delete mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md delete mode 100644 appveyor.yml delete mode 100755 bin/node-which create mode 100755 bin/which.js delete mode 100644 gen-changelog.sh create mode 100644 lib/index.js create mode 100644 lib/is-windows.js create mode 100644 release-please-config.json delete mode 100644 which.js diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..5b0b1a5 --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1,10 @@ +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], + 'header-max-length': [2, 'always', 80], + 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']], + }, +} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..5db9f81 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,17 @@ +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +'use strict' + +const { readdirSync: readdir } = require('fs') + +const localConfigs = readdir(__dirname) + .filter((file) => file.startsWith('.eslintrc.local.')) + .map((file) => `./${file}`) + +module.exports = { + root: true, + extends: [ + '@npmcli', + ...localConfigs, + ], +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..2c54b0d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +* @npm/cli-team diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..d043192 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,54 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Bug +description: File a bug/issue +title: "[BUG] " +labels: [ Bug, Needs Triage ] + +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please [search here](./issues) to see if an issue already exists for your problem. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A clear & concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A clear & concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + value: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + examples: + - **npm**: 7.6.3 + - **Node**: 13.14.0 + - **OS**: Ubuntu 20.04 + - **platform**: Macbook Pro + value: | + - npm: + - Node: + - OS: + - platform: + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d640909 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,3 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +blank_issues_enabled: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8da2a45 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +version: 2 + +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: daily + allow: + - dependency-type: direct + versioning-strategy: increase-if-necessary + commit-message: + prefix: deps + prefix-development: chore + labels: + - "Dependencies" diff --git a/.github/matchers/tap.json b/.github/matchers/tap.json new file mode 100644 index 0000000..2c81ea9 --- /dev/null +++ b/.github/matchers/tap.json @@ -0,0 +1,32 @@ +{ + "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", + "problemMatcher": [ + { + "owner": "tap", + "pattern": [ + { + "regexp": "^\\s*not ok \\d+ - (.*)", + "message": 1 + }, + { + "regexp": "^\\s*---" + }, + { + "regexp": "^\\s*at:" + }, + { + "regexp": "^\\s*line:\\s*(\\d+)", + "line": 1 + }, + { + "regexp": "^\\s*column:\\s*(\\d+)", + "column": 1 + }, + { + "regexp": "^\\s*file:\\s*(.*)", + "file": 1 + } + ] + } + ] +} diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..62892f9 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,39 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +on: + workflow_dispatch: + schedule: + # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 + - cron: "0 8 * * 1" + +jobs: + audit: + name: Audit Dependencies + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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 --package-lock + - name: Run Production Audit + run: npm audit --omit=dev + - name: Run Full Audit + run: npm audit --audit-level=none diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml new file mode 100644 index 0000000..6e80aa6 --- /dev/null +++ b/.github/workflows/ci-release.yml @@ -0,0 +1,216 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI - Release + +on: + workflow_dispatch: + inputs: + ref: + required: true + type: string + default: main + workflow_call: + inputs: + ref: + required: true + type: string + check-sha: + required: true + type: string + +jobs: + lint-all: + name: Lint All + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + if: inputs.check-sha + id: check-output + env: + JOB_NAME: "Lint All" + MATRIX_NAME: "" + with: + script: | + const { owner, repo } = context.repo + + const { data } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: context.runId, + per_page: 100 + }) + + const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME + const job = data.jobs.find(j => j.name.endsWith(jobName)) + const jobUrl = job?.html_url + + const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}` + + let summary = `This check is assosciated with ${shaUrl}\n\n` + + if (jobUrl) { + summary += `For run logs, click here: ${jobUrl}` + } else { + summary += `Run logs could not be found for a job with name: "${jobName}"` + } + + return { summary } + - name: Create Check + uses: LouisBrunner/checks-action@v1.3.1 + id: check + if: inputs.check-sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Lint All + sha: ${{ inputs.check-sha }} + output: ${{ steps.check-output.outputs.result }} + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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: Lint + run: npm run lint --ignore-scripts + - name: Post Lint + run: npm run postlint --ignore-scripts + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.3.1 + if: steps.check.outputs.check_id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check_id: ${{ steps.check.outputs.check_id }} + + test-all: + name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + if: github.repository_owner == 'npm' + strategy: + fail-fast: false + matrix: + platform: + - name: Linux + os: ubuntu-latest + shell: bash + - name: macOS + os: macos-latest + shell: bash + - name: Windows + os: windows-latest + shell: cmd + node-version: + - 14.17.0 + - 14.x + - 16.13.0 + - 16.x + - 18.0.0 + - 18.x + runs-on: ${{ matrix.platform.os }} + defaults: + run: + shell: ${{ matrix.platform.shell }} + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + if: inputs.check-sha + id: check-output + env: + JOB_NAME: "Test All" + MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" + with: + script: | + const { owner, repo } = context.repo + + const { data } = await github.rest.actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: context.runId, + per_page: 100 + }) + + const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME + const job = data.jobs.find(j => j.name.endsWith(jobName)) + const jobUrl = job?.html_url + + const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}` + + let summary = `This check is assosciated with ${shaUrl}\n\n` + + if (jobUrl) { + summary += `For run logs, click here: ${jobUrl}` + } else { + summary += `Run logs could not be found for a job with name: "${jobName}"` + } + + return { summary } + - name: Create Check + uses: LouisBrunner/checks-action@v1.3.1 + id: check + if: inputs.check-sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + sha: ${{ inputs.check-sha }} + output: ${{ steps.check-output.outputs.result }} + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Install npm@7 + if: startsWith(matrix.node-version, '10.') + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Install npm@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 + - name: Add Problem Matcher + run: echo "::add-matcher::.github/matchers/tap.json" + - name: Test + run: npm test --ignore-scripts + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.3.1 + if: steps.check.outputs.check_id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check_id: ${{ steps.check.outputs.check_id }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9cc149d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,107 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + - latest + schedule: + # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" + +jobs: + lint: + name: Lint + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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: Lint + run: npm run lint --ignore-scripts + - name: Post Lint + run: npm run postlint --ignore-scripts + + test: + name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} + if: github.repository_owner == 'npm' + strategy: + fail-fast: false + matrix: + platform: + - name: Linux + os: ubuntu-latest + shell: bash + - name: macOS + os: macos-latest + shell: bash + - name: Windows + os: windows-latest + shell: cmd + node-version: + - 14.17.0 + - 14.x + - 16.13.0 + - 16.x + - 18.0.0 + - 18.x + 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 + - name: Add Problem Matcher + run: echo "::add-matcher::.github/matchers/tap.json" + - name: Test + run: npm test --ignore-scripts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..66b9498 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,38 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CodeQL + +on: + push: + branches: + - main + - latest + pull_request: + branches: + - main + - latest + schedule: + # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 + - cron: "0 10 * * 1" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: 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" + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: javascript + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml new file mode 100644 index 0000000..19902bd --- /dev/null +++ b/.github/workflows/post-dependabot.yml @@ -0,0 +1,121 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Post Dependabot + +on: pull_request + +permissions: + contents: write + +jobs: + template-oss: + name: template-oss + if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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: Fetch Dependabot Metadata + id: metadata + uses: dependabot/fetch-metadata@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Dependabot can update multiple directories so we output which directory + # it is acting on so we can run the command for the correct root or workspace + - name: Get Dependabot Directory + if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') + id: flags + run: | + dependabot_dir="${{ steps.metadata.outputs.directory }}" + if [[ "$dependabot_dir" == "/" ]]; then + echo "::set-output name=workspace::-iwr" + 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#/}" + fi + + - name: Apply Changes + if: steps.flags.outputs.workspace + id: apply + run: | + npm run template-oss-apply ${{ steps.flags.outputs.workspace }} + if [[ `git status --porcelain` ]]; then + echo "::set-output name=changes::true" + fi + # This only sets the conventional commit prefix. This workflow can't reliably determine + # what the breaking change is though. If a BREAKING CHANGE message is required then + # this PR check will fail and the commit will be amended with stafftools + if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then + prefix='feat!' + else + prefix='chore' + fi + echo "::set-output name=message::$prefix: postinstall for dependabot template-oss PR" + + # This step will fail if template-oss has made any workflow updates. It is impossible + # for a workflow to update other workflows. In the case it does fail, we continue + # and then try to apply only a portion of the changes in the next step + - name: Push All Changes + if: steps.apply.outputs.changes + id: push + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git commit -am "${{ steps.apply.outputs.message }}" + git push + + # If the previous step failed, then reset the commit and remove any workflow changes + # and attempt to commit and push again. This is helpful because we will have a commit + # with the correct prefix that we can then --amend with @npmcli/stafftools later. + - name: Push All Changes Except Workflows + if: steps.apply.outputs.changes && steps.push.outcome == 'failure' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git reset HEAD~ + git checkout HEAD -- .github/workflows/ + git clean -fd .github/workflows/ + git commit -am "${{ steps.apply.outputs.message }}" + git push + + # Check if all the necessary template-oss changes were applied. Since we continued + # on errors in one of the previous steps, this check will fail if our follow up + # only applied a portion of the changes and we need to followup manually. + # + # Note that this used to run `lint` and `postlint` but that will fail this action + # if we've also shipped any linting changes separate from template-oss. We do + # linting in another action, so we want to fail this one only if there are + # template-oss changes that could not be applied. + - name: Check Changes + if: steps.apply.outputs.changes + run: | + npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check + + - name: Fail on Breaking Change + if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') + run: | + echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" + echo "for more information on how to fix this with a BREAKING CHANGE footer." + exit 1 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..1a1d1ee --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,48 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Pull Request + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + +jobs: + commitlint: + name: Lint Commits + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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 Commitlint on Commits + id: commit + continue-on-error: true + run: | + npx --offline commitlint -V --from origin/${{ github.base_ref }} --to ${{ github.event.pull_request.head.sha }} + - name: Run Commitlint on PR Title + if: steps.commit.outcome == 'failure' + run: | + echo ${{ github.event.pull_request.title }} | npx --offline commitlint -V diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..15d37cb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,299 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release + +on: + workflow_dispatch: + push: + branches: + - main + - latest + - release/v* + +permissions: + contents: write + pull-requests: write + checks: write + +jobs: + release: + outputs: + pr: ${{ steps.release.outputs.pr }} + 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 }} + check-id: ${{ steps.check.outputs.check_id }} + name: Release + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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: Release Please + id: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx --offline template-oss-release-please ${{ github.ref_name }} ${{ github.event_name }} + - name: Post Pull Request Comment + if: steps.release.outputs.pr-number + uses: actions/github-script@v6 + id: pr-comment + env: + PR_NUMBER: ${{ steps.release.outputs.pr-number }} + REF_NAME: ${{ github.ref_name }} + with: + script: | + const { REF_NAME, PR_NUMBER } = process.env + const repo = { owner: context.repo.owner, repo: context.repo.repo } + const issue = { ...repo, issue_number: PR_NUMBER } + + const { data: workflow } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: context.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 + + body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Rerun for 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 rerun, run this command:\n\n` + body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME}\n\`\`\`` + + if (commentId) { + await github.rest.issues.updateComment({ ...repo, comment_id: commentId, body }) + } else { + const { data: comment } = await github.rest.issues.createComment({ ...issue, 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 + id: check + if: steps.release.outputs.pr-sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Release + sha: ${{ steps.release.outputs.pr-sha }} + output: ${{ steps.check-output.outputs.result }} + + update: + needs: release + outputs: + sha: ${{ steps.commit.outputs.sha }} + check-id: ${{ steps.check.outputs.check_id }} + name: Update - Release + if: github.repository_owner == 'npm' && needs.release.outputs.pr + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ needs.release.outputs.branch }} + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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 Pull Request Actions + env: + RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }} + RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npm exec --offline -- template-oss-release-manager + npm run rp-pull-request --ignore-scripts --if-present + - name: Commit + id: commit + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git commit --all --amend --no-edit || true + git push --force-with-lease + echo "::set-output name=sha::$(git rev-parse HEAD)" + - 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 + id: check + if: steps.commit.outputs.sha + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: in_progress + name: Release + sha: ${{ steps.commit.outputs.sha }} + output: ${{ steps.check-output.outputs.result }} + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.3.1 + if: needs.release.outputs.check-id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check_id: ${{ needs.release.outputs.check-id }} + + ci: + name: CI - Release + needs: [ release, update ] + if: needs.release.outputs.pr + uses: ./.github/workflows/ci-release.yml + with: + ref: ${{ needs.release.outputs.branch }} + check-sha: ${{ needs.update.outputs.sha }} + + post-ci: + needs: [ release, update, ci ] + name: Post CI - Release + if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get Needs Result + id: needs-result + run: | + result="" + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + result="failure" + elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + result="cancelled" + else + result="success" + fi + echo "::set-output name=result::$result" + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.3.1 + if: needs.update.outputs.check-id && always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ steps.needs-result.outputs.result }} + check_id: ${{ needs.update.outputs.check-id }} + + post-release: + needs: release + name: Post Release - Release + if: github.repository_owner == 'npm' && needs.release.outputs.releases + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Git User + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install npm@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 + env: + RELEASES: ${{ needs.release.outputs.releases }} + run: | + npm run rp-release --ignore-scripts --if-present ${{ join(fromJSON(needs.release.outputs.release-flags), ' ') }} diff --git a/.gitignore b/.gitignore index 2d8d77e..0ec3c84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,28 @@ -.nyc_output/ -coverage/ -node_modules/ -package-lock.json +# This file is automatically added by @npmcli/template-oss. Do not edit. + +# ignore everything in the root +/* + +# keep these +!**/.gitignore +!/.commitlintrc.js +!/.eslintrc.js +!/.eslintrc.local.* +!/.github/ +!/.gitignore +!/.npmrc +!/.release-please-manifest.json +!/bin/ +!/CHANGELOG* +!/CODE_OF_CONDUCT.md +!/docs/ +!/lib/ +!/LICENSE* +!/map.js +!/package.json +!/README* +!/release-please-config.json +!/scripts/ +!/SECURITY.md +!/tap-snapshots/ +!/test/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..529f93e --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +; This file is automatically added by @npmcli/template-oss. Do not edit. + +package-lock=false diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..47fb725 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "2.0.2" +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a4b0658..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js - -node_js: - - node - - 12 - - 10 - - 8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb1f20..991bf8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,4 @@ -# Changes - +# Changelog ## 2.0.2 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..167043c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ +<!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> + +All interactions in this repo are covered by the [npm Code of +Conduct](https://docs.npmjs.com/policies/conduct) + +The npm cli team may, at its own discretion, moderate, remove, or edit +any interactions such as pull requests, issues, and comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e88ff8f..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -Please consider signing [the neveragain.tech pledge](http://neveragain.tech/) diff --git a/README.md b/README.md index cd83350..a6e86ab 100644 --- a/README.md +++ b/README.md @@ -9,30 +9,24 @@ needed when the PATH changes. ## USAGE ```javascript -var which = require('which') +const which = require('which') // async usage -which('node', function (er, resolvedPath) { - // er is returned if no "node" is found on the PATH - // if it is found, then the absolute path to the exec is returned -}) +// rejects if not found +const resolved = await which('node') -// or promise -which('node').then(resolvedPath => { ... }).catch(er => { ... not found ... }) +// if nothrow option is used, returns null if not found +const resolvedOrNull = await which('node', { nothrow: true }) // sync usage // throws if not found -var resolved = which.sync('node') +const resolved = which.sync('node') // if nothrow option is used, returns null if not found -resolved = which.sync('node', {nothrow: true}) +const resolvedOrNull = which.sync('node', { nothrow: true }) // Pass options to override the PATH and PATHEXT environment vars. -which('node', { path: someOtherPath }, function (er, resolved) { - if (er) - throw er - console.log('found at %j', resolved) -}) +await which('node', { path: someOtherPath, pathExt: somePathExt }) ``` ## CLI USAGE diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a93106d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +<!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> + +Please send vulnerability reports through [hackerone](https://hackerone.com/github). diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index b5fac87..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,18 +0,0 @@ -environment: - matrix: - - nodejs_version: '6' - - nodejs_version: '4' -install: - - ps: Install-Product node $env:nodejs_version - - set CI=true - - npm -g install npm@latest - - set PATH=%APPDATA%\npm;%PATH% - - npm install -matrix: - fast_finish: true -build: off -version: '{build}' -shallow_clone: true -clone_depth: 1 -test_script: - - npm test diff --git a/bin/node-which b/bin/node-which deleted file mode 100755 index 7cee372..0000000 --- a/bin/node-which +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env node -var which = require("../") -if (process.argv.length < 3) - usage() - -function usage () { - console.error('usage: which [-as] program ...') - process.exit(1) -} - -var all = false -var silent = false -var dashdash = false -var args = process.argv.slice(2).filter(function (arg) { - if (dashdash || !/^-/.test(arg)) - return true - - if (arg === '--') { - dashdash = true - return false - } - - var flags = arg.substr(1).split('') - for (var f = 0; f < flags.length; f++) { - var flag = flags[f] - switch (flag) { - case 's': - silent = true - break - case 'a': - all = true - break - default: - console.error('which: illegal option -- ' + flag) - usage() - } - } - return false -}) - -process.exit(args.reduce(function (pv, current) { - try { - var f = which.sync(current, { all: all }) - if (all) - f = f.join('\n') - if (!silent) - console.log(f) - return pv; - } catch (e) { - return 1; - } -}, 0)) diff --git a/bin/which.js b/bin/which.js new file mode 100755 index 0000000..ff01813 --- /dev/null +++ b/bin/which.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +const which = require('../lib') +const argv = process.argv.slice(2) + +const usage = (err) => { + if (err) { + console.error(`which: ${err}`) + } + console.error('usage: which [-as] program ...') + process.exit(1) +} + +if (!argv.length) { + return usage() +} + +let dashdash = false +const [cmds, flags] = argv.reduce((acc, arg) => { + if (dashdash || arg === '--') { + dashdash = true + return acc + } + + if (!/^-/.test(arg)) { + acc[0].push(arg) + return acc + } + + for (const flag of arg.slice(1).split('')) { + if (flag === 's') { + acc[1].silent = true + } else if (flag === 'a') { + acc[1].all = true + } else { + usage(`illegal option -- ${flag}`) + } + } + + return acc +}, [[], {}]) + +for (const cmd of cmds) { + try { + const res = which.sync(cmd, { all: flags.all }) + if (!flags.silent) { + console.log([].concat(res).join('\n')) + } + } catch (err) { + process.exitCode = 1 + } +} diff --git a/gen-changelog.sh b/gen-changelog.sh deleted file mode 100644 index 360e54a..0000000 --- a/gen-changelog.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -( - echo '# Changes' - echo '' - git log --first-parent --pretty=format:'%s' \ - | grep -v '^update changelog' \ - | perl -p -e 's/^((v?[0-9]+\.?)+)$/\n## \1\n/g' \ - | perl -p -e 's/^([^#\s].*)$/* \1/g' -)> CHANGELOG.md diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..1459bc5 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,103 @@ +const isexe = require('isexe') +const { join } = require('path') +const { isWindows, delimiter } = require('./is-windows.js') + +const getNotFoundError = (cmd) => + Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) + +const rSlash = new RegExp(`[${isWindows ? '\\\\' : ''}/]`) +const rRel = new RegExp(`^\\.[${isWindows ? '\\\\' : ''}/]`) + +const getPathInfo = (cmd, { + delimiter: optDelimiter = delimiter, + path: optPath = process.env.PATH, + pathExt: optPathExt = process.env.PATHEXT, +}) => { + // If it has a slash, then we don't bother searching the pathenv. + // just check the file itself, and that's it. + const pathEnv = cmd.match(rSlash) ? [''] : [ + // windows always checks the cwd first + ...(isWindows ? [process.cwd()] : []), + ...(optPath || /* istanbul ignore next: very unusual */ '').split(optDelimiter), + ] + + if (isWindows) { + const pathExtExe = optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(optDelimiter) + const pathExt = pathExtExe.split(optDelimiter) + if (cmd.includes('.') && pathExt[0] !== '') { + pathExt.unshift('') + } + return { pathEnv, pathExt, pathExtExe } + } + + return { pathEnv, pathExt: [''] } +} + +const getPathPart = (raw, cmd) => { + const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw + const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : '' + return prefix + join(pathPart, cmd) +} + +const which = async (cmd, opt = {}) => { + const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) + const found = [] + + for (const envPart of pathEnv) { + const p = getPathPart(envPart, cmd) + + for (const ext of pathExt) { + const withExt = p + ext + const is = await isexe(withExt, { pathExt: pathExtExe, ignoreErrors: true }) + if (is) { + if (!opt.all) { + return withExt + } + found.push(withExt) + } + } + } + + if (opt.all && found.length) { + return found + } + + if (opt.nothrow) { + return null + } + + throw getNotFoundError(cmd) +} + +const whichSync = (cmd, opt = {}) => { + const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) + const found = [] + + for (const pathEnvPart of pathEnv) { + const p = getPathPart(pathEnvPart, cmd) + + for (const ext of pathExt) { + const withExt = p + ext + const is = isexe.sync(withExt, { pathExt: pathExtExe, ignoreErrors: true }) + if (is) { + if (!opt.all) { + return withExt + } + found.push(withExt) + } + } + } + + if (opt.all && found.length) { + return found + } + + if (opt.nothrow) { + return null + } + + throw getNotFoundError(cmd) +} + +module.exports = which +which.sync = whichSync diff --git a/lib/is-windows.js b/lib/is-windows.js new file mode 100644 index 0000000..af91eba --- /dev/null +++ b/lib/is-windows.js @@ -0,0 +1,10 @@ +const { win32, posix } = require('path') + +const isWindows = process.platform === 'win32' || + process.env.OSTYPE === 'cygwin' || + process.env.OSTYPE === 'msys' + +module.exports = { + isWindows, + delimiter: isWindows ? win32.delimiter : posix.delimiter, +} diff --git a/package.json b/package.json index dcbf044..079ae52 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,52 @@ { - "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", + "author": "GitHub Inc.", "name": "which", "description": "Like which(1) unix command. Find the first instance of an executable in the PATH.", "version": "2.0.2", "repository": { "type": "git", - "url": "git://github.com/isaacs/node-which.git" + "url": "https://github.com/npm/node-which.git" }, - "main": "which.js", + "main": "lib/index.js", "bin": { - "node-which": "./bin/node-which" + "node-which": "./bin/which.js" }, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "devDependencies": { - "mkdirp": "^0.5.0", - "rimraf": "^2.6.2", - "tap": "^16.0.1" + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.8.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2", + "tap": "^16.3.0" }, "scripts": { "test": "tap", - "preversion": "npm test", - "postversion": "npm publish", - "prepublishOnly": "npm run changelog", - "prechangelog": "bash gen-changelog.sh", - "changelog": "git add CHANGELOG.md", - "postchangelog": "git commit -m 'update changelog - '${npm_package_version}", - "postpublish": "git push origin --follow-tags" + "lint": "eslint \"**/*.js\"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "snap": "tap", + "posttest": "npm run lint" }, "files": [ - "which.js", - "bin/node-which" + "bin/", + "lib/" ], "tap": { - "check-coverage": true + "check-coverage": true, + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] }, "engines": { - "node": ">= 8" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "4.8.0" } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..73d1e35 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,36 @@ +{ + "exclude-packages-from-root": true, + "group-pull-request-title-pattern": "chore: release ${version}", + "pull-request-title-pattern": "chore: release${component} ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "docs", + "section": "Documentation", + "hidden": false + }, + { + "type": "deps", + "section": "Dependencies", + "hidden": false + }, + { + "type": "chore", + "hidden": true + } + ], + "packages": { + ".": { + "package-name": "" + } + } +} diff --git a/test/basic.js b/test/basic.js index c656fa5..d121e63 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,193 +1,128 @@ -var t = require('tap') -var fs = require('fs') -var rimraf = require('rimraf') -var mkdirp = require('mkdirp') -const fixdir = `'/fixture-${(+process.env.TAP_CHILD_ID || 0)}` -var fixture = `${__dirname}/${fixdir}` -var which = require('../which.js') -var path = require('path') - -var isWindows = process.platform === 'win32' || - process.env.OSTYPE === 'cygwin' || - process.env.OSTYPE === 'msys' - -var skip = { skip: isWindows ? 'not relevant on windows' : false } - -t.test('setup', function (t) { + +const t = require('tap') +const fs = require('fs') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const { basename, join, relative, sep } = require('path') +const which = require('..') +const { isWindows, delimiter } = require('../lib/is-windows.js') + +const fixdir = `fixture-${(+process.env.TAP_CHILD_ID || 0)}` +const fixture = join(__dirname, fixdir) +const foo = join(fixture, 'foo.sh') + +const skip = { skip: isWindows ? 'not relevant on windows' : false } + +t.before(() => { rimraf.sync(fixture) mkdirp.sync(fixture) - fs.writeFileSync(fixture + '/foo.sh', 'echo foo\n') - t.end() + fs.writeFileSync(foo, 'echo foo\n') }) -t.test('does not find missed', function(t) { - t.plan(3) - - t.rejects(which(fixture + '/foobar.sh'), { - code: 'ENOENT', - }) +t.teardown(() => { + rimraf.sync(fixture) +}) - t.throws(function () { - which.sync(fixture + '/foobar.sh') - }, {code: 'ENOENT'}) +t.test('does not find missed', async (t) => { + const p = join(fixture, 'foobar.sh') + await t.rejects(() => which(p), { code: 'ENOENT' }) + t.equal(await which(p, { nothrow: true }), null) - t.equal(which.sync(fixture + '/foobar.sh', {nothrow:true}), null) + t.throws(() => which.sync(p), { code: 'ENOENT' }) + t.equal(which.sync(p, { nothrow: true }), null) }) -t.test('does not find non-executable', skip, function (t) { - t.plan(2) - - t.test('absolute', function (t) { - t.plan(3) - which(fixture + '/foo.sh', function (er) { - t.type(er, Error) - t.equal(er.code, 'ENOENT') - }) +t.test('does not find non-executable', skip, async (t) => { + t.test('absolute', async (t) => { + await t.rejects(() => which(foo), { code: 'ENOENT' }) + t.throws(() => which.sync(foo), { code: 'ENOENT' }) + }) - t.throws(function () { - which.sync(fixture + '/foo.sh') - }, {code: 'ENOENT'}) + t.test('with path', async (t) => { + await t.rejects(() => which(basename(foo), { path: fixture }), { code: 'ENOENT' }) + t.throws(() => which.sync(basename(foo), { path: fixture }), { code: 'ENOENT' }) }) +}) - t.test('with path', function (t) { - t.plan(3) - which('foo.sh', { path: fixture }, function (er) { - t.type(er, Error) - t.equal(er.code, 'ENOENT') - }) +t.test('find when executable', async (t) => { + t.before(() => fs.chmodSync(foo, '0755')) - t.throws(function () { - which.sync('foo.sh', { path: fixture }) - }, {code: 'ENOENT'}) + const PATH = process.env.PATH + const PATHEXT = process.env.PATHEXT + t.afterEach(() => { + process.env.PATH = PATH + process.env.PATHEXT = PATHEXT }) -}) -t.test('make executable', function (t) { - fs.chmodSync(fixture + '/foo.sh', '0755') - t.end() -}) + const runTest = async (exec, expect, t, opt = {}) => { + opt.pathExt = '.sh' + const found = which.sync(exec, opt).toLowerCase() + t.equal(found, expect.toLowerCase()) -t.test('find when executable', function (t) { - var opt = { pathExt: '.sh' } - var expect = path.resolve(fixture, 'foo.sh').toLowerCase() - var PATH = process.env.PATH + const res = await which(exec, opt) + t.equal(res.toLowerCase(), expect.toLowerCase()) + } - t.test('absolute', function (t) { - runTest(fixture + '/foo.sh', t) + t.test('absolute', async (t) => { + return runTest(foo, foo, t) }) - t.test('with process.env.PATH', function (t) { + t.test('with process.env.PATH', async (t) => { process.env.PATH = fixture - runTest('foo.sh', t) + return runTest(basename(foo), foo, t) }) - t.test('with pathExt', { - skip: isWindows ? false : 'Only for Windows' - }, function (t) { - var pe = process.env.PATHEXT - process.env.PATHEXT = '.SH' - process.env.PATH = fixture - - t.test('foo.sh', function (t) { + t.test('with pathExt', { skip: isWindows ? false : 'Only for Windows' }, async (t) => { + t.test('foo.sh', async (t) => { + process.env.PATHEXT = '.SH' process.env.PATH = fixture - runTest('foo.sh', t) + return runTest(basename(foo), foo, t) }) - t.test('foo', function (t) { + + t.test('foo', async (t) => { + process.env.PATHEXT = '.SH' process.env.PATH = fixture - runTest('foo', t) - }) - t.test('replace', function (t) { - process.env.PATHEXT = pe - t.end() + return runTest(basename(foo, '.sh'), foo, t) }) - t.end() }) - t.test('with path opt', function (t) { - opt.path = fixture - runTest('foo.sh', t) + t.test('with path opt', async (t) => { + return runTest(basename(foo), foo, t, { path: fixture }) }) - t.test('relative path', function (t) { - var opt = { pathExt: '.sh' } - var expect = path.join(`test/${fixdir}/foo.sh`) - t.plan(3) - - t.test('no ./', function (t) { - t.plan(2) - var actual = which.sync(`test/${fixdir}/foo.sh`, opt) - t.equal(actual, expect) - which(`test/${fixdir}/foo.sh`, opt, function (er, actual) { - if (er) - throw er - t.equal(actual, expect) - }) - }) - - t.test('with ./', function (t) { - t.plan(2) - expect = './' + expect - var actual = which.sync(`./test/${fixdir}/foo.sh`, opt) - t.equal(actual, expect) - which(`./test/${fixdir}/foo.sh`, opt, function (er, actual) { - if (er) - throw er - t.equal(actual, expect) - }) - }) - - t.test('with ../', function (t) { - t.plan(2) - var dir = path.basename(process.cwd()) - expect = path.join('..', dir, `test/${fixdir}/foo.sh`) - var actual = which.sync(expect, opt) - t.equal(actual, expect) - which(expect, opt, function (er, actual) { - if (er) - throw er - t.equal(actual, expect) - }) - }) + t.test('no ./', async (t) => { + const rel = relative(process.cwd(), foo) + return runTest(rel, rel, t) }) - function runTest(exec, t) { - t.plan(2) - - var found = which.sync(exec, opt).toLowerCase() - t.equal(found, expect) - - which(exec, opt, function (er, found) { - if (er) - throw er - t.equal(found.toLowerCase(), expect) - t.end() - process.env.PATH = PATH - }) - } + t.test('with ./', async (t) => { + const rel = `.${sep}${relative(process.cwd(), foo)}` + return runTest(rel, rel, t) + }) - t.end() + t.test('with ../', async (t) => { + const dir = basename(process.cwd()) + const rel = join('..', dir, relative(process.cwd(), foo)) + return runTest(rel, rel, t) + }) }) -t.test('find all', t => { +t.test('find all', async t => { mkdirp.sync(`${fixture}/all/a`) mkdirp.sync(`${fixture}/all/b`) fs.writeFileSync(`${fixture}/all/a/x.cmd`, 'exec me') fs.writeFileSync(`${fixture}/all/b/x.cmd`, 'exec me') fs.chmodSync(`${fixture}/all/a/x.cmd`, 0o755) fs.chmodSync(`${fixture}/all/b/x.cmd`, 0o755) + const opt = { - path: `${fixture}/all/a:"${fixture}/all/b"`, - colon: ':', + path: [`${fixture}/all/a`, `"${fixture}/all/b"`].join(delimiter), all: true, } - const allsync = which.sync('x.cmd', opt) - t.same(allsync, [`${fixture}/all/a/x.cmd`, `${fixture}/all/b/x.cmd`]) - return which('x.cmd', opt).then(all => { - t.same(all, [`${fixture}/all/a/x.cmd`, `${fixture}/all/b/x.cmd`]) - }) -}) - -t.test('clean', function (t) { - rimraf.sync(fixture) - t.end() + const expect = [ + join(fixture, 'all', 'a', 'x.cmd'), + join(fixture, 'all', 'b', 'x.cmd'), + ] + t.same(which.sync('x.cmd', opt), expect) + t.same(await which('x.cmd', opt), expect) }) diff --git a/test/bin.js b/test/bin.js index fccd24c..b8b9a34 100644 --- a/test/bin.js +++ b/test/bin.js @@ -1,125 +1,118 @@ -var t = require('tap') -var spawn = require('child_process').spawn -var node = process.execPath -var bin = require.resolve('../bin/node-which') +const t = require('tap') +const spawn = require('child_process').spawn -function which (args, extraPath, cb) { - if (typeof extraPath === 'function') - cb = extraPath, extraPath = null +const node = process.execPath +const bin = require.resolve('../bin/which.js') + +function which (args, extraPath) { + const options = {} - var options = {} if (extraPath) { - var sep = process.platform === 'win32' ? ';' : ':' - var p = process.env.PATH + sep + extraPath + const sep = process.platform === 'win32' ? ';' : ':' + const p = process.env.PATH + sep + extraPath options.env = Object.keys(process.env).reduce(function (env, k) { - if (!k.match(/^path$/i)) + if (!k.match(/^path$/i)) { env[k] = process.env[k] + } return env }, { PATH: p }) } - var out = '' - var err = '' - var child = spawn(node, [bin].concat(args), options) - child.stdout.on('data', function (c) { - out += c - }) - child.stderr.on('data', function (c) { - err += c - }) - child.on('close', function (code, signal) { - cb(code, signal, out.trim(), err.trim()) + return new Promise((res) => { + let out = '' + let err = '' + const child = spawn(node, [bin].concat(args).filter(Boolean), options) + child.stdout.on('data', (c) => out += c) + child.stderr.on('data', (c) => err += c) + child.on('close', (code, signal) => { + out = out.trim() + err = err.trim() + res({ code, signal, out, err }) + }) }) } -t.test('finds node', function (t) { - which('node', function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 0) - t.equal(err, '') - t.match(out, /[\\\/]node(\.exe)?$/i) - t.end() - }) +t.test('finds node', async (t) => { + const { code, signal, out, err } = await which('node') + t.equal(signal, null) + t.equal(code, 0) + t.equal(err, '') + t.match(out, /[\\/]node(\.exe)?$/i) }) -t.test('does not find flergyderp', function (t) { - which('flergyderp', function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 1) - t.equal(err, '') - t.match(out, '') - t.end() - }) +t.test('does not find flergyderp', async (t) => { + const { code, signal, out, err } = await which('flergyderp') + t.equal(signal, null) + t.equal(code, 1) + t.equal(err, '') + t.match(out, '') }) -t.test('finds node and tap', function (t) { - which(['node', 'tap'], function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 0) - t.equal(err, '') - t.match(out.split(/[\r\n]+/), [ - /[\\\/]node(\.exe)?$/i, - /[\\\/]tap(\.cmd)?$/i - ]) - t.end() - }) +t.test('finds node and tap', async (t) => { + const { code, signal, out, err } = await which(['node', 'tap']) + t.equal(signal, null) + t.equal(code, 0) + t.equal(err, '') + t.match(out.split(/[\r\n]+/), [ + /[\\/]node(\.exe)?$/i, + /[\\/]tap(\.cmd)?$/i, + ]) }) -t.test('finds node and tap, but not flergyderp', function (t) { - which(['node', 'flergyderp', 'tap'], function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 1) - t.equal(err, '') - t.match(out.split(/[\r\n]+/), [ - /[\\\/]node(\.exe)?$/i, - /[\\\/]tap(\.cmd)?$/i - ]) - t.end() - }) +t.test('finds node and tap, but not flergyderp', async (t) => { + const { code, signal, out, err } = await which(['node', 'flergyderp', 'tap']) + t.equal(signal, null) + t.equal(code, 1) + t.equal(err, '') + t.match(out.split(/[\r\n]+/), [ + /[\\/]node(\.exe)?$/i, + /[\\/]tap(\.cmd)?$/i, + ]) }) -t.test('cli flags', function (t) { - var p = require('path').dirname(bin) - var cases = [ '-a', '-s', '-as', '-sa' ] - t.plan(cases.length) - cases.forEach(function (c) { - t.test(c, function (t) { - which(['which', c], p, function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 0) - t.equal(err, '') - if (/s/.test(c)) - t.equal(out, '', 'should be silent') - else if (/a/.test(c)) { - out = out.split(/[\r\n]+/) - var opt = { actual: out } - if (process.platform === 'win32') { - opt.skip = 'windows does not have builtin "which"' - } - t.ok(out.length > 0, 'should have a result', opt) +t.test('cli flags', async (t) => { + const p = require('path').dirname(bin) + + for (const c of ['-a', '-s', '-as', '-sa']) { + t.test(c, async (t) => { + let { code, signal, out, err } = await which(['which', c], p) + t.equal(signal, null) + t.equal(code, 0) + t.equal(err, '') + if (/s/.test(c)) { + t.equal(out, '', 'should be silent') + } else if (/a/.test(c)) { + out = out.split(/[\r\n]+/) + const opt = { actual: out } + if (process.platform === 'win32') { + opt.skip = 'windows does not have builtin "which"' } - t.end() - }) + t.ok(out.length > 0, 'should have a result', opt) + } }) - }) + } }) -t.test('shows usage', function (t) { - which([], function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 1) - t.equal(err, 'usage: which [-as] program ...') - t.equal(out, '') - t.end() - }) +t.test('shows usage', async (t) => { + const { code, signal, out, err } = await which() + t.equal(signal, null) + t.equal(code, 1) + t.equal(err, 'usage: which [-as] program ...') + t.equal(out, '') }) -t.test('complains about unknown flag', function (t) { - which(['node', '-sax'], function (code, signal, out, err) { - t.equal(signal, null) - t.equal(code, 1) - t.equal(out, '') - t.equal(err, 'which: illegal option -- x\nusage: which [-as] program ...') - t.end() - }) +t.test('complains about unknown flag', async (t) => { + const { code, signal, out, err } = await which(['node', '-sax']) + t.equal(signal, null) + t.equal(code, 1) + t.equal(out, '') + t.equal(err, 'which: illegal option -- x\nusage: which [-as] program ...') +}) + +t.test('anything after -- is ignored', async (t) => { + const { code, signal, out, err } = await which(['node', '--', '--anything-goes-here']) + t.equal(signal, null) + t.equal(code, 0) + t.equal(err, '') + t.match(out, /[\\/]node(\.exe)?$/i) }) diff --git a/test/windows.js b/test/windows.js index 1d5e429..01695b1 100644 --- a/test/windows.js +++ b/test/windows.js @@ -1,6 +1,6 @@ // pretend to be Windows. if (process.platform === 'win32') { - var t = require('tap') + const t = require('tap') t.plan(0, 'already on windows') process.exit(0) } diff --git a/which.js b/which.js deleted file mode 100644 index 82afffd..0000000 --- a/which.js +++ /dev/null @@ -1,125 +0,0 @@ -const isWindows = process.platform === 'win32' || - process.env.OSTYPE === 'cygwin' || - process.env.OSTYPE === 'msys' - -const path = require('path') -const COLON = isWindows ? ';' : ':' -const isexe = require('isexe') - -const getNotFoundError = (cmd) => - Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) - -const getPathInfo = (cmd, opt) => { - const colon = opt.colon || COLON - - // If it has a slash, then we don't bother searching the pathenv. - // just check the file itself, and that's it. - const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? [''] - : ( - [ - // windows always checks the cwd first - ...(isWindows ? [process.cwd()] : []), - ...(opt.path || process.env.PATH || - /* istanbul ignore next: very unusual */ '').split(colon), - ] - ) - const pathExtExe = isWindows - ? opt.pathExt || process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM' - : '' - const pathExt = isWindows ? pathExtExe.split(colon) : [''] - - if (isWindows) { - if (cmd.indexOf('.') !== -1 && pathExt[0] !== '') - pathExt.unshift('') - } - - return { - pathEnv, - pathExt, - pathExtExe, - } -} - -const which = (cmd, opt, cb) => { - if (typeof opt === 'function') { - cb = opt - opt = {} - } - if (!opt) - opt = {} - - const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) - const found = [] - - const step = i => new Promise((resolve, reject) => { - if (i === pathEnv.length) - return opt.all && found.length ? resolve(found) - : reject(getNotFoundError(cmd)) - - const ppRaw = pathEnv[i] - const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw - - const pCmd = path.join(pathPart, cmd) - const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd - : pCmd - - resolve(subStep(p, i, 0)) - }) - - const subStep = (p, i, ii) => new Promise((resolve, reject) => { - if (ii === pathExt.length) - return resolve(step(i + 1)) - const ext = pathExt[ii] - isexe(p + ext, { pathExt: pathExtExe }, (er, is) => { - if (!er && is) { - if (opt.all) - found.push(p + ext) - else - return resolve(p + ext) - } - return resolve(subStep(p, i, ii + 1)) - }) - }) - - return cb ? step(0).then(res => cb(null, res), cb) : step(0) -} - -const whichSync = (cmd, opt) => { - opt = opt || {} - - const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) - const found = [] - - for (let i = 0; i < pathEnv.length; i ++) { - const ppRaw = pathEnv[i] - const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw - - const pCmd = path.join(pathPart, cmd) - const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd - : pCmd - - for (let j = 0; j < pathExt.length; j ++) { - const cur = p + pathExt[j] - try { - const is = isexe.sync(cur, { pathExt: pathExtExe }) - if (is) { - if (opt.all) - found.push(cur) - else - return cur - } - } catch (ex) {} - } - } - - if (opt.all && found.length) - return found - - if (opt.nothrow) - return null - - throw getNotFoundError(cmd) -} - -module.exports = which -which.sync = whichSync From 35d2105c9c7e850086f139cf0b16b37773701799 Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Sun, 30 Oct 2022 13:14:40 -0700 Subject: [PATCH 02/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 19 ++++++++++------- lib/is-windows.js | 10 --------- test/basic.js | 54 ++++++++++++++++++++++++++++++++++------------- test/windows.js | 10 --------- 4 files changed, 50 insertions(+), 43 deletions(-) delete mode 100644 lib/is-windows.js delete mode 100644 test/windows.js diff --git a/lib/index.js b/lib/index.js index 1459bc5..39a734b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,15 +1,18 @@ const isexe = require('isexe') -const { join } = require('path') -const { isWindows, delimiter } = require('./is-windows.js') +const { join, posix, win32 } = require('path') -const getNotFoundError = (cmd) => - Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) +const isWindows = process.platform === 'win32' || + process.env.OSTYPE === 'cygwin' || + process.env.OSTYPE === 'msys' +const delimiter = isWindows ? win32.delimiter : posix.delimiter const rSlash = new RegExp(`[${isWindows ? '\\\\' : ''}/]`) const rRel = new RegExp(`^\\.[${isWindows ? '\\\\' : ''}/]`) +const getNotFoundError = (cmd) => + Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) + const getPathInfo = (cmd, { - delimiter: optDelimiter = delimiter, path: optPath = process.env.PATH, pathExt: optPathExt = process.env.PATHEXT, }) => { @@ -18,12 +21,12 @@ const getPathInfo = (cmd, { const pathEnv = cmd.match(rSlash) ? [''] : [ // windows always checks the cwd first ...(isWindows ? [process.cwd()] : []), - ...(optPath || /* istanbul ignore next: very unusual */ '').split(optDelimiter), + ...(optPath || /* istanbul ignore next: very unusual */ '').split(delimiter), ] if (isWindows) { - const pathExtExe = optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(optDelimiter) - const pathExt = pathExtExe.split(optDelimiter) + const pathExtExe = optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(delimiter) + const pathExt = pathExtExe.split(delimiter) if (cmd.includes('.') && pathExt[0] !== '') { pathExt.unshift('') } diff --git a/lib/is-windows.js b/lib/is-windows.js deleted file mode 100644 index af91eba..0000000 --- a/lib/is-windows.js +++ /dev/null @@ -1,10 +0,0 @@ -const { win32, posix } = require('path') - -const isWindows = process.platform === 'win32' || - process.env.OSTYPE === 'cygwin' || - process.env.OSTYPE === 'msys' - -module.exports = { - isWindows, - delimiter: isWindows ? win32.delimiter : posix.delimiter, -} diff --git a/test/basic.js b/test/basic.js index d121e63..a42d215 100644 --- a/test/basic.js +++ b/test/basic.js @@ -3,16 +3,13 @@ const t = require('tap') const fs = require('fs') const rimraf = require('rimraf') const mkdirp = require('mkdirp') -const { basename, join, relative, sep } = require('path') +const { basename, join, relative, sep, delimiter } = require('path') const which = require('..') -const { isWindows, delimiter } = require('../lib/is-windows.js') const fixdir = `fixture-${(+process.env.TAP_CHILD_ID || 0)}` const fixture = join(__dirname, fixdir) const foo = join(fixture, 'foo.sh') -const skip = { skip: isWindows ? 'not relevant on windows' : false } - t.before(() => { rimraf.sync(fixture) mkdirp.sync(fixture) @@ -32,7 +29,7 @@ t.test('does not find missed', async (t) => { t.equal(which.sync(p, { nothrow: true }), null) }) -t.test('does not find non-executable', skip, async (t) => { +t.test('does not find non-executable', async (t) => { t.test('absolute', async (t) => { await t.rejects(() => which(foo), { code: 'ENOENT' }) t.throws(() => which.sync(foo), { code: 'ENOENT' }) @@ -47,20 +44,39 @@ t.test('does not find non-executable', skip, async (t) => { t.test('find when executable', async (t) => { t.before(() => fs.chmodSync(foo, '0755')) - const PATH = process.env.PATH - const PATHEXT = process.env.PATHEXT + const { PATH, PATHEXT, OSTYPE } = process.env t.afterEach(() => { - process.env.PATH = PATH - process.env.PATHEXT = PATHEXT + if (PATH) { + process.env.PATH = PATH + } else { + delete process.env.PATH + } + if (PATHEXT) { + process.env.PATHEXT = PATHEXT + } else { + delete process.env.PATHEXT + } + if (OSTYPE) { + process.env.OSTYPE = OSTYPE + } else { + delete process.env.OSTYPE + } }) const runTest = async (exec, expect, t, opt = {}) => { opt.pathExt = '.sh' - const found = which.sync(exec, opt).toLowerCase() - t.equal(found, expect.toLowerCase()) - - const res = await which(exec, opt) - t.equal(res.toLowerCase(), expect.toLowerCase()) + const _which = t.mock('..') + + if (typeof expect === 'string') { + const found = _which.sync(exec, opt).toLowerCase() + t.equal(found, expect.toLowerCase()) + + const res = await _which(exec, opt) + t.equal(res.toLowerCase(), expect.toLowerCase()) + } else { + await t.rejects(() => _which(exec), expect) + t.throws(() => _which.sync(exec), expect) + } } t.test('absolute', async (t) => { @@ -72,18 +88,26 @@ t.test('find when executable', async (t) => { return runTest(basename(foo), foo, t) }) - t.test('with pathExt', { skip: isWindows ? false : 'Only for Windows' }, async (t) => { + t.test('pathExt', async (t) => { t.test('foo.sh', async (t) => { + process.env.OSTYPE = 'cygwin' process.env.PATHEXT = '.SH' process.env.PATH = fixture return runTest(basename(foo), foo, t) }) t.test('foo', async (t) => { + process.env.OSTYPE = 'cygwin' process.env.PATHEXT = '.SH' process.env.PATH = fixture return runTest(basename(foo, '.sh'), foo, t) }) + + t.test('foo nopathext', async (t) => { + process.env.OSTYPE = 'cygwin' + process.env.PATH = fixture + return runTest(basename(foo, '.sh'), { code: 'ENOENT' }, t) + }) }) t.test('with path opt', async (t) => { diff --git a/test/windows.js b/test/windows.js deleted file mode 100644 index 01695b1..0000000 --- a/test/windows.js +++ /dev/null @@ -1,10 +0,0 @@ -// pretend to be Windows. -if (process.platform === 'win32') { - const t = require('tap') - t.plan(0, 'already on windows') - process.exit(0) -} - -process.env.Path = process.env.PATH.split(':').join(';') -process.env.OSTYPE = 'cygwin' -require('./basic.js') From 20328ea7f79f5f0f058c16cf553f05f1cecf6cee Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Sun, 30 Oct 2022 14:17:18 -0700 Subject: [PATCH 03/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 5 +---- test/basic.js | 28 ++++++++++++---------------- test/windows.js | 10 ++++++++++ 3 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 test/windows.js diff --git a/lib/index.js b/lib/index.js index 39a734b..1b754fa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,10 +1,7 @@ const isexe = require('isexe') const { join, posix, win32 } = require('path') -const isWindows = process.platform === 'win32' || - process.env.OSTYPE === 'cygwin' || - process.env.OSTYPE === 'msys' - +const isWindows = process.env.WHICH_FAKE_PLATFORM || process.platform === 'win32' const delimiter = isWindows ? win32.delimiter : posix.delimiter const rSlash = new RegExp(`[${isWindows ? '\\\\' : ''}/]`) const rRel = new RegExp(`^\\.[${isWindows ? '\\\\' : ''}/]`) diff --git a/test/basic.js b/test/basic.js index a42d215..8707ddb 100644 --- a/test/basic.js +++ b/test/basic.js @@ -4,12 +4,14 @@ const fs = require('fs') const rimraf = require('rimraf') const mkdirp = require('mkdirp') const { basename, join, relative, sep, delimiter } = require('path') -const which = require('..') const fixdir = `fixture-${(+process.env.TAP_CHILD_ID || 0)}` const fixture = join(__dirname, fixdir) const foo = join(fixture, 'foo.sh') +const which = (...args) => t.mock('..')(...args) +which.sync = (...args) => t.mock('..').sync(...args) + t.before(() => { rimraf.sync(fixture) mkdirp.sync(fixture) @@ -44,7 +46,7 @@ t.test('does not find non-executable', async (t) => { t.test('find when executable', async (t) => { t.before(() => fs.chmodSync(foo, '0755')) - const { PATH, PATHEXT, OSTYPE } = process.env + const { PATH, PATHEXT } = process.env t.afterEach(() => { if (PATH) { process.env.PATH = PATH @@ -56,26 +58,20 @@ t.test('find when executable', async (t) => { } else { delete process.env.PATHEXT } - if (OSTYPE) { - process.env.OSTYPE = OSTYPE - } else { - delete process.env.OSTYPE - } + delete process.env.WHICH_FAKE_PLATFORM }) const runTest = async (exec, expect, t, opt = {}) => { opt.pathExt = '.sh' - const _which = t.mock('..') - if (typeof expect === 'string') { - const found = _which.sync(exec, opt).toLowerCase() + const found = which.sync(exec, opt).toLowerCase() t.equal(found, expect.toLowerCase()) - const res = await _which(exec, opt) + const res = await which(exec, opt) t.equal(res.toLowerCase(), expect.toLowerCase()) } else { - await t.rejects(() => _which(exec), expect) - t.throws(() => _which.sync(exec), expect) + await t.rejects(() => which(exec), expect) + t.throws(() => which.sync(exec), expect) } } @@ -90,21 +86,21 @@ t.test('find when executable', async (t) => { t.test('pathExt', async (t) => { t.test('foo.sh', async (t) => { - process.env.OSTYPE = 'cygwin' + process.env.WHICH_FAKE_PLATFORM = 'win32' process.env.PATHEXT = '.SH' process.env.PATH = fixture return runTest(basename(foo), foo, t) }) t.test('foo', async (t) => { - process.env.OSTYPE = 'cygwin' + process.env.WHICH_FAKE_PLATFORM = 'win32' process.env.PATHEXT = '.SH' process.env.PATH = fixture return runTest(basename(foo, '.sh'), foo, t) }) t.test('foo nopathext', async (t) => { - process.env.OSTYPE = 'cygwin' + process.env.WHICH_FAKE_PLATFORM = 'win32' process.env.PATH = fixture return runTest(basename(foo, '.sh'), { code: 'ENOENT' }, t) }) diff --git a/test/windows.js b/test/windows.js new file mode 100644 index 0000000..c5831f1 --- /dev/null +++ b/test/windows.js @@ -0,0 +1,10 @@ +// pretend to be Windows. +if (process.platform === 'win32') { + const t = require('tap') + t.plan(0, 'already on windows') + process.exit(0) +} + +process.env.Path = process.env.PATH.split(':').join(';') +process.env.WHICH_FAKE_PLATFORM = 'win32' +require('./basic.js') From b282df1be402395da40d2d4ec1555b38bf3c70a6 Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Sun, 30 Oct 2022 16:02:41 -0700 Subject: [PATCH 04/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 7 ++- lib/is-windows.js | 1 + package.json | 2 - test/basic.js | 148 --------------------------------------------- test/index.js | 149 ++++++++++++++++++++++++++++++++++++++++++++++ test/windows.js | 10 ---- 6 files changed, 154 insertions(+), 163 deletions(-) create mode 100644 lib/is-windows.js delete mode 100644 test/basic.js create mode 100644 test/index.js delete mode 100644 test/windows.js diff --git a/lib/index.js b/lib/index.js index 1b754fa..0f3f558 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,10 +1,11 @@ const isexe = require('isexe') const { join, posix, win32 } = require('path') +const isWindows = require('./is-windows.js') -const isWindows = process.env.WHICH_FAKE_PLATFORM || process.platform === 'win32' const delimiter = isWindows ? win32.delimiter : posix.delimiter -const rSlash = new RegExp(`[${isWindows ? '\\\\' : ''}/]`) -const rRel = new RegExp(`^\\.[${isWindows ? '\\\\' : ''}/]`) +const slashes = `[${isWindows ? win32.sep.replace(/(.)/g, '\\$1') : ''}${posix.sep}]` +const rSlash = new RegExp(slashes) +const rRel = new RegExp(`^\\.${slashes}`) const getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) diff --git a/lib/is-windows.js b/lib/is-windows.js new file mode 100644 index 0000000..fbece90 --- /dev/null +++ b/lib/is-windows.js @@ -0,0 +1 @@ +module.exports = process.platform === 'win32' diff --git a/package.json b/package.json index 079ae52..92d44b2 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,6 @@ "devDependencies": { "@npmcli/eslint-config": "^4.0.0", "@npmcli/template-oss": "4.8.0", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2", "tap": "^16.3.0" }, "scripts": { diff --git a/test/basic.js b/test/basic.js deleted file mode 100644 index 8707ddb..0000000 --- a/test/basic.js +++ /dev/null @@ -1,148 +0,0 @@ - -const t = require('tap') -const fs = require('fs') -const rimraf = require('rimraf') -const mkdirp = require('mkdirp') -const { basename, join, relative, sep, delimiter } = require('path') - -const fixdir = `fixture-${(+process.env.TAP_CHILD_ID || 0)}` -const fixture = join(__dirname, fixdir) -const foo = join(fixture, 'foo.sh') - -const which = (...args) => t.mock('..')(...args) -which.sync = (...args) => t.mock('..').sync(...args) - -t.before(() => { - rimraf.sync(fixture) - mkdirp.sync(fixture) - fs.writeFileSync(foo, 'echo foo\n') -}) - -t.teardown(() => { - rimraf.sync(fixture) -}) - -t.test('does not find missed', async (t) => { - const p = join(fixture, 'foobar.sh') - await t.rejects(() => which(p), { code: 'ENOENT' }) - t.equal(await which(p, { nothrow: true }), null) - - t.throws(() => which.sync(p), { code: 'ENOENT' }) - t.equal(which.sync(p, { nothrow: true }), null) -}) - -t.test('does not find non-executable', async (t) => { - t.test('absolute', async (t) => { - await t.rejects(() => which(foo), { code: 'ENOENT' }) - t.throws(() => which.sync(foo), { code: 'ENOENT' }) - }) - - t.test('with path', async (t) => { - await t.rejects(() => which(basename(foo), { path: fixture }), { code: 'ENOENT' }) - t.throws(() => which.sync(basename(foo), { path: fixture }), { code: 'ENOENT' }) - }) -}) - -t.test('find when executable', async (t) => { - t.before(() => fs.chmodSync(foo, '0755')) - - const { PATH, PATHEXT } = process.env - t.afterEach(() => { - if (PATH) { - process.env.PATH = PATH - } else { - delete process.env.PATH - } - if (PATHEXT) { - process.env.PATHEXT = PATHEXT - } else { - delete process.env.PATHEXT - } - delete process.env.WHICH_FAKE_PLATFORM - }) - - const runTest = async (exec, expect, t, opt = {}) => { - opt.pathExt = '.sh' - if (typeof expect === 'string') { - const found = which.sync(exec, opt).toLowerCase() - t.equal(found, expect.toLowerCase()) - - const res = await which(exec, opt) - t.equal(res.toLowerCase(), expect.toLowerCase()) - } else { - await t.rejects(() => which(exec), expect) - t.throws(() => which.sync(exec), expect) - } - } - - t.test('absolute', async (t) => { - return runTest(foo, foo, t) - }) - - t.test('with process.env.PATH', async (t) => { - process.env.PATH = fixture - return runTest(basename(foo), foo, t) - }) - - t.test('pathExt', async (t) => { - t.test('foo.sh', async (t) => { - process.env.WHICH_FAKE_PLATFORM = 'win32' - process.env.PATHEXT = '.SH' - process.env.PATH = fixture - return runTest(basename(foo), foo, t) - }) - - t.test('foo', async (t) => { - process.env.WHICH_FAKE_PLATFORM = 'win32' - process.env.PATHEXT = '.SH' - process.env.PATH = fixture - return runTest(basename(foo, '.sh'), foo, t) - }) - - t.test('foo nopathext', async (t) => { - process.env.WHICH_FAKE_PLATFORM = 'win32' - process.env.PATH = fixture - return runTest(basename(foo, '.sh'), { code: 'ENOENT' }, t) - }) - }) - - t.test('with path opt', async (t) => { - return runTest(basename(foo), foo, t, { path: fixture }) - }) - - t.test('no ./', async (t) => { - const rel = relative(process.cwd(), foo) - return runTest(rel, rel, t) - }) - - t.test('with ./', async (t) => { - const rel = `.${sep}${relative(process.cwd(), foo)}` - return runTest(rel, rel, t) - }) - - t.test('with ../', async (t) => { - const dir = basename(process.cwd()) - const rel = join('..', dir, relative(process.cwd(), foo)) - return runTest(rel, rel, t) - }) -}) - -t.test('find all', async t => { - mkdirp.sync(`${fixture}/all/a`) - mkdirp.sync(`${fixture}/all/b`) - fs.writeFileSync(`${fixture}/all/a/x.cmd`, 'exec me') - fs.writeFileSync(`${fixture}/all/b/x.cmd`, 'exec me') - fs.chmodSync(`${fixture}/all/a/x.cmd`, 0o755) - fs.chmodSync(`${fixture}/all/b/x.cmd`, 0o755) - - const opt = { - path: [`${fixture}/all/a`, `"${fixture}/all/b"`].join(delimiter), - all: true, - } - const expect = [ - join(fixture, 'all', 'a', 'x.cmd'), - join(fixture, 'all', 'b', 'x.cmd'), - ] - t.same(which.sync('x.cmd', opt), expect) - t.same(await which('x.cmd', opt), expect) -}) diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..4d43074 --- /dev/null +++ b/test/index.js @@ -0,0 +1,149 @@ + +const t = require('tap') +const fs = require('fs') +const { basename, join, relative, sep, posix, win32 } = require('path') + +const envVars = { PATH: process.env.PATH, PATHEXT: process.env.PATHEXT } + +const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ...opt } = {}) => { + t.teardown(() => { + for (const [k, v] of Object.entries(envVars)) { + if (v) { + process.env[k] = v + } else { + delete process.env[k] + } + } + }) + + for (const platform of platforms) { + t.test(`${t.name} - ${platform}`, async t => { + const platformOpt = Object.keys(opt).length ? { + ...opt, + ...Array.isArray(opt.path) + ? { path: opt.path.join(platform === 'win32' ? win32.delimiter : posix.delimiter) } + : {}, + } : undefined + + const which = t.mock('..', { '../lib/is-windows.js': platform === 'win32' }) + + if (expect?.code) { + await t.rejects(() => which(exec, platformOpt), expect, 'async rejects') + t.throws(() => which.sync(exec, platformOpt), expect, 'sync throws') + return + } + + const syncRes = which.sync(exec, platformOpt) + const res = await which(exec, platformOpt) + + if (typeof expect === 'string') { + t.strictSame(syncRes.toLowerCase(), expect.toLowerCase(), 'sync') + t.strictSame(res.toLowerCase(), expect.toLowerCase(), 'async') + } else { + t.strictSame(syncRes, expect, 'sync') + t.strictSame(res, expect, 'async') + } + }) + } +} + +t.test('does not find missed', async (t) => { + const fixture = t.testdir() + const cmd = join(fixture, 'foobar.sh') + + t.test('throw', async t => { + await runTest(t, cmd, { code: 'ENOENT' }) + }) + t.test('nothrow', async t => { + await runTest(t, cmd, null, { nothrow: true }) + }) +}) + +t.test('does not find non-executable', async (t) => { + const dir = t.testdir({ 'foo.sh': 'echo foo\n' }) + const foo = join(dir, 'foo.sh') + + t.test('absolute', async (t) => { + await runTest(t, foo, { code: 'ENOENT' }) + }) + + t.test('with path', async (t) => { + await runTest(t, basename(foo), { code: 'ENOENT' }, { path: dir }) + }) +}) + +t.test('find when executable', async t => { + const fixture = t.testdir({ 'foo.sh': 'echo foo\n' }) + const foo = join(fixture, 'foo.sh') + fs.chmodSync(foo, '0755') + + t.test('absolute', async (t) => { + await runTest(t, foo, foo) + }) + + t.test('with process.env.PATH', async (t) => { + process.env.PATH = fixture + await runTest(t, basename(foo), foo) + }) + + t.test('with path opt', async (t) => { + await runTest(t, basename(foo), foo, { path: fixture }) + }) + + t.test('no ./', async (t) => { + const rel = relative(process.cwd(), foo) + await runTest(t, rel, rel) + }) + + t.test('with ./', async (t) => { + const rel = `.${sep}${relative(process.cwd(), foo)}` + await runTest(t, rel, rel) + }) + + t.test('with ../', async (t) => { + const dir = basename(process.cwd()) + const rel = join('..', dir, relative(process.cwd(), foo)) + await runTest(t, rel, rel) + }) +}) + +t.test('find all', async t => { + const cmdName = 'x.cmd' + const fixture = t.testdir({ + all: { + a: { [cmdName]: 'exec me' }, + b: { [cmdName]: 'exec me' }, + }, + }) + const dirs = [ + join(fixture, 'all', 'a'), + join(fixture, 'all', 'b'), + ] + const cmds = dirs.map(dir => join(dir, cmdName)) + for (const cmd of cmds) { + fs.chmodSync(cmd, 0o755) + } + + await runTest(t, cmdName, cmds, { + all: true, + path: dirs.map((dir, index) => index % 2 ? dir : `"${dir}"`), + }) +}) + +t.test('pathExt', async (t) => { + const fixture = t.testdir({ 'foo.sh': 'echo foo\n' }) + const foo = join(fixture, 'foo.sh') + fs.chmodSync(foo, '0755') + + t.test('foo.sh', async (t) => { + process.env.PATHEXT = '.SH' + process.env.PATH = fixture + await runTest(t, basename(foo), foo, { platforms: ['win32'] }) + }) + + t.test('foo', async (t) => { + process.env.PATHEXT = '.SH' + process.env.PATH = fixture + await runTest(t, basename(foo, '.sh'), foo, { platforms: ['win32'] }) + }) +}) diff --git a/test/windows.js b/test/windows.js deleted file mode 100644 index c5831f1..0000000 --- a/test/windows.js +++ /dev/null @@ -1,10 +0,0 @@ -// pretend to be Windows. -if (process.platform === 'win32') { - const t = require('tap') - t.plan(0, 'already on windows') - process.exit(0) -} - -process.env.Path = process.env.PATH.split(':').join(';') -process.env.WHICH_FAKE_PLATFORM = 'win32' -require('./basic.js') From 6403d8a2d3c67da0c8f6025e1536bfe927a08b73 Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Sun, 30 Oct 2022 16:33:55 -0700 Subject: [PATCH 05/11] fixup! feat!: add @npmcli/template-oss and modernize --- bin/which.js | 6 +++--- test/index.js | 39 ++++++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/bin/which.js b/bin/which.js index ff01813..6df16f2 100755 --- a/bin/which.js +++ b/bin/which.js @@ -16,7 +16,7 @@ if (!argv.length) { } let dashdash = false -const [cmds, flags] = argv.reduce((acc, arg) => { +const [commands, flags] = argv.reduce((acc, arg) => { if (dashdash || arg === '--') { dashdash = true return acc @@ -40,9 +40,9 @@ const [cmds, flags] = argv.reduce((acc, arg) => { return acc }, [[], {}]) -for (const cmd of cmds) { +for (const command of commands) { try { - const res = which.sync(cmd, { all: flags.all }) + const res = which.sync(command, { all: flags.all }) if (!flags.silent) { console.log([].concat(res).join('\n')) } diff --git a/test/index.js b/test/index.js index 4d43074..cff86cc 100644 --- a/test/index.js +++ b/test/index.js @@ -3,6 +3,7 @@ const t = require('tap') const fs = require('fs') const { basename, join, relative, sep, posix, win32 } = require('path') +const DELIMITER = (p) => p === 'win32' ? win32.delimiter : posix.delimiter const envVars = { PATH: process.env.PATH, PATHEXT: process.env.PATHEXT } const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ...opt } = {}) => { @@ -20,9 +21,8 @@ const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ...opt t.test(`${t.name} - ${platform}`, async t => { const platformOpt = Object.keys(opt).length ? { ...opt, - ...Array.isArray(opt.path) - ? { path: opt.path.join(platform === 'win32' ? win32.delimiter : posix.delimiter) } - : {}, + ...Array.isArray(opt.path) ? { path: opt.path.join(DELIMITER(platform)) } : {}, + ...Array.isArray(opt.pathExt) ? { pathExt: opt.pathExt.join(DELIMITER(platform)) } : {}, } : undefined const which = t.mock('..', { '../lib/is-windows.js': platform === 'win32' }) @@ -119,11 +119,11 @@ t.test('find all', async t => { join(fixture, 'all', 'a'), join(fixture, 'all', 'b'), ] - const cmds = dirs.map(dir => join(dir, cmdName)) - for (const cmd of cmds) { + const cmds = dirs.map(dir => { + const cmd = join(dir, cmdName) fs.chmodSync(cmd, 0o755) - } - + return cmd + }) await runTest(t, cmdName, cmds, { all: true, path: dirs.map((dir, index) => index % 2 ? dir : `"${dir}"`), @@ -135,15 +135,28 @@ t.test('pathExt', async (t) => { const foo = join(fixture, 'foo.sh') fs.chmodSync(foo, '0755') - t.test('foo.sh', async (t) => { - process.env.PATHEXT = '.SH' + const pathExt = '.SH;.sh' + const opts = { + platforms: ['win32'], + } + + t.test('foo.sh - env vars', async (t) => { + process.env.PATHEXT = pathExt process.env.PATH = fixture - await runTest(t, basename(foo), foo, { platforms: ['win32'] }) + await runTest(t, basename(foo), foo, opts) }) - t.test('foo', async (t) => { - process.env.PATHEXT = '.SH' + t.test('foo.sh - opts', async (t) => { + await runTest(t, basename(foo), foo, { ...opts, path: fixture, pathExt }) + }) + + t.test('foo - env vars', async (t) => { + process.env.PATHEXT = pathExt process.env.PATH = fixture - await runTest(t, basename(foo, '.sh'), foo, { platforms: ['win32'] }) + await runTest(t, basename(foo, '.sh'), foo, opts) + }) + + t.test('foo - opts', async (t) => { + await runTest(t, basename(foo, '.sh'), foo, { ...opts, path: fixture, pathExt }) }) }) From 84006621790952cfcd2d3c1d91480a799de2aa02 Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Mon, 31 Oct 2022 13:55:01 -0700 Subject: [PATCH 06/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 12 ++++---- lib/is-windows.js | 1 + test/index.js | 78 +++++++++++++++++++++++++++-------------------- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/lib/index.js b/lib/index.js index 0f3f558..1c406d5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,8 @@ const isexe = require('isexe') -const { join, posix, win32 } = require('path') +const { join, delimiter: platformDelimiter } = require('path') const isWindows = require('./is-windows.js') -const delimiter = isWindows ? win32.delimiter : posix.delimiter -const slashes = `[${isWindows ? win32.sep.replace(/(.)/g, '\\$1') : ''}${posix.sep}]` +const slashes = `[\\\\/]` const rSlash = new RegExp(slashes) const rRel = new RegExp(`^\\.${slashes}`) @@ -13,18 +12,19 @@ const getNotFoundError = (cmd) => const getPathInfo = (cmd, { path: optPath = process.env.PATH, pathExt: optPathExt = process.env.PATHEXT, + delimiter: optDelimiter = platformDelimiter, }) => { // If it has a slash, then we don't bother searching the pathenv. // just check the file itself, and that's it. const pathEnv = cmd.match(rSlash) ? [''] : [ // windows always checks the cwd first ...(isWindows ? [process.cwd()] : []), - ...(optPath || /* istanbul ignore next: very unusual */ '').split(delimiter), + ...(optPath || /* istanbul ignore next: very unusual */ '').split(optDelimiter), ] if (isWindows) { - const pathExtExe = optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(delimiter) - const pathExt = pathExtExe.split(delimiter) + const pathExtExe = optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(optDelimiter) + const pathExt = pathExtExe.split(optDelimiter) if (cmd.includes('.') && pathExt[0] !== '') { pathExt.unshift('') } diff --git a/lib/is-windows.js b/lib/is-windows.js index fbece90..3720653 100644 --- a/lib/is-windows.js +++ b/lib/is-windows.js @@ -1 +1,2 @@ +// only separate so it can be more easily mocked in tests module.exports = process.platform === 'win32' diff --git a/test/index.js b/test/index.js index cff86cc..d76ef01 100644 --- a/test/index.js +++ b/test/index.js @@ -1,12 +1,12 @@ const t = require('tap') const fs = require('fs') -const { basename, join, relative, sep, posix, win32 } = require('path') +const { basename, join, relative, sep, delimiter } = require('path') +const realWindows = process.platform === 'win32' -const DELIMITER = (p) => p === 'win32' ? win32.delimiter : posix.delimiter const envVars = { PATH: process.env.PATH, PATHEXT: process.env.PATHEXT } -const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ...opt } = {}) => { +const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ..._opt } = {}) => { t.teardown(() => { for (const [k, v] of Object.entries(envVars)) { if (v) { @@ -19,29 +19,33 @@ const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ...opt for (const platform of platforms) { t.test(`${t.name} - ${platform}`, async t => { - const platformOpt = Object.keys(opt).length ? { - ...opt, - ...Array.isArray(opt.path) ? { path: opt.path.join(DELIMITER(platform)) } : {}, - ...Array.isArray(opt.pathExt) ? { pathExt: opt.pathExt.join(DELIMITER(platform)) } : {}, - } : undefined - - const which = t.mock('..', { '../lib/is-windows.js': platform === 'win32' }) - - if (expect?.code) { - await t.rejects(() => which(exec, platformOpt), expect, 'async rejects') - t.throws(() => which.sync(exec, platformOpt), expect, 'sync throws') - return + // pass in undefined if there are no opts to test default argß + const opt = Object.keys(_opt).length ? { ..._opt } : undefined + + // mock windows detections + const mocks = { '../lib/is-windows.js': platform === 'win32' } + + // if we are actually on windows but testing posix we have to + // mock isexe since that has special windows detection inside + // of it. this is mostly to get 100% coverage on windowsß + if (realWindows && platform === 'posix') { + const isexe = async (p) => [].concat(expect).includes(p) + isexe.sync = (p) => [].concat(expect).includes(p) + mocks.isexe = isexe } - const syncRes = which.sync(exec, platformOpt) - const res = await which(exec, platformOpt) + const which = t.mock('..', mocks) - if (typeof expect === 'string') { - t.strictSame(syncRes.toLowerCase(), expect.toLowerCase(), 'sync') - t.strictSame(res.toLowerCase(), expect.toLowerCase(), 'async') + if (expect?.code) { + await t.rejects(() => which(exec, opt), expect, 'async rejects') + t.throws(() => which.sync(exec, opt), expect, 'sync throws') } else { - t.strictSame(syncRes, expect, 'sync') - t.strictSame(res, expect, 'async') + const res = await which(exec, opt) + const resSync = which.sync(exec, opt) + // use jsonstringify so it works for null and strings + const value = (v) => JSON.stringify(v).toLowerCase() + t.strictSame(value(res), value(expect), 'async') + t.strictSame(value(resSync), value(expect), 'sync') } }) } @@ -77,33 +81,35 @@ t.test('find when executable', async t => { const foo = join(fixture, 'foo.sh') fs.chmodSync(foo, '0755') + const opts = realWindows ? { pathExt: '.sh' } : {} + t.test('absolute', async (t) => { - await runTest(t, foo, foo) + await runTest(t, foo, foo, opts) }) t.test('with process.env.PATH', async (t) => { process.env.PATH = fixture - await runTest(t, basename(foo), foo) + await runTest(t, basename(foo), foo, opts) }) t.test('with path opt', async (t) => { - await runTest(t, basename(foo), foo, { path: fixture }) + await runTest(t, basename(foo), foo, { ...opts, path: fixture }) }) t.test('no ./', async (t) => { const rel = relative(process.cwd(), foo) - await runTest(t, rel, rel) + await runTest(t, rel, rel, opts) }) t.test('with ./', async (t) => { const rel = `.${sep}${relative(process.cwd(), foo)}` - await runTest(t, rel, rel) + await runTest(t, rel, rel, opts) }) t.test('with ../', async (t) => { const dir = basename(process.cwd()) const rel = join('..', dir, relative(process.cwd(), foo)) - await runTest(t, rel, rel) + await runTest(t, rel, rel, opts) }) }) @@ -126,7 +132,7 @@ t.test('find all', async t => { }) await runTest(t, cmdName, cmds, { all: true, - path: dirs.map((dir, index) => index % 2 ? dir : `"${dir}"`), + path: dirs.map((dir, index) => index % 2 ? dir : `"${dir}"`).join(delimiter), }) }) @@ -135,10 +141,8 @@ t.test('pathExt', async (t) => { const foo = join(fixture, 'foo.sh') fs.chmodSync(foo, '0755') - const pathExt = '.SH;.sh' - const opts = { - platforms: ['win32'], - } + const pathExt = '.SH' + const opts = { platforms: ['win32'] } t.test('foo.sh - env vars', async (t) => { process.env.PATHEXT = pathExt @@ -159,4 +163,12 @@ t.test('pathExt', async (t) => { t.test('foo - opts', async (t) => { await runTest(t, basename(foo, '.sh'), foo, { ...opts, path: fixture, pathExt }) }) + + t.test('foo - no pathext', async (t) => { + await runTest(t, basename(foo, '.sh'), { code: 'ENOENT' }, { + ...opts, + path: fixture, + pathExt: '', + }) + }) }) From 98a2705158dddf5b8a6de5332d55cd74b4e8c0c4 Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Mon, 31 Oct 2022 13:59:26 -0700 Subject: [PATCH 07/11] fixup! feat!: add @npmcli/template-oss and modernize --- test/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/index.js b/test/index.js index d76ef01..4a4f68f 100644 --- a/test/index.js +++ b/test/index.js @@ -42,10 +42,8 @@ const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ..._op } else { const res = await which(exec, opt) const resSync = which.sync(exec, opt) - // use jsonstringify so it works for null and strings - const value = (v) => JSON.stringify(v).toLowerCase() - t.strictSame(value(res), value(expect), 'async') - t.strictSame(value(resSync), value(expect), 'sync') + t.strictSame(res, expect, 'async') + t.strictSame(resSync, expect, 'sync') } }) } @@ -141,7 +139,7 @@ t.test('pathExt', async (t) => { const foo = join(fixture, 'foo.sh') fs.chmodSync(foo, '0755') - const pathExt = '.SH' + const pathExt = '.sh' const opts = { platforms: ['win32'] } t.test('foo.sh - env vars', async (t) => { From 25eea06ab82e5d72bc0941aabce9b9e1cc21aa3d Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Mon, 31 Oct 2022 14:35:14 -0700 Subject: [PATCH 08/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 11 +++++------ lib/is-windows.js | 2 -- test/index.js | 31 +++++++++++++++++-------------- 3 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 lib/is-windows.js diff --git a/lib/index.js b/lib/index.js index 1c406d5..1d11674 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,10 +1,9 @@ const isexe = require('isexe') -const { join, delimiter: platformDelimiter } = require('path') -const isWindows = require('./is-windows.js') +const { join, delimiter } = require('path') +const isWindows = process.platform === 'win32' -const slashes = `[\\\\/]` -const rSlash = new RegExp(slashes) -const rRel = new RegExp(`^\\.${slashes}`) +const rSlash = isWindows ? /[\\/]/ : /[/]/ +const rRel = new RegExp(`^\\.${rSlash.source}`) const getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) @@ -12,7 +11,7 @@ const getNotFoundError = (cmd) => const getPathInfo = (cmd, { path: optPath = process.env.PATH, pathExt: optPathExt = process.env.PATHEXT, - delimiter: optDelimiter = platformDelimiter, + delimiter: optDelimiter = delimiter, }) => { // If it has a slash, then we don't bother searching the pathenv. // just check the file itself, and that's it. diff --git a/lib/is-windows.js b/lib/is-windows.js deleted file mode 100644 index 3720653..0000000 --- a/lib/is-windows.js +++ /dev/null @@ -1,2 +0,0 @@ -// only separate so it can be more easily mocked in tests -module.exports = process.platform === 'win32' diff --git a/test/index.js b/test/index.js index 4a4f68f..555b498 100644 --- a/test/index.js +++ b/test/index.js @@ -2,13 +2,14 @@ const t = require('tap') const fs = require('fs') const { basename, join, relative, sep, delimiter } = require('path') -const realWindows = process.platform === 'win32' +const isWindows = process.platform === 'win32' -const envVars = { PATH: process.env.PATH, PATHEXT: process.env.PATHEXT } +const ENV_VARS = { PATH: process.env.PATH, PATHEXT: process.env.PATHEXT } +const PLATFORM = Object.getOwnPropertyDescriptor(process, 'platform') const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ..._opt } = {}) => { t.teardown(() => { - for (const [k, v] of Object.entries(envVars)) { + for (const [k, v] of Object.entries(ENV_VARS)) { if (v) { process.env[k] = v } else { @@ -19,31 +20,32 @@ const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ..._op for (const platform of platforms) { t.test(`${t.name} - ${platform}`, async t => { + Object.defineProperty(process, 'platform', { ...PLATFORM, value: platform }) + + t.teardown(() => { + Object.defineProperty(process, 'platform', PLATFORM) + }) + // pass in undefined if there are no opts to test default argß const opt = Object.keys(_opt).length ? { ..._opt } : undefined - // mock windows detections - const mocks = { '../lib/is-windows.js': platform === 'win32' } - // if we are actually on windows but testing posix we have to // mock isexe since that has special windows detection inside - // of it. this is mostly to get 100% coverage on windowsß - if (realWindows && platform === 'posix') { + // of it. this is mostly to get 100% coverage on windows + const mocks = {} + if (isWindows && platform === 'posix') { const isexe = async (p) => [].concat(expect).includes(p) isexe.sync = (p) => [].concat(expect).includes(p) mocks.isexe = isexe } const which = t.mock('..', mocks) - if (expect?.code) { await t.rejects(() => which(exec, opt), expect, 'async rejects') t.throws(() => which.sync(exec, opt), expect, 'sync throws') } else { - const res = await which(exec, opt) - const resSync = which.sync(exec, opt) - t.strictSame(res, expect, 'async') - t.strictSame(resSync, expect, 'sync') + t.strictSame(await which(exec, opt), expect, 'async') + t.strictSame(which.sync(exec, opt), expect, 'sync') } }) } @@ -79,7 +81,8 @@ t.test('find when executable', async t => { const foo = join(fixture, 'foo.sh') fs.chmodSync(foo, '0755') - const opts = realWindows ? { pathExt: '.sh' } : {} + // windows needs to explicitly look for .sh files by default + const opts = isWindows ? { pathExt: '.sh' } : {} t.test('absolute', async (t) => { await runTest(t, foo, foo, opts) From 29e4841c5e5a0c1ea293c13479dd03da4d81345f Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Mon, 31 Oct 2022 14:59:48 -0700 Subject: [PATCH 09/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1d11674..3f0ceb9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,8 +1,15 @@ const isexe = require('isexe') -const { join, delimiter } = require('path') +const { join, delimiter, sep, posix } = require('path') + const isWindows = process.platform === 'win32' -const rSlash = isWindows ? /[\\/]/ : /[/]/ +// used to check for slashed in commands passed in. always checks for the posix +// seperator on all platforms, and checks for the current separator when not on +// a posix platform. don't use the isWindows check for this since that is mocked +// in tests but we still need the code to actually work when called. that is also +// why it is ignored from coverage. +/* istanbul ignore next */ +const rSlash = new RegExp(`[${posix.sep}${sep !== posix.sep ? sep.replace(/(.)/g, '\\$1') : ''}]`) const rRel = new RegExp(`^\\.${rSlash.source}`) const getNotFoundError = (cmd) => From ba0deef9b962867c6babef74829cad9568ac5754 Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Mon, 31 Oct 2022 15:01:50 -0700 Subject: [PATCH 10/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 3f0ceb9..e7a7087 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,7 +9,7 @@ const isWindows = process.platform === 'win32' // in tests but we still need the code to actually work when called. that is also // why it is ignored from coverage. /* istanbul ignore next */ -const rSlash = new RegExp(`[${posix.sep}${sep !== posix.sep ? sep.replace(/(.)/g, '\\$1') : ''}]`) +const rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? '' : sep}]`.replace(/\\/g, '\\$1')) const rRel = new RegExp(`^\\.${rSlash.source}`) const getNotFoundError = (cmd) => From df7b2ec558904d7bb003e526211b90ea5135199e Mon Sep 17 00:00:00 2001 From: Luke Karrys <luke@lukekarrys.com> Date: Mon, 31 Oct 2022 15:15:54 -0700 Subject: [PATCH 11/11] fixup! feat!: add @npmcli/template-oss and modernize --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index e7a7087..8de3388 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,7 +9,7 @@ const isWindows = process.platform === 'win32' // in tests but we still need the code to actually work when called. that is also // why it is ignored from coverage. /* istanbul ignore next */ -const rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? '' : sep}]`.replace(/\\/g, '\\$1')) +const rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? '' : sep}]`.replace(/(\\)/g, '\\$1')) const rRel = new RegExp(`^\\.${rSlash.source}`) const getNotFoundError = (cmd) =>