[Fizz] Add another fixture (#21627)

This commit is contained in:
Dan Abramov
2021-06-04 23:04:06 +01:00
committed by GitHub
parent cc4d24ab0b
commit fccfc24893
17 changed files with 13331 additions and 0 deletions
+12684
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
{
"name": "react-ssr",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=14.9.0"
},
"license": "MIT",
"dependencies": {
"@babel/core": "7.14.3",
"@babel/register": "7.13.16",
"babel-loader": "8.1.0",
"babel-preset-react-app": "10.0.0",
"compression": "^1.7.4",
"concurrently": "^5.3.0",
"express": "^4.17.1",
"nodemon": "^2.0.6",
"react": "0.0.0-cc4d24ab0",
"react-dom": "0.0.0-cc4d24ab0",
"react-error-boundary": "^3.1.3",
"resolve": "1.12.0",
"rimraf": "^3.0.2",
"webpack": "4.44.2",
"webpack-cli": "^4.2.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"prettier": "1.19.1"
},
"scripts": {
"start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"",
"start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"",
"server:dev": "cross-env NODE_ENV=development nodemon -- server/server.js",
"server:prod": "cross-env NODE_ENV=production nodemon -- server/server.js",
"bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js",
"bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js"
},
"babel": {
"presets": [
[
"react-app",
{
"runtime": "automatic"
}
]
]
},
"nodemonConfig": {
"ignore": [
"build/*"
]
}
}
+70
View File
@@ -0,0 +1,70 @@
body {
font-family: system-ui, sans-serif;
}
nav {
padding: 20px;
}
.sidebar {
padding: 10px;
height: 500px;
float: left;
width: 30%;
}
.post {
padding: 20px;
float: left;
width: 60%;
}
h1, h2 {
padding: 0;
}
ul, li {
margin: 0;
}
.post p {
font-size: larger;
font-family: Georgia, serif;
}
.comments {
margin-top: 40px;
}
.comment {
border: 2px solid #aaa;
border-radius: 4px;
padding: 20px;
}
/* https://codepen.io/mandelid/pen/vwKoe */
.spinner {
display: inline-block;
transition: opacity linear 0.1s;
width: 20px;
height: 20px;
border: 3px solid rgba(80, 80, 80, 0.5);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
opacity: 0;
}
.spinner--active {
opacity: 1;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* 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.
*
*/
'use strict';
const path = require('path');
const rimraf = require('rimraf');
const webpack = require('webpack');
const isProduction = process.env.NODE_ENV === 'production';
rimraf.sync(path.resolve(__dirname, '../build'));
webpack(
{
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
entry: [path.resolve(__dirname, '../src/index.js')],
output: {
path: path.resolve(__dirname, '../build'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
},
(err, stats) => {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
process.exit(1);
return;
}
const info = stats.toJson();
if (stats.hasErrors()) {
console.log('Finished running webpack with errors.');
info.errors.forEach(e => console.error(e));
process.exit(1);
} else {
console.log('Finished running webpack.');
}
}
);
+18
View File
@@ -0,0 +1,18 @@
/**
* 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.
*
*/
// Tweak these to play with different kinds of latency.
// How long the data fetches on the server.
exports.API_DELAY = 2000;
// How long the server waits for data before giving up.
exports.ABORT_DELAY = 10000;
// How long serving the JS bundles is delayed.
exports.JS_BUNDLE_DELAY = 5000;
+88
View File
@@ -0,0 +1,88 @@
/**
* 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.
*
*/
import * as React from 'react';
// import {renderToString} from 'react-dom/server';
import {pipeToNodeWritable} from 'react-dom/unstable-fizz';
import App from '../src/App';
import {DataProvider} from '../src/data';
import {API_DELAY, ABORT_DELAY} from './delays';
// In a real setup, you'd read it from webpack build stats.
let assets = {
'main.js': '/main.js',
'main.css': '/main.css',
};
module.exports = function render(url, res) {
// This is how you would wire it up previously:
//
// res.send(
// '<!DOCTYPE html>' +
// renderToString(
// <DataProvider data={data}>
// <App assets={assets} />
// </DataProvider>,
// )
// );
// The new wiring is a bit more involved.
res.socket.on('error', error => {
console.error('Fatal', error);
});
let didError = false;
const data = createServerData();
const {startWriting, abort} = pipeToNodeWritable(
<DataProvider data={data}>
<App assets={assets} />
</DataProvider>,
res,
{
onReadyToStream() {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
res.write('<!DOCTYPE html>');
startWriting();
},
onError(x) {
didError = true;
console.error(x);
},
}
);
// Abandon and switch to client rendering if enough time passes.
// Try lowering this to see the client recover.
setTimeout(abort, ABORT_DELAY);
};
// Simulate a delay caused by data fetching.
// We fake this because the streaming HTML renderer
// is not yet integrated with real data fetching strategies.
function createServerData() {
let done = false;
let promise = null;
return {
read() {
if (done) {
return;
}
if (promise) {
throw promise;
}
promise = new Promise(resolve => {
setTimeout(() => {
done = true;
promise = null;
resolve();
}, API_DELAY);
});
throw promise;
},
};
}
+96
View File
@@ -0,0 +1,96 @@
/**
* 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.
*
*/
'use strict';
const babelRegister = require('@babel/register');
babelRegister({
ignore: [/[\\\/](build|server\/server|node_modules)[\\\/]/],
presets: [['react-app', {runtime: 'automatic'}]],
plugins: ['@babel/transform-modules-commonjs'],
});
const express = require('express');
const compress = require('compression');
const {readFileSync} = require('fs');
const path = require('path');
const React = require('react');
const render = require('./render');
const {JS_BUNDLE_DELAY} = require('./delays');
const PORT = process.env.PORT || 4000;
const app = express();
app.use((req, res, next) => {
if (req.url.endsWith('.js')) {
// Artificially delay serving JS
// to demonstrate streaming HTML.
setTimeout(next, JS_BUNDLE_DELAY);
} else {
next();
}
});
app.use(compress());
app.get(
'/',
handleErrors(async function(req, res) {
await waitForWebpack();
render(req.url, res);
})
);
app.use(express.static('build'));
app.use(express.static('public'));
app
.listen(PORT, () => {
console.log(`Listening at ${PORT}...`);
})
.on('error', function(error) {
if (error.syscall !== 'listen') {
throw error;
}
const isPipe = portOrPipe => Number.isNaN(portOrPipe);
const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});
function handleErrors(fn) {
return async function(req, res, next) {
try {
return await fn(req, res);
} catch (x) {
next(x);
}
};
}
async function waitForWebpack() {
while (true) {
try {
readFileSync(path.resolve(__dirname, '../build/main.js'));
return;
} catch (err) {
console.log(
'Could not find webpack build output. Will retry in a second...'
);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* 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.
*
*/
import {useState, Suspense, lazy} from 'react';
import {ErrorBoundary} from 'react-error-boundary';
import Html from './Html';
import Spinner from './Spinner';
import Layout from './Layout';
import NavBar from './NavBar';
const Comments = lazy(() => import('./Comments' /* webpackPrefetch: true */));
const Sidebar = lazy(() => import('./Sidebar' /* webpackPrefetch: true */));
const Post = lazy(() => import('./Post' /* webpackPrefetch: true */));
export default function App({assets}) {
return (
<Html assets={assets} title="Hello">
<Suspense fallback={<Spinner />}>
<ErrorBoundary FallbackComponent={Error}>
<Content />
</ErrorBoundary>
</Suspense>
</Html>
);
}
function Content() {
return (
<Layout>
<NavBar />
<aside className="sidebar">
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
</aside>
<article className="post">
<Post />
<section className="comments">
<h2>Comments</h2>
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</section>
<h2>Thanks for reading!</h2>
</article>
</Layout>
);
}
function Error({error}) {
return (
<div>
<h1>Application Error</h1>
<pre style={{whiteSpace: 'pre-wrap'}}>{error.stack}</pre>
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
/**
* 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.
*
*/
import {useData} from './data';
export default function Comments() {
const comments = useData();
return (
<>
{comments.map((comment, i) => (
<p className="comment" key={i}>
{comment}
</p>
))}
</>
);
}
+35
View File
@@ -0,0 +1,35 @@
/**
* 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.
*
*/
export default function Html({assets, children, title}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="stylesheet" href={assets['main.css']} />
<title>{title}</title>
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>
{children}
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,
}}
/>
<script async src={assets['main.js']} />
</body>
</html>
);
}
+11
View File
@@ -0,0 +1,11 @@
/**
* 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.
*
*/
export default function Layout({children}) {
return <main>{children}</main>;
}
+15
View File
@@ -0,0 +1,15 @@
/**
* 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.
*
*/
export default function NavBar() {
return (
<nav>
<a href="/">Home</a>
</nav>
);
}
+28
View File
@@ -0,0 +1,28 @@
/**
* 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.
*
*/
export default function Post() {
return (
<>
<h1>Hello world</h1>
<p>
This demo is <b>artificially slowed down</b>. Open{' '}
<code>server/delays.js</code> to adjust how much different things are
slowed down.
</p>
<p>
Notice how HTML for comments "streams in" before the JS (or React) has
loaded on the page.
</p>
<p>
Also notice that the JS for comments and sidebar has been code-split,
but HTML for it is still included in the server output.
</p>
</>
);
}
+26
View File
@@ -0,0 +1,26 @@
/**
* 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.
*
*/
export default function Sidebar() {
return (
<>
<h1>Archive</h1>
<ul>
<li>May 2021</li>
<li>April 2021</li>
<li>March 2021</li>
<li>February 2021</li>
<li>January 2021</li>
<li>December 2020</li>
<li>November 2020</li>
<li>October 2020</li>
<li>September 2020</li>
</ul>
</>
);
}
+17
View File
@@ -0,0 +1,17 @@
/**
* 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.
*
*/
export default function Spinner({active = true}) {
return (
<div
className={['spinner', active && 'spinner--active'].join(' ')}
role="progressbar"
aria-busy={active ? 'true' : 'false'}
/>
);
}
+38
View File
@@ -0,0 +1,38 @@
/**
* 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.
*
*/
import {createContext, useContext} from 'react';
// Note: this file does not demonstrate a real data fetching strategy.
// We only use this to simulate data fetching happening on the server
// while the cache is populated on the client. In a real app, you would
// instead use a data fetching library or Server Components for this.
const DataContext = createContext(null);
export function DataProvider({children, data}) {
return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}
// In a real implementation the data would be streamed with the HTML.
// We haven't integrated this part yet, so we'll just use fake data.
const fakeData = [
"Wait, it doesn't wait for React to load?",
'How does this even work?',
'I like marshmallows',
];
export function useData() {
const ctx = useContext(DataContext);
if (ctx !== null) {
// This context is only provided on the server.
// It is here to simulate a suspending data fetch.
ctx.read();
}
return fakeData;
}
+14
View File
@@ -0,0 +1,14 @@
/**
* 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.
*
*/
import {createRoot} from 'react-dom';
import App from './App';
import {DataProvider} from './data';
const root = createRoot(document, {hydrate: true});
root.render(<App assets={window.assetManifest} />);