From 03126dd087b285789447ab814ee7f3385e1c179d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 9 Dec 2020 21:46:50 +0000 Subject: [PATCH] [Flight] Add read-only fs methods (#20412) * Don't allocate the inner cache unnecessarily We only need it when we're asking for text. I anticipate I'll want to avoid allocating it in other methods too when it's not strictly necessary. * Add fs.access * Add fs.lstat * Add fs.stat * Add fs.readdir * Add fs.readlink * Add fs.realpath * Rename functions to disambiguate two caches --- packages/react-fs/src/ReactFilesystem.js | 253 ++++++++++++++++++++++- scripts/flow/environment.js | 34 +++ scripts/rollup/modules.js | 1 + 3 files changed, 278 insertions(+), 10 deletions(-) diff --git a/packages/react-fs/src/ReactFilesystem.js b/packages/react-fs/src/ReactFilesystem.js index f13c6ce124760..e505005e21db2 100644 --- a/packages/react-fs/src/ReactFilesystem.js +++ b/packages/react-fs/src/ReactFilesystem.js @@ -20,19 +20,19 @@ const Rejected = 2; type PendingRecord = {| status: 0, value: Wakeable, - cache: Array, + cache: null, |}; type ResolvedRecord = {| status: 1, value: T, - cache: Array, + cache: null | Array, |}; type RejectedRecord = {| status: 2, value: mixed, - cache: Array, + cache: null, |}; type Record = PendingRecord | ResolvedRecord | RejectedRecord; @@ -41,7 +41,7 @@ function createRecordFromThenable(thenable: Thenable): Record { const record: Record = { status: Pending, value: thenable, - cache: [], + cache: null, }; thenable.then( value => { @@ -62,9 +62,10 @@ function createRecordFromThenable(thenable: Thenable): Record { return record; } -function readRecordValue(record: Record): T { +function readRecord(record: Record): ResolvedRecord { if (record.status === Resolved) { - return record.value; + // This is just a type refinement. + return record; } else { throw record.value; } @@ -91,7 +92,122 @@ function checkPathInDev(path: string) { } } -function createReadFileCache(): Map> { +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(); } @@ -106,15 +222,16 @@ export function readFile( signal?: mixed, // We'll have our own signal }, ): string | Buffer { - const map = unstable_getCacheForType(createReadFileCache); 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 buffer: Buffer = readRecordValue(record); + const resolvedRecord = readRecord(record); + const buffer: Buffer = resolvedRecord.value; if (!options) { return buffer; } @@ -136,7 +253,7 @@ export function readFile( if (typeof encoding !== 'string') { return buffer; } - const textCache = record.cache; + const textCache = resolvedRecord.cache || (resolvedRecord.cache = []); for (let i = 0; i < textCache.length; i += 2) { if (textCache[i] === encoding) { return (textCache[i + 1]: any); @@ -146,3 +263,119 @@ export function readFile( 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; +} diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 0fac49adfb211..86b19777cd31f 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -71,6 +71,20 @@ declare function __webpack_chunk_load__(id: string): Promise; declare function __webpack_require__(id: string): any; declare module 'fs/promises' { + declare var access: (path: string, mode?: number) => Promise; + declare var lstat: ( + path: string, + options?: ?{bigint?: boolean}, + ) => Promise; + declare var readdir: ( + path: string, + options?: + | ?string + | { + encoding?: ?string, + withFileTypes?: ?boolean, + }, + ) => Promise; declare var readFile: ( path: string, options?: @@ -79,6 +93,26 @@ declare module 'fs/promises' { encoding?: ?string, }, ) => Promise; + declare var readlink: ( + path: string, + options?: + | ?string + | { + encoding?: ?string, + }, + ) => Promise; + declare var realpath: ( + path: string, + options?: + | ?string + | { + encoding?: ?string, + }, + ) => Promise; + declare var stat: ( + path: string, + options?: ?{bigint?: boolean}, + ) => Promise; } declare module 'pg' { declare var Pool: ( diff --git a/scripts/rollup/modules.js b/scripts/rollup/modules.js index 226ea5be5b77c..80c0a6c2cddbe 100644 --- a/scripts/rollup/modules.js +++ b/scripts/rollup/modules.js @@ -10,6 +10,7 @@ const HAS_NO_SIDE_EFFECTS_ON_IMPORT = false; // const HAS_SIDE_EFFECTS_ON_IMPORT = true; const importSideEffects = Object.freeze({ fs: HAS_NO_SIDE_EFFECTS_ON_IMPORT, + 'fs/promises': HAS_NO_SIDE_EFFECTS_ON_IMPORT, path: HAS_NO_SIDE_EFFECTS_ON_IMPORT, 'prop-types/checkPropTypes': HAS_NO_SIDE_EFFECTS_ON_IMPORT, 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': HAS_NO_SIDE_EFFECTS_ON_IMPORT,