npm-packlist 10.0.4 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +45 -30
  2. package/lib/index.js +86 -47
  3. package/package.json +10 -8
package/README.md CHANGED
@@ -30,11 +30,9 @@ arborist.loadActual().then((tree) => {
30
30
  This uses the following rules:
31
31
 
32
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"`.
33
+ then ignore everything that isn't matched by `files`. Always include the
34
+ root readme, license, licence and copying files, if they exist, as well
35
+ as the package.json file itself.
38
36
  2. If there's no `package.json` file (or it has no `files` list), and
39
37
  there is a `.npmignore` file, then ignore all the files in the
40
38
  `.npmignore` file.
@@ -45,7 +43,7 @@ This uses the following rules:
45
43
  bundled dependency. If it IS a bundled dependency, and it's a
46
44
  symbolic link, then the target of the link is included, not the
47
45
  symlink itself.
48
- 4. Unless they're explicitly included (by being in a `files` list, or
46
+ 5. Unless they're explicitly included (by being in a `files` list, or
49
47
  a `!negated` rule in a relevant `.npmignore` or `.gitignore`),
50
48
  always ignore certain common cruft files:
51
49
 
@@ -58,48 +56,65 @@ This uses the following rules:
58
56
  6. Darwin's `.DS_Store` files because wtf are those even
59
57
  7. `npm-debug.log` files at the root of a project
60
58
 
61
- You can explicitly re-include any of these with a `files` list in
62
- `package.json` or a negated ignore file rule.
59
+ You can explicitly re-include most of these with a negated ignore file rule.
60
+ A small set of files are always ignored regardless of `files` rules:
61
+ `.git`, `.npmrc`, and `node_modules`.
63
62
 
64
63
  Only the `package.json` file in the very root of the project is ever
65
64
  inspected for a `files` list. Below the top level of the root package,
66
65
  `package.json` is treated as just another file, and no package-specific
67
66
  semantics are applied.
68
67
 
69
- ### Interaction between `package.json` and `.npmignore` rules
68
+ ### Interpretation of the `files` list
70
69
 
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.
70
+ Each entry in the `package.json` `files` list is interpreted as a glob
71
+ pattern, expanded against the package root using node's built-in
72
+ [`fs.globSync`](https://nodejs.org/api/fs.html#fsglobsyncpattern-options).
73
+ Literal paths (`lib`, `src/index.js`) are valid globs and resolve to
74
+ themselves; pattern entries (`dist-*`, `**/*.js`) resolve to every
75
+ matching entry on disk; entries that match nothing are silently dropped.
74
76
 
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
+ When an entry matches a directory, the directory and all of its contents
78
+ are included. This is a documented convenience: writing
79
+ `"files": ["dist"]` does not require also writing `"dist/**"`.
77
80
 
78
- If a _directory_ is listed in `files`, then any rules in nested `.npmignore` files within that directory will be honored.
81
+ A leading `!` makes the entry a negation, removing matches from the
82
+ result. Negations are evaluated in order, so a later positive entry
83
+ can re-include something a previous negation excluded.
79
84
 
80
- For example, with this package.json:
85
+ #### Pattern semantics are anchored to the package root
81
86
 
82
- ```json
83
- {
84
- "files": [ "dir" ]
85
- }
86
- ```
87
+ Glob patterns are evaluated as paths relative to the package root, not as
88
+ basename-anywhere matchers. In particular:
87
89
 
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.
90
+ * `"*.js"` matches `.js` files at the package root, not at every depth.
91
+ Use `"**/*.js"` to match at every depth.
92
+ * `"!readme.md"` removes only the root `readme.md`. Use
93
+ `"!**/readme.md"` to remove `readme.md` at every depth.
91
94
 
92
- Additionally, with this package.json:
95
+ Negations only exclude paths that exist on disk at packlist time: a `!path`
96
+ entry that matches nothing is silently dropped, same as a positive entry
97
+ that matches nothing. A typo'd negation has no effect.
93
98
 
94
- ```
99
+ #### Interaction with `.npmignore`
100
+
101
+ If a `package.json` `files` array is present, any **top-level**
102
+ `.npmignore` and `.gitignore` files are ignored: the `files` array is
103
+ the sole source of inclusion at the package root.
104
+
105
+ `.npmignore` files in subdirectories are still honored within their
106
+ subtree. For example, with this package.json:
107
+
108
+ ```json
95
109
  {
96
- "files": ["dir/subdir"]
110
+ "files": [ "dir" ]
97
111
  }
98
112
  ```
99
113
 
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.
114
+ a `.npmignore` at `dir/.npmignore` (and any nested subdirectory) will
115
+ be applied. This means a file listed both in `files` and excluded by a
116
+ nested `.npmignore` will be excluded from the tarball; if you need such
117
+ a file to ship, remove the `.npmignore` rule.
103
118
 
104
119
  ## API
105
120
 
package/lib/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  'use strict'
2
2
 
3
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')
4
+ const { globSync } = require('glob')
5
+ const { readFileSync: readFile } = require('node:fs')
6
+ const { basename, dirname, extname, join, relative, resolve, sep } = require('node:path')
6
7
  const { log } = require('proc-log')
7
8
 
8
9
  // symbols used to represent synthetic rule sets
9
10
  const defaultRules = Symbol('npm-packlist.rules.default')
11
+ const globalRules = Symbol('npm-packlist.rules.global')
10
12
  const strictRules = Symbol('npm-packlist.rules.strict')
11
13
 
12
14
  // There may be others, but :?|<> are handled by node-tar
@@ -83,13 +85,23 @@ class PackWalker extends IgnoreWalker {
83
85
  follow: false,
84
86
  // we path.resolve() here because ignore-walk doesn't do it and we want full paths
85
87
  path: resolve(opts?.path || tree.path).replace(/\\/g, '/'),
86
- ignoreFiles: opts?.ignoreFiles || [
88
+ // globalRules belongs on the root walker only. Child walkers reach it through
89
+ // parent.filterEntry, where package.json rules can still override it; reapplying
90
+ // globalRules at the child level would defeat that override.
91
+ ignoreFiles: opts?.ignoreFiles || (opts?.parent ? [
87
92
  defaultRules,
88
93
  'package.json',
89
94
  '.npmignore',
90
95
  '.gitignore',
91
96
  strictRules,
92
- ],
97
+ ] : [
98
+ defaultRules,
99
+ globalRules,
100
+ 'package.json',
101
+ '.npmignore',
102
+ '.gitignore',
103
+ strictRules,
104
+ ]),
93
105
  }
94
106
 
95
107
  super(options)
@@ -97,7 +109,8 @@ class PackWalker extends IgnoreWalker {
97
109
  this.isPackage = options.isPackage
98
110
  this.seen = options.seen || new Set()
99
111
  this.tree = tree
100
- this.requiredFiles = options.requiredFiles || []
112
+ this.globalIgnoreFile = options.globalIgnoreFile || null
113
+ this.globalIgnoreRules = options.globalIgnoreRules || null
101
114
 
102
115
  const additionalDefaults = []
103
116
  if (options.prefix && options.workspaces) {
@@ -124,13 +137,34 @@ class PackWalker extends IgnoreWalker {
124
137
  // go ahead and inject the default rules now
125
138
  this.injectRules(defaultRules, [...defaults, ...additionalDefaults])
126
139
 
140
+ // inject the global rules from the user's ignore file. positioned between
141
+ // defaultRules and package.json so an explicit entry in the package "files"
142
+ // field can still override. only the root walker carries globalRules in its
143
+ // ignoreFiles list; child walkers see global exclusions via parent.filterEntry,
144
+ // which is what lets the package.json `!/path/**` override win.
145
+ if (this.globalIgnoreRules === null && this.globalIgnoreFile) {
146
+ try {
147
+ this.globalIgnoreRules = readFile(this.globalIgnoreFile, { encoding: 'utf8' })
148
+ } catch (err) {
149
+ // istanbul ignore next -- only ENOENT is expected here
150
+ if (err.code !== 'ENOENT') {
151
+ throw err
152
+ }
153
+ this.globalIgnoreRules = ''
154
+ }
155
+ }
156
+ if (this.globalIgnoreRules) {
157
+ this.injectRules(globalRules, [this.globalIgnoreRules])
158
+ // istanbul ignore else -- globalIgnoreRules without a filename only happens when
159
+ // a caller pre-populates rules directly instead of going through globalIgnoreFile
160
+ if (this.globalIgnoreFile) {
161
+ log.silly('global-ignore-file', `applied rules from ${this.globalIgnoreFile}`)
162
+ }
163
+ }
164
+
127
165
  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
- ])
166
+ // if this instance is not a package, then place some strict default rules
167
+ this.injectRules(strictRules, [...strictDefaults])
134
168
  }
135
169
  }
136
170
 
@@ -234,15 +268,6 @@ class PackWalker extends IgnoreWalker {
234
268
  return {
235
269
  ...super.walkerOpt(entry, opts),
236
270
  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
271
  }
247
272
  }
248
273
 
@@ -295,46 +320,60 @@ class PackWalker extends IgnoreWalker {
295
320
  '/node_modules',
296
321
  '.npmrc',
297
322
  '/package-lock.json',
323
+ '/npm-shrinkwrap.json',
298
324
  '/yarn.lock',
299
325
  '/pnpm-lock.yaml',
300
326
  '/bun.lockb',
327
+ '/bun.lock',
301
328
  ]
302
329
 
303
330
  // if we have a files array in our package, we need to pull rules from it
304
331
  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)
332
+ for (const entry of files) {
333
+ const isNegation = entry.startsWith('!')
334
+ // normalize: strip leading `!`s, `./`, a single leading `/`, and trailing slashes.
335
+ // trailing slashes confuse glob; the leading forms are equivalent to their stripped versions for glob expansion.
336
+ const pattern = (isNegation ? entry.replace(/^!+/, '') : entry)
337
+ .replace(/^\.?\//, '')
338
+ .replace(/\/+$/, '')
339
+ if (!pattern) {
340
+ continue
309
341
  }
310
- if (file.endsWith('/*')) {
311
- file += '*'
312
- }
313
- const inverse = `!${file}`
342
+
343
+ // expand the entry as a glob against the package root.
344
+ // literal paths (e.g. `lib`, `lib/foo.js`) are valid globs and resolve to themselves;
345
+ // patterns (e.g. `dist-*`, `**/*.js`) resolve to all matching entries;
346
+ // typos and non-existent entries resolve to nothing.
347
+ let matches = []
314
348
  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}/**`)
349
+ matches = globSync(pattern, { cwd: this.path, withFileTypes: true, dot: true })
350
+ } catch {
351
+ // unparseable globs produce no matches and drop silently.
352
+ // (Pre-v11 instead fell through to push the raw entry as an ignore-walk pattern rule; we deliberately don't preserve that.)
353
+ }
354
+
355
+ // a positive entry produces `!path` rules (un-ignore from the surrounding `*` deny-all); a negation produces `path` rules (re-ignore them).
356
+ // directory matches expand to also include their contents; this is documented `files[]` behavior, not a glob hack.
357
+ const prefix = isNegation ? '' : '!'
358
+ for (const match of matches) {
359
+ const rel = match.relativePosix()
360
+ // istanbul ignore next -- defensive: glob with withFileTypes returns the cwd as an entry with an empty relative path; skip it.
361
+ if (!rel) {
362
+ continue
363
+ }
364
+ if (match.isDirectory()) {
365
+ ignores.push(`${prefix}/${rel}`)
366
+ ignores.push(`${prefix}/${rel}/**`)
367
+ continue
368
+ }
369
+ // istanbul ignore else -- non-file-non-dir matches (sockets, fifos, broken symlinks) drop silently.
370
+ if (match.isFile()) {
371
+ ignores.push(`${prefix}/${rel}`)
328
372
  }
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
373
  }
