diff --git a/gitignore.js b/gitignore.js index e6490fb..b4d16e8 100644 --- a/gitignore.js +++ b/gitignore.js @@ -4,6 +4,7 @@ import path from 'node:path'; import fastGlob from 'fast-glob'; import gitIgnore from 'ignore'; import slash from 'slash'; +import toPath from './to-path.js'; const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -82,7 +83,7 @@ const getFileSync = (file, cwd) => { const normalizeOptions = ({ ignore = [], cwd = slash(process.cwd()), -} = {}) => ({ignore: [...DEFAULT_IGNORE, ...ignore], cwd}); +} = {}) => ({ignore: [...DEFAULT_IGNORE, ...ignore], cwd: toPath(cwd)}); export const isGitIgnored = async options => { options = normalizeOptions(options); diff --git a/gitignore.test.js b/gitignore.test.js index ea7718a..4c1a894 100644 --- a/gitignore.test.js +++ b/gitignore.test.js @@ -1,101 +1,120 @@ import path from 'node:path'; -import {fileURLToPath} from 'node:url'; +import {fileURLToPath, pathToFileURL} from 'node:url'; import test from 'ava'; import slash from 'slash'; import {isGitIgnored, isGitIgnoredSync} from './gitignore.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const getCwdValues = cwd => [cwd, pathToFileURL(cwd), pathToFileURL(cwd).href]; test('gitignore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const isIgnored = await isGitIgnored({cwd}); - const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); - const expected = ['bar.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/gitignore'))) { + // eslint-disable-next-line no-await-in-loop + const isIgnored = await isGitIgnored({cwd}); + const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); + const expected = ['bar.js']; + t.deepEqual(actual, expected); + } }); test('gitignore - mixed path styles', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const isIgnored = await isGitIgnored({cwd}); - t.true(isIgnored(slash(path.resolve(cwd, 'foo.js')))); + const directory = path.join(__dirname, 'fixtures/gitignore'); + for (const cwd of getCwdValues(directory)) { + // eslint-disable-next-line no-await-in-loop + const isIgnored = await isGitIgnored({cwd}); + t.true(isIgnored(slash(path.resolve(directory, 'foo.js')))); + } }); test('gitignore - os paths', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const isIgnored = await isGitIgnored({cwd}); - t.true(isIgnored(path.resolve(cwd, 'foo.js'))); + const directory = path.join(__dirname, 'fixtures/gitignore'); + for (const cwd of getCwdValues(directory)) { + // eslint-disable-next-line no-await-in-loop + const isIgnored = await isGitIgnored({cwd}); + t.true(isIgnored(path.resolve(directory, 'foo.js'))); + } }); test('gitignore - sync', t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const isIgnored = isGitIgnoredSync({cwd}); - const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); - const expected = ['bar.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/gitignore'))) { + const isIgnored = isGitIgnoredSync({cwd}); + const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); + const expected = ['bar.js']; + t.deepEqual(actual, expected); + } }); test('ignore ignored .gitignore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); const ignore = ['**/.gitignore']; - const isIgnored = await isGitIgnored({cwd, ignore}); - const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); - const expected = ['foo.js', 'bar.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/gitignore'))) { + // eslint-disable-next-line no-await-in-loop + const isIgnored = await isGitIgnored({cwd, ignore}); + const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); + const expected = ['foo.js', 'bar.js']; + t.deepEqual(actual, expected); + } }); test('ignore ignored .gitignore - sync', t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); const ignore = ['**/.gitignore']; - const isIgnored = isGitIgnoredSync({cwd, ignore}); - const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); - const expected = ['foo.js', 'bar.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/gitignore'))) { + const isIgnored = isGitIgnoredSync({cwd, ignore}); + const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); + const expected = ['foo.js', 'bar.js']; + t.deepEqual(actual, expected); + } }); test('negative gitignore', async t => { - const cwd = path.join(__dirname, 'fixtures/negative'); - const isIgnored = await isGitIgnored({cwd}); - const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); - const expected = ['foo.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/negative'))) { + // eslint-disable-next-line no-await-in-loop + const isIgnored = await isGitIgnored({cwd}); + const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); + const expected = ['foo.js']; + t.deepEqual(actual, expected); + } }); test('negative gitignore - sync', t => { - const cwd = path.join(__dirname, 'fixtures/negative'); - const isIgnored = isGitIgnoredSync({cwd}); - const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); - const expected = ['foo.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/negative'))) { + const isIgnored = isGitIgnoredSync({cwd}); + const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); + const expected = ['foo.js']; + t.deepEqual(actual, expected); + } }); test('multiple negation', async t => { - const cwd = path.join(__dirname, 'fixtures/multiple-negation'); - const isIgnored = await isGitIgnored({cwd}); - - const actual = [ - '!!!unicorn.js', - '!!unicorn.js', - '!unicorn.js', - 'unicorn.js', - ].filter(file => !isIgnored(file)); - - const expected = ['!!unicorn.js', '!unicorn.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/multiple-negation'))) { + // eslint-disable-next-line no-await-in-loop + const isIgnored = await isGitIgnored({cwd}); + + const actual = [ + '!!!unicorn.js', + '!!unicorn.js', + '!unicorn.js', + 'unicorn.js', + ].filter(file => !isIgnored(file)); + + const expected = ['!!unicorn.js', '!unicorn.js']; + t.deepEqual(actual, expected); + } }); test('multiple negation - sync', t => { - const cwd = path.join(__dirname, 'fixtures/multiple-negation'); - const isIgnored = isGitIgnoredSync({cwd}); - - const actual = [ - '!!!unicorn.js', - '!!unicorn.js', - '!unicorn.js', - 'unicorn.js', - ].filter(file => !isIgnored(file)); - - const expected = ['!!unicorn.js', '!unicorn.js']; - t.deepEqual(actual, expected); + for (const cwd of getCwdValues(path.join(__dirname, 'fixtures/multiple-negation'))) { + const isIgnored = isGitIgnoredSync({cwd}); + + const actual = [ + '!!!unicorn.js', + '!!unicorn.js', + '!unicorn.js', + 'unicorn.js', + ].filter(file => !isIgnored(file)); + + const expected = ['!!unicorn.js', '!unicorn.js']; + t.deepEqual(actual, expected); + } }); diff --git a/index.d.ts b/index.d.ts index f8a012e..e759683 100644 --- a/index.d.ts +++ b/index.d.ts @@ -12,7 +12,9 @@ export type ExpandDirectoriesOption = | readonly string[] | {files?: readonly string[]; extensions?: readonly string[]}; -export interface Options extends FastGlobOptions { +type FastGlobOptionsWithoutCwd = Omit; + +export interface Options extends FastGlobOptionsWithoutCwd { /** If set to `true`, `globby` will automatically glob directories for you. If you define an `Array` it will only glob files that matches the patterns inside the `Array`. You can also define an `Object` with `files` and `extensions` like in the example below. @@ -43,10 +45,17 @@ export interface Options extends FastGlobOptions { @default false */ readonly gitignore?: boolean; + + /** + The current working directory in which to search. + + @default process.cwd() + */ + readonly cwd?: URL | string; } export interface GitignoreOptions { - readonly cwd?: string; + readonly cwd?: URL | string; readonly ignore?: readonly string[]; } @@ -144,7 +153,14 @@ This function is backed by [`fast-glob`](https://github.com/mrmlnc/fast-glob#isd */ export function isDynamicPattern( patterns: string | readonly string[], - options?: FastGlobOptions + options?: FastGlobOptionsWithoutCwd & { + /** + The current working directory in which to search. + + @default process.cwd() + */ + readonly cwd?: URL | string; + } ): boolean; /** diff --git a/index.js b/index.js index 70c3461..1ebc172 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ import arrayUnion from 'array-union'; import merge2 from 'merge2'; import fastGlob from 'fast-glob'; import dirGlob from 'dir-glob'; +import toPath from './to-path.js'; import {isGitIgnored, isGitIgnoredSync} from './gitignore.js'; import {FilterStream, UniqueStream} from './stream-utils.js'; @@ -16,7 +17,7 @@ const assertPatternsInput = patterns => { } }; -const checkCwdOption = (options = {}) => { +const checkCwdOption = options => { if (!options.cwd) { return; } @@ -35,10 +36,9 @@ const checkCwdOption = (options = {}) => { const getPathString = p => p.stats instanceof fs.Stats ? p.path : p; -export const generateGlobTasks = (patterns, taskOptions) => { +export const generateGlobTasks = (patterns, taskOptions = {}) => { patterns = arrayUnion([patterns].flat()); assertPatternsInput(patterns); - checkCwdOption(taskOptions); const globTasks = []; @@ -46,8 +46,11 @@ export const generateGlobTasks = (patterns, taskOptions) => { ignore: [], expandDirectories: true, ...taskOptions, + cwd: toPath(taskOptions.cwd), }; + checkCwdOption(taskOptions); + for (const [index, pattern] of patterns.entries()) { if (isNegative(pattern)) { continue; @@ -179,8 +182,14 @@ export const globbyStream = (patterns, options) => { .pipe(uniqueStream); }; -export const isDynamicPattern = (patterns, options) => [patterns].flat() - .some(pattern => fastGlob.isDynamicPattern(pattern, options)); +export const isDynamicPattern = (patterns, options = {}) => { + options = { + ...options, + cwd: toPath(options.cwd), + }; + + return [patterns].flat().some(pattern => fastGlob.isDynamicPattern(pattern, options)); +}; export { isGitIgnored, diff --git a/package.json b/package.json index 60ce7f6..1a8b985 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "index.js", "index.d.ts", "gitignore.js", - "stream-utils.js" + "stream-utils.js", + "to-path.js" ], "keywords": [ "all", diff --git a/readme.md b/readme.md index 12b2506..b98467e 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,7 @@ Based on [`fast-glob`](https://github.com/mrmlnc/fast-glob) but adds a bunch of - Negated patterns: `['foo*', '!foobar']` - Expands directories: `foo` → `foo/**/*` - Supports `.gitignore` +- Supports `URL` as `cwd` ## Install @@ -125,7 +126,7 @@ This function is backed by [`fast-glob`](https://github.com/mrmlnc/fast-glob#isd Returns a `Promise<(path: string) => boolean>` indicating whether a given path is ignored via a `.gitignore` file. -Takes `cwd?: string` and `ignore?: string[]` as options. `.gitignore` files matched by the ignore config are not used for the resulting filter function. +Takes `cwd?: URL | string` and `ignore?: string[]` as options. `.gitignore` files matched by the ignore config are not used for the resulting filter function. ```js import {isGitIgnored} from 'globby'; diff --git a/test.js b/test.js index 599badd..b78d9f4 100644 --- a/test.js +++ b/test.js @@ -2,7 +2,7 @@ import process from 'node:process'; import fs from 'node:fs'; import path from 'node:path'; import util from 'node:util'; -import {fileURLToPath} from 'node:url'; +import {fileURLToPath, pathToFileURL} from 'node:url'; import test from 'ava'; import getStream from 'get-stream'; import { @@ -25,6 +25,8 @@ const fixture = [ 'e.tmp', ]; +const getCwdValues = cwd => [cwd, pathToFileURL(cwd), pathToFileURL(cwd).href]; + test.before(() => { if (!fs.existsSync(temporary)) { fs.mkdirSync(temporary); @@ -116,8 +118,11 @@ test('return [] for all negative patterns - stream', async t => { test('cwd option', t => { process.chdir(temporary); - t.deepEqual(globbySync('*.tmp', {cwd}), ['a.tmp', 'b.tmp', 'c.tmp', 'd.tmp', 'e.tmp']); - t.deepEqual(globbySync(['a.tmp', '*.tmp', '!{c,d,e}.tmp'], {cwd}), ['a.tmp', 'b.tmp']); + for (const cwdDirectory of getCwdValues(cwd)) { + t.deepEqual(globbySync('*.tmp', {cwd: cwdDirectory}), ['a.tmp', 'b.tmp', 'c.tmp', 'd.tmp', 'e.tmp']); + t.deepEqual(globbySync(['a.tmp', '*.tmp', '!{c,d,e}.tmp'], {cwd: cwdDirectory}), ['a.tmp', 'b.tmp']); + } + process.chdir(cwd); }); @@ -148,11 +153,18 @@ test('expose isDynamicPattern', t => { t.true(isDynamicPattern('**')); t.true(isDynamicPattern(['**', 'path1', 'path2'])); t.false(isDynamicPattern(['path1', 'path2'])); + + for (const cwdDirectory of getCwdValues(cwd)) { + t.true(isDynamicPattern('**', {cwd: cwdDirectory})); + } }); test('expandDirectories option', t => { t.deepEqual(globbySync(temporary), ['tmp/a.tmp', 'tmp/b.tmp', 'tmp/c.tmp', 'tmp/d.tmp', 'tmp/e.tmp']); - t.deepEqual(globbySync('**', {cwd: temporary}), ['a.tmp', 'b.tmp', 'c.tmp', 'd.tmp', 'e.tmp']); + for (const temporaryDirectory of getCwdValues(temporary)) { + t.deepEqual(globbySync('**', {cwd: temporaryDirectory}), ['a.tmp', 'b.tmp', 'c.tmp', 'd.tmp', 'e.tmp']); + } + t.deepEqual(globbySync(temporary, {expandDirectories: ['a*', 'b*']}), ['tmp/a.tmp', 'tmp/b.tmp']); t.deepEqual(globbySync(temporary, { expandDirectories: { @@ -193,10 +205,13 @@ test('expandDirectories and ignores option', t => { test.failing('relative paths and ignores option', t => { process.chdir(temporary); - t.deepEqual(globbySync('../tmp', { - cwd: process.cwd(), - ignore: ['tmp'], - }), []); + for (const cwd of getCwdValues(process.cwd())) { + t.deepEqual(globbySync('../tmp', { + cwd, + ignore: ['tmp'], + }), []); + } + process.chdir(cwd); }); @@ -327,71 +342,86 @@ test('gitingore option and objectMode option - sync', t => { }); test('`{extension: false}` and `expandDirectories.extensions` option', t => { - t.deepEqual( - globbySync('*', { - cwd: temporary, - extension: false, - expandDirectories: { - extensions: [ - 'md', - 'tmp', - ], - }, - }), - [ - 'a.tmp', - 'b.tmp', - 'c.tmp', - 'd.tmp', - 'e.tmp', - ], - ); + for (const temporaryDirectory of getCwdValues(temporary)) { + t.deepEqual( + globbySync('*', { + cwd: temporaryDirectory, + extension: false, + expandDirectories: { + extensions: [ + 'md', + 'tmp', + ], + }, + }), + [ + 'a.tmp', + 'b.tmp', + 'c.tmp', + 'd.tmp', + 'e.tmp', + ], + ); + } }); test('throws when specifying a file as cwd - async', async t => { const isFile = path.resolve('fixtures/gitignore/bar.js'); - await t.throwsAsync( - globby('.', {cwd: isFile}), - {message: 'The `cwd` option must be a path to a directory'}, - ); - - await t.throwsAsync( - globby('*', {cwd: isFile}), - {message: 'The `cwd` option must be a path to a directory'}, - ); + for (const file of getCwdValues(isFile)) { + // eslint-disable-next-line no-await-in-loop + await t.throwsAsync( + globby('.', {cwd: file}), + {message: 'The `cwd` option must be a path to a directory'}, + ); + + // eslint-disable-next-line no-await-in-loop + await t.throwsAsync( + globby('*', {cwd: file}), + {message: 'The `cwd` option must be a path to a directory'}, + ); + } }); test('throws when specifying a file as cwd - sync', t => { const isFile = path.resolve('fixtures/gitignore/bar.js'); - t.throws(() => { - globbySync('.', {cwd: isFile}); - }, {message: 'The `cwd` option must be a path to a directory'}); + for (const file of getCwdValues(isFile)) { + t.throws(() => { + globbySync('.', {cwd: file}); + }, {message: 'The `cwd` option must be a path to a directory'}); - t.throws(() => { - globbySync('*', {cwd: isFile}); - }, {message: 'The `cwd` option must be a path to a directory'}); + t.throws(() => { + globbySync('*', {cwd: file}); + }, {message: 'The `cwd` option must be a path to a directory'}); + } }); test('throws when specifying a file as cwd - stream', t => { const isFile = path.resolve('fixtures/gitignore/bar.js'); - t.throws(() => { - globbyStream('.', {cwd: isFile}); - }, {message: 'The `cwd` option must be a path to a directory'}); + for (const file of getCwdValues(isFile)) { + t.throws(() => { + globbyStream('.', {cwd: file}); + }, {message: 'The `cwd` option must be a path to a directory'}); - t.throws(() => { - globbyStream('*', {cwd: isFile}); - }, {message: 'The `cwd` option must be a path to a directory'}); + t.throws(() => { + globbyStream('*', {cwd: file}); + }, {message: 'The `cwd` option must be a path to a directory'}); + } }); test('don\'t throw when specifying a non-existing cwd directory - async', async t => { - const actual = await globby('.', {cwd: '/unknown'}); - t.is(actual.length, 0); + for (const cwd of getCwdValues('/unknown')) { + // eslint-disable-next-line no-await-in-loop + const actual = await globby('.', {cwd}); + t.is(actual.length, 0); + } }); test('don\'t throw when specifying a non-existing cwd directory - sync', t => { - const actual = globbySync('.', {cwd: '/unknown'}); - t.is(actual.length, 0); + for (const cwd of getCwdValues('/unknown')) { + const actual = globbySync('.', {cwd}); + t.is(actual.length, 0); + } }); diff --git a/to-path.js b/to-path.js new file mode 100644 index 0000000..3914433 --- /dev/null +++ b/to-path.js @@ -0,0 +1,15 @@ +import {fileURLToPath} from 'node:url'; + +const toPath = urlOrPath => { + if (!urlOrPath) { + return urlOrPath; + } + + if (urlOrPath instanceof URL) { + urlOrPath = urlOrPath.href; + } + + return urlOrPath.startsWith('file://') ? fileURLToPath(urlOrPath) : urlOrPath; +}; + +export default toPath;