Skip to content

node:path: keep path.resolve lexical instead of canonicalizing existing paths #2986

@andrewtdiz

Description

@andrewtdiz

Summary

Node path.resolve() is a lexical path-string operation. It resolves ./.., joins segments, and makes the result absolute, but it does not inspect the filesystem or follow symlinks.

Perry currently calls std::fs::canonicalize() from js_path_resolve(), so existing paths can be rewritten to their realpath. That makes path.resolve() depend on whether the target exists and where symlinks point, which differs from Node.

Node 25.9.0 behavior

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");

const dir = fs.mkdtempSync(path.join(os.tmpdir(), "resolve-symlink-"));
fs.mkdirSync(path.join(dir, "real"));
fs.writeFileSync(path.join(dir, "real", "file.txt"), "");
fs.symlinkSync("real", path.join(dir, "link"));

const target = path.join(dir, "link", "file.txt");
console.log(path.resolve(target));
console.log(fs.realpathSync(target));
console.log(path.resolve(target) === fs.realpathSync(target));

On macOS with Node v25.9.0 this prints different strings and false: path.resolve() preserves the lexical link/file.txt path while fs.realpathSync() follows the symlink to real/file.txt (and may also rewrite /var to /private/var).

Perry source evidence

  • crates/perry-runtime/src/path.rs:505 implements js_path_resolve() by calling std::fs::canonicalize(&path_str) and returning that path when it succeeds.
  • crates/perry-runtime/src/object/native_module_dispatch.rs:123 chains dynamic path.resolve() arguments and then calls js_path_resolve(result), so the final result is still canonicalized.
  • crates/perry-codegen/src/expr/array_methods.rs:232 lowers compiled PathResolve to the same js_path_resolve() runtime helper.

Expected

path.resolve() and path.posix.resolve() should be pure lexical operations. They should not call canonicalize, should not require the path to exist, and should not resolve symlinks. Existing paths and missing paths should be handled by the same normalization algorithm.

Duplicate checks

  • path.resolve canonicalize
  • path.resolve realpath symlink
  • node:path resolve filesystem
  • path resolve symlink
  • path.resolve symlink canonicalize lexical

These searches did not show an existing node:path issue for lexical path.resolve() semantics; the symlink hits were unrelated node:fs issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions