From 2710b10aaf38f2963caf523285e77b0ae5685abd Mon Sep 17 00:00:00 2001 From: Antony Budianto Date: Sat, 7 May 2022 21:47:36 +0700 Subject: [PATCH] fix: enable streaming ssr (#227) * fix: enable streaming ssr * feat: enable toggle streaming * fix: add preset-typescript * docs: update contributing.md * chore: bump 5.0.2 --- CONTRIBUTING.md | 12 +-- packages/@cra-express/core/package.json | 6 +- .../redux-prefetcher/package.json | 2 +- .../router-prefetcher/package.json | 2 +- .../@cra-express/static-loader/package.json | 2 +- .../universal-loader/package.json | 2 +- .../src/renderer/pipe-stream-renderer.js | 77 +++++++++++-------- packages/babel-preset-cra-universal/index.js | 1 + .../babel-preset-cra-universal/package.json | 3 +- packages/cra-universal/package.json | 6 +- packages/cra-universal/templates/package.json | 13 ++-- .../cra-universal/templates/server/app.js | 6 +- packages/cra-universal/templates/src/App.tsx | 43 +++++++---- .../templates/src/Content/Content1.tsx | 5 ++ .../templates/src/Content/Content2.tsx | 5 ++ .../templates/src/Content/lazy1.ts | 8 ++ .../templates/src/Content/lazy2.ts | 8 ++ 17 files changed, 130 insertions(+), 71 deletions(-) create mode 100644 packages/cra-universal/templates/src/Content/Content1.tsx create mode 100644 packages/cra-universal/templates/src/Content/Content2.tsx create mode 100644 packages/cra-universal/templates/src/Content/lazy1.ts create mode 100644 packages/cra-universal/templates/src/Content/lazy2.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c369371..cbe4ae6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,17 +44,19 @@ Since we need a way to test the cra-universal locally and seamlessly, we have a ## Running CRA Universal locally -Use `temp:start:client` to run client and server locally. +- Run `npm run temp:start:client` to run client locally. +- Run `npm run temp:start:server` to run server locally. - Normal CRA starts at http://localhost:3000 -- Server-rendered CRA Universal starts at http://localhost:3001 +- Server-rendered CRA Universal starts at http://localhost:3001 (you'll mostly use this for development) To run a production build of this app, preserving local CRA Universal, use these commands: ```sh -yarn temp:build -cd packages/cra-universal/templates -node --preserve-symlinks dist/server/bundle.js +npm run temp:build +cd packages/cra-universal/templates/dist +pnpm i +npm run serve ``` ### Webpack versions mismatch diff --git a/packages/@cra-express/core/package.json b/packages/@cra-express/core/package.json index cfb63ef1..a80a753b 100644 --- a/packages/@cra-express/core/package.json +++ b/packages/@cra-express/core/package.json @@ -1,6 +1,6 @@ { "name": "@cra-express/core", - "version": "5.0.1", + "version": "5.0.2", "description": "Core module for cra-universal server", "main": "lib/index.js", "files": [ @@ -20,8 +20,8 @@ "express": "^4.18.1" }, "dependencies": { - "@cra-express/static-loader": "^5", - "@cra-express/universal-loader": "^5" + "@cra-express/static-loader": "^5.0.2", + "@cra-express/universal-loader": "^5.0.2" }, "author": "Antony Budianto ", "license": "MIT" diff --git a/packages/@cra-express/redux-prefetcher/package.json b/packages/@cra-express/redux-prefetcher/package.json index 7ef301fe..d3a7f56b 100644 --- a/packages/@cra-express/redux-prefetcher/package.json +++ b/packages/@cra-express/redux-prefetcher/package.json @@ -1,6 +1,6 @@ { "name": "@cra-express/redux-prefetcher", - "version": "5.0.1", + "version": "5.0.2", "description": "Redux Prefetcher for prefetching on server", "main": "lib/index.js", "files": [ diff --git a/packages/@cra-express/router-prefetcher/package.json b/packages/@cra-express/router-prefetcher/package.json index 91d00bbf..7662a2da 100644 --- a/packages/@cra-express/router-prefetcher/package.json +++ b/packages/@cra-express/router-prefetcher/package.json @@ -1,6 +1,6 @@ { "name": "@cra-express/router-prefetcher", - "version": "5.0.1", + "version": "5.0.2", "description": "Router Prefetcher for prefetching on server", "main": "lib/index.js", "files": [ diff --git a/packages/@cra-express/static-loader/package.json b/packages/@cra-express/static-loader/package.json index 4cb63f6c..38f64b66 100644 --- a/packages/@cra-express/static-loader/package.json +++ b/packages/@cra-express/static-loader/package.json @@ -1,6 +1,6 @@ { "name": "@cra-express/static-loader", - "version": "5.0.1", + "version": "5.0.2", "description": "Static loader", "main": "lib/index.js", "files": [ diff --git a/packages/@cra-express/universal-loader/package.json b/packages/@cra-express/universal-loader/package.json index c75fc2ab..ec5370be 100644 --- a/packages/@cra-express/universal-loader/package.json +++ b/packages/@cra-express/universal-loader/package.json @@ -1,6 +1,6 @@ { "name": "@cra-express/universal-loader", - "version": "5.0.1", + "version": "5.0.2", "description": "Universal loader", "main": "lib/index.js", "files": [ diff --git a/packages/@cra-express/universal-loader/src/renderer/pipe-stream-renderer.js b/packages/@cra-express/universal-loader/src/renderer/pipe-stream-renderer.js index 580cda59..fab43e85 100644 --- a/packages/@cra-express/universal-loader/src/renderer/pipe-stream-renderer.js +++ b/packages/@cra-express/universal-loader/src/renderer/pipe-stream-renderer.js @@ -1,5 +1,15 @@ import { renderToPipeableStream } from 'react-dom/server'; +/** + * Reference: + * https://reactjs.org/docs/react-dom-server.html#rendertopipeablestream + * @param {Request} req + * @param {Response} res + * @param {JSX.Element} reactEl + * @param {string} htmlData + * @param {any} options + */ + export default function pipeStreamRenderer( req, res, @@ -9,49 +19,54 @@ export default function pipeStreamRenderer( ) { let str; let error; + const segments = htmlData.split(`
`); + const streaming = reactEl.props.streaming || false; - try { - const { pipe } = renderToPipeableStream(reactEl, { - onAllReady() { - /** - * Allows full customization - */ - if (typeof options.onAllReady === 'function') { - return options.onAllReady({ req, res, htmlData, error, pipe }); - } - - res.statusCode = error ? 500 : 200; - res.setHeader('Content-type', 'text/html'); + const processStream = (res, stream) => { + res.statusCode = error ? 500 : 200; + res.setHeader('Content-type', 'text/html'); - const segments = htmlData.split(`
`); - const errorScript = error - ? '' - : ''; + const errorScript = error + ? '' + : ''; - /** - * Return fallback when error - */ - if (error) { - res.send( - `${segments[0]}${errorScript}\n
${segments[1]}` - ); - return; - } + res.write(segments[0] + errorScript + '
'); - res.write(segments[0] + errorScript + '
'); + stream.pipe(res); + }; - pipe(res); + try { + const stream = renderToPipeableStream(reactEl, { + onShellReady() { + if (typeof options.onShellReady === 'function') { + return options.onShellReady({ req, res, htmlData, error, stream }); + } - if (typeof options.onEndReplace === 'function') { - segments[1] = options.onEndReplace(segments[1]); + if (streaming) { + processStream(res, stream); + } + }, + onAllReady() { + if (typeof options.onAllReady === 'function') { + return options.onAllReady({ req, res, htmlData, error, stream }); } + if (!streaming) { + processStream(res, stream); + } res.write(segments[1]); - res.end(); + }, + onError(e) { + error = true; + console.error('crau/ssr-on-error', e.message); }, onShellError(x) { - console.error('crau/ssr-shell-error: ', x.message); + /** + * Return fallback when error + */ error = true; + console.error('crau/ssr-on-shell-error: ', x.message); + res.send(`${segments[0]}${errorScript}\n
${segments[1]}`); }, }); } catch (e) { diff --git a/packages/babel-preset-cra-universal/index.js b/packages/babel-preset-cra-universal/index.js index 63c54ff3..b28804f7 100644 --- a/packages/babel-preset-cra-universal/index.js +++ b/packages/babel-preset-cra-universal/index.js @@ -4,6 +4,7 @@ var preset = { presets: [ [require.resolve('@babel/preset-env'), { modules: false }], [require.resolve('@babel/preset-react'), { runtime: 'automatic' }], + require.resolve('@babel/preset-typescript'), ], plugins: [ // class { handleThing = () => { } } diff --git a/packages/babel-preset-cra-universal/package.json b/packages/babel-preset-cra-universal/package.json index 7590151b..e5f1469a 100644 --- a/packages/babel-preset-cra-universal/package.json +++ b/packages/babel-preset-cra-universal/package.json @@ -1,6 +1,6 @@ { "name": "babel-preset-cra-universal", - "version": "5.0.1", + "version": "5.0.2", "main": "index.js", "description": "Babel preset for cra-universal", "repository": { @@ -20,6 +20,7 @@ "@babel/plugin-transform-react-jsx-source": "7.16.7", "@babel/preset-env": "7.17.10", "@babel/preset-react": "7.16.7", + "@babel/preset-typescript": "7.16.7", "babel-plugin-dynamic-import-node": "2.3.0", "babel-plugin-transform-react-remove-prop-types": "0.4.24" }, diff --git a/packages/cra-universal/package.json b/packages/cra-universal/package.json index 49ea1510..22f183df 100644 --- a/packages/cra-universal/package.json +++ b/packages/cra-universal/package.json @@ -1,6 +1,6 @@ { "name": "cra-universal", - "version": "5.0.1", + "version": "5.0.2", "description": "Create React App Universal CLI", "main": "src/index.js", "files": [ @@ -30,10 +30,10 @@ "dependencies": { "@babel/core": "7.17.10", "@babel/register": "7.17.7", - "@cra-express/core": "^5.0.1", + "@cra-express/core": "^5.0.2", "@svgr/webpack": "6.2.1", "babel-loader": "^8.2.5", - "babel-preset-cra-universal": "^5.0.1", + "babel-preset-cra-universal": "^5.0.2", "chalk": "^2.4.2", "css-loader": "^6.7.1", "del": "^5.1.0", diff --git a/packages/cra-universal/templates/package.json b/packages/cra-universal/templates/package.json index 1db75049..66580a93 100644 --- a/packages/cra-universal/templates/package.json +++ b/packages/cra-universal/templates/package.json @@ -1,13 +1,13 @@ { "name": "cra-template", - "version": "5.0.1", + "version": "5.0.2", "private": true, "devDependencies": { "better-npm-run": "^0.1.1", - "cra-universal": "^5.0.1" + "cra-universal": "^5.0.2" }, "dependencies": { - "@cra-express/core": "^5.0.1", + "@cra-express/core": "^5.0.2", "@types/webpack-env": "1.15.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", @@ -16,12 +16,13 @@ "@types/node": "^16.11.32", "@types/react": "^18.0.8", "@types/react-dom": "^18.0.3", - "react": "^18.1.0", - "react-dom": "^18.1.0", + "react": "18.2.0-next-e531a4a62-20220505", + "react-dom": "18.2.0-next-e531a4a62-20220505", "react-scripts": "5.0.1", "typescript": "^4.6.4", "web-vitals": "^2.1.4", - "express": "4.18.1" + "express": "4.18.1", + "react-error-boundary": "3.1.4" }, "scripts": { "crau:start": "cra-universal start", diff --git a/packages/cra-universal/templates/server/app.js b/packages/cra-universal/templates/server/app.js index 13b6e40e..0681c1ef 100644 --- a/packages/cra-universal/templates/server/app.js +++ b/packages/cra-universal/templates/server/app.js @@ -1,5 +1,5 @@ -const path = require('path'); -const React = require('react'); +import path from 'path'; +import React from 'react'; import { createReactAppExpress } from '@cra-express/core'; let App = require('../src/App').default; @@ -7,7 +7,7 @@ const clientBuildPath = path.resolve(__dirname, '../client'); const app = createReactAppExpress({ clientBuildPath, - universalRender: (req, res) => + universalRender: (req, res) => , }); if (module.hot) { diff --git a/packages/cra-universal/templates/src/App.tsx b/packages/cra-universal/templates/src/App.tsx index 24553285..e73e0c02 100644 --- a/packages/cra-universal/templates/src/App.tsx +++ b/packages/cra-universal/templates/src/App.tsx @@ -1,22 +1,35 @@ -import { Component } from 'react'; +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import './App.css'; import { ReactComponent as ReactSVG } from './react.svg'; +import LazyContent1 from './Content/lazy1'; +import LazyContent2 from './Content/lazy2'; -class App extends Component { - render() { - return ( -
-
-

Welcome to React

-
- -

- To get started, edit src/App.tsx and save to reload. -

+const ErrorFallback = () =>
Error
; + +const App = () => { + return ( +
+
+

Welcome to React

- ); - } -} + +

+ To get started, edit src/App.tsx and save to reload. +

+ + Loading, please wait...
}> + + + + +
+ ); +}; + +App.defaultProps = { + streaming: true, +}; export default App; diff --git a/packages/cra-universal/templates/src/Content/Content1.tsx b/packages/cra-universal/templates/src/Content/Content1.tsx new file mode 100644 index 00000000..a92c92f3 --- /dev/null +++ b/packages/cra-universal/templates/src/Content/Content1.tsx @@ -0,0 +1,5 @@ +const Content1 = () => { + return
LazyContent1 - with Suspense
; +}; + +export default Content1; diff --git a/packages/cra-universal/templates/src/Content/Content2.tsx b/packages/cra-universal/templates/src/Content/Content2.tsx new file mode 100644 index 00000000..07a96065 --- /dev/null +++ b/packages/cra-universal/templates/src/Content/Content2.tsx @@ -0,0 +1,5 @@ +const Content2 = () => { + return
LazyContent2 - no Suspense
; +}; + +export default Content2; diff --git a/packages/cra-universal/templates/src/Content/lazy1.ts b/packages/cra-universal/templates/src/Content/lazy1.ts new file mode 100644 index 00000000..09de3c72 --- /dev/null +++ b/packages/cra-universal/templates/src/Content/lazy1.ts @@ -0,0 +1,8 @@ +import { lazy } from 'react'; + +/** + * Demonstrate lazy with Suspense + */ +const LazyContent1 = lazy(() => import('./Content1')); + +export default LazyContent1; diff --git a/packages/cra-universal/templates/src/Content/lazy2.ts b/packages/cra-universal/templates/src/Content/lazy2.ts new file mode 100644 index 00000000..575ece93 --- /dev/null +++ b/packages/cra-universal/templates/src/Content/lazy2.ts @@ -0,0 +1,8 @@ +import { lazy } from 'react'; + +/** + * Demonstrate "always" SSR component + */ +const LazyContent2 = lazy(() => import('./Content2')); + +export default LazyContent2;