npm-packlist 5.1.2 → 6.0.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 +13 -56
  2. package/lib/index.js +359 -412
  3. package/package.json +11 -12
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # npm-packlist
2
2
 
3
- [![Build Status](https://travis-ci.com/npm/npm-packlist.svg?token=hHeDp9pQmz9kvsgRNVHy&branch=master)](https://travis-ci.com/npm/npm-packlist)
4
-
5
3
  Get a list of the files to add from a folder into an npm package.
6
4
 
7
5
  These can be handed to [tar](http://npm.im/tar) like so to make an npm
@@ -29,8 +27,8 @@ This uses the following rules:
29
27
 
30
28
  1. If a `package.json` file is found, and it has a `files` list,
31
29
  then ignore everything that isn't in `files`. Always include the
32
- readme, license, notice, changes, changelog, and history files, if
33
- they exist, and the package.json file itself.
30
+ readme, license, licence and copying files, if they exist, as well
31
+ as the package.json file itself.
34
32
  2. If there's no `package.json` file (or it has no `files` list), and
35
33
  there is a `.npmignore` file, then ignore all the files in the
36
34
  `.npmignore` file.
@@ -64,17 +62,14 @@ semantics are applied.
64
62
 
65
63
  ### Interaction between `package.json` and `.npmignore` rules
66
64
 
67
- For simplicity, it is best to use _either_ a `files` list in `package.json`
68
- _or_ a `.npmignore` file, and not both. If you only use one of these
69
- methods, you can skip this documentation section.
65
+ In previous versions of this library, the `files` list in `package.json`
66
+ was used as an initial filter to drive further tree walking. That is no
67
+ longer the case as of version 6.0.0.
70
68
 
71
- The `files` list in `package.json` is used to direct the exploration of the
72
- tree. In other words, that's all the walker will ever look at when
73
- exploring that level.
69
+ If you have a `package.json` file with a `files` array within, any top
70
+ level `.npmignore` and `.gitignore` files *will be ignored*.
74
71
 
75
- In some cases this can lead to a `.npmignore` file being ignored. If a
76
- _directory_ is listed in `files`, then any rules in a root or nested
77
- `.npmignore` files will be honored.
72
+ If a _directory_ is listed in `files`, then any rules in nested `.npmignore` files within that directory will be honored.
78
73
 
79
74
  For example, with this package.json:
80
75
 
@@ -88,7 +83,7 @@ a `.npmignore` file at `dir/.npmignore` (and any subsequent
88
83
  sub-directories) will be honored. However, a `.npmignore` at the root
89
84
  level will be skipped.
90
85
 
91
- Conversely, with this package.json:
86
+ Additionally, with this package.json:
92
87
 
93
88
  ```
94
89
  {
@@ -96,51 +91,13 @@ Conversely, with this package.json:
96
91
  }
97
92
  ```
98
93
 
99
- a `.npmignore` file at `dir/.npmignore` will not be honored.
100
-
101
- Any specific file matched by a glob or filename in the package.json `files`
102
- list will be included, and cannot be excluded by any `.npmignore` files in
103
- nested directories, or by a `.npmignore` file in the root package
104
- directory, unless that root `.npmignore` file is also in the `files` list.
105
-
106
- The previous (v1) implementation used in npm 6 and below treated
107
- `package.json` as a special sort of "reverse ignore" file. That is, it was
108
- parsed and handled as if it was a `.npmignore` file with `!` prepended to
109
- all of the globs in the `files` list. In order to include children of a
110
- directory listed in `files`, they would _also_ have `/**` appended to them.
111
-
112
- This is tricky to explain, but is a significant improvement over the
113
- previous (v1) implementation used in npm 6 and below, with the following
114
- beneficial properties:
115
-
116
- - If you have `{"files":["lib"]}` in `package.json`, then the walker will
117
- still ignore files such as `lib/.DS_Store` and `lib/.foo.swp`. The
118
- previous implementation would include these files, as they'd be matched
119
- by the computed `!lib/**` ignore rule.
120
- - If you have `{"files":["lib/a.js","lib/b.js"]}` in `package.json`, and a
121
- `lib/.npmignore` containing `a.js`, then the walker will still include
122
- the two files indicated in `package.json`, and ignore the
123
- `lib/.npmignore` file. The previous implementation would mark these
124
- files for inclusion, but then _exclude_ them when it came to the nested
125
- `.npmignore` file. (Ignore file semantics dictate that a "closer" ignore
126
- file always takes precedence.)
127
- - A file in `lib/pkg-template/package.json` will be included, and its
128
- `files` list will not have any bearing on other files being included or
129
- skipped. When treating `package.json` as just Yet Another ignore file,
130
- this was not the case, leading to difficulty for modules that aim to
131
- initialize a project.
132
-
133
- In general, this walk should work as a reasonable developer would expect.
134
- Matching human expectation is tricky business, and if you find cases where
135
- it violates those expectations, [please let us
136
- know](https://github.com/npm/npm-packlist/issues).
94
+ a `.npmignore` file at `dir/.npmignore` will be honored, as well as `dir/subdir/.npmignore`.
95
+
96
+ 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.
137
97
 
138
98
  ## API
139
99
 
140
100
  Same API as [ignore-walk](http://npm.im/ignore-walk), just hard-coded
141
101
  file list and rule sets.
142
102
 
143
- The `Walker` and `WalkerSync` classes take a `bundled` argument, which
144
- is a list of package names to include from node_modules. When calling
145
- the top-level `packlist()` and `packlist.sync()` functions, this
146
- module calls into `npm-bundled` directly.
103
+ The `Walker` class will load 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.
package/lib/index.js CHANGED
@@ -1,87 +1,19 @@
1
1
  'use strict'
2
2
 
3
- // Do a two-pass walk, first to get the list of packages that need to be
4
- // bundled, then again to get the actual files and folders.
5
- // Keep a cache of node_modules content and package.json data, so that the
6
- // second walk doesn't have to re-do all the same work.
3
+ const Arborist = require('@npmcli/arborist')
4
+ const { Walker: IgnoreWalker } = require('ignore-walk')
5
+ const { lstatSync: lstat, readFileSync: readFile } = require('fs')
6
+ const { basename, dirname, extname, join, relative, resolve, sep } = require('path')
7
7
 
8
- const bundleWalk = require('npm-bundled')
9
- const BundleWalker = bundleWalk.BundleWalker
8
+ // symbols used to represent synthetic rule sets
9
+ const defaultRules = Symbol('npm-packlist.rules.default')
10
+ const strictRules = Symbol('npm-packlist.rules.strict')
10
11
 
11
- const ignoreWalk = require('ignore-walk')
12
- const IgnoreWalker = ignoreWalk.Walker
13
-
14
- const rootBuiltinRules = Symbol('root-builtin-rules')
15
- const packageNecessaryRules = Symbol('package-necessary-rules')
16
- const path = require('path')
17
-
18
- const normalizePackageBin = require('npm-normalize-package-bin')
19
-
20
- // Weird side-effect of this: a readme (etc) file will be included
21
- // if it exists anywhere within a folder with a package.json file.
22
- // The original intent was only to include these files in the root,
23
- // but now users in the wild are dependent on that behavior for
24
- // localized documentation and other use cases. Adding a `/` to
25
- // these rules, while tempting and arguably more "correct", is a
26
- // significant change that will break existing use cases.
27
- const packageMustHaveFileNames = 'readme|copying|license|licence'
28
-
29
- const packageMustHaves = `@(${packageMustHaveFileNames}){,.*[^~$]}`
30
- const packageMustHavesRE = new RegExp(`^(${packageMustHaveFileNames})(\\..*[^~$])?$`, 'i')
31
-
32
- const fs = require('fs')
33
- const glob = require('glob')
34
- const globify = pattern => pattern.split('\\').join('/')
35
-
36
- const readOutOfTreeIgnoreFiles = (root, rel, result = '') => {
37
- for (const file of ['.npmignore', '.gitignore']) {
38
- try {
39
- const ignoreContent = fs.readFileSync(path.join(root, file), { encoding: 'utf8' })
40
- result += ignoreContent + '\n'
41
- // break the loop immediately after concatting, this allows us to prioritize the
42
- // .npmignore and discard the .gitignore if one exists
43
- break
44
- } catch (err) {
45
- // we ignore ENOENT errors completely because we don't care if the file doesn't exist
46
- // but we throw everything else because failing to read a file that does exist is
47
- // something that the user likely wants to know about. we don't need to test this.
48
- /* istanbul ignore next */
49
- if (err.code !== 'ENOENT') {
50
- throw err
51
- }
52
- }
53
- }
54
-
55
- if (!rel) {
56
- return result
57
- }
58
-
59
- const firstRel = rel.split(path.sep)[0]
60
- const newRoot = path.join(root, firstRel)
61
- const newRel = path.relative(newRoot, path.join(root, rel))
62
-
63
- return readOutOfTreeIgnoreFiles(newRoot, newRel, result)
64
- }
65
-
66
- const pathHasPkg = (input) => {
67
- if (!input.startsWith('node_modules/')) {
68
- return false
69
- }
70
-
71
- const segments = input.slice('node_modules/'.length).split('/', 2)
72
- return segments[0].startsWith('@')
73
- ? segments.length === 2
74
- : true
75
- }
76
-
77
- const pkgFromPath = (input) => {
78
- const segments = input.slice('node_modules/'.length).split('/', 2)
79
- return segments[0].startsWith('@')
80
- ? segments.join('/')
81
- : segments[0]
82
- }
12
+ // There may be others, but :?|<> are handled by node-tar
13
+ const nameIsBadForWindows = file => /\*/.test(file)
83
14
 
84
- const defaultRules = [
15
+ // these are the default rules that are applied to everything except for non-link bundled deps
16
+ const defaults = [
85
17
  '.npmignore',
86
18
  '.gitignore',
87
19
  '**/.git',
@@ -103,410 +35,425 @@ const defaultRules = [
103
35
  '._*',
104
36
  '**/._*/**',
105
37
  '*.orig',
106
- '/package-lock.json',
107
- '/yarn.lock',
108
- '/pnpm-lock.yaml',
109
38
  '/archived-packages/**',
110
39
  ]
111
40
 
112
- // There may be others, but :?|<> are handled by node-tar
113
- const nameIsBadForWindows = file => /\*/.test(file)
41
+ const strictDefaults = [
42
+ // these are forcibly included at all levels
43
+ '!/readme{,.*[^~$]}',
44
+ '!/copying{,.*[^~$]}',
45
+ '!/license{,.*[^~$]}',
46
+ '!/licence{,.*[^~$]}',
47
+ // these are forcibly excluded
48
+ '/.git',
49
+ ]
114
50
 
115
- class Walker extends IgnoreWalker {
116
- constructor (opt) {
117
- opt = opt || {}
51
+ const normalizePath = (path) => path.split('\\').join('/')
118
52
 
119
- // the order in which rules are applied.
120
- opt.ignoreFiles = [
121
- rootBuiltinRules,
122
- 'package.json',
123
- '.npmignore',
124
- '.gitignore',
125
- packageNecessaryRules,
126
- ]
127
-
128
- opt.includeEmpty = false
129
- opt.path = opt.path || process.cwd()
130
-
131
- // only follow links in the root node_modules folder, because if those
132
- // folders are included, it's because they're bundled, and bundles
133
- // should include the contents, not the symlinks themselves.
134
- // This regexp tests to see that we're either a node_modules folder,
135
- // or a @scope within a node_modules folder, in the root's node_modules
136
- // hierarchy (ie, not in test/foo/node_modules/ or something).
137
- const followRe = /^(?:\/node_modules\/(?:@[^/]+\/[^/]+|[^/]+)\/)*\/node_modules(?:\/@[^/]+)?$/
138
- const rootPath = opt.parent ? opt.parent.root : opt.path
139
- const followTestPath = opt.path.replace(/\\/g, '/').slice(rootPath.length)
140
- opt.follow = followRe.test(followTestPath)
141
-
142
- super(opt)
143
-
144
- // ignore a bunch of things by default at the root level.
145
- // also ignore anything in the main project node_modules hierarchy,
146
- // except bundled dependencies
147
- if (this.isProject) {
148
- this.bundled = opt.bundled || []
149
- this.bundledScopes = Array.from(new Set(
150
- this.bundled.filter(f => /^@/.test(f))
151
- .map(f => f.split('/')[0])))
152
- this.packageJsonCache = this.parent ? this.parent.packageJsonCache
153
- : (opt.packageJsonCache || new Map())
154
- let rules = defaultRules.join('\n') + '\n'
155
-
156
- if (opt.prefix && opt.workspaces) {
157
- const gPath = globify(opt.path)
158
- const gPrefix = globify(opt.prefix)
159
- const gWorkspaces = opt.workspaces.map((ws) => globify(ws))
160
- // if opt.path and opt.prefix are not the same directory, and opt.workspaces has opt.path
161
- // in it, then we know that opt.path is a workspace directory. in order to not drop ignore
162
- // rules from directories between the workspace root (opt.prefix) and the workspace itself
163
- // (opt.path), we need to find and read those now
164
- /* istanbul ignore else */
165
- if (gPath !== gPrefix && gWorkspaces.includes(gPath)) {
166
- // relpath is the relative path between the prefix and the parent of opt.path
167
- // we use the parent because ignore-walk will read the files in opt.path already
168
- const relpath = path.relative(opt.prefix, path.dirname(opt.path))
169
- rules += readOutOfTreeIgnoreFiles(opt.prefix, relpath)
170
- } else if (gPath === gPrefix) {
171
- // on the other hand, if the path and the prefix are the same, then we ignore workspaces
172
- // so that we don't pack workspaces inside of a root project
173
- rules += opt.workspaces.map((ws) => globify(path.relative(opt.path, ws))).join('\n')
174
- }
53
+ const readOutOfTreeIgnoreFiles = (root, rel, result = []) => {
54
+ for (const file of ['.npmignore', '.gitignore']) {
55
+ try {
56
+ const ignoreContent = readFile(join(root, file), { encoding: 'utf8' })
57
+ result.push(ignoreContent)
58
+ // break the loop immediately after reading, this allows us to prioritize
59
+ // the .npmignore and discard the .gitignore if one is present
60
+ break
61
+ } catch (err) {
62
+ // we ignore ENOENT errors completely because we don't care if the file doesn't exist
63
+ // but we throw everything else because failing to read a file that does exist is
64
+ // something that the user likely wants to know about
65
+ // istanbul ignore next -- we do not need to test a thrown error
66
+ if (err.code !== 'ENOENT') {
67
+ throw err
175
68
  }
176
-
177
- super.onReadIgnoreFile(rootBuiltinRules, rules, _ => _)
178
- } else {
179
- this.bundled = []
180
- this.bundledScopes = []
181
- this.packageJsonCache = this.parent.packageJsonCache
182
69
  }
183
70
  }
184
71
 
185
- get isProject () {
186
- return !this.parent || this.parent.follow && this.isSymbolicLink
72
+ if (!rel) {
73
+ return result
187
74
  }
188
75
 
189
- onReaddir (entries) {
190
- if (this.isProject) {
191
- entries = entries.filter(e =>
192
- e !== '.git' &&
193
- !(e === 'node_modules' && this.bundled.length === 0)
194
- )
76
+ const firstRel = rel.split(sep, 1)[0]
77
+ const newRoot = join(root, firstRel)
78
+ const newRel = relative(newRoot, join(root, rel))
79
+
80
+ return readOutOfTreeIgnoreFiles(newRoot, newRel, result)
81
+ }
82
+
83
+ class PackWalker extends IgnoreWalker {
84
+ constructor (opts) {
85
+ const options = {
86
+ ...opts,
87
+ includeEmpty: false,
88
+ follow: false,
195
89
  }
196
90
 
197
- // if we have a package.json, then look in it for 'files'
198
- // we _only_ do this in the root project, not bundled deps
199
- // or other random folders. Bundled deps are always assumed
200
- // to be in the state the user wants to include them, and
201
- // a package.json somewhere else might be a template or
202
- // test or something else entirely.
203
- if (!this.isProject || !entries.includes('package.json')) {
204
- return super.onReaddir(entries)
91
+ // we path.resolve() here because ignore-walk doesn't do it and we want full paths
92
+ options.path = resolve(options.path || process.cwd()).replace(/\\/g, '/')
93
+
94
+ if (!options.ignoreFiles) {
95
+ options.ignoreFiles = [
96
+ defaultRules,
97
+ 'package.json',
98
+ '.npmignore',
99
+ '.gitignore',
100
+ strictRules,
101
+ ]
205
102
  }
206
103
 
207
- // when the cache has been seeded with the root manifest,
208
- // we must respect that (it may differ from the filesystem)
209
- const ig = path.resolve(this.path, 'package.json')
104
+ super(options)
105
+ this.isPackage = options.isPackage
106
+ this.seen = options.seen || new Set()
107
+ this.tree = options.tree // no default, we'll load the tree later if we need to
108
+ this.requiredFiles = options.requiredFiles || []
109
+
110
+ const additionalDefaults = []
111
+ if (options.prefix && options.workspaces) {
112
+ const path = normalizePath(options.path)
113
+ const prefix = normalizePath(options.prefix)
114
+ const workspaces = options.workspaces.map((ws) => normalizePath(ws))
115
+
116
+ // istanbul ignore else - this does nothing unless we need it to
117
+ if (path !== prefix && workspaces.includes(path)) {
118
+ // if path and prefix are not the same directory, and workspaces has path in it
119
+ // then we know path is a workspace directory. in order to not drop ignore rules
120
+ // from directories between the workspaces root (prefix) and the workspace itself
121
+ // (path) we need to find and read those now
122
+ const relpath = relative(options.prefix, dirname(options.path))
123
+ additionalDefaults.push(...readOutOfTreeIgnoreFiles(options.prefix, relpath))
124
+ } else if (path === prefix) {
125
+ // on the other hand, if the path and prefix are the same, then we ignore workspaces
126
+ // so that we don't pack a workspace as part of the root project. append them as
127
+ // normalized relative paths from the root
128
+ additionalDefaults.push(...workspaces.map((w) => normalizePath(relative(options.path, w))))
129
+ }
130
+ }
210
131
 
211
- if (this.packageJsonCache.has(ig)) {
212
- const pkg = this.packageJsonCache.get(ig)
132
+ // go ahead and inject the default rules now
133
+ this.injectRules(defaultRules, [...defaults, ...additionalDefaults])
213
134
 
214
- // fall back to filesystem when seeded manifest is invalid
215
- if (!pkg || typeof pkg !== 'object') {
216
- return this.readPackageJson(entries)
217
- }
135
+ if (!this.isPackage) {
136
+ // if this instance is not a package, then place some strict default rules, and append
137
+ // known required files for this directory
138
+ this.injectRules(strictRules, [
139
+ ...strictDefaults,
140
+ ...this.requiredFiles.map((file) => `!${file}`),
141
+ ])
142
+ }
143
+ }
218
144
 
219
- // feels wonky, but this ensures package bin is _always_
220
- // normalized, as well as guarding against invalid JSON
221
- return this.getPackageFiles(entries, JSON.stringify(pkg))
145
+ // overridden method: we intercept the reading of the package.json file here so that we can
146
+ // process it into both the package.json file rules as well as the strictRules synthetic rule set
147
+ addIgnoreFile (file, callback) {
148
+ // if we're adding anything other than package.json, then let ignore-walk handle it
149
+ if (file !== 'package.json' || !this.isPackage) {
150
+ return super.addIgnoreFile(file, callback)
222
151
  }
223
152
 
224
- this.readPackageJson(entries)
153
+ return this.processPackage(callback)
225
154
  }
226
155
 
227
- onReadPackageJson (entries, er, pkg) {
228
- if (er) {
229
- this.emit('error', er)
230
- } else {
231
- this.getPackageFiles(entries, pkg)
156
+ // overridden method: if we're done, but we're a package, then we also need to evaluate bundles
157
+ // before we actually emit our done event
158
+ emit (ev, data) {
159
+ if (ev !== 'done' || !this.isPackage) {
160
+ return super.emit(ev, data)
232
161
  }
162
+
163
+ // we intentionally delay the done event while keeping the function sync here
164
+ // eslint-disable-next-line promise/catch-or-return, promise/always-return
165
+ this.gatherBundles().then(() => {
166
+ super.emit('done', this.result)
167
+ })
168
+ return true
233
169
  }
234
170
 
235
- mustHaveFilesFromPackage (pkg) {
236
- const files = []
237
- if (pkg.browser) {
238
- files.push('/' + pkg.browser)
239
- }
240
- if (pkg.main) {
241
- files.push('/' + pkg.main)
242
- }
243
- if (pkg.bin) {
244
- // always an object because normalized already
245
- for (const key in pkg.bin) {
246
- files.push('/' + pkg.bin[key])
247
- }
171
+ // overridden method: before actually filtering, we make sure that we've removed the rules for
172
+ // files that should no longer take effect due to our order of precedence
173
+ filterEntries () {
174
+ if (this.ignoreRules['package.json']) {
175
+ // package.json means no .npmignore or .gitignore
176
+ this.ignoreRules['.npmignore'] = null
177
+ this.ignoreRules['.gitignore'] = null
178
+ } else if (this.ignoreRules['.npmignore']) {
179
+ // .npmignore means no .gitignore
180
+ this.ignoreRules['.gitignore'] = null
248
181
  }
249
- files.push(
250
- '/package.json',
251
- '/npm-shrinkwrap.json',
252
- '!/package-lock.json',
253
- packageMustHaves
254
- )
255
- return files
182
+
183
+ return super.filterEntries()
256
184
  }
257
185
 
258
- getPackageFiles (entries, pkg) {
259
- try {
260
- // XXX this could be changed to use read-package-json-fast
261
- // which handles the normalizing of bins for us, and simplifies
262
- // the test for bundleDependencies and bundledDependencies later.
263
- // HOWEVER if we do this, we need to be sure that we're careful
264
- // about what we write back out since rpj-fast removes some fields
265
- // that the user likely wants to keep. it also would add a second
266
- // file read that we would want to optimize away.
267
- pkg = normalizePackageBin(JSON.parse(pkg.toString()))
268
- } catch (er) {
269
- // not actually a valid package.json
270
- return super.onReaddir(entries)
186
+ // overridden method: we never want to include anything that isn't a file or directory
187
+ onstat (opts, callback) {
188
+ if (!opts.st.isFile() && !opts.st.isDirectory()) {
189
+ return callback()
271
190
  }
272
191
 
273
- const ig = path.resolve(this.path, 'package.json')
274
- this.packageJsonCache.set(ig, pkg)
192
+ return super.onstat(opts, callback)
193
+ }
275
194
 
276
- // no files list, just return the normal readdir() result
277
- if (!Array.isArray(pkg.files)) {
278
- return super.onReaddir(entries)
195
+ // overridden method: we want to refuse to pack files that are invalid, node-tar protects us from
196
+ // a lot of them but not all
197
+ stat (opts, callback) {
198
+ if (nameIsBadForWindows(opts.entry)) {
199
+ return callback()
279
200
  }
280
201
 
281
- pkg.files.push(...this.mustHaveFilesFromPackage(pkg))
202
+ return super.stat(opts, callback)
203
+ }
282
204
 
283
- // If the package has a files list, then it's unlikely to include
284
- // node_modules, because why would you do that? but since we use
285
- // the files list as the effective readdir result, that means it
286
- // looks like we don't have a node_modules folder at all unless we
287
- // include it here.
288
- if ((pkg.bundleDependencies || pkg.bundledDependencies) && entries.includes('node_modules')) {
289
- pkg.files.push('node_modules')
205
+ // overridden method: we need to load the arborist tree before we actually start running
206
+ start () {
207
+ if (this.isPackage && !this.tree) {
208
+ const arborist = new Arborist({ path: this.path })
209
+ // loading the tree is async while the start function depends on being sync
210
+ // eslint-disable-next-line promise/catch-or-return, promise/always-return
211
+ arborist.loadActual().then((tree) => {
212
+ this.tree = tree
213
+ super.start()
214
+ })
215
+ return this
290
216
  }
291
217
 
292
- const patterns = Array.from(new Set(pkg.files)).reduce((set, pattern) => {
293
- const excl = pattern.match(/^!+/)
294
- if (excl) {
295
- pattern = pattern.slice(excl[0].length)
296
- }
297
- // strip off any / or ./ from the start of the pattern. /foo => foo, ./foo => foo
298
- pattern = pattern.replace(/^\.?\/+/, '')
299
- // an odd number of ! means a negated pattern. !!foo ==> foo
300
- const negate = excl && excl[0].length % 2 === 1
301
- set.push({ pattern, negate })
302
- return set
303
- }, [])
304
-
305
- let n = patterns.length
306
- const set = new Set()
307
- const negates = new Set()
308
- const results = []
309
- const then = (pattern, negate, er, fileList, i) => {
310
- if (er) {
311
- return this.emit('error', er)
312
- }
218
+ return super.start()
219
+ }
220
+
221
+ // overridden method: this is called to create options for a child walker when we step
222
+ // in to a normal child directory (this will never be a bundle). the default method here
223
+ // copies the root's `ignoreFiles` value, but we don't want to respect package.json for
224
+ // subdirectories, so we override it with a list that intentionally omits package.json
225
+ walkerOpt (entry, opts) {
226
+ let ignoreFiles = [
227
+ defaultRules,
228
+ '.npmignore',
229
+ '.gitignore',
230
+ strictRules,
231
+ ]
313
232
 
314
- results[i] = { negate, fileList }
315
- if (--n === 0) {
316
- processResults(results)
233
+ // however, if we have a tree, and we have workspaces, and the directory we're about
234
+ // to step into is a workspace, then we _do_ want to respect its package.json
235
+ if (this.tree && this.tree.workspaces) {
236
+ const workspaceDirs = [...this.tree.workspaces.values()]
237
+ .map((dir) => dir.replace(/\\/g, '/'))
238
+
239
+ const entryPath = join(this.path, entry).replace(/\\/g, '/')
240
+ if (workspaceDirs.includes(entryPath)) {
241
+ ignoreFiles = [
242
+ defaultRules,
243
+ 'package.json',
244
+ '.npmignore',
245
+ '.gitignore',
246
+ strictRules,
247
+ ]
317
248
  }
318
249
  }
319
- const processResults = processed => {
320
- for (const { negate, fileList } of processed) {
321
- if (negate) {
322
- fileList.forEach(f => {
323
- f = f.replace(/\/+$/, '')
324
- set.delete(f)
325
- negates.add(f)
326
- })
327
- } else {
328
- fileList.forEach(f => {
329
- f = f.replace(/\/+$/, '')
330
- set.add(f)
331
- negates.delete(f)
332
- })
333
- }
334
- }
335
250
 
336
- const list = Array.from(set)
337
- // replace the files array with our computed explicit set
338
- pkg.files = list.concat(Array.from(negates).map(f => '!' + f))
339
- const rdResult = Array.from(new Set(
340
- list.map(f => f.replace(/^\/+/, ''))
341
- ))
342
- super.onReaddir(rdResult)
251
+ return {
252
+ ...super.walkerOpt(entry, opts),
253
+ ignoreFiles,
254
+ // we map over our own requiredFiles and pass ones that are within this entry
255
+ requiredFiles: this.requiredFiles
256
+ .map((file) => {
257
+ if (relative(file, entry) === '..') {
258
+ return relative(entry, file).replace(/\\/g, '/')
259
+ }
260
+ return false
261
+ })
262
+ .filter(Boolean),
343
263
  }
264
+ }
344
265
 
345
- // maintain the index so that we process them in-order only once all
346
- // are completed, otherwise the parallelism messes things up, since a
347
- // glob like **/*.js will always be slower than a subsequent !foo.js
348
- patterns.forEach(({ pattern, negate }, i) =>
349
- this.globFiles(pattern, (er, res) => then(pattern, negate, er, res, i)))
266
+ // overridden method: we want child walkers to be instances of this class, not ignore-walk
267
+ walker (entry, opts, callback) {
268
+ new PackWalker(this.walkerOpt(entry, opts)).on('done', callback).start()
350
269
  }
351
270
 
352
- filterEntry (entry, partial) {
353
- // get the partial path from the root of the walk
354
- const p = this.path.slice(this.root.length + 1)
355
- const { isProject } = this
356
- const pkg = isProject && pathHasPkg(entry)
357
- ? pkgFromPath(entry)
358
- : null
359
- const rootNM = isProject && entry === 'node_modules'
360
- const rootPJ = isProject && entry === 'package.json'
361
-
362
- return (
363
- // if we're in a bundled package, check with the parent.
364
- /^node_modules($|\/)/i.test(p) && !this.isProject ? this.parent.filterEntry(
365
- this.basename + '/' + entry, partial)
366
-
367
- // if package is bundled, all files included
368
- // also include @scope dirs for bundled scoped deps
369
- // they'll be ignored if no files end up in them.
370
- // However, this only matters if we're in the root.
371
- // node_modules folders elsewhere, like lib/node_modules,
372
- // should be included normally unless ignored.
373
- : pkg ? this.bundled.indexOf(pkg) !== -1 ||
374
- this.bundledScopes.indexOf(pkg) !== -1
375
-
376
- // only walk top node_modules if we want to bundle something
377
- : rootNM ? !!this.bundled.length
378
-
379
- // always include package.json at the root.
380
- : rootPJ ? true
381
-
382
- // always include readmes etc in any included dir
383
- : packageMustHavesRE.test(entry) ? true
384
-
385
- // npm-shrinkwrap and package.json always included in the root pkg
386
- : isProject && (entry === 'npm-shrinkwrap.json' || entry === 'package.json')
387
- ? true
388
-
389
- // package-lock never included
390
- : isProject && entry === 'package-lock.json' ? false
391
-
392
- // otherwise, follow ignore-walk's logic
393
- : super.filterEntry(entry, partial)
394
- )
271
+ // overridden method: we use a custom sort method to help compressibility
272
+ sort (a, b) {
273
+ // optimize for compressibility
274
+ // extname, then basename, then locale alphabetically
275
+ // https://twitter.com/isntitvacant/status/1131094910923231232
276
+ const exta = extname(a).toLowerCase()
277
+ const extb = extname(b).toLowerCase()
278
+ const basea = basename(a).toLowerCase()
279
+ const baseb = basename(b).toLowerCase()
280
+
281
+ return exta.localeCompare(extb, 'en') ||
282
+ basea.localeCompare(baseb, 'en') ||
283
+ a.localeCompare(b, 'en')
395
284
  }
396
285
 
397
- filterEntries () {
398
- if (this.ignoreRules['.npmignore']) {
399
- this.ignoreRules['.gitignore'] = null
400
- }
401
- this.filterEntries = super.filterEntries
402
- super.filterEntries()
286
+ // convenience method: this joins the given rules with newlines, appends a trailing newline,
287
+ // and calls the internal onReadIgnoreFile method
288
+ injectRules (filename, rules, callback = () => {}) {
289
+ this.onReadIgnoreFile(filename, `${rules.join('\n')}\n`, callback)
403
290
  }
404
291
 
405
- addIgnoreFile (file, then) {
406
- const ig = path.resolve(this.path, file)
407
- if (file === 'package.json' && !this.isProject) {
408
- then()
409
- } else if (this.packageJsonCache.has(ig)) {
410
- this.onPackageJson(ig, this.packageJsonCache.get(ig), then)
411
- } else {
412
- super.addIgnoreFile(file, then)
292
+ // custom method: this is called by addIgnoreFile when we find a package.json, it uses the
293
+ // arborist tree to pull both default rules and strict rules for the package
294
+ processPackage (callback) {
295
+ const {
296
+ bin,
297
+ browser,
298
+ files,
299
+ main,
300
+ } = this.tree.package
301
+
302
+ // rules in these arrays are inverted since they are patterns we want to _not_ ignore
303
+ const ignores = []
304
+ const strict = [
305
+ ...strictDefaults,
306
+ '!/package.json',
307
+ '!/npm-shrinkwrap.json',
308
+ '/.git',
309
+ '/node_modules',
310
+ '/package-lock.json',
311
+ '/yarn.lock',
312
+ '/pnpm-lock.yaml',
313
+ ]
314
+
315
+ // if we have a files array in our package, we need to pull rules from it
316
+ if (files) {
317
+ for (const file of files) {
318
+ // invert the rule because these are things we want to include
319
+ const inverse = `!${file}`
320
+ try {
321
+ // if an entry in the files array is a specific file, then we need to include it as a
322
+ // strict requirement for this package. if it's a directory or a pattern, it's a default
323
+ // pattern instead. this is ugly, but we have to stat to find out if it's a file
324
+ const stat = lstat(join(this.path, file.replace(/^!+/, '')).replace(/\\/g, '/'))
325
+ // if we have a file and we know that, it's strictly required
326
+ if (stat.isFile()) {
327
+ strict.unshift(inverse)
328
+ this.requiredFiles.push(file.startsWith('/') ? file.slice(1) : file)
329
+ } else if (stat.isDirectory()) {
330
+ // otherwise, it's a default ignore, and since we got here we know it's not a pattern
331
+ // so we include the directory contents
332
+ ignores.push(inverse)
333
+ ignores.push(`${inverse}/**`)
334
+ }
335
+ // if the thing exists, but is neither a file or a directory, we don't want it at all
336
+ } catch (err) {
337
+ // if lstat throws, then we assume we're looking at a pattern and treat it as a default
338
+ ignores.push(inverse)
339
+ }
340
+ }
341
+
342
+ // we prepend a '*' to exclude everything, followed by our inverted file rules
343
+ // which now mean to include those
344
+ this.injectRules('package.json', ['*', ...ignores])
413
345
  }
414
- }
415
346
 
416
- onPackageJson (ig, pkg, then) {
417
- this.packageJsonCache.set(ig, pkg)
347
+ // browser is required
348
+ if (browser) {
349
+ strict.push(`!/${browser}`)
350
+ }
418
351
 
419
- if (Array.isArray(pkg.files)) {
420
- // in this case we already included all the must-haves
421
- super.onReadIgnoreFile('package.json', pkg.files.map(
422
- f => '!' + f
423
- ).join('\n') + '\n', then)
424
- } else {
425
- // if there's a bin, browser or main, make sure we don't ignore it
426
- // also, don't ignore the package.json itself, or any files that
427
- // must be included in the package.
428
- const rules = this.mustHaveFilesFromPackage(pkg).map(f => `!${f}`)
429
- const data = rules.join('\n') + '\n'
430
- super.onReadIgnoreFile(packageNecessaryRules, data, then)
352
+ // main is required
353
+ if (main) {
354
+ strict.push(`!/${main}`)
431
355
  }
432
- }
433
356
 
434
- // override parent stat function to completely skip any filenames
435
- // that will break windows entirely.
436
- // XXX(isaacs) Next major version should make this an error instead.
437
- stat ({ entry, file, dir }, then) {
438
- if (nameIsBadForWindows(entry)) {
439
- then()
440
- } else {
441
- super.stat({ entry, file, dir }, then)
357
+ // each bin is required
358
+ if (bin) {
359
+ for (const key in bin) {
360
+ strict.push(`!/${bin[key]}`)
361
+ }
442
362
  }
363
+
364
+ // and now we add all of the strict rules to our synthetic file
365
+ this.injectRules(strictRules, strict, callback)
443
366
  }
444
367
 
445
- // override parent onstat function to nix all symlinks, other than
446
- // those coming out of the followed bundled symlink deps
447
- onstat ({ st, entry, file, dir, isSymbolicLink }, then) {
448
- if (st.isSymbolicLink()) {
449
- then()
450
- } else {
451
- super.onstat({ st, entry, file, dir, isSymbolicLink }, then)
368
+ // custom method: after we've finished gathering the files for the root package, we call this
369
+ // before emitting the 'done' event in order to gather all of the files for bundled deps
370
+ async gatherBundles () {
371
+ if (this.tree && this.seen.has(this.tree)) {
372
+ return
452
373
  }
453
- }
454
374
 
455
- onReadIgnoreFile (file, data, then) {
456
- if (file === 'package.json') {
457
- try {
458
- const ig = path.resolve(this.path, file)
459
- this.onPackageJson(ig, JSON.parse(data), then)
460
- } catch (er) {
461
- // ignore package.json files that are not json
462
- then()
463
- }
375
+ // add this node to our seen tracker
376
+ this.seen.add(this.tree)
377
+
378
+ // if we're the project root, then we look at our bundleDependencies, otherwise we got here
379
+ // because we're a bundled dependency of the root, which means we need to include all prod
380
+ // and optional dependencies in the bundle
381
+ let toBundle
382
+ if (this.tree.isProjectRoot) {
383
+ const { bundleDependencies } = this.tree.package
384
+ toBundle = bundleDependencies || []
464
385
  } else {
465
- super.onReadIgnoreFile(file, data, then)
386
+ const { dependencies, optionalDependencies } = this.tree.package
387
+ toBundle = Object.keys(dependencies || {}).concat(Object.keys(optionalDependencies || {}))
466
388
  }
467
- }
468
389
 
469
- sort (a, b) {
470
- // optimize for compressibility
471
- // extname, then basename, then locale alphabetically
472
- // https://twitter.com/isntitvacant/status/1131094910923231232
473
- const exta = path.extname(a).toLowerCase()
474
- const extb = path.extname(b).toLowerCase()
475
- const basea = path.basename(a).toLowerCase()
476
- const baseb = path.basename(b).toLowerCase()
390
+ for (const dep of toBundle) {
391
+ const edge = this.tree.edgesOut.get(dep)
392
+ // no edgeOut = missing node, so skip it. we can't pack it if it's not here
393
+ // we also refuse to pack peer dependencies and dev dependencies
394
+ if (!edge || edge.peer || edge.dev) {
395
+ continue
396
+ }
477
397
 
478
- return exta.localeCompare(extb, 'en') ||
479
- basea.localeCompare(baseb, 'en') ||
480
- a.localeCompare(b, 'en')
481
- }
398
+ // get a reference to the node we're bundling
399
+ const node = this.tree.edgesOut.get(dep).to
400
+ // and start building options to be passed to the walker for this package
401
+ const walkerOpts = {
402
+ // we use node.path for the path because we want the location the node was linked to,
403
+ // not where it actually lives on disk
404
+ path: node.path,
405
+ // but link nodes don't have edgesOut, so we need to pass in the target of the node
406
+ // in order to make sure we correctly traverse its dependencies
407
+ tree: node.target,
408
+ isPackage: true,
409
+ ignoreFiles: [],
410
+ seen: this.seen, // pass through seen so we can prevent infinite circular loops
411
+ }
482
412
 
483
- globFiles (pattern, cb) {
484
- glob(globify(pattern), { dot: true, cwd: this.path, nocase: true }, cb)
485
- }
413
+ // if our node is a link, we apply defaultRules. we don't do this for regular bundled
414
+ // deps because their .npmignore and .gitignore files are excluded by default and may
415
+ // override defaults
416
+ if (node.isLink) {
417
+ walkerOpts.ignoreFiles.push(defaultRules)
418
+ }
486
419
 
487
- readPackageJson (entries) {
488
- fs.readFile(this.path + '/package.json', (er, pkg) =>
489
- this.onReadPackageJson(entries, er, pkg))
490
- }
420
+ // _all_ nodes will follow package.json rules from their package root
421
+ walkerOpts.ignoreFiles.push('package.json')
422
+
423
+ // only link nodes will obey .npmignore or .gitignore
424
+ if (node.isLink) {
425
+ walkerOpts.ignoreFiles.push('.npmignore')
426
+ walkerOpts.ignoreFiles.push('.gitignore')
427
+ }
491
428
 
492
- walker (entry, opt, then) {
493
- new Walker(this.walkerOpt(entry, opt)).on('done', then).start()
429
+ // _all_ nodes follow strict rules
430
+ walkerOpts.ignoreFiles.push(strictRules)
431
+
432
+ // create a walker for this dependency and gather its results
433
+ const walker = new PackWalker(walkerOpts)
434
+ const bundled = await new Promise((pResolve, pReject) => {
435
+ walker.on('error', pReject)
436
+ walker.on('done', pResolve)
437
+ walker.start()
438
+ })
439
+
440
+ // now we make sure we have our paths correct from the root, and accumulate everything into
441
+ // our own result set to deduplicate
442
+ const relativeFrom = relative(this.root, walker.path)
443
+ for (const file of bundled) {
444
+ this.result.add(join(relativeFrom, file).replace(/\\/g, '/'))
445
+ }
446
+ }
494
447
  }
495
448
  }
496
449
 
497
450
  const walk = (options, callback) => {
498
- options = options || {}
499
- const p = new Promise((resolve, reject) => {
500
- const bw = new BundleWalker(options)
501
- bw.on('done', bundled => {
502
- options.bundled = bundled
503
- options.packageJsonCache = bw.packageJsonCache
504
- new Walker(options).on('done', resolve).on('error', reject).start()
505
- })
506
- bw.start()
451
+ options = { ...options, isPackage: true }
452
+ const p = new Promise((pResolve, pReject) => {
453
+ new PackWalker(options).on('done', pResolve).on('error', pReject).start()
507
454
  })
508
455
  return callback ? p.then(res => callback(null, res), callback) : p
509
456
  }
510
457
 
511
458
  module.exports = walk
512
- walk.Walker = Walker
459
+ walk.Walker = PackWalker
package/package.json CHANGED
@@ -1,16 +1,14 @@
1
1
  {
2
2
  "name": "npm-packlist",
3
- "version": "5.1.2",
3
+ "version": "6.0.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
- "main": "lib",
8
+ "main": "lib/index.js",
9
9
  "dependencies": {
10
- "glob": "^8.0.1",
11
- "ignore-walk": "^5.0.1",
12
- "npm-bundled": "^1.1.2",
13
- "npm-normalize-package-bin": "^2.0.0"
10
+ "@npmcli/arborist": "^5.0.4",
11
+ "ignore-walk": "^5.0.1"
14
12
  },
15
13
  "author": "GitHub Inc.",
16
14
  "license": "ISC",
@@ -20,7 +18,7 @@
20
18
  ],
21
19
  "devDependencies": {
22
20
  "@npmcli/eslint-config": "^3.0.1",
23
- "@npmcli/template-oss": "3.6.0",
21
+ "@npmcli/template-oss": "4.3.2",
24
22
  "mutate-fs": "^2.1.1",
25
23
  "tap": "^16.0.1"
26
24
  },
@@ -29,9 +27,6 @@
29
27
  "posttest": "npm run lint",
30
28
  "snap": "tap",
31
29
  "postsnap": "npm run lintfix --",
32
- "preversion": "npm test",
33
- "postversion": "npm publish",
34
- "prepublishOnly": "git push origin --follow-tags",
35
30
  "eslint": "eslint",
36
31
  "lint": "eslint \"**/*.js\"",
37
32
  "lintfix": "npm run lint -- --fix",
@@ -46,16 +41,20 @@
46
41
  "tap": {
47
42
  "test-env": [
48
43
  "LC_ALL=sk"
44
+ ],
45
+ "nyc-arg": [
46
+ "--exclude",
47
+ "tap-snapshots/**"
49
48
  ]
50
49
  },
51
50
  "bin": {
52
51
  "npm-packlist": "bin/index.js"
53
52
  },
54
53
  "engines": {
55
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
54
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
56
55
  },
57
56
  "templateOSS": {
58
57
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
59
- "version": "3.6.0"
58
+ "version": "4.3.2"
60
59
  }
61
60
  }