Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/streaming-server-side-rendering/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"env": {
"browser": true
},
"rules": {
"import/no-unresolved": "off",
"import/no-extraneous-dependencies": "off"
}
}
1 change: 1 addition & 0 deletions examples/streaming-server-side-rendering/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public/dist
42 changes: 42 additions & 0 deletions examples/streaming-server-side-rendering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Get the streaming SSR example running

Steps:

1. Download repository

```bash
git clone https://github.com/gregberge/loadable-components.git
```

2. Install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already
3. Install libary dependencies and build library

```bash
yarn
yarn build
```

4. Move into example directory

```bash
cd ./loadable-components/examples/streaming-server-side-rendering
```

5. Install project dependencies

```bash
yarn
```

5. Run locally or build and serve

```bash
yarn dev

# Or

yarn build
yarn start
```

🍻
32 changes: 32 additions & 0 deletions examples/streaming-server-side-rendering/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
function isWebTarget(caller) {
return Boolean(caller && caller.target === 'web')
}

function isWebpack(caller) {
return Boolean(caller && caller.name === 'babel-loader')
}

module.exports = api => {
const web = api.caller(isWebTarget)
const webpack = api.caller(isWebpack)

return {
presets: [
'@babel/preset-react',
[
'@babel/preset-env',
{
useBuiltIns: web ? 'entry' : undefined,
corejs: web ? 'core-js@3' : false,
targets: !web ? { node: 'current' } : undefined,
modules: webpack ? false : 'commonjs',
},
],
],
plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin',
/* ["transform-define", {
"process.env.NODE_ENV": process.env.NODE_ENV,
}] */
],
}
}
6 changes: 6 additions & 0 deletions examples/streaming-server-side-rendering/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ignore": ["client", "public"],
"execMap": {
"js": "babel-node"
}
}
39 changes: 39 additions & 0 deletions examples/streaming-server-side-rendering/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"private": true,
"scripts": {
"dev": "nodemon src/server/main.js",
"build": "rm -Rf ./public && NODE_ENV=production yarn build:webpack && yarn build:lib",
"build:webpack": "webpack",
"build:lib": "babel -d lib src",
"start": "NODE_ENV=production node lib/server/main.js",
"link:all": "yarn link @loadable/babel-plugin && yarn link @loadable/server && yarn link @loadable/component"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.6.2",
"@babel/node": "^7.0.0",
"@babel/preset-env": "^7.6.2",
"@babel/preset-react": "^7.0.0",
"@loadable/babel-plugin": "file:./../../packages/babel-plugin",
"@loadable/component": "file:./../../packages/component",
"@loadable/server": "file:./../../packages/server",
"@loadable/webpack-plugin": "file:./../../packages/webpack-plugin",
"babel-loader": "^8.0.6",
"babel-plugin-transform-define": "^2.1.0",
"css-loader": "^2.1.1",
"mini-css-extract-plugin": "^0.6.0",
"nodemon": "^1.19.0",
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2",
"webpack-dev-middleware": "^3.6.2",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"core-js": "^3.0.1",
"express": "^4.18.2",
"moment": "^2.24.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4"
}
}
50 changes: 50 additions & 0 deletions examples/streaming-server-side-rendering/src/client/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import { ErrorBoundary } from 'react-error-boundary';

// eslint-disable-next-line import/no-extraneous-dependencies
import { lazy } from '@loadable/component'
import Html from './Html'

const A = lazy(() => import('./letters/A'))
const B = lazy(() => import('./letters/B'))
const C = lazy(() => import(/* webpackPreload: true */ './letters/C'))
const D = lazy(() => import(/* webpackPrefetch: true */ './letters/D'))
const E = lazy(() => import('./letters/E?param'), { ssr: false })
const X = lazy(props => import(`./letters/${props.letter}`))
const Sub = lazy(props => import(`./letters/${props.letter}/file`))
const RootSub = lazy(props => import(`./${props.letter}/file`))

const App = () => {
return (
<Html title="Hello">
<React.Suspense fallback="Loading">
<ErrorBoundary FallbackComponent={Error}>
<A />
<br />
<B />
<br />
<X letter="A" />
<br />
<X letter="F" />
<br />
<E />
<br />
<Sub letter="Z" />
<br />
<RootSub letter="Y" />
</ErrorBoundary>
</React.Suspense>
</Html>
)
}

