@samitouri / QOS-React-2 / commits / c512aa0081

[Blocks] Scaffolding react-fetch + first pass at node implementation (#18863)

* First pass at scaffolding out the Node implementation of react-data. While incomplete, this patch contains some changes to the react-data package in order to start adding support for Node. The first part of this change accounts for splitting react-data/fetch into two discrete entries, adding (and defaulting to) the Node implementation. The second part is sketching out a rough approximation of `fetch` for Node. This implementation is not complete by any means, but provides a starting point. * Remove NodeFetch module and put it directly into ReactDataFetchNode. * Replaced react-data with react-fetch. This patch shuffles around some of the scaffolding that was in react-data in favor of react-fetch. It also removes the additional "fetch" package in favor of something flatter. * Tweak package organization * Simplify and add a test Co-authored-by: Dan Abramov <dan.abramov@me.com>

Richard Maisano committed May 12, 2020 at 12:21 UTC c512aa008175f43315ad16cd9da86dc0c512c069
24 files changed +376 -70
fixtures/blocks/src/server/Comments.js
+1 -1
@@ -7,7 +7,7 @@
7 /* eslint-disable import/first */
8
9 import * as React from 'react';
10 -import {fetch} from 'react-data/fetch';
10 +import {fetch} from 'react-fetch';
11
12 // TODO: Replace with asset reference.
13 import Link from '../client/Link';
fixtures/blocks/src/server/Feed.js
+1 -1
@@ -7,7 +7,7 @@
7 /* eslint-disable import/first */
8
9 import * as React from 'react';
10 -import {fetch} from 'react-data/fetch';
10 +import {fetch} from 'react-fetch';
11 import PostList from './PostList';
12
13 export default function Feed() {
fixtures/blocks/src/server/PostList.js
+1 -1
@@ -8,7 +8,7 @@
8
9 import * as React from 'react';
10 import {Suspense, unstable_SuspenseList as SuspenseList} from 'react';
11 -import {preload} from 'react-data/fetch';
11 +import {preload} from 'react-fetch';
12 import PostGlimmer from './PostGlimmer';
13 import Post from './Post';
14
fixtures/blocks/src/server/ProfileBio.js
+1 -1
@@ -7,7 +7,7 @@
7 /* eslint-disable import/first */
8
9 import * as React from 'react';
10 -import {fetch} from 'react-data/fetch';
10 +import {fetch} from 'react-fetch';
11
12 export default function ProfileBio({userId}) {
13 const user = fetch(`/users/${userId}`).json();
fixtures/blocks/src/server/ProfilePage.js
+1 -1
@@ -7,7 +7,7 @@
7
8 import * as React from 'react';
9 import {Suspense} from 'react';
10 -import {fetch} from 'react-data/fetch';
10 +import {fetch} from 'react-fetch';
11 import {matchRoute} from './ServerRouter';
12 import ProfileTimeline from './ProfileTimeline';
13 import ProfileBio from './ProfileBio';
fixtures/blocks/src/server/ProfileTimeline.js
+1 -1
@@ -7,7 +7,7 @@
7 /* eslint-disable import/first */
8
9 import * as React from 'react';
10 -import {fetch} from 'react-data/fetch';
10 +import {fetch} from 'react-fetch';
11 import PostList from './PostList';
12
13 export default function ProfileTimeline({userId}) {
packages/react-data/npm/fetch.js deleted
-7
@@ -1,7 +0,0 @@
1 -'use strict';
2 -
3 -if (process.env.NODE_ENV === 'production') {
4 - module.exports = require('./cjs/react-data-fetch.production.min.js');
5 -} else {
6 - module.exports = require('./cjs/react-data-fetch.development.js');
7 -}
packages/react-data/npm/index.js deleted
-7
@@ -1,7 +0,0 @@
1 -'use strict';
2 -
3 -if (process.env.NODE_ENV === 'production') {
4 - module.exports = require('./cjs/react-data.production.min.js');
5 -} else {
6 - module.exports = require('./cjs/react-data.development.js');
7 -}
packages/react-data/src/__tests__/ReactDataFetch-test.js deleted
-23
@@ -1,23 +0,0 @@
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 - * @emails react-core
8 - */
9 -
10 -'use strict';
11 -
12 -describe('ReactDataFetch', () => {
13 - let ReactDataFetch;
14 -
15 - beforeEach(() => {
16 - ReactDataFetch = require('react-data/fetch');
17 - });
18 -
19 - // TODO: test something useful.
20 - it('exports something', () => {
21 - expect(ReactDataFetch.fetch).not.toBe(undefined);
22 - });
23 -});
packages/react-fetch/README.md renamed
+1 -1
@@ -1,4 +1,4 @@
1 -# react-data
1 +# react-fetch
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
packages/react-fetch/index.browser.js renamed
+1 -1
@@ -9,4 +9,4 @@
9
10 'use strict';
11
12 -export * from './src/fetch/ReactDataFetch';
12 +export * from './src/ReactFetchBrowser';
packages/react-fetch/index.js renamed
+1 -1
@@ -9,4 +9,4 @@
9
10 'use strict';
11
12 -export * from './src/ReactData';
12 +export * from './index.node';
packages/react-fetch/index.node.js renamed
+3 -3
@@ -7,6 +7,6 @@
7 * @flow
8 */
9
10 -export function createResource(): any {
11 - // TODO
12 -}
10 +'use strict';
11 +
12 +export * from './src/ReactFetchNode';
packages/react-fetch/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-fetch.browser.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-fetch.browser.development.js');
7 +}
packages/react-fetch/npm/index.js new
+3
@@ -0,0 +1,3 @@
1 +'use strict';
2 +
3 +module.exports = require('./index.node');
packages/react-fetch/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-fetch.node.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-fetch.node.development.js');
7 +}
packages/react-fetch/package.json renamed
+7 -3
@@ -1,22 +1,26 @@
1 {
2 "private": true,
3 - "name": "react-data",
3 + "name": "react-fetch",
4 "description": "Helpers for creating React data sources",
5 "version": "0.0.0",
6 "repository": {
7 "type" : "git",
8 "url" : "https://github.com/facebook/react.git",
9 - "directory": "packages/react-data"
9 + "directory": "packages/react-fetch"
10 },
11 "files": [
12 "LICENSE",
13 "README.md",
14 "build-info.json",
15 "index.js",
16 - "fetch.js",
16 + "index.node.js",
17 + "index.browser.js",
18 "cjs/"
19 ],
20 "peerDependencies": {
21 "react": "^16.13.1"
22 + },
23 + "browser": {
24 + "./index.js": "./index.browser.js"
25 }
26 }
packages/react-fetch/src/ReactFetchBrowser.js renamed
packages/react-fetch/src/ReactFetchNode.js new
+229
@@ -0,0 +1,229 @@
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} from 'shared/ReactTypes';
11 +
12 +import * as http from 'http';
13 +import * as https from 'https';
14 +
15 +import {readCache} from 'react/unstable-cache';
16 +
17 +type FetchResponse = {|
18 + // Properties
19 + headers: any,
20 + ok: boolean,
21 + redirected: boolean,
22 + status: number,
23 + statusText: string,
24 + type: 'basic',
25 + url: string,
26 + // Methods
27 + arrayBuffer(): ArrayBuffer,
28 + blob(): any,
29 + json(): any,
30 + text(): string,
31 +|};
32 +
33 +function nodeFetch(
34 + url: string,
35 + options: mixed,
36 + onResolve: any => void,
37 + onReject: any => void,
38 +): void {
39 + const {hostname, pathname, search, port, protocol} = new URL(url);
40 + const nodeOptions = {
41 + hostname,
42 + port,
43 + path: pathname + search,
44 + // TODO: cherry-pick supported user-passed options.
45 + };
46 + const nodeImpl = protocol === 'https:' ? https : http;
47 + const request = nodeImpl.request(nodeOptions, response => {
48 + // TODO: support redirects.
49 + onResolve(new Response(response));
50 + });
51 + request.on('error', error => {
52 + onReject(error);
53 + });
54 + request.end();
55 +}
56 +
57 +const Pending = 0;
58 +const Resolved = 1;
59 +const Rejected = 2;
60 +
61 +type PendingResult = {|
62 + status: 0,
63 + value: Wakeable,
64 +|};
65 +
66 +type ResolvedResult<V> = {|
67 + status: 1,
68 + value: V,
69 +|};
70 +
71 +type RejectedResult = {|
72 + status: 2,
73 + value: mixed,
74 +|};
75 +
76 +type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
77 +
78 +const fetchKey = {};
79 +
80 +function readResultMap(): Map<string, Result<FetchResponse>> {
81 + const resources = readCache().resources;
82 + let map = resources.get(fetchKey);
83 + if (map === undefined) {
84 + map = new Map();
85 + resources.set(fetchKey, map);
86 + }
87 + return map;
88 +}
89 +
90 +function readResult<T>(result: Result<T>): T {
91 + if (result.status === Resolved) {
92 + return result.value;
93 + } else {
94 + throw result.value;
95 + }
96 +}
97 +
98 +function Response(nativeResponse) {
99 + this.headers = nativeResponse.headers;
100 + this.ok = nativeResponse.statusCode >= 200 && nativeResponse.statusCode < 300;
101 + this.redirected = false; // TODO
102 + this.status = nativeResponse.statusCode;
103 + this.statusText = nativeResponse.statusMessage;
104 + this.type = 'basic';
105 + this.url = nativeResponse.url;
106 +
107 + this._response = nativeResponse;
108 + this._blob = null;
109 + this._json = null;
110 + this._text = null;
111 +
112 + const callbacks = [];
113 + function wake() {
114 + // This assumes they won't throw.
115 + while (callbacks.length > 0) {
116 + const cb = callbacks.pop();
117 + cb();
118 + }
119 + }
120 + const result: PendingResult = (this._result = {
121 + status: Pending,
122 + value: {
123 + then(cb) {
124 + callbacks.push(cb);
125 + },
126 + },
127 + });
128 + const data = [];
129 + nativeResponse.on('data', chunk => data.push(chunk));
130 + nativeResponse.on('end', () => {
131 + if (result.status === Pending) {
132 + const resolvedResult = ((result: any): ResolvedResult<Buffer>);
133 + resolvedResult.status = Resolved;
134 + resolvedResult.value = Buffer.concat(data);
135 + wake();
136 + }
137 + });
138 + nativeResponse.on('error', err => {
139 + if (result.status === Pending) {
140 + const rejectedResult = ((result: any): RejectedResult);
141 + rejectedResult.status = Rejected;
142 + rejectedResult.value = err;
143 + wake();
144 + }
145 + });
146 +}
147 +
148 +Response.prototype = {
149 + constructor: Response,
150 + arrayBuffer() {
151 + const buffer = readResult(this._result);
152 + return buffer;
153 + },
154 + blob() {
155 + // TODO: Is this needed?
156 + throw new Error('Not implemented.');
157 + },
158 + json() {
159 + const buffer = readResult(this._result);
160 + return JSON.parse(buffer.toString());
161 + },
162 + text() {
163 + const buffer = readResult(this._result);
164 + return buffer.toString();
165 + },
166 +};
167 +
168 +function preloadResult(url: string, options: mixed): Result<FetchResponse> {
169 + const map = readResultMap();
170 + let entry = map.get(url);
171 + if (!entry) {
172 + if (options) {
173 + if (options.method || options.body || options.signal) {
174 + // TODO: wire up our own cancellation mechanism.
175 + // TODO: figure out what to do with POST.
176 + throw Error('Unsupported option');
177 + }
178 + }
179 + const callbacks = [];
180 + const wakeable = {
181 + then(cb) {
182 + callbacks.push(cb);
183 + },
184 + };
185 + const wake = () => {
186 + // This assumes they won't throw.
187 + while (callbacks.length > 0) {
188 + const cb = callbacks.pop();
189 + cb();
190 + }
191 + };
192 + const result: Result<FetchResponse> = (entry = {
193 + status: Pending,
194 + value: wakeable,
195 + });
196 + nodeFetch(
197 + url,
198 + options,
199 + response => {
200 + if (result.status === Pending) {
201 + const resolvedResult = ((result: any): ResolvedResult<FetchResponse>);
202 + resolvedResult.status = Resolved;
203 + resolvedResult.value = response;
204 + wake();
205 + }
206 + },
207 + err => {
208 + if (result.status === Pending) {
209 + const rejectedResult = ((result: any): RejectedResult);
210 + rejectedResult.status = Rejected;
211 + rejectedResult.value = err;
212 + wake();
213 + }
214 + },
215 + );
216 + map.set(url, entry);
217 + }
218 + return entry;
219 +}
220 +
221 +export function preload(url: string, options: mixed): void {
222 + preloadResult(url, options);
223 + // Don't return anything.
224 +}
225 +
226 +export function fetch(url: string, options: mixed): FetchResponse {
227 + const result = preloadResult(url, options);
228 + return readResult(result);
229 +}
packages/react-fetch/src/__tests__/ReactFetchBrowser-test.js renamed
+4 -4
@@ -9,15 +9,15 @@
9
10 'use strict';
11
12 -describe('ReactData', () => {
13 - let ReactData;
12 +describe('ReactFetchBrowser', () => {
13 + let ReactFetchBrowser;
14
15 beforeEach(() => {
16 - ReactData = require('react-data');
16 + ReactFetchBrowser = require('react-fetch');
17 });
18
19 // TODO: test something useful.
20 it('exports something', () => {
21 - expect(ReactData.createResource).not.toBe(undefined);
21 + expect(ReactFetchBrowser.fetch).not.toBe(undefined);
22 });
23 });
packages/react-fetch/src/__tests__/ReactFetchNode-test.js new
+92
@@ -0,0 +1,92 @@
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 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +describe('ReactFetchNode', () => {
13 + let ReactCache;
14 + let ReactFetchNode;
15 + let http;
16 + let fetch;
17 + let server;
18 + let serverEndpoint;
19 + let serverImpl;
20 +
21 + beforeEach(done => {
22 + jest.resetModules();
23 + ReactCache = require('react/unstable-cache');
24 + ReactFetchNode = require('react-fetch');
25 + http = require('http');
26 + fetch = ReactFetchNode.fetch;
27 +
28 + server = http.createServer((req, res) => {
29 + serverImpl(req, res);
30 + });
31 + server.listen(done);
32 + serverEndpoint = `http://localhost:${server.address().port}/`;
33 +
34 + // TODO: A way to pass load context.
35 + ReactCache.CacheProvider._context._currentValue = ReactCache.createCache();
36 + });
37 +
38 + afterEach(done => {
39 + server.close(done);
40 + server = null;
41 + });
42 +
43 + async function waitForSuspense(fn) {
44 + while (true) {
45 + try {
46 + return fn();
47 + } catch (promise) {
48 + if (typeof promise.then === 'function') {
49 + await promise;
50 + } else {
51 + throw promise;
52 + }
53 + }
54 + }
55 + }
56 +
57 + it('can read text', async () => {
58 + serverImpl = (req, res) => {
59 + res.write('ok');
60 + res.end();
61 + };
62 + await waitForSuspense(() => {
63 + const response = fetch(serverEndpoint);
64 + expect(response.status).toBe(200);
65 + expect(response.statusText).toBe('OK');
66 + expect(response.ok).toBe(true);
67 + expect(response.text()).toEqual('ok');
68 + // Can read again:
69 + expect(response.text()).toEqual('ok');
70 + });
71 + });
72 +
73 + it('can read json', async () => {
74 + serverImpl = (req, res) => {
75 + res.write(JSON.stringify({name: 'Sema'}));
76 + res.end();
77 + };
78 + await waitForSuspense(() => {
79 + const response = fetch(serverEndpoint);
80 + expect(response.status).toBe(200);
81 + expect(response.statusText).toBe('OK');
82 + expect(response.ok).toBe(true);
83 + expect(response.json()).toEqual({
84 + name: 'Sema',
85 + });
86 + // Can read again:
87 + expect(response.json()).toEqual({
88 + name: 'Sema',
89 + });
90 + });
91 + });
92 +});
scripts/rollup/build.js
+1 -1
@@ -260,7 +260,7 @@ function getFormat(bundleType) {
260
261 function getFilename(name, globalName, bundleType) {
262 // we do this to replace / to -, for react-dom/server
263 - name = name.replace('/', '-');
263 + name = name.replace('/index.', '.').replace('/', '-');
264 switch (bundleType) {
265 case UMD_DEV:
266 return `${name}.development.js`;
scripts/rollup/bundles.js
+13 -13
@@ -115,16 +115,7 @@ const bundles = [
115 externals: ['react'],
116 },
117
118 - /******* React Data (experimental, new) *******/
119 - {
120 - bundleTypes: [NODE_DEV, NODE_PROD, NODE_PROFILING],
121 - moduleType: ISOMORPHIC,
122 - entry: 'react-data',
123 - global: 'ReactData',
124 - externals: ['react'],
125 - },
126 -
127 - /******* React Data Fetch (experimental, new) *******/
118 + /******* React Fetch Browser (experimental, new) *******/
119 {
120 bundleTypes: [
121 NODE_DEV,
@@ -135,9 +126,18 @@ const bundles = [
126 FB_WWW_PROFILING,
127 ],
128 moduleType: ISOMORPHIC,
138 - entry: 'react-data/fetch',
139 - global: 'ReactDataFetch',
140 - externals: ['react', 'react-data'],
129 + entry: 'react-fetch/index.browser',
130 + global: 'ReactFetch',
131 + externals: ['react'],
132 + },
133 +
134 + /******* React Fetch Node (experimental, new) *******/
135 + {
136 + bundleTypes: [NODE_DEV, NODE_PROD],
137 + moduleType: ISOMORPHIC,
138 + entry: 'react-fetch/index.node',
139 + global: 'ReactFetch',
140 + externals: ['react', 'http', 'https'],
141 },
142
143 /******* React DOM *******/
scripts/rollup/modules.js
+1
@@ -15,6 +15,7 @@ const importSideEffects = Object.freeze({
15 'scheduler/tracing': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
16 'react-dom/server': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
17 'react/jsx-dev-runtime': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
18 + 'react-fetch/node': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
19 });
20
21 // Bundles exporting globals that other modules rely on.