Skip to content

Commit 4c4a04e

Browse files
committed
first blood
1 parent c21f230 commit 4c4a04e

16 files changed

+1446
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib
2+
node_modules

.travis.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
language: node_js
2+
3+
node_js: stable
4+
5+
cache: yarn
6+
7+
before_install:
8+
- curl -o- -L https://yarnpkg.com/install.sh | bash
9+
- export PATH="$HOME/.yarn/bin:$PATH"
10+
- yarn global add greenkeeper-lockfile
11+
12+
before_script: greenkeeper-lockfile-update
13+
14+
script:
15+
- yarn lint
16+
- npx tsc
17+
18+
after_script: greenkeeper-lockfile-upload

package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "react-server-renderer",
3+
"version": "0.0.1",
4+
"description": "simple React SSR solution inspired by vue-server-render",
5+
"repository": "git@github.com:JounQin/react-server-render.git",
6+
"main": "lib/index.js",
7+
"types": "lib/index.d.ts",
8+
"author": "JounQin <admin@1stg.me>",
9+
"license": "MIT",
10+
"scripts": {
11+
"lint": "tslint -p . -e 'node_modules/**' -e 'lib/**' -t stylish '**/*.ts'",
12+
"prepublishOnly": "tsc"
13+
},
14+
"files": ["lib"],
15+
"prettier": {
16+
"semi": false,
17+
"singleQuote": true,
18+
"trailingComma": "all",
19+
"overrides": [
20+
{
21+
"files": "*.json",
22+
"options": {
23+
"printWidth": 150
24+
}
25+
}
26+
]
27+
},
28+
"peerDependencies": {
29+
"react": "^16.2.0",
30+
"react-dom": "^16.2.0",
31+
"serialize-javascript": "^1.4.0"
32+
},
33+
"dependencies": {
34+
"lodash": "^4.17.4",
35+
"resolve": "^1.5.0",
36+
"source-map": "^0.6.1"
37+
},
38+
"devDependencies": {
39+
"@types/node": "^8.5.2",
40+
"@types/react": "^16.0.31",
41+
"@types/react-dom": "^16.0.3",
42+
"prettier": "^1.9.2",
43+
"react": "^16.2.0",
44+
"react-dom": "^16.2.0",
45+
"serialize-javascript": "^1.4.0",
46+
"tslint": "^5.8.0",
47+
"tslint-config-prettier": "^1.6.0",
48+
"tslint-plugin-prettier": "^1.3.0",
49+
"typescript": "^2.6.2"
50+
}
51+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import { PassThrough } from 'stream'
4+
5+
import { ReactElement } from 'react'
6+
7+
import { RenderOptions, Renderer } from '../create-renderer'
8+
import { UserContext, createPromiseCallback } from '../util'
9+
import { createBundleRunner } from './create-bundle-runner'
10+
import {
11+
createSourceMapConsumers,
12+
rewriteErrorTrace,
13+
} from './source-map-support'
14+
15+
const INVALID_MSG =
16+
'Invalid server-rendering bundle format. Should be a string ' +
17+
'or a bundle Object of type:\n\n' +
18+
`{
19+
entry: string;
20+
files: { [filename: string]: string; };
21+
maps: { [filename: string]: string; };
22+
}\n`
23+
24+
// The render bundle can either be a string (single bundled file)
25+
// or a bundle manifest object generated by ssr-webpack-plugin.
26+
export interface RenderBundle {
27+
basedir?: string
28+
entry: string
29+
files: { [filename: string]: string }
30+
maps: { [filename: string]: string }
31+
modules?: { [filename: string]: string[] }
32+
}
33+
34+
export function createBundleRendererCreator(
35+
createRenderer: (options?: RenderOptions) => Renderer,
36+
) {
37+
return function createBundleRenderer(
38+
bundle: string | RenderBundle,
39+
rendererOptions: RenderOptions = {},
40+
) {
41+
let files
42+
let entry
43+
let maps
44+
45+
let { basedir } = rendererOptions
46+
47+
// load bundle if given filepath
48+
if (
49+
typeof bundle === 'string' &&
50+
/\.js(on)?$/.test(bundle) &&
51+
path.isAbsolute(bundle)
52+
) {
53+
if (fs.existsSync(bundle)) {
54+
const isJSON = /\.json$/.test(bundle)
55+
basedir = basedir || path.dirname(bundle)
56+
bundle = fs.readFileSync(bundle, 'utf-8')
57+
if (isJSON) {
58+
try {
59+
bundle = JSON.parse(bundle as string)
60+
} catch (e) {
61+
throw new Error(`Invalid JSON bundle file: ${bundle}`)
62+
}
63+
}
64+
} else {
65+
throw new Error(`Cannot locate bundle file: ${bundle}`)
66+
}
67+
}
68+
69+
if (typeof bundle === 'object') {
70+
entry = bundle.entry
71+
files = bundle.files
72+
basedir = basedir || bundle.basedir
73+
maps = createSourceMapConsumers(bundle.maps)
74+
if (typeof entry !== 'string' || typeof files !== 'object') {
75+
throw new Error(INVALID_MSG)
76+
}
77+
} else if (typeof bundle === 'string') {
78+
entry = '__react_ssr_bundle__'
79+
files = { __react_ssr_bundle__: bundle }
80+
maps = {}
81+
} else {
82+
throw new Error(INVALID_MSG)
83+
}
84+
85+
const renderer = createRenderer(rendererOptions)
86+
87+
const run = createBundleRunner(
88+
entry,
89+
files,
90+
basedir,
91+
rendererOptions.runInNewContext,
92+
)
93+
94+
return {
95+
renderToString: (context?: UserContext, cb?: any) => {
96+
if (typeof context === 'function') {
97+
cb = context
98+
context = {}
99+
}
100+
101+
let promise
102+
if (!cb) {
103+
;({ promise, cb } = createPromiseCallback())
104+
}
105+
106+
run(context)
107+
.catch(err => {
108+
rewriteErrorTrace(err, maps)
109+
cb(err)
110+
})
111+
.then((app: ReactElement<any>) => {
112+
if (app) {
113+
renderer.renderToString(app, context, (err, res) => {
114+
rewriteErrorTrace(err, maps)
115+
cb(err, res)
116+
})
117+
}
118+
})
119+
120+
return promise
121+
},
122+
123+
renderToStream: (context?: UserContext) => {
124+
const res = new PassThrough()
125+
run(context)
126+
.catch(err => {
127+
rewriteErrorTrace(err, maps)
128+
// avoid emitting synchronously before user can
129+
// attach error listener
130+
process.nextTick(() => {
131+
res.emit('error', err)
132+
})
133+
})
134+
.then((app: ReactElement<any>) => {
135+
if (app) {
136+
const renderStream = renderer.renderToStream(app, context)
137+
138+
renderStream.on('error', err => {
139+
rewriteErrorTrace(err, maps)
140+
res.emit('error', err)
141+
})
142+
143+
// relay HTMLStream special events
144+
if (rendererOptions && rendererOptions.template) {
145+
renderStream.on('beforeStart', () => {
146+
res.emit('beforeStart')
147+
})
148+
renderStream.on('beforeEnd', () => {
149+
res.emit('beforeEnd')
150+
})
151+
}
152+
153+
renderStream.pipe(res)
154+
}
155+
})
156+
157+
return res
158+
},
159+
}
160+
}
161+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import * as path from 'path'
2+
import * as vm from 'vm'
3+
4+
import { isPlainObject } from 'lodash'
5+
import * as _resolve from 'resolve'
6+
7+
import { UserContext } from '../util'
8+
9+
// tslint:disable-next-line no-var-requires
10+
const NativeModule = require('module')
11+
12+
function createSandbox(context?) {
13+
const sandbox = {
14+
Buffer,
15+
console,
16+
process,
17+
setTimeout,
18+
setInterval,
19+
setImmediate,
20+
clearTimeout,
21+
clearInterval,
22+
clearImmediate,
23+
__REACT_SSR_CONTEXT__: context,
24+
} as any
25+
sandbox.global = sandbox
26+
return sandbox
27+
}
28+
29+
function compileModule(files, basedir, runInNewContext) {
30+
const compiledScripts = {}
31+
const resolvedModules = {}
32+
33+
function getCompiledScript(filename) {
34+
if (compiledScripts[filename]) {
35+
return compiledScripts[filename]
36+
}
37+
const code = files[filename]
38+
const wrapper = NativeModule.wrap(code)
39+
const script = new vm.Script(wrapper, {
40+
filename,
41+
displayErrors: true,
42+
})
43+
compiledScripts[filename] = script
44+
return script
45+
}
46+
47+
function evaluateModule(filename, sandbox, evaluatedFiles = {}) {
48+
if (evaluatedFiles[filename]) {
49+
return evaluatedFiles[filename]
50+
}
51+
52+
const script = getCompiledScript(filename)
53+
const compiledWrapper =
54+
runInNewContext === false
55+
? script.runInThisContext()
56+
: script.runInNewContext(sandbox)
57+
const m = { exports: {} } as any
58+
const r = file => {
59+
file = path.posix.join('.', file)
60+
if (files[file]) {
61+
return evaluateModule(file, sandbox, evaluatedFiles)
62+
} else if (basedir) {
63+
return require(resolvedModules[file] ||
64+
(resolvedModules[file] = _resolve.sync(file, { basedir })))
65+
} else {
66+
return require(file)
67+
}
68+
}
69+
compiledWrapper.call(m.exports, m.exports, r, m)
70+
71+
const res = Object.prototype.hasOwnProperty.call(m.exports, 'default')
72+
? m.exports.default
73+
: m.exports
74+
evaluatedFiles[filename] = res
75+
return res
76+
}
77+
return evaluateModule
78+
}
79+
80+
function deepClone(val) {
81+
if (isPlainObject(val)) {
82+
const res = {}
83+
// tslint:disable-next-line forin
84+
for (const key in val) {
85+
res[key] = deepClone(val[key])
86+
}
87+
return res
88+
} else if (Array.isArray(val)) {
89+
return val.slice()
90+
} else {
91+
return val
92+
}
93+
}
94+
95+
export function createBundleRunner(entry, files, basedir, runInNewContext) {
96+
const evaluate = compileModule(files, basedir, runInNewContext)
97+
if (runInNewContext !== false && runInNewContext !== 'once') {
98+
// new context mode: creates a fresh context and re-evaluate the bundle
99+
// on each render. Ensures entire application state is fresh for each
100+
// render, but incurs extra evaluation cost.
101+
return (userContext: UserContext = {}) =>
102+
new Promise(resolve => {
103+
userContext._registeredComponents = new Set()
104+
const res = evaluate(entry, createSandbox(userContext))
105+
resolve(typeof res === 'function' ? res(userContext) : res)
106+
})
107+
} else {
108+
// direct mode: instead of re-evaluating the whole bundle on
109+
// each render, it simply calls the exported function. This avoids the
110+
// module evaluation costs but requires the source code to be structured
111+
// slightly differently.
112+
let runner // lazy creation so that errors can be caught by user
113+
let initialContext
114+
return (userContext: UserContext = {}) =>
115+
new Promise(resolve => {
116+
if (!runner) {
117+
const sandbox = runInNewContext === 'once' ? createSandbox() : global
118+
// the initial context is only used for collecting possible non-component
119+
// styles injected by react-style-loader.
120+
initialContext = sandbox.__REACT_SSR_CONTEXT__ = {}
121+
runner = evaluate(entry, sandbox)
122+
// On subsequent renders, __REACT_SSR_CONTEXT__ will not be available
123+
// to prevent cross-request pollution.
124+
delete sandbox.__REACT_SSR_CONTEXT__
125+
if (typeof runner !== 'function') {
126+
throw new Error(
127+
'bundle export should be a function when using ' +
128+
'{ runInNewContext: false }.',
129+
)
130+
}
131+
}
132+
userContext._registeredComponents = new Set()
133+
134+
// react-style-loader styles imported outside of component lifecycle hooks
135+
if (initialContext._styles) {
136+
userContext._styles = deepClone(initialContext._styles)
137+
// #6353 ensure "styles" is exposed even if no styles are injected
138+
// in component lifecycles.
139+
// the renderStyles fn is exposed by react-style-loader >= 3.0.3
140+
const renderStyles = initialContext._renderStyles
141+
if (renderStyles) {
142+
Object.defineProperty(userContext, 'styles', {
143+
enumerable: true,
144+
get() {
145+
return renderStyles(userContext._styles)
146+
},
147+
})
148+
}
149+
}
150+
151+
resolve(runner(userContext))
152+
})
153+
}
154+
}

0 commit comments

Comments
 (0)