├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── cli.js ├── examples ├── basic.js ├── fetch.js └── styled-components.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | example 3 | .travis.yml 4 | test.js* 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # static-react 3 | 4 | Zero-configuration CLI React static renderer 5 | 6 | [![Build Status][badge]](https://travis-ci.org/jxnblk/static-react) 7 | 8 | [badge]: https://img.shields.io/travis/jxnblk/static-react.svg?style=flat-square 9 | 10 | 11 | ## Usage 12 | 13 | ``` 14 | npm i -g static-react 15 | ``` 16 | 17 | ``` 18 | static-react RootComponent.js > index.html 19 | ``` 20 | 21 | Static-react include babel presets and React – there is no need to install them separately 22 | 23 | ## Examples 24 | 25 | See the [examples/](examples) directory 26 | 27 | ## Fetching Data 28 | 29 | Use the `getInitialProps` static method to fetch data or get server-side props for things like CSS-in-JS libraries. 30 | 31 | ```jsx 32 | import React from 'react' 33 | import fetch from 'isomorphic-fetch' 34 | 35 | export default class extends React.Component { 36 | static getInitialProps = async () => { 37 | const data = await fetch('https://api.example.com/data') 38 | 39 | return { 40 | data 41 | } 42 | } 43 | 44 | render () { 45 | const { data } = this.props 46 | 47 | return ( 48 | 49 |

Data

50 |
 51 |           {JSON.stringify(data, null, 2)}
 52 |         
