Skip to content

Commit d82fe89

Browse files
committed
feat: move webpack plugin into this package, better workflow
1 parent 1627fae commit d82fe89

File tree

13 files changed

+1631
-55
lines changed

13 files changed

+1631
-55
lines changed

README.md

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# react-server-renderer
22

33
[![Greenkeeper badge](https://badges.greenkeeper.io/JounQin/react-server-renderer.svg)](https://greenkeeper.io/)
4+
[![Travis](https://img.shields.io/travis/JounQin/react-server-renderer.svg)](https://travis-ci.org/JounQin/react-server-renderer)
5+
[![David](https://img.shields.io/david/JounQin/react-server-renderer.svg)](https://david-dm.org/JounQin/react-server-renderer)
6+
[![David Dev](https://img.shields.io/david/dev/JounQin/react-server-renderer.svg)](https://david-dm.org/JounQin/react-server-renderer?type=dev)
7+
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
48

59
Yet another simple React SSR solution inspired by vue-server-render with:
610

711
1. Server bundle with hot reload on development and source map support
8-
2. prefetch/preload client injection with ClientManifest, generated by [ssr-webpack-plugin](https://github.com/JounQin/ssr-webpack-plugin)
12+
2. prefetch/preload client injection with ClientManifest, generated by webpack-plugin inside
913
3. server css support with [react-style-loader](https://github.com/JounQin/react-style-loader)
1014
4. Async component support with [react-async-component](https://github.com/ctrlplusb/react-async-component) and [react-async-bootstrapper](https://github.com/ctrlplusb/react-async-bootstrapper)
1115
5. custom dynamic head management for better SEO
@@ -18,7 +22,7 @@ Yet another simple React SSR solution inspired by vue-server-render with:
1822

1923
This module is heavily inspired by [vue-server-render](https://ssr.vuejs.org), it is recommended to read about [bundle-renderer](https://ssr.vuejs.org/en/bundle-renderer.html).
2024

21-
If you're using [react-router](https://github.com/ReactTraining/react-router), you should read about [Server Rendering](https://reacttraining.com/react-router/web/guides/server-rendering).
25+
It uses [react-router](https://github.com/ReactTraining/react-router) on server, so you should read about [Server Rendering](https://reacttraining.com/react-router/web/guides/server-rendering).
2226

2327
And also, data injection should be implement with [asyncBootstrap](https://github.com/ctrlplusb/react-async-bootstrapper).
2428

@@ -30,7 +34,7 @@ And also, data injection should be implement with [asyncBootstrap](https://githu
3034
import webpack from 'webpack'
3135
import merge from 'webpack-merge'
3236
import nodeExternals from 'webpack-node-externals'
33-
import { SSRServerPlugin } from 'ssr-webpack-plugin'
37+
import { ReactSSRServerPlugin } from 'react-server-renderer/server-plugin'
3438

3539
import { resolve } from './config'
3640

@@ -58,7 +62,7 @@ export default merge.smart(base, {
5862
// and generates a smaller bundle file.
5963
externals: nodeExternals({
6064
// do not externalize dependencies that need to be processed by webpack.
61-
// you can add more file types here e.g. raw *.vue files
65+
// you can add more file types here
6266
// you should also whitelist deps that modifies `global` (e.g. polyfills)
6367
whitelist: /\.s?css$/,
6468
}),
@@ -70,8 +74,8 @@ export default merge.smart(base, {
7074
}),
7175
// This is the plugin that turns the entire output of the server build
7276
// into a single JSON file. The default file name will be
73-
// `ssr-server-bundle.json`
74-
new SSRServerPlugin(),
77+
// `react-ssr-server-bundle.json`
78+
new ReactSSRServerPlugin(),
7579
],
7680
})
7781
```
@@ -83,7 +87,7 @@ import webpack from 'webpack'
8387
import merge from 'webpack-merge'
8488
// do not need 'html-webpack-plugin' any more because we will render html from server
8589
// import HtmlWebpackPlugin from 'html-webpack-plugin'
86-
import { SSRClientPlugin } from 'ssr-webpack-plugin'
90+
import { ReactSSRClientPlugin } from 'react-server-renderer/client-plugin'
8791

8892
import { __DEV__, publicPath, resolve } from './config'
8993

@@ -92,15 +96,6 @@ import base from './base'
9296
export default merge.smart(base, {
9397
entry: {
9498
app: [resolve('src/entry-client.js')],
95-
vendors: [
96-
'react',
97-
'react-dom',
98-
'react-redux',
99-
'react-router',
100-
'react-router-redux',
101-
'react-router-config',
102-
'react-router-dom',
103-
],
10499
},
105100
output: {
106101
publicPath,
@@ -112,14 +107,11 @@ export default merge.smart(base, {
112107
'process.env.REACT_ENV': '"client"',
113108
__SERVER__: false,
114109
}),
115-
new webpack.optimize.CommonsChunkPlugin({
116-
names: ['vendors', 'manifest'],
117-
}),
118-
// This plugins generates `ssr-client-manifest.json` in the
110+
// This plugins generates `react-ssr-client-manifest.json` in the
119111
// output directory.
120-
new SSRClientPlugin({
121-
// path relative to your output path, default to be `ssr-client-manifest.json`
122-
filename: '../ssr-client-manifest.json',
112+
new ReactSSRClientPlugin({
113+
// path relative to your output path, default to be `react-ssr-client-manifest.json`
114+
filename: '../react-ssr-client-manifest.json',
123115
}),
124116
],
125117
})
@@ -131,8 +123,8 @@ You can then use the generated client manifest, together with a page template:
131123
const { createBundleRenderer } = require('react-server-renderer')
132124

133125
const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
134-
const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
135-
const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
126+
const serverBundle = require('/path/to/react-ssr-server-bundle.json')
127+
const clientManifest = require('/path/to/react-ssr-client-manifest.json')
136128

137129
const renderer = createBundleRenderer(serverBundle, {
138130
template,
@@ -349,8 +341,8 @@ Then `react-server-renderer` will automatically collect user styles and title on
349341

350342
Notes:
351343

352-
* Use double-mustache (HTML-escaped interpolation) to avoid XSS attacks.
353-
* You should provide a default title when creating the context object in case no component has set a title during render.
344+
- Use double-mustache (HTML-escaped interpolation) to avoid XSS attacks.
345+
- You should provide a default title when creating the context object in case no component has set a title during render.
354346

355347
Using the same strategy, you can easily expand it into a generic head management utility.
356348

package.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-server-renderer",
3-
"version": "0.3.0",
3+
"version": "1.0.0",
44
"description": "simple React SSR solution inspired by vue-server-render",
55
"repository": "git@github.com:JounQin/react-server-renderer.git",
66
"main": "lib/index.js",
@@ -9,11 +9,27 @@
99
"license": "MIT",
1010
"scripts": {
1111
"lint": "tslint -p . -t stylish",
12-
"prepublishOnly": "tsc"
12+
"build": "tsc"
1313
},
1414
"files": [
1515
"lib"
1616
],
17+
"husky": {
18+
"hooks": {
19+
"pre-commit": "lint-staged",
20+
"pre-push": "tsc"
21+
}
22+
},
23+
"lint-staged": {
24+
"*.ts": [
25+
"tslint -t stylish --fix",
26+
"git add"
27+
],
28+
"*.{md,json}": [
29+
"prettier --write",
30+
"git add"
31+
]
32+
},
1733
"prettier": {
1834
"semi": false,
1935
"singleQuote": true,
@@ -24,20 +40,27 @@
2440
"react-dom": "^16.4.2"
2541
},
2642
"dependencies": {
43+
"hash-sum": "^1.0.2",
2744
"lodash": "^4.17.11",
45+
"lodash.uniq": "^4.5.0",
2846
"resolve": "^1.8.1",
2947
"serialize-javascript": "^1.5.0",
3048
"source-map": "^0.7.0"
3149
},
3250
"devDependencies": {
51+
"@types/hash-sum": "^1.0.0",
52+
"@types/lodash.uniq": "^4.5.4",
3353
"@types/node": "^10.11.7",
3454
"@types/react": "^16.4.16",
3555
"@types/react-dom": "^16.0.9",
56+
"husky": "^1.1.2",
57+
"lint-staged": "^7.3.0",
3658
"prettier": "^1.14.3",
3759
"react": "^16.5.2",
3860
"react-dom": "^16.5.2",
3961
"tslint": "^5.11.0",
4062
"tslint-config-prettier": "^1.15.0",
63+
"tslint-config-standard": "^8.0.1",
4164
"tslint-plugin-prettier": "^2.0.0",
4265
"typescript": "^3.1.3"
4366
}

src/bundle-renderer/create-bundle-renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function createBundleRendererCreator(
5656
bundle = fs.readFileSync(bundle, 'utf-8')
5757
if (isJSON) {
5858
try {
59-
bundle = JSON.parse(bundle as string)
59+
bundle = JSON.parse(bundle)
6060
} catch (e) {
6161
throw new Error(`Invalid JSON bundle file: ${bundle}`)
6262
}

src/client-plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ReactSSRClientPlugin } from './webpack-plugin/client'

src/index.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
// tslint:disable no-unused-variable
2-
import { PassThrough } from 'stream'
3-
4-
import {
5-
RenderBundle,
6-
createBundleRendererCreator,
7-
} from './bundle-renderer/create-bundle-renderer'
1+
import { createBundleRendererCreator } from './bundle-renderer/create-bundle-renderer'
82
import { createRenderer } from './create-renderer'
9-
import { TemplateRendererOptions } from './template-renderer'
10-
import { UserContext } from './util'
113

124
process.env.REACT_ENV = 'server'
135

src/server-plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ReactSSRServerPlugin } from './webpack-plugin/server'

src/template-renderer/template-stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default class TemplateStream extends Transform {
2626
this.inject = renderer.inject
2727
}
2828

29-
_transform(data: Buffer | string, encoding: string, done: () => void) {
29+
_transform(data: Buffer | string, _encoding: string, done: () => void) {
3030
if (!this.started) {
3131
this.emit('beforeStart')
3232
this.start()

src/webpack-plugin/client.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import hash from 'hash-sum'
2+
import uniq from 'lodash.uniq'
3+
import { isCSS, isJS, onEmit } from './util'
4+
5+
export class ReactSSRClientPlugin {
6+
options: {
7+
filename?: string
8+
}
9+
10+
constructor(options = {}) {
11+
this.options = Object.assign(
12+
{
13+
filename: 'react-ssr-client-manifest.json',
14+
},
15+
options,
16+
)
17+
}
18+
19+
apply(compiler) {
20+
onEmit(compiler, 'react-client-plugin', (compilation, cb) => {
21+
const stats = compilation.getStats().toJson()
22+
23+
const allFiles = uniq<string>(stats.assets.map(a => a.name))
24+
25+
const initialFiles = uniq(
26+
Object.keys(stats.entrypoints)
27+
.map(name => stats.entrypoints[name].assets)
28+
.reduce((assets, all) => all.concat(assets), [])
29+
.filter(file => isJS(file) || isCSS(file)),
30+
)
31+
32+
const asyncFiles = allFiles
33+
.filter(file => isJS(file) || isCSS(file))
34+
.filter(file => initialFiles.indexOf(file) < 0)
35+
36+
const manifest = {
37+
publicPath: stats.publicPath,
38+
all: allFiles,
39+
initial: initialFiles,
40+
async: asyncFiles,
41+
modules: {
42+
/* [identifier: string]: Array<index: number> */
43+
},
44+
}
45+
46+
const assetModules = stats.modules.filter(m => m.assets.length)
47+
const fileToIndex = file => manifest.all.indexOf(file)
48+
stats.modules.forEach(m => {
49+
// ignore modules duplicated in multiple chunks
50+
if (m.chunks.length === 1) {
51+
const cid = m.chunks[0]
52+
const chunk = stats.chunks.find(c => c.id === cid)
53+
if (!chunk || !chunk.files) {
54+
return
55+
}
56+
const id = m.identifier.replace(/\s\w+$/, '') // remove appended hash
57+
const files = (manifest.modules[hash(id)] = chunk.files.map(
58+
fileToIndex,
59+
))
60+
// find all asset modules associated with the same chunk
61+
assetModules.forEach(module => {
62+
if (module.chunks.some(chunkId => chunkId === cid)) {
63+
files.push.apply(files, module.assets.map(fileToIndex))
64+
}
65+
})
66+
}
67+
})
68+
69+
// const debug = (file, obj) => {
70+
// require('fs').writeFileSync(__dirname + '/' + file, JSON.stringify(obj, null, 2))
71+
// }
72+
// debug('stats.json', stats)
73+
// debug('client-manifest.json', manifest)
74+
75+
const json = JSON.stringify(manifest, null, 2)
76+
compilation.assets[this.options.filename] = {
77+
source: () => json,
78+
size: () => json.length,
79+
}
80+
cb()
81+
})
82+
}
83+
}

src/webpack-plugin/server.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { isJS, onEmit, validate } from './util'
2+
3+
export class ReactSSRServerPlugin {
4+
options: {
5+
filename?: string
6+
}
7+
8+
constructor(options = {}) {
9+
this.options = Object.assign(
10+
{
11+
filename: 'react-ssr-server-bundle.json',
12+
},
13+
options,
14+
)
15+
}
16+
17+
apply(compiler) {
18+
validate(compiler)
19+
20+
onEmit(compiler, 'react-server-plugin', (compilation, cb) => {
21+
const stats = compilation.getStats().toJson()
22+
const entryName = Object.keys(stats.entrypoints)[0]
23+
const entryInfo = stats.entrypoints[entryName]
24+
25+
if (!entryInfo) {
26+
// #5553
27+
return cb()
28+
}
29+
30+
const entryAssets = entryInfo.assets.filter(isJS)
31+
32+
if (entryAssets.length > 1) {
33+
throw new Error(
34+
`Server-side bundle should have one single entry file. ` +
35+
`Avoid using CommonsChunkPlugin in the server config.`,
36+
)
37+
}
38+
39+
const entry = entryAssets[0]
40+
if (!entry || typeof entry !== 'string') {
41+
throw new Error(
42+
`Entry "${entryName}" not found. Did you specify the correct entry option?`,
43+
)
44+
}
45+
46+
const bundle = {
47+
entry,
48+
files: {},
49+
maps: {},
50+
}
51+
52+
stats.assets.forEach(asset => {
53+
if (asset.name.match(/\.js$/)) {
54+
bundle.files[asset.name] = compilation.assets[asset.name].source()
55+
} else if (asset.name.match(/\.js\.map$/)) {
56+
bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(
57+
compilation.assets[asset.name].source(),
58+
)
59+
}
60+
// do not emit anything else for server
61+
delete compilation.assets[asset.name]
62+
})
63+
64+
const json = JSON.stringify(bundle, null, 2)
65+
const filename = this.options.filename
66+
67+
compilation.assets[filename] = {
68+
source: () => json,
69+
size: () => json.length,
70+
}
71+
72+
cb()
73+
})
74+
}
75+
}

0 commit comments

Comments
 (0)