UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

383 lines (347 loc) 10.7 kB
/** * Platform-independent utility functions for dealing with file system paths and * URLs. * * The functions in this module are designed to be generic and work in any * runtime, whether server-side or browsers. They can be used for both system * paths and URLs. * @module */ import { isDeno, isNodeLike } from "./env.ts"; import { NotSupportedError } from "./error.ts"; import { stripEnd, trim } from "./string.ts"; import { contains, endsWith, equals, isAbsolute, isFileUrl, isFsPath, isNotQuery, isPosixPath, isUrl, isVolume, isWindowsPath, split, startsWith, type PathCompareOptions, } from "./path/util.ts"; export type { PathCompareOptions }; export { isWindowsPath, isPosixPath, isFsPath, isUrl, isFileUrl, isAbsolute, contains, endsWith, startsWith, equals, split, }; /** * Platform-specific path segment separator. The value is `\` in Windows * server-side environments, and `/` elsewhere. */ export const sep: "/" | "\\" = (() => { if (isDeno) { if (Deno.build.os === "windows") { return "\\"; } } else if (isNodeLike) { if (process.platform === "win32") { return "\\"; } } return "/"; })(); /** * Returns the current working directory. * * **NOTE:** In the browser, this function returns the current origin and pathname. * * This function may fail in unsupported environments or being rejected by the * permission system of the runtime. */ export function cwd(): string { if (isDeno) { return Deno.cwd(); } else if (isNodeLike) { return process.cwd(); } else if (typeof location === "object" && location.origin) { return location.origin + (location.pathname === "/" ? "" : location.pathname); } else { throw new NotSupportedError("Unable to determine the current working directory."); } } /** * Concatenates all given `segments` into a well-formed path. * * @example * ```ts * import { join } from "@ayonli/jsext/path"; * * console.log(join("foo", "bar")); // "foo/bar" or "foo\\bar" on Windows * console.log(join("/", "foo", "bar")); // "/foo/bar" * console.log(join("C:\\", "foo", "bar")); // "C:\\foo\\bar" * console.log(join("file:///foo", "bar", "..")) // "file:///foo" * * console.log(join("http://example.com", "foo", "bar", "?query")); * // "http://example.com/foo/bar?query" * ``` */ export function join(...segments: string[]): string { let _paths: string[] = []; for (let i = 0; i < segments.length; i++) { const path = segments[i]!; if (path) { if (isAbsolute(path)) { _paths = []; } _paths.push(path); } } const paths: string[] = []; for (let i = 0; i < _paths.length; i++) { let segment = _paths[i]!; for (const _segment of split(segment)) { if (_segment === "..") { if (!paths.length || paths.every(p => p === "..")) { paths.push(".."); } else if (paths.length > 2 || (paths.length === 2 && !isAbsolute(paths[1]!)) || (paths.length === 1 && !isAbsolute(paths[0]!)) ) { paths.pop(); } } else if (_segment && _segment !== ".") { paths.push(_segment); } } } if (!paths.length) { return "."; } const start = paths[0]!; const _sep = isUrl(start) || isPosixPath(start) ? "/" : isWindowsPath(start) ? "\\" : sep; let path = ""; for (let i = 0; i < paths.length; i++) { const segment = paths[i]!; if (!path || segment[0] === "?" || segment[0] === "#") { path += segment; } else if (isVolume(segment)) { if (path) { path += segment + "/"; } else { path = segment; } } else { path += (path.endsWith(_sep) ? "" : _sep) + trim(segment, "/\\"); } } if (/^file:\/\/\/[a-z]:$/i.test(path)) { return path + "/"; } else { return path; } } /** * This function is similar to Node.js implementation, but does not preserve * trailing slashes. * * Since Node.js implementation is not well-designed and this function is * identical as calling `join(path)`, so it is deprecated. * * @deprecated use {@link join} or {@link sanitize} instead. */ export function normalize(path: string): string { return join(path); } /** * Similar to {@link normalize}, but also remove the search string and hash * string if present. * * @example * ```ts * import { sanitize } from "@ayonli/jsext/path"; * * console.log(sanitize("foo/bar?query")); // "foo/bar" * console.log(sanitize("foo/bar#hash")); // "foo/bar" * console.log(sanitize("foo/bar/..?query#hash")); // "foo" * console.log(sanitize("foo/./bar/..?query#hash")); // "foo" * ``` */ export function sanitize(path: string): string { return join(...split(path).filter(isNotQuery)); } /** * Resolves path `segments` into a well-formed path. * * This function is similar to {@link join}, except it always returns an * absolute path based on the current working directory if the input segments * are not absolute by themselves. */ export function resolve(...segments: string[]): string { segments = segments.filter(s => s !== ""); const _cwd = cwd(); if (!segments.length) { return _cwd; } segments = isAbsolute(segments[0]!) ? segments : [_cwd, ...segments]; return join(...segments); } /** * Returns the parent path of the given `path`. * * @example * ```ts * import { dirname } from "@ayonli/jsext/path"; * * console.log(dirname("foo/bar")); // "foo" * console.log(dirname("/foo/bar")); // "/foo" * console.log(dirname("C:\\foo\\bar")); // "C:\\foo" * console.log(dirname("file:///foo/bar")); // "file:///foo" * console.log(dirname("http://example.com/foo/bar")); // "http://example.com/foo" * console.log(dirname("http://example.com/foo")); // "http://example.com" * console.log(dirname("http://example.com/foo/bar?foo=bar#baz")); // "http://example.com/foo" * ``` */ export function dirname(path: string): string { if (isUrl(path)) { const { protocol, host, pathname } = new URL(path); const origin = protocol + "//" + host; const _dirname = dirname(decodeURI(pathname)); if (_dirname === "/") { return protocol === "file:" && !host ? origin + "/" : origin; } else { return origin + _dirname; } } else { const segments = split(path).filter(isNotQuery); const last = segments.pop()!; if (segments.length) { return join(...segments); } else if (last === "/") { return "/"; } else if (isVolume(last, true)) { return last + "\\"; } else if (isVolume(last)) { return last; } else { return "."; } } } /** * Return the last portion of the given `path`. Trailing directory separators * are ignored, and optional `suffix` is removed. * * @example * ```ts * import { basename } from "@ayonli/jsext/path"; * * console.log(basename("/foo/bar")); // "bar" * console.log(basename("c:\\foo\\bar")); // "bar" * console.log(basename("file:///foo/bar")); // "bar" * console.log(basename("http://example.com/foo/bar")); // "bar" * console.log(basename("http://example.com/foo/bar?foo=bar#baz")); // "bar" * console.log(basename("http://example.com/foo/bar.txt?foo=bar#baz", ".txt")); // "bar" * ``` */ export function basename(path: string, suffix = ""): string { if (isUrl(path)) { const { pathname } = new URL(path); return basename(decodeURI(pathname), suffix); } else { const segments = split(path).filter(isNotQuery); const _basename = segments.pop(); if (!_basename || _basename === "/" || isVolume(_basename)) { return ""; } else if (suffix) { return stripEnd(_basename, suffix); } else { return _basename; } } } /** * Returns the extension of the `path` with leading period. * * @example * ```ts * import { extname } from "@ayonli/jsext/path"; * * console.log(extname("/foo/bar.txt")); // ".txt" * console.log(extname("c:\\foo\\bar.txt")); // ".txt" * console.log(extname("file:///foo/bar.txt")); // ".txt" * console.log(extname("http://example.com/foo/bar.txt")); // ".txt" * console.log(extname("http://example.com/foo/bar.txt?foo=bar#baz")); // ".txt" * ``` */ export function extname(path: string): string { const base = basename(path); const index = base.lastIndexOf("."); if (index === -1) { return ""; } else { return base.slice(index); } } /** * Converts the given path to a file URL if it's not one already. * * @example * ```ts * import { toFileUrl } from "@ayonli/jsext/path"; * * console.log(toFileUrl("foo/bar")); // "file:///foo/bar" * console.log(toFileUrl("c:\\foo\\bar")); // "file:///c:/foo/bar" * ``` */ export function toFileUrl(path: string): string { if (isFileUrl(path)) { return path; } else if (!isUrl(path)) { let _path = resolve(path).replace(/\\/g, "/"); _path = _path[0] === "/" ? _path : "/" + _path; return new URL("file://" + _path).href; } else { throw new NotSupportedError("Cannot convert a URL to a file URL."); } } /** * Converts the given URL to a file system path if it's not one already. * * @example * ```ts * import { toFsPath } from "@ayonli/jsext/path"; * * console.log(toFsPath("file:///foo/bar")); // "/foo/bar" * console.log(toFsPath("file:///c:/foo/bar")); // "c:\\foo\\bar" * ``` */ export function toFsPath(url: string | URL): string { if (typeof url === "object") { if (url.protocol === "file:") { return join(fileUrlToFsPath(url.toString())); } else { throwNonFileUrlConversionError(); } } if (isFsPath(url)) { return url; } else if (isFileUrl(url)) { return join(fileUrlToFsPath(url)); } else if (!isUrl(url)) { return resolve(url); } else { throwNonFileUrlConversionError(); } } function fileUrlToFsPath(url: string): string { return url.replace(/^file:(\/\/)?/i, "").replace(/^\/([a-z]):/i, "$1:"); } function throwNonFileUrlConversionError(): never { throw new NotSupportedError("Cannot convert a non-file URL to a file system path."); }