mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[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>
This commit is contained in:
parent
e936034eec
commit
c512aa0081
|
|
@ -7,7 +7,7 @@
|
|||
/* eslint-disable import/first */
|
||||
|
||||
import * as React from 'react';
|
||||
import {fetch} from 'react-data/fetch';
|
||||
import {fetch} from 'react-fetch';
|
||||
|
||||
// TODO: Replace with asset reference.
|
||||
import Link from '../client/Link';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/* eslint-disable import/first */
|
||||
|
||||
import * as React from 'react';
|
||||
import {fetch} from 'react-data/fetch';
|
||||
import {fetch} from 'react-fetch';
|
||||
import PostList from './PostList';
|
||||
|
||||
export default function Feed() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Suspense, unstable_SuspenseList as SuspenseList} from 'react';
|
||||
import {preload} from 'react-data/fetch';
|
||||
import {preload} from 'react-fetch';
|
||||
import PostGlimmer from './PostGlimmer';
|
||||
import Post from './Post';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/* eslint-disable import/first */
|
||||
|
||||
import * as React from 'react';
|
||||
import {fetch} from 'react-data/fetch';
|
||||
import {fetch} from 'react-fetch';
|
||||
|
||||
export default function ProfileBio({userId}) {
|
||||
const user = fetch(`/users/${userId}`).json();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Suspense} from 'react';
|
||||
import {fetch} from 'react-data/fetch';
|
||||
import {fetch} from 'react-fetch';
|
||||
import {matchRoute} from './ServerRouter';
|
||||
import ProfileTimeline from './ProfileTimeline';
|
||||
import ProfileBio from './ProfileBio';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/* eslint-disable import/first */
|
||||
|
||||
import * as React from 'react';
|
||||
import {fetch} from 'react-data/fetch';
|
||||
import {fetch} from 'react-fetch';
|
||||
import PostList from './PostList';
|
||||
|
||||
export default function ProfileTimeline({userId}) {
|
||||
|
|
|
|||
7
packages/react-data/npm/fetch.js
vendored
7
packages/react-data/npm/fetch.js
vendored
|
|
@ -1,7 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-data-fetch.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-data-fetch.development.js');
|
||||
}
|
||||
7
packages/react-data/npm/index.js
vendored
7
packages/react-data/npm/index.js
vendored
|
|
@ -1,7 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-data.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-data.development.js');
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('ReactDataFetch', () => {
|
||||
let ReactDataFetch;
|
||||
|
||||
beforeEach(() => {
|
||||
ReactDataFetch = require('react-data/fetch');
|
||||
});
|
||||
|
||||
// TODO: test something useful.
|
||||
it('exports something', () => {
|
||||
expect(ReactDataFetch.fetch).not.toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# react-data
|
||||
# react-fetch
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -9,4 +9,4 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
export * from './src/fetch/ReactDataFetch';
|
||||
export * from './src/ReactFetchBrowser';
|
||||
|
|
@ -9,4 +9,4 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
export * from './src/ReactData';
|
||||
export * from './index.node';
|
||||
|
|
@ -7,6 +7,6 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
export function createResource(): any {
|
||||
// TODO
|
||||
}
|
||||
'use strict';
|
||||
|
||||
export * from './src/ReactFetchNode';
|
||||
7
packages/react-fetch/npm/index.browser.js
Normal file
7
packages/react-fetch/npm/index.browser.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-fetch.browser.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-fetch.browser.development.js');
|
||||
}
|
||||
3
packages/react-fetch/npm/index.js
vendored
Normal file
3
packages/react-fetch/npm/index.js
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = require('./index.node');
|
||||
7
packages/react-fetch/npm/index.node.js
Normal file
7
packages/react-fetch/npm/index.node.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-fetch.node.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-fetch.node.development.js');
|
||||
}
|
||||
|
|
@ -1,22 +1,26 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "react-data",
|
||||
"name": "react-fetch",
|
||||
"description": "Helpers for creating React data sources",
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-data"
|
||||
"directory": "packages/react-fetch"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"fetch.js",
|
||||
"index.node.js",
|
||||
"index.browser.js",
|
||||
"cjs/"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1"
|
||||
},
|
||||
"browser": {
|
||||
"./index.js": "./index.browser.js"
|
||||
}
|
||||
}
|
||||
229
packages/react-fetch/src/ReactFetchNode.js
vendored
Normal file
229
packages/react-fetch/src/ReactFetchNode.js
vendored
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* 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} from 'shared/ReactTypes';
|
||||
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
|
||||
import {readCache} from 'react/unstable-cache';
|
||||
|
||||
type FetchResponse = {|
|
||||
// Properties
|
||||
headers: any,
|
||||
ok: boolean,
|
||||
redirected: boolean,
|
||||
status: number,
|
||||
statusText: string,
|
||||
type: 'basic',
|
||||
url: string,
|
||||
// Methods
|
||||
arrayBuffer(): ArrayBuffer,
|
||||
blob(): any,
|
||||
json(): any,
|
||||
text(): string,
|
||||
|};
|
||||
|
||||
function nodeFetch(
|
||||
url: string,
|
||||
options: mixed,
|
||||
onResolve: any => void,
|
||||
onReject: any => void,
|
||||
): void {
|
||||
const {hostname, pathname, search, port, protocol} = new URL(url);
|
||||
const nodeOptions = {
|
||||
hostname,
|
||||
port,
|
||||
path: pathname + search,
|
||||
// TODO: cherry-pick supported user-passed options.
|
||||
};
|
||||
const nodeImpl = protocol === 'https:' ? https : http;
|
||||
const request = nodeImpl.request(nodeOptions, response => {
|
||||
// TODO: support redirects.
|
||||
onResolve(new Response(response));
|
||||
});
|
||||
request.on('error', error => {
|
||||
onReject(error);
|
||||
});
|
||||
request.end();
|
||||
}
|
||||
|
||||
const Pending = 0;
|
||||
const Resolved = 1;
|
||||
const Rejected = 2;
|
||||
|
||||
type PendingResult = {|
|
||||
status: 0,
|
||||
value: Wakeable,
|
||||
|};
|
||||
|
||||
type ResolvedResult<V> = {|
|
||||
status: 1,
|
||||
value: V,
|
||||
|};
|
||||
|
||||
type RejectedResult = {|
|
||||
status: 2,
|
||||
value: mixed,
|
||||
|};
|
||||
|
||||
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
|
||||
|
||||
const fetchKey = {};
|
||||
|
||||
function readResultMap(): Map<string, Result<FetchResponse>> {
|
||||
const resources = readCache().resources;
|
||||
let map = resources.get(fetchKey);
|
||||
if (map === undefined) {
|
||||
map = new Map();
|
||||
resources.set(fetchKey, map);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function readResult<T>(result: Result<T>): T {
|
||||
if (result.status === Resolved) {
|
||||
return result.value;
|
||||
} else {
|
||||
throw result.value;
|
||||
}
|
||||
}
|
||||
|
||||
function Response(nativeResponse) {
|
||||
this.headers = nativeResponse.headers;
|
||||
this.ok = nativeResponse.statusCode >= 200 && nativeResponse.statusCode < 300;
|
||||
this.redirected = false; // TODO
|
||||
this.status = nativeResponse.statusCode;
|
||||
this.statusText = nativeResponse.statusMessage;
|
||||
this.type = 'basic';
|
||||
this.url = nativeResponse.url;
|
||||
|
||||
this._response = nativeResponse;
|
||||
this._blob = null;
|
||||
this._json = null;
|
||||
this._text = null;
|
||||
|
||||
const callbacks = [];
|
||||
function wake() {
|
||||
// This assumes they won't throw.
|
||||
while (callbacks.length > 0) {
|
||||
const cb = callbacks.pop();
|
||||
cb();
|
||||
}
|
||||
}
|
||||
const result: PendingResult = (this._result = {
|
||||
status: Pending,
|
||||
value: {
|
||||
then(cb) {
|
||||
callbacks.push(cb);
|
||||
},
|
||||
},
|
||||
});
|
||||
const data = [];
|
||||
nativeResponse.on('data', chunk => data.push(chunk));
|
||||
nativeResponse.on('end', () => {
|
||||
if (result.status === Pending) {
|
||||
const resolvedResult = ((result: any): ResolvedResult<Buffer>);
|
||||
resolvedResult.status = Resolved;
|
||||
resolvedResult.value = Buffer.concat(data);
|
||||
wake();
|
||||
}
|
||||
});
|
||||
nativeResponse.on('error', err => {
|
||||
if (result.status === Pending) {
|
||||
const rejectedResult = ((result: any): RejectedResult);
|
||||
rejectedResult.status = Rejected;
|
||||
rejectedResult.value = err;
|
||||
wake();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Response.prototype = {
|
||||
constructor: Response,
|
||||
arrayBuffer() {
|
||||
const buffer = readResult(this._result);
|
||||
return buffer;
|
||||
},
|
||||
blob() {
|
||||
// TODO: Is this needed?
|
||||
throw new Error('Not implemented.');
|
||||
},
|
||||
json() {
|
||||
const buffer = readResult(this._result);
|
||||
return JSON.parse(buffer.toString());
|
||||
},
|
||||
text() {
|
||||
const buffer = readResult(this._result);
|
||||
return buffer.toString();
|
||||
},
|
||||
};
|
||||
|
||||
function preloadResult(url: string, options: mixed): Result<FetchResponse> {
|
||||
const map = readResultMap();
|
||||
let entry = map.get(url);
|
||||
if (!entry) {
|
||||
if (options) {
|
||||
if (options.method || options.body || options.signal) {
|
||||
// TODO: wire up our own cancellation mechanism.
|
||||
// TODO: figure out what to do with POST.
|
||||
throw Error('Unsupported option');
|
||||
}
|
||||
}
|
||||
const callbacks = [];
|
||||
const wakeable = {
|
||||
then(cb) {
|
||||
callbacks.push(cb);
|
||||
},
|
||||
};
|
||||
const wake = () => {
|
||||
// This assumes they won't throw.
|
||||
while (callbacks.length > 0) {
|
||||
const cb = callbacks.pop();
|
||||
cb();
|
||||
}
|
||||
};
|
||||
const result: Result<FetchResponse> = (entry = {
|
||||
status: Pending,
|
||||
value: wakeable,
|
||||
});
|
||||
nodeFetch(
|
||||
url,
|
||||
options,
|
||||
response => {
|
||||
if (result.status === Pending) {
|
||||
const resolvedResult = ((result: any): ResolvedResult<FetchResponse>);
|
||||
resolvedResult.status = Resolved;
|
||||
resolvedResult.value = response;
|
||||
wake();
|
||||
}
|
||||
},
|
||||
err => {
|
||||
if (result.status === Pending) {
|
||||
const rejectedResult = ((result: any): RejectedResult);
|
||||
rejectedResult.status = Rejected;
|
||||
rejectedResult.value = err;
|
||||
wake();
|
||||
}
|
||||
},
|
||||
);
|
||||
map.set(url, entry);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function preload(url: string, options: mixed): void {
|
||||
preloadResult(url, options);
|
||||
// Don't return anything.
|
||||
}
|
||||
|
||||
export function fetch(url: string, options: mixed): FetchResponse {
|
||||
const result = preloadResult(url, options);
|
||||
return readResult(result);
|
||||
}
|
||||
|
|
@ -9,15 +9,15 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
describe('ReactData', () => {
|
||||
let ReactData;
|
||||
describe('ReactFetchBrowser', () => {
|
||||
let ReactFetchBrowser;
|
||||
|
||||
beforeEach(() => {
|
||||
ReactData = require('react-data');
|
||||
ReactFetchBrowser = require('react-fetch');
|
||||
});
|
||||
|
||||
// TODO: test something useful.
|
||||
it('exports something', () => {
|
||||
expect(ReactData.createResource).not.toBe(undefined);
|
||||
expect(ReactFetchBrowser.fetch).not.toBe(undefined);
|
||||
});
|
||||
});
|
||||
92
packages/react-fetch/src/__tests__/ReactFetchNode-test.js
vendored
Normal file
92
packages/react-fetch/src/__tests__/ReactFetchNode-test.js
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('ReactFetchNode', () => {
|
||||
let ReactCache;
|
||||
let ReactFetchNode;
|
||||
let http;
|
||||
let fetch;
|
||||
let server;
|
||||
let serverEndpoint;
|
||||
let serverImpl;
|
||||
|
||||
beforeEach(done => {
|
||||
jest.resetModules();
|
||||
ReactCache = require('react/unstable-cache');
|
||||
ReactFetchNode = require('react-fetch');
|
||||
http = require('http');
|
||||
fetch = ReactFetchNode.fetch;
|
||||
|
||||
server = http.createServer((req, res) => {
|
||||
serverImpl(req, res);
|
||||
});
|
||||
server.listen(done);
|
||||
serverEndpoint = `http://localhost:${server.address().port}/`;
|
||||
|
||||
// TODO: A way to pass load context.
|
||||
ReactCache.CacheProvider._context._currentValue = ReactCache.createCache();
|
||||
});
|
||||
|
||||
afterEach(done => {
|
||||
server.close(done);
|
||||
server = null;
|
||||
});
|
||||
|
||||
async function waitForSuspense(fn) {
|
||||
while (true) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
await promise;
|
||||
} else {
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('can read text', async () => {
|
||||
serverImpl = (req, res) => {
|
||||
res.write('ok');
|
||||
res.end();
|
||||
};
|
||||
await waitForSuspense(() => {
|
||||
const response = fetch(serverEndpoint);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.statusText).toBe('OK');
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.text()).toEqual('ok');
|
||||
// Can read again:
|
||||
expect(response.text()).toEqual('ok');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read json', async () => {
|
||||
serverImpl = (req, res) => {
|
||||
res.write(JSON.stringify({name: 'Sema'}));
|
||||
res.end();
|
||||
};
|
||||
await waitForSuspense(() => {
|
||||
const response = fetch(serverEndpoint);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.statusText).toBe('OK');
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.json()).toEqual({
|
||||
name: 'Sema',
|
||||
});
|
||||
// Can read again:
|
||||
expect(response.json()).toEqual({
|
||||
name: 'Sema',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -260,7 +260,7 @@ function getFormat(bundleType) {
|
|||
|
||||
function getFilename(name, globalName, bundleType) {
|
||||
// we do this to replace / to -, for react-dom/server
|
||||
name = name.replace('/', '-');
|
||||
name = name.replace('/index.', '.').replace('/', '-');
|
||||
switch (bundleType) {
|
||||
case UMD_DEV:
|
||||
return `${name}.development.js`;
|
||||
|
|
|
|||
|
|
@ -115,16 +115,7 @@ const bundles = [
|
|||
externals: ['react'],
|
||||
},
|
||||
|
||||
/******* React Data (experimental, new) *******/
|
||||
{
|
||||
bundleTypes: [NODE_DEV, NODE_PROD, NODE_PROFILING],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'react-data',
|
||||
global: 'ReactData',
|
||||
externals: ['react'],
|
||||
},
|
||||
|
||||
/******* React Data Fetch (experimental, new) *******/
|
||||
/******* React Fetch Browser (experimental, new) *******/
|
||||
{
|
||||
bundleTypes: [
|
||||
NODE_DEV,
|
||||
|
|
@ -135,9 +126,18 @@ const bundles = [
|
|||
FB_WWW_PROFILING,
|
||||
],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'react-data/fetch',
|
||||
global: 'ReactDataFetch',
|
||||
externals: ['react', 'react-data'],
|
||||
entry: 'react-fetch/index.browser',
|
||||
global: 'ReactFetch',
|
||||
externals: ['react'],
|
||||
},
|
||||
|
||||
/******* React Fetch Node (experimental, new) *******/
|
||||
{
|
||||
bundleTypes: [NODE_DEV, NODE_PROD],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'react-fetch/index.node',
|
||||
global: 'ReactFetch',
|
||||
externals: ['react', 'http', 'https'],
|
||||
},
|
||||
|
||||
/******* React DOM *******/
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const importSideEffects = Object.freeze({
|
|||
'scheduler/tracing': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
'react-dom/server': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
'react/jsx-dev-runtime': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
'react-fetch/node': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
});
|
||||
|
||||
// Bundles exporting globals that other modules rely on.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user