From f85a3cf646a7b9ade79d2b7e1a616553b1bc75a1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 8 Dec 2020 23:08:50 +0000 Subject: [PATCH 1/5] [Flight] Add rudimentary FS binding --- packages/react-fs/README.md | 12 +++ packages/react-fs/index.browser.js | 12 +++ packages/react-fs/index.js | 12 +++ packages/react-fs/index.node.js | 12 +++ packages/react-fs/npm/index.browser.js | 7 ++ packages/react-fs/npm/index.js | 3 + packages/react-fs/npm/index.node.js | 7 ++ packages/react-fs/package.json | 23 +++++ packages/react-fs/src/ReactFilesystem.js | 108 +++++++++++++++++++++++ scripts/flow/environment.js | 10 +++ scripts/rollup/bundles.js | 18 ++++ 11 files changed, 224 insertions(+) create mode 100644 packages/react-fs/README.md create mode 100644 packages/react-fs/index.browser.js create mode 100644 packages/react-fs/index.js create mode 100644 packages/react-fs/index.node.js create mode 100644 packages/react-fs/npm/index.browser.js create mode 100644 packages/react-fs/npm/index.js create mode 100644 packages/react-fs/npm/index.node.js create mode 100644 packages/react-fs/package.json create mode 100644 packages/react-fs/src/ReactFilesystem.js 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..72250aa1b5f0d --- /dev/null +++ b/packages/react-fs/package.json @@ -0,0 +1,23 @@ +{ + "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/" + ], + "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..85a7f3d28d8b1 --- /dev/null +++ b/packages/react-fs/src/ReactFilesystem.js @@ -0,0 +1,108 @@ +/** + * 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'; + +const Pending = 0; +const Resolved = 1; +const Rejected = 2; + +type PendingResult = {| + status: 0, + value: Wakeable, +|}; + +type ResolvedResult = {| + status: 1, + value: T, +|}; + +type RejectedResult = {| + status: 2, + value: mixed, +|}; + +type Result = PendingResult | ResolvedResult | RejectedResult; + +function toResult(thenable: Thenable): Result { + const result: Result = { + status: Pending, + value: thenable, + }; + 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; + } +} + +function createReadFileCache(): Map> { + return new Map(); +} + +export function readFile( + path: string, + options: + | string + | { + encoding?: string | null, + // Ignored: + flag?: string, + signal?: mixed, + }, +): string | Buffer { + const map = unstable_getCacheForType(createReadFileCache); + 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; + } + const encoding = typeof options === 'string' ? options : options.encoding; + if (typeof encoding !== 'string') { + return result; + } + const textCache = + (result: any)._reactTextCache || ((result: any)._reactTextCache = []); + for (let i = 0; i < textCache.length; i += 2) { + if (textCache[i] === encoding) { + return textCache[i + 1]; + } + } + 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..66bf54ebe6359 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'], + }, + /******* React PG Browser (experimental, new) *******/ { bundleTypes: [NODE_DEV, NODE_PROD], From 969d92bceb5d9ba0573dd68dfb210e63e898b1ee Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 9 Dec 2020 00:36:32 +0000 Subject: [PATCH 2/5] Throw for unsupported --- packages/react-fs/src/ReactFilesystem.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/react-fs/src/ReactFilesystem.js b/packages/react-fs/src/ReactFilesystem.js index 85a7f3d28d8b1..794a5c0f8973b 100644 --- a/packages/react-fs/src/ReactFilesystem.js +++ b/packages/react-fs/src/ReactFilesystem.js @@ -75,9 +75,9 @@ export function readFile( | string | { encoding?: string | null, - // Ignored: - flag?: string, - signal?: mixed, + // Unsupported: + flag?: string, // Doesn't make sense except "r" + signal?: mixed, // We'll have our own signal }, ): string | Buffer { const map = unstable_getCacheForType(createReadFileCache); @@ -91,7 +91,21 @@ export function readFile( if (!options) { return result; } - const encoding = typeof options === 'string' ? options : options.encoding; + 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; } From 90cba372caddcd6764d80f3970f2bdd1ef3695d1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 9 Dec 2020 00:46:18 +0000 Subject: [PATCH 3/5] Don't mess with hidden class --- packages/react-fs/src/ReactFilesystem.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-fs/src/ReactFilesystem.js b/packages/react-fs/src/ReactFilesystem.js index 794a5c0f8973b..5a3bc75faed9c 100644 --- a/packages/react-fs/src/ReactFilesystem.js +++ b/packages/react-fs/src/ReactFilesystem.js @@ -19,16 +19,19 @@ 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; @@ -37,6 +40,7 @@ function toResult(thenable: Thenable): Result { const result: Result = { status: Pending, value: thenable, + cache: [], }; thenable.then( value => { @@ -109,11 +113,10 @@ export function readFile( if (typeof encoding !== 'string') { return result; } - const textCache = - (result: any)._reactTextCache || ((result: any)._reactTextCache = []); + const textCache = entry.cache; for (let i = 0; i < textCache.length; i += 2) { if (textCache[i] === encoding) { - return textCache[i + 1]; + return (textCache[i + 1]: any); } } const text = result.toString((encoding: any)); From 9e0776d628b02793a6b01b9fa111f73fe7a841c8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 9 Dec 2020 01:11:29 +0000 Subject: [PATCH 4/5] Use absolute path as the key --- packages/react-fs/src/ReactFilesystem.js | 8 +++++--- scripts/rollup/bundles.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-fs/src/ReactFilesystem.js b/packages/react-fs/src/ReactFilesystem.js index 5a3bc75faed9c..462086c1add54 100644 --- a/packages/react-fs/src/ReactFilesystem.js +++ b/packages/react-fs/src/ReactFilesystem.js @@ -11,6 +11,7 @@ import type {Wakeable, Thenable} from 'shared/ReactTypes'; import {unstable_getCacheForType} from 'react'; import * as fs from 'fs/promises'; +import {resolve} from 'path'; const Pending = 0; const Resolved = 1; @@ -85,11 +86,12 @@ export function readFile( }, ): string | Buffer { const map = unstable_getCacheForType(createReadFileCache); - let entry = map.get(path); + const resolvedPath = resolve(path); + let entry = map.get(resolvedPath); if (!entry) { - const thenable = fs.readFile(path); + const thenable = fs.readFile(resolvedPath); entry = toResult(thenable); - map.set(path, entry); + map.set(resolvedPath, entry); } const result: Buffer = readResult(entry); if (!options) { diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 66bf54ebe6359..e97f7aa8d3067 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -168,7 +168,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-fs/index.node', global: 'ReactFilesystem', - externals: ['react', 'fs/promises'], + externals: ['react', 'fs/promises', 'path'], }, /******* React PG Browser (experimental, new) *******/ From ebf5981903e65a6a6852285b6e13b30cef115606 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 9 Dec 2020 02:14:53 +0000 Subject: [PATCH 5/5] Warn on relative and non-normalized paths --- packages/react-fs/package.json | 3 +++ packages/react-fs/src/ReactFilesystem.js | 31 ++++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/react-fs/package.json b/packages/react-fs/package.json index 72250aa1b5f0d..d3f9586725e0a 100644 --- a/packages/react-fs/package.json +++ b/packages/react-fs/package.json @@ -17,6 +17,9 @@ "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 index 462086c1add54..cfc74bdfd219b 100644 --- a/packages/react-fs/src/ReactFilesystem.js +++ b/packages/react-fs/src/ReactFilesystem.js @@ -11,7 +11,7 @@ import type {Wakeable, Thenable} from 'shared/ReactTypes'; import {unstable_getCacheForType} from 'react'; import * as fs from 'fs/promises'; -import {resolve} from 'path'; +import {isAbsolute, normalize} from 'path'; const Pending = 0; const Resolved = 1; @@ -70,6 +70,27 @@ function readResult(result: Result): T { } } +// 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(); } @@ -86,12 +107,12 @@ export function readFile( }, ): string | Buffer { const map = unstable_getCacheForType(createReadFileCache); - const resolvedPath = resolve(path); - let entry = map.get(resolvedPath); + checkPathInDev(path); + let entry = map.get(path); if (!entry) { - const thenable = fs.readFile(resolvedPath); + const thenable = fs.readFile(path); entry = toResult(thenable); - map.set(resolvedPath, entry); + map.set(path, entry); } const result: Buffer = readResult(entry); if (!options) {