UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

705 lines (591 loc) 15.5 kB
/** * @module path * * Example usage: * ```js * import { Path } from 'socket:path' * ``` */ import { resolve as resolveURL, URL, URLPattern } from '../url.js' import location from '../location.js' const windowsDriveRegex = /^[a-z]:/i const windowsDriveAndSlashesRegex = /^([a-z]:(\\|\/\/))/i const windowsDriveInPathRegex = /^\/[a-z]:/i function maybeURL (uri, baseURL = undefined) { let url = null if (typeof baseURL === 'string' && baseURL.startsWith('blob:')) { baseURL = new URL(baseURL).pathname } if (typeof uri === 'string' && uri.startsWith('blob:')) { uri = new URL(uri).pathname } try { baseURL = new URL(baseURL) } catch {} try { url = new URL(uri, baseURL) } catch (_) {} return url } /** * The path.resolve() method resolves a sequence of paths or path segments into an absolute path. * @param {strig} ...paths * @returns {string} * @see {@link https://nodejs.org/api/path.html#path_path_resolve_paths} */ export function resolve (options, ...components) { const { sep } = options let resolved = '' while (components.length) { let component = components.shift().replace(/\\/g, '/') if (component.length > 1) { component = component.replace(/\/$/g, '') } if (component.startsWith('blob:')) { component = maybeURL(component) } resolved = resolveURL(resolved + '/', component) if (resolved.length > 1) { resolved = resolved.replace(/\/$/g, '') } } if (sep === '\\') { resolved = resolved.replace(/\//g, sep) } if (resolved.length > 1) { resolved = resolved.replace(new RegExp(`\\${sep}$`, 'g'), '') } return resolved } /** * Computes current working directory for a path * @param {object=} [opts] * @param {boolean=} [opts.posix] Set to `true` to force POSIX style path * @return {string} */ export function cwd (opts) { let cwd = '.' cwd = location.pathname if (cwd && !cwd.endsWith('/')) { cwd = maybeURL('..', location)?.pathname } if (!cwd) { cwd = '.' } if (cwd && opts?.posix === true) { cwd = cwd.replace(/\\/g, '/') } return cwd } /** * Computed location origin. Defaults to `socket:///` if not available. * @return {string} */ export function origin () { return location.origin } /** * Computes the relative path from `from` to `to`. * @param {object} options * @param {PathComponent} from * @param {PathComponent} to * @return {string} */ export function relative (options, from, to) { const { sep } = options if (from === to) return '' from = resolve(options, from) to = resolve(options, to) const components = { output: [], from: from.split(sep).filter(Boolean), to: to.split(sep).filter(Boolean) } for (let i = components.from.length - 1; i > -1; --i) { const a = components.from[i] const b = components.to[i] if (a !== b) { components.output.push('..') } } for (let i = 0; i < components.output.length; ++i) { components.from.pop() } components.output.push( ...components.to .join(sep) .replace(components.from.join(sep), '') .split(sep) .filter(Boolean) ) const relative = components.output.join(sep) if (windowsDriveRegex.test(from) || windowsDriveRegex.test(to)) { return relative.replace(windowsDriveRegex, '') } return relative } /** * Joins path components. This function may not return an absolute path. * @param {object} options * @param {...PathComponent} components * @return {string} */ export function join (options, ...components) { const { sep } = options const queries = [] const resolved = [] const isAbsolute = ( URL.canParse(components[0]) || components[0].trim().startsWith(sep) ) let url = null while (components.length) { const component = components.shift() if (!url && URL.canParse(component)) { url = new URL(component) queries.push(...url.pathname.split('/')) } else { queries.push(...String(component).split(sep)) } } for (const query of queries) { if (!query) { continue } else if (query === '..' && resolved.length > 1 && resolved[0] !== '..') { resolved.pop() } else if (query !== '.') { if (query.startsWith(sep)) { resolved.push(query.slice(1)) } else if (query.endsWith(sep)) { resolved.push(query.slice(0, query.length - 1)) } else { resolved.push(query) } } } const joined = resolved.join(sep) if (url) { return new URL(joined, url.origin).href } if (isAbsolute) { return sep + joined } return joined } /** * Computes directory name of path. * @param {object} options * @param {...PathComponent} components * @return {string} */ export function dirname (options, path) { if (typeof path !== 'string') { throw Object.assign(new Error(`The "path" argument must be of type string. Received: ${path}`), { code: 'ERR_INVALID_ARG_TYPE' }) } if (windowsDriveInPathRegex.test(path)) { path = path.slice(1) } const { sep } = options const [drive] = path.match(windowsDriveRegex) || [] const pathWithoutDrive = path.replace(windowsDriveAndSlashesRegex, '') let resolved = resolve(options, pathWithoutDrive, '..') if (!pathWithoutDrive.startsWith(sep)) { if (resolved.startsWith(sep)) { if (resolved === sep && !drive) { resolved = '.' } else if (pathWithoutDrive.startsWith('.')) { resolved = '.' + resolved } else { resolved = resolved.slice(1) } } } if (drive) { return `${drive}${sep}${resolved}` } return resolved } /** * Computes base name of path. * @param {object} options * @param {...PathComponent} components * @return {string} */ export function basename (options, path) { const { sep } = options const components = path.endsWith(sep) ? path.slice(0, -1).split(sep) : path.split(sep) return components.pop() } /** * Computes extension name of path. * @param {object} options * @param {PathComponent} path * @return {string} */ export function extname (options, path) { const { sep } = options return Path.from(path, sep).ext } /** * Computes normalized path * @param {object} options * @param {PathComponent} path * @return {string} */ export function normalize (options, path) { const { sep } = options path = String(path) const [drive] = path.match(windowsDriveRegex) || [] path = path .replace(windowsDriveAndSlashesRegex, '') .replace(new RegExp(`(${sep + sep})+`, 'g'), sep) const url = maybeURL(path) const pathWithoutDrive = url?.pathname || path let resolved = resolve(options, pathWithoutDrive) if (url?.protocol) { if (!pathWithoutDrive.startsWith(sep) && resolved.startsWith(sep)) { resolved = resolved.slice(1) resolved = `${url?.protocol}${url?.hostname || ''}${resolved}` } else { resolved = `${url?.protocol}/${url?.hostname || ''}${resolved}` } } if (!pathWithoutDrive.startsWith(sep)) { if (resolved.startsWith(sep)) { if (resolved === sep && !drive) { resolved = '.' } else if (pathWithoutDrive.startsWith('.')) { resolved = '.' + resolved } else { resolved = resolved.slice(1) } } } const normalized = resolved .replace(/(\/)+/g, '/') .replace(/\//g, sep) + (path.endsWith(sep) ? sep : '') if (drive) { return `${drive}${sep}${normalized}` } return normalized } /** * Formats `Path` object into a string. * @param {object} options * @param {object|Path} path * @return {string} */ export function format (options, path) { const { root, dir, base, ext, name } = path const { sep } = options const output = [] if (dir) output.push(dir, sep) else if (root) output.push(root) if (base) output.push(base) else if (name) output.push(name + (ext || '')) return output.join('').replace(/\//g, sep) } /** * Parses input `path` into a `Path` instance. * @param {PathComponent} path * @return {object} */ export function parse (path) { const sep = /\\/.test(path) ? '\\' : '/' const [drive] = path.match(windowsDriveRegex) || [] const parsed = { root: '', dir: '', base: '', ext: '', name: '' } const parts = path.replace(windowsDriveRegex, '').split(sep) parsed.root = !parts[0] ? parts[0] || sep : '' parsed.dir = !parts[0] || parts[0].startsWith('.') ? parts.slice(0, -1).join(sep) : [parsed.root].concat(parts.slice(1).slice(0, -1)).join(sep) parsed.ext = extname({ sep }, path) parsed.base = basename({ sep }, path) parsed.name = parsed.base.replace(parsed.ext, '') if (!parsed.dir) parsed.dir = parsed.root if (drive && parsed.root) parsed.root = drive + parsed.root if (drive && parsed.dir) parsed.dir = drive + parsed.dir return parsed } /** * @typedef {(string|Path|URL|{ pathname: string }|{ url: string)} PathComponent */ /** * A container for a parsed Path. */ export class Path { /** * Creates a `Path` instance from `input` and optional `cwd`. * @param {PathComponent} input * @param {string} [cwd] */ static from (input, cwd) { if (typeof input?.href === 'string') { return this.from(input.href, cwd) } else if (typeof input?.pathname === 'string') { return this.from(input.pathname, cwd) } return new this(String(input || '.'), cwd ? String(cwd) : null) } #backSlashesDetected = false #leadingDot = false #isRelative = false #hasProtocol = false #source = null #drive = null /** * `Path` class constructor. * @protected * @param {string} pathname * @param {string} [cwd = Path.cwd()] */ constructor (pathname, cwd) { const isRelative = !/^[/|\\]/.test(pathname.replace(windowsDriveRegex, '')) pathname = String(pathname || '.').trim() if (cwd) { cwd = cwd.replace(/\\/g, '/') cwd = maybeURL(`file://${cwd.replace('file://', '')}`) } else if (pathname.startsWith('..')) { pathname = pathname.slice(2) cwd = 'file:///..' } else if (isRelative) { cwd = maybeURL('file:///.') } else { cwd = maybeURL(`file://${Path.cwd()}`) } if (cwd === 'socket:/') { cwd = Path.origin() } if (pathname.startsWith('.')) { this.#leadingDot = true } try { this.pattern = new URLPattern(pathname) this.#hasProtocol = Boolean(this.pattern.protocol) } catch {} this.url = maybeURL(pathname, cwd) const [drive] = ( pathname.match(windowsDriveRegex) || this.pathname.match(windowsDriveRegex) || [] ) this.#backSlashesDetected = /\\/.test(pathname) this.#isRelative = isRelative this.#source = pathname this.#drive = drive ? drive.replace(/(\\|\/)/g, '') : null } get pathname () { if (!this.url) { return null } let { pathname } = this.url if (this.#leadingDot || this.isRelative) { pathname = pathname.replace(/^\//g, '') } if (this.#backSlashesDetected) { pathname = pathname.replace(/\//g, '\\') } return pathname } get protocol () { return this.url?.protocol } get href () { return this.url?.href } /** * `true` if the path is relative, otherwise `false. * @type {boolean} */ get isRelative () { return this.#isRelative } /** * The working value of this path. */ get value () { const { protocol, drive } = this const regex = new RegExp(`^${protocol}(//)?(.[\\|/])?`, 'i') let { href } = this if (!this.#hasProtocol) { href = href.replace(regex, '') if (this.isRelative) { // eslint-disable-next-line href = href.replace(/^[\/|\\]/g, '') } } if (this.#backSlashesDetected) { const prefix = this.#hasProtocol ? href.slice(0, protocol.length) : '' href = href.slice(prefix.length) if (prefix.length) { href = prefix + '//' + href.slice(2).replace(/\//g, '\\') } else { href = href.replace(/\//g, '\\') } } if (!drive) { href = href.replace(windowsDriveInPathRegex, '') } return href } /** * The original source, unresolved. * @type {string} */ get source () { return this.#source } /** * Computed parent path. * @type {string} */ get parent () { let pathname = this.value let i = pathname.lastIndexOf('/') if (i === -1) i = pathname.lastIndexOf('\\') if (i >= 0) pathname = pathname.slice(0, i + 1) else pathname = '' if (this.drive) { return this.drive + pathname.replace(windowsDriveInPathRegex, '') } return pathname } /** * Computed root in path. * @type {string} */ get root () { const { dir } = this const drive = (dir.match(windowsDriveRegex) || [])[0] || this.drive if (!this.isRelative) { if (this.value.includes('\\') || this.#backSlashesDetected) { return `${drive || ''}\\` } else if (this.value.startsWith('/')) { return '/' } } else if (drive) { return drive } return '' } /** * Computed directory name in path. * @type {string} */ get dir () { const { isRelative } = this let { parent } = this if (this.#backSlashesDetected) { parent = parent.replace(/\//g, '\\') } else { if (parent.endsWith('/') && parent.length > 1) { parent = parent.slice(0, -1) } } if (isRelative && (parent.startsWith('/') || parent.startsWith('\\'))) { parent = parent.slice(1) } if (this.drive) { return this.drive + parent.replace(windowsDriveRegex, '') } if (this.#source.startsWith('./') || this.#source.startsWith('.\\')) { return `.${parent || ''}` } if (parent === '.') { return '' } return parent } /** * Computed base name in path. * @type {string} */ get base () { let i = this.pathname.lastIndexOf('/') if (i === -1) { i = this.pathname.lastIndexOf('\\') } return this.pathname.slice(i >= 0 ? i + 1 : undefined) } /** * Computed base name in path without path extension. * @type {string} */ get name () { return this.base.replace(this.ext, '') } /** * Computed extension name in path. * @type {string} */ get ext () { let i = this.pathname.lastIndexOf('/') if (i === -1) { i = this.pathname.lastIndexOf('\\') } const pathname = (i > -1) ? this.pathname.slice(i) : this.pathname i = pathname.lastIndexOf('.') if (i === -1) return '' return pathname.slice(i >= 0 ? i : undefined) } /** * The computed drive, if given in the path. * @type {string?} */ get drive () { return this.#drive } /** * @return {URL} */ toURL () { return maybeURL(this.href.replace(/\\/g, '/')) } /** * Converts this `Path` instance to a string. * @return {string} */ toString () { return decodeURIComponent(this.value) } /** * @ignore */ inspect () { const p = this // eslint-disable-next-line new-parens return new class Path { root = p.root dir = p.dir base = p.base ext = p.ext name = p.name } } /** * @ignore */ [Symbol.toStringTag] () { return 'Path' } } Object.assign(Path, { basename, cwd, dirname, extname, format, join, normalize, origin, parse, relative, resolve }) export default Path