Following up on #46673 (comment), I started a more thorough investigation of ts.matchFiles (which is just ts.sys.readDirectory without the system host) and discovered that even its long-standing behavior prior to 4.4 is confusing and probably undesigned. Consider this file system:
projects/
├─ a/
│ ├─ subfolder/
│ │ └─ 1.ts
│ └─ 2.ts
└─ b/
└─ 3.ts
Things tend to work as expected if you do three things: (1) use relative paths, (2) read the same directory as your CWD, and (3) don’t use any includes that access a sibling folder with "../". If you start doing those things, especially in combination, the behavior is a bit unpredictable.
Let’s start with a simple example where I think things are working as intended:
> process.cwd()
'/projects/a'
> ts.sys.readDirectory(".", [".ts"], /*excludes*/ undefined, /*includes*/ ["**/*"])
[
'2.ts',
'subfolder/1.ts'
]
Note that the entries are relative to our CWD / the directory we’re reading (which one? We don’t know yet because they’re the same), but without the leading ./. This seems probably intentional because it matches the format of fs.readdir. Also, replacing the first argument with the empty string yields the same results.
Ok, let’s see what happens if we include something outside of the directory we’re reading:
> process.cwd()
'/projects/a'
> ts.sys.readDirectory(".", [".ts"], /*excludes*/ undefined, /*includes*/ ["**/*", "../b/**/*"])
[
'2.ts',
'subfolder/1.ts'
]
No change. This seems to make sense with the name readDirectory. Now let’s find out whether our results are relative to our CWD or to the directory we’re reading. We’ll cd up a level first:
> process.cwd()
'/projects'
> ts.sys.readDirectory("a", [".ts"], /*excludes*/ undefined, /*includes*/ ["**/*"])
[
'a/2.ts',
'a/subfolder/1.ts'
]
Interesting—they’re relative to our CWD, not the directory we’re reading, which is a divergence from fs.readdir. Does this mean we can now get includes outside of the directory we’re reading?
> ts.sys.readDirectory("a", [".ts"], /*excludes*/ undefined, /*includes*/ ["**/*", "../b/**/*"])
[
'a/2.ts',
'a/subfolder/1.ts',
'b/3.ts'
]
Yes it does! readDirectory can return results outside of the target directory, but not outside the CWD. This seems very odd to me. Let’s cd back into a and try absolute paths:
> process.cwd()
'/projects/a'
> ts.sys.readDirectory("/projects/a", [".ts"], /*excludes*/ undefined, /*includes*/ ["**/*"])
[
'/projects/a/2.ts',
'/projects/a/subfolder/1.ts'
]
Absolute in, absolute out. Quite possibly an accident? Now for the final test, an absolute input path with an includes outside the CWD:
> ts.sys.readDirectory("/projects/a", [".ts"], /*excludes*/ undefined, /*includes*/ ["**/*", "../b/**/*"])
[
'/projects/a/2.ts',
'/projects/a/subfolder/1.ts',
'/projects/b/3.ts'
]
👀
Following up on #46673 (comment), I started a more thorough investigation of
ts.matchFiles(which is justts.sys.readDirectorywithout the system host) and discovered that even its long-standing behavior prior to 4.4 is confusing and probably undesigned. Consider this file system:Things tend to work as expected if you do three things: (1) use relative paths, (2) read the same directory as your CWD, and (3) don’t use any
includesthat access a sibling folder with"../". If you start doing those things, especially in combination, the behavior is a bit unpredictable.Let’s start with a simple example where I think things are working as intended:
Note that the entries are relative to our CWD / the directory we’re reading (which one? We don’t know yet because they’re the same), but without the leading
./. This seems probably intentional because it matches the format offs.readdir. Also, replacing the first argument with the empty string yields the same results.Ok, let’s see what happens if we
includesomething outside of the directory we’re reading:No change. This seems to make sense with the name
readDirectory. Now let’s find out whether our results are relative to our CWD or to the directory we’re reading. We’llcdup a level first:Interesting—they’re relative to our CWD, not the directory we’re reading, which is a divergence from
fs.readdir. Does this mean we can now getincludesoutside of the directory we’re reading?Yes it does!
readDirectorycan return results outside of the target directory, but not outside the CWD. This seems very odd to me. Let’scdback intoaand try absolute paths:Absolute in, absolute out. Quite possibly an accident? Now for the final test, an absolute input path with an
includesoutside the CWD:👀