Files
bruno/packages/bruno-app/src/utils/common/path.js

229 lines
8.4 KiB
JavaScript

import platform from 'platform';
import path from 'path';
const isWindowsOS = () => {
const os = platform.os;
const osFamily = os.family.toLowerCase();
return osFamily.includes('windows');
};
/**
* Cross-Platform Path Standardization for Bruno Configuration Files
*
* Bruno stores relative paths in configuration files (bruno.json) that are committed to version control.
* This creates cross-platform compatibility challenges when Windows and Unix users collaborate on the same project.
*
* PROBLEM:
* - Windows users naturally create paths with backslashes (e.g., "certs\\client.pem")
* - Unix systems don't recognize backslashes as path separators
* - When a Windows user commits a bruno.json with Windows-style paths, Unix users cannot resolve these paths
* - This forces manual path conversion before git commits, which is error-prone and inconvenient
*
* SOLUTION:
* - Standardize all stored paths to POSIX format (forward slashes) across all platforms
* - Windows natively supports forward slashes as valid path separators
* - Use the posixify parameter to ensure consistent path storage
* - This enables seamless collaboration between Windows and Unix developers
*
* IMPLEMENTATION:
* - Always enable posixify by default when storing paths in configuration files
* - Client certificates, protobuf files, and other relative paths should use POSIX format
* - Both platforms can then resolve the same paths accurately without manual intervention
*
* BENEFITS:
* - No manual path conversion required before git commits
* - Consistent behavior across all platforms
* - Improved developer experience for cross-platform teams
* - Reduced git conflicts and merge issues related to path differences
*/
/** @param {string} str */
const posixify = (str) => {
return str.replace(/\\/g, '/');
};
const brunoPath = isWindowsOS() ? path.win32 : path.posix;
/**
* Get a relative path from one location to another.
*
* This function attempts to compute the relative path between two given paths
*
* @param {string} fromPath - The starting path.
* @param {string} toPath - The target path.
* @param {boolean} [shouldPosixify=true] - Whether to convert backslashes to forward slashes for cross-platform compatibility.
* @returns {string} The relative path from `fromPath` to `toPath`, `"."` if both are the same,
* or `toPath` if resolution fails.
*
* @example
* Assuming current dir: /users/john/projects
* getRelativePath('/users/john/projects', '/users/john/projects/app');
* → "app"
*
* @example
* getRelativePath('/users/john/projects', '/users/john/projects');
* → "."
*
* @example
* getRelativePath('/users/john/projects', '/users/john/docs/readme.md');
* → "../docs/readme.md"
*
* @example
* On Windows with posixify enabled
* getRelativePath('C:\\Users\\John\\Projects', 'C:\\Users\\John\\Docs\\readme.md', true);
* → "../Docs/readme.md"
*/
const getRelativePath = (fromPath, toPath, shouldPosixify = true) => {
try {
const relativePath = brunoPath.relative(fromPath, toPath);
if (relativePath === '') {
return '.';
}
const result = relativePath || toPath;
return shouldPosixify ? posixify(result) : result;
} catch (error) {
return shouldPosixify ? posixify(toPath) : toPath;
}
};
/**
* Get the basename (filename) of a file from a relative path.
*
* This function resolves a relative path against a base path and returns
* just the filename portion. It handles cross-platform path separators
* and returns an empty string for invalid inputs.
*
* @param {string} basePath - The base path to resolve against (e.g., "/users/john/projects")
* @param {string} relativePath - The relative path to resolve (e.g., "../docs/file.txt")
* @returns {string} The basename of the resolved path, or empty string if relativePath is falsy
*
* @example
* getBasename("/users/john/projects", "../docs/readme.md");
* → "readme.md"
*
* @example
* getBasename("/users/john/projects", "subfolder/config.json");
* → "config.json"
*
* @example
* getBasename("/users/john/projects", "..");
* → "john"
*
* @example
* getBasename("/users/john/projects", ".");
* → "projects"
*/
const getBasename = (basePath, relativePath) => {
if (!relativePath) {
return '';
}
const resolvedPath = brunoPath.resolve(basePath, relativePath);
const basename = brunoPath.basename(resolvedPath);
return basename;
};
/**
* Resolve a relative file path against a base path to get an absolute file path.
*
* This function resolves a relative path against a base path using the appropriate
* path resolution method for the current platform (Windows or Unix). It handles
* cross-platform path separators and returns a normalized absolute path.
*
* @param {string} basePath - The base path to resolve against (e.g., "/users/john/collections" or "C:\\Users\\John\\Collections")
* @param {string} relativePath - The relative path to resolve (e.g., "config/settings.json" or "config\\settings.json")
* @param {boolean} [shouldPosixify=false] - Whether to convert backslashes to forward slashes for cross-platform compatibility.
* @returns {string} The resolved absolute file path
*
* @example
* Basic relative path resolution
* getAbsoluteFilePath('/users/john/collections', 'config/settings.json');
* → "/users/john/collections/config/settings.json"
*
* @example
* Handle parent directory references
* getAbsoluteFilePath('/users/john/collections/api', '../shared/config.json');
* → "/users/john/collections/shared/config.json"
*
* @example
* Handle current directory reference
* getAbsoluteFilePath('/users/john/collections', './local-file.json');
* → "/users/john/collections/local-file.json"
*
* @example
* On Windows with posixify enabled
* getAbsoluteFilePath('C:\\Users\\John\\Collections', 'config\\settings.json', true);
* → "C:/Users/John/Collections/config/settings.json"
*/
const getAbsoluteFilePath = (basePath, relativePath, shouldPosixify = false) => {
const result = brunoPath.resolve(basePath, relativePath);
return shouldPosixify ? posixify(result) : result;
};
/**
* Returns a relative path when filePath is contained within basePath.
* For paths outside basePath (or same path), returns the original filePath unchanged.
*
* @param {string} basePath - The base path to check containment against (e.g., collection pathname).
* @param {string} filePath - The absolute file path to compute a relative path for.
* @param {boolean} [shouldPosixify=false] - When true, output uses '/' separators for
* cross-platform safety. Callers storing to version-controlled config files should opt in
* by passing true. Default false preserves legacy platform-native separators for
* backwards compatibility.
* @returns {string} Relative path if filePath is inside basePath, otherwise filePath itself.
*
* @example
* getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/collections/api/files/payload.txt');
* → "files/payload.txt"
*
* @example
* getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/downloads/payload.txt');
* → "/users/john/downloads/payload.txt"
*
* @example
* On Windows with posixify enabled
* getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:\\Users\\John\\Collections\\Api\\files\\payload.txt', true);
* → "files/payload.txt"
*/
const getRelativePathWithinBasePath = (basePath, filePath, shouldPosixify = false) => {
if (!basePath || !filePath) {
return filePath;
}
try {
const relativePath = getRelativePath(basePath, filePath, shouldPosixify);
const sep = shouldPosixify ? '/' : brunoPath.sep;
if (
!relativePath
|| relativePath === '.'
|| relativePath === '..'
|| relativePath.startsWith(`..${sep}`)
|| brunoPath.isAbsolute(relativePath)
) {
return shouldPosixify ? posixify(filePath) : filePath;
}
return relativePath;
} catch (error) {
return shouldPosixify ? posixify(filePath) : filePath;
}
};
const normalizePath = (p) => {
if (!p) return '';
return p.replace(/\\/g, '/').replace(/\/+$/, '');
};
// Returns true when `targetPath` is located outside `basePath`.
const isPathExternalToBasePath = (basePath, targetPath) => {
if (!basePath || !targetPath) return false;
return brunoPath.isAbsolute(getRelativePathWithinBasePath(basePath, targetPath, true));
};
export default brunoPath;
export { getRelativePath, getBasename, getAbsoluteFilePath, getRelativePathWithinBasePath, normalizePath, isPathExternalToBasePath };