@@ -11,7 +11,12 @@ import {
1111 globbyStream ,
1212 isDynamicPattern ,
1313} from '../index.js' ;
14- import { normalizeDirectoryPatternForFastGlob , normalizeAbsolutePatternToRelative } from '../utilities.js' ;
14+ import {
15+ normalizeDirectoryPatternForFastGlob ,
16+ normalizeAbsolutePatternToRelative ,
17+ getStaticAbsolutePathPrefix ,
18+ normalizeNegativePattern ,
19+ } from '../utilities.js' ;
1520import {
1621 PROJECT_ROOT ,
1722 createContextAwareFs ,
@@ -218,16 +223,63 @@ test('normalizeDirectoryPatternForFastGlob handles recursive directory patterns'
218223 t . is ( normalizeDirectoryPatternForFastGlob ( '' ) , '' , 'empty string should remain empty' ) ;
219224} ) ;
220225
221- test ( 'normalizeAbsolutePatternToRelative strips leading slash' , t => {
226+ test ( 'normalizeAbsolutePatternToRelative strips leading slash for anchored globs' , t => {
227+ // Single-segment patterns are normalized (root-anchored globs)
222228 t . is ( normalizeAbsolutePatternToRelative ( '/**' ) , '**' ) ;
223229 t . is ( normalizeAbsolutePatternToRelative ( '/foo' ) , 'foo' ) ;
224- t . is ( normalizeAbsolutePatternToRelative ( '/foo/**' ) , 'foo/**' ) ;
225230 t . is ( normalizeAbsolutePatternToRelative ( '/*.txt' ) , '*.txt' ) ;
231+
232+ // Multi-segment patterns with glob in first segment are normalized
233+ t . is ( normalizeAbsolutePatternToRelative ( '/{src,dist}/**' ) , '{src,dist}/**' ) ;
234+ t . is ( normalizeAbsolutePatternToRelative ( '/@(src|dist)/**' ) , '@(src|dist)/**' ) ;
235+ t . is ( normalizeAbsolutePatternToRelative ( '/*/foo' ) , '*/foo' ) ;
236+
237+ // Multi-segment patterns with non-glob first segment are real absolute paths - preserved
238+ t . is ( normalizeAbsolutePatternToRelative ( '/foo/**' ) , '/foo/**' ) ;
239+ t . is ( normalizeAbsolutePatternToRelative ( '/Users/foo/bar' ) , '/Users/foo/bar' ) ;
240+ t . is ( normalizeAbsolutePatternToRelative ( '/home/user/project/_*' ) , '/home/user/project/_*' ) ;
241+
242+ // Non-absolute patterns are unchanged
226243 t . is ( normalizeAbsolutePatternToRelative ( 'foo' ) , 'foo' , 'relative patterns unchanged' ) ;
227244 t . is ( normalizeAbsolutePatternToRelative ( '**' ) , '**' , 'globstar unchanged' ) ;
228245 t . is ( normalizeAbsolutePatternToRelative ( '' ) , '' , 'empty string unchanged' ) ;
229246} ) ;
230247
248+ test ( 'getStaticAbsolutePathPrefix returns leading static absolute segments' , t => {
249+ t . is ( getStaticAbsolutePathPrefix ( '/tmp/project/**/*.js' ) , '/tmp/project' ) ;
250+ t . is ( getStaticAbsolutePathPrefix ( '/tmp*/project/**/*.js' ) , undefined , 'glob in first segment' ) ;
251+ t . is ( getStaticAbsolutePathPrefix ( 'relative/**/*.js' ) , undefined , 'relative pattern' ) ;
252+ t . is ( getStaticAbsolutePathPrefix ( '/tmp' ) , '/tmp' , 'single static segment' ) ;
253+ t . is ( getStaticAbsolutePathPrefix ( '/' ) , undefined , 'root only' ) ;
254+ t . is ( getStaticAbsolutePathPrefix ( '/tmp/project' ) , '/tmp/project' , 'fully static path' ) ;
255+ } ) ;
256+
257+ test ( 'normalizeNegativePattern handles root-anchored and absolute filesystem patterns' , t => {
258+ // Dynamic root-anchored: normalized to relative
259+ t . is ( normalizeNegativePattern ( '/**' ) , '**' ) ;
260+ t . is ( normalizeNegativePattern ( '/{src,dist}/**' ) , '{src,dist}/**' ) ;
261+
262+ // Single-segment literal: treated as root-anchored (no real filesystem path has just one segment)
263+ t . is ( normalizeNegativePattern ( '/src' ) , 'src' ) ;
264+
265+ // Multi-segment literal without matching positive prefix: strip to cwd-relative
266+ t . is ( normalizeNegativePattern ( '/src/**' ) , 'src/**' ) ;
267+ t . is ( normalizeNegativePattern ( '/tmp/project/_*' , [ '/Users/someone' ] ) , 'tmp/project/_*' ) ;
268+
269+ // Multi-segment literal with matching positive prefix: preserve as absolute
270+ t . is ( normalizeNegativePattern ( '/tmp/project/_*' , [ '/tmp/project' ] ) , '/tmp/project/_*' ) ;
271+
272+ // Mixed positive pattern styles should keep root-anchored literals cwd-relative.
273+ t . is ( normalizeNegativePattern ( '/tmp/project/_*' , [ '/tmp/project' ] , true ) , 'tmp/project/_*' ) ;
274+
275+ // Ancestor positive prefixes should not force absolute behavior.
276+ t . is ( normalizeNegativePattern ( '/tmp/project/src/**' , [ '/tmp/project' ] ) , 'tmp/project/src/**' , 'ancestor positive prefixes should not force absolute negations' ) ;
277+
278+ // Non-absolute: pass through unchanged
279+ t . is ( normalizeNegativePattern ( 'foo/bar' ) , 'foo/bar' ) ;
280+ t . is ( normalizeNegativePattern ( '**' ) , '**' ) ;
281+ } ) ;
282+
231283test ( 'glob' , async t => {
232284 const result = await runGlobby ( t , '*.tmp' ) ;
233285 t . deepEqual ( result . sort ( ) , [ 'a.tmp' , 'b.tmp' , 'c.tmp' , 'd.tmp' , 'e.tmp' ] ) ;
@@ -272,6 +324,210 @@ test('negation pattern with absolute path is normalized to relative', async t =>
272324 t . deepEqual ( result , [ ] ) ;
273325} ) ;
274326
327+ test ( 'negation with absolute filesystem paths (issue #275)' , async t => {
328+ const temporaryCwd = temporaryDirectory ( ) ;
329+
330+ fs . writeFileSync ( path . join ( temporaryCwd , 'app.scss' ) , '' , 'utf8' ) ;
331+ fs . writeFileSync ( path . join ( temporaryCwd , '_partial.scss' ) , '' , 'utf8' ) ;
332+ fs . writeFileSync ( path . join ( temporaryCwd , 'b.scss' ) , '' , 'utf8' ) ;
333+
334+ try {
335+ // Single absolute negation
336+ const result = await runGlobby ( t , [
337+ `${ temporaryCwd } /*.scss` ,
338+ `!${ temporaryCwd } /_*` ,
339+ ] ) ;
340+
341+ t . deepEqual ( result . map ( filePath => path . basename ( filePath ) ) . sort ( ) , [ 'app.scss' , 'b.scss' ] ) ;
342+
343+ // Multiple absolute negations
344+ const result2 = await runGlobby ( t , [
345+ `${ temporaryCwd } /*.scss` ,
346+ `!${ temporaryCwd } /_*` ,
347+ `!${ temporaryCwd } /b*` ,
348+ ] ) ;
349+
350+ t . deepEqual ( result2 . map ( filePath => path . basename ( filePath ) ) , [ 'app.scss' ] ) ;
351+ } finally {
352+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
353+ }
354+ } ) ;
355+
356+ test ( 'negation-only root-anchored extglob excludes directories from cwd root' , async t => {
357+ const temporaryCwd = temporaryDirectory ( ) ;
358+
359+ fs . mkdirSync ( path . join ( temporaryCwd , 'src' ) , { recursive : true } ) ;
360+ fs . mkdirSync ( path . join ( temporaryCwd , 'dist' ) , { recursive : true } ) ;
361+ fs . mkdirSync ( path . join ( temporaryCwd , 'other' ) , { recursive : true } ) ;
362+
363+ fs . writeFileSync ( path . join ( temporaryCwd , 'src' , 'a.js' ) , '' , 'utf8' ) ;
364+ fs . writeFileSync ( path . join ( temporaryCwd , 'dist' , 'b.js' ) , '' , 'utf8' ) ;
365+ fs . writeFileSync ( path . join ( temporaryCwd , 'other' , 'c.js' ) , '' , 'utf8' ) ;
366+
367+ try {
368+ const result = await runGlobby ( t , [ '!/@(src|dist)/**' ] , { cwd : temporaryCwd } ) ;
369+ t . deepEqual ( result , [ 'other/c.js' ] ) ;
370+ } finally {
371+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
372+ }
373+ } ) ;
374+
375+ test ( 'negation-only root-anchored literal excludes directories from cwd root' , async t => {
376+ const temporaryCwd = temporaryDirectory ( ) ;
377+
378+ fs . mkdirSync ( path . join ( temporaryCwd , 'src' ) , { recursive : true } ) ;
379+ fs . mkdirSync ( path . join ( temporaryCwd , 'other' ) , { recursive : true } ) ;
380+
381+ fs . writeFileSync ( path . join ( temporaryCwd , 'src' , 'a.js' ) , '' , 'utf8' ) ;
382+ fs . writeFileSync ( path . join ( temporaryCwd , 'other' , 'c.js' ) , '' , 'utf8' ) ;
383+
384+ try {
385+ const result = await runGlobby ( t , [ '!/src/**' ] , { cwd : temporaryCwd } ) ;
386+ t . deepEqual ( result , [ 'other/c.js' ] ) ;
387+ } finally {
388+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
389+ }
390+ } ) ;
391+
392+ test ( 'root-anchored literal negation works with mixed relative and absolute positives' , async t => {
393+ const temporaryCwd = temporaryDirectory ( ) ;
394+
395+ fs . mkdirSync ( path . join ( temporaryCwd , 'src' ) , { recursive : true } ) ;
396+ fs . mkdirSync ( path . join ( temporaryCwd , 'other' ) , { recursive : true } ) ;
397+
398+ fs . writeFileSync ( path . join ( temporaryCwd , 'src' , 'a.js' ) , '' , 'utf8' ) ;
399+ fs . writeFileSync ( path . join ( temporaryCwd , 'other' , 'c.js' ) , '' , 'utf8' ) ;
400+
401+ try {
402+ const result = await runGlobby ( t , [
403+ 'src/**/*.js' ,
404+ `${ temporaryCwd } /other/**/*.js` ,
405+ '!/src/**' ,
406+ ] , { cwd : temporaryCwd } ) ;
407+
408+ t . deepEqual ( result . map ( filePath => path . basename ( filePath ) ) . sort ( ) , [ 'c.js' ] ) ;
409+ } finally {
410+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
411+ }
412+ } ) ;
413+
414+ test ( 'root-anchored literal negation stays cwd-relative when absolute positive has unknown prefix' , async t => {
415+ const temporaryCwd = temporaryDirectory ( ) ;
416+
417+ fs . mkdirSync ( path . join ( temporaryCwd , 'src' ) , { recursive : true } ) ;
418+ fs . writeFileSync ( path . join ( temporaryCwd , 'src' , 'a.js' ) , '' , 'utf8' ) ;
419+
420+ try {
421+ const result = await runGlobby ( t , [
422+ 'src/**/*.js' ,
423+ '/tmp*/nomatch/**/*.js' ,
424+ '!/src/**' ,
425+ ] , { cwd : temporaryCwd } ) ;
426+
427+ t . deepEqual ( result , [ ] ) ;
428+ } finally {
429+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
430+ }
431+ } ) ;
432+
433+ test ( 'root-anchored literal negation does not depend on later absolute positive patterns' , async t => {
434+ const temporaryCwd = temporaryDirectory ( ) ;
435+ const rootSegment = temporaryCwd . split ( '/' ) . find ( Boolean ) ;
436+ const rootAnchoredPattern = `!/${ rootSegment } /**` ;
437+
438+ fs . mkdirSync ( path . join ( temporaryCwd , 'other' ) , { recursive : true } ) ;
439+ fs . writeFileSync ( path . join ( temporaryCwd , 'z.js' ) , '' , 'utf8' ) ;
440+ fs . writeFileSync ( path . join ( temporaryCwd , 'other' , 'c.js' ) , '' , 'utf8' ) ;
441+
442+ try {
443+ const result = await runGlobby ( t , [
444+ '**/*.js' ,
445+ rootAnchoredPattern ,
446+ `${ temporaryCwd } /other/**/*.js` ,
447+ ] , { cwd : temporaryCwd } ) ;
448+
449+ t . true ( result . map ( filePath => path . basename ( filePath ) ) . includes ( 'z.js' ) ) ;
450+ } finally {
451+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
452+ }
453+ } ) ;
454+
455+ test ( 'root-anchored literal negation stays cwd-relative with ancestor absolute positive' , async t => {
456+ const temporaryCwd = temporaryDirectory ( ) ;
457+ if ( path . sep === '\\' ) {
458+ t . pass ( ) ;
459+ return ;
460+ }
461+
462+ const rootSegment = temporaryCwd . split ( '/' ) . find ( Boolean ) ;
463+ const rootAnchoredPattern = `!/${ rootSegment } /**` ;
464+
465+ fs . mkdirSync ( path . join ( temporaryCwd , 'src' ) , { recursive : true } ) ;
466+ fs . mkdirSync ( path . join ( temporaryCwd , 'other' ) , { recursive : true } ) ;
467+ fs . writeFileSync ( path . join ( temporaryCwd , 'src' , 'a.js' ) , '' , 'utf8' ) ;
468+ fs . writeFileSync ( path . join ( temporaryCwd , 'other' , 'c.js' ) , '' , 'utf8' ) ;
469+
470+ try {
471+ const result = await runGlobby ( t , [
472+ 'src/**/*.js' ,
473+ `${ temporaryCwd } /other/**/*.js` ,
474+ rootAnchoredPattern ,
475+ ] , { cwd : temporaryCwd } ) ;
476+
477+ t . deepEqual ( result . map ( filePath => path . basename ( filePath ) ) . sort ( ) , [ 'a.js' , 'c.js' ] ) ;
478+ } finally {
479+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
480+ }
481+ } ) ;
482+
483+ test ( 'root-anchored literal negation stays cwd-relative with mixed absolute and relative positives' , async t => {
484+ const temporaryCwd = temporaryDirectory ( ) ;
485+ if ( path . sep === '\\' ) {
486+ t . pass ( ) ;
487+ return ;
488+ }
489+
490+ fs . mkdirSync ( path . join ( temporaryCwd , 'tmp' , 'project' ) , { recursive : true } ) ;
491+ fs . writeFileSync ( path . join ( temporaryCwd , 'tmp' , 'project' , '_partial.js' ) , '' , 'utf8' ) ;
492+ fs . writeFileSync ( path . join ( temporaryCwd , 'tmp' , 'project' , 'app.js' ) , '' , 'utf8' ) ;
493+
494+ try {
495+ const result = await runGlobby ( t , [
496+ 'tmp/project/**/*.js' ,
497+ '/tmp/**/*.nomatch' ,
498+ '!/tmp/project/_*' ,
499+ ] , { cwd : temporaryCwd } ) ;
500+
501+ t . deepEqual ( result , [ 'tmp/project/app.js' ] ) ;
502+ } finally {
503+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
504+ }
505+ } ) ;
506+
507+ test ( 'root-anchored literal negation stays cwd-relative when absolute positive shares exact prefix' , async t => {
508+ const temporaryCwd = temporaryDirectory ( ) ;
509+ if ( path . sep === '\\' ) {
510+ t . pass ( ) ;
511+ return ;
512+ }
513+
514+ fs . mkdirSync ( path . join ( temporaryCwd , 'tmp' , 'project' ) , { recursive : true } ) ;
515+ fs . writeFileSync ( path . join ( temporaryCwd , 'tmp' , 'project' , '_partial.js' ) , '' , 'utf8' ) ;
516+ fs . writeFileSync ( path . join ( temporaryCwd , 'tmp' , 'project' , 'app.js' ) , '' , 'utf8' ) ;
517+
518+ try {
519+ const result = await runGlobby ( t , [
520+ 'tmp/project/**/*.js' ,
521+ '/tmp/project/**/*.nomatch' ,
522+ '!/tmp/project/_*' ,
523+ ] , { cwd : temporaryCwd } ) ;
524+
525+ t . deepEqual ( result , [ 'tmp/project/app.js' ] ) ;
526+ } finally {
527+ fs . rmSync ( temporaryCwd , { recursive : true , force : true } ) ;
528+ }
529+ } ) ;
530+
275531test ( 'expandNegationOnlyPatterns: false returns empty array for negation-only patterns' , async t => {
276532 const result = await runGlobby ( t , [ '!a.tmp' , '!b.tmp' ] , { cwd : temporary , expandNegationOnlyPatterns : false } ) ;
277533 t . deepEqual ( result , [ ] ) ;
0 commit comments