diff --git a/packages/react-fs/README.md b/packages/react-fs/README.md new file mode 100644 index 0000000000000..cca1192f52c45 --- /dev/null +++ b/packages/react-fs/README.md @@ -0,0 +1,12 @@ +# react-fs + +This package is meant to be used alongside yet-to-be-released, experimental React features. It's unlikely to be useful in any other context. + +**Do not use in a real application.** We're publishing this early for +demonstration purposes. + +**Use it at your own risk.** + +# No, Really, It Is Unstable + +The API ~~may~~ will change wildly between versions. diff --git a/packages/react-fs/index.browser.js b/packages/react-fs/index.browser.js new file mode 100644 index 0000000000000..444c63ec765ff --- /dev/null +++ b/packages/react-fs/index.browser.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +throw new Error( + 'This entry point is not yet supported in the browser environment', +); diff --git a/packages/react-fs/index.js b/packages/react-fs/index.js new file mode 100644 index 0000000000000..ceb2071c4f055 --- /dev/null +++ b/packages/react-fs/index.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +'use strict'; + +export * from './index.node'; diff --git a/packages/react-fs/index.node.js b/packages/react-fs/index.node.js new file mode 100644 index 0000000000000..ea6d021f8d2d8 --- /dev/null +++ b/packages/react-fs/index.node.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +'use strict'; + +export * from './src/ReactFilesystem'; diff --git a/packages/react-fs/npm/index.browser.js b/packages/react-fs/npm/index.browser.js new file mode 100644 index 0000000000000..d08a7383d699f --- /dev/null +++ b/packages/react-fs/npm/index.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-fs.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-fs.browser.development.js'); +} diff --git a/packages/react-fs/npm/index.js b/packages/react-fs/npm/index.js new file mode 100644 index 0000000000000..ee510df2ad686 --- /dev/null +++ b/packages/react-fs/npm/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./index.node'); diff --git a/packages/react-fs/npm/index.node.js b/packages/react-fs/npm/index.node.js new file mode 100644 index 0000000000000..fa1d5b986e658 --- /dev/null +++ b/packages/react-fs/npm/index.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-fs.node.production.min.js'); +} else { + module.exports = require('./cjs/react-fs.node.development.js'); +} diff --git a/packages/react-fs/package.json b/packages/react-fs/package.json new file mode 100644 index 0000000000000..d3f9586725e0a --- /dev/null +++ b/packages/react-fs/package.json @@ -0,0 +1,26 @@ +{ + "private": true, + "name": "react-fs", + "description": "React bindings for the filesystem", + "version": "0.0.0", + "repository": { + "type" : "git", + "url" : "https://github.com/facebook/react.git", + "directory": "packages/react-fs" + }, + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "index.node.js", + "index.browser.js", + "cjs/" + ], + "peerDependencies": { + "react": "^17.0.0" + }, + "browser": { + "./index.js": "./index.browser.js" + } +} diff --git a/packages/react-fs/src/ReactFilesystem.js b/packages/react-fs/src/ReactFilesystem.js new file mode 100644 index 0000000000000..cfc74bdfd219b --- /dev/null +++ b/packages/react-fs/src/ReactFilesystem.js @@ -0,0 +1,148 @@ +/** + * 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 PendingResult = {| + status: 0, + value: Wakeable, + cache: Array, +|}; + +type ResolvedResult = {| + status: 1, + value: T, + cache: Array, +|}; + +type RejectedResult = {| + status: 2, + value: mixed, + cache: Array, +|}; + +type Result = PendingResult | ResolvedResult | RejectedResult; + +function toResult(thenable: Thenable): Result { + const result: Result = { + status: Pending, + value: thenable, + cache: [], + }; + thenable.then( + value => { + if (result.status === Pending) { + const resolvedResult = ((result: any): ResolvedResult); + resolvedResult.status = Resolved; + resolvedResult.value = value; + } + }, + err => { + if (result.status === Pending) { + const rejectedResult = ((result: any): RejectedResult); + rejectedResult.status = Rejected; + rejectedResult.value = err; + } + }, + ); + return result; +} + +function readResult(result: Result): T { + if (result.status === Resolved) { + return result.value; + } else { + throw result.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 createReadFileCache(): 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 { + const map = unstable_getCacheForType(createReadFileCache); + checkPathInDev(path); + let entry = map.get(path); + if (!entry) { + const thenable = fs.readFile(path); + entry = toResult(thenable); + map.set(path, entry); + } + const result: Buffer = readResult(entry); + if (!options) { + return result; + } + 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 result; + } + const textCache = entry.cache; + for (let i = 0; i < textCache.length; i += 2) { + if (textCache[i] === encoding) { + return (textCache[i + 1]: any); + } + } + const text = result.toString((encoding: any)); + textCache.push(encoding, text); + return text; +} diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 65d7c81da974b..0fac49adfb211 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -70,6 +70,16 @@ declare module 'EventListener' { declare function __webpack_chunk_load__(id: string): Promise; declare function __webpack_require__(id: string): any; +declare module 'fs/promises' { + declare var readFile: ( + path: string, + options?: + | ?string + | { + encoding?: ?string, + }, + ) => Promise; +} declare module 'pg' { declare var Pool: ( options: mixed, diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index e6ef2e59bec9f..e97f7aa8d3067 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -153,6 +153,24 @@ const bundles = [ externals: ['react', 'http', 'https'], }, + /******* React FS Browser (experimental, new) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-fs/index.browser', + global: 'ReactFilesystem', + externals: [], + }, + + /******* React FS Node (experimental, new) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-fs/index.node', + global: 'ReactFilesystem', + externals: ['react', 'fs/promises', 'path'], + }, + /******* React PG Browser (experimental, new) *******/ { bundleTypes: [NODE_DEV, NODE_PROD],