function Error({ error }) {
return (
<div>
<h1>Application Error</h1>
<pre style={{ whiteSpace: 'pre-wrap' }}>{error.stack}</pre>
</div>
);
}

export default App
23 changes: 23 additions & 0 deletions examples/streaming-server-side-rendering/src/client/Html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'

export default function Html({ 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" />
<title>{title}</title>
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`
}}
/>
{children}
</body>
</html>
);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'Y'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* A CSS */
body {
background: pink;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// We simulate that "moment" is called in "A" and "B"

const A = () => 'A'


export default A
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// We simulate that "moment" is called in "A" and "B"

const B = () => 'B'


export default B
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'C'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'D'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'E'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'F'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react'

const G = ({ prefix }) => <span className="my-cool-class">{prefix} - G</span>

export default G
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'Z'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './App'

export const hello = 'hello'
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'core-js'
import React from 'react'
import { hydrateRoot } from "react-dom/client";
// eslint-disable-next-line import/no-extraneous-dependencies
import App from './App'

hydrateRoot(document, <App />);
4 changes: 4 additions & 0 deletions examples/streaming-server-side-rendering/src/client/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* Main CSS */
h1 {
color: cyan;
}
128 changes: 128 additions & 0 deletions examples/streaming-server-side-rendering/src/server/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import path from 'path'
import express, { json } from 'express'
import React from 'react'
import { renderToPipeableStream } from 'react-dom/server';
import { ChunkExtractor } from '@loadable/server'
import { Writable } from 'stream';
import fs from 'fs';

const app = express()

app.use(express.static(path.join(__dirname, '../../public')))

if (process.env.NODE_ENV !== 'production') {
/* eslint-disable global-require, import/no-extraneous-dependencies */
const { default: webpackConfig } = require('../../webpack.config.babel')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
/* eslint-enable global-require, import/no-extraneous-dependencies */

const compiler = webpack(webpackConfig)

app.use(
webpackDevMiddleware(compiler, {
logLevel: 'silent',
publicPath: '/dist/web',
writeToDisk(filePath) {
return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath)
},
}),
)
}

const nodeStats = path.resolve(
__dirname,
'../../public/dist/node/loadable-stats.json',
)

const webStats = path.resolve(
__dirname,
'../../public/dist/web/loadable-stats.json',
)

app.get('*', (req, res) => {
let didError = false;
let shellReady = false;
let firstWrite = true;

let statsNode = JSON.parse(fs.readFileSync(nodeStats))
let statsWeb = JSON.parse(fs.readFileSync(webStats))


const nodeExtractor = new ChunkExtractor({ stats: statsNode })
const { default: App } = nodeExtractor.requireEntrypoint()

const webExtractor = new ChunkExtractor({ stats: statsWeb })

const writeable = new Writable({
write(chunk, encoding, callback) {
// This should pick up any new link tags that hasn't been previously
// written to this stream. Should not write before html if nothing suspended.
if (shellReady && !firstWrite) {
const scriptTags = webExtractor.getScriptTagsSince()
const linkTags = webExtractor.getLinkTagsSince()
if (scriptTags) {
res.write(scriptTags, encoding)
}
if (linkTags) {
res.write(linkTags, encoding)
}
// Finally write whatever React tried to write.
}
firstWrite = false
res.write(chunk, encoding, callback)
},
final(callback) {
res.end()
callback()
},
flush() {
if (typeof res.flush === 'function') {
res.flush();
}
},
destroy(err) {
res.destroy(err ?? undefined)
}
})

const stream = renderToPipeableStream(webExtractor.collectChunks(<App />),
{
onShellReady() {
// The content above all Suspense boundaries is ready.
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
stream.pipe(writeable);
shellReady = true;

},
onShellError(error) {
// Something errored before we could complete the shell so we emit an alternative shell.
res.statusCode = 500;
res.send(
'<!doctype html><p>Loading...</p><script src="clientrender.js"></script>'
);
},
onAllReady() {
// If you don't want streaming, use this instead of onShellReady.
// This will fire after the entire page content is ready.
// You can use this for crawlers or static generation.
// If nothing suspends, make sure scripts are written
writeable.write('')
// res.statusCode = didError ? 500 : 200;
// res.setHeader('Content-type', 'text/html');
// stream.pipe(res);
},
onError(err) {
didError = true;
console.error(err);
},
}
);


});

// eslint-disable-next-line no-console
app.listen(9000, () => console.log('Server started http://localhost:9000'))
Loading