├── .release-please-manifest.json ├── .npmrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml ├── workflows │ ├── codeql-analysis.yml │ ├── audit.yml │ ├── pull-request.yml │ ├── release-integration.yml │ ├── ci.yml │ ├── ci-release.yml │ ├── post-dependabot.yml │ └── release.yml └── actions │ ├── create-check │ └── action.yml │ └── install-latest-npm │ └── action.yml ├── CODE_OF_CONDUCT.md ├── .commitlintrc.js ├── .eslintrc.js ├── test ├── sorting.js ├── cannot-exclude-package-json.js ├── callbacks.js ├── can-exclude-shrinkwrap.js ├── constructor.js ├── cannot-include-package-lock.js ├── package-json-negated-files.js ├── package-json-filed-dir-nested-npmignore.js ├── package-json-files-no-dir-nested-npmignore.js ├── package-json-nested.js ├── cannot-exclude-readme.js ├── package-json-files-including-npmignore.js ├── package-json-files-dir-and-nested-ignore.js ├── package-json-bin-single.js ├── package-json-files-nested-dir-and-nested-ignore.js ├── bundle-missing-dep.js ├── package-json-bin-multiple.js ├── package-json-files-and-containing-dir.js ├── package-json-directory-glob.js ├── package-json-dir-with-slashes.js ├── package-not-json.js ├── package-json-files-with-slashes.js ├── bundled-scoped.js ├── scoped.js ├── bundled-missing-optional.js ├── utils │ └── bin.js ├── nested-lock-and-core.js ├── package-json-empty.js ├── include-gitignore.js ├── bundled-cycle.js ├── empty-npmignore.js ├── package-json.js ├── package-json-nested-readme-include-npmignore.js ├── strip-slash-from-package-files-list-entries.js ├── bundled-symlink.js ├── bundled-workspace.js ├── package-json-nested-readme.js ├── package-json-main.js ├── bundled-scoped-symlink.js ├── ignore-file-included-by-globstar.js ├── package-json-negate-non-root.js ├── cannot-include-non-file-or-directory.js ├── package-json-roots-and-nests.js ├── star-names.js ├── symlink.js ├── bundled.js ├── ignores.js ├── bundled-files.js ├── bundled-file-in-workspace.js ├── bin.js └── workspace.js ├── .gitignore ├── LICENSE ├── release-please-config.json ├── SECURITY.md ├── package.json ├── CONTRIBUTING.md ├── README.md ├── CHANGELOG.md └── lib └── index.js /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "10.0.3" 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /test/sorting.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const Walker = require('../').Walker 5 | const sort = Walker.prototype.sort 6 | 7 | t.same([ 8 | 'a/b/1', 9 | 'cat', 10 | 'dog', 11 | 'chai', 12 | 'package.json', 13 | 'node_modules/b/c', 14 | 'asdf.js', 15 | 'node_modules/a/b/c', 16 | 'a/b/c/d/e/f/g', 17 | 'a/b/x/y', 18 | ].sort(sort), [ 19 | 'a/b/1', 20 | 'node_modules/a/b/c', 21 | 'node_modules/b/c', 22 | 'cat', 23 | 'chai', 24 | 'dog', 25 | 'a/b/c/d/e/f/g', 26 | 'a/b/x/y', 27 | 'asdf.js', 28 | 'package.json', 29 | ]) 30 | -------------------------------------------------------------------------------- /test/cannot-exclude-package-json.js: -------------------------------------------------------------------------------- 1 | // cannot exclude package.json in the root 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const t = require('tap') 6 | const packlist = require('../') 7 | 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | files: ['.npmignore', '!package.json'], 11 | }), 12 | '.npmignore': 'package.json', 13 | }) 14 | 15 | t.test('try to exclude package.json but cannot', async (t) => { 16 | const arborist = new Arborist({ path: pkg }) 17 | const tree = await arborist.loadActual() 18 | const files = await packlist(tree) 19 | t.same(files, [ 20 | '.npmignore', 21 | 'package.json', 22 | ]) 23 | }) 24 | -------------------------------------------------------------------------------- /test/callbacks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | name: 'test', 10 | }), 11 | }) 12 | 13 | t.test('export supports callbacks', async (t) => { 14 | const arborist = new Arborist({ path: pkg }) 15 | const tree = await arborist.loadActual() 16 | 17 | return new Promise((resolve, reject) => { 18 | packlist(tree, (err, files) => { 19 | if (err) { 20 | reject(err) 21 | } 22 | 23 | t.same(files, [ 24 | 'package.json', 25 | ]) 26 | resolve(files) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/can-exclude-shrinkwrap.js: -------------------------------------------------------------------------------- 1 | // can exclude npm-shrinkwrap.json in the root 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const t = require('tap') 6 | const packlist = require('../') 7 | 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | files: ['.npmignore', '!npm-shrinkwrap.json'], 11 | }), 12 | '.npmignore': 'npm-shrinkwrap.json', 13 | 'npm-shrinkwrap.json': '{}', 14 | }) 15 | 16 | t.test('package with negated files', async (t) => { 17 | const arborist = new Arborist({ path: pkg }) 18 | const tree = await arborist.loadActual() 19 | const files = await packlist(tree) 20 | t.same(files, [ 21 | '.npmignore', 22 | 'package.json', 23 | ]) 24 | }) 25 | -------------------------------------------------------------------------------- /test/constructor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | name: 'test', 10 | }), 11 | }) 12 | 13 | t.test('Walker constructor allows ommitting options entirely', async (t) => { 14 | const arborist = new Arborist({ path: pkg }) 15 | const tree = await arborist.loadActual() 16 | const walker = new packlist.Walker(tree) 17 | const files = await new Promise((resolve, reject) => { 18 | walker 19 | .on('done', resolve) 20 | .on('error', reject) 21 | .start() 22 | }) 23 | t.same(files, [ 24 | 'package.json', 25 | ]) 26 | }) 27 | -------------------------------------------------------------------------------- /test/cannot-include-package-lock.js: -------------------------------------------------------------------------------- 1 | // cannot include package-lock.json in the root 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const t = require('tap') 6 | const packlist = require('../') 7 | 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | files: ['.npmignore', 'package-lock.json'], 11 | }), 12 | '.npmignore': ` 13 | !package-lock.json 14 | `, 15 | 'package-lock.json': '{}', 16 | }) 17 | 18 | t.test('try to include package-lock.json but cannot', async (t) => { 19 | const arborist = new Arborist({ path: pkg }) 20 | const tree = await arborist.loadActual() 21 | const files = await packlist(tree) 22 | t.same(files, [ 23 | '.npmignore', 24 | 'package.json', 25 | ]) 26 | }) 27 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/package-json-negated-files.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | files: [ 10 | 'lib', 11 | '!lib/one', 12 | ], 13 | }), 14 | lib: { 15 | one: 'one', 16 | two: 'two', 17 | tre: 'tre', 18 | for: 'for', 19 | '.npmignore': 'two', 20 | '.DS_Store': 'a store of ds', 21 | }, 22 | }) 23 | 24 | t.test('package with negated files', async (t) => { 25 | const arborist = new Arborist({ path: pkg }) 26 | const tree = await arborist.loadActual() 27 | const files = await packlist(tree) 28 | t.same(files, [ 29 | 'lib/for', 30 | 'lib/tre', 31 | 'package.json', 32 | ]) 33 | }) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/package-json-filed-dir-nested-npmignore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | files: [ 10 | 'lib', 11 | ], 12 | }), 13 | lib: { 14 | 'one.js': 'one', 15 | 'two.js': 'two', 16 | 'tre.js': 'tre', 17 | 'for.js': 'for', 18 | '.npmignore': 'two.js', 19 | '.DS_Store': 'a store of ds', 20 | }, 21 | }) 22 | 23 | t.test('package with negated files', async (t) => { 24 | const arborist = new Arborist({ path: pkg }) 25 | const tree = await arborist.loadActual() 26 | const files = await packlist(tree) 27 | t.same(files, [ 28 | 'lib/for.js', 29 | 'lib/one.js', 30 | 'lib/tre.js', 31 | 'package.json', 32 | ]) 33 | }) 34 | -------------------------------------------------------------------------------- /test/package-json-files-no-dir-nested-npmignore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | files: [ 10 | 'lib/*.js', 11 | ], 12 | }), 13 | lib: { 14 | 'one.js': 'one', 15 | 'two.js': 'two', 16 | 'tre.js': 'tre', 17 | 'for.js': 'for', 18 | '.npmignore': 'two.js', 19 | '.DS_Store': 'a store of ds', 20 | }, 21 | }) 22 | 23 | t.test('package with negated files', async (t) => { 24 | const arborist = new Arborist({ path: pkg }) 25 | const tree = await arborist.loadActual() 26 | const files = await packlist(tree) 27 | t.same(files, [ 28 | 'lib/for.js', 29 | 'lib/one.js', 30 | 'lib/tre.js', 31 | 'package.json', 32 | ]) 33 | }) 34 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/package-json-nested.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | name: 'test-package', 10 | version: '1.2.3', 11 | }), 12 | nest: { 13 | 'package.json': JSON.stringify({ 14 | name: 'nested-package', 15 | version: '1.2.3', 16 | files: ['index.js'], 17 | }), 18 | 'index.js': 'console.log("hi")', 19 | 'foo.js': 'console.log("no")', 20 | }, 21 | }) 22 | 23 | t.test('includes nested package.json file', async (t) => { 24 | const arborist = new Arborist({ path: pkg }) 25 | const tree = await arborist.loadActual() 26 | const files = await packlist(tree) 27 | t.same(files, [ 28 | 'nest/foo.js', 29 | 'nest/index.js', 30 | 'nest/package.json', 31 | 'package.json', 32 | ]) 33 | }) 34 | -------------------------------------------------------------------------------- /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 | "prerelease": false 35 | } 36 | }, 37 | "prerelease-type": "pre.0" 38 | } 39 | -------------------------------------------------------------------------------- /test/cannot-exclude-readme.js: -------------------------------------------------------------------------------- 1 | // cannot exclude readme.md in the root 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const t = require('tap') 6 | const packlist = require('../') 7 | 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | files: ['.npmignore', '!readme.md'], 11 | }), 12 | '.npmignore': 'readme.md\nlicense.md\nhistory.md\n*.xyz', 13 | 'readme.md': 'hello', 14 | 'license.md': 'hello', 15 | 'history.md': 'hello', 16 | 'changes.md': 'hello', 17 | 'changelog.xyz': 'hello', 18 | 'notice.md': 'hello', 19 | }) 20 | 21 | t.test('try to exclude package.json but cannot', async (t) => { 22 | const arborist = new Arborist({ path: pkg }) 23 | const tree = await arborist.loadActual() 24 | const files = await packlist(tree) 25 | t.same(files, [ 26 | '.npmignore', 27 | 'package.json', 28 | 'license.md', 29 | 'readme.md', 30 | ]) 31 | }) 32 | -------------------------------------------------------------------------------- /test/package-json-files-including-npmignore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | files: [ 10 | 'lib/sub/*.js', 11 | 'lib/.npmignore', 12 | ], 13 | }), 14 | lib: { 15 | '.npmignore': 'two.js', 16 | sub: { 17 | 'one.js': 'one', 18 | 'two.js': 'two', 19 | 'tre.js': 'tre', 20 | 'for.js': 'for', 21 | }, 22 | '.DS_Store': 'a store of ds', 23 | }, 24 | }) 25 | 26 | t.test('package with negated files', async (t) => { 27 | const arborist = new Arborist({ path: pkg }) 28 | const tree = await arborist.loadActual() 29 | const files = await packlist(tree) 30 | t.same(files, [ 31 | 'lib/.npmignore', 32 | 'lib/sub/for.js', 33 | 'lib/sub/one.js', 34 | 'lib/sub/tre.js', 35 | 'package.json', 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /test/package-json-files-dir-and-nested-ignore.js: -------------------------------------------------------------------------------- 1 | // Show that a nested .npmignore will be honored if the 2 | // 'files' array matches a dir, but not any specific files. 3 | 'use strict' 4 | 5 | const Arborist = require('@npmcli/arborist') 6 | const t = require('tap') 7 | const packlist = require('../') 8 | 9 | const pkg = t.testdir({ 10 | 'package.json': JSON.stringify({ 11 | files: [ 12 | 'lib', 13 | ], 14 | }), 15 | lib: { 16 | 'one.js': 'one', 17 | 'two.js': 'two', 18 | 'tre.js': 'tre', 19 | 'for.js': 'for', 20 | '.npmignore': 'two.js', 21 | '.DS_Store': 'a store of ds', 22 | }, 23 | }) 24 | 25 | t.test('package with negated files', async (t) => { 26 | const arborist = new Arborist({ path: pkg }) 27 | const tree = await arborist.loadActual() 28 | const files = await packlist(tree) 29 | t.same(files, [ 30 | 'lib/for.js', 31 | 'lib/one.js', 32 | 'lib/tre.js', 33 | 'package.json', 34 | ]) 35 | }) 36 | -------------------------------------------------------------------------------- /test/package-json-bin-single.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const bin = ` 8 | #!/usr/bin/env node 9 | require("../lib/elf")() 10 | ` 11 | 12 | const elfJS = ` 13 | module.exports = elf => 14 | console.log("i'm angry about elves") 15 | ` 16 | 17 | const pkg = t.testdir({ 18 | 'package.json': JSON.stringify({ 19 | name: 'test-package', 20 | version: '1.6.2', 21 | bin: '__bin', 22 | files: [ 23 | 'lib', 24 | ], 25 | }), 26 | __bin: bin, 27 | lib: { 28 | 'elf.js': elfJS, 29 | }, 30 | dummy: 'ignore', 31 | }) 32 | 33 | t.test('follows npm package ignoring rules', async (t) => { 34 | const arborist = new Arborist({ path: pkg }) 35 | const tree = await arborist.loadActual() 36 | const files = await packlist(tree) 37 | t.same(files, [ 38 | '__bin', 39 | 'lib/elf.js', 40 | 'package.json', 41 | ]) 42 | }) 43 | -------------------------------------------------------------------------------- /test/package-json-files-nested-dir-and-nested-ignore.js: -------------------------------------------------------------------------------- 1 | // Show that a nested .npmignore will be skipped over if the 2 | // 'files' array matches a dir that passes by it. 3 | 'use strict' 4 | 5 | const Arborist = require('@npmcli/arborist') 6 | const t = require('tap') 7 | const packlist = require('../') 8 | 9 | const pkg = t.testdir({ 10 | 'package.json': JSON.stringify({ 11 | files: [ 12 | 'lib/dir', 13 | ], 14 | }), 15 | lib: { 16 | dir: { 17 | 'one.js': 'one', 18 | 'two.js': 'two', 19 | 'tre.js': 'tre', 20 | 'for.js': 'for', 21 | }, 22 | '.npmignore': 'dir/two.js', 23 | '.DS_Store': 'a store of ds', 24 | }, 25 | }) 26 | 27 | t.test('package with negated files', async (t) => { 28 | const arborist = new Arborist({ path: pkg }) 29 | const tree = await arborist.loadActual() 30 | const files = await packlist(tree) 31 | t.same(files, [ 32 | 'lib/dir/for.js', 33 | 'lib/dir/one.js', 34 | 'lib/dir/tre.js', 35 | 'package.json', 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /test/bundle-missing-dep.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | t.test('skips bundling deps with missing edges', async (t) => { 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | name: 'test', 11 | version: '1.0.0', 12 | main: 'index.js', 13 | // named in bundleDependencies, but not actually a dependency 14 | bundleDependencies: ['history'], 15 | }), 16 | 'index.js': '', 17 | node_modules: { 18 | history: { 19 | 'package.json': JSON.stringify({ 20 | name: 'history', 21 | version: '1.0.0', 22 | main: 'index.js', 23 | }), 24 | 'index.js': '', 25 | }, 26 | }, 27 | }) 28 | const arborist = new Arborist({ path: pkg }) 29 | const tree = await arborist.loadActual() 30 | 31 | const files = await packlist(tree) 32 | t.same(files, [ 33 | 'index.js', 34 | 'package.json', 35 | ]) 36 | }) 37 | -------------------------------------------------------------------------------- /test/package-json-bin-multiple.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const bin = ` 8 | #!/usr/bin/env node 9 | require("../lib/elf")() 10 | ` 11 | 12 | const elfJS = ` 13 | module.exports = elf => 14 | console.log("i'm angry about elves") 15 | ` 16 | 17 | const pkg = t.testdir({ 18 | 'package.json': JSON.stringify({ 19 | name: 'test-package', 20 | version: '1.6.2', 21 | bin: { 22 | bar: '__bin_bar', 23 | foo: '__bin_foo', 24 | }, 25 | files: [ 26 | 'lib', 27 | ], 28 | }), 29 | __bin_foo: bin, 30 | __bin_bar: bin, 31 | lib: { 32 | 'elf.js': elfJS, 33 | }, 34 | dummy: 'ignore this', 35 | }) 36 | 37 | t.test('follows npm package ignoring rules', async (t) => { 38 | const arborist = new Arborist({ path: pkg }) 39 | const tree = await arborist.loadActual() 40 | const files = await packlist(tree) 41 | t.same(files, [ 42 | '__bin_bar', 43 | '__bin_foo', 44 | 'lib/elf.js', 45 | 'package.json', 46 | ]) 47 | }) 48 | -------------------------------------------------------------------------------- /test/package-json-files-and-containing-dir.js: -------------------------------------------------------------------------------- 1 | // ensure we don't get files more than one time, even if specified 2 | // in ways that will have them included from multiple directions 3 | 'use strict' 4 | 5 | const Arborist = require('@npmcli/arborist') 6 | const t = require('tap') 7 | const packlist = require('../') 8 | 9 | const pkg = t.testdir({ 10 | 'package.json': JSON.stringify({ 11 | files: [ 12 | 'lib', 13 | '/lib/*.js', 14 | 'lib/*.js', 15 | '/lib/one.js', 16 | 'lib/one.js', 17 | 'lib/one.*', 18 | ], 19 | }), 20 | lib: { 21 | 'one.js': 'one', 22 | 'two.js': 'two', 23 | 'tre.js': 'tre', 24 | 'for.js': 'for', 25 | '.npmignore': 'two.js', 26 | '.DS_Store': 'a store of ds', 27 | }, 28 | }) 29 | 30 | t.test('package with negated files', async (t) => { 31 | const arborist = new Arborist({ path: pkg }) 32 | const tree = await arborist.loadActual() 33 | const files = await packlist(tree) 34 | t.same(files, [ 35 | 'lib/for.js', 36 | 'lib/one.js', 37 | 'lib/tre.js', 38 | 'package.json', 39 | ]) 40 | }) 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/package-json-directory-glob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('..') 6 | 7 | const createTestdir = (...files) => t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | files, 10 | }), 11 | folder: { 12 | one: { file: 'one' }, 13 | two: { file: 'two' }, 14 | }, 15 | folder1: { 16 | one: { file: 'one' }, 17 | two: { file: 'two' }, 18 | }, 19 | }) 20 | 21 | t.test('package json directory glob', async (t) => { 22 | const pkgFiles = [ 23 | 'folder', 24 | 'folder/', 25 | 'folder/*', 26 | 'folder/**', 27 | 'folder/**/*', 28 | './folder/*', 29 | ] 30 | 31 | for (const files of pkgFiles) { 32 | await t.test(files, async t => { 33 | const pkg = createTestdir(files) 34 | const arborist = new Arborist({ path: pkg }) 35 | const tree = await arborist.loadActual() 36 | const res = await packlist(tree) 37 | t.same(res, [ 38 | 'folder/one/file', 39 | 'folder/two/file', 40 | 'package.json', 41 | ]) 42 | }) 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /test/package-json-dir-with-slashes.js: -------------------------------------------------------------------------------- 1 | // In v1, this would exclude the 'lib/two.js' file, because 2 | // the .npmignore is deeper in the tree and thus had higher 3 | // precedence. In v2, because /lib/two.js is in the files 4 | // list as a file path, it will be included no matter what. 5 | 'use strict' 6 | 7 | const Arborist = require('@npmcli/arborist') 8 | const t = require('tap') 9 | const packlist = require('../') 10 | 11 | const pkg = t.testdir({ 12 | 'package.json': JSON.stringify({ 13 | files: [ 14 | '/lib', 15 | './lib2', 16 | './lib3/*', 17 | ], 18 | }), 19 | lib: { 20 | 'one.js': 'one', 21 | 'two.js': 'two', 22 | 'tre.js': 'tre', 23 | 'for.js': 'for', 24 | '.npmignore': 'two.js', 25 | }, 26 | lib2: { 27 | 'fiv.js': 'fiv', 28 | '.DS_Store': 'a store of ds', 29 | }, 30 | lib3: 'not a dir', 31 | }) 32 | 33 | t.test('package with slash directories', async (t) => { 34 | const arborist = new Arborist({ path: pkg }) 35 | const tree = await arborist.loadActual() 36 | const files = await packlist(tree) 37 | t.same(files, [ 38 | 'lib2/fiv.js', 39 | 'lib/for.js', 40 | 'lib/one.js', 41 | 'lib/tre.js', 42 | 'package.json', 43 | ]) 44 | }) 45 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /test/package-not-json.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': "c'est ne pas une j'son", 14 | 'elf.js': elfJS, 15 | '.npmrc': 'packaged=false', 16 | '.npmignore': '.npmignore\ndummy\npackage.json\n', 17 | dummy: 'foo', 18 | build: { 19 | 'config.gypi': "i_wont_be_included='with any luck'", 20 | 'npm-debug.log': '0 lol\n', 21 | }, 22 | deps: { 23 | foo: { 24 | config: { 25 | 'config.gypi': "i_will_be_included='with any luck'", 26 | }, 27 | }, 28 | }, 29 | '.git': { 30 | gitstub: "won't fool git, also won't be included", 31 | }, 32 | node_modules: { 33 | history: { 34 | 'README.md': "please don't include me", 35 | }, 36 | }, 37 | }) 38 | 39 | t.test('follows npm package ignoring rules', async (t) => { 40 | const arborist = new Arborist({ path: pkg }) 41 | const tree = await arborist.loadActual() 42 | const files = await packlist(tree) 43 | t.same(files, [ 44 | 'deps/foo/config/config.gypi', 45 | 'elf.js', 46 | 'package.json', 47 | ]) 48 | }) 49 | -------------------------------------------------------------------------------- /test/package-json-files-with-slashes.js: -------------------------------------------------------------------------------- 1 | // In v1, this would exclude the 'lib/two.js' file, because 2 | // the .npmignore is deeper in the tree and thus had higher 3 | // precedence. In v2, because /lib/two.js is in the files 4 | // list as a file path, it will be included no matter what. 5 | 'use strict' 6 | 7 | const Arborist = require('@npmcli/arborist') 8 | const t = require('tap') 9 | const packlist = require('../') 10 | 11 | const pkg = t.testdir({ 12 | 'package.json': JSON.stringify({ 13 | files: [ 14 | './fiv.js', 15 | '/lib/one.js', 16 | '/lib/two.js', 17 | '/lib/tre.js', 18 | './lib/for.js', 19 | ], 20 | }), 21 | 'fiv.js': 'fiv', 22 | lib: { 23 | 'one.js': 'one', 24 | 'two.js': 'two', 25 | 'tre.js': 'tre', 26 | 'for.js': 'for', 27 | 'fiv.js': 'fiv', 28 | '.npmignore': 'two.js', 29 | '.DS_Store': 'a store of ds', 30 | }, 31 | }) 32 | 33 | t.test('package with slash files', async (t) => { 34 | const arborist = new Arborist({ path: pkg }) 35 | const tree = await arborist.loadActual() 36 | const files = await packlist(tree) 37 | t.same(files, [ 38 | 'fiv.js', 39 | 'lib/for.js', 40 | 'lib/one.js', 41 | 'lib/tre.js', 42 | 'lib/two.js', 43 | 'package.json', 44 | ]) 45 | }) 46 | -------------------------------------------------------------------------------- /test/bundled-scoped.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | dependencies: { 18 | '@npmwombat/history': '1.0.0', 19 | }, 20 | bundleDependencies: [ 21 | '@npmwombat/history', 22 | ], 23 | }), 24 | 'elf.js': elfJS, 25 | '.npmrc': 'packaged=false', 26 | node_modules: { 27 | '@npmwombat': { 28 | history: { 29 | 'package.json': JSON.stringify({ 30 | name: '@npmwombat/history', 31 | version: '1.0.0', 32 | main: 'index.js', 33 | }), 34 | 'index.js': elfJS, 35 | }, 36 | }, 37 | }, 38 | }) 39 | 40 | t.test('includes bundled dependency', async (t) => { 41 | const arborist = new Arborist({ path: pkg }) 42 | const tree = await arborist.loadActual() 43 | const files = await packlist(tree) 44 | t.same(files, [ 45 | 'elf.js', 46 | 'node_modules/@npmwombat/history/index.js', 47 | 'node_modules/@npmwombat/history/package.json', 48 | 'package.json', 49 | ]) 50 | }) 51 | -------------------------------------------------------------------------------- /test/scoped.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package-scoped', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | dependencies: { 18 | '@npmwombat/scoped': '1.0.0', 19 | }, 20 | bundledDependencies: [ 21 | '@npmwombat/scoped', 22 | ], 23 | }), 24 | 'elf.js': elfJS, 25 | node_modules: { 26 | '@npmwombat': { 27 | scoped: { 28 | 'index.js': "console.log('hello wombat')", 29 | }, 30 | no: { 31 | 'wombat.js': "console.log('no bundle please')", 32 | }, 33 | }, 34 | '@ignore': { 35 | scoped: { 36 | 'index.js': "console.log('i do not want to be bundled')", 37 | }, 38 | }, 39 | }, 40 | }) 41 | 42 | t.test('includes bundledDependencies', async (t) => { 43 | const arborist = new Arborist({ path: pkg }) 44 | const tree = await arborist.loadActual() 45 | const files = await packlist(tree, { bundled: ['@npmwombat/scoped'] }) 46 | t.same(files, [ 47 | 'elf.js', 48 | 'node_modules/@npmwombat/scoped/index.js', 49 | 'package.json', 50 | ]) 51 | }) 52 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/bundled-missing-optional.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | t.test('includes bundled dependency using bundleDependencies', async (t) => { 13 | const pkg = t.testdir({ 14 | 'package.json': JSON.stringify({ 15 | name: 'test-package', 16 | version: '3.1.4', 17 | main: 'elf.js', 18 | dependencies: { 19 | history: '1.0.0', 20 | }, 21 | bundleDependencies: [ 22 | 'history', 23 | ], 24 | }), 25 | 'elf.js': elfJS, 26 | '.npmrc': 'packaged=false', 27 | node_modules: { 28 | history: { 29 | 'package.json': JSON.stringify({ 30 | name: 'history', 31 | version: '1.0.0', 32 | main: 'index.js', 33 | optionalDependencies: { 34 | // defined here, but not installed 35 | optionalDep: '^1.0.0', 36 | }, 37 | }), 38 | 'index.js': elfJS, 39 | }, 40 | }, 41 | }) 42 | 43 | const arborist = new Arborist({ path: pkg }) 44 | const tree = await arborist.loadActual() 45 | const files = await packlist(tree) 46 | t.same(files, [ 47 | 'elf.js', 48 | 'node_modules/history/index.js', 49 | 'node_modules/history/package.json', 50 | 'package.json', 51 | ]) 52 | }) 53 | -------------------------------------------------------------------------------- /test/utils/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const packlist = require('../../') 6 | 7 | const dirs = [] 8 | let doSort = false 9 | process.argv.slice(2).forEach(arg => { 10 | if (arg === '-h' || arg === '--help') { 11 | console.log('usage: npm-packlist [-s --sort] [directory, directory, ...]') 12 | process.exit(0) 13 | } else if (arg === '-s' || arg === '--sort') { 14 | doSort = true 15 | } else { 16 | dirs.push(arg) 17 | } 18 | }) 19 | 20 | const sort = list => doSort ? list.sort((a, b) => a.localeCompare(b, 'en')) : list 21 | 22 | const main = async () => { 23 | if (!dirs.length) { 24 | const path = process.cwd() 25 | const arborist = new Arborist({ path }) 26 | const tree = await arborist.loadActual() 27 | const results = await packlist(tree) 28 | console.log(sort(results).join('\n')) 29 | } else { 30 | for (const dir of dirs) { 31 | const arborist = new Arborist({ path: dir }) 32 | const tree = await arborist.loadActual() 33 | console.group(`> ${dir}`) 34 | const results = await packlist(tree) 35 | console.log(sort(results).join('\n')) 36 | console.groupEnd() 37 | } 38 | } 39 | } 40 | 41 | // coverage disabled for catch handler because we don't need to test that 42 | main().catch(/* istanbul ignore next */(err) => { 43 | process.exitCode = 1 44 | console.error(err.stack) 45 | }) 46 | -------------------------------------------------------------------------------- /test/nested-lock-and-core.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | name: 'test-package', 10 | version: '1.2.3', 11 | }), 12 | 'package-lock.json': JSON.stringify({ 13 | lock: true, 14 | include: false, 15 | }), 16 | 'yarn.lock': JSON.stringify({ 17 | lock: 'file', 18 | include: false, 19 | }), 20 | 'bun.lockb': JSON.stringify({ 21 | lock: 'file', 22 | include: false, 23 | }), 24 | lib: { 25 | core: 'no longer excluded dump file', 26 | 'package-lock.json': JSON.stringify({ 27 | lock: 'file', 28 | include: true, 29 | }), 30 | 'yarn.lock': JSON.stringify({ 31 | lock: 'file', 32 | include: true, 33 | }), 34 | 'bun.lockb': JSON.stringify({ 35 | lock: 'file', 36 | include: true, 37 | }), 38 | }, 39 | core: { 40 | 'include-me.txt': 'please include me', 41 | }, 42 | }) 43 | 44 | t.test('follows npm package ignoring rules', async (t) => { 45 | const arborist = new Arborist({ path: pkg }) 46 | const tree = await arborist.loadActual() 47 | const files = await packlist(tree) 48 | t.same(files, [ 49 | 'lib/core', 50 | 'lib/package-lock.json', 51 | 'package.json', 52 | 'lib/yarn.lock', 53 | 'lib/bun.lockb', 54 | 'core/include-me.txt', 55 | ]) 56 | }) 57 | -------------------------------------------------------------------------------- /test/package-json-empty.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | files: [], 18 | }), 19 | 'index.js': elfJS, 20 | 'elf.js': elfJS, 21 | '.npmrc': 'packaged=false', 22 | 23 | // don't bother even reading this file, because we have files list 24 | '.npmignore': '!.npmignore\n!dummy\npackage.json\n!index.js\n', 25 | dummy: 'foo', 26 | build: { 27 | 'config.gypi': "i_wont_be_included='with any luck'", 28 | 'npm-debug.log': '0 lol\n', 29 | }, 30 | deps: { 31 | foo: { 32 | config: { 33 | 'config.gypi': "i_wont_be_included='with any luck'", 34 | }, 35 | }, 36 | }, 37 | '.git': { 38 | gitstub: "won't fool git, also won't be included", 39 | }, 40 | node_modules: { 41 | history: { 42 | 'README.md': "please don't include me", 43 | }, 44 | }, 45 | }) 46 | 47 | t.test('follows npm package ignoring rules', async (t) => { 48 | const arborist = new Arborist({ path: pkg }) 49 | const tree = await arborist.loadActual() 50 | const files = await packlist(tree) 51 | t.same(files, [ 52 | 'elf.js', 53 | 'package.json', 54 | ]) 55 | }) 56 | -------------------------------------------------------------------------------- /test/include-gitignore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | }), 18 | z: { 19 | '.gitignore': '!.gitignore', 20 | }, 21 | '.DS_Store': 'do not include me', 22 | 'elf.js': elfJS, 23 | '.npmrc': 'packaged=false', 24 | '.npmignore': '.npmignore\ndummy\npackage.json', 25 | dummy: 'foo', 26 | build: { 27 | 'config.gypi': "i_wont_be_included='with any luck'", 28 | 'npm-debug.log': '0 lol\n', 29 | }, 30 | deps: { 31 | foo: { 32 | config: { 33 | 'config.gypi': "i_wont_be_included='with any luck'", 34 | }, 35 | }, 36 | }, 37 | '.git': { 38 | gitstub: "won't fool git, also won't be included", 39 | }, 40 | node_modules: { 41 | history: { 42 | 'README.md': "please don't include me", 43 | }, 44 | }, 45 | }) 46 | 47 | t.test('follows npm package ignoring rules', async (t) => { 48 | const arborist = new Arborist({ path: pkg }) 49 | const tree = await arborist.loadActual() 50 | const files = await packlist(tree) 51 | t.same(files, [ 52 | 'z/.gitignore', 53 | 'deps/foo/config/config.gypi', 54 | 'elf.js', 55 | 'package.json', 56 | ]) 57 | }) 58 | -------------------------------------------------------------------------------- /test/bundled-cycle.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | t.test('correctly bundles cyclic deps', async (t) => { 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | name: 'root', 11 | version: '1.0.0', 12 | main: 'index.js', 13 | dependencies: { 14 | a: '1.0.0', 15 | }, 16 | bundleDependencies: ['a'], 17 | }), 18 | 'index.js': '', 19 | node_modules: { 20 | a: { 21 | 'package.json': JSON.stringify({ 22 | name: 'a', 23 | version: '1.0.0', 24 | main: 'index.js', 25 | dependencies: { 26 | b: '1.0.0', 27 | }, 28 | }), 29 | 'index.js': '', 30 | }, 31 | b: { 32 | 'package.json': JSON.stringify({ 33 | name: 'b', 34 | version: '1.0.0', 35 | main: 'index.js', 36 | dependencies: { 37 | a: '1.0.0', 38 | }, 39 | }), 40 | 'index.js': '', 41 | }, 42 | }, 43 | }) 44 | 45 | const arborist = new Arborist({ path: pkg }) 46 | const tree = await arborist.loadActual() 47 | const files = await packlist(tree) 48 | t.same(files, [ 49 | 'index.js', 50 | 'node_modules/a/index.js', 51 | 'node_modules/b/index.js', 52 | 'node_modules/a/package.json', 53 | 'node_modules/b/package.json', 54 | 'package.json', 55 | ]) 56 | }) 57 | -------------------------------------------------------------------------------- /test/empty-npmignore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | }), 18 | 'elf.js': elfJS, 19 | '.gitignore': '*', 20 | '.npmignore': '', 21 | lib: { 22 | node_modules: { 23 | foo: { 24 | 'package.json': '{"name":"just a test","version":"bugbear"}', 25 | }, 26 | }, 27 | }, 28 | '.npmrc': 'packaged=false', 29 | build: { 30 | 'config.gypi': "i_wont_be_included='with any luck'", 31 | 'npm-debug.log': '0 lol\n', 32 | }, 33 | deps: { 34 | foo: { 35 | config: { 36 | 'config.gypi': "i_will_be_included='with any luck'", 37 | }, 38 | }, 39 | }, 40 | '.git': { 41 | gitstub: "won't fool git, also won't be included", 42 | }, 43 | node_modules: { 44 | history: { 45 | 'README.md': "please don't include me", 46 | }, 47 | }, 48 | }) 49 | 50 | t.test('follows npm package ignoring rules', async (t) => { 51 | const arborist = new Arborist({ path: pkg }) 52 | const tree = await arborist.loadActual() 53 | const files = await packlist(tree) 54 | t.same(files, [ 55 | 'deps/foo/config/config.gypi', 56 | 'elf.js', 57 | 'lib/node_modules/foo/package.json', 58 | 'package.json', 59 | ]) 60 | }) 61 | -------------------------------------------------------------------------------- /test/package-json.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | files: [ 17 | 'elf.js', 18 | 'deps/foo/config/config.gypi', 19 | ], 20 | }), 21 | 'npm-shrinkwrap.json': JSON.stringify({ shrink: 'wrap' }), 22 | 'elf.js': elfJS, 23 | '.npmrc': 'packaged=false', 24 | // don't bother even reading this file, because we have files list 25 | '.npmignore': '!.npmignore\n!dummy\npackage.json', 26 | dummy: 'foo', 27 | build: { 28 | 'config.gypi': "i_wont_be_included='with any luck'", 29 | 'npm-debug.log': '0 lol\n', 30 | }, 31 | deps: { 32 | foo: { 33 | config: { 34 | 'config.gypi': "i_will_be_included='with any luck'", 35 | }, 36 | }, 37 | }, 38 | '.git': { 39 | gitstub: "won't fool git, also won't be included", 40 | }, 41 | node_modules: { 42 | history: { 43 | 'README.md': "please don't include me", 44 | }, 45 | }, 46 | }) 47 | 48 | t.test('follows npm package ignoring rules', async (t) => { 49 | const arborist = new Arborist({ path: pkg }) 50 | const tree = await arborist.loadActual() 51 | const files = await packlist(tree) 52 | t.same(files, [ 53 | 'deps/foo/config/config.gypi', 54 | 'elf.js', 55 | 'package.json', 56 | ]) 57 | }) 58 | -------------------------------------------------------------------------------- /test/package-json-nested-readme-include-npmignore.js: -------------------------------------------------------------------------------- 1 | // readme rules can be overridden by files array 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const t = require('tap') 6 | const packlist = require('../') 7 | 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | files: ['.npmignore', 'lib'], 11 | }), 12 | lib: { 13 | '.npmignore': ` 14 | * 15 | !*.js 16 | !**/*.js 17 | test 18 | `, 19 | a: { 20 | b: { 21 | c: { 22 | 'readme.md': 'one', 23 | 'file.txt': 'one', 24 | 'c.js': 'one', 25 | }, 26 | 'readme.md': 'one', 27 | 'file.txt': 'one', 28 | 'b.js': 'one', 29 | }, 30 | 'readme.md': 'one', 31 | 'file.txt': 'one', 32 | 'a.js': 'one', 33 | }, 34 | }, 35 | test: { 36 | a: { 37 | b: { 38 | c: { 39 | 'readme.md': 'one', 40 | 'file.txt': 'one', 41 | 'c.js': 'one', 42 | }, 43 | 'readme.md': 'one', 44 | 'file.txt': 'one', 45 | 'b.js': 'one', 46 | }, 47 | 'readme.md': 'one', 48 | 'file.txt': 'one', 49 | 'a.js': 'one', 50 | }, 51 | }, 52 | }) 53 | 54 | t.test('package with negated files', async (t) => { 55 | const arborist = new Arborist({ path: pkg }) 56 | const tree = await arborist.loadActual() 57 | const files = await packlist(tree) 58 | t.same(files, [ 59 | 'lib/a/a.js', 60 | 'lib/a/b/b.js', 61 | 'lib/a/b/c/c.js', 62 | 'package.json', 63 | ]) 64 | }) 65 | -------------------------------------------------------------------------------- /test/strip-slash-from-package-files-list-entries.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | t.test('should strip / from package.json files array entry results', async (t) => { 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | files: [ 11 | // include without slash, then exclude with it 12 | 'somedir', 13 | '!somedir/', 14 | 15 | // other way around 16 | 'otherdir/', 17 | '!otherdir', 18 | 19 | // now including it that way 20 | '!incldir/', 21 | 'incldir', 22 | 23 | // exclude without slash, then include with it 24 | '!dist', 25 | 'dist/', 26 | '!dist/foo/*.src', 27 | ], 28 | }), 29 | otherdir: { 30 | donotinclude: '', 31 | }, 32 | somedir: { 33 | donotinclude: '', 34 | }, 35 | incldir: { 36 | yesinclude: '', 37 | }, 38 | foo: '', 39 | dist: { 40 | foo: { 41 | 'foo.src': '', 42 | 'foo.result': '', 43 | }, 44 | bar: '', 45 | baz: { 46 | boo: '', 47 | 'boo.src': '', 48 | }, 49 | }, 50 | }) 51 | 52 | const arborist = new Arborist({ path: pkg }) 53 | const tree = await arborist.loadActual() 54 | const files = await packlist(tree) 55 | t.same(files, [ 56 | 'dist/bar', 57 | 'dist/baz/boo', 58 | 'incldir/yesinclude', 59 | 'package.json', 60 | 'dist/foo/foo.result', 61 | 'dist/baz/boo.src', 62 | ]) 63 | }) 64 | -------------------------------------------------------------------------------- /test/bundled-symlink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | pkg: { 14 | 'package.json': JSON.stringify({ 15 | name: 'test-package', 16 | version: '3.1.4', 17 | main: 'elf.js', 18 | dependencies: { 19 | history: '1.0.0', 20 | }, 21 | bundleDependencies: [ 22 | 'history', 23 | ], 24 | }), 25 | 'elf.js': elfJS, 26 | '.npmrc': 'packaged=false', 27 | node_modules: { 28 | history: t.fixture('symlink', '../../history'), 29 | }, 30 | }, 31 | history: { 32 | 'package.json': JSON.stringify({ 33 | name: 'history', 34 | version: '1.0.0', 35 | main: 'index.js', 36 | files: [ 37 | 'index.js', 38 | 'lib/', 39 | ], 40 | }), 41 | 'index.js': elfJS, 42 | tests: { 43 | 'test.js': 'please do not include me', 44 | }, 45 | // this should not be followed, even though the bundled dep is 46 | lib: { 47 | linky: t.fixture('symlink', '../tests'), 48 | }, 49 | }, 50 | }) + '/pkg' 51 | 52 | t.test('includes bundled dependency', async (t) => { 53 | const arborist = new Arborist({ path: pkg }) 54 | const tree = await arborist.loadActual() 55 | const files = await packlist(tree) 56 | t.same(files, [ 57 | 'elf.js', 58 | 'node_modules/history/index.js', 59 | 'node_modules/history/package.json', 60 | 'package.json', 61 | ]) 62 | }) 63 | -------------------------------------------------------------------------------- /test/bundled-workspace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | t.test('packs workspace dependencies correctly', async (t) => { 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | name: 'root', 11 | version: '1.2.3', 12 | main: 'index.js', 13 | files: ['index.js'], 14 | dependencies: { 15 | foo: '1.0.0', 16 | bar: '1.0.0', 17 | }, 18 | bundleDependencies: ['foo', 'bar'], 19 | workspaces: ['./workspaces/*'], 20 | }), 21 | 'index.js': '', 22 | workspaces: { 23 | foo: { 24 | 'package.json': JSON.stringify({ 25 | name: 'foo', 26 | version: '1.0.0', 27 | main: 'index.js', 28 | }), 29 | 'index.js': '', 30 | }, 31 | bar: { 32 | 'package.json': JSON.stringify({ 33 | name: 'bar', 34 | version: '1.0.0', 35 | main: 'index.js', 36 | }), 37 | 'index.js': '', 38 | }, 39 | }, 40 | node_modules: { 41 | foo: t.fixture('symlink', '../workspaces/foo'), 42 | bar: t.fixture('symlink', '../workspaces/bar'), 43 | }, 44 | }) 45 | 46 | const arborist = new Arborist({ path: pkg }) 47 | const tree = await arborist.loadActual() 48 | const files = await packlist(tree) 49 | t.same(files, [ 50 | 'index.js', 51 | 'node_modules/bar/index.js', 52 | 'node_modules/foo/index.js', 53 | 'node_modules/bar/package.json', 54 | 'node_modules/foo/package.json', 55 | 'package.json', 56 | ]) 57 | }) 58 | -------------------------------------------------------------------------------- /test/package-json-nested-readme.js: -------------------------------------------------------------------------------- 1 | // include readme.* files anywhere in a package 2 | 'use strict' 3 | 4 | const Arborist = require('@npmcli/arborist') 5 | const t = require('tap') 6 | const packlist = require('../') 7 | 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({}), 10 | lib: { 11 | a: { 12 | b: { 13 | c: { 14 | 'readme.md': 'one', 15 | 'file.txt': 'one', 16 | 'c.js': 'one', 17 | }, 18 | 'readme.md': 'one', 19 | 'file.txt': 'one', 20 | 'b.js': 'one', 21 | }, 22 | 'readme.md': 'one', 23 | 'file.txt': 'one', 24 | 'a.js': 'one', 25 | }, 26 | }, 27 | test: { 28 | a: { 29 | b: { 30 | c: { 31 | 'readme.md': 'one', 32 | 'file.txt': 'one', 33 | 'c.js': 'one', 34 | }, 35 | 'readme.md': 'one', 36 | 'file.txt': 'one', 37 | 'b.js': 'one', 38 | }, 39 | 'readme.md': 'one', 40 | 'file.txt': 'one', 41 | 'a.js': 'one', 42 | }, 43 | }, 44 | '.npmignore': ` 45 | !*.js 46 | !**/*.js 47 | test 48 | `, 49 | }) 50 | 51 | t.test('package with negated files', async (t) => { 52 | const arborist = new Arborist({ path: pkg }) 53 | const tree = await arborist.loadActual() 54 | const files = await packlist(tree) 55 | t.same(files, [ 56 | 'lib/a/a.js', 57 | 'lib/a/b/b.js', 58 | 'lib/a/b/c/c.js', 59 | 'package.json', 60 | 'lib/a/b/c/readme.md', 61 | 'lib/a/b/readme.md', 62 | 'lib/a/readme.md', 63 | 'lib/a/b/c/file.txt', 64 | 'lib/a/b/file.txt', 65 | 'lib/a/file.txt', 66 | ]) 67 | }) 68 | -------------------------------------------------------------------------------- /test/package-json-main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: '__main.js', 17 | browser: 'browser.js', 18 | files: [ 19 | 'elf.js', 20 | 'deps/foo/config/config.gypi', 21 | ], 22 | }), 23 | 'elf.js': elfJS, 24 | '__main.js': elfJS, 25 | 'browser.js': elfJS, 26 | '.npmrc': 'packaged=false', 27 | 28 | // don't bother even reading this file, because we have files list 29 | '.npmignore': '!.npmignore\n!dummy\npackage.json', 30 | dummy: 'foo', 31 | build: { 32 | 'config.gypi': "i_wont_be_included='with any luck'", 33 | 'npm-debug.log': '0 lol\n', 34 | }, 35 | deps: { 36 | foo: { 37 | config: { 38 | 'config.gypi': "i_will_be_included='with any luck'", 39 | }, 40 | }, 41 | }, 42 | '.git': { 43 | gitstub: "won't fool git, also won't be included", 44 | }, 45 | node_modules: { 46 | history: { 47 | 'README.md': "please don't include me", 48 | }, 49 | }, 50 | }) 51 | 52 | t.test('follows npm package ignoring rules', async (t) => { 53 | const arborist = new Arborist({ path: pkg }) 54 | const tree = await arborist.loadActual() 55 | const files = await packlist(tree) 56 | t.same(files, [ 57 | 'deps/foo/config/config.gypi', 58 | '__main.js', 59 | 'browser.js', 60 | 'elf.js', 61 | 'package.json', 62 | ]) 63 | }) 64 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-packlist", 3 | "version": "10.0.3", 4 | "description": "Get a list of the files to add from a folder into an npm package", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "main": "lib/index.js", 9 | "dependencies": { 10 | "ignore-walk": "^8.0.0", 11 | "proc-log": "^6.0.0" 12 | }, 13 | "author": "GitHub Inc.", 14 | "license": "ISC", 15 | "files": [ 16 | "bin/", 17 | "lib/" 18 | ], 19 | "devDependencies": { 20 | "@npmcli/arborist": "^9.0.0", 21 | "@npmcli/eslint-config": "^6.0.0", 22 | "@npmcli/template-oss": "4.28.1", 23 | "mutate-fs": "^2.1.1", 24 | "tap": "^16.0.1" 25 | }, 26 | "scripts": { 27 | "test": "tap", 28 | "posttest": "npm run lint", 29 | "snap": "tap", 30 | "postsnap": "npm run lintfix --", 31 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"", 32 | "lint": "npm run eslint", 33 | "lintfix": "npm run eslint -- --fix", 34 | "npmclilint": "npmcli-lint", 35 | "postlint": "template-oss-check", 36 | "template-oss-apply": "template-oss-apply --force" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/npm/npm-packlist.git" 41 | }, 42 | "tap": { 43 | "test-env": [ 44 | "LC_ALL=sk" 45 | ], 46 | "nyc-arg": [ 47 | "--exclude", 48 | "tap-snapshots/**" 49 | ], 50 | "files": [ 51 | "test/*.js" 52 | ] 53 | }, 54 | "engines": { 55 | "node": "^20.17.0 || >=22.9.0" 56 | }, 57 | "templateOSS": { 58 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 59 | "version": "4.28.1", 60 | "publish": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/bundled-scoped-symlink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('..') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | pkg: { 14 | 'package.json': JSON.stringify({ 15 | name: 'test-package', 16 | version: '3.1.4', 17 | main: 'elf.js', 18 | dependencies: { 19 | '@npmwombat/history': '1.0.0', 20 | }, 21 | bundleDependencies: [ 22 | '@npmwombat/history', 23 | ], 24 | }), 25 | 'elf.js': elfJS, 26 | '.npmrc': 'packaged=false', 27 | node_modules: { 28 | '@npmwombat': { 29 | history: t.fixture('symlink', '../../../history'), 30 | }, 31 | }, 32 | }, 33 | history: { 34 | 'package.json': JSON.stringify({ 35 | name: '@npmwombat/history', 36 | version: '1.0.0', 37 | main: 'index.js', 38 | files: [ 39 | 'index.js', 40 | 'lib/', 41 | ], 42 | }), 43 | 'index.js': elfJS, 44 | tests: { 45 | 'test.js': 'please do not include me', 46 | }, 47 | // this should not be followed, even though the bundled dep is 48 | lib: { 49 | linky: t.fixture('symlink', '../tests'), 50 | }, 51 | }, 52 | }) + '/pkg' 53 | 54 | t.test('includes bundled dependency', async (t) => { 55 | const arborist = new Arborist({ path: pkg }) 56 | const tree = await arborist.loadActual() 57 | const files = await packlist(tree) 58 | t.same(files, [ 59 | 'elf.js', 60 | 'node_modules/@npmwombat/history/index.js', 61 | 'node_modules/@npmwombat/history/package.json', 62 | 'package.json', 63 | ]) 64 | }) 65 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /test/ignore-file-included-by-globstar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | t.test('exclude certain files always', async t => { 8 | const path = t.testdir({ 9 | '.npmrc': 'secrets=true', 10 | '.git': { 11 | HEAD: 'empty', 12 | }, 13 | node_modules: { 14 | foo: { 15 | 'index.js': '', 16 | }, 17 | }, 18 | subdir: { 19 | 'other.js': '', 20 | '.npmrc': 'sneaky=true', 21 | }, 22 | 'index.js': '', 23 | 'glorp.txt': '', 24 | 'package.json': JSON.stringify({ 25 | name: '@npmcli/globstar-test', 26 | version: '1.0.0', 27 | files: ['*'], 28 | }), 29 | 'package-lock.json': '{}', 30 | 'yarn.lock': '{}', 31 | 'pnpm-lock.yaml': '{}', 32 | }) 33 | const arborist = new Arborist({ path }) 34 | const tree = await arborist.loadActual() 35 | const files = await packlist(tree) 36 | t.same(files, [ 37 | 'index.js', 38 | 'subdir/other.js', 39 | 'package.json', 40 | 'glorp.txt', 41 | ]) 42 | }) 43 | 44 | t.test('include a globstar, then exclude one of them', async (t) => { 45 | const path = t.testdir({ 46 | 'bar.js': '', 47 | bar: { 48 | 'bar.js': '', 49 | }, 50 | 'glorp.txt': '', 51 | 'package.json': JSON.stringify({ 52 | name: 'cli-issue-2009', 53 | version: '1.0.0', 54 | files: [ 55 | '**/*.js', 56 | '!foo.js', 57 | ], 58 | }), 59 | }) 60 | 61 | const arborist = new Arborist({ path }) 62 | const tree = await arborist.loadActual() 63 | const files = await packlist(tree) 64 | t.same(files, [ 65 | 'bar.js', 66 | 'bar/bar.js', 67 | 'package.json', 68 | ]) 69 | }) 70 | -------------------------------------------------------------------------------- /test/package-json-negate-non-root.js: -------------------------------------------------------------------------------- 1 | // exclude readme, license, and licence files if package.json 2 | // files array includes !readme, !license, or !licence 3 | 'use strict' 4 | 5 | const Arborist = require('@npmcli/arborist') 6 | const t = require('tap') 7 | const packlist = require('../') 8 | 9 | const pkg = t.testdir({ 10 | 'package.json': JSON.stringify({ 11 | files: [ 12 | 'lib', 13 | '!readme.md', 14 | '!licence', 15 | '!license', 16 | '!copying', 17 | ], 18 | 19 | }), 20 | 'readme.md': 'one', 21 | licence: 'two', 22 | license: 'tre', 23 | copying: 'for', 24 | lib: { 25 | 'readme.md': 'one', 26 | licence: 'two', 27 | license: 'tre', 28 | copying: 'for', 29 | a: { 30 | 'readme.md': 'one', 31 | licence: 'two', 32 | license: 'tre', 33 | copying: 'for', 34 | b: { 35 | 'readme.md': 'one', 36 | licence: 'two', 37 | license: 'tre', 38 | copying: 'for', 39 | c: { 40 | 'readme.md': 'one', 41 | licence: 'two', 42 | license: 'tre', 43 | copying: 'for', 44 | 'file.txt': 'one', 45 | 'c.js': 'two', 46 | }, 47 | 'file.txt': 'one', 48 | 'b.js': 'two', 49 | }, 50 | 'file.txt': 'one', 51 | 'a.js': 'two', 52 | }, 53 | } }) 54 | 55 | t.test('package with negated readme, licence and license files', async (t) => { 56 | const arborist = new Arborist({ path: pkg }) 57 | const tree = await arborist.loadActual() 58 | const files = await packlist(tree) 59 | t.same(files, [ 60 | 'copying', 61 | 'licence', 62 | 'license', 63 | 'lib/a/a.js', 64 | 'lib/a/b/b.js', 65 | 'lib/a/b/c/c.js', 66 | 'package.json', 67 | 'readme.md', 68 | 'lib/a/b/c/file.txt', 69 | 'lib/a/b/file.txt', 70 | 'lib/a/file.txt', 71 | ]) 72 | }) 73 | -------------------------------------------------------------------------------- /test/cannot-include-non-file-or-directory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const fs = require('fs') 5 | const t = require('tap') 6 | const { join } = require('path') 7 | 8 | t.test('cannot include something that exists but is neither a file nor a directory', async (t) => { 9 | const pkg = t.testdir({ 10 | 'package.json': JSON.stringify({ 11 | name: 'root', 12 | version: '1.0.0', 13 | main: 'index.js', 14 | files: ['lib', 'device'], 15 | }), 16 | 'index.js': '', 17 | lib: { 18 | socket: '', 19 | }, 20 | device: '', 21 | }) 22 | 23 | // mock fs.lstat for ignore-walk so it tells us that lib/socket is not a file, dir, or symlink 24 | const ignoreWalk = t.mock('ignore-walk', { 25 | fs: { 26 | ...fs, 27 | lstat: (path, options, callback) => { 28 | if (typeof options === 'function') { 29 | callback = options 30 | options = undefined 31 | } 32 | if (path === join(pkg, 'lib', 'socket').replace(/\\/g, '/')) { 33 | return callback(null, { 34 | isFile: () => false, 35 | isDirectory: () => false, 36 | isSymbolicLink: () => false, 37 | }) 38 | } 39 | return fs.lstat(path, options, callback) 40 | }, 41 | }, 42 | }) 43 | 44 | const packlist = t.mock('../', { 45 | 'ignore-walk': ignoreWalk, 46 | fs: { 47 | ...fs, 48 | lstatSync: (path) => { 49 | if (path === join(pkg, 'device').replace(/\\/g, '/')) { 50 | return { isFile: () => false, isDirectory: () => false } 51 | } 52 | 53 | return fs.lstatSync(path) 54 | }, 55 | }, 56 | }) 57 | 58 | const arborist = new Arborist({ path: pkg }) 59 | const tree = await arborist.loadActual() 60 | const files = await packlist(tree) 61 | t.same(files, [ 62 | 'index.js', 63 | 'package.json', 64 | ]) 65 | }) 66 | -------------------------------------------------------------------------------- /test/package-json-roots-and-nests.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const pkg = t.testdir({ 8 | 'package.json': JSON.stringify({ 9 | bin: 'bin.js', 10 | main: 'main.js', 11 | browser: 'browser.js', 12 | dependencies: { 13 | foo: '1.0.0', 14 | '@foo/bar': '1.0.0', 15 | }, 16 | bundleDependencies: [ 17 | 'foo', 18 | '@foo/bar', 19 | ], 20 | }), 21 | node_modules: { 22 | // bundle dep files are ALWAYS included 23 | // even questionable things 24 | foo: { 25 | 'package-lock.json': 'include', 26 | }, 27 | '@foo': { 28 | bar: { 29 | '.DS_Store': 'not this tho', 30 | }, 31 | }, 32 | }, 33 | lib: { 34 | // these are not included 35 | 'package-lock.json': 'sw', 36 | 'package.json.js': '{}', 37 | 'bin.js': 'bin', 38 | 'main.js': 'main', 39 | 'browser.js': 'browser', 40 | 'npm-shrinkwrap.json': 'sw', 41 | }, 42 | 43 | // these get included 44 | 'bin.js': 'bin', 45 | 'main.js': 'main', 46 | 'browser.js': 'browser', 47 | 'npm-shrinkwrap.json': 'sw', 48 | inc: { 49 | 'package.json': JSON.stringify({ files: [] }), 50 | 'package-lock.json': 'include me plz', 51 | foo: 'include me plz', 52 | }, 53 | 54 | // these do not 55 | '.npmignore': 'lib/*', 56 | 'package-lock.json': 'sw', 57 | }) 58 | 59 | t.test('package with negated files', async (t) => { 60 | const arborist = new Arborist({ path: pkg }) 61 | const tree = await arborist.loadActual() 62 | const files = await packlist(tree) 63 | t.same(files, [ 64 | 'node_modules/@foo/bar/.DS_Store', 65 | 'inc/foo', 66 | 'bin.js', 67 | 'browser.js', 68 | 'main.js', 69 | 'npm-shrinkwrap.json', 70 | 'inc/package-lock.json', 71 | 'node_modules/foo/package-lock.json', 72 | 'inc/package.json', 73 | 'package.json', 74 | ]) 75 | }) 76 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/star-names.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const fs = require('fs') 6 | 7 | // the fs.readdir call takes place in ignore-walk, not npm-packlist, so we t.mock 8 | // ignore-walk first to clear its require cache and pick up the mocked fs 9 | const ignoreWalk = t.mock('ignore-walk', { 10 | fs: { 11 | ...fs, 12 | readdir: (path, callback) => { 13 | fs.readdir(path, (err, entries) => { 14 | if (err) { 15 | return callback(err) 16 | } 17 | 18 | return callback(null, entries.concat('made*of*stars')) 19 | }) 20 | }, 21 | }, 22 | }) 23 | 24 | // then we t.mock npm-packlist itself so that we can pick up the mocked ignore-walk 25 | const packlist = t.mock('../', { 26 | 'ignore-walk': ignoreWalk, 27 | }) 28 | 29 | const elfJS = ` 30 | module.exports = elf => 31 | console.log("i'm a elf") 32 | ` 33 | 34 | const pkg = t.testdir({ 35 | 'package.json': JSON.stringify({ 36 | name: 'test-package', 37 | version: '3.1.4', 38 | main: 'elf.js', 39 | }), 40 | 'elf.js': elfJS, 41 | '.npmrc': 'packaged=false', 42 | '.npmignore': '.npmignore\ndummy\npackage.json', 43 | dummy: 'foo', 44 | 45 | // empty dir should be ignored 46 | this: { dir: { is: { empty: { and: { ignored: {} } } } } }, 47 | build: { 48 | 'config.gypi': "i_wont_be_included='with any luck'", 49 | 'npm-debug.log': '0 lol\n', 50 | }, 51 | deps: { 52 | foo: { 53 | config: { 54 | 'config.gypi': "i_will_be_included='with any luck'", 55 | }, 56 | }, 57 | }, 58 | '.git': { 59 | gitstub: "won't fool git, also won't be included", 60 | }, 61 | node_modules: { 62 | history: { 63 | 'README.md': "please don't include me", 64 | }, 65 | }, 66 | }) 67 | 68 | t.test('follows npm package ignoring rules', async (t) => { 69 | const arborist = new Arborist({ path: pkg }) 70 | const tree = await arborist.loadActual() 71 | const files = await packlist(tree) 72 | t.same(files, [ 73 | 'deps/foo/config/config.gypi', 74 | 'elf.js', 75 | 'package.json', 76 | ]) 77 | }) 78 | -------------------------------------------------------------------------------- /test/symlink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | }), 18 | 'elf.js': elfJS, 19 | 'link.js': t.fixture('symlink', 'elf.js'), 20 | '.npmrc': 'packaged=false', 21 | '.npmignore': '.npmignore\ndummy\n/package.json\n', 22 | // empty dir should be ignored 23 | this: { dir: { is: { empty: { and: { ignored: {} } } } } }, 24 | dummy: 'foo', 25 | build: { 26 | 'config.gypi': "i_wont_be_included='with any luck'", 27 | 'npm-debug.log': '0 lol\n', 28 | }, 29 | deps: { 30 | foo: { 31 | config: { 32 | 'config.gypi': "i_will_be_included='with any luck'", 33 | }, 34 | }, 35 | }, 36 | '.git': { 37 | gitstub: "won't fool git, also won't be included", 38 | }, 39 | node_modules: { 40 | history: { 41 | 'README.md': "please don't include me", 42 | }, 43 | }, 44 | 45 | // ljharb's monorepo test goober that blew up 46 | test: { resolver: { multirepo: { packages: { 47 | a: { 48 | README: 'included', 49 | node_modules: { 50 | some_dep: { 51 | 'package.json': JSON.stringify({ version: '1.2.3' }), 52 | }, 53 | '@scope': { 54 | b: t.fixture('symlink', '../../../b'), 55 | }, 56 | }, 57 | }, 58 | b: { 59 | 'index.js': 'console.log("woop")', 60 | node_modules: { 61 | a: t.fixture('symlink', '../../a'), 62 | }, 63 | }, 64 | } } } }, 65 | }) 66 | 67 | t.test('follows npm package ignoring rules', async (t) => { 68 | const arborist = new Arborist({ path: pkg }) 69 | const tree = await arborist.loadActual() 70 | const files = await packlist(tree) 71 | t.same(files, [ 72 | 'test/resolver/multirepo/packages/a/README', 73 | 'deps/foo/config/config.gypi', 74 | 'elf.js', 75 | 'test/resolver/multirepo/packages/b/index.js', 76 | 'package.json', 77 | 'test/resolver/multirepo/packages/a/node_modules/some_dep/package.json', 78 | ]) 79 | }) 80 | -------------------------------------------------------------------------------- /test/bundled.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | t.test('includes bundled dependency using bundleDependencies', async (t) => { 13 | const pkg = t.testdir({ 14 | 'package.json': JSON.stringify({ 15 | name: 'test-package', 16 | version: '3.1.4', 17 | main: 'elf.js', 18 | dependencies: { 19 | history: '1.0.0', 20 | }, 21 | bundleDependencies: [ 22 | 'history', 23 | ], 24 | }), 25 | 'elf.js': elfJS, 26 | '.npmrc': 'packaged=false', 27 | node_modules: { 28 | history: { 29 | 'package.json': JSON.stringify({ 30 | name: 'history', 31 | version: '1.0.0', 32 | main: 'index.js', 33 | }), 34 | 'index.js': elfJS, 35 | }, 36 | }, 37 | }) 38 | 39 | const arborist = new Arborist({ path: pkg }) 40 | const tree = await arborist.loadActual() 41 | const files = await packlist(tree) 42 | t.same(files, [ 43 | 'elf.js', 44 | 'node_modules/history/index.js', 45 | 'node_modules/history/package.json', 46 | 'package.json', 47 | ]) 48 | }) 49 | 50 | t.test('includes bundled dependency using bundledDependencies', async (t) => { 51 | const pkg = t.testdir({ 52 | 'package.json': JSON.stringify({ 53 | name: 'test-package', 54 | version: '3.1.4', 55 | main: 'elf.js', 56 | dependencies: { 57 | history: '1.0.0', 58 | }, 59 | bundledDependencies: [ 60 | 'history', 61 | ], 62 | }), 63 | 'elf.js': elfJS, 64 | '.npmrc': 'packaged=false', 65 | node_modules: { 66 | history: { 67 | 'package.json': JSON.stringify({ 68 | name: 'history', 69 | version: '1.0.0', 70 | main: 'index.js', 71 | }), 72 | 'index.js': elfJS, 73 | }, 74 | }, 75 | }) 76 | 77 | const arborist = new Arborist({ path: pkg }) 78 | const tree = await arborist.loadActual() 79 | const files = await packlist(tree) 80 | t.same(files, [ 81 | 'elf.js', 82 | 'node_modules/history/index.js', 83 | 'node_modules/history/package.json', 84 | 'package.json', 85 | ]) 86 | }) 87 | -------------------------------------------------------------------------------- /test/ignores.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | const pkg = t.testdir({ 13 | 'package.json': JSON.stringify({ 14 | name: 'test-package', 15 | version: '3.1.4', 16 | main: 'elf.js', 17 | }), 18 | 'archived-packages': { 19 | ignoreme: 'this should be ignored', 20 | }, 21 | 'elf.js': elfJS, 22 | '.npmrc': 'packaged=false', 23 | // the '!**/non.existent' rule is important as it tests if the default rules 24 | // block .git contents even if it's accidentally 'unlocked'. 25 | // see https://npm.community/t/1805 26 | '.npmignore': ` 27 | .npmignore 28 | dummy 29 | package.json 30 | !**/non.existent 31 | readme.md 32 | *~ 33 | `, 34 | dummy: 'foo', 35 | core: 'foo', 36 | '.DS_Store': { 37 | foo: 'foo', 38 | }, 39 | 'readme.md': 'Elf package readme included even if ignored', 40 | 'readme.md~': 'Editor backup file should not be auto-included', 41 | this: { 42 | dir: { 43 | is: { 44 | empty: { 45 | and: { 46 | ignored: {}, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | build: { 53 | 'config.gypi': "i_wont_be_included='with any luck'", 54 | 'npm-debug.log': '0 lol\n', 55 | }, 56 | deps: { 57 | foo: { 58 | config: { 59 | 'config.gypi': "i_will_be_included='with any luck'", 60 | }, 61 | }, 62 | '.git': 'do not include me', 63 | }, 64 | '.git': { 65 | gitstub: 'wont fool git, also wont be included', 66 | logs: { 67 | refs: { 68 | remotes: { 69 | name: { 70 | readme: 'please donot include git dirs (or even walk them)', 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | node_modules: { 77 | history: { 78 | 'README.md': "please don't include me", 79 | }, 80 | }, 81 | }) 82 | 83 | t.test('follows npm package ignoring rules', async (t) => { 84 | const arborist = new Arborist({ path: pkg }) 85 | const tree = await arborist.loadActual() 86 | const files = await packlist(tree) 87 | t.same(files, [ 88 | 'core', 89 | 'deps/foo/config/config.gypi', 90 | 'elf.js', 91 | 'package.json', 92 | 'readme.md', 93 | ]) 94 | }) 95 | -------------------------------------------------------------------------------- /test/bundled-files.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | const elfJS = ` 8 | module.exports = elf => 9 | console.log("i'm a elf") 10 | ` 11 | 12 | t.test('includes bundled dependency using bundleDependencies', async (t) => { 13 | const pkg = t.testdir({ 14 | 'package.json': JSON.stringify({ 15 | name: 'test-package', 16 | version: '3.1.4', 17 | main: 'elf.js', 18 | dependencies: { 19 | history: '1.0.0', 20 | }, 21 | bundleDependencies: [ 22 | 'history', 23 | ], 24 | files: ['elf.js'], 25 | }), 26 | 'elf.js': elfJS, 27 | '.npmrc': 'packaged=false', 28 | node_modules: { 29 | history: { 30 | 'package.json': JSON.stringify({ 31 | name: 'history', 32 | version: '1.0.0', 33 | main: 'index.js', 34 | }), 35 | 'index.js': elfJS, 36 | }, 37 | }, 38 | }) 39 | 40 | const arborist = new Arborist({ path: pkg }) 41 | const tree = await arborist.loadActual() 42 | const files = await packlist(tree) 43 | t.same(files, [ 44 | 'elf.js', 45 | 'node_modules/history/index.js', 46 | 'node_modules/history/package.json', 47 | 'package.json', 48 | ]) 49 | }) 50 | 51 | t.test('includes bundled dependency using bundledDependencies', async (t) => { 52 | const pkg = t.testdir({ 53 | 'package.json': JSON.stringify({ 54 | name: 'test-package', 55 | version: '3.1.4', 56 | main: 'elf.js', 57 | dependencies: { 58 | history: '1.0.0', 59 | }, 60 | bundledDependencies: [ 61 | 'history', 62 | ], 63 | files: ['elf.js'], 64 | }), 65 | 'elf.js': elfJS, 66 | '.npmrc': 'packaged=false', 67 | node_modules: { 68 | history: { 69 | 'package.json': JSON.stringify({ 70 | name: 'history', 71 | version: '1.0.0', 72 | main: 'index.js', 73 | }), 74 | 'index.js': elfJS, 75 | }, 76 | }, 77 | }) 78 | 79 | const arborist = new Arborist({ path: pkg }) 80 | const tree = await arborist.loadActual() 81 | const files = await packlist(tree) 82 | t.same(files, [ 83 | 'elf.js', 84 | 'node_modules/history/index.js', 85 | 'node_modules/history/package.json', 86 | 'package.json', 87 | ]) 88 | }) 89 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/bundled-file-in-workspace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const t = require('tap') 5 | const packlist = require('../') 6 | 7 | t.test('correctly filters files from workspace subdirectory', async (t) => { 8 | const pkg = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | name: 'root', 11 | version: '1.0.0', 12 | files: ['docs/*.txt'], 13 | main: 'index.js', 14 | workspaces: ['./docs'], 15 | }), 16 | 'index.js': '', 17 | docs: { 18 | 'package.json': JSON.stringify({ 19 | name: 'docs', 20 | version: '1.0.0', 21 | main: 'index.js', 22 | files: ['*.txt'], 23 | }), 24 | 'bar.txt': '', 25 | 'foo.txt': '', 26 | 'readme.md': '', 27 | test: { 28 | 'index.js': '', 29 | }, 30 | }, 31 | }) 32 | 33 | const arborist = new Arborist({ path: pkg }) 34 | const tree = await arborist.loadActual() 35 | const files = await packlist(tree) 36 | t.same(files, [ 37 | 'index.js', 38 | 'package.json', 39 | 'docs/bar.txt', 40 | 'docs/foo.txt', 41 | ]) 42 | }) 43 | 44 | t.test('does not filter based on package.json if subdirectory is not a workspace', async (t) => { 45 | const pkg = t.testdir({ 46 | 'package.json': JSON.stringify({ 47 | name: 'root', 48 | version: '1.0.0', 49 | files: ['docs/*.txt'], 50 | main: 'index.js', 51 | // this test needs a workspace to exist, but that workspace cannot be the one we include 52 | // files from 53 | workspaces: ['./unrelated'], 54 | }), 55 | 'index.js': '', 56 | docs: { 57 | 'package.json': JSON.stringify({ 58 | name: 'docs', 59 | version: '1.0.0', 60 | main: 'index.js', 61 | files: ['bar.txt', 'foo.txt'], 62 | }), 63 | 'bar.txt': '', 64 | 'baz.txt': '', 65 | 'foo.txt': '', 66 | 'readme.md': '', 67 | test: { 68 | 'index.js': '', 69 | }, 70 | }, 71 | unrelated: { 72 | 'package.json': JSON.stringify({ 73 | name: 'unrelated', 74 | version: '1.0.0', 75 | main: 'index.js', 76 | }), 77 | 'index.js': '', 78 | }, 79 | }) 80 | 81 | const arborist = new Arborist({ path: pkg }) 82 | const tree = await arborist.loadActual() 83 | const files = await packlist(tree) 84 | t.same(files, [ 85 | 'index.js', 86 | 'package.json', 87 | 'docs/bar.txt', 88 | 'docs/baz.txt', // was _not_ filtered 89 | 'docs/foo.txt', 90 | ]) 91 | }) 92 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/bin.js: -------------------------------------------------------------------------------- 1 | // disabling the no-sparse-arrays rule because we use them for assertions 2 | /* eslint-disable no-sparse-arrays */ 3 | 'use strict' 4 | 5 | const t = require('tap') 6 | const { spawnSync } = require('child_process') 7 | 8 | const nodePath = process.execPath 9 | const binPath = require.resolve('./utils/bin.js') 10 | 11 | const cwd = t.testdir({ 12 | 'package.json': JSON.stringify({ 13 | files: ['index.js', 'lib'], 14 | }), 15 | 'README.md': 'hello', 16 | 'LICENSE.txt': 'you can use it but you gotta pay me', 17 | 'index.js': 'console.log(/xss/)', 18 | lib: { 19 | 'index.js': 'console.log(/xss/)', 20 | 'cat.js': 'console.log("meow")', 21 | 'dog.js': 'console.log("woof")', 22 | 'chai.js': 'console.log("blub")', 23 | }, 24 | 'ignore.js': 'throw new Error("dont look at me!")', 25 | }) 26 | 27 | t.test('no args', async (t) => { 28 | const result = spawnSync(nodePath, [binPath], { cwd, encoding: 'utf8' }) 29 | t.equal(result.status, 0, 'completed successfully') 30 | t.same(result.stdout, [ 31 | 'lib/cat.js', 32 | 'lib/chai.js', 33 | 'lib/dog.js', 34 | 'index.js', 35 | 'lib/index.js', 36 | 'package.json', 37 | 'README.md', 38 | 'LICENSE.txt',, // empty element at the end so we get a trailing \n 39 | ].join('\n')) 40 | }) 41 | 42 | t.test('--sort', async (t) => { 43 | const result = spawnSync(nodePath, [binPath, '--sort'], { cwd, encoding: 'utf8' }) 44 | t.equal(result.status, 0, 'completed successfully') 45 | t.same(result.stdout, [ 46 | 'index.js', 47 | 'lib/cat.js', 48 | 'lib/chai.js', 49 | 'lib/dog.js', 50 | 'lib/index.js', 51 | 'LICENSE.txt', 52 | 'package.json', 53 | 'README.md',, // empty element at the end so we get a trailing \n 54 | ].join('\n')) 55 | }) 56 | 57 | t.test('-s', async (t) => { 58 | const result = spawnSync(nodePath, [binPath, '-s'], { cwd, encoding: 'utf8' }) 59 | t.equal(result.status, 0, 'completed successfully') 60 | t.same(result.stdout, [ 61 | 'index.js', 62 | 'lib/cat.js', 63 | 'lib/chai.js', 64 | 'lib/dog.js', 65 | 'lib/index.js', 66 | 'LICENSE.txt', 67 | 'package.json', 68 | 'README.md',, // empty element at the end so we get a trailing \n 69 | ].join('\n')) 70 | }) 71 | 72 | t.test('dir argument', async (t) => { 73 | const result = spawnSync(nodePath, [binPath, '.'], { cwd, encoding: 'utf8' }) 74 | t.equal(result.status, 0, 'completed successfully') 75 | t.same(result.stdout, [ 76 | '> .', // the directory name prefixed with "> " 77 | ' lib/cat.js', // the files will all be indented 78 | ' lib/chai.js', 79 | ' lib/dog.js', 80 | ' index.js', 81 | ' lib/index.js', 82 | ' package.json', 83 | ' README.md', 84 | ' LICENSE.txt',, // empty element at the end so we get a trailing \n 85 | ].join('\n')) 86 | }) 87 | 88 | t.test('-h', async (t) => { 89 | const result = spawnSync(nodePath, [binPath, '-h'], { cwd, encoding: 'utf8' }) 90 | t.equal(result.status, 0, 'completed successfully') 91 | t.match(result.stdout, /^usage: npm-packlist/, 'printed help') 92 | }) 93 | -------------------------------------------------------------------------------- /.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 | - 20.17.0 71 | - 20.x 72 | - 22.9.0 73 | - 22.x 74 | exclude: 75 | - platform: { name: macOS, os: macos-13, shell: bash } 76 | node-version: 20.17.0 77 | - platform: { name: macOS, os: macos-13, shell: bash } 78 | node-version: 20.x 79 | - platform: { name: macOS, os: macos-13, shell: bash } 80 | node-version: 22.9.0 81 | - platform: { name: macOS, os: macos-13, shell: bash } 82 | node-version: 22.x 83 | runs-on: ${{ matrix.platform.os }} 84 | defaults: 85 | run: 86 | shell: ${{ matrix.platform.shell }} 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | - name: Setup Git User 91 | run: | 92 | git config --global user.email "npm-cli+bot@github.com" 93 | git config --global user.name "npm CLI robot" 94 | - name: Setup Node 95 | uses: actions/setup-node@v4 96 | id: node 97 | with: 98 | node-version: ${{ matrix.node-version }} 99 | check-latest: contains(matrix.node-version, '.x') 100 | - name: Install Latest npm 101 | uses: ./.github/actions/install-latest-npm 102 | with: 103 | node: ${{ steps.node.outputs.node-version }} 104 | - name: Install Dependencies 105 | run: npm i --ignore-scripts --no-audit --no-fund 106 | - name: Add Problem Matcher 107 | run: echo "::add-matcher::.github/matchers/tap.json" 108 | - name: Test 109 | run: npm test --ignore-scripts 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-packlist 2 | 3 | Get a list of the files to add from a folder into an npm package. 4 | 5 | These can be handed to [tar](http://npm.im/tar) like so to make an npm 6 | package tarball: 7 | 8 | ```js 9 | const Arborist = require('@npmcli/arborist') 10 | const packlist = require('npm-packlist') 11 | const tar = require('tar') 12 | const packageDir = '/path/to/package' 13 | const packageTarball = '/path/to/package.tgz' 14 | 15 | const arborist = new Arborist({ path: packageDir }) 16 | arborist.loadActual().then((tree) => { 17 | packlist(tree) 18 | .then(files => tar.create({ 19 | prefix: 'package/', 20 | cwd: packageDir, 21 | file: packageTarball, 22 | gzip: true 23 | }, files)) 24 | .then(_ => { 25 | // tarball has been created, continue with your day 26 | }) 27 | }) 28 | ``` 29 | 30 | This uses the following rules: 31 | 32 | 1. If a `package.json` file is found, and it has a `files` list, 33 | then ignore everything that isn't in `files`. Always include the root 34 | readme, license, licence and copying files, if they exist, as well 35 | as the package.json file itself. Non-root readme, license, licence and 36 | copying files are included by default, but can be excluded using the 37 | `files` list e.g. `"!readme"`. 38 | 2. If there's no `package.json` file (or it has no `files` list), and 39 | there is a `.npmignore` file, then ignore all the files in the 40 | `.npmignore` file. 41 | 3. If there's no `package.json` with a `files` list, and there's no 42 | `.npmignore` file, but there is a `.gitignore` file, then ignore 43 | all the files in the `.gitignore` file. 44 | 4. Everything in the root `node_modules` is ignored, unless it's a 45 | bundled dependency. If it IS a bundled dependency, and it's a 46 | symbolic link, then the target of the link is included, not the 47 | symlink itself. 48 | 4. Unless they're explicitly included (by being in a `files` list, or 49 | a `!negated` rule in a relevant `.npmignore` or `.gitignore`), 50 | always ignore certain common cruft files: 51 | 52 | 1. .npmignore and .gitignore files (their effect is in the package 53 | already, there's no need to include them in the package) 54 | 2. editor junk like `.*.swp`, `._*` and `.*.orig` files 55 | 3. `.npmrc` files (these may contain private configs) 56 | 4. The `node_modules/.bin` folder 57 | 5. Waf and gyp cruft like `/build/config.gypi` and `.lock-wscript` 58 | 6. Darwin's `.DS_Store` files because wtf are those even 59 | 7. `npm-debug.log` files at the root of a project 60 | 61 | You can explicitly re-include any of these with a `files` list in 62 | `package.json` or a negated ignore file rule. 63 | 64 | Only the `package.json` file in the very root of the project is ever 65 | inspected for a `files` list. Below the top level of the root package, 66 | `package.json` is treated as just another file, and no package-specific 67 | semantics are applied. 68 | 69 | ### Interaction between `package.json` and `.npmignore` rules 70 | 71 | In previous versions of this library, the `files` list in `package.json` 72 | was used as an initial filter to drive further tree walking. That is no 73 | longer the case as of version 6.0.0. 74 | 75 | If you have a `package.json` file with a `files` array within, any top 76 | level `.npmignore` and `.gitignore` files *will be ignored*. 77 | 78 | If a _directory_ is listed in `files`, then any rules in nested `.npmignore` files within that directory will be honored. 79 | 80 | For example, with this package.json: 81 | 82 | ```json 83 | { 84 | "files": [ "dir" ] 85 | } 86 | ``` 87 | 88 | a `.npmignore` file at `dir/.npmignore` (and any subsequent 89 | sub-directories) will be honored. However, a `.npmignore` at the root 90 | level will be skipped. 91 | 92 | Additionally, with this package.json: 93 | 94 | ``` 95 | { 96 | "files": ["dir/subdir"] 97 | } 98 | ``` 99 | 100 | a `.npmignore` file at `dir/.npmignore` will be honored, as well as `dir/subdir/.npmignore`. 101 | 102 | Any specific file matched by an exact filename in the package.json `files` list will be included, and cannot be excluded, by any `.npmignore` files. 103 | 104 | ## API 105 | 106 | Same API as [ignore-walk](http://npm.im/ignore-walk), except providing a `tree` is required and there are hard-coded file list and rule sets. 107 | 108 | The `Walker` class requires an [arborist](https://github.com/npm/cli/tree/latest/workspaces/arborist) tree, and if any bundled dependencies are found will include them as well as their own dependencies in the resulting file set. 109 | -------------------------------------------------------------------------------- /.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 | - 20.17.0 95 | - 20.x 96 | - 22.9.0 97 | - 22.x 98 | exclude: 99 | - platform: { name: macOS, os: macos-13, shell: bash } 100 | node-version: 20.17.0 101 | - platform: { name: macOS, os: macos-13, shell: bash } 102 | node-version: 20.x 103 | - platform: { name: macOS, os: macos-13, shell: bash } 104 | node-version: 22.9.0 105 | - platform: { name: macOS, os: macos-13, shell: bash } 106 | node-version: 22.x 107 | runs-on: ${{ matrix.platform.os }} 108 | defaults: 109 | run: 110 | shell: ${{ matrix.platform.shell }} 111 | steps: 112 | - name: Checkout 113 | uses: actions/checkout@v4 114 | with: 115 | ref: ${{ inputs.ref }} 116 | - name: Setup Git User 117 | run: | 118 | git config --global user.email "npm-cli+bot@github.com" 119 | git config --global user.name "npm CLI robot" 120 | - name: Create Check 121 | id: create-check 122 | if: ${{ inputs.check-sha }} 123 | uses: ./.github/actions/create-check 124 | with: 125 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 126 | token: ${{ secrets.GITHUB_TOKEN }} 127 | sha: ${{ inputs.check-sha }} 128 | - name: Setup Node 129 | uses: actions/setup-node@v4 130 | id: node 131 | with: 132 | node-version: ${{ matrix.node-version }} 133 | check-latest: contains(matrix.node-version, '.x') 134 | - name: Install Latest npm 135 | uses: ./.github/actions/install-latest-npm 136 | with: 137 | node: ${{ steps.node.outputs.node-version }} 138 | - name: Install Dependencies 139 | run: npm i --ignore-scripts --no-audit --no-fund 140 | - name: Add Problem Matcher 141 | run: echo "::add-matcher::.github/matchers/tap.json" 142 | - name: Test 143 | run: npm test --ignore-scripts 144 | - name: Conclude Check 145 | uses: LouisBrunner/checks-action@v1.6.0 146 | if: steps.create-check.outputs.check-id && always() 147 | with: 148 | token: ${{ secrets.GITHUB_TOKEN }} 149 | conclusion: ${{ job.status }} 150 | check_id: ${{ steps.create-check.outputs.check-id }} 151 | -------------------------------------------------------------------------------- /.github/workflows/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=--workspace ${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 | -------------------------------------------------------------------------------- /test/workspace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Arborist = require('@npmcli/arborist') 4 | const path = require('path') 5 | const t = require('tap') 6 | 7 | const packlist = require('../') 8 | 9 | t.test('respects workspace root ignore files', async (t) => { 10 | const root = t.testdir({ 11 | 'package.json': JSON.stringify({ 12 | name: 'workspace-root', 13 | version: '1.0.0', 14 | main: 'root.js', 15 | workspaces: ['./workspaces/foo'], 16 | }), 17 | 'root.js': `console.log('hello')`, 18 | '.gitignore': 'ignore-me', 19 | 'ignore-me': 'should be ignored', 20 | workspaces: { 21 | '.gitignore': 'ignore-me-also', 22 | 'ignore-me': 'should be ignored', 23 | 'ignore-me-also': 'should also be ignored', 24 | foo: { 25 | 'package.json': JSON.stringify({ 26 | name: 'workspace-child', 27 | version: '1.0.0', 28 | main: 'child.js', 29 | }), 30 | 'child.js': `console.log('hello')`, 31 | 'ignore-me': 'should be ignored', 32 | 'ignore-me-also': 'should also be ignored', 33 | }, 34 | }, 35 | }) 36 | 37 | const workspacePath = path.join(root, 'workspaces', 'foo') 38 | const arborist = new Arborist({ path: workspacePath }) 39 | const tree = await arborist.loadActual() 40 | // this simulates what it looks like when a user does i.e. npm pack -w ./workspaces/foo 41 | const files = await packlist(tree, { 42 | prefix: root, 43 | workspaces: [workspacePath], 44 | }) 45 | t.same(files, [ 46 | 'child.js', 47 | 'package.json', 48 | ]) 49 | 50 | // leave off workspaces to prove that when prefix and root differ there is no change 51 | // in behavior without also specifying workspaces 52 | const secondFiles = await packlist(tree, { 53 | prefix: root, 54 | }) 55 | t.same(secondFiles, [ 56 | 'ignore-me', 57 | 'ignore-me-also', 58 | 'child.js', 59 | 'package.json', 60 | ]) 61 | }) 62 | 63 | t.test('packing a workspace root does not include children', async (t) => { 64 | const root = t.testdir({ 65 | 'package.json': JSON.stringify({ 66 | name: 'workspace-root', 67 | version: '1.0.0', 68 | main: 'root.js', 69 | workspaces: ['./workspaces/foo'], 70 | }), 71 | 'root.js': `console.log('hello')`, 72 | '.gitignore': 'ignore-me', 73 | 'ignore-me': 'should be ignored', 74 | workspaces: { 75 | '.gitignore': 'ignore-me-also', 76 | 'ignore-me': 'should be ignored', 77 | 'ignore-me-also': 'should also be ignored', 78 | foo: { 79 | 'package.json': JSON.stringify({ 80 | name: 'workspace-child', 81 | version: '1.0.0', 82 | main: 'child.js', 83 | }), 84 | 'child.js': `console.log('hello')`, 85 | 'ignore-me': 'should be ignored', 86 | 'ignore-me-also': 'should also be ignored', 87 | }, 88 | }, 89 | }) 90 | 91 | const workspacePath = path.join(root, 'workspaces', 'foo') 92 | // this simulates what it looks like when a user does `npm pack` from a workspace root 93 | const arborist = new Arborist({ path: root }) 94 | const tree = await arborist.loadActual() 95 | const files = await packlist(tree, { 96 | prefix: root, 97 | workspaces: [workspacePath], 98 | }) 99 | t.same(files, [ 100 | 'root.js', 101 | 'package.json', 102 | ]) 103 | 104 | // prove if we leave off workspaces we do not omit them 105 | const secondFiles = await packlist(tree, { 106 | prefix: root, 107 | }) 108 | t.same(secondFiles, [ 109 | 'workspaces/foo/child.js', 110 | 'root.js', 111 | 'package.json', 112 | 'workspaces/foo/package.json', 113 | ]) 114 | }) 115 | 116 | t.test('.gitignore is discarded if .npmignore exists outside of tree', async (t) => { 117 | const root = t.testdir({ 118 | 'package.json': JSON.stringify({ 119 | name: 'workspace-root', 120 | version: '1.0.0', 121 | main: 'root.js', 122 | workspaces: ['./workspaces/foo'], 123 | }), 124 | 'root.js': `console.log('hello')`, 125 | '.gitignore': 'dont-ignore-me', 126 | '.npmignore': 'only-ignore-me', 127 | 'dont-ignore-me': 'should not be ignored', 128 | 'only-ignore-me': 'should be ignored', 129 | workspaces: { 130 | '.gitignore': 'dont-ignore-me-either', 131 | '.npmignore': 'ignore-me-also', 132 | 'dont-ignore-me': 'should not be ignored', 133 | 'dont-ignore-me-either': 'should not be ignored', 134 | 'only-ignore-me': 'should be ignored', 135 | 'ignore-me-also': 'should be ignored', 136 | foo: { 137 | 'package.json': JSON.stringify({ 138 | name: 'workspace-child', 139 | version: '1.0.0', 140 | main: 'child.js', 141 | }), 142 | 'child.js': `console.log('hello')`, 143 | 'dont-ignore-me': 'should not be ignored', 144 | 'dont-ignore-me-either': 'should not be ignored', 145 | 'only-ignore-me': 'should be ignored', 146 | 'ignore-me-also': 'should also be ignored', 147 | }, 148 | }, 149 | }) 150 | 151 | const workspacePath = path.join(root, 'workspaces', 'foo') 152 | // this simulates what it looks like when a user does i.e. npm pack -w ./workspaces/foo 153 | const arborist = new Arborist({ path: workspacePath }) 154 | const tree = await arborist.loadActual() 155 | const files = await packlist(tree, { 156 | prefix: root, 157 | workspaces: [workspacePath], 158 | }) 159 | t.same(files, [ 160 | 'dont-ignore-me', 161 | 'dont-ignore-me-either', 162 | 'child.js', 163 | 'package.json', 164 | ]) 165 | 166 | // here we leave off workspaces to satisfy coverage 167 | const secondFiles = await packlist(tree, { 168 | prefix: root, 169 | }) 170 | t.same(secondFiles, [ 171 | 'dont-ignore-me', 172 | 'dont-ignore-me-either', 173 | 'ignore-me-also', 174 | 'only-ignore-me', 175 | 'child.js', 176 | 'package.json', 177 | ]) 178 | }) 179 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [10.0.3](https://github.com/npm/npm-packlist/compare/v10.0.2...v10.0.3) (2025-10-22) 4 | ### Dependencies 5 | * [`7b4c677`](https://github.com/npm/npm-packlist/commit/7b4c6774b65e8bee0c4fe7de8b282d673037a361) [#270](https://github.com/npm/npm-packlist/pull/270) bump proc-log from 5.0.0 to 6.0.0 (#270) (@dependabot[bot]) 6 | ### Chores 7 | * [`c23dea8`](https://github.com/npm/npm-packlist/commit/c23dea831f8aac9a38366e61c0355197a30ff84d) [#269](https://github.com/npm/npm-packlist/pull/269) bump @npmcli/template-oss from 4.26.0 to 4.27.1 (#269) (@dependabot[bot], @npm-cli-bot) 8 | 9 | ## [10.0.2](https://github.com/npm/npm-packlist/compare/v10.0.1...v10.0.2) (2025-09-22) 10 | ### Bug Fixes 11 | * [`566ffc2`](https://github.com/npm/npm-packlist/commit/566ffc25998c356ceb578adf2a57b89997b27ff4) [#265](https://github.com/npm/npm-packlist/pull/265) log warning for .gitignore versus .npmignore logic (#265) (@simwai) 12 | ### Chores 13 | * [`5d905ef`](https://github.com/npm/npm-packlist/commit/5d905ef11c814c4ac6badf92ab8057f4b624c2b1) [#266](https://github.com/npm/npm-packlist/pull/266) bump @npmcli/template-oss from 4.25.0 to 4.25.1 (#266) (@dependabot[bot], @npm-cli-bot) 14 | 15 | ## [10.0.1](https://github.com/npm/npm-packlist/compare/v10.0.0...v10.0.1) (2025-07-24) 16 | ### Dependencies 17 | * [`4bd0838`](https://github.com/npm/npm-packlist/commit/4bd08382015ce15989cf634d60b491648418280e) [#262](https://github.com/npm/npm-packlist/pull/262) `ignore-walk@8.0.0` 18 | ### Chores 19 | * [`7e5f43b`](https://github.com/npm/npm-packlist/commit/7e5f43bcab016a62d406c3ad4c391ee436fb76d3) [#256](https://github.com/npm/npm-packlist/pull/256) bump @npmcli/arborist from 8.0.0 to 9.0.0 (#256) (@dependabot[bot]) 20 | * [`7e3cd57`](https://github.com/npm/npm-packlist/commit/7e3cd572c111d02ca6e1fdf55bcfa1e6ae77b7a2) [#261](https://github.com/npm/npm-packlist/pull/261) bump @npmcli/template-oss from 4.24.4 to 4.25.0 (#261) (@dependabot[bot], @owlstronaut) 21 | 22 | ## [10.0.0](https://github.com/npm/npm-packlist/compare/v9.0.0...v10.0.0) (2024-11-25) 23 | ### ⚠️ BREAKING CHANGES 24 | * this module is now compatible with the following node versions: ^20.17.0 || >=22.9.0 25 | * `bun.lockb` is now included in the strict ignorelist 26 | ### Features 27 | * [`9f74fd3`](https://github.com/npm/npm-packlist/commit/9f74fd37c857840a8c361a8ada8a9e9524136658) [#168](https://github.com/npm/npm-packlist/pull/168) add `bun.lockb` to ignorelist (#168) (@antongolub) 28 | ### Bug Fixes 29 | * [`67fef03`](https://github.com/npm/npm-packlist/commit/67fef031feea8990a8a288c71d35a8a96e7d9e8e) update engines to ^20.17.0 || >=22.9.0 (#254) (@wraithgar) 30 | ### Chores 31 | * [`fd425fa`](https://github.com/npm/npm-packlist/commit/fd425fa70c15d14c2356656c67deb660680f714d) bump @npmcli/eslint-config from 4.0.5 to 5.0.1 (#250) (@dependabot[bot]) 32 | * [`3b66b5a`](https://github.com/npm/npm-packlist/commit/3b66b5a16cf0b23a39487c17cc5e755b57c89496) [#252](https://github.com/npm/npm-packlist/pull/252) bump @npmcli/arborist from 7.5.4 to 8.0.0 (#252) (@dependabot[bot]) 33 | * [`e688d26`](https://github.com/npm/npm-packlist/commit/e688d26302629e0b6eb7b1e8ee3d152d06306913) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#251) (@dependabot[bot], @npm-cli-bot) 34 | 35 | ## [9.0.0](https://github.com/npm/npm-packlist/compare/v8.0.2...v9.0.0) (2024-09-25) 36 | ### ⚠️ BREAKING CHANGES 37 | * `npm-packlist` now supports node `^18.17.0 || >=20.5.0` 38 | ### Bug Fixes 39 | * [`4679b12`](https://github.com/npm/npm-packlist/commit/4679b12aabbcd524a4b6ccd42601297a51b6bd7e) [#247](https://github.com/npm/npm-packlist/pull/247) align to npm 10 node engine range (@hashtagchris) 40 | ### Dependencies 41 | * [`b0500f4`](https://github.com/npm/npm-packlist/commit/b0500f4cb6a2e2e30d7c9df33dc367d6632f2d0a) [#247](https://github.com/npm/npm-packlist/pull/247) `ignore-walk@7.0.0` 42 | ### Chores 43 | * [`46ed801`](https://github.com/npm/npm-packlist/commit/46ed8013989409ab1f50354bcb6e3c0f989bd245) [#245](https://github.com/npm/npm-packlist/pull/245) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (#245) (@dependabot[bot]) 44 | * [`625ddca`](https://github.com/npm/npm-packlist/commit/625ddca24b20a0934b4a5b9ae14fee2232be979f) [#240](https://github.com/npm/npm-packlist/pull/240) bump @npmcli/arborist from 6.5.1 to 7.5.4 (#240) (@dependabot[bot]) 45 | * [`e2eb28d`](https://github.com/npm/npm-packlist/commit/e2eb28dec2f8abf75357564cca683c049006be50) [#247](https://github.com/npm/npm-packlist/pull/247) run template-oss-apply (@hashtagchris) 46 | * [`3c27bb3`](https://github.com/npm/npm-packlist/commit/3c27bb305d769ad0f8fd128a8b99787162444de2) [#230](https://github.com/npm/npm-packlist/pull/230) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 47 | * [`fa9e80d`](https://github.com/npm/npm-packlist/commit/fa9e80ddfa348daf690b548a22a887f3c7fadeec) [#242](https://github.com/npm/npm-packlist/pull/242) bump @npmcli/template-oss from 4.22.0 to 4.23.3 (#242) (@dependabot[bot]) 48 | * [`cb4a823`](https://github.com/npm/npm-packlist/commit/cb4a823cd42d50475a8e1e7582b95b15766f5ca2) [#230](https://github.com/npm/npm-packlist/pull/230) postinstall for dependabot template-oss PR (@lukekarrys) 49 | 50 | ## [8.0.2](https://github.com/npm/npm-packlist/compare/v8.0.1...v8.0.2) (2024-01-03) 51 | 52 | ### Bug Fixes 53 | 54 | * [`728d68a`](https://github.com/npm/npm-packlist/commit/728d68ad3dfeb86de6d319f05435a6ac7319232e) [#214](https://github.com/npm/npm-packlist/pull/214) handling of directory glob (#214) (@mohd-akram) 55 | * [`837af58`](https://github.com/npm/npm-packlist/commit/837af580e4b5623afdc1d034b533290bc0a93ebb) [#210](https://github.com/npm/npm-packlist/pull/210) avoid modifying singleton variable (#210) (@mohd-akram) 56 | 57 | ## [8.0.1](https://github.com/npm/npm-packlist/compare/v8.0.0...v8.0.1) (2023-12-06) 58 | 59 | ### Bug Fixes 60 | 61 | * [`8fc4df1`](https://github.com/npm/npm-packlist/commit/8fc4df1b01ddfa99c476c1108f557ab6e172b28c) [#204](https://github.com/npm/npm-packlist/pull/204) always ignore .npmrc files at every level (@wraithgar) 62 | * [`cd5ddbd`](https://github.com/npm/npm-packlist/commit/cd5ddbd9fc7069d62fd89e0de741523e408c889b) [#205](https://github.com/npm/npm-packlist/pull/205) preserve slashes in specified files (#205) (@mohd-akram) 63 | 64 | ### Dependencies 65 | 66 | * [`6058cc5`](https://github.com/npm/npm-packlist/commit/6058cc5ffd8850c0bc7d0235586d21d5724f1c78) [#204](https://github.com/npm/npm-packlist/pull/204) `ignore-walk@6.0.4` 67 | 68 | ### Chores 69 | 70 | * [`20fe0cd`](https://github.com/npm/npm-packlist/commit/20fe0cdfbf0d4b447745c6149b502510f5a9655a) [#207](https://github.com/npm/npm-packlist/pull/207) bump @npmcli/template-oss from 4.21.1 to 4.21.2 (#207) (@dependabot[bot], @lukekarrys) 71 | * [`f8be7bd`](https://github.com/npm/npm-packlist/commit/f8be7bd0d4954b675c3da9f4954df15fb60dc071) [#202](https://github.com/npm/npm-packlist/pull/202) bump @npmcli/template-oss from 4.19.0 to 4.21.1 (#202) (@dependabot[bot], @lukekarrys) 72 | * [`8e1d900`](https://github.com/npm/npm-packlist/commit/8e1d9008cf8ead74a27530d3cda89200fe40f28c) [#204](https://github.com/npm/npm-packlist/pull/204) tests reflect fixed ignore-walk rules (@wraithgar) 73 | * [`6d7cbe9`](https://github.com/npm/npm-packlist/commit/6d7cbe9719fc16115d622398f64e588445fecfad) [#181](https://github.com/npm/npm-packlist/pull/181) postinstall for dependabot template-oss PR (@lukekarrys) 74 | * [`80ec501`](https://github.com/npm/npm-packlist/commit/80ec50165e98050988c0bbb935faf43163fcb5af) [#181](https://github.com/npm/npm-packlist/pull/181) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot]) 75 | * [`f327738`](https://github.com/npm/npm-packlist/commit/f327738576e0076592880633df358a4b9f76f3b4) [#179](https://github.com/npm/npm-packlist/pull/179) postinstall for dependabot template-oss PR (@lukekarrys) 76 | * [`a770a96`](https://github.com/npm/npm-packlist/commit/a770a96ba042b2221aa909a172bb3ffb51f8fb22) [#179](https://github.com/npm/npm-packlist/pull/179) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot]) 77 | 78 | ## [8.0.0](https://github.com/npm/npm-packlist/compare/v7.0.4...v8.0.0) (2023-08-24) 79 | 80 | ### ⚠️ BREAKING CHANGES 81 | 82 | * The files array can now be used to exclude non-root readme, license, licence, and copying files. 83 | 84 | ### Bug Fixes 85 | 86 | * [`24344a2`](https://github.com/npm/npm-packlist/commit/24344a2ce2f5a860d8c6048c642bae8db50b9618) [#173](https://github.com/npm/npm-packlist/pull/173) exclude non-root README.md/LICENSE files (#173) (@AaronHamilton965, @rahulio96) 87 | 88 | ## [7.0.4](https://github.com/npm/npm-packlist/compare/v7.0.3...v7.0.4) (2022-12-07) 89 | 90 | ### Bug Fixes 91 | 92 | * [`e5256de`](https://github.com/npm/npm-packlist/commit/e5256de99776ab243ab40653e960a20a8ab6a2ab) [#149](https://github.com/npm/npm-packlist/pull/149) skip missing optional deps when bundling, closes npm/cli#5924 (#149) (@nlf) 93 | 94 | ## [7.0.3](https://github.com/npm/npm-packlist/compare/v7.0.2...v7.0.3) (2022-12-07) 95 | 96 | ### Bug Fixes 97 | 98 | * [`c6f2b69`](https://github.com/npm/npm-packlist/commit/c6f2b69b025575dc683f26f3d098e23a22c462da) [#147](https://github.com/npm/npm-packlist/pull/147) treat glob the same as globstar (#147) (@lukekarrys) 99 | 100 | ## [7.0.2](https://github.com/npm/npm-packlist/compare/v7.0.1...v7.0.2) (2022-10-26) 101 | 102 | ### Bug Fixes 103 | 104 | * [`d5b653c`](https://github.com/npm/npm-packlist/commit/d5b653ce860f6601fb5007c7fcec903c8d58c1c8) [#140](https://github.com/npm/npm-packlist/pull/140) account for directories and files prefixed with `./` (#140) (@wraithgar) 105 | 106 | ## [7.0.1](https://github.com/npm/npm-packlist/compare/v7.0.0...v7.0.1) (2022-10-17) 107 | 108 | ### Dependencies 109 | 110 | * [`250d589`](https://github.com/npm/npm-packlist/commit/250d5896669e2fc84e2512124c28541e5396aad8) [#136](https://github.com/npm/npm-packlist/pull/136) bump ignore-walk from 5.0.1 to 6.0.0 111 | 112 | ## [7.0.0](https://github.com/npm/npm-packlist/compare/v7.0.0-pre.1...v7.0.0) (2022-10-04) 113 | 114 | ### Features 115 | 116 | * [`b83e0aa`](https://github.com/npm/npm-packlist/commit/b83e0aaa31936496499283fc44b773645d3dd969) [#133](https://github.com/npm/npm-packlist/pull/133) set as release (@fritzy) 117 | 118 | ## [7.0.0-pre.1](https://github.com/npm/npm-packlist/compare/v7.0.0-pre.0...v7.0.0-pre.1) (2022-10-03) 119 | 120 | ### ⚠️ BREAKING CHANGES 121 | 122 | * if npm-shrinkwrap.json is included in your .npmignore, the shrinkwrap will now be excluded from your packlist. 123 | 124 | ### Features 125 | 126 | * [`5e80968`](https://github.com/npm/npm-packlist/commit/5e8096883080d8d1650d4558fb62e34ce61d0dda) [#131](https://github.com/npm/npm-packlist/pull/131) npm-shrinkwrap.json files can now be ignored (#131) (@fritzy) 127 | 128 | ## [7.0.0-pre.0](https://github.com/npm/npm-packlist/compare/v6.0.1...v7.0.0-pre.0) (2022-09-26) 129 | 130 | ### ⚠️ BREAKING CHANGES 131 | 132 | * `tree` is now the first parameter 133 | * the arborist tree must now be provided in the options and will not be generated for you. the npm-packlist bin has also been removed. 134 | 135 | ### Features 136 | 137 | * [`87c778e`](https://github.com/npm/npm-packlist/commit/87c778eb0f89101ff5ef4a488e614fb9e7773480) make the required tree the first parameter (@lukekarrys) 138 | * [`123875a`](https://github.com/npm/npm-packlist/commit/123875a9db0d18c2ba91e9841ab8e15df3057e18) remove dependency on arborist, require tree to be passed in (@nlf) 139 | 140 | ## [6.0.1](https://github.com/npm/npm-packlist/compare/v6.0.0...v6.0.1) (2022-09-23) 141 | 142 | ### Dependencies 143 | 144 | * [`f823be6`](https://github.com/npm/npm-packlist/commit/f823be6e9bc9239acbb8e8b6149f0a8b457a0d0a) [#125](https://github.com/npm/npm-packlist/pull/125) arborist@5||6||6.pre 145 | 146 | ## [6.0.0](https://github.com/npm/npm-packlist/compare/v5.1.3...v6.0.0) (2022-09-21) 147 | 148 | ### ⚠️ BREAKING CHANGES 149 | 150 | * this module now follows a strict order of operations when applying ignore rules. if a `files` array is present in the package.json, then rules in `.gitignore` and `.npmignore` files from the root will be ignored. 151 | * `npm-packlist` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 152 | 153 | ### Features 154 | 155 | * [`c37371b`](https://github.com/npm/npm-packlist/commit/c37371b060ee42ebaebf134f37471c8330fc4d27) [#88](https://github.com/npm/npm-packlist/pull/88) change interactions between `files` array and ignore files to be more consistent (#88) (@nlf) 156 | * [`a2c96ef`](https://github.com/npm/npm-packlist/commit/a2c96effdadd1cee1d6b584b92a45a7d09b260cb) [#123](https://github.com/npm/npm-packlist/pull/123) postinstall for dependabot template-oss PR (@lukekarrys) 157 | 158 | ## [5.1.3](https://github.com/npm/npm-packlist/compare/v5.1.2...v5.1.3) (2022-08-25) 159 | 160 | 161 | ### Dependencies 162 | 163 | * bump npm-bundled from 1.1.2 to 2.0.0 ([#113](https://github.com/npm/npm-packlist/issues/113)) ([de5e96b](https://github.com/npm/npm-packlist/commit/de5e96bbdd9ccf345104715fe3080c4d239b7f3b)) 164 | 165 | ## [5.1.2](https://github.com/npm/npm-packlist/compare/v5.1.1...v5.1.2) (2022-08-23) 166 | 167 | 168 | ### Dependencies 169 | 170 | * bump npm-normalize-package-bin from 1.0.1 to 2.0.0 ([#110](https://github.com/npm/npm-packlist/issues/110)) ([c97d1a8](https://github.com/npm/npm-packlist/commit/c97d1a8b0d61ed76422bd7da0adaa4285c2a1cb7)) 171 | 172 | ### [5.1.1](https://github.com/npm/npm-packlist/compare/v5.1.0...v5.1.1) (2022-05-31) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * correctly ignore .gitignore when a .npmignore is present ([#108](https://github.com/npm/npm-packlist/issues/108)) ([da1ba4a](https://github.com/npm/npm-packlist/commit/da1ba4a3051ea822d9625347e00c186677247a45)) 178 | 179 | ## [5.1.0](https://github.com/npm/npm-packlist/compare/v5.0.4...v5.1.0) (2022-05-25) 180 | 181 | 182 | ### Features 183 | 184 | * correctly handle workspace roots ([#104](https://github.com/npm/npm-packlist/issues/104)) ([d74bdd1](https://github.com/npm/npm-packlist/commit/d74bdd157ef6a701c9aeef902be9e0e51f388c98)) 185 | 186 | ### [5.0.4](https://github.com/npm/npm-packlist/compare/v5.0.3...v5.0.4) (2022-05-19) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * do not pack workspaces by default ([0f31f71](https://github.com/npm/npm-packlist/commit/0f31f71ab652c1bc9250bcb1603357ee7b4fbf28)) 192 | * respect gitignore and npmignore files in workspace roots ([839e6e8](https://github.com/npm/npm-packlist/commit/839e6e8b13dc8c5ec14fab79509649d081c3ef54)) 193 | 194 | ### [5.0.3](https://github.com/npm/npm-packlist/compare/v5.0.2...v5.0.3) (2022-05-04) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * strip leading ./ from files array entries ([#97](https://github.com/npm/npm-packlist/issues/97)) ([9f519b7](https://github.com/npm/npm-packlist/commit/9f519b7d38ee46e08dc77b3b730842a2ca0e7500)) 200 | 201 | ### [5.0.2](https://github.com/npm/npm-packlist/compare/v5.0.1...v5.0.2) (2022-04-21) 202 | 203 | 204 | ### Bug Fixes 205 | 206 | * normalize win32 paths before globbing ([16f1343](https://github.com/npm/npm-packlist/commit/16f13436ebe31144ea86e3d2c7f1f16022f82885)) 207 | 208 | ### [5.0.1](https://github.com/npm/npm-packlist/compare/v5.0.0...v5.0.1) (2022-04-20) 209 | 210 | 211 | ### Dependencies 212 | 213 | * bump glob from 7.2.0 to 8.0.1 ([#90](https://github.com/npm/npm-packlist/issues/90)) ([dc3d40a](https://github.com/npm/npm-packlist/commit/dc3d40a1b89019e5343f76c184cd2fbb296fdb27)) 214 | 215 | ## [5.0.0](https://github.com/npm/npm-packlist/compare/v4.0.0...v5.0.0) (2022-04-06) 216 | 217 | 218 | ### ⚠ BREAKING CHANGES 219 | 220 | * this module no longer supports synchronous usage 221 | 222 | ### Features 223 | 224 | * remove synchronous interface ([#80](https://github.com/npm/npm-packlist/issues/80)) ([eb68f64](https://github.com/npm/npm-packlist/commit/eb68f64fe7d70d4776922246dd9cea5da6f1e21a)) 225 | 226 | 227 | ### Bug Fixes 228 | 229 | * remove polynomial regex ([b2e2f7b](https://github.com/npm/npm-packlist/commit/b2e2f7b9122b15c8f8041953aa07b5436232b903)) 230 | * replace deprecated String.prototype.substr() ([#85](https://github.com/npm/npm-packlist/issues/85)) ([79d6f7e](https://github.com/npm/npm-packlist/commit/79d6f7ebd5b881b3a3ec393769dd132a9a438778)) 231 | 232 | 233 | ### Dependencies 234 | 235 | * bump ignore-walk from 4.0.1 to 5.0.1 ([#87](https://github.com/npm/npm-packlist/issues/87)) ([0e241f5](https://github.com/npm/npm-packlist/commit/0e241f50e57b95274cd988e09763e205020c5b84)) 236 | 237 | ## [4.0.0](https://www.github.com/npm/npm-packlist/compare/v3.0.0...v4.0.0) (2022-03-03) 238 | 239 | 240 | ### ⚠ BREAKING CHANGES 241 | 242 | * This drops support for node10 and non-LTS versions of node12 and node14 243 | 244 | ### Bug Fixes 245 | 246 | * always ignore pnpm-lock.yaml ([#72](https://www.github.com/npm/npm-packlist/issues/72)) ([f56a4cf](https://www.github.com/npm/npm-packlist/commit/f56a4cf77fbbb123f3c818777cf00555538e1c1c)) 247 | 248 | 249 | * @npmcli/template-oss@2.9.1 ([0401893](https://www.github.com/npm/npm-packlist/commit/04018939fc7ae6ceed1504a2fa4de44cfa049036)) 250 | 251 | 252 | ### Dependencies 253 | 254 | * update glob requirement from ^7.1.6 to ^7.2.0 ([#70](https://www.github.com/npm/npm-packlist/issues/70)) ([d6b34ac](https://www.github.com/npm/npm-packlist/commit/d6b34ac471215290f2198c5ad14c8eed8b203179)) 255 | * update npm-bundled requirement from ^1.1.1 to ^1.1.2 ([#71](https://www.github.com/npm/npm-packlist/issues/71)) ([2a5e140](https://www.github.com/npm/npm-packlist/commit/2a5e1402fb4617fc3791b2be405aa3bbb3181ff3)) 256 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Walker: IgnoreWalker } = require('ignore-walk') 4 | const { lstatSync: lstat, readFileSync: readFile } = require('fs') 5 | const { basename, dirname, extname, join, relative, resolve, sep } = require('path') 6 | const { log } = require('proc-log') 7 | 8 | // symbols used to represent synthetic rule sets 9 | const defaultRules = Symbol('npm-packlist.rules.default') 10 | const strictRules = Symbol('npm-packlist.rules.strict') 11 | 12 | // There may be others, but :?|<> are handled by node-tar 13 | const nameIsBadForWindows = file => /\*/.test(file) 14 | 15 | // these are the default rules that are applied to everything except for non-link bundled deps 16 | const defaults = [ 17 | '.npmignore', 18 | '.gitignore', 19 | '**/.git', 20 | '**/.svn', 21 | '**/.hg', 22 | '**/CVS', 23 | '**/.git/**', 24 | '**/.svn/**', 25 | '**/.hg/**', 26 | '**/CVS/**', 27 | '/.lock-wscript', 28 | '/.wafpickle-*', 29 | '/build/config.gypi', 30 | 'npm-debug.log', 31 | '**/.npmrc', 32 | '.*.swp', 33 | '.DS_Store', 34 | '**/.DS_Store/**', 35 | '._*', 36 | '**/._*/**', 37 | '*.orig', 38 | '/archived-packages/**', 39 | ] 40 | 41 | const strictDefaults = [ 42 | // these are forcibly excluded 43 | '/.git', 44 | ] 45 | 46 | const normalizePath = (path) => path.split('\\').join('/') 47 | 48 | const readOutOfTreeIgnoreFiles = (root, rel, result = []) => { 49 | for (const file of ['.npmignore', '.gitignore']) { 50 | try { 51 | const ignoreContent = readFile(join(root, file), { encoding: 'utf8' }) 52 | result.push(ignoreContent) 53 | // break the loop immediately after reading, this allows us to prioritize 54 | // the .npmignore and discard the .gitignore if one is present 55 | break 56 | } catch (err) { 57 | // we ignore ENOENT errors completely because we don't care if the file doesn't exist 58 | // but we throw everything else because failing to read a file that does exist is 59 | // something that the user likely wants to know about 60 | // istanbul ignore next -- we do not need to test a thrown error 61 | if (err.code !== 'ENOENT') { 62 | throw err 63 | } 64 | } 65 | } 66 | 67 | if (!rel) { 68 | return result 69 | } 70 | 71 | const firstRel = rel.split(sep, 1)[0] 72 | const newRoot = join(root, firstRel) 73 | const newRel = relative(newRoot, join(root, rel)) 74 | 75 | return readOutOfTreeIgnoreFiles(newRoot, newRel, result) 76 | } 77 | 78 | class PackWalker extends IgnoreWalker { 79 | constructor (tree, opts) { 80 | const options = { 81 | ...opts, 82 | includeEmpty: false, 83 | follow: false, 84 | // we path.resolve() here because ignore-walk doesn't do it and we want full paths 85 | path: resolve(opts?.path || tree.path).replace(/\\/g, '/'), 86 | ignoreFiles: opts?.ignoreFiles || [ 87 | defaultRules, 88 | 'package.json', 89 | '.npmignore', 90 | '.gitignore', 91 | strictRules, 92 | ], 93 | } 94 | 95 | super(options) 96 | 97 | this.isPackage = options.isPackage 98 | this.seen = options.seen || new Set() 99 | this.tree = tree 100 | this.requiredFiles = options.requiredFiles || [] 101 | 102 | const additionalDefaults = [] 103 | if (options.prefix && options.workspaces) { 104 | const path = normalizePath(options.path) 105 | const prefix = normalizePath(options.prefix) 106 | const workspaces = options.workspaces.map((ws) => normalizePath(ws)) 107 | 108 | // istanbul ignore else - this does nothing unless we need it to 109 | if (path !== prefix && workspaces.includes(path)) { 110 | // if path and prefix are not the same directory, and workspaces has path in it 111 | // then we know path is a workspace directory. in order to not drop ignore rules 112 | // from directories between the workspaces root (prefix) and the workspace itself 113 | // (path) we need to find and read those now 114 | const relpath = relative(options.prefix, dirname(options.path)) 115 | additionalDefaults.push(...readOutOfTreeIgnoreFiles(options.prefix, relpath)) 116 | } else if (path === prefix) { 117 | // on the other hand, if the path and prefix are the same, then we ignore workspaces 118 | // so that we don't pack a workspace as part of the root project. append them as 119 | // normalized relative paths from the root 120 | additionalDefaults.push(...workspaces.map((w) => normalizePath(relative(options.path, w)))) 121 | } 122 | } 123 | 124 | // go ahead and inject the default rules now 125 | this.injectRules(defaultRules, [...defaults, ...additionalDefaults]) 126 | 127 | if (!this.isPackage) { 128 | // if this instance is not a package, then place some strict default rules, and append 129 | // known required files for this directory 130 | this.injectRules(strictRules, [ 131 | ...strictDefaults, 132 | ...this.requiredFiles.map((file) => `!${file}`), 133 | ]) 134 | } 135 | } 136 | 137 | // overridden method: we intercept the reading of the package.json file here so that we can 138 | // process it into both the package.json file rules as well as the strictRules synthetic rule set 139 | addIgnoreFile (file, callback) { 140 | // if we're adding anything other than package.json, then let ignore-walk handle it 141 | if (file !== 'package.json' || !this.isPackage) { 142 | return super.addIgnoreFile(file, callback) 143 | } 144 | 145 | return this.processPackage(callback) 146 | } 147 | 148 | // overridden method: if we're done, but we're a package, then we also need to evaluate bundles 149 | // before we actually emit our done event 150 | emit (ev, data) { 151 | if (ev !== 'done' || !this.isPackage) { 152 | return super.emit(ev, data) 153 | } 154 | 155 | // we intentionally delay the done event while keeping the function sync here 156 | // eslint-disable-next-line promise/catch-or-return, promise/always-return 157 | this.gatherBundles().then(() => { 158 | super.emit('done', this.result) 159 | }) 160 | return true 161 | } 162 | 163 | // overridden method: before actually filtering, we make sure that we've removed the rules for 164 | // files that should no longer take effect due to our order of precedence 165 | filterEntries () { 166 | if (this.ignoreRules['package.json']) { 167 | // package.json means no .npmignore or .gitignore 168 | this.ignoreRules['.npmignore'] = null 169 | this.ignoreRules['.gitignore'] = null 170 | } else if (this.ignoreRules['.npmignore']) { 171 | // .npmignore means no .gitignore 172 | this.ignoreRules['.gitignore'] = null 173 | } else if (this.ignoreRules['.gitignore'] && !this.ignoreRules['.npmignore']) { 174 | log.warn( 175 | 'gitignore-fallback', 176 | 'No .npmignore file found, using .gitignore for file exclusion. Consider creating a .npmignore file to explicitly control published files.' 177 | ) 178 | } 179 | 180 | return super.filterEntries() 181 | } 182 | 183 | // overridden method: we never want to include anything that isn't a file or directory 184 | onstat (opts, callback) { 185 | if (!opts.st.isFile() && !opts.st.isDirectory()) { 186 | return callback() 187 | } 188 | 189 | return super.onstat(opts, callback) 190 | } 191 | 192 | // overridden method: we want to refuse to pack files that are invalid, node-tar protects us from 193 | // a lot of them but not all 194 | stat (opts, callback) { 195 | if (nameIsBadForWindows(opts.entry)) { 196 | return callback() 197 | } 198 | 199 | return super.stat(opts, callback) 200 | } 201 | 202 | // overridden method: this is called to create options for a child walker when we step 203 | // in to a normal child directory (this will never be a bundle). the default method here 204 | // copies the root's `ignoreFiles` value, but we don't want to respect package.json for 205 | // subdirectories, so we override it with a list that intentionally omits package.json 206 | walkerOpt (entry, opts) { 207 | let ignoreFiles = null 208 | 209 | // however, if we have a tree, and we have workspaces, and the directory we're about 210 | // to step into is a workspace, then we _do_ want to respect its package.json 211 | if (this.tree.workspaces) { 212 | const workspaceDirs = [...this.tree.workspaces.values()] 213 | .map((dir) => dir.replace(/\\/g, '/')) 214 | 215 | const entryPath = join(this.path, entry).replace(/\\/g, '/') 216 | if (workspaceDirs.includes(entryPath)) { 217 | ignoreFiles = [ 218 | defaultRules, 219 | 'package.json', 220 | '.npmignore', 221 | '.gitignore', 222 | strictRules, 223 | ] 224 | } 225 | } else { 226 | ignoreFiles = [ 227 | defaultRules, 228 | '.npmignore', 229 | '.gitignore', 230 | strictRules, 231 | ] 232 | } 233 | 234 | return { 235 | ...super.walkerOpt(entry, opts), 236 | ignoreFiles, 237 | // we map over our own requiredFiles and pass ones that are within this entry 238 | requiredFiles: this.requiredFiles 239 | .map((file) => { 240 | if (relative(file, entry) === '..') { 241 | return relative(entry, file).replace(/\\/g, '/') 242 | } 243 | return false 244 | }) 245 | .filter(Boolean), 246 | } 247 | } 248 | 249 | // overridden method: we want child walkers to be instances of this class, not ignore-walk 250 | walker (entry, opts, callback) { 251 | new PackWalker(this.tree, this.walkerOpt(entry, opts)).on('done', callback).start() 252 | } 253 | 254 | // overridden method: we use a custom sort method to help compressibility 255 | sort (a, b) { 256 | // optimize for compressibility 257 | // extname, then basename, then locale alphabetically 258 | // https://twitter.com/isntitvacant/status/1131094910923231232 259 | const exta = extname(a).toLowerCase() 260 | const extb = extname(b).toLowerCase() 261 | const basea = basename(a).toLowerCase() 262 | const baseb = basename(b).toLowerCase() 263 | 264 | return exta.localeCompare(extb, 'en') || 265 | basea.localeCompare(baseb, 'en') || 266 | a.localeCompare(b, 'en') 267 | } 268 | 269 | // convenience method: this joins the given rules with newlines, appends a trailing newline, 270 | // and calls the internal onReadIgnoreFile method 271 | injectRules (filename, rules, callback = () => {}) { 272 | this.onReadIgnoreFile(filename, `${rules.join('\n')}\n`, callback) 273 | } 274 | 275 | // custom method: this is called by addIgnoreFile when we find a package.json, it uses the 276 | // arborist tree to pull both default rules and strict rules for the package 277 | processPackage (callback) { 278 | const { 279 | bin, 280 | browser, 281 | files, 282 | main, 283 | } = this.tree.package 284 | 285 | // rules in these arrays are inverted since they are patterns we want to _not_ ignore 286 | const ignores = [] 287 | const strict = [ 288 | ...strictDefaults, 289 | '!/package.json', 290 | '!/readme{,.*[^~$]}', 291 | '!/copying{,.*[^~$]}', 292 | '!/license{,.*[^~$]}', 293 | '!/licence{,.*[^~$]}', 294 | '/.git', 295 | '/node_modules', 296 | '.npmrc', 297 | '/package-lock.json', 298 | '/yarn.lock', 299 | '/pnpm-lock.yaml', 300 | '/bun.lockb', 301 | ] 302 | 303 | // if we have a files array in our package, we need to pull rules from it 304 | if (files) { 305 | for (let file of files) { 306 | // invert the rule because these are things we want to include 307 | if (file.startsWith('./')) { 308 | file = file.slice(1) 309 | } 310 | if (file.endsWith('/*')) { 311 | file += '*' 312 | } 313 | const inverse = `!${file}` 314 | try { 315 | // if an entry in the files array is a specific file, then we need to include it as a 316 | // strict requirement for this package. if it's a directory or a pattern, it's a default 317 | // pattern instead. this is ugly, but we have to stat to find out if it's a file 318 | const stat = lstat(join(this.path, file.replace(/^!+/, '')).replace(/\\/g, '/')) 319 | // if we have a file and we know that, it's strictly required 320 | if (stat.isFile()) { 321 | strict.unshift(inverse) 322 | this.requiredFiles.push(file.startsWith('/') ? file.slice(1) : file) 323 | } else if (stat.isDirectory()) { 324 | // otherwise, it's a default ignore, and since we got here we know it's not a pattern 325 | // so we include the directory contents 326 | ignores.push(inverse) 327 | ignores.push(`${inverse}/**`) 328 | } 329 | // if the thing exists, but is neither a file or a directory, we don't want it at all 330 | } catch (err) { 331 | // if lstat throws, then we assume we're looking at a pattern and treat it as a default 332 | ignores.push(inverse) 333 | } 334 | } 335 | 336 | // we prepend a '*' to exclude everything, followed by our inverted file rules 337 | // which now mean to include those 338 | this.injectRules('package.json', ['*', ...ignores]) 339 | } 340 | 341 | // browser is required 342 | if (browser) { 343 | strict.push(`!/${browser}`) 344 | } 345 | 346 | // main is required 347 | if (main) { 348 | strict.push(`!/${main}`) 349 | } 350 | 351 | // each bin is required 352 | if (bin) { 353 | for (const key in bin) { 354 | strict.push(`!/${bin[key]}`) 355 | } 356 | } 357 | 358 | // and now we add all of the strict rules to our synthetic file 359 | this.injectRules(strictRules, strict, callback) 360 | } 361 | 362 | // custom method: after we've finished gathering the files for the root package, we call this 363 | // before emitting the 'done' event in order to gather all of the files for bundled deps 364 | async gatherBundles () { 365 | if (this.seen.has(this.tree)) { 366 | return 367 | } 368 | 369 | // add this node to our seen tracker 370 | this.seen.add(this.tree) 371 | 372 | // if we're the project root, then we look at our bundleDependencies, otherwise we got here 373 | // because we're a bundled dependency of the root, which means we need to include all prod 374 | // and optional dependencies in the bundle 375 | let toBundle 376 | if (this.tree.isProjectRoot) { 377 | const { bundleDependencies } = this.tree.package 378 | toBundle = bundleDependencies || [] 379 | } else { 380 | const { dependencies, optionalDependencies } = this.tree.package 381 | toBundle = Object.keys(dependencies || {}).concat(Object.keys(optionalDependencies || {})) 382 | } 383 | 384 | for (const dep of toBundle) { 385 | const edge = this.tree.edgesOut.get(dep) 386 | // no edgeOut = missing node, so skip it. we can't pack it if it's not here 387 | // we also refuse to pack peer dependencies and dev dependencies 388 | if (!edge || edge.peer || edge.dev) { 389 | continue 390 | } 391 | 392 | // get a reference to the node we're bundling 393 | const node = this.tree.edgesOut.get(dep).to 394 | // if there's no node, this is most likely an optional dependency that hasn't been 395 | // installed. just skip it. 396 | if (!node) { 397 | continue 398 | } 399 | // we use node.path for the path because we want the location the node was linked to, 400 | // not where it actually lives on disk 401 | const path = node.path 402 | // but link nodes don't have edgesOut, so we need to pass in the target of the node 403 | // in order to make sure we correctly traverse its dependencies 404 | const tree = node.target 405 | 406 | // and start building options to be passed to the walker for this package 407 | const walkerOpts = { 408 | path, 409 | isPackage: true, 410 | ignoreFiles: [], 411 | seen: this.seen, // pass through seen so we can prevent infinite circular loops 412 | } 413 | 414 | // if our node is a link, we apply defaultRules. we don't do this for regular bundled 415 | // deps because their .npmignore and .gitignore files are excluded by default and may 416 | // override defaults 417 | if (node.isLink) { 418 | walkerOpts.ignoreFiles.push(defaultRules) 419 | } 420 | 421 | // _all_ nodes will follow package.json rules from their package root 422 | walkerOpts.ignoreFiles.push('package.json') 423 | 424 | // only link nodes will obey .npmignore or .gitignore 425 | if (node.isLink) { 426 | walkerOpts.ignoreFiles.push('.npmignore') 427 | walkerOpts.ignoreFiles.push('.gitignore') 428 | } 429 | 430 | // _all_ nodes follow strict rules 431 | walkerOpts.ignoreFiles.push(strictRules) 432 | 433 | // create a walker for this dependency and gather its results 434 | const walker = new PackWalker(tree, walkerOpts) 435 | const bundled = await new Promise((pResolve, pReject) => { 436 | walker.on('error', pReject) 437 | walker.on('done', pResolve) 438 | walker.start() 439 | }) 440 | 441 | // now we make sure we have our paths correct from the root, and accumulate everything into 442 | // our own result set to deduplicate 443 | const relativeFrom = relative(this.root, walker.path) 444 | for (const file of bundled) { 445 | this.result.add(join(relativeFrom, file).replace(/\\/g, '/')) 446 | } 447 | } 448 | } 449 | } 450 | 451 | const walk = (tree, options, callback) => { 452 | if (typeof options === 'function') { 453 | callback = options 454 | options = {} 455 | } 456 | const p = new Promise((pResolve, pReject) => { 457 | new PackWalker(tree, { ...options, isPackage: true }) 458 | .on('done', pResolve).on('error', pReject).start() 459 | }) 460 | return callback ? p.then(res => callback(null, res), callback) : p 461 | } 462 | 463 | module.exports = walk 464 | walk.Walker = PackWalker 465 | --------------------------------------------------------------------------------