chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+137
View File
@@ -0,0 +1,137 @@
import type { Directory, File } from '../FileSystem';
import * as nodePath from './path';
import { asUrl, isUrl, encodeURLChars } from './url';
function uriObjectToString(path: string | File | Directory): string {
return typeof path === 'string' ? path : path.uri;
}
export class PathUtilities {
/**
* Joins path segments into a single path.
* @param paths - An array of path segments.
* @returns A string representing the joined path.
*/
static join(...paths: (string | File | Directory)[]): string {
const [firstSegment, ...rest] = paths.map(uriObjectToString);
const pathAsUrl = asUrl(firstSegment);
if (pathAsUrl) {
pathAsUrl.pathname = nodePath.join(pathAsUrl.pathname, ...rest.map(encodeURLChars));
return pathAsUrl.toString();
}
return nodePath.join(firstSegment, ...rest.map(encodeURLChars));
}
/**
* Resolves a relative path to an absolute path.
* @param from - The base path.
* @param to - The relative path.
* @returns A string representing the resolved path.
*/
static relative(from: string | File | Directory, to: string | File | Directory): string {
const fromString = uriObjectToString(from);
const toString = uriObjectToString(to);
// If the first path is a file URL, convert it to a path
if (isUrl(fromString)) {
from = asUrl(fromString)!.pathname;
}
// If the second path is a file URL, convert it to a path
if (isUrl(toString)) {
to = asUrl(toString)!.pathname;
}
return nodePath.relative(fromString, toString);
}
/**
* Checks if a path is absolute.
* @param path - The path to check.
* @returns `true` if the path is absolute, `false` otherwise.
*/
static isAbsolute(path: string | File | Directory): boolean {
const pathString = uriObjectToString(path);
if (isUrl(pathString)) {
return true;
}
return nodePath.isAbsolute(pathString);
}
/**
* Normalizes a path.
* @param path - The path to normalize.
* @returns A string representing the normalized path.
*/
static normalize(path: string | File | Directory): string {
const pathString = uriObjectToString(path);
const pathURL = asUrl(encodeURLChars(pathString));
if (pathURL) {
pathURL.pathname = encodeURLChars(nodePath.normalize(decodeURIComponent(pathURL.pathname)));
return pathURL.toString();
}
return nodePath.normalize(pathString);
}
/**
* Returns the directory name of a path.
* @param path - The path to get the directory name from.
* @returns A string representing the directory name.
*/
static dirname(path: string | File | Directory): string {
const pathString = uriObjectToString(path);
const pathURL = asUrl(pathString);
if (pathURL) {
pathURL.pathname = encodeURLChars(nodePath.dirname(decodeURIComponent(pathURL.pathname)));
return pathURL.toString();
}
return nodePath.dirname(pathString);
}
/**
* Returns the base name of a path.
* @param path - The path to get the base name from.
* @param ext - An optional file extension.
* @returns A string representing the base name.
*/
static basename(path: string | File | Directory, ext?: string): string {
const pathString = uriObjectToString(path);
const pathURL = asUrl(pathString);
if (pathURL) {
return nodePath.basename(decodeURIComponent(pathURL.pathname));
}
return nodePath.basename(pathString, ext);
}
/**
* Returns the extension of a path.
* @param path - The path to get the extension from.
* @returns A string representing the extension.
*/
static extname(path: string | File | Directory): string {
const pathString = uriObjectToString(path);
const pathURL = asUrl(pathString);
if (pathURL) {
return nodePath.extname(decodeURIComponent(pathURL.pathname));
}
return nodePath.extname(pathString);
}
/**
* Parses a path into its components.
* @param path - The path to parse.
* @returns An object containing the parsed path components.
*/
static parse(path: string | File | Directory): {
root: string;
dir: string;
base: string;
ext: string;
name: string;
} {
const pathString = uriObjectToString(path);
const pathURL = asUrl(pathString);
if (pathURL) {
return nodePath.parse(decodeURIComponent(pathURL.pathname));
}
return nodePath.parse(pathString);
}
}
+448
View File
@@ -0,0 +1,448 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
function isPathSeparator(code: string) {
return code === '/';
}
// Resolves . and .. elements in a path with directory names
function normalizeString(path: string, allowAboveRoot: boolean, separator: string) {
let res: string = '';
let lastSegmentLength: number = 0;
let lastSlash: number = -1;
let dots: number = 0;
let code: string = '';
for (let i = 0; i <= path.length; ++i) {
if (i < path.length) code = path.charAt(i);
else if (isPathSeparator(code)) break;
else code = '/';
if (isPathSeparator(code)) {
if (lastSlash === i - 1 || dots === 1) {
// NOOP
} else if (dots === 2) {
if (
res.length < 2 ||
lastSegmentLength !== 2 ||
res.charAt(res.length - 1) !== '.' ||
res.charAt(res.length - 2) !== '.'
) {
if (res.length > 2) {
const lastSlashIndex = res.lastIndexOf(separator);
if (lastSlashIndex === -1) {
res = '';
lastSegmentLength = 0;
} else {
res = res.slice(0, lastSlashIndex);
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
}
lastSlash = i;
dots = 0;
continue;
} else if (res.length !== 0) {
res = '';
lastSegmentLength = 0;
lastSlash = i;
dots = 0;
continue;
}
}
if (allowAboveRoot) {
res += res.length > 0 ? `${separator}..` : '..';
lastSegmentLength = 2;
}
} else {
if (res.length > 0) res += `${separator}${path.slice(lastSlash + 1, i)}`;
else res = path.slice(lastSlash + 1, i);
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i;
dots = 0;
} else if (code === '.' && dots !== -1) {
++dots;
} else {
dots = -1;
}
}
return res;
}
function formatExt(ext?: string) {
return ext ? `${ext[0] === '.' ? '' : '.'}${ext}` : '';
}
export function format(
sep: string,
pathObject: {
dir?: string;
root?: string;
base?: string;
name?: string;
ext?: string;
}
): string {
const dir = pathObject.dir || pathObject.root;
const base = pathObject.base || `${pathObject.name || ''}${formatExt(pathObject.ext)}`;
if (!dir) {
return base;
}
return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
}
export function resolve(...args: string[]) {
let resolvedPath = '';
let resolvedAbsolute = false;
for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) {
const path = i >= 0 ? args[i] : '';
// Skip empty entries
if (!path || path.length === 0) {
continue;
}
resolvedPath = `${path}/${resolvedPath}`;
resolvedAbsolute = !!(path && path.charAt(0) === '/');
}
// At this point the path should be resolved to a full absolute path, but
// handle relative paths to be safe (might happen when process.cwd() fails)
// Normalize the path
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/');
if (resolvedAbsolute) {
return `/${resolvedPath}`;
}
return resolvedPath.length > 0 ? resolvedPath : '.';
}
export function normalize(path: string) {
if (path.length === 0) return '.';
const isAbsolute = path.charAt(0) === '/';
const trailingSeparator = path.charAt(path.length - 1) === '/';
// Normalize the path
path = normalizeString(path, !isAbsolute, '/');
if (path.length === 0) {
if (isAbsolute) return '/';
return trailingSeparator ? './' : '.';
}
if (trailingSeparator) path += '/';
return isAbsolute ? `/${path}` : path;
}
export function isAbsolute(path: string) {
return path.length > 0 && path.charAt(0) === '/';
}
export function join(...args: string[]) {
if (args.length === 0) return '.';
const path: string[] = [];
for (let i = 0; i < args.length; ++i) {
const arg = args[i];
if (arg && arg.length > 0) {
path.push(arg);
}
}
if (path.length === 0) return '.';
return normalize(path.join('/'));
}
export function relative(from: string, to: string) {
if (from === to) return '';
// Trim leading forward slashes.
from = resolve(from);
to = resolve(to);
if (from === to) return '';
const fromStart = 1;
const fromEnd = from.length;
const fromLen = fromEnd - fromStart;
const toStart = 1;
const toLen = to.length - toStart;
// Compare paths to find the longest common path from root
const length = fromLen < toLen ? fromLen : toLen;
let lastCommonSep = -1;
let i = 0;
for (; i < length; i++) {
const fromChar = from.charAt(fromStart + i);
if (fromChar !== to.charAt(toStart + i)) break;
else if (fromChar === '/') lastCommonSep = i;
}
if (i === length) {
if (toLen > length) {
if (to.charAt(toStart + i) === '/') {
// We get here if `from` is the exact base path for `to`.
// For example: from='/foo/bar'; to='/foo/bar/baz'
return to.slice(toStart + i + 1);
}
if (i === 0) {
// We get here if `from` is the root
// For example: from='/'; to='/foo'
return to.slice(toStart + i);
}
} else if (fromLen > length) {
if (from.charAt(fromStart + i) === '/') {
// We get here if `to` is the exact base path for `from`.
// For example: from='/foo/bar/baz'; to='/foo/bar'
lastCommonSep = i;
} else if (i === 0) {
// We get here if `to` is the root.
// For example: from='/foo/bar'; to='/'
lastCommonSep = 0;
}
}
}
let out = '';
// Generate the relative path based on the path difference between `to`
// and `from`.
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
if (i === fromEnd || from.charAt(i) === '/') {
out += out.length === 0 ? '..' : '/..';
}
}
// Lastly, append the rest of the destination (`to`) path that comes after
// the common path parts.
return `${out}${to.slice(toStart + lastCommonSep)}`;
}
export function toNamespacedPath(path: string) {
// Non-op on posix systems
return path;
}
export function dirname(path: string) {
if (path.length === 0) return '.';
const hasRoot = path.charAt(0) === '/';
let end = -1;
let matchedSlash = true;
for (let i = path.length - 1; i >= 1; --i) {
if (path.charAt(i) === '/') {
if (!matchedSlash) {
end = i;
break;
}
} else {
// We saw the first non-path separator
matchedSlash = false;
}
}
if (end === -1) return hasRoot ? '/' : '.';
if (hasRoot && end === 1) return '//';
return path.slice(0, end);
}
export function basename(path: string, suffix?: string) {
let start = 0;
let end = -1;
let matchedSlash = true;
if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) {
if (suffix === path) return '';
let extIdx = suffix.length - 1;
let firstNonSlashEnd = -1;
for (let i = path.length - 1; i >= 0; --i) {
const code = path.charAt(i);
if (code === '/') {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
start = i + 1;
break;
}
} else {
if (firstNonSlashEnd === -1) {
// We saw the first non-path separator, remember this index in case
// we need it if the extension ends up not matching
matchedSlash = false;
firstNonSlashEnd = i + 1;
}
if (extIdx >= 0) {
// Try to match the explicit extension
if (code === suffix.charAt(extIdx)) {
if (--extIdx === -1) {
// We matched the extension, so mark this as the end of our path
// component
end = i;
}
} else {
// Extension does not match, so our result is the entire path
// component
extIdx = -1;
end = firstNonSlashEnd;
}
}
}
}
if (start === end) end = firstNonSlashEnd;
else if (end === -1) end = path.length;
return path.slice(start, end);
}
for (let i = path.length - 1; i >= 0; --i) {
if (path.charAt(i) === '/') {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
start = i + 1;
break;
}
} else if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// path component
matchedSlash = false;
end = i + 1;
}
}
if (end === -1) return '';
return path.slice(start, end);
}
export function extname(path: string) {
let startDot = -1;
let startPart = 0;
let end = -1;
let matchedSlash = true;
// Track the state of characters (if any) we see before our first dot and
// after any path separator we find
let preDotState = 0;
for (let i = path.length - 1; i >= 0; --i) {
const code = path.charAt(i);
if (code === '/') {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// extension
matchedSlash = false;
end = i + 1;
}
if (code === '.') {
// If this is our first dot, mark it as the start of our extension
if (startDot === -1) startDot = i;
else if (preDotState !== 1) preDotState = 1;
} else if (startDot !== -1) {
// We saw a non-dot and non-path separator before our dot, so we should
// have a good chance at having a non-empty extension
preDotState = -1;
}
}
if (
startDot === -1 ||
end === -1 ||
// We saw a non-dot character immediately before the dot
preDotState === 0 ||
// The (right-most) trimmed path component is exactly '..'
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
) {
return '';
}
return path.slice(startDot, end);
}
export function parse(path: string) {
const ret = { root: '', dir: '', base: '', ext: '', name: '' };
if (path.length === 0) return ret;
const isAbsolute = path.charAt(0) === '/';
let start;
if (isAbsolute) {
ret.root = '/';
start = 1;
} else {
start = 0;
}
let startDot = -1;
let startPart = 0;
let end = -1;
let matchedSlash = true;
let i = path.length - 1;
// Track the state of characters (if any) we see before our first dot and
// after any path separator we find
let preDotState = 0;
// Get non-dir info
for (; i >= start; --i) {
const code = path.charAt(i);
if (code === '/') {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// extension
matchedSlash = false;
end = i + 1;
}
if (code === '.') {
// If this is our first dot, mark it as the start of our extension
if (startDot === -1) startDot = i;
else if (preDotState !== 1) preDotState = 1;
} else if (startDot !== -1) {
// We saw a non-dot and non-path separator before our dot, so we should
// have a good chance at having a non-empty extension
preDotState = -1;
}
}
if (end !== -1) {
const start = startPart === 0 && isAbsolute ? 1 : startPart;
if (
startDot === -1 ||
// We saw a non-dot character immediately before the dot
preDotState === 0 ||
// The (right-most) trimmed path component is exactly '..'
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
) {
// eslint-disable-next-line no-multi-assign
ret.base = ret.name = path.slice(start, end);
} else {
ret.name = path.slice(start, startDot);
ret.base = path.slice(start, end);
ret.ext = path.slice(startDot, end);
}
}
if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
else if (isAbsolute) ret.dir = '/';
return ret;
}
export const sep = '/';
export const delimiter = ':';
+78
View File
@@ -0,0 +1,78 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import { resolve, sep } from './path';
const percentRegEx = /%/g;
const backslashRegEx = /\\/g;
const newlineRegEx = /\n/g;
const carriageReturnRegEx = /\r/g;
const tabRegEx = /\t/g;
const questionRegex = /\?/g;
const hashRegex = /#/g;
const spaceRegEx = / /g;
function encodePathChars(filepath: string) {
if (filepath.indexOf('%') !== -1) filepath = filepath.replace(percentRegEx, '%25');
// In posix, backslash is a valid character in paths:
if (filepath.indexOf('\\') !== -1) filepath = filepath.replace(backslashRegEx, '%5C');
if (filepath.indexOf('\n') !== -1) filepath = filepath.replace(newlineRegEx, '%0A');
if (filepath.indexOf('\r') !== -1) filepath = filepath.replace(carriageReturnRegEx, '%0D');
if (filepath.indexOf('\t') !== -1) filepath = filepath.replace(tabRegEx, '%09');
if (filepath.indexOf(' ') !== -1) filepath = filepath.replace(spaceRegEx, '%20');
return filepath;
}
export function encodeURLChars(path: string) {
let resolved = resolve(path);
// path.resolve strips trailing slashes so we must add them back
const filePathLast = path.charAt(path.length - 1);
if (filePathLast === '/' && resolved[resolved.length - 1] !== sep) resolved += '/';
// Call encodePathChars first to avoid encoding % again for ? and #.
resolved = encodePathChars(resolved);
// Question and hash character should be included in pathname.
// Therefore, encoding is required to eliminate parsing them in different states.
// This is done as an optimization to not creating a URL instance and
// later triggering pathname setter, which impacts performance
if (resolved.indexOf('?') !== -1) resolved = resolved.replace(questionRegex, '%3F');
if (resolved.indexOf('#') !== -1) resolved = resolved.replace(hashRegex, '%23');
return resolved;
}
export function isUrl(url: string) {
try {
return !!new URL(url);
} catch (error) {
return false;
}
}
export function asUrl(url: string | URL) {
try {
const newUrl = new URL(url);
newUrl.hash = '';
return newUrl;
} catch (error) {
return null;
}
}