UNPKG

7.47 kBJavaScriptView Raw
1// Essentially, this is a fstream.DirReader class, but with a
2// bit of special logic to read the specified sort of ignore files,
3// and a filter that prevents it from picking up anything excluded
4// by those files.
5
6var Minimatch = require("minimatch").Minimatch
7, fstream = require("fstream")
8, DirReader = fstream.DirReader
9, inherits = require("inherits")
10, path = require("path")
11, fs = require("fs")
12
13module.exports = IgnoreReader
14
15inherits(IgnoreReader, DirReader)
16
17function IgnoreReader (props) {
18 if (!(this instanceof IgnoreReader)) {
19 return new IgnoreReader(props)
20 }
21
22 // must be a Directory type
23 if (typeof props === "string") {
24 props = { path: path.resolve(props) }
25 }
26
27 props.type = "Directory"
28 props.Directory = true
29
30 if (!props.ignoreFiles) props.ignoreFiles = [".ignore"]
31 this.ignoreFiles = props.ignoreFiles
32
33 this.ignoreRules = null
34
35 // ensure that .ignore files always show up at the top of the list
36 // that way, they can be read before proceeding to handle other
37 // entries in that same folder
38 if (props.sort) {
39 this._sort = props.sort === "alpha" ? alphasort : props.sort
40 props.sort = null
41 }
42
43 this.on("entries", function () {
44 // if there are any ignore files in the list, then
45 // pause and add them.
46 // then, filter the list based on our ignoreRules
47
48 var hasIg = this.entries.some(this.isIgnoreFile, this)
49
50 if (!hasIg) return this.filterEntries()
51
52 this.addIgnoreFiles()
53 })
54
55 // we filter entries before we know what they are.
56 // however, directories have to be re-tested against
57 // rules with a "/" appended, because "a/b/" will only
58 // match if "a/b" is a dir, and not otherwise.
59 this.on("_entryStat", function (entry, props) {
60 var t = entry.basename
61 if (!this.applyIgnores(entry.basename,
62 entry.type === "Directory",
63 entry)) {
64 entry.abort()
65 }
66 }.bind(this))
67
68 DirReader.call(this, props)
69}
70
71
72IgnoreReader.prototype.addIgnoreFiles = function () {
73 if (this._paused) {
74 this.once("resume", this.addIgnoreFiles)
75 return
76 }
77 if (this._ignoreFilesAdded) return
78 this._ignoreFilesAdded = true
79
80 var newIg = this.entries.filter(this.isIgnoreFile, this)
81 , count = newIg.length
82 , errState = null
83
84 if (!count) return
85
86 this.pause()
87
88 var then = function then (er) {
89 if (errState) return
90 if (er) return this.emit("error", errState = er)
91 if (-- count === 0) {
92 this.filterEntries()
93 this.resume()
94 }
95 }.bind(this)
96
97 newIg.forEach(function (ig) {
98 this.addIgnoreFile(ig, then)
99 }, this)
100}
101
102
103IgnoreReader.prototype.isIgnoreFile = function (e) {
104 return e !== "." &&
105 e !== ".." &&
106 -1 !== this.ignoreFiles.indexOf(e)
107}
108
109
110IgnoreReader.prototype.getChildProps = function (stat) {
111 var props = DirReader.prototype.getChildProps.call(this, stat)
112 props.ignoreFiles = this.ignoreFiles
113
114 // Directories have to be read as IgnoreReaders
115 // otherwise fstream.Reader will create a DirReader instead.
116 if (stat.isDirectory()) {
117 props.type = this.constructor
118 }
119 return props
120}
121
122
123IgnoreReader.prototype.addIgnoreFile = function (e, cb) {
124 // read the file, and then call addIgnoreRules
125 // if there's an error, then tell the cb about it.
126
127 var ig = path.resolve(this.path, e)
128 fs.readFile(ig, function (er, data) {
129 if (er) return cb(er)
130
131 this.emit("ignoreFile", e, data)
132 var rules = this.readRules(data, e)
133 this.addIgnoreRules(rules, e)
134 cb()
135 }.bind(this))
136}
137
138
139IgnoreReader.prototype.readRules = function (buf, e) {
140 return buf.toString().split(/\r?\n/)
141}
142
143
144// Override this to do fancier things, like read the
145// "files" array from a package.json file or something.
146IgnoreReader.prototype.addIgnoreRules = function (set, e) {
147 // filter out anything obvious
148 set = set.filter(function (s) {
149 s = s.trim()
150 return s && !s.match(/^#/)
151 })
152
153 // no rules to add!
154 if (!set.length) return
155
156 // now get a minimatch object for each one of these.
157 // Note that we need to allow dot files by default, and
158 // not switch the meaning of their exclusion
159 var mmopt = { matchBase: true, dot: true, flipNegate: true }
160 , mm = set.map(function (s) {
161 var m = new Minimatch(s, mmopt)
162 m.ignoreFile = e
163 return m
164 })
165
166 if (!this.ignoreRules) this.ignoreRules = []
167 this.ignoreRules.push.apply(this.ignoreRules, mm)
168}
169
170
171IgnoreReader.prototype.filterEntries = function () {
172 // this exclusion is at the point where we know the list of
173 // entries in the dir, but don't know what they are. since
174 // some of them *might* be directories, we have to run the
175 // match in dir-mode as well, so that we'll pick up partials
176 // of files that will be included later. Anything included
177 // at this point will be checked again later once we know
178 // what it is.
179 this.entries = this.entries.filter(function (entry) {
180 // at this point, we don't know if it's a dir or not.
181 return this.applyIgnores(entry) || this.applyIgnores(entry, true)
182 }, this)
183}
184
185
186IgnoreReader.prototype.applyIgnores = function (entry, partial, obj) {
187 var included = true
188
189 // this = /a/b/c
190 // entry = d
191 // parent /a/b sees c/d
192 if (this.parent && this.parent.applyIgnores) {
193 var pt = this.basename + "/" + entry
194 included = this.parent.applyIgnores(pt, partial)
195 }
196
197 // Negated Rules
198 // Since we're *ignoring* things here, negating means that a file
199 // is re-included, if it would have been excluded by a previous
200 // rule. So, negated rules are only relevant if the file
201 // has been excluded.
202 //
203 // Similarly, if a file has been excluded, then there's no point
204 // trying it against rules that have already been applied
205 //
206 // We're using the "flipnegate" flag here, which tells minimatch
207 // to set the "negate" for our information, but still report
208 // whether the core pattern was a hit or a miss.
209
210 if (!this.ignoreRules) {
211 return included
212 }
213
214 this.ignoreRules.forEach(function (rule) {
215 // negation means inclusion
216 if (rule.negate && included ||
217 !rule.negate && !included) {
218 // unnecessary
219 return
220 }
221
222 // first, match against /foo/bar
223 var match = rule.match("/" + entry)
224
225 if (!match) {
226 // try with the leading / trimmed off the test
227 // eg: foo/bar instead of /foo/bar
228 match = rule.match(entry)
229 }
230
231 // if the entry is a directory, then it will match
232 // with a trailing slash. eg: /foo/bar/ or foo/bar/
233 if (!match && partial) {
234 match = rule.match("/" + entry + "/") ||
235 rule.match(entry + "/")
236 }
237
238 // When including a file with a negated rule, it's
239 // relevant if a directory partially matches, since
240 // it may then match a file within it.
241 // Eg, if you ignore /a, but !/a/b/c
242 if (!match && rule.negate && partial) {
243 match = rule.match("/" + entry, true) ||
244 rule.match(entry, true)
245 }
246
247 if (match) {
248 included = rule.negate
249 }
250 }, this)
251
252 return included
253}
254
255
256IgnoreReader.prototype.sort = function (a, b) {
257 var aig = this.ignoreFiles.indexOf(a) !== -1
258 , big = this.ignoreFiles.indexOf(b) !== -1
259
260 if (aig && !big) return -1
261 if (big && !aig) return 1
262 return this._sort(a, b)
263}
264
265IgnoreReader.prototype._sort = function (a, b) {
266 return 0
267}
268
269function alphasort (a, b) {
270 return a === b ? 0
271 : a.toLowerCase() > b.toLowerCase() ? 1
272 : a.toLowerCase() < b.toLowerCase() ? -1
273 : a > b ? 1
274 : -1
275}