[Flight] Add rudimentary FS binding (#20409)
* [Flight] Add rudimentary FS binding * Throw for unsupported * Don't mess with hidden class * Use absolute path as the key * Warn on relative and non-normalized paths
Dan Abramov committed
Dec 9, 2020 at 02:37 UTC
6a4b12b81c4c855934985225f9f448f1b49a8a14
11 files changed
+267
packages/react-fs/README.md
new
+12
@@ -0,0 +1,12 @@
1
+# react-fs
2
+
3
+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.
4
+
5
+**Do not use in a real application.** We're publishing this early for
6
+demonstration purposes.
7
+
8
+**Use it at your own risk.**
9
+
10
+# No, Really, It Is Unstable
11
+
12
+The API ~~may~~ will change wildly between versions.
packages/react-fs/index.browser.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+throw new Error(
11
+ 'This entry point is not yet supported in the browser environment',
12
+);
packages/react-fs/index.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+'use strict';
11
+
12
+export * from './index.node';
packages/react-fs/index.node.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+'use strict';
11
+
12
+export * from './src/ReactFilesystem';
packages/react-fs/npm/index.browser.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-fs.browser.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-fs.browser.development.js');
7
+}
packages/react-fs/npm/index.js
new
+3
@@ -0,0 +1,3 @@
1
+'use strict';
2
+
3
+module.exports = require('./index.node');
packages/react-fs/npm/index.node.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-fs.node.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-fs.node.development.js');
7
+}
packages/react-fs/package.json
new
+26
@@ -0,0 +1,26 @@
1
+{
2
+ "private": true,
3
+ "name": "react-fs",
4
+ "description": "React bindings for the filesystem",
5
+ "version": "0.0.0",
6
+ "repository": {
7
+ "type" : "git",
8
+ "url" : "https://github.com/facebook/react.git",
9
+ "directory": "packages/react-fs"
10
+ },
11
+ "files": [
12
+ "LICENSE",
13
+ "README.md",
14
+ "build-info.json",
15
+ "index.js",
16
+ "index.node.js",
17
+ "index.browser.js",
18
+ "cjs/"
19
+ ],
20
+ "peerDependencies": {
21
+ "react": "^17.0.0"
22
+ },
23
+ "browser": {
24
+ "./index.js": "./index.browser.js"
25
+ }
26
+}
packages/react-fs/src/ReactFilesystem.js
new
+148
@@ -0,0 +1,148 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Wakeable, Thenable} from 'shared/ReactTypes';
11
+
12
+import {unstable_getCacheForType} from 'react';
13
+import * as fs from 'fs/promises';
14
+import {isAbsolute, normalize} from 'path';
15
+
16
+const Pending = 0;
17
+const Resolved = 1;
18
+const Rejected = 2;
19
+
20
+type PendingResult = {|
21
+ status: 0,
22
+ value: Wakeable,
23
+ cache: Array<mixed>,
24
+|};
25
+
26
+type ResolvedResult<T> = {|
27
+ status: 1,
28
+ value: T,
29
+ cache: Array<mixed>,
30
+|};
31
+
32
+type RejectedResult = {|
33
+ status: 2,
34
+ value: mixed,
35
+ cache: Array<mixed>,
36
+|};
37
+
38
+type Result<T> = PendingResult | ResolvedResult<T> | RejectedResult;
39
+
40
+function toResult<T>(thenable: Thenable<T>): Result<T> {
41
+ const result: Result<T> = {
42
+ status: Pending,
43
+ value: thenable,
44
+ cache: [],
45
+ };
46
+ thenable.then(
47
+ value => {
48
+ if (result.status === Pending) {
49
+ const resolvedResult = ((result: any): ResolvedResult<T>);
50
+ resolvedResult.status = Resolved;
51
+ resolvedResult.value = value;
52
+ }
53
+ },
54
+ err => {
55
+ if (result.status === Pending) {
56
+ const rejectedResult = ((result: any): RejectedResult);
57
+ rejectedResult.status = Rejected;
58
+ rejectedResult.value = err;
59
+ }
60
+ },
61
+ );
62
+ return result;
63
+}
64
+
65
+function readResult<T>(result: Result<T>): T {
66
+ if (result.status === Resolved) {
67
+ return result.value;
68
+ } else {
69
+ throw result.value;
70
+ }
71
+}
72
+
73
+// We don't want to normalize every path ourselves in production.
74
+// However, relative or non-normalized paths will lead to cache misses.
75
+// So we encourage the developer to fix it in DEV and normalize on their end.
76
+function checkPathInDev(path: string) {
77
+ if (__DEV__) {
78
+ if (!isAbsolute(path)) {
79
+ console.error(
80
+ 'The provided path was not absolute: "%s". ' +
81
+ 'Convert it to an absolute path first.',
82
+ path,
83
+ );
84
+ } else if (path !== normalize(path)) {
85
+ console.error(
86
+ 'The provided path was not normalized: "%s". ' +
87
+ 'Convert it to a normalized path first.',
88
+ path,
89
+ );
90
+ }
91
+ }
92
+}
93
+
94
+function createReadFileCache(): Map<string, Result<Buffer>> {
95
+ return new Map();
96
+}
97
+
98
+export function readFile(
99
+ path: string,
100
+ options:
101
+ | string
102
+ | {
103
+ encoding?: string | null,
104
+ // Unsupported:
105
+ flag?: string, // Doesn't make sense except "r"
106
+ signal?: mixed, // We'll have our own signal
107
+ },
108
+): string | Buffer {
109
+ const map = unstable_getCacheForType(createReadFileCache);
110
+ checkPathInDev(path);
111
+ let entry = map.get(path);
112
+ if (!entry) {
113
+ const thenable = fs.readFile(path);
114
+ entry = toResult(thenable);
115
+ map.set(path, entry);
116
+ }
117
+ const result: Buffer = readResult(entry);
118
+ if (!options) {
119
+ return result;
120
+ }
121
+ let encoding;
122
+ if (typeof options === 'string') {
123
+ encoding = options;
124
+ } else {
125
+ const flag = options.flag;
126
+ if (flag != null && flag !== 'r') {
127
+ throw Error(
128
+ 'The flag option is not supported, and always defaults to "r".',
129
+ );
130
+ }
131
+ if (options.signal) {
132
+ throw Error('The signal option is not supported.');
133
+ }
134
+ encoding = options.encoding;
135
+ }
136
+ if (typeof encoding !== 'string') {
137
+ return result;
138
+ }
139
+ const textCache = entry.cache;
140
+ for (let i = 0; i < textCache.length; i += 2) {
141
+ if (textCache[i] === encoding) {
142
+ return (textCache[i + 1]: any);
143
+ }
144
+ }
145
+ const text = result.toString((encoding: any));
146
+ textCache.push(encoding, text);
147
+ return text;
148
+}
scripts/flow/environment.js
+10
@@ -70,6 +70,16 @@ declare module 'EventListener' {
70
declare function __webpack_chunk_load__(id: string): Promise<mixed>;
71
declare function __webpack_require__(id: string): any;
72
73
+declare module 'fs/promises' {
74
+ declare var readFile: (
75
+ path: string,
76
+ options?:
77
+ | ?string
78
+ | {
79
+ encoding?: ?string,
80
+ },
81
+ ) => Promise<Buffer>;
82
+}
83
declare module 'pg' {
84
declare var Pool: (
85
options: mixed,
scripts/rollup/bundles.js
+18
@@ -153,6 +153,24 @@ const bundles = [
153
externals: ['react', 'http', 'https'],
154
},
155
156
+ /******* React FS Browser (experimental, new) *******/
157
+ {
158
+ bundleTypes: [NODE_DEV, NODE_PROD],
159
+ moduleType: ISOMORPHIC,
160
+ entry: 'react-fs/index.browser',
161
+ global: 'ReactFilesystem',
162
+ externals: [],
163
+ },
164
+
165
+ /******* React FS Node (experimental, new) *******/
166
+ {
167
+ bundleTypes: [NODE_DEV, NODE_PROD],
168
+ moduleType: ISOMORPHIC,
169
+ entry: 'react-fs/index.node',
170
+ global: 'ReactFilesystem',
171
+ externals: ['react', 'fs/promises', 'path'],
172
+ },
173
+
174
/******* React PG Browser (experimental, new) *******/
175
{
176
bundleTypes: [NODE_DEV, NODE_PROD],