├── .babelrc ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── rollup-min.config.js ├── rollup.config.js ├── src ├── __tests__ │ └── asyncBootstrapper.test.js └── index.js ├── tools ├── .eslintrc ├── scripts │ └── build.js └── utils.js ├── wallaby.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": true 6 | } 7 | }], "stage-3", "react" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependencies 6 | node_modules 7 | 8 | # Debug log from npm 9 | npm-debug.log 10 | 11 | # Jest 12 | coverage 13 | 14 | # Build 15 | dist 16 | 17 | # IDEs 18 | .vscode 19 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | yarn: true 5 | directories: 6 | - node_modules 7 | node_js: 8 | - '8' 9 | script: 10 | - npm run precommit 11 | after_success: 12 | # Deploy code coverage report to codecov.io 13 | - npm run test:coverage:deploy 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sean Matheson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-async-bootstrapper 👢 2 | 3 | Execute a `bootstrap` method on your React/Preact components. Useful for data prefetching and other activities. 4 | 5 | [![npm](https://img.shields.io/npm/v/react-async-bootstrapper.svg?style=flat-square)](http://npm.im/react-async-bootstrapper) 6 | [![MIT License](https://img.shields.io/npm/l/react-async-bootstrapper.svg?style=flat-square)](http://opensource.org/licenses/MIT) 7 | [![Travis](https://img.shields.io/travis/ctrlplusb/react-async-bootstrapper.svg?style=flat-square)](https://travis-ci.org/ctrlplusb/react-async-bootstrapper) 8 | [![Codecov](https://img.shields.io/codecov/c/github/ctrlplusb/react-async-bootstrapper.svg?style=flat-square)](https://codecov.io/github/ctrlplusb/react-async-bootstrapper) 9 | 10 | ## TOCs 11 | 12 | * [Introduction](#introduction) 13 | * [Simple Example](#simple-example) 14 | 15 | ## Introduction 16 | 17 | This library is a simple implementation of [`react-tree-walker`](https://github.com/ctrlplusb/react-tree-walker), allowing you to attach a `bootstrap` method to your React/Preact "class" components. I would highly recommend you review `react-tree-walkers` documentation so as to gain more familiarity with what is being wrapped up by `react-bootstrapper`. 18 | 19 | I have created this implementation that responds to a `bootstrap` method to allow me to have a standard implementation that would allow for interop between multiple packages requiring a bootstrapping process. For example I have create [`react-async-component`](https://github.com/ctrlplusb/react-async-component) which provides code splitting support, and [`react-jobs`](https://github.com/ctrlplusb/react-jobs) which enables data fetching. Both packages use this library to allow for a single bootstrapping parse satisfying the needs of both. 20 | 21 | ## Simple Example 22 | 23 | ```jsx 24 | import bootstrapper from 'react-async-bootstrapper' 25 | 26 | // Our super naive global state. Don't copy this, it's just illustrative. You'd 27 | // likely want to use something 28 | const globalStateManager = { 29 | products: {}, 30 | } 31 | 32 | class Product extends Component { 33 | // 👇 34 | bootstrap() { 35 | // Fetch our product and load up our state 36 | return fetch(`/api/products/${this.props.productId}`).then(response => { 37 | // store in our global state 38 | globalStateManager.products[this.props.productId] = response.json() 39 | }) 40 | } 41 | 42 | render() { 43 | const product = globalStateManager.products[this.props.productId] 44 | return ( 45 |
46 | {product.name} - {product.price} 47 |
48 | ) 49 | } 50 | } 51 | 52 | const app = ( 53 |
54 |

My favourite product

55 | 56 |
57 | ) 58 | 59 | // Now for the bootstrapping/rendering process (on a client/server) 60 | bootstrapper(app) 61 | .then(() => { 62 | // Bootstrapping is complete, now when we render our application to the DOM 63 | // the global products state will be populated and so our components 64 | // should render immediately with the data. 65 | ReactDOM.render(app, document.getElementById('app')) 66 | }) 67 | .catch(err => console.log('Eek, error!', err)) 68 | ``` 69 | 70 | Yep, not a particularly useful idea in the context of executing on the front end only, but when doing server side rendering of your react application this pattern can be extremely useful. 71 | 72 | ## API 73 | 74 | The API is very simple at the moment, only exposing a single function. 75 | 76 | ### **bootstrapper** 77 | 78 | The default export of the library. The function that performs the magic. 79 | 80 | ```javascript 81 | const bootstrapper = require('react-async-bootstrapper') 82 | ``` 83 | 84 | _or_ 85 | 86 | ```javascript 87 | import bootstrapper from 'react-async-bootstrapper' 88 | ``` 89 | 90 | **Paramaters** 91 | 92 | * **app** (React/Preact application/element, _required_) 93 | 94 | The react application you wish to walk. 95 | 96 | e.g. `
Hello world
` 97 | 98 | * **options** (`Object`, _optional_) 99 | 100 | Additional options/configuration. It currently supports the following values: 101 | 102 | * _componentWillUnmount_: Enable this to have the `componentWillUnmount` lifecycle event be executed during the bootstrapping process. Defaults to `false`. This was added as an experimental additional flag to help with applications where they have critical disposal logic being executed within the `componentWillUnmount` lifecycle event. 103 | 104 | * **context** (`Object`, _optional_) 105 | 106 | Any context you wish to expose to your application. This will become available to the entire application and could be useful for exposing configuration to your `bootstrap` methods. 107 | 108 | e.g. `{ myContextItem: 'foo' }` 109 | 110 | **Returns** 111 | 112 | A `Promise` that resolves when the bootstrapping has completed. 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-async-bootstrapper", 3 | "version": "2.1.1", 4 | "description": "Execute a bootstrap method on your React/Preact components. Useful for data prefetching and other activities.", 5 | "license": "MIT", 6 | "main": "dist/react-async-bootstrapper.js", 7 | "files": [ 8 | "*.js", 9 | "*.md", 10 | "dist" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ctrlplusb/react-async-bootstrapper.git" 15 | }, 16 | "homepage": "https://github.com/ctrlplusb/react-async-bootstrapper#readme", 17 | "author": "Sean Matheson ", 18 | "keywords": [ 19 | "library" 20 | ], 21 | "scripts": { 22 | "build": "node ./tools/scripts/build.js", 23 | "clean": "rimraf ./dist && rimraf ./coverage", 24 | "lint": "eslint src", 25 | "precommit": "lint-staged && npm run test", 26 | "prepublish": "npm run build", 27 | "test": "jest", 28 | "test:coverage": "npm run test -- --coverage", 29 | "test:coverage:deploy": "npm run test:coverage && codecov" 30 | }, 31 | "peerDependencies": { 32 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0", 33 | "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" 34 | }, 35 | "devDependencies": { 36 | "app-root-dir": "1.0.2", 37 | "babel-cli": "^6.26.0", 38 | "babel-core": "^6.26.3", 39 | "babel-eslint": "^8.2.6", 40 | "babel-jest": "^23.4.2", 41 | "babel-plugin-external-helpers": "^6.22.0", 42 | "babel-polyfill": "^6.26.0", 43 | "babel-preset-env": "^1.7.0", 44 | "babel-preset-latest": "^6.24.1", 45 | "babel-preset-react": "^6.24.1", 46 | "babel-preset-stage-3": "^6.24.1", 47 | "babel-register": "^6.26.0", 48 | "change-case": "^3.0.2", 49 | "codecov": "^3.0.4", 50 | "cross-env": "^5.2.0", 51 | "enzyme": "^3.4.1", 52 | "enzyme-to-json": "^3.3.4", 53 | "eslint": "^4.19.1", 54 | "eslint-config-airbnb": "^16.0.0", 55 | "eslint-config-prettier": "^2.6.0", 56 | "eslint-plugin-import": "^2.7.0", 57 | "eslint-plugin-jsx-a11y": "^6.0.2", 58 | "eslint-plugin-react": "^7.4.0", 59 | "gzip-size": "^5.0.0", 60 | "husky": "^0.14.3", 61 | "in-publish": "2.0.0", 62 | "jest": "^23.4.2", 63 | "lint-staged": "^7.2.0", 64 | "prettier": "^1.14.2", 65 | "pretty-bytes": "5.1.0", 66 | "ramda": "^0.25.0", 67 | "react": "^16.4.2", 68 | "react-addons-test-utils": "^15.6.2", 69 | "react-dom": "^16.4.2", 70 | "readline-sync": "1.4.9", 71 | "rimraf": "^2.6.2", 72 | "rollup": "^0.64.1", 73 | "rollup-plugin-babel": "^3.0.7", 74 | "rollup-plugin-uglify": "^4.0.0" 75 | }, 76 | "dependencies": { 77 | "react-tree-walker": "^4.2.0" 78 | }, 79 | "jest": { 80 | "collectCoverageFrom": [ 81 | "src/**/*.{js,jsx}" 82 | ], 83 | "snapshotSerializers": [ 84 | "/node_modules/enzyme-to-json/serializer" 85 | ], 86 | "testPathIgnorePatterns": [ 87 | "/(coverage|dist|node_modules|tools)/" 88 | ] 89 | }, 90 | "eslintConfig": { 91 | "root": true, 92 | "parser": "babel-eslint", 93 | "env": { 94 | "browser": true, 95 | "es6": true, 96 | "node": true, 97 | "jest": true 98 | }, 99 | "extends": [ 100 | "airbnb", 101 | "prettier" 102 | ], 103 | "rules": { 104 | "camelcase": 0, 105 | "import/prefer-default-export": 0, 106 | "import/no-extraneous-dependencies": 0, 107 | "no-nested-ternary": 0, 108 | "no-underscore-dangle": 0, 109 | "react/no-array-index-key": 0, 110 | "react/react-in-jsx-scope": 0, 111 | "semi": [ 112 | 2, 113 | "never" 114 | ], 115 | "react/forbid-prop-types": 0, 116 | "react/jsx-filename-extension": 0, 117 | "react/sort-comp": 0 118 | } 119 | }, 120 | "eslintIgnore": [ 121 | "node_modules/", 122 | "dist/", 123 | "coverage/" 124 | ], 125 | "prettier": { 126 | "semi": false, 127 | "singleQuote": true, 128 | "trailingComma": "all" 129 | }, 130 | "lint-staged": { 131 | "*.js": [ 132 | "prettier --write \"src/**/*.js\"", 133 | "git add" 134 | ] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /rollup-min.config.js: -------------------------------------------------------------------------------- 1 | const { uglify } = require('rollup-plugin-uglify') 2 | const packageJson = require('./package.json') 3 | 4 | const baseConfig = require('./rollup.config.js') 5 | 6 | baseConfig.plugins.push(uglify()) 7 | baseConfig.output.file = `dist/${packageJson.name}.min.js` 8 | 9 | module.exports = baseConfig 10 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel') 2 | const changeCase = require('change-case') 3 | const packageJson = require('./package.json') 4 | 5 | process.env.BABEL_ENV = 'production' 6 | 7 | module.exports = { 8 | external: ['react', 'react-tree-walker'], 9 | input: 'src/index.js', 10 | output: { 11 | file: `dist/${packageJson.name}.js`, 12 | format: 'cjs', 13 | sourcemap: true, 14 | name: changeCase 15 | .titleCase(packageJson.name.replace(/-/g, ' ')) 16 | .replace(/ /g, ''), 17 | }, 18 | plugins: [ 19 | babel({ 20 | babelrc: false, 21 | exclude: 'node_modules/**', 22 | presets: [['env', { modules: false }], 'stage-3', 'react'], 23 | plugins: ['external-helpers'], 24 | }), 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/asyncBootstrapper.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable react/no-multi-comp */ 3 | 4 | import React, { Component } from 'react' 5 | import asyncBootstrapper from '../' 6 | 7 | describe('asyncBootstrapper()', () => { 8 | let values = [] 9 | let actualContext 10 | 11 | class DeprecatedAPI extends Component { 12 | asyncBootstrap() { 13 | values.push(this.props.id) 14 | return true 15 | } 16 | 17 | render() { 18 | return
{this.props.children}
19 | } 20 | } 21 | 22 | class NewAPI extends Component { 23 | bootstrap() { 24 | values.push(this.props.id) 25 | actualContext = this.context 26 | return true 27 | } 28 | 29 | render() { 30 | return
{this.props.children}
31 | } 32 | } 33 | 34 | const app = Foo => ( 35 | 36 |
37 |

Test

38 |
39 | 40 | 41 | 42 | 43 | 44 | ) 45 | 46 | beforeEach(() => { 47 | values = [] 48 | }) 49 | 50 | it('deprecated API', () => 51 | asyncBootstrapper(app(DeprecatedAPI)).then(() => 52 | expect(values).toEqual([1, 2, 4, 3]), 53 | )) 54 | 55 | it('new API', () => 56 | asyncBootstrapper(app(NewAPI), null, { bespokeContext: true }).then(() => { 57 | expect(values).toEqual([1, 2, 4, 3]) 58 | expect(actualContext).toEqual({ 59 | bespokeContext: true, 60 | reactAsyncBootstrapperRunning: true, 61 | }) 62 | })) 63 | }) 64 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import reactTreeWalker from 'react-tree-walker' 4 | 5 | const warnmsg = 6 | '"react-async-bootstrapper" deprecation notice: please rename your "asyncBootsrap" methods to "bootstrap"' 7 | 8 | const defaultContext = { 9 | reactAsyncBootstrapperRunning: true, 10 | } 11 | 12 | export default function asyncBootstrapper(app, options, context = {}) { 13 | const visitor = (element, instance) => { 14 | if ( 15 | instance && 16 | (typeof instance.asyncBootstrap === 'function' || 17 | typeof instance.bootstrap === 'function') 18 | ) { 19 | return typeof instance.bootstrap === 'function' 20 | ? instance.bootstrap() 21 | : console.log(warnmsg) || instance.asyncBootstrap() 22 | } 23 | return undefined 24 | } 25 | 26 | return reactTreeWalker( 27 | app, 28 | visitor, 29 | Object.assign({}, context, defaultContext), 30 | options, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /tools/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "import/no-extraneous-dependencies": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools/scripts/build.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs') 2 | const { inInstall } = require('in-publish') 3 | const prettyBytes = require('pretty-bytes') 4 | const gzipSize = require('gzip-size') 5 | const { pipe } = require('ramda') 6 | const { exec } = require('../utils') 7 | const packageJson = require('../../package.json') 8 | 9 | if (inInstall()) { 10 | process.exit(0) 11 | } 12 | 13 | const nodeEnv = Object.assign({}, process.env, { 14 | NODE_ENV: 'production', 15 | }) 16 | 17 | exec('npx rollup -c rollup-min.config.js', nodeEnv) 18 | exec('npx rollup -c rollup.config.js', nodeEnv) 19 | 20 | function fileGZipSize(path) { 21 | return pipe(readFileSync, gzipSize.sync, prettyBytes)(path) 22 | } 23 | 24 | console.log( 25 | `\ngzipped, the build is ${fileGZipSize(`dist/${packageJson.name}.min.js`)}`, 26 | ) 27 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const appRootDir = require('app-root-dir') 3 | 4 | function exec(command) { 5 | execSync(command, { stdio: 'inherit', cwd: appRootDir.get() }) 6 | } 7 | 8 | module.exports = { 9 | exec, 10 | } 11 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | process.env.NODE_ENV = 'test' 5 | 6 | const babelConfigContents = fs.readFileSync(path.join(__dirname, '.babelrc')) 7 | const babelConfig = JSON.parse(babelConfigContents) 8 | 9 | module.exports = wallaby => ({ 10 | files: ['src/**/*.js', { pattern: 'src/**/*.test.js', ignore: true }], 11 | tests: ['src/**/*.test.js'], 12 | testFramework: 'jest', 13 | env: { 14 | type: 'node', 15 | runner: 'node', 16 | }, 17 | compilers: { 18 | 'src/**/*.js': wallaby.compilers.babel(babelConfig), 19 | }, 20 | }) 21 | --------------------------------------------------------------------------------