├── .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 | [](http://npm.im/react-async-bootstrapper)
6 | [](http://opensource.org/licenses/MIT)
7 | [](https://travis-ci.org/ctrlplusb/react-async-bootstrapper)
8 | [](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 |
--------------------------------------------------------------------------------