53 | 54 | ) 55 | } 56 | } 57 | ``` 58 | 59 | ## CSS-in-JS 60 | 61 | Use the `getInitialProps` to pass side effects from CSS-in-JS libraries as props. 62 | 63 | ```jsx 64 | import React from 'react' 65 | import { Box } from 'rebass' 66 | 67 | export default class Root extends React.Component { 68 | static getInitialProps = async (app) => { 69 | const { ServerStyleSheet } = require('styled-components') 70 | const { renderToString } = require('react-dom/server') 71 | const sheet = new ServerStyleSheet() 72 | renderToString( 73 | sheet.collectStyles(app) 74 | ) 75 | const css = sheet.getStyleTags() 76 | return { css } 77 | } 78 | 79 | static defaultProps = { 80 | css: '' 81 | } 82 | 83 | render () { 84 | const { css } = this.props 85 | 86 | return ( 87 | 88 | 89 | ${css} 90 | 91 | 92 | 93 | Beep boop 94 | 95 | 96 | 97 | ) 98 | } 99 | } 100 | ``` 101 | 102 | --- 103 | 104 | MIT License 105 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('babel-register')({ 3 | presets: [ 4 | 'babel-preset-env', 5 | 'babel-preset-stage-0', 6 | 'babel-preset-react' 7 | ].map(require.resolve), 8 | plugins: [ 9 | 'babel-plugin-transform-runtime' 10 | ].map(require.resolve) 11 | }) 12 | const fs = require('fs') 13 | const path = require('path') 14 | const meow = require('meow') 15 | const render = require('./index') 16 | const config = require('pkg-conf').sync('static-react') 17 | 18 | const cli = meow(` 19 | Usage 20 | $ static-react Component.js > index.html 21 | `, { 22 | flags: {} 23 | }) 24 | 25 | const [ input ] = cli.input 26 | const opts = Object.assign({}, config, cli.flags) 27 | const file = path.resolve(input) 28 | const mod = require(file) 29 | const Component = mod.default || mod 30 | 31 | render(Component, opts) 32 | .then(html => { 33 | console.log(html) 34 | }) 35 | .catch(err => { 36 | console.error(err) 37 | process.exit(1) 38 | }) 39 | 40 | // process.stdout.write(html) 41 | 42 | require('update-notifier')({ 43 | pkg: require('./package.json') 44 | }).notify() 45 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Root extends React.Component { 4 | static defaultProps = { 5 | title: 'static-react', 6 | } 7 | 8 | render () { 9 | const { title } = this.props 10 | 11 | return ( 12 | 13 | 14 | {title} 15 | 16 | 17 |

{title}

18 | 19 | 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/fetch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import fetch from 'isomorphic-fetch' 3 | 4 | const endpoint = 'https://microbeats.now.sh/tracks' 5 | 6 | export default class Root extends React.Component { 7 | static getInitialProps = async () => { 8 | const tracks = await fetch(endpoint).then(res => res.json()) 9 | return { 10 | tracks 11 | } 12 | } 13 | 14 | static defaultProps = { 15 | title: 'static-react', 16 | } 17 | 18 | render () { 19 | const { title, tracks } = this.props 20 | 21 | return ( 22 | 23 | 24 | {title} 25 | 26 | 27 |

{title}

28 | 35 | 36 | 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/styled-components.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Box 4 | } from 'rebass' 5 | 6 | export default class Root extends React.Component { 7 | static getInitialProps = async (el) => { 8 | const { ServerStyleSheet } = require('styled-components') 9 | const { renderToString } = require('react-dom/server') 10 | const sheet = new ServerStyleSheet() 11 | renderToString( 12 | sheet.collectStyles(el) 13 | ) 14 | const css = sheet.getStyleTags() 15 | return { css } 16 | } 17 | 18 | static defaultProps = { 19 | title: 'static-react', 20 | css: '' 21 | } 22 | 23 | render () { 24 | const { 25 | title, 26 | css 27 | } = this.props 28 | 29 | return ( 30 | 31 | 32 | {title} 33 | ${css} 34 | 35 | 36 | 37 |
38 |

{title}

39 |
40 |
41 | Kitten 43 |
44 | 47 |
48 | 49 | 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const h = require('react').createElement 3 | const { renderToStaticMarkup: render } = require('react-dom/server') 4 | 5 | module.exports = async (Component, opts = {}) => { 6 | const element = h(Component, opts) 7 | const props = typeof Component.getInitialProps === 'function' 8 | ? await Component.getInitialProps(element) 9 | : {} 10 | const html = render( 11 | h(Component, Object.assign({}, opts, props)) 12 | ) 13 | 14 | return `${html}` 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static-react", 3 | "version": "4.0.0-3", 4 | "description": "React static HTML generator", 5 | "author": "Brent Jackson", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "nyc ava", 9 | "start": "./cli.js examples/basic.js", 10 | "start:sc": "./cli.js examples/styled-components.js", 11 | "start:fetch": "./cli.js examples/fetch.js" 12 | }, 13 | "main": "index.js", 14 | "bin": { 15 | "static-react": "./cli.js" 16 | }, 17 | "dependencies": { 18 | "babel-plugin-transform-runtime": "^6.23.0", 19 | "babel-preset-env": "^1.7.0", 20 | "babel-preset-react": "^6.24.1", 21 | "babel-preset-stage-0": "^6.24.1", 22 | "babel-register": "^6.26.0", 23 | "meow": "^5.0.0", 24 | "pkg-conf": "^2.1.0", 25 | "react": "^16.4.1", 26 | "react-dom": "^16.4.1", 27 | "update-notifier": "^2.5.0" 28 | }, 29 | "devDependencies": { 30 | "ava": "^0.19.1", 31 | "is-html": "^1.0.0", 32 | "nyc": "^12.0.2", 33 | "rebass": "^2.0.0-6", 34 | "styled-components": "^3.3.2" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/jxnblk/static-react.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/jxnblk/static-react/issues" 42 | }, 43 | "homepage": "https://github.com/jxnblk/static-react" 44 | } 45 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { 3 | Component, 4 | createElement: h 5 | } = require('react') 6 | const render = require('./index') 7 | 8 | const Root = props => h('div', null, 9 | h('h1', null, props.title) 10 | ) 11 | 12 | class App extends Component { 13 | render () { 14 | return h('h1', null, this.props.hi) 15 | } 16 | } 17 | 18 | App.getInitialProps = async () => { 19 | return { hi: 'hello' } 20 | } 21 | 22 | test('exports a function', t => { 23 | t.is(typeof render, 'function') 24 | }) 25 | 26 | test('returns a string', async t => { 27 | const html = await render(Root) 28 | t.is(typeof html, 'string') 29 | }) 30 | 31 | test('accepts options', async t => { 32 | const html = await render(Root, { 33 | title: 'Hello', 34 | }) 35 | t.regex(html, /Hello/) 36 | }) 37 | 38 | test('getInitialProps', async t => { 39 | const html = await render(App) 40 | t.regex(html, /hello/) 41 | }) 42 | --------------------------------------------------------------------------------