Repository: browserslist/browserslist (language: javascript)

Issue:
Browserslist assumes it has read access to all directories in path
Browserslist throws a fatal error if it fails to read a file or directory while searching for config files on the ancestors of the path. From the Readme:

"Browserslist will check config in every directory in path."

This causes problems if running this in Deno with restrictive permissions. A typical usage will be to set `--allow-read=.`. However this causes BL to throw an error as it tries to find configs up the path.

BL does the same thing with usage stats. It goes up the path and throws a fatal error if denied read access.

I realize I can get around this problem by explicitly passing the config and usage_stats files as environment variables. That's fine for the config, but it's awkward for usage stats: I have to create a valid json file even if I don't have any stats to work with.

Furthermore it's a frustrating developer experience to have a script throw an error trying to read something that doesn't even exist (a bl config up the path) given that it will carry on without even a warning if it it successfully determines there is nothing there.

I wonder if it would be possible to give users the ability to stop this behavior, or control it? One idea:

`BROWSERSLIST_ROOT_PATH`: browserslist will check for config or usage_data files up to and including this path.

Note that while my issue is entirely due to my choice of using Deno and with restrictive permissions, Node too is working on a permissions system, so this issue may be relevant for node users someday too.

Thank you!




Discussion / hints:
Yes, it is a valid issue.

I prefer to have some out-of-box solution (like doing on dir with `package.json`), but I afraid that it will be a major change.

So we can try with environment variable. Can you send a PR?
Yes I'll try to submit a PR.

Fix patch:
diff --git a/README.md b/README.md
index e5c7912e..dcf61aeb 100644
--- a/README.md
+++ b/README.md
@@ -683,6 +683,12 @@ with [environment variables]:
   BROWSERSLIST_DANGEROUS_EXTEND=1 npx webpack
    ```
 
+* `BROWSERSLIST_ROOT_PATH` to prevent reading files above this path.
+
+   ```sh
+  BROWSERSLIST_ROOT_PATH=. npx webpack
+   ```
+
 [environment variables]: https://en.wikipedia.org/wiki/Environment_variable
 
 
diff --git a/index.d.ts b/index.d.ts
index 7f8e7605..e47c5cfe 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -193,6 +193,7 @@ declare global {
       BROWSERSLIST_ENV?: string
       BROWSERSLIST_IGNORE_OLD_DATA?: string
       BROWSERSLIST_STATS?: string
+      BROWSERSLIST_ROOT_PATH?: string
     }
   }
 }
diff --git a/node.js b/node.js
index ab3124cb..a935371d 100644
--- a/node.js
+++ b/node.js
@@ -50,12 +50,22 @@ function eachParent(file, callback) {
   var dir = isFile(file) ? path.dirname(file) : file
   var loc = path.resolve(dir)
   do {
+    if (!pathInRoot(loc)) break;
     var result = callback(loc)
     if (typeof result !== 'undefined') return result
   } while (loc !== (loc = path.dirname(loc)))
   return undefined
 }
 
+function pathInRoot(p) {
+  if (!process.env.BROWSERSLIST_ROOT_PATH) return true
+  var rootPath = path.resolve(process.env.BROWSERSLIST_ROOT_PATH);
+  if (path.relative(rootPath, p).substring(0,2) === '..') {
+    return false;
+  }
+  return true
+}
+
 function check(section) {
   if (Array.isArray(section)) {
     for (var i = 0; i < section.length; i++) {


Test patch:
diff --git a/test/config.test.js b/test/config.test.js
index 9711334b..e993715a 100644
--- a/test/config.test.js
+++ b/test/config.test.js
@@ -120,4 +120,16 @@ test('reads config with one string', () => {
   equal(browserslist.findConfig(STRING), { defaults: 'ie 9, ie 8' })
 })
 
+test('stops at ROOT', () => {
+  browserslist.clearCaches()
+  process.env.BROWSERSLIST_ROOT_PATH = join(__dirname, 'fixtures', 'dir')
+  equal(browserslist.findConfig(FILE), undefined)
+})
+
+test('allows up to ROOT', () => {
+  browserslist.clearCaches()
+  process.env.BROWSERSLIST_ROOT_PATH = join(__dirname, 'fixtures')
+  equal(browserslist.findConfig(FILE), { defaults: ['ie 11', 'ie 10'] })
+})
+
 test.run()
diff --git a/test/main.test.js b/test/main.test.js
index f8d136d8..2ba22205 100644
--- a/test/main.test.js
+++ b/test/main.test.js
@@ -21,6 +21,7 @@ test.after.each(() => {
   delete process.env.BROWSERSLIST
   delete process.env.BROWSERSLIST_CONFIG
   delete process.env.BROWSERSLIST_ENV
+  delete process.env.BROWSERSLIST_ROOT_PATH
 })
 
 test('accepts array', () => {