334
374
  }
335
375
 
336
- // we prepend a '*' to exclude everything, followed by our inverted file rules
337
- // which now mean to include those
376
+ // we prepend a '*' to exclude everything, followed by our inverted file rules which now mean to include those
338
377
  this.injectRules('package.json', ['*', ...ignores])
339
378
  }
340
379
 
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "npm-packlist",
3
- "version": "10.0.4",
3
+ "version": "11.1.0",
4
4
  "description": "Get a list of the files to add from a folder into an npm package",
5
5
  "directories": {
6
6
  "test": "test"
7
7
  },
8
8
  "main": "lib/index.js",
9
9
  "dependencies": {
10
- "ignore-walk": "^8.0.0",
11
- "proc-log": "^6.0.0"
10
+ "glob": "^13.0.6",
11
+ "ignore-walk": "^9.0.0",
12
+ "proc-log": "^7.0.0"
12
13
  },
13
14
  "author": "GitHub Inc.",
14
15
  "license": "ISC",
@@ -18,8 +19,8 @@
18
19
  ],
19
20
  "devDependencies": {
20
21
  "@npmcli/arborist": "^9.0.0",
21
- "@npmcli/eslint-config": "^6.0.0",
22
- "@npmcli/template-oss": "4.29.0",
22
+ "@npmcli/eslint-config": "^7.0.0",
23
+ "@npmcli/template-oss": "5.1.0",
23
24
  "mutate-fs": "^2.1.1",
24
25
  "tap": "^16.0.1"
25
26
  },
@@ -52,11 +53,12 @@
52
53
  ]
53
54
  },
54
55
  "engines": {
55
- "node": "^20.17.0 || >=22.9.0"
56
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
56
57
  },
57
58
  "templateOSS": {
58
59
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
59
- "version": "4.29.0",
60
- "publish": true
60
+ "version": "5.1.0",
61
+ "publish": true,
62
+ "updateNpm": false
61
63
  }
62
64
  }