/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {Wakeable, Thenable} from 'shared/ReactTypes'; import {unstable_getCacheForType} from 'react'; import * as fs from 'fs/promises'; import {isAbsolute, normalize} from 'path'; const Pending = 0; const Resolved = 1; const Rejected = 2; type PendingRecord = {| status: 0, value: Wakeable, cache: null, |}; type ResolvedRecord = {| status: 1, value: T, cache: null | Array, |}; type RejectedRecord = {| status: 2, value: mixed, cache: null, |}; type Record = PendingRecord | ResolvedRecord | RejectedRecord; function createRecordFromThenable(thenable: Thenable): Record { const record: Record = { status: Pending, value: thenable, cache: null, }; thenable.then( value => { if (record.status === Pending) { const resolvedRecord = ((record: any): ResolvedRecord); resolvedRecord.status = Resolved; resolvedRecord.value = value; } }, err => { if (record.status === Pending) { const rejectedRecord = ((record: any): RejectedRecord); rejectedRecord.status = Rejected; rejectedRecord.value = err; } }, ); return record; } function readRecord(record: Record): ResolvedRecord { if (record.status === Resolved) { // This is just a type refinement. return record; } else { throw record.value; } } // We don't want to normalize every path ourselves in production. // However, relative or non-normalized paths will lead to cache misses. // So we encourage the developer to fix it in DEV and normalize on their end. function checkPathInDev(path: string) { if (__DEV__) { if (!isAbsolute(path)) { console.error( 'The provided path was not absolute: "%s". ' + 'Convert it to an absolute path first.', path, ); } else if (path !== normalize(path)) { console.error( 'The provided path was not normalized: "%s". ' + 'Convert it to a normalized path first.', path, ); } } } function createAccessMap(): Map>> { return new Map(); } export function access(path: string, mode?: number): void { checkPathInDev(path); if (mode == null) { mode = 0; // fs.constants.F_OK } const map = unstable_getCacheForType(createAccessMap); let accessCache = map.get(path); if (!accessCache) { accessCache = []; map.set(path, accessCache); } let record; for (let i = 0; i < accessCache.length; i += 2) { const cachedMode: number = (accessCache[i]: any); if (mode === cachedMode) { const cachedRecord: Record = (accessCache[i + 1]: any); record = cachedRecord; break; } } if (!record) { const thenable = fs.access(path, mode); record = createRecordFromThenable(thenable); accessCache.push(mode, record); } readRecord(record); // No return value. } function createLstatMap(): Map>> { return new Map(); } export function lstat(path: string, options?: {bigint?: boolean}): mixed { checkPathInDev(path); let bigint = false; if (options && options.bigint) { bigint = true; } const map = unstable_getCacheForType(createLstatMap); let lstatCache = map.get(path); if (!lstatCache) { lstatCache = []; map.set(path, lstatCache); } let record; for (let i = 0; i < lstatCache.length; i += 2) { const cachedBigint: boolean = (lstatCache[i]: any); if (bigint === cachedBigint) { const cachedRecord: Record = (lstatCache[i + 1]: any); record = cachedRecord; break; } } if (!record) { const thenable = fs.lstat(path, {bigint}); record = createRecordFromThenable(thenable); lstatCache.push(bigint, record); } const stats = readRecord(record).value; return stats; } function createReaddirMap(): Map< string, Array>, > { return new Map(); } export function readdir( path: string, options?: string | {encoding?: string, withFileTypes?: boolean}, ): mixed { checkPathInDev(path); let encoding = 'utf8'; let withFileTypes = false; if (typeof options === 'string') { encoding = options; } else if (options != null) { if (options.encoding) { encoding = options.encoding; } if (options.withFileTypes) { withFileTypes = true; } } const map = unstable_getCacheForType(createReaddirMap); let readdirCache = map.get(path); if (!readdirCache) { readdirCache = []; map.set(path, readdirCache); } let record; for (let i = 0; i < readdirCache.length; i += 3) { const cachedEncoding: string = (readdirCache[i]: any); const cachedWithFileTypes: boolean = (readdirCache[i + 1]: any); if (encoding === cachedEncoding && withFileTypes === cachedWithFileTypes) { const cachedRecord: Record = (readdirCache[i + 2]: any); record = cachedRecord; break; } } if (!record) { const thenable = fs.readdir(path, {encoding, withFileTypes}); record = createRecordFromThenable(thenable); readdirCache.push(encoding, withFileTypes, record); } const files = readRecord(record).value; return files; } function createReadFileMap(): Map> { return new Map(); } export function readFile( path: string, options: | string | { encoding?: string | null, // Unsupported: flag?: string, // Doesn't make sense except "r" signal?: mixed, // We'll have our own signal }, ): string | Buffer { checkPathInDev(path); const map = unstable_getCacheForType(createReadFileMap); let record = map.get(path); if (!record) { const thenable = fs.readFile(path); record = createRecordFromThenable(thenable); map.set(path, record); } const resolvedRecord = readRecord(record); const buffer: Buffer = resolvedRecord.value; if (!options) { return buffer; } let encoding; if (typeof options === 'string') { encoding = options; } else { const flag = options.flag; if (flag != null && flag !== 'r') { throw Error( 'The flag option is not supported, and always defaults to "r".', ); } if (options.signal) { throw Error('The signal option is not supported.'); } encoding = options.encoding; } if (typeof encoding !== 'string') { return buffer; } const textCache = resolvedRecord.cache || (resolvedRecord.cache = []); for (let i = 0; i < textCache.length; i += 2) { if (textCache[i] === encoding) { return (textCache[i + 1]: any); } } const text = buffer.toString((encoding: any)); textCache.push(encoding, text); return text; } function createReadlinkMap(): Map>> { return new Map(); } export function readlink( path: string, options?: string | {encoding?: string}, ): mixed { checkPathInDev(path); let encoding = 'utf8'; if (typeof options === 'string') { encoding = options; } else if (options != null) { if (options.encoding) { encoding = options.encoding; } } const map = unstable_getCacheForType(createReadlinkMap); let readlinkCache = map.get(path); if (!readlinkCache) { readlinkCache = []; map.set(path, readlinkCache); } let record; for (let i = 0; i < readlinkCache.length; i += 2) { const cachedEncoding: string = (readlinkCache[i]: any); if (encoding === cachedEncoding) { const cachedRecord: Record = (readlinkCache[i + 1]: any); record = cachedRecord; break; } } if (!record) { const thenable = fs.readlink(path, {encoding}); record = createRecordFromThenable(thenable); readlinkCache.push(encoding, record); } const linkString = readRecord(record).value; return linkString; } function createRealpathMap(): Map>> { return new Map(); } export function realpath( path: string, options?: string | {encoding?: string}, ): mixed { checkPathInDev(path); let encoding = 'utf8'; if (typeof options === 'string') { encoding = options; } else if (options != null) { if (options.encoding) { encoding = options.encoding; } } const map = unstable_getCacheForType(createRealpathMap); let realpathCache = map.get(path); if (!realpathCache) { realpathCache = []; map.set(path, realpathCache); } let record; for (let i = 0; i < realpathCache.length; i += 2) { const cachedEncoding: string = (realpathCache[i]: any); if (encoding === cachedEncoding) { const cachedRecord: Record = (realpathCache[i + 1]: any); record = cachedRecord; break; } } if (!record) { const thenable = fs.realpath(path, {encoding}); record = createRecordFromThenable(thenable); realpathCache.push(encoding, record); } const resolvedPath = readRecord(record).value; return resolvedPath; } function createStatMap(): Map>> { return new Map(); } export function stat(path: string, options?: {bigint?: boolean}): mixed { checkPathInDev(path); let bigint = false; if (options && options.bigint) { bigint = true; } const map = unstable_getCacheForType(createStatMap); let statCache = map.get(path); if (!statCache) { statCache = []; map.set(path, statCache); } let record; for (let i = 0; i < statCache.length; i += 2) { const cachedBigint: boolean = (statCache[i]: any); if (bigint === cachedBigint) { const cachedRecord: Record = (statCache[i + 1]: any); record = cachedRecord; break; } } if (!record) { const thenable = fs.stat(path, {bigint}); record = createRecordFromThenable(thenable); statCache.push(bigint, record); } const stats = readRecord(record).value; return stats; }