├── .commitlintrc.js ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── CODEOWNERS.md ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── bug.yml │ ├── config.yml │ └── feature.md ├── PULL_REQUEST_TEMPLATE │ ├── bug.md │ ├── feature.md │ └── optimization.md ├── 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.md ├── README.md ├── SECURITY.md ├── lib └── index.js ├── package.json ├── release-please-config.json ├── tap-snapshots └── test │ ├── test.js.test.cjs │ └── test.virtual.js.test.cjs └── test ├── test.js └── test.virtual.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/CODEOWNERS.md: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # @npm/cli-team will be requested for review when 4 | # someone opens a pull request. 5 | * @npm/cli-team -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Expected Behavior 9 | 10 | 11 | ## Current Behavior 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | ## Supporting Materials 21 | 22 | 23 | 24 |
package.json

25 | 26 | 27 | ```json 28 | 29 | 30 | ``` 31 |

32 | 33 |
npm-debug.log

34 | 35 | 36 | ```txt 37 | 38 | 39 | ``` 40 |

41 | 42 | #### Environments & Engine Versions 43 | 44 | | Executable | Version | 45 | | --- | --- | 46 | | `node --version` | {{VERSION}} | 47 | | `npm --version` | {{VERSION}} | 48 | 49 | | OS | Version | 50 | | --- | --- | 51 | | {{NAME}} | {{VERSION}} | 52 | 57 | 58 | ## Other Supporting Details or Comments 59 | 60 | 61 | -------------------------------------------------------------------------------- /.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/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: feature-type issue 4 | title: '[FEATURE] <title>' 5 | assignees: 6 | labels: 7 | --- 8 | 9 | <!--- 10 | # Before Opening Please... 11 | - [ ] Search for an existing/duplicate issues which might be relevant to your idea 12 | - [ ] Provide a general summary of the feature in the Title above 13 | --> 14 | 15 | ## Description 16 | <!-- reference RFC or issues if applicable --> 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | <!--- 2 | # Before Opening Please... 3 | - [ ] Read our CONTRIBUTING.md 4 | - [ ] Provide a general summary of the feature in the Title above 5 | - [ ] Ensure your fix is complete 6 | - [ ] Ensure your PR has updated docs if functional changes were made 7 | - [ ] Ensure your PR has tests that will break/error without your fix 8 | - [ ] Ensure your PR has labels 9 | - [ ] Ensure your PR has reviewers outlined 10 | --> 11 | 12 | ## Description 13 | <!-- reference issue if applicable --> 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | <!--- 2 | # Before Opening Please... 3 | - [ ] Read our CONTRIBUTING.md 4 | - [ ] Provide a general summary of the feature in the Title above 5 | - [ ] Ensure your feature is complete 6 | - [ ] Ensure your PR has docs 7 | - [ ] Ensure your PR has tests 8 | - [ ] Ensure your PR has labels 9 | - [ ] Ensure your PR has reviewers outlined 10 | --> 11 | 12 | ## Description 13 | <!-- reference RFC or issue if applicable --> 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/optimization.md: -------------------------------------------------------------------------------- 1 | <!--- 2 | # Before Opening Please... 3 | - [ ] Read our CONTRIBUTING.md 4 | - [ ] Provide a general summary of the feature in the Title above 5 | - [ ] Ensure your work is complete 6 | - [ ] Ensure your PR has monitoring &/or benchmarks associated 7 | - [ ] Ensure your PR has labels 8 | - [ ] Ensure your PR has reviewers outlined 9 | --> 10 | 11 | ## Description 12 | <!-- reference RFC or issue if applicable --> 13 | -------------------------------------------------------------------------------- /.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 | - name: Windows 91 | os: windows-latest 92 | shell: cmd 93 | node-version: 94 | - 18.17.0 95 | - 18.x 96 | - 20.5.0 97 | - 20.x 98 | - 22.x 99 | exclude: 100 | - platform: { name: macOS, os: macos-13, shell: bash } 101 | node-version: 18.17.0 102 | - platform: { name: macOS, os: macos-13, shell: bash } 103 | node-version: 18.x 104 | - platform: { name: macOS, os: macos-13, shell: bash } 105 | node-version: 20.5.0 106 | - platform: { name: macOS, os: macos-13, shell: bash } 107 | node-version: 20.x 108 | - platform: { name: macOS, os: macos-13, shell: bash } 109 | node-version: 22.x 110 | runs-on: ${{ matrix.platform.os }} 111 | defaults: 112 | run: 113 | shell: ${{ matrix.platform.shell }} 114 | steps: 115 | - name: Checkout 116 | uses: actions/checkout@v4 117 | with: 118 | ref: ${{ inputs.ref }} 119 | - name: Setup Git User 120 | run: | 121 | git config --global user.email "npm-cli+bot@github.com" 122 | git config --global user.name "npm CLI robot" 123 | - name: Create Check 124 | id: create-check 125 | if: ${{ inputs.check-sha }} 126 | uses: ./.github/actions/create-check 127 | with: 128 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 129 | token: ${{ secrets.GITHUB_TOKEN }} 130 | sha: ${{ inputs.check-sha }} 131 | - name: Setup Node 132 | uses: actions/setup-node@v4 133 | id: node 134 | with: 135 | node-version: ${{ matrix.node-version }} 136 | check-latest: contains(matrix.node-version, '.x') 137 | - name: Install Latest npm 138 | uses: ./.github/actions/install-latest-npm 139 | with: 140 | node: ${{ steps.node.outputs.node-version }} 141 | - name: Install Dependencies 142 | run: npm i --ignore-scripts --no-audit --no-fund 143 | - name: Add Problem Matcher 144 | run: echo "::add-matcher::.github/matchers/tap.json" 145 | - name: Test 146 | run: npm test --ignore-scripts 147 | - name: Conclude Check 148 | uses: LouisBrunner/checks-action@v1.6.0 149 | if: steps.create-check.outputs.check-id && always() 150 | with: 151 | token: ${{ secrets.GITHUB_TOKEN }} 152 | conclusion: ${{ job.status }} 153 | check_id: ${{ steps.create-check.outputs.check-id }} 154 | -------------------------------------------------------------------------------- /.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 | - name: Windows 67 | os: windows-latest 68 | shell: cmd 69 | node-version: 70 | - 18.17.0 71 | - 18.x 72 | - 20.5.0 73 | - 20.x 74 | - 22.x 75 | exclude: 76 | - platform: { name: macOS, os: macos-13, shell: bash } 77 | node-version: 18.17.0 78 | - platform: { name: macOS, os: macos-13, shell: bash } 79 | node-version: 18.x 80 | - platform: { name: macOS, os: macos-13, shell: bash } 81 | node-version: 20.5.0 82 | - platform: { name: macOS, os: macos-13, shell: bash } 83 | node-version: 20.x 84 | - platform: { name: macOS, os: macos-13, shell: bash } 85 | node-version: 22.x 86 | runs-on: ${{ matrix.platform.os }} 87 | defaults: 88 | run: 89 | shell: ${{ matrix.platform.shell }} 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | - name: Setup Git User 94 | run: | 95 | git config --global user.email "npm-cli+bot@github.com" 96 | git config --global user.name "npm CLI robot" 97 | - name: Setup Node 98 | uses: actions/setup-node@v4 99 | id: node 100 | with: 101 | node-version: ${{ matrix.node-version }} 102 | check-latest: contains(matrix.node-version, '.x') 103 | - name: Install Latest npm 104 | uses: ./.github/actions/install-latest-npm 105 | with: 106 | node: ${{ steps.node.outputs.node-version }} 107 | - name: Install Dependencies 108 | run: npm i --ignore-scripts --no-audit --no-fund 109 | - name: Add Problem Matcher 110 | run: echo "::add-matcher::.github/matchers/tap.json" 111 | - name: Test 112 | run: npm test --ignore-scripts 113 | -------------------------------------------------------------------------------- /.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 | ".": "4.0.2" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.0.2](https://github.com/npm/map-workspaces/compare/v4.0.1...v4.0.2) (2024-11-20) 4 | ### Bug Fixes 5 | * [`ae9cd9e`](https://github.com/npm/map-workspaces/commit/ae9cd9e090d7737a7d9f5cb0d5b15f7be36acb5a) [#176](https://github.com/npm/map-workspaces/pull/176) ignore `ENOTDIR` errors (#176) (@brianlenz) 6 | ### Chores 7 | * [`84d179f`](https://github.com/npm/map-workspaces/commit/84d179f7167ae3d50183788fde352f2596b6cc14) [#174](https://github.com/npm/map-workspaces/pull/174) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#174) (@dependabot[bot], @npm-cli-bot) 8 | 9 | ## [4.0.1](https://github.com/npm/map-workspaces/compare/v4.0.0...v4.0.1) (2024-10-02) 10 | ### Dependencies 11 | * [`b21250e`](https://github.com/npm/map-workspaces/commit/b21250e9115f29786479ed34ed6c352528366f66) [#172](https://github.com/npm/map-workspaces/pull/172) bump `@npmcli/package-json@6.0.0` 12 | 13 | ## [4.0.0](https://github.com/npm/map-workspaces/compare/v3.0.6...v4.0.0) (2024-09-26) 14 | ### ⚠️ BREAKING CHANGES 15 | * `@npmcli/map-workspaces` now supports node `^18.17.0 || >=20.5.0` 16 | ### Bug Fixes 17 | * [`211aef3`](https://github.com/npm/map-workspaces/commit/211aef36df4b51aae4ebd8dce361064b3b072a52) [#153](https://github.com/npm/map-workspaces/pull/153) use @npmcli/package-json to parse packages (@wraithgar) 18 | * [`3959800`](https://github.com/npm/map-workspaces/commit/395980086f6152436b494b88a2441c4e7836c3b3) [#168](https://github.com/npm/map-workspaces/pull/168) align to npm 10 node engine range (@reggi) 19 | ### Dependencies 20 | * [`044b3a8`](https://github.com/npm/map-workspaces/commit/044b3a83616b3df4917e49dfc2dabe148db8d02e) [#153](https://github.com/npm/map-workspaces/pull/153) `@npmcli/package-json@5.2.0` 21 | * [`a493618`](https://github.com/npm/map-workspaces/commit/a493618d0627dca39f6c435bc8df72991a323be1) [#153](https://github.com/npm/map-workspaces/pull/153) remove read-package-json-fast 22 | * [`e52dd76`](https://github.com/npm/map-workspaces/commit/e52dd762b608d5d4ed76c577eac98500053e8398) [#168](https://github.com/npm/map-workspaces/pull/168) `read-package-json-fast@4.0.0` 23 | * [`2ec2a15`](https://github.com/npm/map-workspaces/commit/2ec2a1537741d36a267f7eed0c70f31c35e5c7a1) [#168](https://github.com/npm/map-workspaces/pull/168) `@npmcli/name-from-folder@3.0.0` 24 | ### Chores 25 | * [`f066c79`](https://github.com/npm/map-workspaces/commit/f066c79b7305443c3a0ad62d7b0276260a558d34) [#166](https://github.com/npm/map-workspaces/pull/166) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (#166) (@dependabot[bot]) 26 | * [`888c87c`](https://github.com/npm/map-workspaces/commit/888c87c2275e812715e91a5c29b1eb9679de0484) [#168](https://github.com/npm/map-workspaces/pull/168) run template-oss-apply (@reggi) 27 | * [`d1b3556`](https://github.com/npm/map-workspaces/commit/d1b355622cefcc28cada257aa20561a0bf4604e1) [#150](https://github.com/npm/map-workspaces/pull/150) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 28 | * [`938d94c`](https://github.com/npm/map-workspaces/commit/938d94ccf9fed4f8598368ea741faa33b8e0a3ba) [#162](https://github.com/npm/map-workspaces/pull/162) bump @npmcli/template-oss from 4.22.0 to 4.23.3 (#162) (@dependabot[bot]) 29 | * [`c5cb12c`](https://github.com/npm/map-workspaces/commit/c5cb12c05e703982b0783c50aca54388c163a144) [#150](https://github.com/npm/map-workspaces/pull/150) postinstall for dependabot template-oss PR (@lukekarrys) 30 | 31 | ## [3.0.6](https://github.com/npm/map-workspaces/compare/v3.0.5...v3.0.6) (2024-04-10) 32 | 33 | ### Bug Fixes 34 | 35 | * [`e2a803b`](https://github.com/npm/map-workspaces/commit/e2a803bd610cca87a9618c8f118aca56b6e936f1) [#145](https://github.com/npm/map-workspaces/pull/145) allow for workspace patterns to start with `./` (#145) (@wraithgar) 36 | 37 | ## [3.0.5](https://github.com/npm/map-workspaces/compare/v3.0.4...v3.0.5) (2024-04-10) 38 | 39 | ### Bug Fixes 40 | 41 | * [`c89a529`](https://github.com/npm/map-workspaces/commit/c89a529e1db38d2f672dc5d71561efeab6d93d16) [#143](https://github.com/npm/map-workspaces/pull/143) faster workspace mapping (#143) (@wraithgar, @thecodrr) 42 | 43 | ### Documentation 44 | 45 | * [`304c345`](https://github.com/npm/map-workspaces/commit/304c345a52eeb298437c51cb64655ae1872efac7) [#141](https://github.com/npm/map-workspaces/pull/141) readme: fix broken badge URLs (#141) (@10xLaCroixDrinker) 46 | 47 | ### Chores 48 | 49 | * [`2c3d889`](https://github.com/npm/map-workspaces/commit/2c3d889bb2a354e0293ff17b5f6c25af7d96d6ba) [#137](https://github.com/npm/map-workspaces/pull/137) postinstall for dependabot template-oss PR (@lukekarrys) 50 | * [`1e78272`](https://github.com/npm/map-workspaces/commit/1e7827287e30ccb6a0bb5501ac0acf67f2aabe9e) [#137](https://github.com/npm/map-workspaces/pull/137) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot]) 51 | * [`a5b6bf5`](https://github.com/npm/map-workspaces/commit/a5b6bf5f5c2ab304e96fea6e039ef5135f9dc543) [#134](https://github.com/npm/map-workspaces/pull/134) postinstall for dependabot template-oss PR (@lukekarrys) 52 | * [`797d0a0`](https://github.com/npm/map-workspaces/commit/797d0a04c2be15c09f18407b8ef32b312f47bcae) [#134](https://github.com/npm/map-workspaces/pull/134) bump @npmcli/template-oss from 4.19.0 to 4.21.1 (@dependabot[bot]) 53 | * [`4017779`](https://github.com/npm/map-workspaces/commit/4017779a15e92fea8e31a92b99f486225e9488de) [#115](https://github.com/npm/map-workspaces/pull/115) postinstall for dependabot template-oss PR (@lukekarrys) 54 | * [`803e42f`](https://github.com/npm/map-workspaces/commit/803e42fcd9ec25b8cc84a6a089d8d3ae0c2029a6) [#115](https://github.com/npm/map-workspaces/pull/115) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot]) 55 | * [`b214753`](https://github.com/npm/map-workspaces/commit/b21475370393cf980fad1983d22c1415403f7991) [#114](https://github.com/npm/map-workspaces/pull/114) postinstall for dependabot template-oss PR (@lukekarrys) 56 | * [`17747a7`](https://github.com/npm/map-workspaces/commit/17747a78077a4d8faa5450941802b9f757fed60d) [#114](https://github.com/npm/map-workspaces/pull/114) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot]) 57 | * [`ff82968`](https://github.com/npm/map-workspaces/commit/ff82968a3dbb78659fb7febfce4841bf58c514de) [#113](https://github.com/npm/map-workspaces/pull/113) postinstall for dependabot template-oss PR (@lukekarrys) 58 | * [`f38ff1e`](https://github.com/npm/map-workspaces/commit/f38ff1ec13fb4e74f4be09d7ebc46cb40c670276) [#113](https://github.com/npm/map-workspaces/pull/113) bump @npmcli/template-oss from 4.17.0 to 4.18.0 (@dependabot[bot]) 59 | * [`cf234ce`](https://github.com/npm/map-workspaces/commit/cf234cef3e4454de8f4203b5e13178cef3632084) [#112](https://github.com/npm/map-workspaces/pull/112) postinstall for dependabot template-oss PR (@lukekarrys) 60 | * [`414331d`](https://github.com/npm/map-workspaces/commit/414331d8fbb42d3568534ad7b2744e9982271629) [#112](https://github.com/npm/map-workspaces/pull/112) bump @npmcli/template-oss from 4.15.1 to 4.17.0 (@dependabot[bot]) 61 | * [`003893e`](https://github.com/npm/map-workspaces/commit/003893e523539920f5df3487bf473f9abdb3b391) [#110](https://github.com/npm/map-workspaces/pull/110) postinstall for dependabot template-oss PR (@lukekarrys) 62 | * [`8fcd7ee`](https://github.com/npm/map-workspaces/commit/8fcd7ee03a4f27da081af8f4d4bd3a8f6b8b6337) [#110](https://github.com/npm/map-workspaces/pull/110) bump @npmcli/template-oss from 4.14.1 to 4.15.1 (@dependabot[bot]) 63 | 64 | ## [3.0.4](https://github.com/npm/map-workspaces/compare/v3.0.3...v3.0.4) (2023-04-27) 65 | 66 | ### Dependencies 67 | 68 | * [`978c416`](https://github.com/npm/map-workspaces/commit/978c4164368a5821284a62a051cb996728a10d93) [#106](https://github.com/npm/map-workspaces/pull/106) bump glob from 9.3.5 to 10.2.2 (#106) 69 | * [`1c8e72d`](https://github.com/npm/map-workspaces/commit/1c8e72d4c253369a60b336ed59c2c3f7601bc47a) [#103](https://github.com/npm/map-workspaces/pull/103) bump minimatch from 7.4.6 to 9.0.0 (#103) 70 | 71 | ## [3.0.3](https://github.com/npm/map-workspaces/compare/v3.0.2...v3.0.3) (2023-03-21) 72 | 73 | ### Dependencies 74 | 75 | * [`f37aeb1`](https://github.com/npm/map-workspaces/commit/f37aeb1dd83aa64ae96f1622061544d8b5466f4b) [#96](https://github.com/npm/map-workspaces/pull/96) bump glob from 8.1.0 to 9.3.1 (#96) 76 | * [`98d3b80`](https://github.com/npm/map-workspaces/commit/98d3b8037fc5558403403fd930b744fe30d97f81) [#92](https://github.com/npm/map-workspaces/pull/92) bump minimatch from 6.2.0 to 7.4.2 (#92) 77 | 78 | ## [3.0.2](https://github.com/npm/map-workspaces/compare/v3.0.1...v3.0.2) (2023-02-06) 79 | 80 | ### Dependencies 81 | 82 | * [`33326ea`](https://github.com/npm/map-workspaces/commit/33326ea8a71e79370975b4547df0aa04d108a35a) [#79](https://github.com/npm/map-workspaces/pull/79) bump minimatch from 5.1.6 to 6.1.6 83 | 84 | ## [3.0.1](https://github.com/npm/map-workspaces/compare/v3.0.0...v3.0.1) (2022-12-14) 85 | 86 | ### Dependencies 87 | 88 | * [`15c752f`](https://github.com/npm/map-workspaces/commit/15c752f9bdf18ffbd719e68a385f19494f7ee110) [#75](https://github.com/npm/map-workspaces/pull/75) bump @npmcli/name-from-folder from 1.0.1 to 2.0.0 89 | 90 | ## [3.0.0](https://github.com/npm/map-workspaces/compare/v2.0.4...v3.0.0) (2022-10-12) 91 | 92 | ### ⚠️ BREAKING CHANGES 93 | 94 | * `@npmcli/map-workspaces` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 95 | 96 | ### Features 97 | 98 | * [`d018d85`](https://github.com/npm/map-workspaces/commit/d018d850e67ca4da179db0368728b144dde10cad) [#60](https://github.com/npm/map-workspaces/pull/60) postinstall for dependabot template-oss PR (@lukekarrys) 99 | 100 | ### Documentation 101 | 102 | * [`2f9abc7`](https://github.com/npm/map-workspaces/commit/2f9abc7037e1872ff94787f59ee103c64939f708) [#64](https://github.com/npm/map-workspaces/pull/64) update readme install package name (#64) (@wraithgar) 103 | 104 | ### Dependencies 105 | 106 | * [`8f3fcce`](https://github.com/npm/map-workspaces/commit/8f3fccefa57e06d4dd5e271cc1255f43e5528029) [#68](https://github.com/npm/map-workspaces/pull/68) bump read-package-json-fast from 2.0.3 to 3.0.0 107 | 108 | ## [2.0.4](https://github.com/npm/map-workspaces/compare/v2.0.3...v2.0.4) (2022-06-29) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * replace deprecated String.prototype.substr() ([#41](https://github.com/npm/map-workspaces/issues/41)) ([1452a05](https://github.com/npm/map-workspaces/commit/1452a052495664505313027928213c4473c44842)) 114 | 115 | ### [2.0.3](https://github.com/npm/map-workspaces/compare/v2.0.2...v2.0.3) (2022-04-20) 116 | 117 | 118 | ### Dependencies 119 | 120 | * bump glob from 7.2.0 to 8.0.1 ([#43](https://github.com/npm/map-workspaces/issues/43)) ([d1db8e7](https://github.com/npm/map-workspaces/commit/d1db8e7dcf3b70008bf050e41472ba600d8a24de)) 121 | 122 | ### [2.0.2](https://www.github.com/npm/map-workspaces/compare/v2.0.1...v2.0.2) (2022-03-10) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * evaluate all patterns before throwing EDUPLICATEWORKSPACE ([#32](https://www.github.com/npm/map-workspaces/issues/32)) ([ca0bf18](https://www.github.com/npm/map-workspaces/commit/ca0bf18d4852017c3befc3c908baf29e6e72a55f)) 128 | 129 | 130 | ### Dependencies 131 | 132 | * update minimatch requirement from ^5.0.0 to ^5.0.1 ([#28](https://www.github.com/npm/map-workspaces/issues/28)) ([5c4fd0d](https://www.github.com/npm/map-workspaces/commit/5c4fd0d28d19539fdb9df85bcafcc7122b3702b0)) 133 | 134 | ### [2.0.1](https://www.github.com/npm/map-workspaces/compare/v2.0.0...v2.0.1) (2022-02-17) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * normalize backslashes ([777e4d3](https://www.github.com/npm/map-workspaces/commit/777e4d3a8670b94dba91e4305ce2b846fc02b7d8)) 140 | 141 | 142 | ### Dependencies 143 | 144 | * bump minimatch from 3.1.1 to 4.1.1 ([305629e](https://www.github.com/npm/map-workspaces/commit/305629e32609451af7ce8b6464dde224ab5dcc7a)) 145 | * bump minimatch from 4.2.1 to 5.0.0 ([2098531](https://www.github.com/npm/map-workspaces/commit/2098531776a31172a3664097b769e83cb8dbe682)) 146 | * update glob requirement from ^7.1.6 to ^7.2.0 ([588b229](https://www.github.com/npm/map-workspaces/commit/588b22938dd514b806dea4f6cb76298c4d468b3a)) 147 | * update read-package-json-fast requirement from ^2.0.1 to ^2.0.3 ([0eeb67d](https://www.github.com/npm/map-workspaces/commit/0eeb67d2dff646553321957438d9b9d86202e8b7)) 148 | 149 | ## 0.0.0-pre.0 150 | 151 | - Initial pre-release. 152 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | ISC License 4 | 5 | Copyright npm, Inc. 6 | 7 | Permission to use, copy, modify, and/or distribute this 8 | software for any purpose with or without fee is hereby 9 | granted, provided that the above copyright notice and this 10 | permission notice appear in all copies. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS" AND NPM DISCLAIMS ALL 13 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO 15 | EVENT SHALL NPM BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 18 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 19 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE 20 | USE OR PERFORMANCE OF THIS SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @npmcli/map-workspaces 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@npmcli/map-workspaces)](https://www.npmjs.com/package/@npmcli/map-workspaces) 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/npm/map-workspaces/ci.yml?branch=main)](https://github.com/npm/map-workspaces) 5 | [![License](https://img.shields.io/npm/l/@npmcli/map-workspaces)](https://github.com/npm/map-workspaces/blob/main/LICENSE.md) 6 | 7 | Retrieves a name:pathname Map for a given workspaces config. 8 | 9 | Long version: Reads the `workspaces` property from a valid **workspaces configuration** object and traverses the paths and globs defined there in order to find valid nested packages and return a **Map** of all found packages where keys are package names and values are folder locations. 10 | 11 | ## Install 12 | 13 | `npm install @npmcli/map-workspaces` 14 | 15 | ## Usage: 16 | 17 | ```js 18 | const mapWorkspaces = require('@npmcli/map-workspaces') 19 | await mapWorkspaces({ 20 | cwd, 21 | pkg: { 22 | workspaces: { 23 | packages: [ 24 | "a", 25 | "b" 26 | ] 27 | } 28 | } 29 | }) 30 | // -> 31 | // Map { 32 | // 'a': '<cwd>/a' 33 | // 'b': '<cwd>/b' 34 | // } 35 | ``` 36 | 37 | ## Examples: 38 | 39 | ### Glob usage: 40 | 41 | Given a folder structure such as: 42 | 43 | ``` 44 | ├── package.json 45 | └── apps 46 | ├── a 47 | │ └── package.json 48 | ├── b 49 | │ └── package.json 50 | └── c 51 | └── package.json 52 | ``` 53 | 54 | ```js 55 | const mapWorkspaces = require('@npmcli/map-workspaces') 56 | await mapWorkspaces({ 57 | cwd, 58 | pkg: { 59 | workspaces: [ 60 | "apps/*" 61 | ] 62 | } 63 | }) 64 | // -> 65 | // Map { 66 | // 'a': '<cwd>/apps/a' 67 | // 'b': '<cwd>/apps/b' 68 | // 'c': '<cwd>/apps/c' 69 | // } 70 | ``` 71 | 72 | ## API: 73 | 74 | ### `mapWorkspaces(opts) -> Promise<Map>` 75 | 76 | - `opts`: 77 | - `pkg`: A valid `package.json` **Object** 78 | - `cwd`: A **String** defining the base directory to use when reading globs and paths. 79 | - `ignore`: An **Array** of paths to be ignored when using [globs](https://www.npmjs.com/package/glob) to look for nested package. 80 | - ...[Also support all other glob options](https://www.npmjs.com/package/glob#options) 81 | 82 | #### Returns 83 | 84 | A **Map** in which keys are **package names** and values are the **pathnames** for each found **workspace**. 85 | 86 | ## LICENSE 87 | 88 | [ISC](./LICENSE) 89 | 90 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const getName = require('@npmcli/name-from-folder') 4 | const { minimatch } = require('minimatch') 5 | const pkgJson = require('@npmcli/package-json') 6 | const { glob } = require('glob') 7 | 8 | function appendNegatedPatterns (allPatterns) { 9 | const patterns = [] 10 | const negatedPatterns = [] 11 | for (let pattern of allPatterns) { 12 | const excl = pattern.match(/^!+/) 13 | if (excl) { 14 | pattern = pattern.slice(excl[0].length) 15 | } 16 | 17 | // strip off any / or ./ from the start of the pattern. /foo => foo 18 | pattern = pattern.replace(/^\.?\/+/, '') 19 | 20 | // an odd number of ! means a negated pattern. !!foo ==> foo 21 | const negate = excl && excl[0].length % 2 === 1 22 | if (negate) { 23 | negatedPatterns.push(pattern) 24 | } else { 25 | // remove negated patterns that appeared before this pattern to avoid 26 | // ignoring paths that were matched afterwards 27 | // e.g: ['packages/**', '!packages/b/**', 'packages/b/a'] 28 | // in the above list, the last pattern overrides the negated pattern 29 | // right before it. In effect, the above list would become: 30 | // ['packages/**', 'packages/b/a'] 31 | // The order matters here which is why we must do it inside the loop 32 | // as opposed to doing it all together at the end. 33 | for (let i = 0; i < negatedPatterns.length; ++i) { 34 | const negatedPattern = negatedPatterns[i] 35 | if (minimatch(pattern, negatedPattern)) { 36 | negatedPatterns.splice(i, 1) 37 | } 38 | } 39 | patterns.push(pattern) 40 | } 41 | } 42 | 43 | // use the negated patterns to eagerly remove all the patterns that 44 | // can be removed to avoid unnecessary crawling 45 | for (const negated of negatedPatterns) { 46 | for (const pattern of minimatch.match(patterns, negated)) { 47 | patterns.splice(patterns.indexOf(pattern), 1) 48 | } 49 | } 50 | return { patterns, negatedPatterns } 51 | } 52 | 53 | function getPatterns (workspaces) { 54 | const workspacesDeclaration = 55 | Array.isArray(workspaces.packages) 56 | ? workspaces.packages 57 | : workspaces 58 | 59 | if (!Array.isArray(workspacesDeclaration)) { 60 | throw getError({ 61 | message: 'workspaces config expects an Array', 62 | code: 'EWORKSPACESCONFIG', 63 | }) 64 | } 65 | 66 | return appendNegatedPatterns(workspacesDeclaration) 67 | } 68 | 69 | function getPackageName (pkg, pathname) { 70 | return pkg.name || getName(pathname) 71 | } 72 | 73 | // make sure glob pattern only matches folders 74 | function getGlobPattern (pattern) { 75 | pattern = pattern.replace(/\\/g, '/') 76 | return pattern.endsWith('/') 77 | ? pattern 78 | : `${pattern}/` 79 | } 80 | 81 | function getError ({ Type = TypeError, message, code }) { 82 | return Object.assign(new Type(message), { code }) 83 | } 84 | 85 | function reverseResultMap (map) { 86 | return new Map(Array.from(map, item => item.reverse())) 87 | } 88 | 89 | async function mapWorkspaces (opts = {}) { 90 | if (!opts || !opts.pkg) { 91 | throw getError({ 92 | message: 'mapWorkspaces missing pkg info', 93 | code: 'EMAPWORKSPACESPKG', 94 | }) 95 | } 96 | if (!opts.cwd) { 97 | opts.cwd = process.cwd() 98 | } 99 | 100 | const { workspaces = [] } = opts.pkg 101 | const { patterns, negatedPatterns } = getPatterns(workspaces) 102 | const results = new Map() 103 | 104 | if (!patterns.length && !negatedPatterns.length) { 105 | return results 106 | } 107 | 108 | const seen = new Map() 109 | const getGlobOpts = () => ({ 110 | ...opts, 111 | ignore: [ 112 | ...opts.ignore || [], 113 | '**/node_modules/**', 114 | // just ignore the negated patterns to avoid unnecessary crawling 115 | ...negatedPatterns, 116 | ], 117 | }) 118 | 119 | let matches = await glob(patterns.map((p) => getGlobPattern(p)), getGlobOpts()) 120 | // preserves glob@8 behavior 121 | matches = matches.sort((a, b) => a.localeCompare(b, 'en')) 122 | 123 | // we must preserve the order of results according to the given list of 124 | // workspace patterns 125 | const orderedMatches = [] 126 | for (const pattern of patterns) { 127 | orderedMatches.push(...matches.filter((m) => { 128 | return minimatch(m, pattern, { partial: true, windowsPathsNoEscape: true }) 129 | })) 130 | } 131 | 132 | for (const match of orderedMatches) { 133 | let pkg 134 | try { 135 | pkg = await pkgJson.normalize(path.join(opts.cwd, match)) 136 | } catch (err) { 137 | if (err.code === 'ENOENT' || err.code === 'ENOTDIR') { 138 | continue 139 | } else { 140 | throw err 141 | } 142 | } 143 | 144 | const name = getPackageName(pkg.content, pkg.path) 145 | 146 | let seenPackagePathnames = seen.get(name) 147 | if (!seenPackagePathnames) { 148 | seenPackagePathnames = new Set() 149 | seen.set(name, seenPackagePathnames) 150 | } 151 | seenPackagePathnames.add(pkg.path) 152 | } 153 | 154 | const errorMessageArray = ['must not have multiple workspaces with the same name'] 155 | for (const [packageName, seenPackagePathnames] of seen) { 156 | if (seenPackagePathnames.size > 1) { 157 | addDuplicateErrorMessages(errorMessageArray, packageName, seenPackagePathnames) 158 | } else { 159 | results.set(packageName, seenPackagePathnames.values().next().value) 160 | } 161 | } 162 | 163 | if (errorMessageArray.length > 1) { 164 | throw getError({ 165 | Type: Error, 166 | message: errorMessageArray.join('\n'), 167 | code: 'EDUPLICATEWORKSPACE', 168 | }) 169 | } 170 | 171 | return results 172 | } 173 | 174 | function addDuplicateErrorMessages (messageArray, packageName, packagePathnames) { 175 | messageArray.push( 176 | `package '${packageName}' has conflicts in the following paths:` 177 | ) 178 | 179 | for (const packagePathname of packagePathnames) { 180 | messageArray.push( 181 | ' ' + packagePathname 182 | ) 183 | } 184 | } 185 | 186 | mapWorkspaces.virtual = function (opts = {}) { 187 | if (!opts || !opts.lockfile) { 188 | throw getError({ 189 | message: 'mapWorkspaces.virtual missing lockfile info', 190 | code: 'EMAPWORKSPACESLOCKFILE', 191 | }) 192 | } 193 | if (!opts.cwd) { 194 | opts.cwd = process.cwd() 195 | } 196 | 197 | const { packages = {} } = opts.lockfile 198 | const { workspaces = [] } = packages[''] || {} 199 | // uses a pathname-keyed map in order to negate the exact items 200 | const results = new Map() 201 | const { patterns, negatedPatterns } = getPatterns(workspaces) 202 | if (!patterns.length && !negatedPatterns.length) { 203 | return results 204 | } 205 | negatedPatterns.push('**/node_modules/**') 206 | 207 | const packageKeys = Object.keys(packages) 208 | for (const pattern of negatedPatterns) { 209 | for (const packageKey of minimatch.match(packageKeys, pattern)) { 210 | packageKeys.splice(packageKeys.indexOf(packageKey), 1) 211 | } 212 | } 213 | 214 | for (const pattern of patterns) { 215 | for (const packageKey of minimatch.match(packageKeys, pattern)) { 216 | const packagePathname = path.join(opts.cwd, packageKey) 217 | const name = getPackageName(packages[packageKey], packagePathname) 218 | results.set(packagePathname, name) 219 | } 220 | } 221 | 222 | // Invert pathname-keyed to a proper name-to-pathnames Map 223 | return reverseResultMap(results) 224 | } 225 | 226 | module.exports = mapWorkspaces 227 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@npmcli/map-workspaces", 3 | "version": "4.0.2", 4 | "main": "lib/index.js", 5 | "files": [ 6 | "bin/", 7 | "lib/" 8 | ], 9 | "engines": { 10 | "node": "^18.17.0 || >=20.5.0" 11 | }, 12 | "description": "Retrieves a name:pathname Map for a given workspaces config", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/npm/map-workspaces.git" 16 | }, 17 | "keywords": [ 18 | "npm", 19 | "npmcli", 20 | "libnpm", 21 | "cli", 22 | "workspaces", 23 | "map-workspaces" 24 | ], 25 | "author": "GitHub Inc.", 26 | "license": "ISC", 27 | "scripts": { 28 | "lint": "npm run eslint", 29 | "pretest": "npm run lint", 30 | "test": "tap", 31 | "snap": "tap", 32 | "postlint": "template-oss-check", 33 | "lintfix": "npm run eslint -- --fix", 34 | "posttest": "npm run lint", 35 | "template-oss-apply": "template-oss-apply --force", 36 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" 37 | }, 38 | "tap": { 39 | "check-coverage": true, 40 | "nyc-arg": [ 41 | "--exclude", 42 | "tap-snapshots/**" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@npmcli/eslint-config": "^5.0.0", 47 | "@npmcli/template-oss": "4.24.3", 48 | "tap": "^16.0.1" 49 | }, 50 | "dependencies": { 51 | "@npmcli/name-from-folder": "^3.0.0", 52 | "@npmcli/package-json": "^6.0.0", 53 | "glob": "^10.2.2", 54 | "minimatch": "^9.0.0" 55 | }, 56 | "templateOSS": { 57 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 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/test.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/test.js TAP backslashes are normalized > matches with backslashes 1`] = ` 9 | Map { 10 | "a" => "{CWD}/test/tap-testdir-test-backslashes-are-normalized/packages/a", 11 | } 12 | ` 13 | 14 | exports[`test/test.js TAP double negated patterns > should include doubly-negated items into resulting map 1`] = ` 15 | Map { 16 | "a" => "{CWD}/test/tap-testdir-test-double-negated-patterns/packages/a", 17 | } 18 | ` 19 | 20 | exports[`test/test.js TAP duplicated workspaces glob pattern > should allow dup glob-declared packages that resolve to same pathname 1`] = ` 21 | Map { 22 | "a" => "{CWD}/test/tap-testdir-test-duplicated-workspaces-glob-pattern/packages/a", 23 | "b" => "{CWD}/test/tap-testdir-test-duplicated-workspaces-glob-pattern/packages/nested/b", 24 | } 25 | ` 26 | 27 | exports[`test/test.js TAP empty folders > should ignore empty folders 1`] = ` 28 | Map { 29 | "a" => "{CWD}/test/tap-testdir-test-empty-folders/a", 30 | "b" => "{CWD}/test/tap-testdir-test-empty-folders/b", 31 | } 32 | ` 33 | 34 | exports[`test/test.js TAP empty packages declaration > should return an empty map 1`] = ` 35 | Map {} 36 | ` 37 | 38 | exports[`test/test.js TAP ignore option > should ignore things from opts.ignore 1`] = ` 39 | Map { 40 | "a" => "{CWD}/test/tap-testdir-test-ignore-option/packages/a", 41 | } 42 | ` 43 | 44 | exports[`test/test.js TAP ignore symlink workspace files > should not throw an error 1`] = ` 45 | Map { 46 | "a" => "{CWD}/test/tap-testdir-test-ignore-symlink-workspace-files/packages/a", 47 | } 48 | ` 49 | 50 | exports[`test/test.js TAP match duplicates then exclude one > should include the non-excluded item on returned Map 1`] = ` 51 | Map { 52 | "a" => "{CWD}/test/tap-testdir-test-match-duplicates-then-exclude-one/packages/a", 53 | } 54 | ` 55 | 56 | exports[`test/test.js TAP matched then negated then match again > should include item on returned Map 1`] = ` 57 | Map { 58 | "a" => "{CWD}/test/tap-testdir-test-matched-then-negated-then-match-again/packages/b/a", 59 | } 60 | ` 61 | 62 | exports[`test/test.js TAP matched then negated then match again with wildcards > should exclude item on returned Map 1`] = ` 63 | Map {} 64 | ` 65 | 66 | exports[`test/test.js TAP missing pkg info > should return an empty map 1`] = ` 67 | Array [ 68 | Map {}, 69 | Map {}, 70 | Map {}, 71 | ] 72 | ` 73 | 74 | exports[`test/test.js TAP multiple duplicated workspaces config > should throw an error listing all duplicates 1`] = ` 75 | Error: must not have multiple workspaces with the same name 76 | package 'a' has conflicts in the following paths: 77 | {CWD}/test/tap-testdir-test-multiple-duplicated-workspaces-config/packages/a 78 | {CWD}/test/tap-testdir-test-multiple-duplicated-workspaces-config/packages/b 79 | {CWD}/test/tap-testdir-test-multiple-duplicated-workspaces-config/packages/c 80 | package 'b' has conflicts in the following paths: 81 | {CWD}/test/tap-testdir-test-multiple-duplicated-workspaces-config/packages/d 82 | {CWD}/test/tap-testdir-test-multiple-duplicated-workspaces-config/packages/e { 83 | "code": "EDUPLICATEWORKSPACE", 84 | } 85 | ` 86 | 87 | exports[`test/test.js TAP multiple negated patterns > should not include any negated pattern 1`] = ` 88 | Map {} 89 | ` 90 | 91 | exports[`test/test.js TAP negated pattern > should not include negated patterns 1`] = ` 92 | Map { 93 | "a" => "{CWD}/test/tap-testdir-test-negated-pattern/packages/a", 94 | } 95 | ` 96 | 97 | exports[`test/test.js TAP nested glob lookups > should return a valid map 1`] = ` 98 | Map { 99 | "a" => "{CWD}/test/tap-testdir-test-nested-glob-lookups/packages/a", 100 | } 101 | ` 102 | 103 | exports[`test/test.js TAP nested node_modules > should ignore packages within node_modules 1`] = ` 104 | Map { 105 | "a" => "{CWD}/test/tap-testdir-test-nested-node_modules/packages/a", 106 | "b" => "{CWD}/test/tap-testdir-test-nested-node_modules/packages/b", 107 | "e" => "{CWD}/test/tap-testdir-test-nested-node_modules/foo/bar/baz/e", 108 | } 109 | ` 110 | 111 | exports[`test/test.js TAP no cwd provided > value is pkg pathname 1`] = ` 112 | {CWD}/packages/a 113 | ` 114 | 115 | exports[`test/test.js TAP no package name > should return map containing valid names as keys 1`] = ` 116 | Map { 117 | "@foo/bar" => "{CWD}/test/tap-testdir-test-no-package-name/packages/@foo/bar", 118 | "a" => "{CWD}/test/tap-testdir-test-no-package-name/packages/a", 119 | "b" => "{CWD}/test/tap-testdir-test-no-package-name/packages/b", 120 | } 121 | ` 122 | 123 | exports[`test/test.js TAP overlapping negated pattern > should not include negated patterns 1`] = ` 124 | Map { 125 | "a" => "{CWD}/test/tap-testdir-test-overlapping-negated-pattern/packages/a", 126 | } 127 | ` 128 | 129 | exports[`test/test.js TAP root declared within workspaces > should allow the root package to be declared within workspaces 1`] = ` 130 | Map { 131 | "a" => "{CWD}/test/tap-testdir-test-root-declared-within-workspaces/packages/a", 132 | "root-workspace" => "{CWD}/test/tap-testdir-test-root-declared-within-workspaces", 133 | } 134 | ` 135 | 136 | exports[`test/test.js TAP simple workspaces config > should return a valid map 1`] = ` 137 | Map { 138 | "a" => "{CWD}/test/tap-testdir-test-simple-workspaces-config/a", 139 | "b" => "{CWD}/test/tap-testdir-test-simple-workspaces-config/b", 140 | } 141 | ` 142 | 143 | exports[`test/test.js TAP simple workspaces config with scoped pkg > should return a valid map 1`] = ` 144 | Map { 145 | "@ruyadorno/scoped-a" => "{CWD}/test/tap-testdir-test-simple-workspaces-config-with-scoped-pkg/packages/a", 146 | "@ruyadorno/scoped-b" => "{CWD}/test/tap-testdir-test-simple-workspaces-config-with-scoped-pkg/packages/b", 147 | } 148 | ` 149 | 150 | exports[`test/test.js TAP triple negated patterns > should exclude thrice-negated items from resulting map 1`] = ` 151 | Map {} 152 | ` 153 | 154 | exports[`test/test.js TAP try to declare node_modules > should not include declared packages within node_modules 1`] = ` 155 | Map {} 156 | ` 157 | 158 | exports[`test/test.js TAP use of / at end of defined globs > should return a valid map 1`] = ` 159 | Map { 160 | "a" => "{CWD}/test/tap-testdir-test-use-of-at-end-of-defined-globs/a", 161 | "b" => "{CWD}/test/tap-testdir-test-use-of-at-end-of-defined-globs/b", 162 | } 163 | ` 164 | 165 | exports[`test/test.js TAP workspaces config using relative path globs > should return a valid map 1`] = ` 166 | Map { 167 | "a" => "{CWD}/test/tap-testdir-test-workspaces-config-using-relative-path-globs/packages/a", 168 | } 169 | ` 170 | 171 | exports[`test/test.js TAP workspaces config using simplistic glob > should return a valid map 1`] = ` 172 | Map { 173 | "a" => "{CWD}/test/tap-testdir-test-workspaces-config-using-simplistic-glob/packages/a", 174 | "b" => "{CWD}/test/tap-testdir-test-workspaces-config-using-simplistic-glob/packages/b", 175 | } 176 | ` 177 | -------------------------------------------------------------------------------- /tap-snapshots/test/test.virtual.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/test.virtual.js TAP double-negated > should return the doubly-negated item as part of the Map 1`] = ` 9 | Map { 10 | "a" => "{CWD}/test/tap-testdir-test.virtual-double-negated/packages/a", 11 | "b" => "{CWD}/test/tap-testdir-test.virtual-double-negated/packages/b", 12 | } 13 | ` 14 | 15 | exports[`test/test.virtual.js TAP matched then negated then match again > should include item on returned Map 1`] = ` 16 | Map { 17 | "a" => "{CWD}/test/tap-testdir-test.virtual-matched-then-negated-then-match-again/packages/a", 18 | "b" => "{CWD}/test/tap-testdir-test.virtual-matched-then-negated-then-match-again/packages/b", 19 | } 20 | ` 21 | 22 | exports[`test/test.virtual.js TAP matched then negated then match again then negate again > should exclude negated item from returned Map 1`] = ` 23 | Map { 24 | "a" => "{CWD}/test/tap-testdir-test.virtual-matched-then-negated-then-match-again-then-negate-again/packages/a", 25 | } 26 | ` 27 | 28 | exports[`test/test.virtual.js TAP negate globs in workspaces config > should not return negated workspaces 1`] = ` 29 | Map { 30 | "a" => "{CWD}/test/tap-testdir-test.virtual-negate-globs-in-workspaces-config/packages/a", 31 | } 32 | ` 33 | 34 | exports[`test/test.virtual.js TAP should ignore nested node_modules > should return a valid map 1`] = ` 35 | Map { 36 | "a" => "{CWD}/test/tap-testdir-test.virtual-should-ignore-nested-node_modules/packages/a", 37 | } 38 | ` 39 | 40 | exports[`test/test.virtual.js TAP simple workspaces config > should return a valid map 1`] = ` 41 | Map { 42 | "a" => "{CWD}/test/tap-testdir-test.virtual-simple-workspaces-config/a", 43 | "b" => "{CWD}/test/tap-testdir-test.virtual-simple-workspaces-config/b", 44 | } 45 | ` 46 | 47 | exports[`test/test.virtual.js TAP transitive dependencies > should return a map containing only the valid workspaces 1`] = ` 48 | Map { 49 | "a" => "{CWD}/test/tap-testdir-test.virtual-transitive-dependencies/packages/a", 50 | } 51 | ` 52 | 53 | exports[`test/test.virtual.js TAP triple-negated > should exclude that item from returned Map 1`] = ` 54 | Map { 55 | "a" => "{CWD}/test/tap-testdir-test.virtual-triple-negated/packages/a", 56 | } 57 | ` 58 | 59 | exports[`test/test.virtual.js TAP unexpected lockfile info > should return an empty map 1`] = ` 60 | Array [ 61 | Map {}, 62 | Map {}, 63 | Map {}, 64 | ] 65 | ` 66 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const { test } = tap 3 | 4 | const mapWorkspaces = require('../') 5 | 6 | tap.cleanSnapshot = str => { 7 | const cleanPath = path => path 8 | .replace(/\\+/g, '/') // normalize slashes 9 | .replace(/"\w:/g, '"') // gets rid of drive letter in snapshot 10 | .replace(/^(\s*)\w:/gm, '$1') // gets rid of drive letter in cwd/paths 11 | const cwd = cleanPath(process.cwd()) 12 | const pathname = cleanPath(str) 13 | return pathname.split(cwd).join('{CWD}') 14 | } 15 | 16 | test('simple workspaces config', t => { 17 | const cwd = t.testdir({ 18 | a: { 19 | 'package.json': '{ "name": "a" }', 20 | }, 21 | b: { 22 | 'package.json': '{ "name": "b" }', 23 | }, 24 | }) 25 | 26 | return t.resolveMatchSnapshot( 27 | mapWorkspaces({ 28 | cwd, 29 | pkg: { 30 | workspaces: { 31 | packages: [ 32 | 'a', 33 | 'b', 34 | ], 35 | }, 36 | }, 37 | }), 38 | 'should return a valid map' 39 | ) 40 | }) 41 | 42 | test('simple workspaces config with scoped pkg', t => { 43 | const cwd = t.testdir({ 44 | packages: { 45 | a: { 46 | 'package.json': '{ "name": "@ruyadorno/scoped-a" }', 47 | }, 48 | b: { 49 | 'package.json': '{ "name": "@ruyadorno/scoped-b" }', 50 | }, 51 | }, 52 | }) 53 | 54 | return t.resolveMatchSnapshot( 55 | mapWorkspaces({ 56 | cwd, 57 | pkg: { 58 | workspaces: { 59 | packages: [ 60 | 'packages/*', 61 | ], 62 | }, 63 | }, 64 | }), 65 | 'should return a valid map' 66 | ) 67 | }) 68 | 69 | test('missing pkg info', t => { 70 | const cwd = t.testdir({ 71 | a: { 72 | 'package.json': '{ "name": "a" }', 73 | }, 74 | }) 75 | 76 | const results = Promise.all([ 77 | mapWorkspaces({ 78 | cwd, 79 | pkg: 1, 80 | }), 81 | mapWorkspaces({ 82 | cwd, 83 | pkg: 'foo', 84 | }), 85 | mapWorkspaces({ 86 | cwd, 87 | pkg: {}, 88 | }), 89 | ]) 90 | return t.resolveMatchSnapshot(results, 'should return an empty map') 91 | }) 92 | 93 | test('invalid options', async t => { 94 | const invalid = [ 95 | () => mapWorkspaces({}), 96 | () => mapWorkspaces({ pkg: null }), 97 | () => mapWorkspaces([]), 98 | () => mapWorkspaces('foo'), 99 | () => mapWorkspaces(1), 100 | () => mapWorkspaces(NaN), 101 | () => mapWorkspaces(null), 102 | () => mapWorkspaces(), 103 | ] 104 | 105 | for (const i of invalid) { 106 | await t.rejects( 107 | i(), 108 | { code: 'EMAPWORKSPACESPKG' }, 109 | 'should throw a TypeError' 110 | ) 111 | } 112 | 113 | t.end() 114 | }) 115 | 116 | test('workspaces config using simplistic glob', t => { 117 | const cwd = t.testdir({ 118 | packages: { 119 | a: { 120 | 'package.json': '{ "name": "a" }', 121 | }, 122 | b: { 123 | 'package.json': '{ "name": "b" }', 124 | }, 125 | }, 126 | }) 127 | 128 | return t.resolveMatchSnapshot( 129 | mapWorkspaces({ 130 | cwd, 131 | pkg: { 132 | workspaces: { 133 | packages: [ 134 | 'packages/*', 135 | ], 136 | }, 137 | }, 138 | }), 139 | 'should return a valid map' 140 | ) 141 | }) 142 | 143 | test('workspaces config using relative path globs', t => { 144 | const cwd = t.testdir({ 145 | packages: { 146 | a: { 147 | 'package.json': '{ "name": "a" }', 148 | }, 149 | b: { 150 | 'package.json': '{ "name": "b" }', 151 | }, 152 | }, 153 | }) 154 | 155 | return t.resolveMatchSnapshot( 156 | mapWorkspaces({ 157 | cwd, 158 | pkg: { 159 | workspaces: { 160 | packages: [ 161 | './packages/*', 162 | '!./packages/b', 163 | ], 164 | }, 165 | }, 166 | }), 167 | 'should return a valid map' 168 | ) 169 | }) 170 | 171 | test('duplicated workspaces config', t => { 172 | const cwd = t.testdir({ 173 | packages: { 174 | a: { 175 | 'package.json': '{ "name": "a" }', 176 | }, 177 | b: { 178 | 'package.json': '{ "name": "a" }', 179 | }, 180 | }, 181 | }) 182 | 183 | return t.rejects( 184 | mapWorkspaces({ 185 | cwd, 186 | pkg: { 187 | workspaces: { 188 | packages: [ 189 | 'packages/*', 190 | ], 191 | }, 192 | }, 193 | }), 194 | { code: 'EDUPLICATEWORKSPACE' }, 195 | 'should throw an error' 196 | ) 197 | }) 198 | 199 | test('duplicated workspaces globstar pattern', t => { 200 | const cwd = t.testdir({ 201 | packages: { 202 | a: { 203 | 'package.json': '{ "name": "a" }', 204 | }, 205 | nested: { 206 | b: { 207 | 'package.json': '{ "name": "a" }', // name is same as packages/a 208 | }, 209 | }, 210 | }, 211 | }) 212 | 213 | return t.rejects( 214 | mapWorkspaces({ 215 | cwd, 216 | pkg: { 217 | workspaces: { 218 | packages: [ 219 | 'packages/**', 220 | 'packages/nested/**', 221 | ], 222 | }, 223 | }, 224 | }), 225 | { code: 'EDUPLICATEWORKSPACE' }, 226 | 'should throw an error' 227 | ) 228 | }) 229 | 230 | test('duplicated workspaces glob pattern', t => { 231 | const cwd = t.testdir({ 232 | packages: { 233 | a: { 234 | 'package.json': '{ "name": "a" }', 235 | }, 236 | nested: { 237 | b: { 238 | 'package.json': '{ "name": "b" }', 239 | }, 240 | }, 241 | }, 242 | }) 243 | 244 | return t.resolveMatchSnapshot( 245 | mapWorkspaces({ 246 | cwd, 247 | pkg: { 248 | workspaces: { 249 | packages: [ 250 | 'packages/**', 251 | 'packages/nested/**', 252 | ], 253 | }, 254 | }, 255 | }), 256 | 'should allow dup glob-declared packages that resolve to same pathname' 257 | ) 258 | }) 259 | 260 | test('multiple duplicated workspaces config', t => { 261 | const cwd = t.testdir({ 262 | packages: { 263 | a: { 264 | 'package.json': '{ "name": "a" }', 265 | }, 266 | b: { 267 | 'package.json': '{ "name": "a" }', 268 | }, 269 | c: { 270 | 'package.json': '{ "name": "a" }', 271 | }, 272 | d: { 273 | 'package.json': '{ "name": "b" }', 274 | }, 275 | e: { 276 | 'package.json': '{ "name": "b" }', 277 | }, 278 | }, 279 | }) 280 | 281 | return t.resolveMatchSnapshot( 282 | mapWorkspaces({ 283 | cwd, 284 | pkg: { 285 | workspaces: { 286 | packages: [ 287 | 'packages/*', 288 | ], 289 | }, 290 | }, 291 | }).catch(error => error), 292 | 'should throw an error listing all duplicates' 293 | ) 294 | }) 295 | 296 | test('empty packages declaration', t => { 297 | const cwd = t.testdir({ 298 | packages: { 299 | a: { 300 | 'package.json': '{ "name": "a" }', 301 | }, 302 | }, 303 | }) 304 | 305 | return t.resolveMatchSnapshot( 306 | mapWorkspaces({ 307 | cwd, 308 | pkg: { 309 | workspaces: { 310 | packages: [], 311 | }, 312 | }, 313 | }), 314 | 'should return an empty map' 315 | ) 316 | }) 317 | 318 | test('invalid packages declaration', async t => { 319 | const cwd = t.testdir({ 320 | packages: { 321 | a: { 322 | 'package.json': '{ "name": "a" }', 323 | }, 324 | }, 325 | }) 326 | 327 | const invalid = [ 328 | () => mapWorkspaces({ 329 | cwd, 330 | pkg: { 331 | workspaces: { 332 | packages: 'packages/*', 333 | }, 334 | }, 335 | }), 336 | () => mapWorkspaces({ 337 | cwd, 338 | pkg: { 339 | workspaces: 'packages/*', 340 | }, 341 | }), 342 | () => mapWorkspaces({ 343 | cwd, 344 | pkg: { 345 | workspaces: { 346 | packages: '', 347 | }, 348 | }, 349 | }), 350 | () => mapWorkspaces({ 351 | cwd, 352 | pkg: { 353 | workspaces: '', 354 | }, 355 | }), 356 | () => mapWorkspaces({ 357 | cwd, 358 | pkg: { 359 | workspaces: NaN, 360 | }, 361 | }), 362 | () => mapWorkspaces({ 363 | cwd, 364 | pkg: { 365 | workspaces: 0, 366 | }, 367 | }), 368 | ] 369 | 370 | for (const i of invalid) { 371 | await t.rejects( 372 | i(), 373 | { code: 'EWORKSPACESCONFIG' }, 374 | 'should throw workspaces config error' 375 | ) 376 | } 377 | 378 | t.end() 379 | }) 380 | 381 | test('no cwd provided', async t => { 382 | const cwd = t.testdir({ 383 | packages: { 384 | a: { 385 | 'package.json': '{ "name": "a" }', 386 | }, 387 | }, 388 | }) 389 | 390 | const _cwd = process.cwd() 391 | process.chdir(cwd) 392 | t.teardown(() => { 393 | process.chdir(_cwd) 394 | }) 395 | 396 | const map = await mapWorkspaces({ 397 | pkg: { 398 | workspaces: ['packages/*'], 399 | }, 400 | }) 401 | t.ok(map.has('a'), 'has package name key') 402 | t.matchSnapshot(map.get('a'), 'value is pkg pathname') 403 | }) 404 | 405 | test('no package name', t => { 406 | const cwd = t.testdir({ 407 | packages: { 408 | a: { 409 | 'package.json': '{ "version": "1.0.0" }', 410 | }, 411 | b: { 412 | 'package.json': '{ "name": "", "version": "1.0.0" }', 413 | }, 414 | '@foo': { 415 | bar: { 416 | 'package.json': '{ "version": "1.0.0" }', 417 | }, 418 | }, 419 | }, 420 | }) 421 | 422 | return t.resolveMatchSnapshot( 423 | mapWorkspaces({ 424 | cwd, 425 | pkg: { 426 | workspaces: ['packages/**'], 427 | }, 428 | }), 429 | 'should return map containing valid names as keys' 430 | ) 431 | }) 432 | 433 | test('empty folders', t => { 434 | const cwd = t.testdir({ 435 | a: { 436 | 'package.json': '{ "name": "a" }', 437 | }, 438 | b: { 439 | 'package.json': '{ "name": "b" }', 440 | }, 441 | c: {}, 442 | }) 443 | 444 | return t.resolveMatchSnapshot( 445 | mapWorkspaces({ 446 | cwd, 447 | pkg: { 448 | workspaces: { 449 | packages: [ 450 | 'a', 451 | 'b', 452 | 'c', 453 | ], 454 | }, 455 | }, 456 | }), 457 | 'should ignore empty folders' 458 | ) 459 | }) 460 | 461 | test('unexpected rpj errors', t => { 462 | const cwd = t.testdir({ 463 | a: { 464 | 'package.json': '{ "name": "a" }', 465 | }, 466 | b: { 467 | 'package.json': '{ "name": "b" }', 468 | }, 469 | }) 470 | 471 | const err = new Error('ERR') 472 | err.code = 'ERR' 473 | 474 | const mapW = t.mock('../', { 475 | '@npmcli/package-json': { normalize: () => Promise.reject(err) }, 476 | }) 477 | 478 | return t.rejects( 479 | mapW({ 480 | cwd, 481 | pkg: { 482 | workspaces: { 483 | packages: [ 484 | 'a', 485 | 'b', 486 | ], 487 | }, 488 | }, 489 | }), 490 | err, 491 | 'should reject with unexpected error' 492 | ) 493 | }) 494 | 495 | test('nested glob lookups', t => { 496 | const cwd = t.testdir({ 497 | packages: { 498 | a: { 499 | 'package.json': '{ "name": "a" }', 500 | }, 501 | }, 502 | }) 503 | 504 | return t.resolveMatchSnapshot( 505 | mapWorkspaces({ 506 | cwd, 507 | pkg: { 508 | workspaces: { 509 | packages: [ 510 | 'packages/**', 511 | ], 512 | }, 513 | }, 514 | }), 515 | 'should return a valid map' 516 | ) 517 | }) 518 | 519 | test('use of / at end of defined globs', t => { 520 | const cwd = t.testdir({ 521 | a: { 522 | 'package.json': '{ "name": "a" }', 523 | }, 524 | b: { 525 | 'package.json': '{ "name": "b" }', 526 | }, 527 | }) 528 | 529 | return t.resolveMatchSnapshot( 530 | mapWorkspaces({ 531 | cwd, 532 | pkg: { 533 | workspaces: { 534 | packages: [ 535 | 'a/', 536 | 'b/', 537 | ], 538 | }, 539 | }, 540 | }), 541 | 'should return a valid map' 542 | ) 543 | }) 544 | 545 | test('nested node_modules', t => { 546 | const cwd = t.testdir({ 547 | node_modules: { 548 | d: { 549 | 'package.json': '{ "name": "d" }', 550 | }, 551 | }, 552 | foo: { 553 | bar: { 554 | node_modules: { 555 | f: { 556 | 'package.json': '{ "name": "f" }', 557 | }, 558 | }, 559 | baz: { 560 | e: { 561 | 'package.json': '{ "name": "e" }', 562 | }, 563 | }, 564 | }, 565 | }, 566 | packages: { 567 | node_modules: { 568 | g: { 569 | 'package.json': '{ "name": "g" }', 570 | }, 571 | }, 572 | a: { 573 | 'package.json': '{ "name": "a" }', 574 | node_modules: { 575 | c: { 576 | 'package.json': '{ "name": "c" }', 577 | }, 578 | }, 579 | }, 580 | b: { 581 | 'package.json': '{ "name": "b" }', 582 | }, 583 | }, 584 | }) 585 | 586 | return t.resolveMatchSnapshot( 587 | mapWorkspaces({ 588 | cwd, 589 | pkg: { 590 | workspaces: [ 591 | 'packages/*', 592 | 'foo/**', 593 | ], 594 | }, 595 | }), 596 | 'should ignore packages within node_modules' 597 | ) 598 | }) 599 | 600 | test('root declared within workspaces', t => { 601 | const cwd = t.testdir({ 602 | node_modules: { 603 | b: { 604 | 'package.json': '{ "name": "b" }', 605 | }, 606 | }, 607 | packages: { 608 | a: { 609 | 'package.json': JSON.stringify({ 610 | name: 'a', 611 | dependencies: { 612 | b: '*', 613 | }, 614 | }), 615 | }, 616 | }, 617 | 'package.json': JSON.stringify({ 618 | name: 'root-workspace', 619 | version: '1.0.0', 620 | }), 621 | }) 622 | 623 | return t.resolveMatchSnapshot( 624 | mapWorkspaces({ 625 | cwd, 626 | pkg: { 627 | workspaces: [ 628 | 'packages/*', 629 | '.', 630 | ], 631 | }, 632 | }), 633 | 'should allow the root package to be declared within workspaces' 634 | ) 635 | }) 636 | 637 | test('ignore option', t => { 638 | const cwd = t.testdir({ 639 | foo: { 640 | bar: { 641 | baz: { 642 | e: { 643 | 'package.json': '{ "name": "e" }', 644 | }, 645 | }, 646 | node_modules: { 647 | b: { 648 | 'package.json': '{ "name": "b" }', 649 | }, 650 | }, 651 | }, 652 | }, 653 | packages: { 654 | a: { 655 | 'package.json': '{ "name": "a" }', 656 | node_modules: { 657 | c: { 658 | 'package.json': '{ "name": "c" }', 659 | }, 660 | }, 661 | }, 662 | }, 663 | }) 664 | 665 | return t.resolveMatchSnapshot( 666 | mapWorkspaces({ 667 | cwd, 668 | ignore: ['**/baz/**'], 669 | pkg: { 670 | workspaces: [ 671 | 'packages/*', 672 | 'foo/**', 673 | ], 674 | }, 675 | }), 676 | 'should ignore things from opts.ignore' 677 | ) 678 | }) 679 | 680 | test('negated pattern', t => { 681 | const cwd = t.testdir({ 682 | foo: { 683 | bar: { 684 | baz: { 685 | e: { 686 | 'package.json': '{ "name": "e" }', 687 | }, 688 | }, 689 | node_modules: { 690 | b: { 691 | 'package.json': '{ "name": "b" }', 692 | }, 693 | }, 694 | }, 695 | }, 696 | packages: { 697 | a: { 698 | 'package.json': '{ "name": "a" }', 699 | node_modules: { 700 | c: { 701 | 'package.json': '{ "name": "c" }', 702 | }, 703 | }, 704 | }, 705 | }, 706 | }) 707 | 708 | return t.resolveMatchSnapshot( 709 | mapWorkspaces({ 710 | cwd, 711 | pkg: { 712 | workspaces: [ 713 | 'packages/*', 714 | 'foo/**', 715 | '!**/baz/**', 716 | ], 717 | }, 718 | }), 719 | 'should not include negated patterns' 720 | ) 721 | }) 722 | 723 | test('multiple negated patterns', t => { 724 | const cwd = t.testdir({ 725 | foo: { 726 | bar: { 727 | baz: { 728 | e: { 729 | 'package.json': '{ "name": "e" }', 730 | }, 731 | }, 732 | node_modules: { 733 | b: { 734 | 'package.json': '{ "name": "b" }', 735 | }, 736 | }, 737 | }, 738 | }, 739 | packages: { 740 | a: { 741 | 'package.json': '{ "name": "a" }', 742 | node_modules: { 743 | c: { 744 | 'package.json': '{ "name": "c" }', 745 | }, 746 | }, 747 | }, 748 | }, 749 | }) 750 | 751 | return t.resolveMatchSnapshot( 752 | mapWorkspaces({ 753 | cwd, 754 | pkg: { 755 | workspaces: [ 756 | 'packages/*', 757 | '!foo/**', 758 | 'foo/baz/*', 759 | '!foo/baz/e', 760 | '!packages/a', 761 | ], 762 | }, 763 | }), 764 | 'should not include any negated pattern' 765 | ) 766 | }) 767 | 768 | test('overlapping negated pattern', t => { 769 | const cwd = t.testdir({ 770 | foo: { 771 | bar: { 772 | baz: { 773 | e: { 774 | 'package.json': '{ "name": "e" }', 775 | }, 776 | }, 777 | node_modules: { 778 | b: { 779 | 'package.json': '{ "name": "b" }', 780 | }, 781 | }, 782 | }, 783 | }, 784 | packages: { 785 | a: { 786 | 'package.json': '{ "name": "a" }', 787 | node_modules: { 788 | c: { 789 | 'package.json': '{ "name": "c" }', 790 | }, 791 | }, 792 | }, 793 | }, 794 | }) 795 | 796 | return t.resolveMatchSnapshot( 797 | mapWorkspaces({ 798 | cwd, 799 | pkg: { 800 | workspaces: [ 801 | 'packages/*', 802 | 'foo/**', 803 | '**/baz/**', 804 | '!**/baz/**', 805 | ], 806 | }, 807 | }), 808 | 'should not include negated patterns' 809 | ) 810 | }) 811 | 812 | test('double negated patterns', t => { 813 | const cwd = t.testdir({ 814 | packages: { 815 | a: { 816 | 'package.json': '{ "name": "a" }', 817 | }, 818 | }, 819 | }) 820 | 821 | return t.resolveMatchSnapshot( 822 | mapWorkspaces({ 823 | cwd, 824 | pkg: { 825 | workspaces: [ 826 | '!!packages/a', 827 | ], 828 | }, 829 | }), 830 | 'should include doubly-negated items into resulting map' 831 | ) 832 | }) 833 | 834 | test('triple negated patterns', t => { 835 | const cwd = t.testdir({ 836 | packages: { 837 | a: { 838 | 'package.json': '{ "name": "a" }', 839 | }, 840 | }, 841 | }) 842 | 843 | return t.resolveMatchSnapshot( 844 | mapWorkspaces({ 845 | cwd, 846 | pkg: { 847 | workspaces: [ 848 | 'packages/*', 849 | '!!!packages/a', 850 | ], 851 | }, 852 | }), 853 | 'should exclude thrice-negated items from resulting map' 854 | ) 855 | }) 856 | 857 | test('try to declare node_modules', t => { 858 | const cwd = t.testdir({ 859 | foo: { 860 | bar: { 861 | node_modules: { 862 | b: { 863 | 'package.json': '{ "name": "b" }', 864 | }, 865 | }, 866 | }, 867 | }, 868 | }) 869 | 870 | return t.resolveMatchSnapshot( 871 | mapWorkspaces({ 872 | cwd, 873 | pkg: { 874 | workspaces: [ 875 | 'foo/bar/node_modules/b', 876 | ], 877 | }, 878 | }), 879 | 'should not include declared packages within node_modules' 880 | ) 881 | }) 882 | 883 | test('backslashes are normalized', t => { 884 | const cwd = t.testdir({ 885 | packages: { 886 | a: { 887 | 'package.json': '{ "name": "a" }', 888 | }, 889 | }, 890 | }) 891 | 892 | return t.resolveMatchSnapshot( 893 | mapWorkspaces({ 894 | cwd, 895 | pkg: { 896 | workspaces: [ 897 | 'packages\\*', 898 | ], 899 | }, 900 | }), 901 | 'matches with backslashes' 902 | ) 903 | }) 904 | 905 | test('matched then negated then match again with wildcards', t => { 906 | const cwd = t.testdir({ 907 | packages: { 908 | b: { 909 | a: { 910 | 'package.json': '{ "name": "a" }', 911 | }, 912 | }, 913 | }, 914 | }) 915 | 916 | return t.resolveMatchSnapshot( 917 | mapWorkspaces({ 918 | cwd, 919 | pkg: { 920 | workspaces: [ 921 | 'packages/**', 922 | '!packages/b/**', 923 | ], 924 | }, 925 | }), 926 | 'should exclude item on returned Map' 927 | ) 928 | }) 929 | 930 | test('matched then negated then match again', t => { 931 | const cwd = t.testdir({ 932 | packages: { 933 | b: { 934 | a: { 935 | 'package.json': '{ "name": "a" }', 936 | }, 937 | }, 938 | }, 939 | }) 940 | 941 | return t.resolveMatchSnapshot( 942 | mapWorkspaces({ 943 | cwd, 944 | pkg: { 945 | workspaces: [ 946 | 'packages/**', 947 | '!packages/b/**', 948 | 'packages/b/a', 949 | ], 950 | }, 951 | }), 952 | 'should include item on returned Map' 953 | ) 954 | }) 955 | 956 | test('match duplicates then exclude one', t => { 957 | const cwd = t.testdir({ 958 | packages: { 959 | a: { 960 | 'package.json': '{ "name": "a" }', 961 | }, 962 | b: { 963 | a: { 964 | 'package.json': '{ "name": "a" }', 965 | }, 966 | }, 967 | }, 968 | }) 969 | 970 | return t.resolveMatchSnapshot( 971 | mapWorkspaces({ 972 | cwd, 973 | pkg: { 974 | workspaces: [ 975 | 'packages/**', 976 | '!packages/b/**', 977 | ], 978 | }, 979 | }), 980 | 'should include the non-excluded item on returned Map' 981 | ) 982 | }) 983 | 984 | test('ignore symlink workspace files', t => { 985 | const cwd = t.testdir({ 986 | 'index.js': 'console.log("hi");', 987 | packages: { 988 | a: { 989 | 'package.json': '{ "name": "a" }', 990 | 'index.js': t.fixture('symlink', '../../index.js'), 991 | }, 992 | }, 993 | }) 994 | 995 | return t.resolveMatchSnapshot( 996 | mapWorkspaces({ 997 | cwd, 998 | pkg: { 999 | workspaces: { 1000 | packages: [ 1001 | 'packages/**', 1002 | ], 1003 | }, 1004 | }, 1005 | }), 1006 | 'should not throw an error' 1007 | ) 1008 | }) 1009 | -------------------------------------------------------------------------------- /test/test.virtual.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const { test } = tap 3 | 4 | const mapWorkspaces = require('../') 5 | 6 | tap.cleanSnapshot = str => { 7 | const cleanPath = path => path 8 | .replace(/\\+/g, '/') // normalize slashes 9 | .replace(/"\w:/g, '"') // gets rid of drive letter in snapshot 10 | .replace(/^\w:/g, '') // gets rid of drive letter in cwd/paths 11 | const cwd = cleanPath(process.cwd()) 12 | const pathname = cleanPath(str) 13 | return pathname.split(cwd).join('{CWD}') 14 | } 15 | 16 | test('simple workspaces config', t => { 17 | const cwd = t.testdir() 18 | t.matchSnapshot( 19 | mapWorkspaces.virtual({ 20 | cwd, 21 | lockfile: { 22 | name: 'workspace-simple', 23 | lockfileVersion: 2, 24 | requires: true, 25 | packages: { 26 | '': { 27 | name: 'workspace-simple', 28 | workspaces: { 29 | packages: [ 30 | 'a', 31 | 'b', 32 | ], 33 | }, 34 | }, 35 | a: { 36 | name: 'a', 37 | version: '1.0.0', 38 | dependencies: { 39 | b: '^1.0.0', 40 | }, 41 | }, 42 | b: { 43 | name: 'b', 44 | version: '1.0.0', 45 | }, 46 | 'node_modules/a': { 47 | resolved: 'a', 48 | link: true, 49 | }, 50 | 'node_modules/b': { 51 | resolved: 'b', 52 | link: true, 53 | }, 54 | }, 55 | dependencies: { 56 | a: { 57 | version: 'file:a', 58 | }, 59 | b: { 60 | version: 'file:b', 61 | }, 62 | }, 63 | }, 64 | }), 65 | 'should return a valid map' 66 | ) 67 | t.end() 68 | }) 69 | 70 | test('unexpected lockfile info', t => { 71 | const cwd = t.testdir() 72 | 73 | const results = [ 74 | mapWorkspaces.virtual({ 75 | cwd, 76 | lockfile: 1, 77 | }), 78 | mapWorkspaces.virtual({ 79 | cwd, 80 | lockfile: 'foo', 81 | }), 82 | mapWorkspaces.virtual({ 83 | cwd, 84 | lockfile: {}, 85 | }), 86 | ] 87 | t.matchSnapshot(results, 'should return an empty map') 88 | t.end() 89 | }) 90 | 91 | test('invalid options', t => { 92 | const invalid = [ 93 | () => mapWorkspaces.virtual({}), 94 | () => mapWorkspaces.virtual({ lockfile: null }), 95 | () => mapWorkspaces.virtual([]), 96 | () => mapWorkspaces.virtual('foo'), 97 | () => mapWorkspaces.virtual(1), 98 | () => mapWorkspaces.virtual(NaN), 99 | () => mapWorkspaces.virtual(null), 100 | () => mapWorkspaces.virtual(), 101 | ] 102 | 103 | for (const i of invalid) { 104 | t.throws( 105 | i, 106 | { code: 'EMAPWORKSPACESLOCKFILE' }, 107 | 'should throw a TypeError' 108 | ) 109 | } 110 | 111 | t.end() 112 | }) 113 | 114 | test('no cwd provided', t => { 115 | const cwd = t.testdir() 116 | 117 | const processCwd = process.cwd 118 | process.cwd = () => { 119 | process.cwd = processCwd 120 | t.ok('should default to process.cwd()') 121 | t.end() 122 | return cwd 123 | } 124 | 125 | mapWorkspaces.virtual({ 126 | lockfile: { 127 | name: 'workspace-simple', 128 | packages: { 129 | '': { 130 | name: 'workspace-simple', 131 | workspaces: { 132 | packages: [ 133 | 'a', 134 | 'b', 135 | ], 136 | }, 137 | }, 138 | a: { 139 | name: 'a', 140 | version: '1.0.0', 141 | }, 142 | }, 143 | }, 144 | }) 145 | }) 146 | 147 | test('should ignore nested node_modules', t => { 148 | const cwd = t.testdir() 149 | t.matchSnapshot( 150 | mapWorkspaces.virtual({ 151 | cwd, 152 | lockfile: { 153 | name: 'workspace-ignore-nm', 154 | lockfileVersion: 2, 155 | requires: true, 156 | packages: { 157 | '': { 158 | name: 'workspace-ignore-nm', 159 | workspaces: { 160 | packages: [ 161 | 'packages/**', 162 | ], 163 | }, 164 | }, 165 | 'node_modules/a': { 166 | resolved: 'packages/a', 167 | link: true, 168 | }, 169 | 'packages/a': { 170 | name: 'a', 171 | version: '1.0.0', 172 | }, 173 | 'packages/a/node_modules/not-a-workspace': { 174 | name: 'not-a-workspace', 175 | }, 176 | }, 177 | dependencies: { 178 | a: { 179 | version: 'file:packages/a', 180 | }, 181 | }, 182 | }, 183 | }), 184 | 'should return a valid map' 185 | ) 186 | t.end() 187 | }) 188 | 189 | test('transitive dependencies', t => { 190 | const cwd = t.testdir() 191 | t.matchSnapshot( 192 | mapWorkspaces.virtual({ 193 | cwd, 194 | lockfile: { 195 | name: 'workspaces-transitive-deps', 196 | version: '1.0.0', 197 | lockfileVersion: 2, 198 | requires: true, 199 | packages: { 200 | '': { 201 | name: 'workspaces-transitive-deps', 202 | version: '1.0.0', 203 | workspaces: [ 204 | 'packages/**', 205 | ], 206 | }, 207 | 'node_modules/a': { 208 | resolved: 'packages/a', 209 | link: true, 210 | }, 211 | 'node_modules/once': { 212 | name: 'once', 213 | version: '1.4.0', 214 | resolved: 'https://registry.npmjs.org/once/-/once-1.4.0.tgz', 215 | integrity: 'sha1-WDsap3WWHUsROsF9nFC6753Xa9E=', 216 | dev: true, 217 | dependencies: { 218 | wrappy: '1', 219 | }, 220 | }, 221 | 'node_modules/wrappy': { 222 | name: 'wrappy', 223 | version: '1.0.2', 224 | resolved: 'https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz', 225 | integrity: 'sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=', 226 | dev: true, 227 | }, 228 | 'packages/a': { 229 | name: 'a', 230 | version: '1.0.0', 231 | devDependencies: { 232 | once: '^1.4.0', 233 | }, 234 | }, 235 | }, 236 | dependencies: { 237 | a: { 238 | version: 'file:packages/a', 239 | }, 240 | once: { 241 | version: '1.4.0', 242 | resolved: 'https://registry.npmjs.org/once/-/once-1.4.0.tgz', 243 | integrity: 'sha1-WDsap3WWHUsROsF9nFC6753Xa9E=', 244 | dev: true, 245 | requires: { 246 | wrappy: '1', 247 | }, 248 | }, 249 | wrappy: { 250 | version: '1.0.2', 251 | resolved: 'https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz', 252 | integrity: 'sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=', 253 | dev: true, 254 | }, 255 | }, 256 | }, 257 | }), 258 | 'should return a map containing only the valid workspaces' 259 | ) 260 | t.end() 261 | }) 262 | 263 | test('negate globs in workspaces config', t => { 264 | const cwd = t.testdir() 265 | t.matchSnapshot( 266 | mapWorkspaces.virtual({ 267 | cwd, 268 | lockfile: { 269 | name: 'negate-glob-example', 270 | lockfileVersion: 2, 271 | requires: true, 272 | packages: { 273 | '': { 274 | name: 'negate-glob-example', 275 | workspaces: { 276 | packages: [ 277 | 'packages/*', 278 | '!packages/b', 279 | ], 280 | }, 281 | }, 282 | 'packages/a': { 283 | name: 'a', 284 | version: '1.0.0', 285 | }, 286 | 'packages/b': { 287 | name: 'b', 288 | version: '1.0.0', 289 | }, 290 | 'node_modules/a': { 291 | resolved: 'packages/a', 292 | link: true, 293 | }, 294 | }, 295 | dependencies: { 296 | a: { 297 | version: 'file:packages/a', 298 | }, 299 | b: { 300 | version: 'file:packages/b', 301 | }, 302 | }, 303 | }, 304 | }), 305 | 'should not return negated workspaces' 306 | ) 307 | t.end() 308 | }) 309 | 310 | test('double-negated', t => { 311 | const cwd = t.testdir() 312 | t.matchSnapshot( 313 | mapWorkspaces.virtual({ 314 | cwd, 315 | lockfile: { 316 | name: 'negate-glob-example', 317 | lockfileVersion: 2, 318 | requires: true, 319 | packages: { 320 | '': { 321 | name: 'negate-glob-example', 322 | workspaces: { 323 | packages: [ 324 | 'packages/*', 325 | '!!packages/b', 326 | ], 327 | }, 328 | }, 329 | 'packages/a': { 330 | name: 'a', 331 | version: '1.0.0', 332 | }, 333 | 'packages/b': { 334 | name: 'b', 335 | version: '1.0.0', 336 | }, 337 | 'node_modules/a': { 338 | resolved: 'packages/a', 339 | link: true, 340 | }, 341 | }, 342 | dependencies: { 343 | a: { 344 | version: 'file:packages/a', 345 | }, 346 | b: { 347 | version: 'file:packages/b', 348 | }, 349 | }, 350 | }, 351 | }), 352 | 'should return the doubly-negated item as part of the Map' 353 | ) 354 | t.end() 355 | }) 356 | 357 | test('triple-negated', t => { 358 | const cwd = t.testdir() 359 | t.matchSnapshot( 360 | mapWorkspaces.virtual({ 361 | cwd, 362 | lockfile: { 363 | name: 'negate-glob-example', 364 | lockfileVersion: 2, 365 | requires: true, 366 | packages: { 367 | '': { 368 | name: 'negate-glob-example', 369 | workspaces: { 370 | packages: [ 371 | 'packages/*', 372 | '!!!packages/b', 373 | ], 374 | }, 375 | }, 376 | 'packages/a': { 377 | name: 'a', 378 | version: '1.0.0', 379 | }, 380 | 'packages/b': { 381 | name: 'b', 382 | version: '1.0.0', 383 | }, 384 | 'node_modules/a': { 385 | resolved: 'packages/a', 386 | link: true, 387 | }, 388 | }, 389 | dependencies: { 390 | a: { 391 | version: 'file:packages/a', 392 | }, 393 | b: { 394 | version: 'file:packages/b', 395 | }, 396 | }, 397 | }, 398 | }), 399 | 'should exclude that item from returned Map' 400 | ) 401 | t.end() 402 | }) 403 | 404 | test('matched then negated then match again', t => { 405 | const cwd = t.testdir() 406 | t.matchSnapshot( 407 | mapWorkspaces.virtual({ 408 | cwd, 409 | lockfile: { 410 | name: 'negate-glob-example', 411 | lockfileVersion: 2, 412 | requires: true, 413 | packages: { 414 | '': { 415 | name: 'negate-glob-example', 416 | workspaces: { 417 | packages: [ 418 | 'packages/*', 419 | '!packages/b', 420 | 'packages/b', 421 | ], 422 | }, 423 | }, 424 | 'packages/a': { 425 | name: 'a', 426 | version: '1.0.0', 427 | }, 428 | 'packages/b': { 429 | name: 'b', 430 | version: '1.0.0', 431 | }, 432 | 'node_modules/a': { 433 | resolved: 'packages/a', 434 | link: true, 435 | }, 436 | }, 437 | dependencies: { 438 | a: { 439 | version: 'file:packages/a', 440 | }, 441 | b: { 442 | version: 'file:packages/b', 443 | }, 444 | }, 445 | }, 446 | }), 447 | 'should include item on returned Map' 448 | ) 449 | t.end() 450 | }) 451 | 452 | test('matched then negated then match again then negate again', t => { 453 | const cwd = t.testdir() 454 | t.matchSnapshot( 455 | mapWorkspaces.virtual({ 456 | cwd, 457 | lockfile: { 458 | name: 'negate-glob-example', 459 | lockfileVersion: 2, 460 | requires: true, 461 | packages: { 462 | '': { 463 | name: 'negate-glob-example', 464 | workspaces: { 465 | packages: [ 466 | 'packages/**', 467 | '!packages/foo', 468 | 'packages/foo/*', 469 | '!packages/foo/b', 470 | ], 471 | }, 472 | }, 473 | 'packages/a': { 474 | name: 'a', 475 | version: '1.0.0', 476 | }, 477 | 'packages/foo/b': { 478 | name: 'b', 479 | version: '1.0.0', 480 | }, 481 | 'node_modules/a': { 482 | resolved: 'packages/a', 483 | link: true, 484 | }, 485 | }, 486 | dependencies: { 487 | a: { 488 | version: 'file:packages/a', 489 | }, 490 | b: { 491 | version: 'file:packages/foo/b', 492 | }, 493 | }, 494 | }, 495 | }), 496 | 'should exclude negated item from returned Map' 497 | ) 498 | t.end() 499 | }) 500 | --------------------------------------------------------------------------------