Skip to content

fix: crawl through native paths and give only the output a custom separator - #173

Open
xyrolle wants to merge 9 commits into
thecodrr:masterfrom
xyrolle:fix/native-crawl-paths
Open

xyrolle wants to merge 9 commits into
thecodrr:masterfrom
xyrolle:fix/native-crawl-paths

Conversation

@xyrolle

@xyrolle xyrolle commented Oct 26, 2026 •

Copy link
Copy Markdown

With .withPathSeparator("\\") on Linux or macOS, a crawl reads nothing: the walker hands the file system the output path, so readdir gets node_modules\ and fails with ENOENT. On Windows a / separator happens to work with the file system, which is why the bug only shows on POSIX. "crawl all files and invert path separator" only passed before because the error was suppressed and an empty result satisfies every().

The fix: the walker gives the file system native paths and the custom separator only to what it outputs.

  • The root, readdir, realpath, stat, state.root, state.visited and state.symlinks all use path.sep.
  • normalizePath now normalizes in the platform's own syntax and ends the path with path.sep. On POSIX a backslash is part of a name, as fs reads it, so a root or a symlink target such as literal\ is read as literal\, not as literal/. On Windows nothing changes: a / still gets the path cleaned to \, so the case behind 4a2a6b3 (some-root\ with a / separator) still gives some-root/.
  • Each directory carries both forms. Pushed directories, files, groups, base and relative paths, filters and the exclude callback get the output form, as before. Separators are replaced only there, and only when the output separator is not the native one (toOutputPath).
  • Nothing is converted back to native: a name that contains the other separator (a\b on POSIX) would otherwise be read as two names. Tests cover that case.

One change with the default separator, where the old behaviour was wrong: on POSIX, a backslash in the root or in a resolved symlink target used to become /. So crawl("literal\\") read literal/ (or failed with ENOENT), and a symlink to dir\name read dir/name/. Both now read the directory that exists. Relative paths that leave the root through a resolved symlink keep the backslash too.

Performance, default separator, against #172's head on macOS: 300 to 600 alternating samples per version on a warm cache, crawling node_modules (about 6k files).

  • With a cached fs, the walker's own work is the same: 0.20 ms before and after.
  • End-to-end sync crawls differ by −0.04 to +0.16 ms on 15–20 ms, with the branch slower in 49–53% of pairs.
  • Promise crawls are unchanged at 11.5 ms.

Under vitest's default interop, every read of an import from path goes through a Proxy (about 56 ns), so measure with deps.interopDefault: false or the built package.

Tests, each failing before and passing after, for both sync and withPromise:

  • the inverted-separator crawl, with .withBasePath() and a check that it finds files (the test.fails marker from test: bring master's suite back to green after errors were turned on in the tests #172 is removed);
  • the output forms (base and relative paths, dirs, groups, the exclude argument) under the other separator, plus the same forms under / to pin today's default output;
  • a file a\b and a directory c\d crawled with \; a root named literal\ next to a sibling literal, with either separator; symlinks (to a directory and to a file) into a directory named dir\name next to dir/name, with real paths, link paths and relative paths, under either separator (POSIX only, skipped on Windows);
  • symlinks under the other separator: resolvePaths true and false, with and without relative paths, both exclude cases, and recursive symlinks under both useRealPaths settings.

The backslash cases were also checked on the real macOS file system, in the sync, promise and callback APIs. npx vitest run: 174 passed. npx tsc --noEmit and npx prettier --check __tests__ src are clean.

Stacked on #172, which fixes master's tests, so this branch holds #172's four commits as well until it lands. It meets #170 only at the path import in resolve-symlink.ts (take import { dirname, normalize, sep } from "path";); with that resolved, the two together pass 176 tests.

…X name keeps its backslash, with the default separator too

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant