Skip to content

realpath-missing: Get real path of a non-existent folder #167

@tukusejssirs

Description

@tukusejssirs

I have no idea if I got it right, but as I understood it, realpath-missing should return a path even for folders that does not exist (is missing), however, looking at the code, it is not the case.

I was looking for alternative of realpath from node:fs/promises which does not check for folder existence (like when we need to get realpath of a folder before its creation in case it does not exist yet), so I created the following function (using node:fs/promises). Are you interested in pulling the code into you package? We could add a configuration parameter if we should check if the path exists (simply run and return the original await realpath(path)) or not (run the function below).

Note that I wrote it in TypeScript.

import {realpath as rp} from 'node:fs/promises'

/**
 * Get real path of a folder regardless if the folder exists
 *
 * @param    path   - Folder path to check
 * @param    append - Folders to append
 * @returns  Real path of the folder
 */
async function realpath({path, append}) {
  if (/^\//.test(append)) {
    path = append
    append = undefined
  }

  if (/^\.\//.test(append)) {
    append = append.replace(/^\.\//g, '')
  }

  path = path ? path.replace(/\/*$/, '') : path
  path = path ? path.replace(/\/\/+/, '/') : path
  append = append ? append.replace(/\/*$/, '') : append
  append = append ? append.replace(/\/\/+/, '/') : append

  while (/^\.\.\//.test(append)) {
    const newPath = path ? path.replace(/\/[^/]*$/, '') : ''

    if (path && newPath) {
      path = newPath
    } else if (path !== '/' && !newPath) {
      path = '/'
    } else {
      throw Error(`Invalid path or append value provided.`)
    }

    append = append.replace(/^\.\.\//g, '')
  }

  if (!path) {
    path = '/'
  }

  try {
    return `${await rp(path)}${/^[^/]$/.test(path) && append ? '/' : ''}${append ? append : ''}`
  } catch (e) {
    if (e.code === 'ENOENT') {
      let newPath = path.replace(/\/[^/]+$/, '')

      if (newPath === path) {
        if (path === '.') {
          throw Error(e)
        } else if (/^[^/]+$/.test(path)) {
          const newAppend = `${path}${append ? `/${append}` : ''}`
          return await this.realpath({path: '.', append: newAppend})
        }
      } else {
        const newAppend = `${path && path !== '/' ? path.match(/[^/]+$/g)[0] : ''}${append ? `/${append}` : ''}`
        return await this.realpath({path: newPath, append: newAppend})
      }
    }

    throw Error(e)
  }
}

Update

  • I added TSDoc.
  • I added many checks of path and append values in order to make the function robust, including:
    • if append starts with a slash, then replace path with append value;
    • if append starts with ./, remove these characters;
    • if append start with ../, remove the final folder from path:
      • I didn’t use join from node:path, as running join('/foo/bar', '../../../baz') results in /baz, but IMHO it should throw an error;
    • remove trailing slashes from append and path;
    • replace multiple consequent slashes with one slash;
  • test cases:
// Test existing relative paths
realpath({path: '.'})      // '${PWD}'
realpath({path: './src'})  // '${PWD}/src'
realpath({path: 'src'})    // '${PWD}/src'

// Test existing absolute paths
realpath({path: '/etc'})   // '/etc'
realpath({path: '/'})      // '/'

// Test non-existent relative paths
realpath({path: 'asdf'})         // '${PWD}/asdf'
realpath({path: './asdf'})       // '${PWD}/asdf'
realpath({path: 'asdf/qwer'})    // '${PWD}/asdf/qwer'
realpath({path: './asdf/qwer'})  // '${PWD}/asdf/qwer'

// Test combining `append` with `../`
realpath({path: '/qwer/asdf/xzcv/yuoi', append: '../../../asdf'})  // '/qwer/asdf'
realpath({path: '/qwer/asdf/xzcv', append: '../../../asdf'})  // '/asdf'
realpath({path: '/qwer/asdf', append: '../../../asdf'})  // error

// Test some special cases
// - trailing slashes are removed
realpath({path: '/qwer/asdf/xzcv////', append: '../../../asdf////'})  // '/asdf'
// - multiple consequent slashes should be replaced with a slash
realpath({path: '/qwer/asdf////xzcv', append: '../../////../asdf'})  // '/asdf'
// - falsy `path` results in `/`
realpath({path: '', append: 'asdf'})         // '/asdf'
realpath({path: undefined, append: 'asdf'})  // '/asdf'
realpath({path: null, append: 'asdf'})       // '/asdf'

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions