├── .gitignore ├── .prettierignore ├── .travis.yml ├── README.md ├── jest.config.js ├── modules ├── __mocks__ │ ├── ContextComponent.js │ └── TestComponent.js ├── __tests__ │ └── server-test.js ├── cli.js └── index.js ├── package-lock.json ├── package.json └── scripts └── build.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 8 3 | cache: 4 | directories: 5 | - "$HOME/.npm" 6 | - "$HOME/.pkg-cache" 7 | env: 8 | - NPM_TAG=$([[ "$TRAVIS_TAG" == *-* ]] && echo "next" || echo "latest") 9 | script: 10 | - npm test 11 | - npm run build 12 | deploy: 13 | - provider: npm 14 | skip_cleanup: true 15 | email: npm@mjackson.me 16 | tag: "$NPM_TAG" 17 | api_key: 18 | secure: Ouhk1VI9D8JUihTJl16S2yVYHu1ngFEbmrrq6KJzTCD69N4NiyvXzMYhN97K/R0twQqnxS10aAufPvXnFtTvQz48/USGp34gSzLySSqWCXsCpOothsew4SUDqHZPdLIUJqwhpYyJsyCtDbwf8cOTgxlbXW+DE8Y1s0LTJjbbJrg2RlwNjIdYeAjfDQuO2nL3zHPAGJQ/mYJEyicZ6T2H46O4kLuzWvocvUYnEV/j/gtBtBCukg9UG//YxHJVlNfeRXYCiQ/mf5ReCY7zgghK+eJXiaKJZ+l/hV2sp7C893YwKs1IQJ5kn/liwtJfqLGSKrsw7jwu1AESx1jhQsdhCJx5pj61BUTtAGF1acetiD7/MmIHcuDUYqiVsK38UYUxW3XUIOOWH8FpcAJ3+tOpO34kDd4LCBxe09BMbDaGruuSh7+JmbBKNgvX+kJu8b4I80vRs0SrrF9lqdjQ9q/BqwnxbBWXhCZo6dDZEEwwNtLyz6hiuafnZKZgZGb9gcNQ/kwccSnZuifhnNU5nHJL9eJJrAL+XY0l8OWVOMbmuX+iSW7b0yVWqgf3bTiAez2fUUhx1bWFQlOPo/Z5I8y2zZjzCKdFOk59J4hFAhtMGkw3LJmoCyqkVGxR2x1TnH0z8R8ScBZafV8RK4/GDQnOxNIPHIPtdRn8XIXtGkev/YE= 19 | on: 20 | tags: true 21 | condition: '"$TRAVIS_TAG" =~ ^v[0-9]' 22 | - provider: releases 23 | skip_cleanup: true 24 | file: build/*.zip 25 | file_glob: true 26 | prerelease: true 27 | api_key: 28 | secure: RFzhM7HoiSqoOt79Y1Obx7RkgO4UujKLTyqgci6vln0x5A/DchUh7rErpvDLNzvrDeIIp5S41MeQ+CJuVnL5BSWvhsf50/qmSfY2iky3Gl8r5qDG0uKffwPv5Pw+TmtiS3hy9n7PY4Py6u+2fIUChK7OKbPfvvjGLNWAqUi+B9U1c9qOBkHg2LkH+XdtO4dwsjHB/mZ4s10PntLfI6mA0RXIW8/mhYD4gYTWg/DtuenNMcZKDAP+z+GWZr8cen7haMOtoYqY9SA103VgeudXpSzZr5JKSMGnx6YrHicsGaRIal69sC6RspelJsW2+6yaQtIWH8zvb9IvFPv9QP3xYEftVkqxJXr9fsDBVFZIkrF3fCBf7at8UT1CVz3kEH07oc+RZahnVBNqvTbelO32whY2wSV7QBVg6xf0UEeP+un9eBIn1M9AUXiH90KskNVu+Gcs0kNqurpo/cEXXcAuGxK/2KGyfJkg9MKx1B6FmyW1QOIS8ZGOwiUha77fudDq+PUTrjByY2NEyk1GhUmildyoGx5Ds+vwqgBHHkGu7VVitltX7SnnMh48E3AeylYHE2iMSfDRujRefOgL3sEAnHjhU3yBnfRdPsD7jdi/Rux1qzbpVdK5u7gPxrr9SHYc49zVd6e7075UC7twiV99NQoYUpI+7f2q1KffRRt+q24= 29 | on: 30 | tags: true 31 | condition: '"$TRAVIS_TAG" =~ ^v[0-9] && "$NPM_TAG" = "next"' 32 | - provider: releases 33 | skip_cleanup: true 34 | file: build/*.zip 35 | file_glob: true 36 | prerelease: false 37 | api_key: 38 | secure: RFzhM7HoiSqoOt79Y1Obx7RkgO4UujKLTyqgci6vln0x5A/DchUh7rErpvDLNzvrDeIIp5S41MeQ+CJuVnL5BSWvhsf50/qmSfY2iky3Gl8r5qDG0uKffwPv5Pw+TmtiS3hy9n7PY4Py6u+2fIUChK7OKbPfvvjGLNWAqUi+B9U1c9qOBkHg2LkH+XdtO4dwsjHB/mZ4s10PntLfI6mA0RXIW8/mhYD4gYTWg/DtuenNMcZKDAP+z+GWZr8cen7haMOtoYqY9SA103VgeudXpSzZr5JKSMGnx6YrHicsGaRIal69sC6RspelJsW2+6yaQtIWH8zvb9IvFPv9QP3xYEftVkqxJXr9fsDBVFZIkrF3fCBf7at8UT1CVz3kEH07oc+RZahnVBNqvTbelO32whY2wSV7QBVg6xf0UEeP+un9eBIn1M9AUXiH90KskNVu+Gcs0kNqurpo/cEXXcAuGxK/2KGyfJkg9MKx1B6FmyW1QOIS8ZGOwiUha77fudDq+PUTrjByY2NEyk1GhUmildyoGx5Ds+vwqgBHHkGu7VVitltX7SnnMh48E3AeylYHE2iMSfDRujRefOgL3sEAnHjhU3yBnfRdPsD7jdi/Rux1qzbpVdK5u7gPxrr9SHYc49zVd6e7075UC7twiV99NQoYUpI+7f2q1KffRRt+q24= 39 | on: 40 | tags: true 41 | condition: '"$TRAVIS_TAG" =~ ^v[0-9] && "$NPM_TAG" = "latest"' 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-stdio [![Travis][build-badge]][build] [![npm package][npm-badge]][npm] 2 | 3 | [build-badge]: https://img.shields.io/travis/ReactTraining/react-stdio/master.svg?style=flat-square 4 | [build]: https://travis-ci.org/ReactTraining/react-stdio 5 | [npm-badge]: https://img.shields.io/npm/v/react-stdio.svg?style=flat-square 6 | [npm]: https://www.npmjs.org/package/react-stdio 7 | 8 | [react-stdio](https://npmjs.org/package/react-stdio) lets you render [React](https://reactjs.org/) components on the server, regardless of the backend technology you're using. 9 | 10 | As its name suggests, other processes communicate with react-stdio using standard streams. The protocol is JSON, so any environment that can spawn a child process and write JSON to its stdin can use the server. Requests are handled serially, so responses are issued in the same order requests are received. 11 | 12 | ## Installation 13 | 14 | If you have node installed, you can install using npm: 15 | 16 | $ npm install -g react-stdio 17 | 18 | This will put the `react-stdio` executable in your [`npm bin`](https://docs.npmjs.com/cli/bin). 19 | 20 | If you don't have node installed, you can download the executable for your architecture from [the releases page](https://github.com/ReactTraining/react-stdio/releases). 21 | 22 | ## Usage 23 | 24 | After installation, execute `react-stdio` to start the server. 25 | 26 | To render a React component, write a JSON blob to stdin with any of the following properties: 27 | 28 | component The path to a file that exports a React component (required) 29 | props Any props you want to pass to the component (optional, default is {}) 30 | render The type of rendering (optional, default is renderToString) 31 | 32 | If the request is successful, the server will put a JSON blob with `{"html":"...","context":...}` on stdout. If the request fails for some reason, the JSON will have an `error` property instead of `html`. 33 | 34 | Example: 35 | 36 | $ echo '{"component":"./MyComponent","props":{"message":"hello"}}' | react-stdio 37 | 38 | If you'd like to use a render method other than [`renderToString`](https://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostring) or [`renderToStaticMarkup`](https://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostaticmarkup) you can pass a path to a file that exports your rendering function. The signature of your `render` function should be: 39 | 40 | ```js 41 | function render(element, callback) { 42 | // ... 43 | } 44 | ``` 45 | 46 | This function is asynchronous so you have time to do data fetching before you render if you wish. Call `callback(error, html)` when you're finished. 47 | 48 | ## Environment 49 | 50 | Your component file is loaded in a vanilla node.js environment. If you need additional code transforms to run (e.g. using webpack or Browserify) you should create your bundle first and tell react-stdio to load your bundle instead of the plain component file. If you're using webpack to build your bundle, you'll want to use `"libraryTarget": "commonjs2"` in your config so the bundle exports the component using `module.exports = MyComponent`. 51 | 52 | Also, since react-stdio uses the `stdout` stream for all program output, all writes your code makes to `process.stdout` (including `console.log` statements) are redirected to `process.stderr`. 53 | 54 | ## Integrations 55 | 56 | * [Elixir/Phoenix](http://blog.overstuffedgorilla.com/render-react-with-phoenix/) 57 | * [Ruby on Rails](https://github.com/aaronvb/rails_react_stdio) 58 | 59 | If you'd like to add an integration here, please submit a PR. 60 | 61 | ## About 62 | 63 | react-stdio is developed and maintained by [React Training](https://reacttraining.com). If you're interested in learning more about what React can do for your company, please [get in touch](mailto:hello@reacttraining.com)! 64 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost/' 3 | }; 4 | -------------------------------------------------------------------------------- /modules/__mocks__/ContextComponent.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | 3 | let context = {}; 4 | 5 | function ContextComponent() { 6 | context.test = true; 7 | return React.createElement("div", null, "I am a context component"); 8 | } 9 | 10 | ContextComponent.context = context; 11 | 12 | module.exports = ContextComponent; 13 | -------------------------------------------------------------------------------- /modules/__mocks__/TestComponent.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | 3 | function TestComponent() { 4 | return React.createElement("div", null, "I am a test component"); 5 | } 6 | 7 | module.exports = TestComponent; 8 | -------------------------------------------------------------------------------- /modules/__tests__/server-test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const childProcess = require('child_process'); 3 | 4 | function mock(name) { 5 | return path.join(__dirname, '..', '__mocks__', name); 6 | } 7 | 8 | describe('server', () => { 9 | let proc; 10 | 11 | beforeEach(() => { 12 | proc = childProcess.spawn(path.join(__dirname, '..', 'cli.js'), { 13 | stdio: 'pipe' 14 | }); 15 | }); 16 | 17 | afterEach(() => { 18 | proc.kill(); 19 | }); 20 | 21 | it('throws an error when component is missing', done => { 22 | proc.stdin.write(JSON.stringify({})); 23 | 24 | proc.stdout.once('data', out => { 25 | expect(JSON.parse(out).error).toEqual('Missing { component } in request'); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('throws an error when component cannot be found', done => { 31 | proc.stdin.write(JSON.stringify({ component: 'component.js' })); 32 | 33 | proc.stdout.once('data', out => { 34 | expect(JSON.parse(out).error).toEqual( 35 | 'Cannot load component: component.js' 36 | ); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('renders the component', done => { 42 | proc.stdin.write( 43 | JSON.stringify({ 44 | component: mock('TestComponent.js') 45 | }) 46 | ); 47 | 48 | proc.stdout.once('data', out => { 49 | expect(JSON.parse(out).html).toMatch('I am a test component'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('renders a component and exposes additional context', done => { 55 | proc.stdin.write( 56 | JSON.stringify({ 57 | component: mock('ContextComponent.js') 58 | }) 59 | ); 60 | 61 | proc.stdout.once('data', out => { 62 | const result = JSON.parse(out); 63 | expect(result.html).toMatch('I am a context component'); 64 | expect(result.context).toEqual({ test: true }); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /modules/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const EventStream = require("event-stream"); 4 | const JSONStream = require("JSONStream"); 5 | const createRequestHandler = require("./index").createRequestHandler; 6 | 7 | // Redirect stdout to stderr, but save a reference so we can 8 | // still write to stdout. 9 | const stdout = process.stdout; 10 | Object.defineProperty(process, "stdout", { 11 | configurable: true, 12 | enumerable: true, 13 | value: process.stderr 14 | }); 15 | 16 | // Ensure console.log knows about the new stdout. 17 | const Console = require("console").Console; 18 | Object.defineProperty(global, "console", { 19 | configurable: true, 20 | enumerable: true, 21 | value: new Console(process.stdout, process.stderr) 22 | }); 23 | 24 | // Read JSON blobs from stdin, pipe output to stdout. 25 | process.stdin 26 | .pipe(JSONStream.parse()) 27 | .pipe(EventStream.map(createRequestHandler(process.cwd()))) 28 | .pipe(stdout); 29 | -------------------------------------------------------------------------------- /modules/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const invariant = require("invariant"); 3 | const ReactDOMServer = require("react-dom/server"); 4 | const React = require("react"); 5 | 6 | function loadModule(moduleId) { 7 | // Clear the require cache, in case the file was 8 | // changed since the server was started. 9 | const cacheKey = require.resolve(moduleId); 10 | delete require.cache[cacheKey]; 11 | 12 | const moduleExports = require(moduleId); 13 | 14 | // Return exports.default if using ES modules. 15 | if (moduleExports && moduleExports.default) { 16 | return moduleExports.default; 17 | } 18 | 19 | return moduleExports; 20 | } 21 | 22 | function renderToStaticMarkup(element, callback) { 23 | callback(null, ReactDOMServer.renderToStaticMarkup(element)); 24 | } 25 | 26 | function renderToString(element, callback) { 27 | callback(null, ReactDOMServer.renderToString(element)); 28 | } 29 | 30 | function handleRequest(workingDir, request, callback) { 31 | const componentPath = request.component; 32 | const renderMethod = request.render; 33 | const props = request.props; 34 | 35 | invariant(componentPath != null, "Missing { component } in request"); 36 | 37 | let render; 38 | if (renderMethod == null || renderMethod === "renderToString") { 39 | render = renderToString; 40 | } else if (renderMethod === "renderToStaticMarkup") { 41 | render = renderToStaticMarkup; 42 | } else { 43 | const methodFile = path.resolve(workingDir, renderMethod); 44 | 45 | try { 46 | render = loadModule(methodFile); 47 | } catch (error) { 48 | if (error.code !== "MODULE_NOT_FOUND") { 49 | process.stderr.write(error.stack + "\n"); 50 | } 51 | } 52 | } 53 | 54 | invariant( 55 | typeof render === "function", 56 | "Cannot load render method: %s", 57 | renderMethod 58 | ); 59 | 60 | const componentFile = path.resolve(workingDir, componentPath); 61 | 62 | let component; 63 | try { 64 | component = loadModule(componentFile); 65 | } catch (error) { 66 | if (error.code !== "MODULE_NOT_FOUND") { 67 | process.stderr.write(error.stack + "\n"); 68 | } 69 | } 70 | 71 | invariant(component != null, "Cannot load component: %s", componentPath); 72 | 73 | render(React.createElement(component, props), function(err, html) { 74 | callback(err, html, component.context); 75 | }); 76 | } 77 | 78 | function createRequestHandler(workingDir) { 79 | return function(request, callback) { 80 | try { 81 | handleRequest(workingDir, request, function(error, html, context) { 82 | if (error) { 83 | callback(error); 84 | } else if (typeof html !== "string") { 85 | // Crash the server process. 86 | callback(new Error("Render method must return a string")); 87 | } else { 88 | callback(null, JSON.stringify({ html: html, context: context })); 89 | } 90 | }); 91 | } catch (error) { 92 | callback(null, JSON.stringify({ error: error.message })); 93 | } 94 | }; 95 | } 96 | 97 | module.exports = { 98 | createRequestHandler 99 | }; 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stdio", 3 | "version": "3.4.8", 4 | "description": "Render React.js components on any backend", 5 | "author": "Michael Jackson", 6 | "license": "MIT", 7 | "bin": { 8 | "react-stdio": "./modules/cli.js" 9 | }, 10 | "files": [ 11 | "modules/cli.js", 12 | "modules/index.js" 13 | ], 14 | "preferGlobal": true, 15 | "repository": "ReactTraining/react-stdio", 16 | "scripts": { 17 | "build": "./scripts/build.sh", 18 | "clean": "git clean -fdX .", 19 | "test": "jest" 20 | }, 21 | "dependencies": { 22 | "JSONStream": "^1.0.7", 23 | "event-stream": "3.3.4", 24 | "invariant": "^2.2.0", 25 | "react": "^16.2.0", 26 | "react-dom": "^16.2.0" 27 | }, 28 | "devDependencies": { 29 | "jest": "^22.1.4", 30 | "pkg": "^4.3.4" 31 | }, 32 | "jest": { 33 | "testPathIgnorePatterns": [ 34 | "/node_modules/", 35 | "__tests__/mocks" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p build 4 | 5 | tag=${TRAVIS_TAG:-"v$npm_package_version"} 6 | 7 | # TODO: Enable x86 builds when https://github.com/zeit/pkg/issues/310 is fixed 8 | platforms=( win-x64 linux-x64 macos ) 9 | 10 | for platform in "${platforms[@]}" 11 | do 12 | archive=react-stdio-$tag-$platform 13 | 14 | echo "Creating $archive build..." 15 | 16 | pkg modules/cli.js -t $platform -o build/$archive/react-stdio 17 | echo "$tag" > build/$archive/version 18 | 19 | cd build 20 | zip -q -r $archive.zip $archive 21 | cd - > /dev/null 22 | done 23 | --------------------------------------------------------------------------------