Files
paseo/vendor/react-native-gpui/modules/fs/src/index.ts
T

770 lines
20 KiB
TypeScript
Vendored

/**
* `@react-native-gpui/fs`: a Node-shaped file API for the desktop app.
*
* The native module underneath is one TurboModule on its own thread with
* `ArrayBuffer` arguments and plain-object results. Everything Node adds on top
* of that — encodings, `Stats` and `Dirent` instances, callback forms, the
* `fs/promises` object — lives in this package's JS layer, so the bridge stays a
* byte mover and the API stays recognisably Node's.
*
* The default export is the `node:fs` object; `dialog`, `paths` and `watch` are
* named exports because they have no `fs` counterpart to hang off.
*/
import { constants } from './constants';
import NativeFs from './NativeFs';
import {
Dirent,
FileHandle,
Stats,
promises,
readFileImpl,
type MkdirOptions,
type OpenOptions,
type RmOptions,
type WriteOptions,
} from './promises';
import {
decodeString,
flagFrom,
missingPath,
modeFrom,
normalizeEncoding,
relativeEntryName,
toArrayBuffer,
toPath,
withContext,
type BinaryData,
type Encoding,
type NodeError,
} from './shared';
import { watch } from './watch';
export type {
CpOptions,
MkdirOptions,
OpenOptions,
ReaddirOptions,
RmOptions,
TimeLike,
WriteOptions,
} from './promises';
export type { BinaryData, Encoding, NodeError } from './shared';
export type {
FSWatcher,
WatchEventType,
WatchListener,
WatchOptions,
} from './watch';
export { Dirent, Stats, FileHandle, promises, constants, watch };
/* -------------------------------------------------------------------------- */
/* Dialogs */
/* -------------------------------------------------------------------------- */
export interface DialogFilter {
name: string;
extensions: Array<string>;
}
export interface OpenDialogOptions {
title?: string;
defaultPath?: string;
multiple?: boolean;
directory?: boolean;
filters?: Array<DialogFilter>;
}
export interface SaveDialogOptions {
title?: string;
defaultPath?: string;
defaultName?: string;
filters?: Array<DialogFilter>;
}
/**
* The native pickers.
*
* Cancelling is not an error: `open` resolves with `[]` and `save` with `null`,
* which is what the design specifies and what libraries pairing the two expect.
*/
export const dialog = {
async open(options: OpenDialogOptions = {}): Promise<string[]> {
return NativeFs.showOpenDialog({
title: options.title,
defaultPath:
options.defaultPath === undefined
? undefined
: toPath(options.defaultPath),
multiple: options.multiple ?? false,
directory: options.directory ?? false,
filters: options.filters ?? [],
});
},
async save(options: SaveDialogOptions = {}): Promise<string | null> {
return NativeFs.showSaveDialog({
title: options.title,
defaultPath:
options.defaultPath === undefined
? undefined
: toPath(options.defaultPath),
defaultName: options.defaultName,
filters: options.filters ?? [],
});
},
};
/* -------------------------------------------------------------------------- */
/* Known paths */
/* -------------------------------------------------------------------------- */
/** The well-known directories, as the native side reports them. */
export interface KnownPaths {
home: string;
documents: string;
downloads: string;
desktop: string;
temp: string;
appData: string;
appCache: string;
appLogs: string;
}
const KNOWN_PATH_NAMES = [
'home',
'documents',
'downloads',
'desktop',
'temp',
'appData',
'appCache',
'appLogs',
] as const;
let cachedPaths: KnownPaths | null = null;
/**
* Reads the known paths once and remembers them.
*
* Lazy rather than at import time so that importing this package cannot fail: the
* native module is only asked when an app actually names a directory, and the
* native side creates the per-app folders on that first read.
*/
function knownPaths(): KnownPaths {
if (cachedPaths === null) {
cachedPaths = NativeFs.getPaths();
}
return cachedPaths;
}
/**
* `paths.home`, `paths.documents` and the rest.
*
* Each property is a getter over one cached native read, so destructuring works
* and the object can be exported before the module has been asked anything.
*/
export const paths: KnownPaths = Object.defineProperties(
{},
Object.fromEntries(
KNOWN_PATH_NAMES.map(name => [
name,
{ enumerable: true, get: () => knownPaths()[name] },
]),
),
) as KnownPaths;
/* -------------------------------------------------------------------------- */
/* Sync forms */
/* -------------------------------------------------------------------------- */
function readFileSync(path: string): Uint8Array;
function readFileSync(
path: string,
options: Encoding | { encoding: Encoding },
): string;
function readFileSync(
path: string,
options?: Encoding | { encoding?: Encoding | null } | null,
): Uint8Array | string {
const target = toPath(path);
const encoding =
typeof options === 'string' ? options : (options?.encoding ?? null);
try {
if (encoding === null) {
return new Uint8Array(NativeFs.readFileSync(target));
}
const normalized = normalizeEncoding(encoding) ?? 'utf8';
if (normalized === 'utf8') {
// The dedicated text method decodes in Rust instead of handing JS bytes
// to decode a second time.
return NativeFs.readTextFileSync(target);
}
return decodeString(
new Uint8Array(NativeFs.readFileSync(target)),
normalized,
);
} catch (error) {
throw withContext(error, 'open', target);
}
}
function writeFileSync(
path: string,
data: BinaryData,
options?: WriteOptions | Encoding,
): void {
const target = toPath(path);
const opts: WriteOptions =
typeof options === 'string' ? { encoding: options } : (options ?? {});
const flag = flagFrom(
opts.flag,
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC,
);
const mode = modeFrom(opts.mode);
try {
if (typeof data === 'string') {
const encoding = normalizeEncoding(opts.encoding ?? 'utf8') ?? 'utf8';
if (encoding === 'utf8') {
NativeFs.writeTextFileSync(target, data, flag, mode);
return;
}
NativeFs.writeFileSync(target, toArrayBuffer(data, encoding), flag, mode);
return;
}
NativeFs.writeFileSync(target, toArrayBuffer(data), flag, mode);
} catch (error) {
throw withContext(error, 'open', target);
}
}
function appendFileSync(
path: string,
data: BinaryData,
options?: WriteOptions | Encoding,
): void {
const opts: WriteOptions =
typeof options === 'string' ? { encoding: options } : (options ?? {});
writeFileSync(path, data, {
...opts,
flag:
opts.flag ?? constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND,
});
}
function existsSync(path: string): boolean {
try {
return NativeFs.existsSync(toPath(path));
} catch {
// `existsSync` answers a question rather than reporting a failure, so every
// error — a permission problem as much as a missing path — is a `false`.
return false;
}
}
function statSync(path: string): Stats {
const target = toPath(path);
try {
const result = NativeFs.statSync(target, true);
if (result === null) {
throw missingPath(target, 'stat');
}
return new Stats(result);
} catch (error) {
throw withContext(error, 'stat', target);
}
}
function lstatSync(path: string): Stats {
const target = toPath(path);
try {
const result = NativeFs.statSync(target, false);
if (result === null) {
throw missingPath(target, 'lstat');
}
return new Stats(result);
} catch (error) {
throw withContext(error, 'lstat', target);
}
}
function readdirSync(
path: string,
options?: { withFileTypes?: false; recursive?: boolean } | null,
): string[];
function readdirSync(
path: string,
options: { withFileTypes: true; recursive?: boolean },
): Dirent[];
function readdirSync(
path: string,
options?: { withFileTypes?: boolean; recursive?: boolean } | null,
): string[] | Dirent[] {
const target = toPath(path);
try {
const recursive = options?.recursive ?? false;
const entries = NativeFs.readdirSync(target, recursive);
return options?.withFileTypes
? entries.map(entry => new Dirent(entry))
: entries.map(entry =>
recursive
? relativeEntryName(target, entry.parentPath, entry.name)
: entry.name,
);
} catch (error) {
throw withContext(error, 'scandir', target);
}
}
function mkdirSync(path: string, options?: MkdirOptions | number): void {
const target = toPath(path);
const opts: MkdirOptions =
typeof options === 'number' ? { mode: options } : (options ?? {});
try {
NativeFs.mkdirSync(
target,
opts.recursive ?? false,
modeFrom(opts.mode, 0o777),
);
} catch (error) {
throw withContext(error, 'mkdir', target);
}
}
function rmSync(path: string, options?: RmOptions): void {
const target = toPath(path);
try {
NativeFs.rmSync(
target,
options?.recursive ?? false,
options?.force ?? false,
);
} catch (error) {
throw withContext(error, 'rm', target);
}
}
function renameSync(from: string, to: string): void {
const source = toPath(from);
const destination = toPath(to);
try {
NativeFs.renameSync(source, destination);
} catch (error) {
const wrapped = withContext(error, 'rename', source);
// Node's rename error names both ends; the second is `dest`, not `path`.
if (wrapped.dest === undefined) {
wrapped.dest = destination;
}
throw wrapped;
}
}
function copyFileSync(from: string, to: string): void {
const source = toPath(from);
const destination = toPath(to);
try {
NativeFs.copyFileSync(source, destination);
} catch (error) {
const wrapped = withContext(error, 'copyfile', source);
if (wrapped.dest === undefined) {
wrapped.dest = destination;
}
throw wrapped;
}
}
/* -------------------------------------------------------------------------- */
/* Callback forms */
/* -------------------------------------------------------------------------- */
/**
* The callback forms are wrappers over the promise implementations, as the
* design says, so there is one implementation of each operation.
*
* The trailing argument is the callback in every form; the optional argument
* before it is an options object (or an encoding, or a mode) when it is not a
* function. That is the whole of Node's overloading convention here.
*/
type NodeCallback<T> = (error: NodeError | null, value?: T) => void;
function callbackOf<T>(args: unknown[]): NodeCallback<T> {
const callback = args[args.length - 1];
if (typeof callback !== 'function') {
throw new TypeError('A callback function is required');
}
return callback as NodeCallback<T>;
}
/** The options argument, if the caller supplied one rather than the callback. */
function before<T>(args: unknown[], index: number, fallback: T): T {
const value = args[index];
return value === undefined || typeof value === 'function'
? fallback
: (value as T);
}
/**
* Reports a settled promise the Node way.
*
* A rejection that has a callback is delivered to it; nothing is thrown, because
* a throw from here would escape the caller's stack rather than the callback.
*/
function settle<T>(promise: Promise<T>, callback: NodeCallback<T>): void {
promise.then(
value => callback(null, value),
(error: NodeError) => callback(error),
);
}
function readFile(
path: string,
callback: NodeCallback<Uint8Array | string>,
): void;
function readFile(
path: string,
options: Encoding | { encoding?: Encoding | null } | null,
callback: NodeCallback<Uint8Array | string>,
): void;
function readFile(path: string, ...args: unknown[]): void {
// The promise overloads keep the result precise; the callback form goes
// through the coarse alias because at that point the encoding is a runtime
// value and either shape is possible.
const options = before(args, 0, undefined) as
Encoding | { encoding?: Encoding | null } | null | undefined;
settle(readFileImpl(path, options), callbackOf(args));
}
function writeFile(
path: string,
data: BinaryData,
callback: NodeCallback<void>,
): void;
function writeFile(
path: string,
data: BinaryData,
options: WriteOptions | Encoding | null,
callback: NodeCallback<void>,
): void;
function writeFile(path: string, data: BinaryData, ...args: unknown[]): void {
settle(
promises.writeFile(
path,
data,
before(args, 0, undefined) as WriteOptions | Encoding | undefined,
),
callbackOf(args),
);
}
function appendFile(
path: string,
data: BinaryData,
callback: NodeCallback<void>,
): void;
function appendFile(
path: string,
data: BinaryData,
options: WriteOptions | Encoding | null,
callback: NodeCallback<void>,
): void;
function appendFile(path: string, data: BinaryData, ...args: unknown[]): void {
settle(
promises.appendFile(
path,
data,
before(args, 0, undefined) as WriteOptions | Encoding | undefined,
),
callbackOf(args),
);
}
function readdir(
path: string,
callback: NodeCallback<string[] | Dirent[]>,
): void;
function readdir(
path: string,
options: { withFileTypes?: boolean; recursive?: boolean } | null,
callback: NodeCallback<string[] | Dirent[]>,
): void;
function readdir(path: string, ...args: unknown[]): void {
const options = before(args, 0, null) as {
withFileTypes?: boolean;
recursive?: boolean;
} | null;
const pending: Promise<string[] | Dirent[]> = options?.withFileTypes
? promises.readdir(path, { ...options, withFileTypes: true })
: promises.readdir(path, {
recursive: options?.recursive,
withFileTypes: false,
});
settle(pending, callbackOf(args));
}
function stat(path: string, callback: NodeCallback<Stats>): void;
function stat(path: string, ...args: unknown[]): void {
settle(promises.stat(path), callbackOf([...args]));
}
function lstat(path: string, callback: NodeCallback<Stats>): void;
function lstat(path: string, ...args: unknown[]): void {
settle(promises.lstat(path), callbackOf([...args]));
}
function mkdir(path: string, callback: NodeCallback<string | undefined>): void;
function mkdir(
path: string,
options: MkdirOptions | number | null,
callback: NodeCallback<string | undefined>,
): void;
function mkdir(path: string, ...args: unknown[]): void {
settle(
promises.mkdir(
path,
before(args, 0, undefined) as MkdirOptions | number | undefined,
),
callbackOf(args),
);
}
function rm(path: string, callback: NodeCallback<void>): void;
function rm(
path: string,
options: RmOptions | null,
callback: NodeCallback<void>,
): void;
function rm(path: string, ...args: unknown[]): void {
settle(
promises.rm(path, before(args, 0, undefined) as RmOptions | undefined),
callbackOf(args),
);
}
function rename(from: string, to: string, callback: NodeCallback<void>): void;
function rename(from: string, to: string, ...args: unknown[]): void {
settle(promises.rename(from, to), callbackOf([...args]));
}
function copyFile(from: string, to: string, callback: NodeCallback<void>): void;
function copyFile(
from: string,
to: string,
mode: number,
callback: NodeCallback<void>,
): void;
function copyFile(from: string, to: string, ...args: unknown[]): void {
settle(
promises.copyFile(
from,
to,
before(args, 0, undefined) as number | undefined,
),
callbackOf(args),
);
}
function cp(from: string, to: string, callback: NodeCallback<void>): void;
function cp(
from: string,
to: string,
options: { recursive?: boolean } | null,
callback: NodeCallback<void>,
): void;
function cp(from: string, to: string, ...args: unknown[]): void {
settle(
promises.cp(
from,
to,
before(args, 0, undefined) as { recursive?: boolean } | undefined,
),
callbackOf(args),
);
}
function access(path: string, callback: NodeCallback<void>): void;
function access(path: string, mode: number, callback: NodeCallback<void>): void;
function access(path: string, ...args: unknown[]): void {
settle(
promises.access(path, before(args, 0, undefined) as number | undefined),
callbackOf(args),
);
}
function realpath(path: string, callback: NodeCallback<string>): void;
function realpath(path: string, ...args: unknown[]): void {
settle(promises.realpath(path), callbackOf([...args]));
}
function readlink(path: string, callback: NodeCallback<string>): void;
function readlink(path: string, ...args: unknown[]): void {
settle(promises.readlink(path), callbackOf([...args]));
}
function symlink(
target: string,
path: string,
callback: NodeCallback<void>,
): void;
function symlink(
target: string,
path: string,
type: string,
callback: NodeCallback<void>,
): void;
function symlink(target: string, path: string, ...args: unknown[]): void {
settle(
promises.symlink(
target,
path,
before(args, 0, undefined) as 'file' | 'dir' | 'junction' | undefined,
),
callbackOf(args),
);
}
function truncate(path: string, callback: NodeCallback<void>): void;
function truncate(
path: string,
length: number,
callback: NodeCallback<void>,
): void;
function truncate(path: string, ...args: unknown[]): void {
settle(
promises.truncate(path, before(args, 0, undefined) as number | undefined),
callbackOf(args),
);
}
function utimes(
path: string,
atime: Date | number | string,
mtime: Date | number | string,
callback: NodeCallback<void>,
): void;
function utimes(
path: string,
atime: Date | number | string,
mtime: Date | number | string,
...args: unknown[]
): void {
settle(promises.utimes(path, atime, mtime), callbackOf([...args]));
}
function chmod(
path: string,
mode: number | string,
callback: NodeCallback<void>,
): void;
function chmod(path: string, mode: number | string, ...args: unknown[]): void {
settle(promises.chmod(path, mode), callbackOf([...args]));
}
function mkdtemp(prefix: string, callback: NodeCallback<string>): void;
function mkdtemp(
prefix: string,
options: Encoding | { encoding?: Encoding },
callback: NodeCallback<string>,
): void;
function mkdtemp(prefix: string, ...args: unknown[]): void {
settle(
promises.mkdtemp(
prefix,
before(args, 0, undefined) as
Encoding | { encoding?: Encoding } | undefined,
),
callbackOf(args),
);
}
function open(path: string, callback: NodeCallback<FileHandle>): void;
function open(
path: string,
flags: string | number | OpenOptions,
callback: NodeCallback<FileHandle>,
): void;
function open(
path: string,
flags: string | number | OpenOptions,
mode: number | string,
callback: NodeCallback<FileHandle>,
): void;
function open(path: string, ...args: unknown[]): void {
const flags = before(args, 0, undefined) as
string | number | OpenOptions | undefined;
const mode = before(args, 1, undefined) as number | string | undefined;
settle(
mode === undefined
? promises.open(path, flags)
: promises.open(path, flags, mode),
callbackOf(args),
);
}
function reveal(path: string, callback: NodeCallback<void>): void;
function reveal(path: string, ...args: unknown[]): void {
settle(promises.reveal(path), callbackOf([...args]));
}
function openExternal(path: string, callback: NodeCallback<void>): void;
function openExternal(path: string, ...args: unknown[]): void {
settle(promises.openExternal(path), callbackOf([...args]));
}
/* -------------------------------------------------------------------------- */
/* The `fs` object */
/* -------------------------------------------------------------------------- */
/**
* Node's `fs`, as far as this platform implements it.
*
* The callback forms wrap the promise implementations; the sync forms call the
* bridge's JS-thread methods directly; `promises` is the same object shape Node
* exposes as `fs.promises`.
*/
const fs = {
constants,
promises,
readFile,
writeFile,
appendFile,
readdir,
stat,
lstat,
mkdir,
rm,
rename,
copyFile,
cp,
access,
realpath,
readlink,
symlink,
truncate,
utimes,
chmod,
mkdtemp,
open,
reveal,
openExternal,
readFileSync,
writeFileSync,
appendFileSync,
existsSync,
statSync,
lstatSync,
readdirSync,
mkdirSync,
rmSync,
renameSync,
copyFileSync,
Stats,
Dirent,
FileHandle,
};
export default fs;