├── .commitlintrc.js ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── actions │ ├── create-check │ │ └── action.yml │ └── install-latest-npm │ │ └── action.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml └── workflows │ ├── audit.yml │ ├── ci-release.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── post-dependabot.yml │ ├── pull-request.yml │ ├── release-integration.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── lib ├── bin-target.js ├── check-bin.js ├── check-bins.js ├── fix-bin.js ├── get-node-modules.js ├── get-paths.js ├── get-prefix.js ├── index.js ├── is-windows.js ├── link-bin.js ├── link-bins.js ├── link-gently.js ├── link-mans.js ├── man-target.js └── shim-bin.js ├── map.js ├── package.json ├── release-please-config.json ├── tap-snapshots └── test │ ├── bin-target.js.test.cjs │ ├── get-paths.js.test.cjs │ ├── index.js.test.cjs │ └── man-target.js.test.cjs └── test ├── bin-target.js ├── check-bin.js ├── check-bins.js ├── fix-bin.js ├── get-node-modules.js ├── get-paths.js ├── get-prefix.js ├── index.js ├── is-windows.js ├── link-bin.js ├── link-bins.js ├── link-gently.js ├── link-mans.js ├── man-target.js └── shim-bin.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 7 | 'header-max-length': [2, 'always', 80], 8 | 'subject-case': [0], 9 | 'body-max-line-length': [0], 10 | 'footer-max-line-length': [0], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | 'use strict' 4 | 5 | const { readdirSync: readdir } = require('fs') 6 | 7 | const localConfigs = readdir(__dirname) 8 | .filter((file) => file.startsWith('.eslintrc.local.')) 9 | .map((file) => `./${file}`) 10 | 11 | module.exports = { 12 | root: true, 13 | ignorePatterns: [ 14 | 'tap-testdir*/', 15 | ], 16 | extends: [ 17 | '@npmcli', 18 | ...localConfigs, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Bug 4 | description: File a bug/issue 5 | title: "[BUG] " 6 | labels: [ Bug, Needs Triage ] 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please [search here](./issues) to see if an issue already exists for your problem. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Current Behavior 19 | description: A clear & concise description of what you're experiencing. 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavior 25 | description: A clear & concise description of what you expected to happen. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Steps To Reproduce 31 | description: Steps to reproduce the behavior. 32 | value: | 33 | 1. In this environment... 34 | 2. With this config... 35 | 3. Run '...' 36 | 4. See error... 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **npm**: 7.6.3 45 | - **Node**: 13.14.0 46 | - **OS**: Ubuntu 20.04 47 | - **platform**: Macbook Pro 48 | value: | 49 | - npm: 50 | - Node: 51 | - OS: 52 | - platform: 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | blank_issues_enabled: true 4 | -------------------------------------------------------------------------------- /.github/actions/create-check/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Create Check' 4 | inputs: 5 | name: 6 | required: true 7 | token: 8 | required: true 9 | sha: 10 | required: true 11 | check-name: 12 | default: '' 13 | outputs: 14 | check-id: 15 | value: ${{ steps.create-check.outputs.check_id }} 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Get Workflow Job 20 | uses: actions/github-script@v7 21 | id: workflow 22 | env: 23 | JOB_NAME: "${{ inputs.name }}" 24 | SHA: "${{ inputs.sha }}" 25 | with: 26 | result-encoding: string 27 | script: | 28 | const { repo: { owner, repo}, runId, serverUrl } = context 29 | const { JOB_NAME, SHA } = process.env 30 | 31 | const job = await github.rest.actions.listJobsForWorkflowRun({ 32 | owner, 33 | repo, 34 | run_id: runId, 35 | per_page: 100 36 | }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME))) 37 | 38 | return [ 39 | `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`, 40 | 'Run logs:', 41 | job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`, 42 | ].join(' ') 43 | - name: Create Check 44 | uses: LouisBrunner/checks-action@v1.6.0 45 | id: create-check 46 | with: 47 | token: ${{ inputs.token }} 48 | sha: ${{ inputs.sha }} 49 | status: in_progress 50 | name: ${{ inputs.check-name || inputs.name }} 51 | output: | 52 | {"summary":"${{ steps.workflow.outputs.result }}"} 53 | -------------------------------------------------------------------------------- /.github/actions/install-latest-npm/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Install Latest npm' 4 | description: 'Install the latest version of npm compatible with the Node version' 5 | inputs: 6 | node: 7 | description: 'Current Node version' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows 13 | - name: Update Windows npm 14 | if: | 15 | runner.os == 'Windows' && ( 16 | startsWith(inputs.node, 'v10.') || 17 | startsWith(inputs.node, 'v12.') || 18 | startsWith(inputs.node, 'v14.') 19 | ) 20 | shell: cmd 21 | run: | 22 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz 23 | tar xf npm-7.5.4.tgz 24 | cd package 25 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz 26 | cd .. 27 | rmdir /s /q package 28 | - name: Install Latest npm 29 | shell: bash 30 | env: 31 | NODE_VERSION: ${{ inputs.node }} 32 | working-directory: ${{ runner.temp }} 33 | run: | 34 | MATCH="" 35 | SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") 36 | 37 | echo "node@$NODE_VERSION" 38 | 39 | for SPEC in ${SPECS[@]}; do 40 | ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') 41 | echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" 42 | 43 | if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then 44 | MATCH=$SPEC 45 | echo "Found compatible version: npm@$MATCH" 46 | break 47 | fi 48 | done 49 | 50 | if [ -z $MATCH ]; then 51 | echo "Could not find a compatible version of npm for node@$NODE_VERSION" 52 | exit 1 53 | fi 54 | 55 | npm i --prefer-online --no-fund --no-audit -g npm@$MATCH 56 | - name: npm Version 57 | shell: bash 58 | run: npm -v 59 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | target-branch: "main" 11 | allow: 12 | - dependency-type: direct 13 | versioning-strategy: increase-if-necessary 14 | commit-message: 15 | prefix: deps 16 | prefix-development: chore 17 | labels: 18 | - "Dependencies" 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /.github/matchers/tap.json: -------------------------------------------------------------------------------- 1 | { 2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", 3 | "problemMatcher": [ 4 | { 5 | "owner": "tap", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\s*not ok \\d+ - (.*)", 9 | "message": 1 10 | }, 11 | { 12 | "regexp": "^\\s*---" 13 | }, 14 | { 15 | "regexp": "^\\s*at:" 16 | }, 17 | { 18 | "regexp": "^\\s*line:\\s*(\\d+)", 19 | "line": 1 20 | }, 21 | { 22 | "regexp": "^\\s*column:\\s*(\\d+)", 23 | "column": 1 24 | }, 25 | { 26 | "regexp": "^\\s*file:\\s*(.*)", 27 | "file": 1 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | block_creations: true 19 | required_pull_request_reviews: 20 | required_approving_review_count: 1 21 | require_code_owner_reviews: true 22 | require_last_push_approval: true 23 | dismiss_stale_reviews: true 24 | restrictions: 25 | apps: [] 26 | users: [] 27 | teams: [ "cli-team" ] 28 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Audit 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 9 | - cron: "0 8 * * 1" 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | audit: 16 | name: Audit Dependencies 17 | if: github.repository_owner == 'npm' 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | shell: bash 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup Git User 26 | run: | 27 | git config --global user.email "npm-cli+bot@github.com" 28 | git config --global user.name "npm CLI robot" 29 | - name: Setup Node 30 | uses: actions/setup-node@v4 31 | id: node 32 | with: 33 | node-version: 22.x 34 | check-latest: contains('22.x', '.x') 35 | - name: Install Latest npm 36 | uses: ./.github/actions/install-latest-npm 37 | with: 38 | node: ${{ steps.node.outputs.node-version }} 39 | - name: Install Dependencies 40 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock 41 | - name: Run Production Audit 42 | run: npm audit --omit=dev 43 | - name: Run Full Audit 44 | run: npm audit --audit-level=none 45 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI - Release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | ref: 9 | required: true 10 | type: string 11 | default: main 12 | workflow_call: 13 | inputs: 14 | ref: 15 | required: true 16 | type: string 17 | check-sha: 18 | required: true 19 | type: string 20 | 21 | permissions: 22 | contents: read 23 | checks: write 24 | 25 | jobs: 26 | lint-all: 27 | name: Lint All 28 | if: github.repository_owner == 'npm' 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | ref: ${{ inputs.ref }} 38 | - name: Setup Git User 39 | run: | 40 | git config --global user.email "npm-cli+bot@github.com" 41 | git config --global user.name "npm CLI robot" 42 | - name: Create Check 43 | id: create-check 44 | if: ${{ inputs.check-sha }} 45 | uses: ./.github/actions/create-check 46 | with: 47 | name: "Lint All" 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | sha: ${{ inputs.check-sha }} 50 | - name: Setup Node 51 | uses: actions/setup-node@v4 52 | id: node 53 | with: 54 | node-version: 22.x 55 | check-latest: contains('22.x', '.x') 56 | - name: Install Latest npm 57 | uses: ./.github/actions/install-latest-npm 58 | with: 59 | node: ${{ steps.node.outputs.node-version }} 60 | - name: Install Dependencies 61 | run: npm i --ignore-scripts --no-audit --no-fund 62 | - name: Lint 63 | run: npm run lint --ignore-scripts 64 | - name: Post Lint 65 | run: npm run postlint --ignore-scripts 66 | - name: Conclude Check 67 | uses: LouisBrunner/checks-action@v1.6.0 68 | if: steps.create-check.outputs.check-id && always() 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | conclusion: ${{ job.status }} 72 | check_id: ${{ steps.create-check.outputs.check-id }} 73 | 74 | test-all: 75 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 76 | if: github.repository_owner == 'npm' 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | platform: 81 | - name: Linux 82 | os: ubuntu-latest 83 | shell: bash 84 | - name: macOS 85 | os: macos-latest 86 | shell: bash 87 | - name: macOS 88 | os: macos-13 89 | shell: bash 90 | node-version: 91 | - 18.17.0 92 | - 18.x 93 | - 20.5.0 94 | - 20.x 95 | - 22.x 96 | exclude: 97 | - platform: { name: macOS, os: macos-13, shell: bash } 98 | node-version: 18.17.0 99 | - platform: { name: macOS, os: macos-13, shell: bash } 100 | node-version: 18.x 101 | - platform: { name: macOS, os: macos-13, shell: bash } 102 | node-version: 20.5.0 103 | - platform: { name: macOS, os: macos-13, shell: bash } 104 | node-version: 20.x 105 | - platform: { name: macOS, os: macos-13, shell: bash } 106 | node-version: 22.x 107 | runs-on: ${{ matrix.platform.os }} 108 | defaults: 109 | run: 110 | shell: ${{ matrix.platform.shell }} 111 | steps: 112 | - name: Checkout 113 | uses: actions/checkout@v4 114 | with: 115 | ref: ${{ inputs.ref }} 116 | - name: Setup Git User 117 | run: | 118 | git config --global user.email "npm-cli+bot@github.com" 119 | git config --global user.name "npm CLI robot" 120 | - name: Create Check 121 | id: create-check 122 | if: ${{ inputs.check-sha }} 123 | uses: ./.github/actions/create-check 124 | with: 125 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 126 | token: ${{ secrets.GITHUB_TOKEN }} 127 | sha: ${{ inputs.check-sha }} 128 | - name: Setup Node 129 | uses: actions/setup-node@v4 130 | id: node 131 | with: 132 | node-version: ${{ matrix.node-version }} 133 | check-latest: contains(matrix.node-version, '.x') 134 | - name: Install Latest npm 135 | uses: ./.github/actions/install-latest-npm 136 | with: 137 | node: ${{ steps.node.outputs.node-version }} 138 | - name: Install Dependencies 139 | run: npm i --ignore-scripts --no-audit --no-fund 140 | - name: Add Problem Matcher 141 | run: echo "::add-matcher::.github/matchers/tap.json" 142 | - name: Test 143 | run: npm test --ignore-scripts 144 | - name: Conclude Check 145 | uses: LouisBrunner/checks-action@v1.6.0 146 | if: steps.create-check.outputs.check-id && always() 147 | with: 148 | token: ${{ secrets.GITHUB_TOKEN }} 149 | conclusion: ${{ job.status }} 150 | check_id: ${{ steps.create-check.outputs.check-id }} 151 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | schedule: 12 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 13 | - cron: "0 9 * * 1" 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | lint: 20 | name: Lint 21 | if: github.repository_owner == 'npm' 22 | runs-on: ubuntu-latest 23 | defaults: 24 | run: 25 | shell: bash 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Lint 46 | run: npm run lint --ignore-scripts 47 | - name: Post Lint 48 | run: npm run postlint --ignore-scripts 49 | 50 | test: 51 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 52 | if: github.repository_owner == 'npm' 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | platform: 57 | - name: Linux 58 | os: ubuntu-latest 59 | shell: bash 60 | - name: macOS 61 | os: macos-latest 62 | shell: bash 63 | - name: macOS 64 | os: macos-13 65 | shell: bash 66 | node-version: 67 | - 18.17.0 68 | - 18.x 69 | - 20.5.0 70 | - 20.x 71 | - 22.x 72 | exclude: 73 | - platform: { name: macOS, os: macos-13, shell: bash } 74 | node-version: 18.17.0 75 | - platform: { name: macOS, os: macos-13, shell: bash } 76 | node-version: 18.x 77 | - platform: { name: macOS, os: macos-13, shell: bash } 78 | node-version: 20.5.0 79 | - platform: { name: macOS, os: macos-13, shell: bash } 80 | node-version: 20.x 81 | - platform: { name: macOS, os: macos-13, shell: bash } 82 | node-version: 22.x 83 | runs-on: ${{ matrix.platform.os }} 84 | defaults: 85 | run: 86 | shell: ${{ matrix.platform.shell }} 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | - name: Setup Git User 91 | run: | 92 | git config --global user.email "npm-cli+bot@github.com" 93 | git config --global user.name "npm CLI robot" 94 | - name: Setup Node 95 | uses: actions/setup-node@v4 96 | id: node 97 | with: 98 | node-version: ${{ matrix.node-version }} 99 | check-latest: contains(matrix.node-version, '.x') 100 | - name: Install Latest npm 101 | uses: ./.github/actions/install-latest-npm 102 | with: 103 | node: ${{ steps.node.outputs.node-version }} 104 | - name: Install Dependencies 105 | run: npm i --ignore-scripts --no-audit --no-fund 106 | - name: Add Problem Matcher 107 | run: echo "::add-matcher::.github/matchers/tap.json" 108 | - name: Test 109 | run: npm test --ignore-scripts 110 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CodeQL 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 14 | - cron: "0 10 * * 1" 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | permissions: 24 | actions: read 25 | contents: read 26 | security-events: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Setup Git User 31 | run: | 32 | git config --global user.email "npm-cli+bot@github.com" 33 | git config --global user.name "npm CLI robot" 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: javascript 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Post Dependabot 4 | 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | template-oss: 12 | name: template-oss 13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Setup Git User 24 | run: | 25 | git config --global user.email "npm-cli+bot@github.com" 26 | git config --global user.name "npm CLI robot" 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | id: node 30 | with: 31 | node-version: 22.x 32 | check-latest: contains('22.x', '.x') 33 | - name: Install Latest npm 34 | uses: ./.github/actions/install-latest-npm 35 | with: 36 | node: ${{ steps.node.outputs.node-version }} 37 | - name: Install Dependencies 38 | run: npm i --ignore-scripts --no-audit --no-fund 39 | - name: Fetch Dependabot Metadata 40 | id: metadata 41 | uses: dependabot/fetch-metadata@v1 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Dependabot can update multiple directories so we output which directory 46 | # it is acting on so we can run the command for the correct root or workspace 47 | - name: Get Dependabot Directory 48 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') 49 | id: flags 50 | run: | 51 | dependabot_dir="${{ steps.metadata.outputs.directory }}" 52 | if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then 53 | echo "workspace=-iwr" >> $GITHUB_OUTPUT 54 | else 55 | # strip leading slash from directory so it works as a 56 | # a path to the workspace flag 57 | echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Apply Changes 61 | if: steps.flags.outputs.workspace 62 | id: apply 63 | run: | 64 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} 65 | if [[ `git status --porcelain` ]]; then 66 | echo "changes=true" >> $GITHUB_OUTPUT 67 | fi 68 | # This only sets the conventional commit prefix. This workflow can't reliably determine 69 | # what the breaking change is though. If a BREAKING CHANGE message is required then 70 | # this PR check will fail and the commit will be amended with stafftools 71 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then 72 | prefix='feat!' 73 | else 74 | prefix='chore' 75 | fi 76 | echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT 77 | 78 | # This step will fail if template-oss has made any workflow updates. It is impossible 79 | # for a workflow to update other workflows. In the case it does fail, we continue 80 | # and then try to apply only a portion of the changes in the next step 81 | - name: Push All Changes 82 | if: steps.apply.outputs.changes 83 | id: push 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | run: | 88 | git commit -am "${{ steps.apply.outputs.message }}" 89 | git push 90 | 91 | # If the previous step failed, then reset the commit and remove any workflow changes 92 | # and attempt to commit and push again. This is helpful because we will have a commit 93 | # with the correct prefix that we can then --amend with @npmcli/stafftools later. 94 | - name: Push All Changes Except Workflows 95 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure' 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | git reset HEAD~ 100 | git checkout HEAD -- .github/workflows/ 101 | git clean -fd .github/workflows/ 102 | git commit -am "${{ steps.apply.outputs.message }}" 103 | git push 104 | 105 | # Check if all the necessary template-oss changes were applied. Since we continued 106 | # on errors in one of the previous steps, this check will fail if our follow up 107 | # only applied a portion of the changes and we need to followup manually. 108 | # 109 | # Note that this used to run `lint` and `postlint` but that will fail this action 110 | # if we've also shipped any linting changes separate from template-oss. We do 111 | # linting in another action, so we want to fail this one only if there are 112 | # template-oss changes that could not be applied. 113 | - name: Check Changes 114 | if: steps.apply.outputs.changes 115 | run: | 116 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check 117 | 118 | - name: Fail on Breaking Change 119 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') 120 | run: | 121 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" 122 | echo "for more information on how to fix this with a BREAKING CHANGE footer." 123 | exit 1 124 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Pull Request 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | commitlint: 18 | name: Lint Commits 19 | if: github.repository_owner == 'npm' 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Run Commitlint on Commits 46 | id: commit 47 | continue-on-error: true 48 | run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} 49 | - name: Run Commitlint on PR Title 50 | if: steps.commit.outcome == 'failure' 51 | env: 52 | PR_TITLE: ${{ github.event.pull_request.title }} 53 | run: echo "$PR_TITLE" | npx --offline commitlint -V 54 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release Integration 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | releases: 9 | required: true 10 | type: string 11 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 12 | workflow_call: 13 | inputs: 14 | releases: 15 | required: true 16 | type: string 17 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 18 | secrets: 19 | PUBLISH_TOKEN: 20 | required: true 21 | 22 | permissions: 23 | contents: read 24 | id-token: write 25 | 26 | jobs: 27 | publish: 28 | name: Publish 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | permissions: 34 | id-token: write 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | ref: ${{ fromJSON(inputs.releases)[0].tagName }} 40 | - name: Setup Git User 41 | run: | 42 | git config --global user.email "npm-cli+bot@github.com" 43 | git config --global user.name "npm CLI robot" 44 | - name: Setup Node 45 | uses: actions/setup-node@v4 46 | id: node 47 | with: 48 | node-version: 22.x 49 | check-latest: contains('22.x', '.x') 50 | - name: Install Latest npm 51 | uses: ./.github/actions/install-latest-npm 52 | with: 53 | node: ${{ steps.node.outputs.node-version }} 54 | - name: Install Dependencies 55 | run: npm i --ignore-scripts --no-audit --no-fund 56 | - name: Set npm authToken 57 | run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} 58 | - name: Publish 59 | env: 60 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 61 | RELEASES: ${{ inputs.releases }} 62 | run: | 63 | EXIT_CODE=0 64 | 65 | for release in $(echo $RELEASES | jq -r '.[] | @base64'); do 66 | PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag) 67 | npm publish --provenance --tag="$PUBLISH_TAG" 68 | STATUS=$? 69 | if [[ "$STATUS" -eq 1 ]]; then 70 | EXIT_CODE=$STATUS 71 | fi 72 | done 73 | 74 | exit $EXIT_CODE 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | checks: write 14 | 15 | jobs: 16 | release: 17 | outputs: 18 | pr: ${{ steps.release.outputs.pr }} 19 | pr-branch: ${{ steps.release.outputs.pr-branch }} 20 | pr-number: ${{ steps.release.outputs.pr-number }} 21 | pr-sha: ${{ steps.release.outputs.pr-sha }} 22 | releases: ${{ steps.release.outputs.releases }} 23 | comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }} 24 | check-id: ${{ steps.create-check.outputs.check-id }} 25 | name: Release 26 | if: github.repository_owner == 'npm' 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | shell: bash 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | id: node 41 | with: 42 | node-version: 22.x 43 | check-latest: contains('22.x', '.x') 44 | - name: Install Latest npm 45 | uses: ./.github/actions/install-latest-npm 46 | with: 47 | node: ${{ steps.node.outputs.node-version }} 48 | - name: Install Dependencies 49 | run: npm i --ignore-scripts --no-audit --no-fund 50 | - name: Release Please 51 | id: release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest" 55 | - name: Create Release Manager Comment Text 56 | if: steps.release.outputs.pr-number 57 | uses: actions/github-script@v7 58 | id: comment-text 59 | with: 60 | result-encoding: string 61 | script: | 62 | const { runId, repo: { owner, repo } } = context 63 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) 64 | return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n') 65 | - name: Find Release Manager Comment 66 | uses: peter-evans/find-comment@v2 67 | if: steps.release.outputs.pr-number 68 | id: found-comment 69 | with: 70 | issue-number: ${{ steps.release.outputs.pr-number }} 71 | comment-author: 'github-actions[bot]' 72 | body-includes: '## Release Manager' 73 | - name: Create Release Manager Comment 74 | id: create-comment 75 | if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id 76 | uses: peter-evans/create-or-update-comment@v3 77 | with: 78 | issue-number: ${{ steps.release.outputs.pr-number }} 79 | body: ${{ steps.comment-text.outputs.result }} 80 | - name: Update Release Manager Comment 81 | id: update-comment 82 | if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id 83 | uses: peter-evans/create-or-update-comment@v3 84 | with: 85 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 86 | body: ${{ steps.comment-text.outputs.result }} 87 | edit-mode: 'replace' 88 | - name: Create Check 89 | id: create-check 90 | uses: ./.github/actions/create-check 91 | if: steps.release.outputs.pr-sha 92 | with: 93 | name: "Release" 94 | token: ${{ secrets.GITHUB_TOKEN }} 95 | sha: ${{ steps.release.outputs.pr-sha }} 96 | 97 | update: 98 | needs: release 99 | outputs: 100 | sha: ${{ steps.commit.outputs.sha }} 101 | check-id: ${{ steps.create-check.outputs.check-id }} 102 | name: Update - Release 103 | if: github.repository_owner == 'npm' && needs.release.outputs.pr 104 | runs-on: ubuntu-latest 105 | defaults: 106 | run: 107 | shell: bash 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | ref: ${{ needs.release.outputs.pr-branch }} 114 | - name: Setup Git User 115 | run: | 116 | git config --global user.email "npm-cli+bot@github.com" 117 | git config --global user.name "npm CLI robot" 118 | - name: Setup Node 119 | uses: actions/setup-node@v4 120 | id: node 121 | with: 122 | node-version: 22.x 123 | check-latest: contains('22.x', '.x') 124 | - name: Install Latest npm 125 | uses: ./.github/actions/install-latest-npm 126 | with: 127 | node: ${{ steps.node.outputs.node-version }} 128 | - name: Install Dependencies 129 | run: npm i --ignore-scripts --no-audit --no-fund 130 | - name: Create Release Manager Checklist Text 131 | id: comment-text 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish 135 | - name: Append Release Manager Comment 136 | uses: peter-evans/create-or-update-comment@v3 137 | with: 138 | comment-id: ${{ needs.release.outputs.comment-id }} 139 | body: ${{ steps.comment-text.outputs.result }} 140 | edit-mode: 'append' 141 | - name: Run Post Pull Request Actions 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}" 145 | - name: Commit 146 | id: commit 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | run: | 150 | git commit --all --amend --no-edit || true 151 | git push --force-with-lease 152 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 153 | - name: Create Check 154 | id: create-check 155 | uses: ./.github/actions/create-check 156 | with: 157 | name: "Update - Release" 158 | check-name: "Release" 159 | token: ${{ secrets.GITHUB_TOKEN }} 160 | sha: ${{ steps.commit.outputs.sha }} 161 | - name: Conclude Check 162 | uses: LouisBrunner/checks-action@v1.6.0 163 | with: 164 | token: ${{ secrets.GITHUB_TOKEN }} 165 | conclusion: ${{ job.status }} 166 | check_id: ${{ needs.release.outputs.check-id }} 167 | 168 | ci: 169 | name: CI - Release 170 | needs: [ release, update ] 171 | if: needs.release.outputs.pr 172 | uses: ./.github/workflows/ci-release.yml 173 | with: 174 | ref: ${{ needs.release.outputs.pr-branch }} 175 | check-sha: ${{ needs.update.outputs.sha }} 176 | 177 | post-ci: 178 | needs: [ release, update, ci ] 179 | name: Post CI - Release 180 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() 181 | runs-on: ubuntu-latest 182 | defaults: 183 | run: 184 | shell: bash 185 | steps: 186 | - name: Get CI Conclusion 187 | id: conclusion 188 | run: | 189 | result="" 190 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 191 | result="failure" 192 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 193 | result="cancelled" 194 | else 195 | result="success" 196 | fi 197 | echo "result=$result" >> $GITHUB_OUTPUT 198 | - name: Conclude Check 199 | uses: LouisBrunner/checks-action@v1.6.0 200 | with: 201 | token: ${{ secrets.GITHUB_TOKEN }} 202 | conclusion: ${{ steps.conclusion.outputs.result }} 203 | check_id: ${{ needs.update.outputs.check-id }} 204 | 205 | post-release: 206 | needs: release 207 | outputs: 208 | comment-id: ${{ steps.create-comment.outputs.comment-id }} 209 | name: Post Release - Release 210 | if: github.repository_owner == 'npm' && needs.release.outputs.releases 211 | runs-on: ubuntu-latest 212 | defaults: 213 | run: 214 | shell: bash 215 | steps: 216 | - name: Create Release PR Comment Text 217 | id: comment-text 218 | uses: actions/github-script@v7 219 | env: 220 | RELEASES: ${{ needs.release.outputs.releases }} 221 | with: 222 | result-encoding: string 223 | script: | 224 | const releases = JSON.parse(process.env.RELEASES) 225 | const { runId, repo: { owner, repo } } = context 226 | const issue_number = releases[0].prNumber 227 | const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` 228 | 229 | return [ 230 | '## Release Workflow\n', 231 | ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`), 232 | `- Workflow run: :arrows_counterclockwise: ${runUrl}`, 233 | ].join('\n') 234 | - name: Create Release PR Comment 235 | id: create-comment 236 | uses: peter-evans/create-or-update-comment@v3 237 | with: 238 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 239 | body: ${{ steps.comment-text.outputs.result }} 240 | 241 | release-integration: 242 | needs: release 243 | name: Release Integration 244 | if: needs.release.outputs.releases 245 | uses: ./.github/workflows/release-integration.yml 246 | permissions: 247 | contents: read 248 | id-token: write 249 | secrets: 250 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 251 | with: 252 | releases: ${{ needs.release.outputs.releases }} 253 | 254 | post-release-integration: 255 | needs: [ release, release-integration, post-release ] 256 | name: Post Release Integration - Release 257 | if: github.repository_owner == 'npm' && needs.release.outputs.releases && always() 258 | runs-on: ubuntu-latest 259 | defaults: 260 | run: 261 | shell: bash 262 | steps: 263 | - name: Get Post Release Conclusion 264 | id: conclusion 265 | run: | 266 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 267 | result="x" 268 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 269 | result="heavy_multiplication_x" 270 | else 271 | result="white_check_mark" 272 | fi 273 | echo "result=$result" >> $GITHUB_OUTPUT 274 | - name: Find Release PR Comment 275 | uses: peter-evans/find-comment@v2 276 | id: found-comment 277 | with: 278 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 279 | comment-author: 'github-actions[bot]' 280 | body-includes: '## Release Workflow' 281 | - name: Create Release PR Comment Text 282 | id: comment-text 283 | if: steps.found-comment.outputs.comment-id 284 | uses: actions/github-script@v7 285 | env: 286 | RESULT: ${{ steps.conclusion.outputs.result }} 287 | BODY: ${{ steps.found-comment.outputs.comment-body }} 288 | with: 289 | result-encoding: string 290 | script: | 291 | const { RESULT, BODY } = process.env 292 | const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)] 293 | if (RESULT !== 'white_check_mark') { 294 | body.push(':rotating_light::rotating_light::rotating_light:') 295 | body.push([ 296 | '@npm/cli-team: The post-release workflow failed for this release.', 297 | 'Manual steps may need to be taken after examining the workflow output.' 298 | ].join(' ')) 299 | body.push(':rotating_light::rotating_light::rotating_light:') 300 | } 301 | return body.join('\n\n').trim() 302 | - name: Update Release PR Comment 303 | if: steps.comment-text.outputs.result 304 | uses: peter-evans/create-or-update-comment@v3 305 | with: 306 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 307 | body: ${{ steps.comment-text.outputs.result }} 308 | edit-mode: 'replace' 309 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | # ignore everything in the root 4 | /* 5 | 6 | !**/.gitignore 7 | !/.commitlintrc.js 8 | !/.eslint.config.js 9 | !/.eslintrc.js 10 | !/.eslintrc.local.* 11 | !/.git-blame-ignore-revs 12 | !/.github/ 13 | !/.gitignore 14 | !/.npmrc 15 | !/.prettierignore 16 | !/.prettierrc.js 17 | !/.release-please-manifest.json 18 | !/bin/ 19 | !/CHANGELOG* 20 | !/CODE_OF_CONDUCT.md 21 | !/CONTRIBUTING.md 22 | !/docs/ 23 | !/lib/ 24 | !/LICENSE* 25 | !/map.js 26 | !/package.json 27 | !/README* 28 | !/release-please-config.json 29 | !/scripts/ 30 | !/SECURITY.md 31 | !/tap-snapshots/ 32 | !/test/ 33 | !/tsconfig.json 34 | tap-testdir*/ 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "5.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.0.0](https://github.com/npm/bin-links/compare/v4.0.4...v5.0.0) (2024-09-25) 4 | ### ⚠️ BREAKING CHANGES 5 | * `bin-links` now supports node `^18.17.0 || >=20.5.0` 6 | ### Features 7 | * [`9648a7d`](https://github.com/npm/bin-links/commit/9648a7d49589be1c991ddc3c2842c461e2a017cb) [#121](https://github.com/npm/bin-links/pull/121) ignore EACCES on linking (@antongolub) 8 | ### Bug Fixes 9 | * [`013be50`](https://github.com/npm/bin-links/commit/013be506a28d014e0db023457e084789f414b287) [#139](https://github.com/npm/bin-links/pull/139) align to npm 10 node engine range (@reggi) 10 | ### Dependencies 11 | * [`a08d09b`](https://github.com/npm/bin-links/commit/a08d09b7e823a8ca321bcab3f6168bdd8057e3cd) [#139](https://github.com/npm/bin-links/pull/139) `write-file-atomic@6.0.0` 12 | * [`7d90298`](https://github.com/npm/bin-links/commit/7d9029869e4759dcaa5f90667c62840898c07c6a) [#139](https://github.com/npm/bin-links/pull/139) `read-cmd-shim@5.0.0` 13 | * [`0c915a3`](https://github.com/npm/bin-links/commit/0c915a3daf8548f7b9d8576ff53cd92db575eeec) [#139](https://github.com/npm/bin-links/pull/139) `proc-log@5.0.0` 14 | * [`502417c`](https://github.com/npm/bin-links/commit/502417c398b67175d9961f954eb6e54388d4559d) [#139](https://github.com/npm/bin-links/pull/139) `npm-normalize-package-bin@4.0.0` 15 | * [`722452a`](https://github.com/npm/bin-links/commit/722452a3276803217ffd866cd5d65383d74c4591) [#139](https://github.com/npm/bin-links/pull/139) `cmd-shim@7.0.0` 16 | * [`af7e347`](https://github.com/npm/bin-links/commit/af7e347bbf2346741500df4906c948f5f02e19fb) [#121](https://github.com/npm/bin-links/pull/121) add proc-log 17 | ### Chores 18 | * [`463c32f`](https://github.com/npm/bin-links/commit/463c32fed8b131c9cea5488fcfe9102200cbf6e9) [#139](https://github.com/npm/bin-links/pull/139) run template-oss-apply (@reggi) 19 | * [`ba52600`](https://github.com/npm/bin-links/commit/ba52600cf31a9bfc9db983e6fc622291a61c985a) [#133](https://github.com/npm/bin-links/pull/133) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot]) 20 | * [`fdf22ae`](https://github.com/npm/bin-links/commit/fdf22ae617daa5ad6516c14def9e291f2367542c) [#134](https://github.com/npm/bin-links/pull/134) postinstall for dependabot template-oss PR (@hashtagchris) 21 | * [`d949f7d`](https://github.com/npm/bin-links/commit/d949f7de78aa0d58e0cf228f816215678878278c) [#134](https://github.com/npm/bin-links/pull/134) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot]) 22 | 23 | ## [4.0.4](https://github.com/npm/bin-links/compare/v4.0.3...v4.0.4) (2024-05-04) 24 | 25 | ### Bug Fixes 26 | 27 | * [`100a4b7`](https://github.com/npm/bin-links/commit/100a4b73111065aebc5284f5f7060c9665c4279a) [#117](https://github.com/npm/bin-links/pull/117) linting: no-unused-vars (@lukekarrys) 28 | 29 | ### Chores 30 | 31 | * [`e955437`](https://github.com/npm/bin-links/commit/e955437eef356ea8edb344448086a23a8fe38f03) [#117](https://github.com/npm/bin-links/pull/117) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 32 | * [`b602aca`](https://github.com/npm/bin-links/commit/b602acab28889793a96b06dedc1c66d225223999) [#117](https://github.com/npm/bin-links/pull/117) postinstall for dependabot template-oss PR (@lukekarrys) 33 | * [`955cc34`](https://github.com/npm/bin-links/commit/955cc3407ddcb579ef8da2d4247103c68972fb52) [#116](https://github.com/npm/bin-links/pull/116) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot]) 34 | 35 | ## [4.0.3](https://github.com/npm/bin-links/compare/v4.0.2...v4.0.3) (2023-10-12) 36 | 37 | ### Bug Fixes 38 | 39 | * [`af17744`](https://github.com/npm/bin-links/commit/af1774455f0dc342840ebe6b8dd5ee946dcda5e2) [#100](https://github.com/npm/bin-links/pull/100) promisify/cleanup link-mans and simplify regex (#100) (@wraithgar) 40 | 41 | ## [4.0.2](https://github.com/npm/bin-links/compare/v4.0.1...v4.0.2) (2023-07-11) 42 | 43 | ### Bug Fixes 44 | 45 | * [`08f8981`](https://github.com/npm/bin-links/commit/08f898114accd24f70714a6a5b253cc93f91e509) [#80](https://github.com/npm/bin-links/pull/80) don’t try to chmod unlinked files (#80) (@remcohaszing) 46 | 47 | ## [4.0.1](https://github.com/npm/bin-links/compare/v4.0.0...v4.0.1) (2022-10-17) 48 | 49 | ### Dependencies 50 | 51 | * [`cf738fb`](https://github.com/npm/bin-links/commit/cf738fb3ec95539fe7c81f2508ba34f4662e9bc2) [#62](https://github.com/npm/bin-links/pull/62) bump read-cmd-shim from 3.0.1 to 4.0.0 52 | * [`61717bf`](https://github.com/npm/bin-links/commit/61717bfe2f56b71b68febcc10980462b7dac72a0) [#64](https://github.com/npm/bin-links/pull/64) bump write-file-atomic from 4.0.2 to 5.0.0 53 | * [`d26ec29`](https://github.com/npm/bin-links/commit/d26ec2945571fc7f9b27416c0f8de201d0ca0df9) [#61](https://github.com/npm/bin-links/pull/61) bump npm-normalize-package-bin from 2.0.0 to 3.0.0 54 | 55 | ## [4.0.0](https://github.com/npm/bin-links/compare/v3.0.3...v4.0.0) (2022-10-13) 56 | 57 | ### ⚠️ BREAKING CHANGES 58 | 59 | * this module no longer attempts to change file ownership automatically 60 | * `bin-links` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 61 | 62 | ### Features 63 | 64 | * [`4f4c58c`](https://github.com/npm/bin-links/commit/4f4c58cd30acac8e26f76f5e0e534b94f0e353db) [#59](https://github.com/npm/bin-links/pull/59) do not alter file ownership (#59) (@nlf) 65 | * [`36b2668`](https://github.com/npm/bin-links/commit/36b2668aad3495f256b392b2ba9dd41487e72b41) [#52](https://github.com/npm/bin-links/pull/52) postinstall for dependabot template-oss PR (@lukekarrys) 66 | 67 | ## [3.0.3](https://github.com/npm/bin-links/compare/v3.0.2...v3.0.3) (2022-08-23) 68 | 69 | 70 | ### Dependencies 71 | 72 | * bump npm-normalize-package-bin from 1.0.1 to 2.0.0 ([#50](https://github.com/npm/bin-links/issues/50)) ([3ffe1e9](https://github.com/npm/bin-links/commit/3ffe1e9192575bebaf5ec0860fa2f90ca03ba3fe)) 73 | 74 | ## [3.0.2](https://github.com/npm/bin-links/compare/v3.0.1...v3.0.2) (2022-08-11) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * linting ([#48](https://github.com/npm/bin-links/issues/48)) ([163f021](https://github.com/npm/bin-links/commit/163f021115b7d724759ab7bdd878aabc2b5a94dd)) 80 | 81 | ### [3.0.1](https://github.com/npm/bin-links/compare/v3.0.0...v3.0.1) (2022-04-05) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * remove unsafe regex ([5d01244](https://github.com/npm/bin-links/commit/5d01244a178488b3e110b967a81e5c2349316bb3)) 87 | * replace deprecated String.prototype.substr() ([#38](https://github.com/npm/bin-links/issues/38)) ([56dbfa0](https://github.com/npm/bin-links/commit/56dbfa06ee1efc9224fa7e8b8cab71643feff664)) 88 | 89 | 90 | ### Dependencies 91 | 92 | * bump cmd-shim from 4.1.0 to 5.0.0 ([#39](https://github.com/npm/bin-links/issues/39)) ([24a1f3c](https://github.com/npm/bin-links/commit/24a1f3cfb5b98a9e58ff59c0627877a20762a7ed)) 93 | * bump read-cmd-shim from 2.0.0 to 3.0.0 ([#40](https://github.com/npm/bin-links/issues/40)) ([36a652f](https://github.com/npm/bin-links/commit/36a652f50c09c88447893305a8ed9ec2c2f27b85)) 94 | 95 | ## [3.0.0](https://www.github.com/npm/bin-links/compare/v2.3.0...v3.0.0) (2022-01-18) 96 | 97 | 98 | ### ⚠ BREAKING CHANGES 99 | 100 | * This drops support for node10 and non-LTS versions of node12 and node14 101 | 102 | ### Bug Fixes 103 | 104 | * template-oss ([#30](https://www.github.com/npm/bin-links/issues/30)) ([3a50664](https://www.github.com/npm/bin-links/commit/3a5066464dc3497be7aaa39a19444494c41bc9a9)) 105 | 106 | 107 | ### dependencies 108 | 109 | * write-file-atomic@4.0.0 ([#32](https://www.github.com/npm/bin-links/issues/32)) ([788d0ee](https://www.github.com/npm/bin-links/commit/788d0ee94841b20651d300acb4b1ca607192efcd)) 110 | 111 | ## 2.0.0 112 | 113 | * Rewrite to promisify and remove dependence on gentle-fs 114 | 115 | <a name="1.1.7"></a> 116 | ## [1.1.7](https://github.com/npm/bin-links/compare/v1.1.6...v1.1.7) (2019-12-26) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * resolve folder that is passed in ([0bbd303](https://github.com/npm/bin-links/commit/0bbd303)) 122 | 123 | 124 | 125 | <a name="1.1.6"></a> 126 | ## [1.1.6](https://github.com/npm/bin-links/compare/v1.1.5...v1.1.6) (2019-12-11) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * prevent improper clobbering of man/bin links ([642cd18](https://github.com/npm/bin-links/commit/642cd18)), closes [#11](https://github.com/npm/bin-links/issues/11) [#12](https://github.com/npm/bin-links/issues/12) 132 | 133 | 134 | 135 | <a name="1.1.5"></a> 136 | ## [1.1.5](https://github.com/npm/bin-links/compare/v1.1.4...v1.1.5) (2019-12-10) 137 | 138 | 139 | ### Bug Fixes 140 | 141 | * don't filter out ./ man references ([b3cfd2e](https://github.com/npm/bin-links/commit/b3cfd2e)) 142 | 143 | 144 | 145 | <a name="1.1.4"></a> 146 | ## [1.1.4](https://github.com/npm/bin-links/compare/v1.1.3...v1.1.4) (2019-12-09) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * sanitize and validate bin and man link targets ([25a34f9](https://github.com/npm/bin-links/commit/25a34f9)) 152 | 153 | 154 | 155 | <a name="1.1.3"></a> 156 | ## [1.1.3](https://github.com/npm/bin-links/compare/v1.1.2...v1.1.3) (2019-08-14) 157 | 158 | 159 | 160 | <a name="1.1.2"></a> 161 | ## [1.1.2](https://github.com/npm/bin-links/compare/v1.1.1...v1.1.2) (2018-03-22) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * **linkMans:** return the promise! ([5eccc7f](https://github.com/npm/bin-links/commit/5eccc7f)) 167 | 168 | 169 | 170 | <a name="1.1.1"></a> 171 | ## [1.1.1](https://github.com/npm/bin-links/compare/v1.1.0...v1.1.1) (2018-03-07) 172 | 173 | 174 | ### Bug Fixes 175 | 176 | * **shebangs:** only convert CR when doing CRLF -> LF ([#2](https://github.com/npm/bin-links/issues/2)) ([43bf857](https://github.com/npm/bin-links/commit/43bf857)) 177 | 178 | 179 | 180 | <a name="1.1.0"></a> 181 | # [1.1.0](https://github.com/npm/bin-links/compare/v1.0.0...v1.1.0) (2017-11-20) 182 | 183 | 184 | ### Features 185 | 186 | * **dos2unix:** Log the fact line endings are being changed upon install. ([e9f8a6f](https://github.com/npm/bin-links/commit/e9f8a6f)) 187 | 188 | 189 | 190 | <a name="1.0.0"></a> 191 | # 1.0.0 (2017-10-07) 192 | 193 | 194 | ### Features 195 | 196 | * **import:** initial extraction from npm ([6ed0bfb](https://github.com/npm/bin-links/commit/6ed0bfb)) 197 | * **initial commit:** README ([3fc9cf0](https://github.com/npm/bin-links/commit/3fc9cf0)) 198 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) npm, Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bin-links [![npm version](https://img.shields.io/npm/v/bin-links.svg)](https://npm.im/bin-links) [![license](https://img.shields.io/npm/l/bin-links.svg)](https://npm.im/bin-links) [![Travis](https://img.shields.io/travis/npm/bin-links.svg)](https://travis-ci.org/npm/bin-links) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/npm/bin-links?svg=true)](https://ci.appveyor.com/project/npm/bin-links) [![Coverage Status](https://coveralls.io/repos/github/npm/bin-links/badge.svg?branch=latest)](https://coveralls.io/github/npm/bin-links?branch=latest) 2 | 3 | [`bin-links`](https://github.com/npm/bin-links) is a standalone library that links 4 | binaries and man pages for JavaScript packages 5 | 6 | ## Install 7 | 8 | `$ npm install bin-links` 9 | 10 | ## Table of Contents 11 | 12 | * [Example](#example) 13 | * [Features](#features) 14 | * [Contributing](#contributing) 15 | * [API](#api) 16 | * [`binLinks`](#binLinks) 17 | * [`binLinks.getPaths()`](#getPaths) 18 | * [`binLinks.checkBins()`](#checkBins) 19 | 20 | ### Example 21 | 22 | ```javascript 23 | const binLinks = require('bin-links') 24 | const readPackageJson = require('read-package-json-fast') 25 | binLinks({ 26 | path: '/path/to/node_modules/some-package', 27 | pkg: readPackageJson('/path/to/node_modules/some-package/package.json'), 28 | 29 | // true if it's a global install, false for local. default: false 30 | global: true, 31 | 32 | // true if it's the top level package being installed, false otherwise 33 | top: true, 34 | 35 | // true if you'd like to recklessly overwrite files. 36 | force: true, 37 | }) 38 | ``` 39 | 40 | ### Features 41 | 42 | * Links bin files listed under the `bin` property of pkg to the 43 | `node_modules/.bin` directory of the installing environment. (Or 44 | `${prefix}/bin` for top level global packages on unix, and `${prefix}` 45 | for top level global packages on Windows.) 46 | * Links man files listed under the `man` property of pkg to the share/man 47 | directory. (This is only done for top-level global packages on Unix 48 | systems.) 49 | 50 | ### Contributing 51 | 52 | The npm team enthusiastically welcomes contributions and project participation! 53 | There's a bunch of things you can do if you want to contribute! The [Contributor 54 | Guide](CONTRIBUTING.md) has all the information you need for everything from 55 | reporting bugs to contributing entire new features. Please don't hesitate to 56 | jump in if you'd like to, or even ask us questions if something isn't clear. 57 | 58 | ### API 59 | 60 | #### <a name="binLinks"></a> `> binLinks({path, pkg, force, global, top})` 61 | 62 | Returns a Promise that resolves when the requisite things have been linked. 63 | 64 | #### <a name="getPaths"></a> `> binLinks.getPaths({path, pkg, global, top })` 65 | 66 | Returns an array of all the paths of links and shims that _might_ be 67 | created (assuming that they exist!) for the package at the specified path. 68 | 69 | Does not touch the filesystem. 70 | 71 | #### <a name="checkBins"></a> `> binLinks.checkBins({path, pkg, global, top, force })` 72 | 73 | Checks if there are any conflicting bins which will prevent the linking of 74 | bins for the given package. Returns a Promise that resolves with no value 75 | if the way is clear, and rejects if there's something in the way. 76 | 77 | Always returns successfully if `global` or `top` are false, or if `force` 78 | is true, or if the `pkg` object does not contain any bins to link. 79 | 80 | Note that changes to the file system _may_ still cause the `binLinks` 81 | method to fail even if this method succeeds. Does not check for 82 | conflicting `man` links. 83 | 84 | Reads from the filesystem but does not make any changes. 85 | 86 | ##### Example 87 | 88 | ```javascript 89 | binLinks({path, pkg, force, global, top}).then(() => console.log('bins linked!')) 90 | ``` 91 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /lib/bin-target.js: -------------------------------------------------------------------------------- 1 | const isWindows = require('./is-windows.js') 2 | const getPrefix = require('./get-prefix.js') 3 | const getNodeModules = require('./get-node-modules.js') 4 | const { dirname } = require('path') 5 | 6 | module.exports = ({ top, path }) => 7 | !top ? getNodeModules(path) + '/.bin' 8 | : isWindows ? getPrefix(path) 9 | : dirname(getPrefix(path)) + '/bin' 10 | -------------------------------------------------------------------------------- /lib/check-bin.js: -------------------------------------------------------------------------------- 1 | // check to see if a bin is allowed to be overwritten 2 | // either rejects or resolves to nothing. return value not relevant. 3 | const isWindows = require('./is-windows.js') 4 | const binTarget = require('./bin-target.js') 5 | const { resolve, dirname } = require('path') 6 | const readCmdShim = require('read-cmd-shim') 7 | const { readlink } = require('fs/promises') 8 | 9 | const checkBin = async ({ bin, path, top, global, force }) => { 10 | // always ok to clobber when forced 11 | // always ok to clobber local bins, or when forced 12 | if (force || !global || !top) { 13 | return 14 | } 15 | 16 | // ok, need to make sure, then 17 | const target = resolve(binTarget({ path, top }), bin) 18 | path = resolve(path) 19 | return isWindows ? checkShim({ target, path }) : checkLink({ target, path }) 20 | } 21 | 22 | // only enoent is allowed. anything else is a problem. 23 | const handleReadLinkError = async ({ er, target }) => 24 | er.code === 'ENOENT' ? null 25 | : failEEXIST({ target }) 26 | 27 | const checkLink = async ({ target, path }) => { 28 | const current = await readlink(target) 29 | .catch(er => handleReadLinkError({ er, target })) 30 | 31 | if (!current) { 32 | return 33 | } 34 | 35 | const resolved = resolve(dirname(target), current) 36 | 37 | if (resolved.toLowerCase().indexOf(path.toLowerCase()) !== 0) { 38 | return failEEXIST({ target }) 39 | } 40 | } 41 | 42 | const handleReadCmdShimError = ({ er, target }) => 43 | er.code === 'ENOENT' ? null 44 | : failEEXIST({ target }) 45 | 46 | const failEEXIST = ({ target }) => 47 | Promise.reject(Object.assign(new Error('EEXIST: file already exists'), { 48 | path: target, 49 | code: 'EEXIST', 50 | })) 51 | 52 | const checkShim = async ({ target, path }) => { 53 | const shims = [ 54 | target, 55 | target + '.cmd', 56 | target + '.ps1', 57 | ] 58 | await Promise.all(shims.map(async shim => { 59 | const current = await readCmdShim(shim) 60 | .catch(er => handleReadCmdShimError({ er, target: shim })) 61 | 62 | if (!current) { 63 | return 64 | } 65 | 66 | const resolved = resolve(dirname(shim), current.replace(/\\/g, '/')) 67 | 68 | if (resolved.toLowerCase().indexOf(path.toLowerCase()) !== 0) { 69 | return failEEXIST({ target: shim }) 70 | } 71 | })) 72 | } 73 | 74 | module.exports = checkBin 75 | -------------------------------------------------------------------------------- /lib/check-bins.js: -------------------------------------------------------------------------------- 1 | const checkBin = require('./check-bin.js') 2 | const normalize = require('npm-normalize-package-bin') 3 | const checkBins = async ({ pkg, path, top, global, force }) => { 4 | // always ok to clobber when forced 5 | // always ok to clobber local bins, or when forced 6 | if (force || !global || !top) { 7 | return 8 | } 9 | 10 | pkg = normalize(pkg) 11 | if (!pkg.bin) { 12 | return 13 | } 14 | 15 | await Promise.all(Object.keys(pkg.bin) 16 | .map(bin => checkBin({ bin, path, top, global, force }))) 17 | } 18 | module.exports = checkBins 19 | -------------------------------------------------------------------------------- /lib/fix-bin.js: -------------------------------------------------------------------------------- 1 | // make sure that bins are executable, and that they don't have 2 | // windows line-endings on the hashbang line. 3 | const { 4 | chmod, 5 | open, 6 | readFile, 7 | } = require('fs/promises') 8 | 9 | const execMode = 0o777 & (~process.umask()) 10 | 11 | const writeFileAtomic = require('write-file-atomic') 12 | 13 | const isWindowsHashBang = buf => 14 | buf[0] === '#'.charCodeAt(0) && 15 | buf[1] === '!'.charCodeAt(0) && 16 | /^#![^\n]+\r\n/.test(buf.toString()) 17 | 18 | const isWindowsHashbangFile = file => { 19 | const FALSE = () => false 20 | return open(file, 'r').then(fh => { 21 | const buf = Buffer.alloc(2048) 22 | return fh.read(buf, 0, 2048, 0) 23 | .then( 24 | () => { 25 | const isWHB = isWindowsHashBang(buf) 26 | return fh.close().then(() => isWHB, () => isWHB) 27 | }, 28 | // don't leak FD if read() fails 29 | () => fh.close().then(FALSE, FALSE) 30 | ) 31 | }, FALSE) 32 | } 33 | 34 | const dos2Unix = file => 35 | readFile(file, 'utf8').then(content => 36 | writeFileAtomic(file, content.replace(/^(#![^\n]+)\r\n/, '$1\n'))) 37 | 38 | const fixBin = (file, mode = execMode) => chmod(file, mode) 39 | .then(() => isWindowsHashbangFile(file)) 40 | .then(isWHB => isWHB ? dos2Unix(file) : null) 41 | 42 | module.exports = fixBin 43 | -------------------------------------------------------------------------------- /lib/get-node-modules.js: -------------------------------------------------------------------------------- 1 | // we know it's global and/or not top, so the path has to be 2 | // {prefix}/node_modules/{name}. Can't rely on pkg.name, because 3 | // it might be installed as an alias. 4 | 5 | const { dirname, basename } = require('path') 6 | // this gets called a lot and can't change, so memoize it 7 | const memo = new Map() 8 | module.exports = path => { 9 | if (memo.has(path)) { 10 | return memo.get(path) 11 | } 12 | 13 | const scopeOrNm = dirname(path) 14 | const nm = basename(scopeOrNm) === 'node_modules' ? scopeOrNm 15 | : dirname(scopeOrNm) 16 | 17 | memo.set(path, nm) 18 | return nm 19 | } 20 | -------------------------------------------------------------------------------- /lib/get-paths.js: -------------------------------------------------------------------------------- 1 | // get all the paths that are (or might be) installed for a given pkg 2 | // There's no guarantee that all of these will be installed, but if they 3 | // are present, then we can assume that they're associated. 4 | const binTarget = require('./bin-target.js') 5 | const manTarget = require('./man-target.js') 6 | const { resolve, basename, extname } = require('path') 7 | const isWindows = require('./is-windows.js') 8 | module.exports = ({ path, pkg, global, top }) => { 9 | if (top && !global) { 10 | return [] 11 | } 12 | 13 | const binSet = [] 14 | const binTarg = binTarget({ path, top }) 15 | if (pkg.bin) { 16 | for (const bin of Object.keys(pkg.bin)) { 17 | const b = resolve(binTarg, bin) 18 | binSet.push(b) 19 | if (isWindows) { 20 | binSet.push(b + '.cmd') 21 | binSet.push(b + '.ps1') 22 | } 23 | } 24 | } 25 | 26 | const manTarg = manTarget({ path, top }) 27 | const manSet = [] 28 | if (manTarg && pkg.man && Array.isArray(pkg.man) && pkg.man.length) { 29 | for (const man of pkg.man) { 30 | if (!/.\.[0-9]+(\.gz)?$/.test(man)) { 31 | return binSet 32 | } 33 | 34 | const section = extname(basename(man, '.gz')).slice(1) 35 | const base = basename(man) 36 | 37 | manSet.push(resolve(manTarg, 'man' + section, base)) 38 | } 39 | } 40 | 41 | return manSet.length ? [...binSet, ...manSet] : binSet 42 | } 43 | -------------------------------------------------------------------------------- /lib/get-prefix.js: -------------------------------------------------------------------------------- 1 | const { dirname } = require('path') 2 | const getNodeModules = require('./get-node-modules.js') 3 | module.exports = path => dirname(getNodeModules(path)) 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const linkBins = require('./link-bins.js') 2 | const linkMans = require('./link-mans.js') 3 | 4 | const binLinks = opts => { 5 | const { path, pkg, force, global, top } = opts 6 | // global top pkgs on windows get bins installed in {prefix}, and no mans 7 | // 8 | // unix global top pkgs get their bins installed in {prefix}/bin, 9 | // and mans in {prefix}/share/man 10 | // 11 | // non-top pkgs get their bins installed in {prefix}/node_modules/.bin, 12 | // and do not install mans 13 | // 14 | // non-global top pkgs don't have any bins or mans linked. From here on 15 | // out, if it's top, we know that it's global, so no need to pass that 16 | // option further down the stack. 17 | if (top && !global) { 18 | return Promise.resolve() 19 | } 20 | 21 | return Promise.all([ 22 | // allow clobbering within the local node_modules/.bin folder. 23 | // only global bins are protected in this way, or else it is 24 | // yet another vector for excessive dependency conflicts. 25 | linkBins({ path, pkg, top, force: force || !top }), 26 | linkMans({ path, pkg, top, force }), 27 | ]) 28 | } 29 | 30 | const shimBin = require('./shim-bin.js') 31 | const linkGently = require('./link-gently.js') 32 | const resetSeen = () => { 33 | shimBin.resetSeen() 34 | linkGently.resetSeen() 35 | } 36 | 37 | const checkBins = require('./check-bins.js') 38 | const getPaths = require('./get-paths.js') 39 | 40 | module.exports = Object.assign(binLinks, { 41 | checkBins, 42 | resetSeen, 43 | getPaths, 44 | }) 45 | -------------------------------------------------------------------------------- /lib/is-windows.js: -------------------------------------------------------------------------------- 1 | const platform = process.env.__TESTING_BIN_LINKS_PLATFORM__ || process.platform 2 | module.exports = platform === 'win32' 3 | -------------------------------------------------------------------------------- /lib/link-bin.js: -------------------------------------------------------------------------------- 1 | const linkGently = require('./link-gently.js') 2 | const fixBin = require('./fix-bin.js') 3 | 4 | // linking bins is simple. just symlink, and if we linked it, fix the bin up 5 | const linkBin = ({ path, to, from, absFrom, force }) => 6 | linkGently({ path, to, from, absFrom, force }) 7 | .then(linked => linked && fixBin(absFrom)) 8 | 9 | module.exports = linkBin 10 | -------------------------------------------------------------------------------- /lib/link-bins.js: -------------------------------------------------------------------------------- 1 | const isWindows = require('./is-windows.js') 2 | const binTarget = require('./bin-target.js') 3 | const { dirname, resolve, relative } = require('path') 4 | const linkBin = isWindows ? require('./shim-bin.js') : require('./link-bin.js') 5 | const normalize = require('npm-normalize-package-bin') 6 | 7 | const linkBins = ({ path, pkg, top, force }) => { 8 | pkg = normalize(pkg) 9 | if (!pkg.bin) { 10 | return Promise.resolve([]) 11 | } 12 | const promises = [] 13 | const target = binTarget({ path, top }) 14 | for (const [key, val] of Object.entries(pkg.bin)) { 15 | const to = resolve(target, key) 16 | const absFrom = resolve(path, val) 17 | const from = relative(dirname(to), absFrom) 18 | promises.push(linkBin({ path, from, to, absFrom, force })) 19 | } 20 | return Promise.all(promises) 21 | } 22 | 23 | module.exports = linkBins 24 | -------------------------------------------------------------------------------- /lib/link-gently.js: -------------------------------------------------------------------------------- 1 | // if the thing isn't there, skip it 2 | // if there's a non-symlink there already, eexist 3 | // if there's a symlink already, pointing somewhere else, eexist 4 | // if there's a symlink already, pointing into our pkg, remove it first 5 | // then create the symlink 6 | 7 | const { resolve, dirname } = require('path') 8 | const { lstat, mkdir, readlink, rm, symlink } = require('fs/promises') 9 | const { log } = require('proc-log') 10 | const throwSignificant = er => { 11 | if (er.code === 'ENOENT') { 12 | return 13 | } 14 | if (er.code === 'EACCES') { 15 | log.warn('error adding file', er.message) 16 | return 17 | } 18 | throw er 19 | } 20 | 21 | const rmOpts = { 22 | recursive: true, 23 | force: true, 24 | } 25 | 26 | // even in --force mode, we never create a link over a link we've 27 | // already created. you can have multiple packages in a tree trying 28 | // to contend for the same bin, or the same manpage listed multiple times, 29 | // which creates a race condition and nondeterminism. 30 | const seen = new Set() 31 | 32 | const SKIP = Symbol('skip - missing or already installed') 33 | const CLOBBER = Symbol('clobber - ours or in forceful mode') 34 | 35 | const linkGently = async ({ path, to, from, absFrom, force }) => { 36 | if (seen.has(to)) { 37 | return false 38 | } 39 | seen.add(to) 40 | 41 | // if the script or manpage isn't there, just ignore it. 42 | // this arguably *should* be an install error of some sort, 43 | // or at least a warning, but npm has always behaved this 44 | // way in the past, so it'd be a breaking change 45 | return Promise.all([ 46 | lstat(absFrom).catch(throwSignificant), 47 | lstat(to).catch(throwSignificant), 48 | ]).then(([stFrom, stTo]) => { 49 | // not present in package, skip it 50 | if (!stFrom) { 51 | return SKIP 52 | } 53 | 54 | // exists! maybe clobber if we can 55 | if (stTo) { 56 | if (!stTo.isSymbolicLink()) { 57 | return force && rm(to, rmOpts).then(() => CLOBBER) 58 | } 59 | 60 | return readlink(to).then(target => { 61 | if (target === from) { 62 | return SKIP 63 | } // skip it, already set up like we want it. 64 | 65 | target = resolve(dirname(to), target) 66 | if (target.indexOf(path) === 0 || force) { 67 | return rm(to, rmOpts).then(() => CLOBBER) 68 | } 69 | // neither skip nor clobber 70 | return false 71 | }) 72 | } else { 73 | // doesn't exist, dir might not either 74 | return mkdir(dirname(to), { recursive: true }) 75 | } 76 | }) 77 | .then(skipOrClobber => { 78 | if (skipOrClobber === SKIP) { 79 | return false 80 | } 81 | return symlink(from, to, 'file').catch(er => { 82 | if (skipOrClobber === CLOBBER || force) { 83 | return rm(to, rmOpts).then(() => symlink(from, to, 'file')) 84 | } 85 | throw er 86 | }).then(() => true) 87 | }) 88 | } 89 | 90 | const resetSeen = () => { 91 | for (const p of seen) { 92 | seen.delete(p) 93 | } 94 | } 95 | 96 | module.exports = Object.assign(linkGently, { resetSeen }) 97 | -------------------------------------------------------------------------------- /lib/link-mans.js: -------------------------------------------------------------------------------- 1 | const { dirname, relative, join, resolve, basename } = require('path') 2 | const linkGently = require('./link-gently.js') 3 | const manTarget = require('./man-target.js') 4 | 5 | const linkMans = async ({ path, pkg, top, force }) => { 6 | const target = manTarget({ path, top }) 7 | if (!target || !Array.isArray(pkg?.man) || !pkg.man.length) { 8 | return [] 9 | } 10 | 11 | const links = [] 12 | // `new Set` to filter out duplicates 13 | for (let man of new Set(pkg.man)) { 14 | if (!man || typeof man !== 'string') { 15 | continue 16 | } 17 | // break any links to c:\\blah or /foo/blah or ../blah 18 | man = join('/', man).replace(/\\|:/g, '/').slice(1) 19 | const parseMan = man.match(/\.([0-9]+)(\.gz)?$/) 20 | if (!parseMan) { 21 | throw Object.assign(new Error('invalid man entry name\n' + 22 | 'Man files must end with a number, ' + 23 | 'and optionally a .gz suffix if they are compressed.' 24 | ), { 25 | code: 'EBADMAN', 26 | path, 27 | pkgid: pkg._id, 28 | man, 29 | }) 30 | } 31 | 32 | const section = parseMan[1] 33 | const base = basename(man) 34 | const absFrom = resolve(path, man) 35 | /* istanbul ignore if - that unpossible */ 36 | if (absFrom.indexOf(path) !== 0) { 37 | throw Object.assign(new Error('invalid man entry'), { 38 | code: 'EBADMAN', 39 | path, 40 | pkgid: pkg._id, 41 | man, 42 | }) 43 | } 44 | 45 | const to = resolve(target, 'man' + section, base) 46 | const from = relative(dirname(to), absFrom) 47 | 48 | links.push(linkGently({ from, to, path, absFrom, force })) 49 | } 50 | return Promise.all(links) 51 | } 52 | 53 | module.exports = linkMans 54 | -------------------------------------------------------------------------------- /lib/man-target.js: -------------------------------------------------------------------------------- 1 | const isWindows = require('./is-windows.js') 2 | const getPrefix = require('./get-prefix.js') 3 | const { dirname } = require('path') 4 | 5 | module.exports = ({ top, path }) => !top || isWindows ? null 6 | : dirname(getPrefix(path)) + '/share/man' 7 | -------------------------------------------------------------------------------- /lib/shim-bin.js: -------------------------------------------------------------------------------- 1 | const { resolve, dirname } = require('path') 2 | const { lstat } = require('fs/promises') 3 | const throwNonEnoent = er => { 4 | if (er.code !== 'ENOENT') { 5 | throw er 6 | } 7 | } 8 | 9 | const cmdShim = require('cmd-shim') 10 | const readCmdShim = require('read-cmd-shim') 11 | 12 | const fixBin = require('./fix-bin.js') 13 | 14 | // even in --force mode, we never create a shim over a shim we've 15 | // already created. you can have multiple packages in a tree trying 16 | // to contend for the same bin, which creates a race condition and 17 | // nondeterminism. 18 | const seen = new Set() 19 | 20 | const failEEXIST = ({ to, from }) => 21 | Promise.reject(Object.assign(new Error('EEXIST: file already exists'), { 22 | path: to, 23 | dest: from, 24 | code: 'EEXIST', 25 | })) 26 | 27 | const handleReadCmdShimError = ({ er, from, to }) => 28 | er.code === 'ENOENT' ? null 29 | : er.code === 'ENOTASHIM' ? failEEXIST({ from, to }) 30 | : Promise.reject(er) 31 | 32 | const SKIP = Symbol('skip - missing or already installed') 33 | const shimBin = ({ path, to, from, absFrom, force }) => { 34 | const shims = [ 35 | to, 36 | to + '.cmd', 37 | to + '.ps1', 38 | ] 39 | 40 | for (const shim of shims) { 41 | if (seen.has(shim)) { 42 | return true 43 | } 44 | seen.add(shim) 45 | } 46 | 47 | return Promise.all([ 48 | ...shims, 49 | absFrom, 50 | ].map(f => lstat(f).catch(throwNonEnoent))).then((stats) => { 51 | const [, , , stFrom] = stats 52 | if (!stFrom) { 53 | return SKIP 54 | } 55 | 56 | if (force) { 57 | return false 58 | } 59 | 60 | return Promise.all(shims.map((s, i) => [s, stats[i]]).map(([s, st]) => { 61 | if (!st) { 62 | return false 63 | } 64 | return readCmdShim(s) 65 | .then(target => { 66 | target = resolve(dirname(to), target) 67 | if (target.indexOf(resolve(path)) !== 0) { 68 | return failEEXIST({ from, to, path }) 69 | } 70 | return false 71 | }, er => handleReadCmdShimError({ er, from, to })) 72 | })) 73 | }) 74 | .then(skip => skip !== SKIP && doShim(absFrom, to)) 75 | } 76 | 77 | const doShim = (absFrom, to) => 78 | cmdShim(absFrom, to).then(() => fixBin(absFrom)) 79 | 80 | const resetSeen = () => { 81 | for (const p of seen) { 82 | seen.delete(p) 83 | } 84 | } 85 | 86 | module.exports = Object.assign(shimBin, { resetSeen }) 87 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | const { basename } = require('path') 2 | const map = base => base === 'index.js' ? 'index.js' : `lib/${base}` 3 | module.exports = test => map(basename(test)) 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bin-links", 3 | "version": "5.0.0", 4 | "description": "JavaScript package binary linker", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "snap": "tap", 8 | "test": "tap", 9 | "lint": "npm run eslint", 10 | "postlint": "template-oss-check", 11 | "lintfix": "npm run eslint -- --fix", 12 | "posttest": "npm run lint", 13 | "template-oss-apply": "template-oss-apply --force", 14 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/npm/bin-links.git" 19 | }, 20 | "keywords": [ 21 | "npm", 22 | "link", 23 | "bins" 24 | ], 25 | "license": "ISC", 26 | "dependencies": { 27 | "cmd-shim": "^7.0.0", 28 | "npm-normalize-package-bin": "^4.0.0", 29 | "proc-log": "^5.0.0", 30 | "read-cmd-shim": "^5.0.0", 31 | "write-file-atomic": "^6.0.0" 32 | }, 33 | "devDependencies": { 34 | "@npmcli/eslint-config": "^5.0.0", 35 | "@npmcli/template-oss": "4.24.3", 36 | "require-inject": "^1.4.4", 37 | "tap": "^16.0.1" 38 | }, 39 | "tap": { 40 | "check-coverage": true, 41 | "coverage-map": "map.js", 42 | "nyc-arg": [ 43 | "--exclude", 44 | "tap-snapshots/**" 45 | ] 46 | }, 47 | "files": [ 48 | "bin/", 49 | "lib/" 50 | ], 51 | "engines": { 52 | "node": "^18.17.0 || >=20.5.0" 53 | }, 54 | "author": "GitHub Inc.", 55 | "templateOSS": { 56 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 57 | "windowsCI": false, 58 | "version": "4.24.3", 59 | "publish": true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release ${version}", 3 | "pull-request-title-pattern": "chore: release${component} ${version}", 4 | "changelog-sections": [ 5 | { 6 | "type": "feat", 7 | "section": "Features", 8 | "hidden": false 9 | }, 10 | { 11 | "type": "fix", 12 | "section": "Bug Fixes", 13 | "hidden": false 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation", 18 | "hidden": false 19 | }, 20 | { 21 | "type": "deps", 22 | "section": "Dependencies", 23 | "hidden": false 24 | }, 25 | { 26 | "type": "chore", 27 | "section": "Chores", 28 | "hidden": true 29 | } 30 | ], 31 | "packages": { 32 | ".": { 33 | "package-name": "" 34 | } 35 | }, 36 | "prerelease-type": "pre.0" 37 | } 38 | -------------------------------------------------------------------------------- /tap-snapshots/test/bin-target.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/bin-target.js TAP posix > not top 1`] = ` 9 | /path/to/node_modules/.bin 10 | ` 11 | 12 | exports[`test/bin-target.js TAP posix > top (and thus global) 1`] = ` 13 | /path/bin 14 | ` 15 | 16 | exports[`test/bin-target.js TAP win32 > not top 1`] = ` 17 | /path/to/node_modules/.bin 18 | ` 19 | 20 | exports[`test/bin-target.js TAP win32 > top (and thus global) 1`] = ` 21 | /path/to 22 | ` 23 | -------------------------------------------------------------------------------- /tap-snapshots/test/get-paths.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/get-paths.js TAP posix global nested badman > scoped package 1`] = ` 9 | Array [] 10 | ` 11 | 12 | exports[`test/get-paths.js TAP posix global nested badman > unscoped package 1`] = ` 13 | Array [] 14 | ` 15 | 16 | exports[`test/get-paths.js TAP posix global nested both > scoped package 1`] = ` 17 | Array [ 18 | "/usr/local/lib/node_modules/xyz/node_modules/.bin/foo", 19 | ] 20 | ` 21 | 22 | exports[`test/get-paths.js TAP posix global nested both > unscoped package 1`] = ` 23 | Array [ 24 | "/usr/local/lib/node_modules/xyz/node_modules/.bin/foo", 25 | ] 26 | ` 27 | 28 | exports[`test/get-paths.js TAP posix global nested nobin > scoped package 1`] = ` 29 | Array [] 30 | ` 31 | 32 | exports[`test/get-paths.js TAP posix global nested nobin > unscoped package 1`] = ` 33 | Array [] 34 | ` 35 | 36 | exports[`test/get-paths.js TAP posix global top badman > scoped package 1`] = ` 37 | Array [] 38 | ` 39 | 40 | exports[`test/get-paths.js TAP posix global top badman > unscoped package 1`] = ` 41 | Array [] 42 | ` 43 | 44 | exports[`test/get-paths.js TAP posix global top both > scoped package 1`] = ` 45 | Array [ 46 | "/usr/local/bin/foo", 47 | "/usr/local/share/man/man1/foo.1.gz", 48 | ] 49 | ` 50 | 51 | exports[`test/get-paths.js TAP posix global top both > unscoped package 1`] = ` 52 | Array [ 53 | "/usr/local/bin/foo", 54 | "/usr/local/share/man/man1/foo.1.gz", 55 | ] 56 | ` 57 | 58 | exports[`test/get-paths.js TAP posix global top nobin > scoped package 1`] = ` 59 | Array [ 60 | "/usr/local/share/man/man1/foo.1.gz", 61 | ] 62 | ` 63 | 64 | exports[`test/get-paths.js TAP posix global top nobin > unscoped package 1`] = ` 65 | Array [ 66 | "/usr/local/share/man/man1/foo.1.gz", 67 | ] 68 | ` 69 | 70 | exports[`test/get-paths.js TAP posix local nested badman > scoped package 1`] = ` 71 | Array [] 72 | ` 73 | 74 | exports[`test/get-paths.js TAP posix local nested badman > unscoped package 1`] = ` 75 | Array [] 76 | ` 77 | 78 | exports[`test/get-paths.js TAP posix local nested both > scoped package 1`] = ` 79 | Array [ 80 | "/path/to/project/node_modules/.bin/foo", 81 | ] 82 | ` 83 | 84 | exports[`test/get-paths.js TAP posix local nested both > unscoped package 1`] = ` 85 | Array [ 86 | "/path/to/project/node_modules/.bin/foo", 87 | ] 88 | ` 89 | 90 | exports[`test/get-paths.js TAP posix local nested nobin > scoped package 1`] = ` 91 | Array [] 92 | ` 93 | 94 | exports[`test/get-paths.js TAP posix local nested nobin > unscoped package 1`] = ` 95 | Array [] 96 | ` 97 | 98 | exports[`test/get-paths.js TAP posix local top badman > scoped package 1`] = ` 99 | Array [] 100 | ` 101 | 102 | exports[`test/get-paths.js TAP posix local top badman > unscoped package 1`] = ` 103 | Array [] 104 | ` 105 | 106 | exports[`test/get-paths.js TAP posix local top both > scoped package 1`] = ` 107 | Array [] 108 | ` 109 | 110 | exports[`test/get-paths.js TAP posix local top both > unscoped package 1`] = ` 111 | Array [] 112 | ` 113 | 114 | exports[`test/get-paths.js TAP posix local top nobin > scoped package 1`] = ` 115 | Array [] 116 | ` 117 | 118 | exports[`test/get-paths.js TAP posix local top nobin > unscoped package 1`] = ` 119 | Array [] 120 | ` 121 | 122 | exports[`test/get-paths.js TAP win32 global nested badman > scoped package 1`] = ` 123 | Array [] 124 | ` 125 | 126 | exports[`test/get-paths.js TAP win32 global nested badman > unscoped package 1`] = ` 127 | Array [] 128 | ` 129 | 130 | exports[`test/get-paths.js TAP win32 global nested both > scoped package 1`] = ` 131 | Array [ 132 | "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\.bin\\\\foo", 133 | "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\.bin\\\\foo.cmd", 134 | "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\.bin\\\\foo.ps1", 135 | ] 136 | ` 137 | 138 | exports[`test/get-paths.js TAP win32 global nested both > unscoped package 1`] = ` 139 | Array [ 140 | "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\.bin\\\\foo", 141 | "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\.bin\\\\foo.cmd", 142 | "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\.bin\\\\foo.ps1", 143 | ] 144 | ` 145 | 146 | exports[`test/get-paths.js TAP win32 global nested nobin > scoped package 1`] = ` 147 | Array [] 148 | ` 149 | 150 | exports[`test/get-paths.js TAP win32 global nested nobin > unscoped package 1`] = ` 151 | Array [] 152 | ` 153 | 154 | exports[`test/get-paths.js TAP win32 global top badman > scoped package 1`] = ` 155 | Array [] 156 | ` 157 | 158 | exports[`test/get-paths.js TAP win32 global top badman > unscoped package 1`] = ` 159 | Array [] 160 | ` 161 | 162 | exports[`test/get-paths.js TAP win32 global top both > scoped package 1`] = ` 163 | Array [ 164 | "c:\\\\path\\\\to\\\\prefix\\\\foo", 165 | "c:\\\\path\\\\to\\\\prefix\\\\foo.cmd", 166 | "c:\\\\path\\\\to\\\\prefix\\\\foo.ps1", 167 | ] 168 | ` 169 | 170 | exports[`test/get-paths.js TAP win32 global top both > unscoped package 1`] = ` 171 | Array [ 172 | "c:\\\\path\\\\to\\\\prefix\\\\foo", 173 | "c:\\\\path\\\\to\\\\prefix\\\\foo.cmd", 174 | "c:\\\\path\\\\to\\\\prefix\\\\foo.ps1", 175 | ] 176 | ` 177 | 178 | exports[`test/get-paths.js TAP win32 global top nobin > scoped package 1`] = ` 179 | Array [] 180 | ` 181 | 182 | exports[`test/get-paths.js TAP win32 global top nobin > unscoped package 1`] = ` 183 | Array [] 184 | ` 185 | 186 | exports[`test/get-paths.js TAP win32 local nested badman > scoped package 1`] = ` 187 | Array [] 188 | ` 189 | 190 | exports[`test/get-paths.js TAP win32 local nested badman > unscoped package 1`] = ` 191 | Array [] 192 | ` 193 | 194 | exports[`test/get-paths.js TAP win32 local nested both > scoped package 1`] = ` 195 | Array [ 196 | "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\.bin\\\\foo", 197 | "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\.bin\\\\foo.cmd", 198 | "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\.bin\\\\foo.ps1", 199 | ] 200 | ` 201 | 202 | exports[`test/get-paths.js TAP win32 local nested both > unscoped package 1`] = ` 203 | Array [ 204 | "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\.bin\\\\foo", 205 | "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\.bin\\\\foo.cmd", 206 | "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\.bin\\\\foo.ps1", 207 | ] 208 | ` 209 | 210 | exports[`test/get-paths.js TAP win32 local nested nobin > scoped package 1`] = ` 211 | Array [] 212 | ` 213 | 214 | exports[`test/get-paths.js TAP win32 local nested nobin > unscoped package 1`] = ` 215 | Array [] 216 | ` 217 | 218 | exports[`test/get-paths.js TAP win32 local top badman > scoped package 1`] = ` 219 | Array [] 220 | ` 221 | 222 | exports[`test/get-paths.js TAP win32 local top badman > unscoped package 1`] = ` 223 | Array [] 224 | ` 225 | 226 | exports[`test/get-paths.js TAP win32 local top both > scoped package 1`] = ` 227 | Array [] 228 | ` 229 | 230 | exports[`test/get-paths.js TAP win32 local top both > unscoped package 1`] = ` 231 | Array [] 232 | ` 233 | 234 | exports[`test/get-paths.js TAP win32 local top nobin > scoped package 1`] = ` 235 | Array [] 236 | ` 237 | 238 | exports[`test/get-paths.js TAP win32 local top nobin > unscoped package 1`] = ` 239 | Array [] 240 | ` 241 | -------------------------------------------------------------------------------- /tap-snapshots/test/index.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/index.js TAP posix global nested force=false > scoped pkg 1`] = ` 9 | Array [ 10 | Object { 11 | "force": true, 12 | "path": "/usr/local/lib/node_modules/xyz/node_modules/@foo/bar", 13 | "pkg": Object { 14 | "bin": Object { 15 | "foo": "bar", 16 | }, 17 | "man": Array [ 18 | "foo.1.gz", 19 | ], 20 | }, 21 | "top": false, 22 | }, 23 | null, 24 | ] 25 | ` 26 | 27 | exports[`test/index.js TAP posix global nested force=false > unscoped pkg 1`] = ` 28 | Array [ 29 | Object { 30 | "force": true, 31 | "path": "/usr/local/lib/node_modules/xyz/node_modules/foo", 32 | "pkg": Object { 33 | "bin": Object { 34 | "foo": "bar", 35 | }, 36 | "man": Array [ 37 | "foo.1.gz", 38 | ], 39 | }, 40 | "top": false, 41 | }, 42 | null, 43 | ] 44 | ` 45 | 46 | exports[`test/index.js TAP posix global nested force=true > scoped pkg 1`] = ` 47 | Array [ 48 | Object { 49 | "force": true, 50 | "path": "/usr/local/lib/node_modules/xyz/node_modules/@foo/bar", 51 | "pkg": Object { 52 | "bin": Object { 53 | "foo": "bar", 54 | }, 55 | "man": Array [ 56 | "foo.1.gz", 57 | ], 58 | }, 59 | "top": false, 60 | }, 61 | null, 62 | ] 63 | ` 64 | 65 | exports[`test/index.js TAP posix global nested force=true > unscoped pkg 1`] = ` 66 | Array [ 67 | Object { 68 | "force": true, 69 | "path": "/usr/local/lib/node_modules/xyz/node_modules/foo", 70 | "pkg": Object { 71 | "bin": Object { 72 | "foo": "bar", 73 | }, 74 | "man": Array [ 75 | "foo.1.gz", 76 | ], 77 | }, 78 | "top": false, 79 | }, 80 | null, 81 | ] 82 | ` 83 | 84 | exports[`test/index.js TAP posix global top force=false > scoped pkg 1`] = ` 85 | Array [ 86 | Object { 87 | "force": false, 88 | "path": "/usr/local/lib/node_modules/@foo/bar", 89 | "pkg": Object { 90 | "bin": Object { 91 | "foo": "bar", 92 | }, 93 | "man": Array [ 94 | "foo.1.gz", 95 | ], 96 | }, 97 | "top": true, 98 | }, 99 | Object { 100 | "force": false, 101 | "isWindows": false, 102 | "path": "/usr/local/lib/node_modules/@foo/bar", 103 | "pkg": Object { 104 | "bin": Object { 105 | "foo": "bar", 106 | }, 107 | "man": Array [ 108 | "foo.1.gz", 109 | ], 110 | }, 111 | "top": true, 112 | }, 113 | ] 114 | ` 115 | 116 | exports[`test/index.js TAP posix global top force=false > unscoped pkg 1`] = ` 117 | Array [ 118 | Object { 119 | "force": false, 120 | "path": "/usr/local/lib/node_modules/foo", 121 | "pkg": Object { 122 | "bin": Object { 123 | "foo": "bar", 124 | }, 125 | "man": Array [ 126 | "foo.1.gz", 127 | ], 128 | }, 129 | "top": true, 130 | }, 131 | Object { 132 | "force": false, 133 | "isWindows": false, 134 | "path": "/usr/local/lib/node_modules/foo", 135 | "pkg": Object { 136 | "bin": Object { 137 | "foo": "bar", 138 | }, 139 | "man": Array [ 140 | "foo.1.gz", 141 | ], 142 | }, 143 | "top": true, 144 | }, 145 | ] 146 | ` 147 | 148 | exports[`test/index.js TAP posix global top force=true > scoped pkg 1`] = ` 149 | Array [ 150 | Object { 151 | "force": true, 152 | "path": "/usr/local/lib/node_modules/@foo/bar", 153 | "pkg": Object { 154 | "bin": Object { 155 | "foo": "bar", 156 | }, 157 | "man": Array [ 158 | "foo.1.gz", 159 | ], 160 | }, 161 | "top": true, 162 | }, 163 | Object { 164 | "force": true, 165 | "isWindows": false, 166 | "path": "/usr/local/lib/node_modules/@foo/bar", 167 | "pkg": Object { 168 | "bin": Object { 169 | "foo": "bar", 170 | }, 171 | "man": Array [ 172 | "foo.1.gz", 173 | ], 174 | }, 175 | "top": true, 176 | }, 177 | ] 178 | ` 179 | 180 | exports[`test/index.js TAP posix global top force=true > unscoped pkg 1`] = ` 181 | Array [ 182 | Object { 183 | "force": true, 184 | "path": "/usr/local/lib/node_modules/foo", 185 | "pkg": Object { 186 | "bin": Object { 187 | "foo": "bar", 188 | }, 189 | "man": Array [ 190 | "foo.1.gz", 191 | ], 192 | }, 193 | "top": true, 194 | }, 195 | Object { 196 | "force": true, 197 | "isWindows": false, 198 | "path": "/usr/local/lib/node_modules/foo", 199 | "pkg": Object { 200 | "bin": Object { 201 | "foo": "bar", 202 | }, 203 | "man": Array [ 204 | "foo.1.gz", 205 | ], 206 | }, 207 | "top": true, 208 | }, 209 | ] 210 | ` 211 | 212 | exports[`test/index.js TAP posix local nested force=false > scoped pkg 1`] = ` 213 | Array [ 214 | Object { 215 | "force": true, 216 | "path": "/path/to/project/node_modules/@foo/bar", 217 | "pkg": Object { 218 | "bin": Object { 219 | "foo": "bar", 220 | }, 221 | "man": Array [ 222 | "foo.1.gz", 223 | ], 224 | }, 225 | "top": false, 226 | }, 227 | null, 228 | ] 229 | ` 230 | 231 | exports[`test/index.js TAP posix local nested force=false > unscoped pkg 1`] = ` 232 | Array [ 233 | Object { 234 | "force": true, 235 | "path": "/path/to/project/node_modules/foo", 236 | "pkg": Object { 237 | "bin": Object { 238 | "foo": "bar", 239 | }, 240 | "man": Array [ 241 | "foo.1.gz", 242 | ], 243 | }, 244 | "top": false, 245 | }, 246 | null, 247 | ] 248 | ` 249 | 250 | exports[`test/index.js TAP posix local nested force=true > scoped pkg 1`] = ` 251 | Array [ 252 | Object { 253 | "force": true, 254 | "path": "/path/to/project/node_modules/@foo/bar", 255 | "pkg": Object { 256 | "bin": Object { 257 | "foo": "bar", 258 | }, 259 | "man": Array [ 260 | "foo.1.gz", 261 | ], 262 | }, 263 | "top": false, 264 | }, 265 | null, 266 | ] 267 | ` 268 | 269 | exports[`test/index.js TAP posix local nested force=true > unscoped pkg 1`] = ` 270 | Array [ 271 | Object { 272 | "force": true, 273 | "path": "/path/to/project/node_modules/foo", 274 | "pkg": Object { 275 | "bin": Object { 276 | "foo": "bar", 277 | }, 278 | "man": Array [ 279 | "foo.1.gz", 280 | ], 281 | }, 282 | "top": false, 283 | }, 284 | null, 285 | ] 286 | ` 287 | 288 | exports[`test/index.js TAP posix local top force=false > scoped pkg 1`] = ` 289 | undefined 290 | ` 291 | 292 | exports[`test/index.js TAP posix local top force=false > unscoped pkg 1`] = ` 293 | undefined 294 | ` 295 | 296 | exports[`test/index.js TAP posix local top force=true > scoped pkg 1`] = ` 297 | undefined 298 | ` 299 | 300 | exports[`test/index.js TAP posix local top force=true > unscoped pkg 1`] = ` 301 | undefined 302 | ` 303 | 304 | exports[`test/index.js TAP win32 global nested force=false > scoped pkg 1`] = ` 305 | Array [ 306 | Object { 307 | "force": true, 308 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\@foo/bar", 309 | "pkg": Object { 310 | "bin": Object { 311 | "foo": "bar", 312 | }, 313 | "man": Array [ 314 | "foo.1.gz", 315 | ], 316 | }, 317 | "top": false, 318 | }, 319 | null, 320 | ] 321 | ` 322 | 323 | exports[`test/index.js TAP win32 global nested force=false > unscoped pkg 1`] = ` 324 | Array [ 325 | Object { 326 | "force": true, 327 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\foo", 328 | "pkg": Object { 329 | "bin": Object { 330 | "foo": "bar", 331 | }, 332 | "man": Array [ 333 | "foo.1.gz", 334 | ], 335 | }, 336 | "top": false, 337 | }, 338 | null, 339 | ] 340 | ` 341 | 342 | exports[`test/index.js TAP win32 global nested force=true > scoped pkg 1`] = ` 343 | Array [ 344 | Object { 345 | "force": true, 346 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\@foo/bar", 347 | "pkg": Object { 348 | "bin": Object { 349 | "foo": "bar", 350 | }, 351 | "man": Array [ 352 | "foo.1.gz", 353 | ], 354 | }, 355 | "top": false, 356 | }, 357 | null, 358 | ] 359 | ` 360 | 361 | exports[`test/index.js TAP win32 global nested force=true > unscoped pkg 1`] = ` 362 | Array [ 363 | Object { 364 | "force": true, 365 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\xyz\\\\node_modules\\\\foo", 366 | "pkg": Object { 367 | "bin": Object { 368 | "foo": "bar", 369 | }, 370 | "man": Array [ 371 | "foo.1.gz", 372 | ], 373 | }, 374 | "top": false, 375 | }, 376 | null, 377 | ] 378 | ` 379 | 380 | exports[`test/index.js TAP win32 global top force=false > scoped pkg 1`] = ` 381 | Array [ 382 | Object { 383 | "force": false, 384 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\@foo/bar", 385 | "pkg": Object { 386 | "bin": Object { 387 | "foo": "bar", 388 | }, 389 | "man": Array [ 390 | "foo.1.gz", 391 | ], 392 | }, 393 | "top": true, 394 | }, 395 | null, 396 | ] 397 | ` 398 | 399 | exports[`test/index.js TAP win32 global top force=false > unscoped pkg 1`] = ` 400 | Array [ 401 | Object { 402 | "force": false, 403 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\foo", 404 | "pkg": Object { 405 | "bin": Object { 406 | "foo": "bar", 407 | }, 408 | "man": Array [ 409 | "foo.1.gz", 410 | ], 411 | }, 412 | "top": true, 413 | }, 414 | null, 415 | ] 416 | ` 417 | 418 | exports[`test/index.js TAP win32 global top force=true > scoped pkg 1`] = ` 419 | Array [ 420 | Object { 421 | "force": true, 422 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\@foo/bar", 423 | "pkg": Object { 424 | "bin": Object { 425 | "foo": "bar", 426 | }, 427 | "man": Array [ 428 | "foo.1.gz", 429 | ], 430 | }, 431 | "top": true, 432 | }, 433 | null, 434 | ] 435 | ` 436 | 437 | exports[`test/index.js TAP win32 global top force=true > unscoped pkg 1`] = ` 438 | Array [ 439 | Object { 440 | "force": true, 441 | "path": "c:\\\\path\\\\to\\\\prefix\\\\node_modules\\\\foo", 442 | "pkg": Object { 443 | "bin": Object { 444 | "foo": "bar", 445 | }, 446 | "man": Array [ 447 | "foo.1.gz", 448 | ], 449 | }, 450 | "top": true, 451 | }, 452 | null, 453 | ] 454 | ` 455 | 456 | exports[`test/index.js TAP win32 local nested force=false > scoped pkg 1`] = ` 457 | Array [ 458 | Object { 459 | "force": true, 460 | "path": "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\@foo/bar", 461 | "pkg": Object { 462 | "bin": Object { 463 | "foo": "bar", 464 | }, 465 | "man": Array [ 466 | "foo.1.gz", 467 | ], 468 | }, 469 | "top": false, 470 | }, 471 | null, 472 | ] 473 | ` 474 | 475 | exports[`test/index.js TAP win32 local nested force=false > unscoped pkg 1`] = ` 476 | Array [ 477 | Object { 478 | "force": true, 479 | "path": "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\foo", 480 | "pkg": Object { 481 | "bin": Object { 482 | "foo": "bar", 483 | }, 484 | "man": Array [ 485 | "foo.1.gz", 486 | ], 487 | }, 488 | "top": false, 489 | }, 490 | null, 491 | ] 492 | ` 493 | 494 | exports[`test/index.js TAP win32 local nested force=true > scoped pkg 1`] = ` 495 | Array [ 496 | Object { 497 | "force": true, 498 | "path": "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\@foo/bar", 499 | "pkg": Object { 500 | "bin": Object { 501 | "foo": "bar", 502 | }, 503 | "man": Array [ 504 | "foo.1.gz", 505 | ], 506 | }, 507 | "top": false, 508 | }, 509 | null, 510 | ] 511 | ` 512 | 513 | exports[`test/index.js TAP win32 local nested force=true > unscoped pkg 1`] = ` 514 | Array [ 515 | Object { 516 | "force": true, 517 | "path": "c:\\\\path\\\\to\\\\project\\\\node_modules\\\\foo", 518 | "pkg": Object { 519 | "bin": Object { 520 | "foo": "bar", 521 | }, 522 | "man": Array [ 523 | "foo.1.gz", 524 | ], 525 | }, 526 | "top": false, 527 | }, 528 | null, 529 | ] 530 | ` 531 | 532 | exports[`test/index.js TAP win32 local top force=false > scoped pkg 1`] = ` 533 | undefined 534 | ` 535 | 536 | exports[`test/index.js TAP win32 local top force=false > unscoped pkg 1`] = ` 537 | undefined 538 | ` 539 | 540 | exports[`test/index.js TAP win32 local top force=true > scoped pkg 1`] = ` 541 | undefined 542 | ` 543 | 544 | exports[`test/index.js TAP win32 local top force=true > unscoped pkg 1`] = ` 545 | undefined 546 | ` 547 | -------------------------------------------------------------------------------- /tap-snapshots/test/man-target.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/man-target.js TAP posix > not top 1`] = ` 9 | null 10 | ` 11 | 12 | exports[`test/man-target.js TAP posix > top (and thus global) 1`] = ` 13 | /path/share/man 14 | ` 15 | 16 | exports[`test/man-target.js TAP win32 > not top 1`] = ` 17 | null 18 | ` 19 | 20 | exports[`test/man-target.js TAP win32 > top (and thus global) 1`] = ` 21 | null 22 | ` 23 | -------------------------------------------------------------------------------- /test/bin-target.js: -------------------------------------------------------------------------------- 1 | const requireInject = require('require-inject') 2 | const t = require('tap') 3 | 4 | for (const isWindows of [true, false]) { 5 | t.test(isWindows ? 'win32' : 'posix', t => { 6 | const path = require('path')[isWindows ? 'win32' : 'posix'] 7 | const binTarget = requireInject('../lib/bin-target.js', { 8 | '../lib/is-windows.js': isWindows, 9 | path, 10 | }) 11 | t.matchSnapshot(binTarget({ 12 | path: '/path/to/node_modules/foo', 13 | top: true, 14 | }), 'top (and thus global)') 15 | 16 | t.matchSnapshot(binTarget({ 17 | path: '/path/to/node_modules/foo', 18 | }), 'not top') 19 | 20 | t.end() 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /test/check-bin.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const requireInject = require('require-inject') 3 | 4 | t.test('always ok when forced ', async t => { 5 | const checkBin = requireInject('../lib/check-bin.js') 6 | t.resolves(checkBin({ force: true }), 'ok when forced') 7 | t.resolves(checkBin({ global: false }), 'ok when local') 8 | t.resolves(checkBin({ global: true, top: false }), 'ok when not top') 9 | }) 10 | 11 | t.test('windows', async t => { 12 | const checkBin = requireInject('../lib/check-bin.js', { 13 | '../lib/is-windows.js': true, 14 | }) 15 | const dir = t.testdir({ 16 | foo: `#!/bin/sh 17 | basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") 18 | 19 | case \`uname\` in 20 | *CYGWIN*|*MINGW*|*MSYS*) basedir=\`cygpath -w "$basedir"\`;; 21 | esac 22 | 23 | "$basedir/node_modules/foo" "$@" 24 | exit $? 25 | `, 26 | 'foo.cmd': `@ECHO off 27 | SETLOCAL 28 | CALL :find_dp0 29 | "%dp0%\\node_modules\\foo" %* 30 | ENDLOCAL 31 | EXIT /b %errorlevel% 32 | :find_dp0 33 | SET dp0=%~dp0 34 | EXIT /b 35 | `, 36 | 'foo.ps1': `#!/usr/bin/env pwsh 37 | $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent 38 | 39 | $exe="" 40 | if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { 41 | # Fix case when both the Windows and Linux builds of Node 42 | # are installed in the same directory 43 | $exe=".exe" 44 | } 45 | & "$basedir/node_modules/foo" $args 46 | exit $LASTEXITCODE 47 | `, 48 | notashim: 'this is not a shim', 49 | dir: {}, 50 | }) 51 | 52 | t.rejects(checkBin({ 53 | bin: 'foo', 54 | global: true, 55 | top: true, 56 | path: `${dir}/node_modules/bar`, 57 | }), { code: 'EEXIST' }) 58 | 59 | t.rejects(checkBin({ 60 | bin: 'notashim', 61 | global: true, 62 | top: true, 63 | path: `${dir}/node_modules/bar`, 64 | }), { code: 'EEXIST' }) 65 | 66 | t.rejects(checkBin({ 67 | bin: 'dir', 68 | global: true, 69 | top: true, 70 | path: `${dir}/node_modules/bar`, 71 | }), { code: 'EEXIST' }) 72 | 73 | t.resolves(checkBin({ 74 | bin: 'foo', 75 | global: true, 76 | top: true, 77 | path: `${dir}/node_modules/foo`, 78 | })) 79 | 80 | t.resolves(checkBin({ 81 | bin: 'not-existing', 82 | global: true, 83 | top: true, 84 | path: `${dir}/node_modules/foo`, 85 | })) 86 | }) 87 | 88 | t.test('not windows', async t => { 89 | const checkBin = requireInject('../lib/check-bin.js', { 90 | '../lib/is-windows.js': false, 91 | }) 92 | 93 | const dir = t.testdir({ 94 | bin: { 95 | foo: t.fixture('symlink', '../lib/node_modules/foo/foo.js'), 96 | notalink: 'hello', 97 | dir: {}, 98 | }, 99 | }) 100 | 101 | t.rejects(checkBin({ 102 | bin: 'foo', 103 | global: true, 104 | top: true, 105 | path: `${dir}/lib/node_modules/bar`, 106 | }), { code: 'EEXIST' }) 107 | 108 | t.rejects(checkBin({ 109 | bin: 'notalink', 110 | global: true, 111 | top: true, 112 | path: `${dir}/lib/node_modules/bar`, 113 | }), { code: 'EEXIST' }) 114 | 115 | t.rejects(checkBin({ 116 | bin: 'dir', 117 | global: true, 118 | top: true, 119 | path: `${dir}/lib/node_modules/bar`, 120 | }), { code: 'EEXIST' }) 121 | 122 | t.resolves(checkBin({ 123 | bin: 'foo', 124 | global: true, 125 | top: true, 126 | path: `${dir}/lib/node_modules/foo`, 127 | })) 128 | 129 | t.resolves(checkBin({ 130 | bin: 'not-existing', 131 | global: true, 132 | top: true, 133 | path: `${dir}/lib/node_modules/foo`, 134 | })) 135 | }) 136 | -------------------------------------------------------------------------------- /test/check-bins.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const requireInject = require('require-inject') 3 | const checkBin = async ({ bin }) => { 4 | if (bin === 'fail') { 5 | throw new Error('fail') 6 | } 7 | } 8 | const checkBins = requireInject('../lib/check-bins.js', { 9 | '../lib/check-bin.js': checkBin, 10 | }) 11 | 12 | const o = { global: true, force: false, top: true } 13 | 14 | t.test('gets normalized', t => 15 | t.resolves(checkBins({ pkg: { bin: 'lib/foo.js', name: 'foo' }, ...o }))) 16 | 17 | t.test('ok if all ok', t => 18 | t.resolves(checkBins({ pkg: { bin: { foo: 'lib/foo.js' } }, ...o }))) 19 | 20 | t.test('no bin is fine', t => 21 | t.resolves(checkBins({ pkg: {}, ...o }))) 22 | 23 | t.test('always ok if forced', t => 24 | // eslint-disable-next-line max-len 25 | t.resolves(checkBins({ pkg: { bin: { foo: 'lib/foo.js', fail: 'fail.js' } }, ...o, force: true }))) 26 | 27 | t.test('always ok if not global', t => 28 | // eslint-disable-next-line max-len 29 | t.resolves(checkBins({ pkg: { bin: { foo: 'lib/foo.js', fail: 'fail.js' } }, ...o, global: false }))) 30 | 31 | t.test('always ok if not top', t => 32 | t.resolves(checkBins({ pkg: { bin: { foo: 'lib/foo.js', fail: 'fail.js' } }, ...o, top: false }))) 33 | 34 | t.test('fail if any fail', t => 35 | t.rejects(checkBins({ pkg: { bin: { foo: 'lib/foo.js', fail: 'fail.js' } }, ...o }))) 36 | -------------------------------------------------------------------------------- /test/fix-bin.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const requireInject = require('require-inject') 3 | const fixBin = require('../lib/fix-bin.js') 4 | const umask = process.umask() 5 | const fs = require('fs') 6 | const { readFileSync, statSync, chmodSync } = fs 7 | 8 | t.test('fix windows hashbang', async t => { 9 | const dir = t.testdir({ 10 | whb: `#!/usr/bin/env node\r\nconsole.log('hello')\r\n`, 11 | }) 12 | chmodSync(`${dir}/whb`, 0o644) 13 | await fixBin(`${dir}/whb`) 14 | t.equal((statSync(`${dir}/whb`).mode & 0o777), 0o777 & (~umask), 'has exec perms') 15 | t.equal(readFileSync(`${dir}/whb`, 'utf8'), 16 | `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 'fixed \\r on hashbang line') 17 | }) 18 | 19 | t.test('dont fix non-windows hashbang file', async t => { 20 | const dir = t.testdir({ 21 | goodhb: `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 22 | }) 23 | chmodSync(`${dir}/goodhb`, 0o644) 24 | await fixBin(`${dir}/goodhb`) 25 | t.equal((statSync(`${dir}/goodhb`).mode & 0o777), 0o777 & (~umask), 'has exec perms') 26 | t.equal(readFileSync(`${dir}/goodhb`, 'utf8'), 27 | `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 'fixed \\r on hashbang line') 28 | }) 29 | 30 | t.test('failure to read means not a windows hash bang file', async t => { 31 | const fsMock = { 32 | ...fs.promises, 33 | open: async (...args) => { 34 | const fh = await fs.promises.open(...args) 35 | fh.read = async () => { 36 | throw new Error('witaf') 37 | } 38 | return fh 39 | }, 40 | } 41 | 42 | const mockedFixBin = requireInject('../lib/fix-bin.js', { 43 | 'fs/promises': fsMock, 44 | }) 45 | 46 | const dir = t.testdir({ 47 | whb: `#!/usr/bin/env node\r\nconsole.log('hello')\r\n`, 48 | }) 49 | chmodSync(`${dir}/whb`, 0o644) 50 | await mockedFixBin(`${dir}/whb`) 51 | t.equal((statSync(`${dir}/whb`).mode & 0o777), 0o777 & (~umask), 'has exec perms') 52 | t.equal(readFileSync(`${dir}/whb`, 'utf8'), 53 | /* eslint-disable-next-line max-len */ 54 | `#!/usr/bin/env node\r\nconsole.log('hello')\r\n`, 'did not fix \\r on hashbang line (failed read)') 55 | }) 56 | 57 | t.test('failure to close is ignored', async t => { 58 | const fsMock = { 59 | ...fs.promises, 60 | open: async (...args) => { 61 | const fh = await fs.promises.open(...args) 62 | fh.close = async () => { 63 | throw new Error('witaf') 64 | } 65 | return fh 66 | }, 67 | } 68 | const mockedFixBin = requireInject('../lib/fix-bin.js', { 69 | 'fs/promises': fsMock, 70 | }) 71 | 72 | const dir = t.testdir({ 73 | whb: `#!/usr/bin/env node\r\nconsole.log('hello')\r\n`, 74 | }) 75 | chmodSync(`${dir}/whb`, 0o644) 76 | await mockedFixBin(`${dir}/whb`) 77 | t.equal((statSync(`${dir}/whb`).mode & 0o777), 0o777 & (~umask), 'has exec perms') 78 | t.equal( 79 | readFileSync(`${dir}/whb`, 'utf8'), 80 | `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 81 | 'fixed \\r on hashbang line (ignored failed close)' 82 | ) 83 | }) 84 | 85 | t.test('custom exec mode', async t => { 86 | const dir = t.testdir({ 87 | goodhb: `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 88 | }) 89 | chmodSync(`${dir}/goodhb`, 0o644) 90 | await fixBin(`${dir}/goodhb`, 0o755) 91 | t.equal((statSync(`${dir}/goodhb`).mode & 0o755), 0o755 & (~umask), 'has exec perms') 92 | t.equal(readFileSync(`${dir}/goodhb`, 'utf8'), 93 | `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 'fixed \\r on hashbang line') 94 | }) 95 | 96 | t.test('custom exec mode in windows', async t => { 97 | const dir = t.testdir({ 98 | goodhb: `#!/usr/bin/env node\r\nconsole.log('hello')\r\n`, 99 | }) 100 | chmodSync(`${dir}/goodhb`, 0o644) 101 | await fixBin(`${dir}/goodhb`, 0o755) 102 | t.equal((statSync(`${dir}/goodhb`).mode & 0o755), 0o755 & (~umask), 'has exec perms') 103 | t.equal(readFileSync(`${dir}/goodhb`, 'utf8'), 104 | `#!/usr/bin/env node\nconsole.log('hello')\r\n`, 'fixed \\r on hashbang line') 105 | }) 106 | -------------------------------------------------------------------------------- /test/get-node-modules.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const getNodeModules = require('../lib/get-node-modules.js', { 3 | path: require('path').posix, 4 | }) 5 | 6 | t.equal(getNodeModules('/path/to/node_modules/foo'), 7 | '/path/to/node_modules') 8 | t.equal(getNodeModules('/path/to/node_modules/@foo/bar'), 9 | '/path/to/node_modules') 10 | 11 | // call again to hit the memoizing code path 12 | t.equal(getNodeModules('/path/to/node_modules/foo'), 13 | '/path/to/node_modules') 14 | t.equal(getNodeModules('/path/to/node_modules/@foo/bar'), 15 | '/path/to/node_modules') 16 | -------------------------------------------------------------------------------- /test/get-paths.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const both = { 4 | name: 'both', 5 | bin: { 6 | foo: 'bar', 7 | }, 8 | man: ['foo.1.gz'], 9 | } 10 | 11 | const nobin = { 12 | name: 'nobin', 13 | man: ['foo.1.gz'], 14 | } 15 | 16 | const badman = { 17 | name: 'badman', 18 | man: ['hello.txt'], 19 | } 20 | 21 | const requireInject = require('require-inject') 22 | for (const isWindows of [true, false]) { 23 | t.test(isWindows ? 'win32' : 'posix', t => { 24 | t.plan(2) 25 | for (const global of [true, false]) { 26 | t.test(global ? 'global' : 'local', t => { 27 | t.plan(2) 28 | for (const top of [true, false]) { 29 | t.test(top ? 'top' : 'nested', t => { 30 | const path = require('path')[isWindows ? 'win32' : 'posix'] 31 | const prefix = isWindows ? ( 32 | global ? ( 33 | top ? 'c:\\path\\to\\prefix\\node_modules\\' 34 | : 'c:\\path\\to\\prefix\\node_modules\\xyz\\node_modules\\' 35 | ) : ( 36 | top ? 'c:\\path\\to\\' 37 | : 'c:\\path\\to\\project\\node_modules\\' 38 | ) 39 | ) : ( 40 | global ? ( 41 | top ? '/usr/local/lib/node_modules/' 42 | : '/usr/local/lib/node_modules/xyz/node_modules/' 43 | ) : ( 44 | top ? '/path/to/' 45 | : '/path/to/project/node_modules/' 46 | ) 47 | ) 48 | 49 | const getPaths = requireInject('../lib/get-paths.js', { 50 | path, 51 | '../lib/is-windows.js': isWindows, 52 | }) 53 | 54 | t.plan(3) 55 | for (const pkg of [both, nobin, badman]) { 56 | t.test(pkg.name, t => { 57 | t.matchSnapshot(getPaths({ 58 | path: prefix + 'foo', 59 | pkg, 60 | global, 61 | top, 62 | }), 'unscoped package') 63 | 64 | t.matchSnapshot(getPaths({ 65 | path: prefix + '@foo/bar', 66 | pkg, 67 | global, 68 | top, 69 | }), 'scoped package') 70 | 71 | t.end() 72 | }) 73 | } 74 | }) 75 | } 76 | }) 77 | } 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /test/get-prefix.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const getPrefix = require('../lib/get-prefix.js') 3 | const { dirname } = require('path') 4 | 5 | t.equal(getPrefix('/path/to/node_modules/foo'), dirname('/path/to/node_modules')) 6 | t.equal(getPrefix('/path/to/node_modules/@foo/bar'), dirname('/path/to/node_modules')) 7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const requireInject = require('require-inject') 3 | 4 | const mockLinkBins = opt => opt 5 | const mockLinkMans = opt => opt.top && !opt.isWindows ? opt : null 6 | 7 | // root unit test. go through each combination and snapshot them all. 8 | // the index just sets up paths and calls the appropriate functions, 9 | // so this is enough to test that functionality. 10 | const pkg = { 11 | bin: { 12 | foo: 'bar', 13 | }, 14 | man: ['foo.1.gz'], 15 | } 16 | for (const isWindows of [true, false]) { 17 | t.test(isWindows ? 'win32' : 'posix', t => { 18 | t.plan(2) 19 | for (const global of [true, false]) { 20 | t.test(global ? 'global' : 'local', t => { 21 | t.plan(2) 22 | for (const top of [true, false]) { 23 | t.test(top ? 'top' : 'nested', t => { 24 | t.plan(2) 25 | for (const force of [true, false]) { 26 | t.test(`force=${force}`, t => { 27 | const path = require('path')[isWindows ? 'win32' : 'posix'] 28 | const prefix = isWindows ? ( 29 | global ? ( 30 | top ? 'c:\\path\\to\\prefix\\node_modules\\' 31 | : 'c:\\path\\to\\prefix\\node_modules\\xyz\\node_modules\\' 32 | ) : ( 33 | top ? 'c:\\path\\to\\' 34 | : 'c:\\path\\to\\project\\node_modules\\' 35 | ) 36 | ) : ( 37 | global ? ( 38 | top ? '/usr/local/lib/node_modules/' 39 | : '/usr/local/lib/node_modules/xyz/node_modules/' 40 | ) : ( 41 | top ? '/path/to/' 42 | : '/path/to/project/node_modules/' 43 | ) 44 | ) 45 | 46 | const binLinks = requireInject('../', { 47 | path, 48 | '../lib/is-windows.js': isWindows, 49 | '../lib/link-bins.js': mockLinkBins, 50 | '../lib/link-mans.js': opt => mockLinkMans({ 51 | ...opt, 52 | top, 53 | isWindows, 54 | }), 55 | }) 56 | 57 | return Promise.all([ 58 | t.resolveMatchSnapshot(binLinks({ 59 | path: prefix + 'foo', 60 | pkg, 61 | force, 62 | global, 63 | top, 64 | }), 'unscoped pkg'), 65 | t.resolveMatchSnapshot(binLinks({ 66 | path: prefix + '@foo/bar', 67 | pkg, 68 | force, 69 | global, 70 | top, 71 | }), 'scoped pkg'), 72 | ]) 73 | }) 74 | } 75 | }) 76 | } 77 | }) 78 | } 79 | }) 80 | } 81 | 82 | t.test('the resetSeen() method calls appropriate resets', t => { 83 | const binLinks = requireInject('../', { 84 | '../lib/shim-bin.js': { resetSeen: () => { 85 | t.pass('shimBin.resetSeen() called') 86 | } }, 87 | '../lib/link-gently.js': { resetSeen: () => { 88 | t.pass('linkGently.resetSeen() called') 89 | } }, 90 | }) 91 | t.plan(2) 92 | binLinks.resetSeen() 93 | }) 94 | -------------------------------------------------------------------------------- /test/is-windows.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const isWindows = require('../lib/is-windows.js') 3 | if (!process.env.__TESTING_BIN_LINKS_PLATFORM__) { 4 | t.spawn(process.execPath, [__filename], { 5 | env: { 6 | ...process.env, 7 | __TESTING_BIN_LINKS_PLATFORM__: isWindows ? 'posix' : 'win32', 8 | }, 9 | }) 10 | } 11 | const platform = process.env.__TESTING_BIN_LINKS_PLATFORM__ || process.platform 12 | t.equal(isWindows, platform === 'win32') 13 | -------------------------------------------------------------------------------- /test/link-bin.js: -------------------------------------------------------------------------------- 1 | const requireInject = require('require-inject') 2 | const t = require('tap') 3 | const linkBin = requireInject('../lib/link-bin.js', { 4 | // only link if forced, in this mock 5 | '../lib/link-gently.js': ({ force }) => Promise.resolve(force), 6 | '../lib/fix-bin.js': absFrom => Promise.resolve(absFrom), 7 | }) 8 | 9 | t.test('if not linked, dont fix bin', t => 10 | linkBin({ absFrom: '/some/path', force: false }).then(f => t.equal(f, false))) 11 | 12 | t.test('if linked, fix bin', t => 13 | linkBin({ absFrom: '/some/path', force: true }).then(f => t.equal(f, '/some/path'))) 14 | -------------------------------------------------------------------------------- /test/link-bins.js: -------------------------------------------------------------------------------- 1 | const requireInject = require('require-inject') 2 | const t = require('tap') 3 | const pkg = { 4 | name: 'foo', 5 | bin: 'foo.js', 6 | } 7 | 8 | for (const isWindows of [true, false]) { 9 | t.test(isWindows ? 'win32' : 'posix', t => { 10 | const linkBins = requireInject('../lib/link-bins.js', { 11 | // only link if forced, in this mock 12 | '../lib/link-bin.js': ({ from, to }) => Promise.resolve(`LINK ${from} ${to}`), 13 | '../lib/shim-bin.js': ({ from, to }) => Promise.resolve(`SHIM ${from} ${to}`), 14 | '../lib/is-windows.js': isWindows, 15 | path: require('path')[isWindows ? 'win32' : 'posix'], 16 | }) 17 | 18 | const { resolve } = require('path')[isWindows ? 'win32' : 'posix'] 19 | 20 | t.test('link bins', t => linkBins({ 21 | path: resolve('/path/to/lib/node_modules/foo'), 22 | top: true, 23 | pkg, 24 | }).then(linked => t.strictSame(linked, [ 25 | isWindows ? 'SHIM node_modules\\foo\\foo.js \\path\\to\\lib\\foo' 26 | : 'LINK ../lib/node_modules/foo/foo.js /path/to/bin/foo', 27 | ]))) 28 | 29 | t.test('no bins to link', t => linkBins({ 30 | path: resolve('/path/to/node_modules/foo'), 31 | top: true, 32 | pkg: { name: 'foo' }, 33 | }).then(linked => t.strictSame(linked, []))) 34 | 35 | t.end() 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /test/link-gently.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const linkGently = require('../lib/link-gently.js') 3 | const fs = require('fs') 4 | const fsp = require('fs/promises') 5 | const requireInject = require('require-inject') 6 | 7 | t.test('make links gently', async t => { 8 | const dir = t.testdir({ 9 | pkg: { 10 | 'hello.js': `#!/usr/bin/env node\nconsole.log('hello')`, 11 | }, 12 | otherpkg: { 13 | 'hello.js': `#!/usr/bin/env node\nconsole.log('other hello')`, 14 | }, 15 | existingLink: t.fixture('symlink', './pkg/hello.js'), 16 | existingFile: 'hello', 17 | }) 18 | 19 | let linkResult 20 | linkResult = await linkGently({ 21 | path: `${dir}/pkg`, 22 | to: `${dir}/bin/hello`, 23 | from: `../pkg/hello.js`, 24 | absFrom: `${dir}/pkg/hello.js`, 25 | }) 26 | t.equal(linkResult, true) 27 | t.equal(fs.readlinkSync(`${dir}/bin/hello`), '../pkg/hello.js') 28 | linkGently.resetSeen() 29 | 30 | // call it again to test the 'SKIP' code path and ensure it returns false 31 | linkResult = await linkGently({ 32 | path: `${dir}/pkg`, 33 | to: `${dir}/bin/hello`, 34 | from: `../pkg/hello.js`, 35 | absFrom: `${dir}/pkg/hello.js`, 36 | }) 37 | t.equal(linkResult, false) 38 | linkGently.resetSeen() 39 | await t.rejects(linkGently({ 40 | path: `${dir}/otherpkg`, 41 | to: `${dir}/bin/hello`, 42 | from: `../otherpkg/hello.js`, 43 | absFrom: `${dir}/otherpkg/hello.js`, 44 | }), { code: 'EEXIST' }) 45 | linkGently.resetSeen() 46 | linkResult = await linkGently({ 47 | path: `${dir}/otherpkg`, 48 | to: `${dir}/bin/hello`, 49 | from: `../otherpkg/hello.js`, 50 | absFrom: `${dir}/otherpkg/hello.js`, 51 | force: true, 52 | }) 53 | t.equal(linkResult, true) 54 | t.equal(fs.readlinkSync(`${dir}/bin/hello`), '../otherpkg/hello.js') 55 | 56 | await t.rejects(linkGently({ 57 | path: `${dir}/pkg`, 58 | to: `${dir}/existingFile/notadir`, 59 | from: `../pkg/hello.js`, 60 | absFrom: `${dir}/pkg/hello.js`, 61 | }), { code: 'ENOTDIR' }) 62 | 63 | linkGently.resetSeen() 64 | await t.rejects(linkGently({ 65 | path: `${dir}/pkg`, 66 | to: `${dir}/existingFile`, 67 | from: `./pkg/hello.js`, 68 | absFrom: `${dir}/pkg/hello.js`, 69 | }), { code: 'EEXIST' }) 70 | linkGently.resetSeen() 71 | await linkGently({ 72 | path: `${dir}/pkg`, 73 | to: `${dir}/existingFile`, 74 | from: `./pkg/hello.js`, 75 | absFrom: `${dir}/pkg/hello.js`, 76 | force: true, 77 | }) 78 | t.equal(fs.readlinkSync(`${dir}/existingFile`), './pkg/hello.js') 79 | linkGently.resetSeen() 80 | 81 | await linkGently({ 82 | path: `${dir}/pkg`, 83 | to: `${dir}/bin/missing`, 84 | from: `../pkg/missing.js`, 85 | absFrom: `${dir}/pkg/missing.js`, 86 | }) 87 | t.throws(() => fs.readlinkSync(`${dir}/bin/missing`), { code: 'ENOENT' }) 88 | linkGently.resetSeen() 89 | }) 90 | 91 | t.test('handles link errors', async t => { 92 | const dir = t.testdir({ 93 | pkg: { 94 | 'hello.js': `#!/usr/bin/env node\nconsole.log('hello')`, 95 | }, 96 | }) 97 | const fspMock = { 98 | ...fsp, 99 | lstat: (ref) => { 100 | const code = (/\/(e\w+)$/.exec(ref) || [])[1] 101 | if (code) { 102 | return Promise.reject(Object.assign(new Error(), { code: code.toUpperCase() })) 103 | } 104 | 105 | return fsp.lstat(ref) 106 | }, 107 | } 108 | const mockedLinkGently = requireInject('../lib/link-gently.js', { 109 | 'fs/promises': fspMock, 110 | }) 111 | await mockedLinkGently({ 112 | path: `${dir}/pkg`, 113 | to: `${dir}/bin/eacces`, 114 | from: `../pkg/hello.js`, 115 | absFrom: `${dir}/pkg/hello.js`, 116 | }) 117 | 118 | await mockedLinkGently({ 119 | path: `${dir}/pkg`, 120 | to: `${dir}/bin/enoent`, 121 | from: `../pkg/hello.js`, 122 | absFrom: `${dir}/pkg/hello.js`, 123 | }) 124 | 125 | await t.rejects(mockedLinkGently({ 126 | path: `${dir}/pkg`, 127 | to: `${dir}/bin/eperm`, 128 | from: `../pkg/hello.js`, 129 | absFrom: `${dir}/pkg/hello.js`, 130 | }), { code: 'EPERM' }) 131 | }) 132 | 133 | t.test('racey race', async t => { 134 | const fsMock = { 135 | ...fs, 136 | symlink: (path, dest, type, cb) => { 137 | // throw a lag on it to ensure that one of them doesn't finish 138 | // before the other even starts. 139 | setTimeout(() => fs.symlink(path, dest, type, cb), 200) 140 | }, 141 | } 142 | const mockedLinkGently = requireInject('../lib/link-gently.js', { 143 | fs: fsMock, 144 | }) 145 | const dir = t.testdir({ 146 | pkg: { 147 | 'hello.js': `#!/usr/bin/env node\nconsole.log('hello')`, 148 | }, 149 | otherpkg: { 150 | 'hello.js': `#!/usr/bin/env node\nconsole.log('other hello')`, 151 | }, 152 | existingLink: t.fixture('symlink', './pkg/hello.js'), 153 | existingFile: 'hello', 154 | }) 155 | const multipleLinked = await Promise.all([ 156 | mockedLinkGently({ 157 | path: `${dir}/pkg`, 158 | from: `./pkg/hello.js`, 159 | to: `${dir}/racecar`, 160 | absFrom: `${dir}/pkg/hello.js`, 161 | force: true, 162 | }), 163 | mockedLinkGently({ 164 | path: `${dir}/otherpkg`, 165 | from: `./otherpkg/hello.js`, 166 | to: `${dir}/racecar`, 167 | absFrom: `${dir}/otherpkg/hello.js`, 168 | force: true, 169 | }), 170 | new Promise((res) => fs.symlink(__filename, `${dir}/racecar`, 'file', res)), 171 | ]) 172 | t.ok(multipleLinked[0] || multipleLinked[1], 'should link one path succesfully') 173 | t.notSame(multipleLinked[0], multipleLinked[1], 'should fail to link the other path') 174 | const target = fs.readlinkSync(`${dir}/racecar`) 175 | t.match(target, /^\.\/(other)?pkg\/hello\.js$/, 'should link to one of them') 176 | }) 177 | -------------------------------------------------------------------------------- /test/link-mans.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const requireInject = require('require-inject') 4 | const linkMans = requireInject('../lib/link-mans.js', { 5 | '../lib/link-gently.js': ({ from, to }) => Promise.resolve(`LINK ${from} ${to}`), 6 | }) 7 | 8 | t.test('not top/global', t => linkMans({ 9 | pkg: { man: ['foo.1'] }, 10 | top: false, 11 | path: '/usr/local/lib/node_modules/pkg', 12 | }).then(res => t.strictSame(res, []))) 13 | 14 | t.test('man not an array', t => linkMans({ 15 | pkg: { man: 'not an array' }, 16 | top: true, 17 | path: '/usr/local/lib/node_modules/pkg', 18 | }).then(res => t.strictSame(res, []))) 19 | 20 | t.test('no man', t => linkMans({ 21 | pkg: {}, 22 | top: true, 23 | path: '/usr/local/lib/node_modules/pkg', 24 | }).then(res => t.strictSame(res, []))) 25 | 26 | t.test('link some mans', t => linkMans({ 27 | pkg: { 28 | man: [ 29 | 'foo.1', 30 | null, 31 | 'docs/foo.1', 32 | 'foo.1.gz', 33 | '/path/to/etc/passwd.1', 34 | 'c:\\path\\to\\passwd.2', 35 | ], 36 | }, 37 | top: true, 38 | path: '/usr/local/lib/node_modules/pkg', 39 | }).then(res => t.strictSame(res.sort((a, b) => a.localeCompare(b)), [ 40 | 'LINK ../../../lib/node_modules/pkg/c/path/to/passwd.2 /usr/local/share/man/man2/passwd.2', 41 | 'LINK ../../../lib/node_modules/pkg/docs/foo.1 /usr/local/share/man/man1/foo.1', 42 | 'LINK ../../../lib/node_modules/pkg/foo.1 /usr/local/share/man/man1/foo.1', 43 | 'LINK ../../../lib/node_modules/pkg/foo.1.gz /usr/local/share/man/man1/foo.1.gz', 44 | 'LINK ../../../lib/node_modules/pkg/path/to/etc/passwd.1 /usr/local/share/man/man1/passwd.1', 45 | ]))) 46 | 47 | t.test('bad man', t => t.rejects(linkMans({ 48 | pkg: { 49 | _id: 'foo@1.2.3', 50 | name: 'foo', 51 | version: '1.2.3', 52 | man: [ 53 | 'foo.readme', 54 | null, 55 | 'docs/foo.1', 56 | 'foo.1.gz', 57 | ], 58 | }, 59 | top: true, 60 | path: '/usr/local/lib/node_modules/pkg', 61 | }), { code: 'EBADMAN' })) 62 | -------------------------------------------------------------------------------- /test/man-target.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const requireInject = require('require-inject') 3 | 4 | for (const isWindows of [true, false]) { 5 | t.test(isWindows ? 'win32' : 'posix', t => { 6 | const manTarget = requireInject('../lib/man-target.js', { 7 | '../lib/is-windows.js': isWindows, 8 | path: require('path')[isWindows ? 'win32' : 'posix'], 9 | }) 10 | 11 | t.matchSnapshot(manTarget({ 12 | path: '/path/to/node_modules/foo', 13 | top: true, 14 | }), 'top (and thus global)') 15 | 16 | t.matchSnapshot(manTarget({ 17 | path: '/path/to/node_modules/foo', 18 | }), 'not top') 19 | 20 | t.end() 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /test/shim-bin.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const requireInject = require('require-inject') 3 | const fs = require('fs') 4 | const { statSync } = fs 5 | const path = require('path').win32 6 | 7 | t.test('basic shim bin', async t => { 8 | const dir = t.testdir({ 9 | pkg: { 10 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 11 | }, 12 | otherpkg: { 13 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 14 | }, 15 | notashim: 'definitely not', 16 | }) 17 | const shimBin = requireInject('../lib/shim-bin.js', { path }) 18 | await shimBin({ 19 | path: `${dir}/pkg`, 20 | to: `${dir}/bin/hello`, 21 | from: `../pkg/hello.js`, 22 | absFrom: `${dir}/pkg/hello.js`, 23 | }) 24 | { 25 | const shims = ['hello', 'hello.cmd', 'hello.ps1'].map(f => `${dir}/bin/${f}`) 26 | for (const shim of shims) { 27 | t.equal(statSync(shim).mode & 0o100, 0o100, 'exists and executable') 28 | } 29 | } 30 | shimBin.resetSeen() 31 | 32 | await t.rejects(shimBin({ 33 | path: `${dir}/otherpkg`, 34 | to: `${dir}/bin/hello`, 35 | from: `../otherpkg/hello.js`, 36 | absFrom: `${dir}/otherpkg/hello.js`, 37 | }), { code: 'EEXIST' }) 38 | shimBin.resetSeen() 39 | await t.rejects(shimBin({ 40 | path: `${dir}/otherpkg`, 41 | to: `${dir}/notashim`, 42 | from: `./otherpkg/hello.js`, 43 | absFrom: `${dir}/otherpkg/hello.js`, 44 | }), { code: 'EEXIST' }) 45 | shimBin.resetSeen() 46 | 47 | await shimBin({ 48 | path: `${dir}/otherpkg`, 49 | to: `${dir}/notashim`, 50 | from: `./otherpkg/hello.js`, 51 | absFrom: `${dir}/otherpkg/hello.js`, 52 | force: true, 53 | }) 54 | statSync(`${dir}/notashim.cmd`) 55 | shimBin.resetSeen() 56 | await shimBin({ 57 | path: `${dir}/pkg`, 58 | to: `${dir}/bin/hello`, 59 | from: `../pkg/hello.js`, 60 | absFrom: `${dir}/pkg/hello.js`, 61 | }) 62 | shimBin.resetSeen() 63 | await shimBin({ 64 | path: `${dir}/pkg`, 65 | to: `${dir}/bin/missing`, 66 | from: `../pkg/missing.js`, 67 | absFrom: `${dir}/pkg/missing.js`, 68 | }) 69 | t.throws(() => statSync(`${dir}/bin/missing.cmd`)) 70 | shimBin.resetSeen() 71 | }) 72 | 73 | t.test('eperm on stat', async t => { 74 | const dir = t.testdir({ 75 | pkg: { 76 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 77 | }, 78 | otherpkg: { 79 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 80 | }, 81 | notashim: 'definitely not', 82 | }) 83 | const shimBin = requireInject('../lib/shim-bin.js', { 84 | path, 85 | 'fs/promises': { 86 | ...fs.promises, 87 | lstat: () => Promise.reject(Object.assign(new Error('wakawaka'), { 88 | code: 'EPERM', 89 | })), 90 | }, 91 | }) 92 | shimBin.resetSeen() 93 | await t.rejects(shimBin({ 94 | path: `${dir}/pkg`, 95 | to: `${dir}/bin/hello`, 96 | from: `../pkg/hello.js`, 97 | absFrom: `${dir}/pkg/hello.js`, 98 | }), { code: 'EPERM' }) 99 | shimBin.resetSeen() 100 | }) 101 | 102 | t.test('strange enoent from read-cmd-shim', async t => { 103 | const dir = t.testdir({ 104 | pkg: { 105 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 106 | }, 107 | otherpkg: { 108 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 109 | }, 110 | notashim: 'definitely not', 111 | }) 112 | const shimBin = requireInject('../lib/shim-bin.js', { 113 | path, 114 | 'read-cmd-shim': () => Promise.reject(Object.assign(new Error('xyz'), { 115 | code: 'ENOENT', 116 | })), 117 | }) 118 | 119 | // run two so that we do hit the seen path 120 | await Promise.all([ 121 | shimBin({ 122 | path: `${dir}/pkg`, 123 | to: `${dir}/bin/hello`, 124 | from: `../pkg/hello.js`, 125 | absFrom: `${dir}/pkg/hello.js`, 126 | }), 127 | shimBin({ 128 | path: `${dir}/pkg`, 129 | to: `${dir}/bin/hello`, 130 | from: `../pkg/hello.js`, 131 | absFrom: `${dir}/pkg/hello.js`, 132 | }), 133 | ]) 134 | 135 | { 136 | const shims = ['hello', 'hello.cmd', 'hello.ps1'].map(f => `${dir}/bin/${f}`) 137 | for (const shim of shims) { 138 | t.equal(statSync(shim).mode & 0o100, 0o100, 'exists and executable') 139 | } 140 | } 141 | shimBin.resetSeen() 142 | await shimBin({ 143 | path: `${dir}/pkg`, 144 | to: `${dir}/bin/hello`, 145 | from: `../pkg/hello.js`, 146 | absFrom: `${dir}/pkg/hello.js`, 147 | }) 148 | shimBin.resetSeen() 149 | }) 150 | 151 | t.test('unknown error from read-cmd-shim', async t => { 152 | const dir = t.testdir({ 153 | pkg: { 154 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 155 | }, 156 | otherpkg: { 157 | 'hello.js': `#!/usr/bin/env node\r\nconsole.log('hello')`, 158 | }, 159 | notashim: 'definitely not', 160 | }) 161 | const shimBin = requireInject('../lib/shim-bin.js', { 162 | path, 163 | 'read-cmd-shim': () => Promise.reject(Object.assign(new Error('xyz'), { 164 | code: 'ELDERGAWDS', 165 | })), 166 | }) 167 | shimBin.resetSeen() 168 | await shimBin({ 169 | path: `${dir}/pkg`, 170 | to: `${dir}/bin/hello`, 171 | from: `../pkg/hello.js`, 172 | absFrom: `${dir}/pkg/hello.js`, 173 | }) 174 | { 175 | const shims = ['hello', 'hello.cmd', 'hello.ps1'].map(f => `${dir}/bin/${f}`) 176 | for (const shim of shims) { 177 | t.equal(statSync(shim).mode & 0o100, 0o100, 'exists and executable') 178 | } 179 | } 180 | shimBin.resetSeen() 181 | await t.rejects(shimBin({ 182 | path: `${dir}/pkg`, 183 | to: `${dir}/bin/hello`, 184 | from: `../pkg/hello.js`, 185 | absFrom: `${dir}/pkg/hello.js`, 186 | }), { code: 'ELDERGAWDS' }) 187 | shimBin.resetSeen() 188 | }) 189 | --------------------------------------------------------------------------------