├── .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 |
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 |
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 |
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 |
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 |
156 | ## [1.1.3](https://github.com/npm/bin-links/compare/v1.1.2...v1.1.3) (2019-08-14)
157 |
158 |
159 |
160 |
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 |
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 |
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 |
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 |
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 |
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 [](https://npm.im/bin-links) [](https://npm.im/bin-links) [](https://travis-ci.org/npm/bin-links) [](https://ci.appveyor.com/project/npm/bin-links) [](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 | #### `> binLinks({path, pkg, force, global, top})`
61 |
62 | Returns a Promise that resolves when the requisite things have been linked.
63 |
64 | #### `> 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 | #### `> 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 |
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 |
--------------------------------------------------------------------------------