14 |
15 | Options
16 | --port, -p Server port
17 | --raw, -r Serve raw output with no doctype declaration
18 | --bundle, -b Render with bundled javascript
19 | --noWrap, -n Opt out of wrapping component in a div
20 | `, {
21 | flags: {
22 | port: {
23 | type: 'string',
24 | alias: 'p'
25 | },
26 | raw: {
27 | type: 'boolean',
28 | alias: 'r'
29 | },
30 | bundle: {
31 | type: 'boolean',
32 | alias: 'b'
33 | },
34 | noWrap: {
35 | type: 'boolean',
36 | default: false,
37 | alias: 'n'
38 | }
39 | }
40 | })
41 |
42 | const [ file ] = cli.input
43 | const filename = path.join(process.cwd(), file)
44 | const opts = Object.assign({}, cli.flags, {
45 | filename,
46 | port: parseInt(cli.flags.port || 3000)
47 | })
48 |
49 | start(opts)
50 | .then(server => {
51 | if (!server.address()) {
52 | console.log(`failed to start server on ${cli.flags.port || 3000}`)
53 | process.exit(1)
54 | }
55 |
56 | const { port } = server.address()
57 | console.log(`listening on port ${port}`)
58 | })
59 | .catch(err => {
60 | console.log(err)
61 | process.exit(1)
62 | })
63 |
--------------------------------------------------------------------------------
/bundle.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const { Readable } = require('stream')
4 | const babel = require('babel-core')
5 | const browserify = require('browserify')
6 | const { minify } = require('uglify-es')
7 |
8 | const parse = (filename, raw) => babel.transform(raw, {
9 | filename,
10 | presets: [
11 | require('babel-preset-env'),
12 | require('babel-preset-stage-0'),
13 | require('babel-preset-react'),
14 | ],
15 | compact: true,
16 | minified: true,
17 | comments: false,
18 | }).code
19 |
20 | const browser = (filename, code) => {
21 | const stream = new Readable
22 | stream.push(code)
23 | stream.push(null)
24 | const dirname = path.dirname(filename)
25 |
26 | return new Promise((resolve, reject) => {
27 | browserify(stream, {
28 | basedir: dirname
29 | })
30 | .bundle((err, res) => {
31 | if (err) reject(err)
32 | else {
33 | const script = res.toString()
34 | resolve(script)
35 | }
36 | })
37 | })
38 | }
39 |
40 | const bundle = async filename => {
41 | process.env.NODE_ENV = 'production'
42 | const raw = fs.readFileSync(filename)
43 | const component = parse(filename, raw)
44 | const entry = createEntry(component)
45 | const script = await browser(filename, entry)
46 | const min = minify(script).code
47 | return min
48 | }
49 |
50 | // current doesn't handle async components
51 | const createEntry = component => (`
52 | ${component}
53 | const { hydrate } = require('react-dom')
54 |
55 | const props = JSON.parse(
56 | initial_props.innerHTML
57 | )
58 | const el = React.createElement(App, props)
59 | hydrate(el, div)
60 | `)
61 |
62 | module.exports = bundle
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # micro-react
3 |
4 | Create microservice apps with React components
5 |
6 | ```sh
7 | npm i micro-react
8 | ```
9 |
10 | ## Usage
11 |
12 | Create a server-side compatible React component
13 |
14 | ```jsx
15 | // App.js
16 | const React = require('react')
17 |
18 | const App = props => (
19 | Hello
20 | )
21 |
22 | module.exports = App
23 | ```
24 |
25 | Add a start script to `package.json`
26 |
27 | ```json
28 | "scripts": {
29 | "start": "micro-react App.js"
30 | }
31 | ```
32 |
33 | Start the server with `npm start`
34 |
35 | ## Request Object
36 |
37 | The Node.js http request object is passed as `props.req`
38 |
39 | ```jsx
40 | const React = require('react')
41 |
42 | const App = props => (
43 | Hello {props.req.url}
44 | )
45 |
46 | module.exports = App
47 | ```
48 |
49 | ## Response Object
50 |
51 | The Node.js http response object is passed as `props.res`.
52 | This can be used to set headers if you want to, for example, change the content type to `image/svg+xml`.
53 |
54 | ```jsx
55 | const React = require('react')
56 |
57 | const SvgIcon = require('./SvgIcon')
58 |
59 | module.exports = props => {
60 | props.res.setHeader('Content-Type', 'image/svg+xml')
61 | return
62 | }
63 | ```
64 |
65 | ## Async Components
66 |
67 | Use async functions to fetch data and handle other asynchronous tasks before rendering.
68 |
69 | ```jsx
70 | const React = require('react')
71 | const fetch = require('node-fetch')
72 |
73 | const App = async props => {
74 | const res = await fetch('http://example.com/data')
75 | const data = await res.json()
76 |
77 | return (
78 | Hello {data}
79 | )
80 | }
81 |
82 | module.exports = App
83 | ```
84 |
85 | ## Client-side JS
86 |
87 | By default micro-react only serves static HTML.
88 | Pass the `--bundle` flag to create a browser-compatible bundle on start,
89 | that will be sent in the request after the React Node stream has finished.
90 |
91 | ```sh
92 | micro-react App.js --bundle
93 | ```
94 |
95 | See the [examples](docs) for more.
96 |
97 | [MIT License](LICENSE.md)
98 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('babel-register')({
2 | presets: [
3 | 'babel-preset-react'
4 | ].map(require.resolve)
5 | })
6 |
7 | const http = require('http')
8 | const url = require('url')
9 | const React = require('react')
10 | const {
11 | renderToNodeStream,
12 | renderToStaticNodeStream
13 | } = require('react-dom/server')
14 | const jsonStringify = require('json-stringify-safe')
15 |
16 | const bundle = require('./bundle')
17 |
18 | const start = async (opts) => {
19 | opts.port = opts.port || 3000
20 | if (opts.bundle) {
21 | opts.script = await bundle(opts.filename)
22 | console.log('bundle size: ' + opts.script.length + ' bytes')
23 | }
24 | const App = require(opts.filename)
25 | const server = http.createServer(handleRequest(App, opts))
26 | return await server.listen(opts.port)
27 | }
28 |
29 | const handleRequest = (App, opts) => async (req, res) => {
30 | if (!opts.raw && !opts.noWrap) res.write(header)
31 | if (!opts.noWrap) res.write('')
32 | const props = Object.assign({}, opts, { req, res })
33 |
34 | delete props.script
35 |
36 | const el = isAsync(App)
37 | ? await createAsyncElement(App, props)
38 | : React.createElement(App, props)
39 | const stream = opts.bundle
40 | ? renderToNodeStream(el)
41 | : renderToStaticNodeStream(el)
42 |
43 | stream.pipe(res, { end: false })
44 |
45 | stream.on('end', async () => {
46 | if (!opts.noWrap) res.write('
')
47 | if (opts.script) {
48 | const json = jsonStringify(props)
49 | res.write(``)
50 | res.write(``)
51 | }
52 |
53 | res.end()
54 | })
55 |
56 | stream.on('error', error => {
57 | console.error(error)
58 | res.end()
59 | })
60 | }
61 |
62 | const isAsync = fn => fn.constructor.name === 'AsyncFunction'
63 |
64 | const createAsyncElement = async (Component, props) =>
65 | await Component(props)
66 |
67 | const header = `
68 |
69 | `
70 |
71 | module.exports = start
72 |
--------------------------------------------------------------------------------