Skip to content

Commit 2e5d663

Browse files
authored
Fix behavior for matching paths outside cwd (#92)
1 parent cc5cbde commit 2e5d663

File tree

3 files changed

+167
-31
lines changed

3 files changed

+167
-31
lines changed

index.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ const path = require('path');
33
const PluginError = require('plugin-error');
44
const multimatch = require('multimatch');
55
const streamfilter = require('streamfilter');
6+
const toAbsoluteGlob = require('to-absolute-glob');
67

8+
/**
9+
* @param {string | string[]|function(string):boolean} pattern function or glob pattern or array of glob patterns to filter files
10+
* @param {object} options see minimatch options, also root option for path resolving
11+
* @returns {Stream} Transform stream of Vinyl files
12+
*/
713
module.exports = (pattern, options = {}) => {
814
pattern = typeof pattern === 'string' ? [pattern] : pattern;
915

@@ -17,16 +23,30 @@ module.exports = (pattern, options = {}) => {
1723
if (typeof pattern === 'function') {
1824
match = pattern(file);
1925
} else {
20-
let relativePath = path.relative(file.cwd, file.path);
26+
const base = path.dirname(file.path);
27+
const patterns = pattern.map(pattern => {
28+
// Filename only matching glob
29+
// prepend full path
30+
if (!pattern.includes('/')) {
31+
if (pattern[0] === '!') {
32+
return '!' + path.resolve(base, pattern.slice(1));
33+
}
2134

22-
// If the path leaves the current working directory, then we need to
23-
// resolve the absolute path so that the path can be properly matched
24-
// by minimatch (via multimatch)
25-
if (/^\.\.[\\/]/.test(relativePath)) {
26-
relativePath = path.resolve(relativePath);
27-
}
35+
return path.resolve(base, pattern);
36+
}
2837

29-
match = multimatch(relativePath, pattern, options).length > 0;
38+
pattern = toAbsoluteGlob(pattern, {cwd: file.cwd, root: options.root});
39+
40+
// Calling path.resolve after toAbsoluteGlob is required for removing .. from path
41+
// this is useful for ../A/B cases
42+
if (pattern[0] === '!') {
43+
return '!' + path.resolve(pattern.slice(1));
44+
}
45+
46+
return path.resolve(pattern);
47+
});
48+
49+
match = multimatch(path.resolve(file.cwd, file.path), patterns, options).length > 0;
3050
}
3151

3252
callback(!match);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"dependencies": {
3434
"multimatch": "^4.0.0",
3535
"plugin-error": "^1.0.1",
36-
"streamfilter": "^3.0.0"
36+
"streamfilter": "^3.0.0",
37+
"to-absolute-glob": "^2.0.2"
3738
},
3839
"devDependencies": {
3940
"mocha": "^6.2.0",

test.js

Lines changed: 137 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -158,28 +158,6 @@ describe('filter()', () => {
158158

159159
stream.end();
160160
});
161-
162-
it('should filter relative paths that leave current directory tree', cb => {
163-
const stream = filter('**/test/**/*.js');
164-
const buffer = [];
165-
const gfile = path.join('..', '..', 'test', 'included.js');
166-
167-
stream.on('data', file => {
168-
buffer.push(file);
169-
});
170-
171-
stream.on('end', () => {
172-
assert.equal(buffer.length, 1);
173-
assert.equal(buffer[0].relative, gfile);
174-
cb();
175-
});
176-
177-
stream.write(new Vinyl({
178-
path: gfile
179-
}));
180-
181-
stream.end();
182-
});
183161
});
184162

185163
describe('filter.restore', () => {
@@ -331,3 +309,140 @@ describe('filter.restore', () => {
331309
stream.end();
332310
});
333311
});
312+
313+
// Base directory: /A/B
314+
// Files:
315+
// A /test.js
316+
// B /A/test.js
317+
// C /A/C/test.js
318+
// D /A/B/test.js
319+
// E /A/B/C/test.js
320+
321+
// matching behaviour:
322+
// 1) starting with / - absolute path matching
323+
// 2) starting with .. - relative path mapping, cwd prepended
324+
// 3) starting with just path, like abcd/<...> or **/**.js - relative path mapping, cwd prepended
325+
// same rules for !
326+
327+
describe('path matching', () => {
328+
const testFilesPaths = [
329+
'/test.js',
330+
'/A/test.js',
331+
'/A/C/test.js',
332+
'/A/B/test.js',
333+
'/A/B/C/test.js',
334+
'/A/B/C/d.js'
335+
];
336+
const testFiles = testFilesPaths.map(path => new Vinyl({cwd: '/A/B', path}));
337+
338+
const testCases = [
339+
{
340+
description: 'Filename by suffix',
341+
pattern: ['*.js'],
342+
expectedFiles: testFiles
343+
},
344+
{
345+
description: 'Filename by suffix, excluding d.js',
346+
pattern: ['*.js', '!d.js'],
347+
expectedFiles: testFiles.slice(0, -1)
348+
},
349+
{
350+
description: 'Absolute filter by suffix',
351+
pattern: ['/**/*.js'],
352+
expectedFiles: testFiles
353+
},
354+
{
355+
description: 'Absolute filter by suffix with prefix',
356+
pattern: ['/A/**/*.js'],
357+
expectedFiles: testFiles.slice(1)
358+
},
359+
{
360+
description: 'Absolute filter by suffix with prefix equal to base',
361+
pattern: ['/A/B/**/*.js'],
362+
expectedFiles: testFiles.slice(3)
363+
},
364+
{
365+
description: 'Relative filter',
366+
pattern: ['**/*.js'],
367+
expectedFiles: testFiles.slice(3)
368+
},
369+
{
370+
description: 'Relative filter but explicit',
371+
pattern: ['./**/*.js'],
372+
expectedFiles: testFiles.slice(3)
373+
},
374+
{
375+
description: 'Relative filter with .. prefix',
376+
pattern: ['../**/*.js'],
377+
expectedFiles: testFiles.slice(1)
378+
},
379+
{
380+
description: 'Relative filter with path prefix',
381+
pattern: ['C/**/*.js'],
382+
expectedFiles: testFiles.slice(4)
383+
},
384+
{
385+
description: 'Relative filter with path prefix, but then ..',
386+
pattern: ['C/../**/*.js'],
387+
expectedFiles: testFiles.slice(3)
388+
},
389+
{
390+
description: 'Absolute filter starting with !',
391+
pattern: ['/**/*', '!/**/*.js'],
392+
expectedFiles: []
393+
},
394+
{
395+
description: 'Absolute filter starting with !, filters out all test.js',
396+
pattern: ['/**/*', '!/**/test.js'],
397+
expectedFiles: [testFiles[5]]
398+
},
399+
{
400+
description: 'Absolute filter starting with !, . omitted',
401+
pattern: ['/**/*', '!**/*.js'],
402+
expectedFiles: testFiles.slice(0, 3)
403+
},
404+
{
405+
description: 'Relative filter starting with !, with .',
406+
pattern: ['/**/*', '!./**/*.js'],
407+
expectedFiles: testFiles.slice(0, 3)
408+
},
409+
{
410+
description: 'Mixed filters: absolute filter take files, when absolute negated filter rejects',
411+
pattern: ['/A/**/*.js', '!/A/B/**/*.js'],
412+
expectedFiles: testFiles.slice(1, 3)
413+
},
414+
{
415+
description: 'Mixed filters: relative filter take files, when absolute negated filter rejects',
416+
pattern: ['**/*.js', '!/A/B/C/**/*.js'],
417+
expectedFiles: testFiles.slice(3, 4)
418+
},
419+
{
420+
description: 'Mixed filters: absolute filter take files, when relative negated filter rejects',
421+
pattern: ['/A/**/*.js', '!./C/**/*.js'],
422+
expectedFiles: testFiles.slice(1, 4)
423+
},
424+
{
425+
description: 'Mixed filters: relative filter take files, when relative negated filter rejects',
426+
pattern: ['**/*.js', '!./C/**/*.js'],
427+
expectedFiles: testFiles.slice(3, 4)
428+
}
429+
];
430+
431+
for (const testCase of testCases) {
432+
it('Should ' + testCase.description, cb => {
433+
const stream = filter(testCase.pattern);
434+
435+
testFiles.forEach(file => stream.write(file));
436+
437+
const files = [];
438+
stream.on('data', file => {
439+
files.push(file);
440+
});
441+
stream.on('end', () => {
442+
assert.deepEqual(files.map(f => f.path), testCase.expectedFiles.map(f => f.path));
443+
cb();
444+
});
445+
stream.end();
446+
});
447+
}
448+
});

0 commit comments

Comments
 (0)