├── plugins.dist.js ├── examples └── basic │ ├── plugins.js │ ├── src │ ├── assets │ │ └── scss │ │ │ └── styles.scss │ ├── actions │ │ ├── PluginActions.js │ │ └── index.js │ ├── index.js │ ├── components │ │ ├── Start.js │ │ ├── NotFound.js │ │ ├── Footer.js │ │ ├── Todo.js │ │ ├── TodoList.js │ │ ├── Dashboard.js │ │ ├── Todos.js │ │ └── App.js │ ├── containers │ │ ├── FilterLink.js │ │ ├── AddTodo.js │ │ └── VisibleTodoList.js │ ├── services │ │ └── PluginsRegistry.js │ ├── reducers │ │ ├── index.js │ │ ├── todo.js │ │ ├── pluginsHandler.js │ │ └── todos.js │ ├── routes.js │ ├── configureStore.js │ └── api.js │ ├── .gitignore │ ├── index.html │ ├── .babelrc │ ├── server.js │ ├── README.md │ ├── webpack.config.js │ ├── webpack.prod.js │ ├── package.json │ └── plugins │ └── example │ └── index.js ├── src ├── reducers │ ├── index.js │ └── pluginsHandler.js ├── actions │ └── PluginActions.js ├── services │ └── PluginsRegistry.js ├── configureStore.js ├── components │ ├── CustomRouter.js │ └── App.js └── routes.js ├── .gitignore ├── .stylelintrc ├── .babelrc ├── .eslintignore ├── .eslintrc ├── webpack.config.js.dist ├── LICENSE ├── webpack.prod.js.dist ├── package.json └── README.md /plugins.dist.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PLUGINS: [], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/basic/plugins.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PLUGINS: ['example'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/basic/src/assets/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/bootstrap.scss'; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import pluginsHandler from './pluginsHandler'; 2 | 3 | export default { 4 | pluginsHandler, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | .DS_Store 4 | dist 5 | 6 | package-lock.json 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | .DS_Store 4 | dist 5 | /static 6 | .vscode 7 | .history 8 | 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /src/actions/PluginActions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const addPlugins = createAction('ADD-PLUGINS'); 4 | -------------------------------------------------------------------------------- /examples/basic/src/actions/PluginActions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const addPlugins = createAction('ADD-PLUGINS'); 4 | -------------------------------------------------------------------------------- /examples/basic/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "string-quotes": "single", 4 | "indentation": 4, 5 | "declaration-block-no-duplicate-properties": true, 6 | "length-zero-no-unit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample react-plugin-system app 5 | 6 | 7 |
8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/basic/src/components/Start.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Start = () => ( 4 |
5 | Application start page 6 |
7 | ); 8 | 9 | export default Start; 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["@babel/plugin-syntax-dynamic-import", 5 | "dynamic-import-node", 6 | "@babel/plugin-transform-modules-commonjs", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | .DS_Store 4 | dist 5 | /plugins.js 6 | /static 7 | .history 8 | 9 | 10 | # Coverage report 11 | .nyc_output 12 | coverage 13 | 14 | package-lock.json 15 | 16 | plugins 17 | 18 | yarn-error.log 19 | -------------------------------------------------------------------------------- /examples/basic/src/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const FilterLink = ({ filter, children }) => ( 5 | 8 | {children} 9 | 10 | ); 11 | 12 | export default FilterLink; 13 | -------------------------------------------------------------------------------- /examples/basic/src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from 'react-bootstrap/Container'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const NotFound = () => ( 6 | 7 |

404 not found

8 | Bring me back to the start 9 |
10 | ); 11 | 12 | export default NotFound; 13 | -------------------------------------------------------------------------------- /src/services/PluginsRegistry.js: -------------------------------------------------------------------------------- 1 | export default class PluginsRegistry { 2 | constructor(hostApplication) { 3 | this.hostApp = hostApplication; 4 | this.plugins = {}; 5 | } 6 | 7 | getEntry(pluginName) { 8 | if (pluginName) { 9 | return this.plugins[`${pluginName}`]; 10 | } 11 | } 12 | 13 | addEntry(name, file) { 14 | this.plugins[`${name}`] = file; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic/src/services/PluginsRegistry.js: -------------------------------------------------------------------------------- 1 | export default class PluginsRegistry { 2 | constructor(hostApplication) { 3 | this.hostApp = hostApplication; 4 | this.plugins = {}; 5 | } 6 | 7 | getEntry(pluginName) { 8 | if (pluginName) { 9 | return this.plugins[`${pluginName}`]; 10 | } 11 | } 12 | 13 | addEntry(name, file) { 14 | this.plugins[`${name}`] = file; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/basic/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FilterLink from '../containers/FilterLink'; 3 | 4 | const Footer = () => ( 5 |

6 | Show: All 7 | {', '} 8 | Active 9 | {', '} 10 | Completed 11 |

12 | ); 13 | 14 | export default Footer; 15 | -------------------------------------------------------------------------------- /examples/basic/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { connectRouter } from 'connected-react-router'; 2 | 3 | import pluginsHandler from './pluginsHandler'; 4 | import todos, * as fromTodos from './todos'; 5 | 6 | export const createRootReducer = (history) => ({ 7 | router: connectRouter(history), 8 | todos, 9 | pluginsHandler, 10 | }) 11 | 12 | export const getVisibleTodos = (state, filter) => 13 | fromTodos.getVisibleTodos(state.todos, filter); 14 | -------------------------------------------------------------------------------- /examples/basic/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-class-properties", { "spec": true }], 5 | "@babel/plugin-syntax-dynamic-import", 6 | "dynamic-import-node", 7 | "@babel/plugin-transform-modules-commonjs", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-transform-async-to-generator", 10 | "react-hot-loader/babel" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/basic/src/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Todo = ({ onClick, completed, text }) => ( 5 |
  • 11 | {text} 12 |
  • 13 | ); 14 | 15 | Todo.propTypes = { 16 | onClick: PropTypes.func.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired, 19 | }; 20 | 21 | export default Todo; 22 | -------------------------------------------------------------------------------- /examples/basic/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4'; 2 | import * as api from '../api'; 3 | 4 | const receiveTodos = (filter, response) => ({ 5 | type: 'RECEIVE_TODOS', 6 | filter, 7 | response, 8 | }); 9 | 10 | export const fetchTodos = filter => 11 | api.fetchTodos(filter).then(response => receiveTodos(filter, response)); 12 | 13 | export const addTodo = text => ({ 14 | type: 'ADD_TODO', 15 | id: uuid(), 16 | text, 17 | }); 18 | 19 | export const toggleTodo = id => ({ 20 | type: 'TOGGLE_TODO', 21 | id, 22 | }); 23 | -------------------------------------------------------------------------------- /examples/basic/src/reducers/todo.js: -------------------------------------------------------------------------------- 1 | const todo = (state, action) => { 2 | switch (action.type) { 3 | case 'ADD_TODO': 4 | return { 5 | id: action.id, 6 | text: action.text, 7 | completed: false, 8 | }; 9 | case 'TOGGLE_TODO': 10 | if (state.id !== action.id) { 11 | return state; 12 | } 13 | 14 | return Object.assign({}, state, { 15 | completed: !state.completed, 16 | }); 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default todo; 23 | -------------------------------------------------------------------------------- /examples/basic/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | // var listenHost = process.env.DOCKER ? '0.0.0.0' : 'localhost'; 6 | var listenHost = '0.0.0.0'; 7 | 8 | new WebpackDevServer(webpack(config), { 9 | publicPath: config.output.publicPath, 10 | hot: true, 11 | historyApiFallback: true, 12 | }).listen(3000, listenHost, function(err) { 13 | if (err) { 14 | // eslint-disable-next-line no-console 15 | return console.error(err); 16 | } 17 | 18 | // eslint-disable-next-line no-console 19 | return console.warn('Listening at http://' + listenHost + ':3000/'); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/basic/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Todo from './Todo'; 4 | 5 | const TodoList = ({ todos, onTodoClick }) => ( 6 | 11 | ); 12 | 13 | TodoList.propTypes = { 14 | todos: PropTypes.arrayOf( 15 | PropTypes.shape({ 16 | id: PropTypes.string.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired, 19 | }).isRequired 20 | ).isRequired, 21 | onTodoClick: PropTypes.func.isRequired, 22 | }; 23 | 24 | export default TodoList; 25 | -------------------------------------------------------------------------------- /examples/basic/src/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { addTodo } from '../actions'; 4 | 5 | let AddTodo = ({ dispatch }) => { 6 | let input; 7 | 8 | return ( 9 |
    10 |
    { 11 | e.preventDefault(); 12 | if (!input.value.trim()) { 13 | return; 14 | } 15 | dispatch(addTodo(input.value)); 16 | input.value = ''; 17 | }}> 18 | { 19 | input = node; 20 | }} /> 21 | 24 |
    25 |
    26 | ); 27 | }; 28 | AddTodo = connect()(AddTodo); 29 | 30 | export default AddTodo; 31 | -------------------------------------------------------------------------------- /src/reducers/pluginsHandler.js: -------------------------------------------------------------------------------- 1 | import { handleAction } from 'redux-actions'; 2 | 3 | const initialState = { 4 | files: [], 5 | components: {}, 6 | }; 7 | 8 | export default handleAction( 9 | 'ADD-PLUGINS', 10 | (state, action) => { 11 | const plugins = action.payload; 12 | const files = []; 13 | let components = {}; 14 | 15 | plugins.forEach(plugin => { 16 | if (plugin.file.components) { 17 | plugin.file.components.forEach(component => { 18 | components[`${component.id}`] = plugin.name; 19 | }); 20 | } 21 | 22 | files.push(plugin.file); 23 | }); 24 | 25 | return { 26 | ...state, 27 | files: [...state.files, ...files], 28 | components: { ...state.components, ...components }, 29 | }; 30 | }, 31 | initialState 32 | ); 33 | -------------------------------------------------------------------------------- /examples/basic/src/reducers/pluginsHandler.js: -------------------------------------------------------------------------------- 1 | import { handleAction } from 'redux-actions'; 2 | 3 | const initialState = { 4 | files: [], 5 | components: {}, 6 | }; 7 | 8 | export default handleAction( 9 | 'ADD-PLUGINS', 10 | (state, action) => { 11 | const plugins = action.payload; 12 | const files = []; 13 | let components = {}; 14 | 15 | plugins.forEach(plugin => { 16 | if (plugin.file.components) { 17 | plugin.file.components.forEach(component => { 18 | components[`${component.id}`] = plugin.name; 19 | }); 20 | } 21 | 22 | files.push(plugin.file); 23 | }); 24 | 25 | return { 26 | ...state, 27 | files: [...state.files, ...files], 28 | components: { ...state.components, ...components }, 29 | }; 30 | }, 31 | initialState 32 | ); 33 | -------------------------------------------------------------------------------- /src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose } from 'redux'; 2 | import { createStore } from 'redux-dynamic-reducer'; 3 | 4 | export default function configureStore(history) { 5 | // your middlewares 6 | const middleware = []; 7 | const store = createStore( 8 | // store is initialized without state 9 | null, 10 | compose( 11 | applyMiddleware(...middleware), 12 | window.__REDUX_DEVTOOLS_EXTENSION__ 13 | ? window.__REDUX_DEVTOOLS_EXTENSION__() 14 | : f => f 15 | ) 16 | ); 17 | 18 | // attach reducers to your empty store 19 | store.attachReducers(rootReducer); 20 | 21 | // optional hot-reload 22 | /* 23 | if (module.hot) { 24 | module.hot.accept('./reducers', () => { 25 | const nextReducer = rootReducer; 26 | store.replaceReducer(nextReducer); 27 | }); 28 | } 29 | */ 30 | 31 | return store; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/CustomRouter.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Router } from 'react-router'; 4 | import { connect } from 'react-redux'; 5 | 6 | import { getRoutes } from '../routes.js'; 7 | 8 | class CustomRouter extends PureComponent { 9 | UNSAFE_componentWillMount() { 10 | const { plugins, store } = this.props; 11 | 12 | this.routes = getRoutes(store, plugins); 13 | } 14 | 15 | render() { 16 | const { history } = this.props; 17 | 18 | return ; 19 | } 20 | } 21 | 22 | CustomRouter.propTypes = { 23 | store: PropTypes.object.isRequired, 24 | plugins: PropTypes.array, 25 | history: PropTypes.any, 26 | }; 27 | 28 | const mapStateToProps = state => ({ 29 | plugins: state.pluginsHandler.files, 30 | }); 31 | 32 | export default connect(mapStateToProps)(CustomRouter); 33 | -------------------------------------------------------------------------------- /examples/basic/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import Dashboard from './components/Dashboard'; 5 | import Todos from './components/Todos'; 6 | import NotFound from './components/NotFound'; 7 | 8 | export const getRoutes = (store, plugins) => { 9 | if (plugins.length < 1) { 10 | return []; 11 | } 12 | 13 | let pluginRoutes = []; 14 | plugins.map(plugin => { 15 | if (typeof plugin.routes === 'undefined') { 16 | return; 17 | } 18 | 19 | pluginRoutes.push(plugin.routes); 20 | }); 21 | console.log(pluginRoutes); 22 | 23 | let result = ( 24 | 25 | 26 | 27 | 28 | {pluginRoutes} 29 | 30 | 31 | 32 | ); 33 | 34 | console.log(result); 35 | 36 | return result; 37 | }; 38 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IndexRoute, NoMatch, Route } from 'react-router'; 3 | 4 | export const getRoutes = (store, plugins) => { 5 | const getPluginsRoutes = plugins => { 6 | if (plugins.length) { 7 | const routes = plugins.map(plugin => { 8 | if (plugin.routes && plugin.routes.length) { 9 | const pluginRoutes = [...plugin.routes]; 10 | 11 | return pluginRoutes[0]; 12 | } 13 | 14 | return []; 15 | }); 16 | 17 | return routes; 18 | } 19 | 20 | return []; 21 | }; 22 | 23 | const pluginRoutes = getPluginsRoutes(plugins); 24 | const childRoutes = [ 25 | // your app routes 26 | ...pluginRoutes, 27 | ]; 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "jest/globals": true 7 | }, 8 | "parser": "babel-eslint", 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "prettier/prettier": ["error", { 16 | "singleQuote": true, 17 | "trailingComma": "es5", 18 | "tabWidth": 2 19 | }], 20 | "react/prop-types": "warn", 21 | "react/jsx-filename-extension": "off", 22 | "react/prefer-stateless-function": "off", 23 | "react/display-name": "warn" 24 | }, 25 | "plugins": ["prettier", "react", "jest"], 26 | "globals": { 27 | "localStorage": true, 28 | "Promise": true, 29 | "config": true, 30 | "PLUGINS": true, 31 | "context": true, 32 | "before": true, 33 | "document": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/basic/src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { routerMiddleware } from 'connected-react-router'; 2 | import { applyMiddleware, compose } from 'redux'; 3 | import { createStore } from 'redux-dynamic-reducer'; 4 | import thunk from 'redux-thunk'; 5 | import promiseMiddleware from 'redux-promise'; 6 | import { createRootReducer } from './reducers'; 7 | 8 | export default function configureStore(history) { 9 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 10 | 11 | const middleware = [thunk, promiseMiddleware, routerMiddleware(history)]; 12 | const store = createStore( 13 | null, 14 | composeEnhancer( 15 | applyMiddleware(...middleware), 16 | ) 17 | ); 18 | 19 | store.attachReducers(createRootReducer(history)); 20 | 21 | if (module.hot) { 22 | module.hot.accept('./reducers', () => { 23 | const nextReducer = createRootReducer(history); 24 | store.replaceReducer(nextReducer); 25 | }); 26 | } 27 | 28 | return store; 29 | } 30 | -------------------------------------------------------------------------------- /examples/basic/src/api.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4'; 2 | 3 | // This is a fake in-memory implementation of something 4 | // that would be implemented by calling REST server. 5 | 6 | const fakeDatabase = { 7 | todos: [ 8 | { 9 | id: uuid(), 10 | text: 'hey', 11 | completed: true, 12 | }, 13 | { 14 | id: uuid(), 15 | text: 'ho', 16 | completed: true, 17 | }, 18 | { 19 | id: uuid(), 20 | text: 'let’s go', 21 | completed: false, 22 | }, 23 | ], 24 | }; 25 | 26 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 27 | 28 | export const fetchTodos = filter => 29 | delay(500).then(() => { 30 | switch (filter) { 31 | case 'all': 32 | return fakeDatabase.todos; 33 | case 'active': 34 | return fakeDatabase.todos.filter(t => !t.completed); 35 | case 'completed': 36 | return fakeDatabase.todos.filter(t => t.completed); 37 | default: 38 | throw new Error(`Unknown filter: ${filter}`); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /webpack.config.js.dist: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var fs = require('fs'); 4 | 5 | const plugins = []; 6 | const entries = { 7 | index: [ 8 | './src/index.js', 9 | ], 10 | }; 11 | let appPlugins = []; 12 | 13 | if (fs.existsSync(path.join(__dirname, 'plugins.js'))) { 14 | const loadedPlugins = require('./plugins'); 15 | 16 | appPlugins = loadedPlugins.PLUGINS; 17 | } 18 | 19 | plugins.push( 20 | new webpack.DefinePlugin({ 21 | PLUGINS: JSON.stringify(appPlugins), 22 | }) 23 | ); 24 | 25 | module.exports = { 26 | mode: 'development', 27 | devtool: 'eval', 28 | entry: entries, 29 | output: { 30 | path: '/', 31 | filename: '[name].bundle-[hash].js', 32 | publicPath: '/', 33 | }, 34 | plugins, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.jsx?$/, 39 | loader: 'babel-loader', 40 | include: path.join(__dirname, 'src'), 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.js'], 46 | alias: { 47 | '@plugins': path.resolve('./plugins'), 48 | }, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kuba Siemiątkowski 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 | -------------------------------------------------------------------------------- /examples/basic/src/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import * as actions from '../actions'; 5 | import { getVisibleTodos } from '../reducers'; 6 | import TodoList from '../components/TodoList'; 7 | 8 | class VisibleTodoList extends Component { 9 | componentDidMount() { 10 | this.fetchData(); 11 | } 12 | 13 | componentDidUpdate(prevProps) { 14 | if (this.props.filter !== prevProps.filter) { 15 | this.fetchData(); 16 | } 17 | } 18 | 19 | fetchData() { 20 | const { filter, fetchTodos } = this.props; 21 | fetchTodos(filter); 22 | } 23 | 24 | render() { 25 | const { toggleTodo, ...rest } = this.props; 26 | 27 | return ; 28 | } 29 | } 30 | 31 | const mapStateToProps = (state, params) => { 32 | const filter = params.filter || 'all'; 33 | return { 34 | todos: getVisibleTodos(state, filter), 35 | filter, 36 | }; 37 | }; 38 | 39 | export default withRouter( 40 | connect( 41 | mapStateToProps, 42 | actions 43 | )(VisibleTodoList) 44 | ); 45 | -------------------------------------------------------------------------------- /examples/basic/src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Nav, Navbar } from 'react-bootstrap'; 3 | import { LinkContainer } from 'react-router-bootstrap'; 4 | 5 | const Dashboard = ({ children }) => ( 6 |
    7 |
    8 |
    9 | 10 | 11 | 12 | Home 13 | 14 | 15 | 16 | 17 | 24 | 25 | 26 |
    27 |
    28 |
    29 |
    {children}
    30 |
    31 |
    32 | ); 33 | 34 | export default Dashboard; 35 | -------------------------------------------------------------------------------- /examples/basic/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import todo from './todo'; 3 | 4 | const byId = (state = {}, action) => { 5 | switch (action.type) { 6 | case 'ADD_TODO': 7 | case 'TOGGLE_TODO': 8 | return { 9 | ...state, 10 | [action.id]: todo(state[action.id], action), 11 | }; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | const allIds = (state = [], action) => { 18 | switch (action.type) { 19 | case 'ADD_TODO': 20 | return [...state, action.id]; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | const todos = combineReducers({ 27 | byId, 28 | allIds, 29 | }); 30 | 31 | export default todos; 32 | 33 | const getAllTodos = state => state.allIds.map(id => state.byId[id]); 34 | 35 | export const getVisibleTodos = (state, filter) => { 36 | const allTodos = getAllTodos(state); 37 | switch (filter) { 38 | case 'all': 39 | return allTodos; 40 | case 'completed': 41 | return allTodos.filter(t => t.completed); 42 | case 'active': 43 | return allTodos.filter(t => !t.completed); 44 | default: 45 | throw new Error(`Unknown filter: ${filter}`); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /webpack.prod.js.dist: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | var fs = require('fs'); 5 | 6 | const plugins = [ 7 | new webpack.DefinePlugin({ 8 | 'process.env': { 9 | NODE_ENV: JSON.stringify('production'), 10 | }, 11 | }), 12 | new CopyWebpackPlugin([{ from: './plugins/**', to: './', ignore: ['*.md'] }]), 13 | ]; 14 | 15 | if (!fs.existsSync(path.join(__dirname, 'dist/plugins.js'))) { 16 | plugins.push( 17 | new webpack.DefinePlugin({ 18 | PLUGINS: JSON.stringify([]), 19 | }) 20 | ); 21 | } 22 | 23 | module.exports = { 24 | mode: 'production', 25 | devtool: 'cheap-module-source-map', 26 | entry: ['./src/index.js'], 27 | output: { 28 | path: path.join(__dirname, 'dist'), 29 | filename: 'bundle-[hash].js', 30 | publicPath: '/', 31 | }, 32 | plugins, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.jsx?$/, 37 | loader: 'babel-loader', 38 | include: path.join(__dirname, 'src'), 39 | }, 40 | ], 41 | }, 42 | resolve: { 43 | extensions: ['.js'], 44 | alias: { 45 | '@plugins$': path.resolve('./plugins'), 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic example 2 | 3 | This example is a simple ReactJS application based on [gaeron's](https://github.com/gaearon) [Idiomatic Redux tutorial](https://egghead.io/series/building-react-applications-with-idiomatic-redux). It is then extended with a plugin to add a separate section, which is basically a standalone app. There's no communication between the two whatsoever. 4 | 5 | For the purpose of this example a bundled [rps-basic plugin](https://github.com/siemiatj/rps-basic) is used. 6 | 7 | ## Dev environment 8 | 9 | - install npm and node.js 10 | 11 | - make sure you have all dependencies by: 12 | > npm install 13 | 14 | - Then you should run node server by: 15 | > npm start 16 | 17 | ## Production environment 18 | When running in production mode you will need to build the static version of the app and serve it from an http-compatible server. Here's a quick guide how you can run production mode locally. 19 | 20 | ## Production 21 | In case of static version building execute: 22 | > npm run build-prod 23 | 24 | ### Running 25 | The easiest way to test production build is by serving it via a simple [http-server](https://www.npmjs.com/package/http-server). You can install it globally with npm : 26 | > npm install http-server -g 27 | 28 | and then run it pointing to your dist folder: 29 | > http-server ./dist 30 | 31 | Now open your browser and go to `localhost:8080` to see the application running. 32 | -------------------------------------------------------------------------------- /examples/basic/src/components/Todos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Nav, Navbar } from 'react-bootstrap'; 3 | import { useParams } from 'react-router-dom'; 4 | import { LinkContainer } from 'react-router-bootstrap'; 5 | 6 | import Footer from './Footer'; 7 | import AddTodo from '../containers/AddTodo'; 8 | import VisibleTodoList from '../containers/VisibleTodoList'; 9 | 10 | const Todos = () => { 11 | let { filter } = useParams(); 12 | if (typeof filter === 'undefined') { 13 | filter = 'all'; 14 | } 15 | return ( 16 |
    17 |
    18 |
    19 | 20 | 21 | 33 | 34 | 35 |
    36 |
    37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 |
    44 |
    45 | ) 46 | }; 47 | 48 | export default Todos; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-plugin-system", 3 | "version": "0.0.3", 4 | "description": "Plugin system for ReactJS", 5 | "author": "Kuba Siemiatkowski ", 6 | "scripts": { 7 | "build-prod": "webpack --config webpack.prod.js --bail --display-error-details", 8 | "start": "node server.js" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "7.2.2", 12 | "@babel/plugin-proposal-class-properties": "7.3.0", 13 | "@babel/plugin-proposal-object-rest-spread": "7.3.1", 14 | "@babel/plugin-syntax-dynamic-import": "7.2.0", 15 | "@babel/plugin-transform-async-to-generator": "7.2.0", 16 | "@babel/plugin-transform-modules-commonjs": "7.2.0", 17 | "@babel/polyfill": "7.2.5", 18 | "@babel/preset-env": "7.3.1", 19 | "@babel/preset-react": "7.0.0", 20 | "@babel/register": "7.0.0", 21 | "babel-eslint": "10.0.1", 22 | "babel-loader": "8.0.5", 23 | "babel-plugin-dynamic-import-node": "2.2.0", 24 | "eslint": "5.9.0", 25 | "eslint-config-prettier": "4.0.0", 26 | "eslint-plugin-import": "2.7.0", 27 | "eslint-plugin-jest": "22.1.0", 28 | "eslint-plugin-jsx-a11y": "6.1.2", 29 | "eslint-plugin-prettier": "3.0.1", 30 | "eslint-plugin-react": "7.11.1", 31 | "prettier": "1.16.4", 32 | "webpack": "4.2.0", 33 | "webpack-cli": "3.2.1" 34 | }, 35 | "dependencies": { 36 | "bundle-loader": "0.5.6", 37 | "file-loader": "2.0.0", 38 | "prop-types": "15.5.6", 39 | "react": "16.5.0", 40 | "react-dom": "16.5.0", 41 | "react-redux": "5.0.6", 42 | "react-router": "3.2.0", 43 | "react-router-redux": "4.0.8", 44 | "redux": "3.7.2", 45 | "redux-dynamic-reducer": "2.0.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { browserHistory } from 'react-router'; 4 | 5 | import { addPlugins } from '../actions/PluginActions'; 6 | import PluginsRegistry from '../services/PluginsRegistry'; 7 | import CustomRouter from './CustomRouter'; 8 | import configureStore from '../configureStore'; 9 | 10 | const store = configureStore(browserHistory); 11 | const APP_PLUGINS = PLUGINS ? PLUGINS : []; 12 | 13 | export default class App extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | pluginsLoading: !!APP_PLUGINS.length, 19 | }; 20 | 21 | this.pluginsRegistry = new PluginsRegistry(this); 22 | window.META_HOST_APP = this; 23 | 24 | if (APP_PLUGINS.length) { 25 | const plugins = APP_PLUGINS.map(plugin => { 26 | const waitForChunk = () => 27 | import(`@plugins/${plugin}/index.js`) 28 | .then(module => module) 29 | .catch(() => { 30 | // eslint-disable-next-line no-console 31 | console.error(`Error loading plugin ${plugin}`); 32 | }); 33 | 34 | return new Promise(resolve => 35 | waitForChunk().then(file => { 36 | this.pluginsRegistry.addEntry(plugin, file); 37 | resolve({ name: plugin, file }); 38 | }) 39 | ); 40 | }); 41 | 42 | Promise.all(plugins).then(res => { 43 | const plugins = res.reduce((prev, current) => prev.concat(current), []); 44 | 45 | if (plugins.length) { 46 | store.dispatch(addPlugins(plugins)); 47 | } 48 | 49 | plugins.forEach(({ file }) => { 50 | if (file.reducers && file.reducers.name) { 51 | store.attachReducers({ 52 | plugins: { 53 | [`${file.reducers.name}`]: file.reducers.reducer, 54 | }, 55 | }); 56 | } 57 | }); 58 | 59 | this.setState({ 60 | pluginsLoading: false, 61 | }); 62 | }); 63 | } 64 | } 65 | 66 | getRegistry() { 67 | return this.pluginsRegistry; 68 | } 69 | 70 | render() { 71 | if (APP_PLUGINS.length && this.state.pluginsLoading) { 72 | return null; 73 | } 74 | 75 | return ( 76 | 77 | 78 | 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/basic/src/components/App.js: -------------------------------------------------------------------------------- 1 | import '../assets/scss/styles.scss'; 2 | import React, { Component } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { createBrowserHistory } from 'history'; 5 | 6 | import { addPlugins } from '../actions/PluginActions'; 7 | import PluginsRegistry from '../services/PluginsRegistry'; 8 | import { ConnectedRouter } from 'connected-react-router'; 9 | import configureStore from '../configureStore'; 10 | import { getRoutes } from '../routes'; 11 | 12 | const history = createBrowserHistory(); 13 | 14 | const store = configureStore(history); 15 | const APP_PLUGINS = PLUGINS ? PLUGINS : []; 16 | 17 | export default class App extends Component { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.state = { 22 | pluginsLoading: !!APP_PLUGINS.length, 23 | }; 24 | 25 | this.pluginsRegistry = new PluginsRegistry(this); 26 | window.META_HOST_APP = this; 27 | 28 | if (APP_PLUGINS.length) { 29 | const plugins = APP_PLUGINS.map(plugin => { 30 | const waitForChunk = () => { 31 | return import(`@plugins/${plugin}/index.js`) 32 | .then(module => module); 33 | } 34 | 35 | return new Promise(resolve => 36 | waitForChunk().then(file => { 37 | this.pluginsRegistry.addEntry(plugin, file); 38 | resolve({ name: plugin, file }); 39 | }) 40 | ).catch((e) => { 41 | // eslint-disable-next-line no-console 42 | console.error(`Error loading plugin "${plugin}": ${e}`); 43 | }); 44 | }); 45 | 46 | Promise.all(plugins).then(res => { 47 | const plugins = res.reduce((prev, current) => prev.concat(current), []); 48 | 49 | if (plugins.length) { 50 | store.dispatch(addPlugins(plugins)); 51 | } 52 | 53 | plugins.forEach(({ file }) => { 54 | if (file.reducers && file.reducers.name) { 55 | store.attachReducers({ 56 | plugins: { 57 | [`${file.reducers.name}`]: file.reducers.reducer, 58 | }, 59 | }); 60 | } 61 | }); 62 | 63 | this.setState({ 64 | pluginsLoading: false, 65 | }); 66 | }); 67 | } 68 | } 69 | 70 | getRegistry() { 71 | return this.pluginsRegistry; 72 | } 73 | 74 | render() { 75 | if (APP_PLUGINS.length && this.state.pluginsLoading) { 76 | return null; 77 | } 78 | 79 | return ( 80 | 81 | 82 | {getRoutes(store, store.getState().pluginsHandler.files)} 83 | 84 | 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var fs = require('fs'); 5 | 6 | const plugins = [ 7 | new webpack.HotModuleReplacementPlugin(), 8 | new webpack.NoEmitOnErrorsPlugin(), 9 | new HtmlWebpackPlugin({ 10 | template: 'index.html', 11 | }), 12 | ]; 13 | 14 | const entries = { 15 | index: [ 16 | 'webpack-dev-server/client?http://localhost:3000', 17 | 'webpack/hot/only-dev-server', 18 | '@babel/polyfill', 19 | './src/index.js', 20 | ], 21 | }; 22 | 23 | let appPlugins = []; 24 | 25 | if (fs.existsSync(path.join(__dirname, 'plugins.js'))) { 26 | const loadedPlugins = require('./plugins'); 27 | 28 | appPlugins = loadedPlugins.PLUGINS; 29 | } 30 | 31 | plugins.push( 32 | new webpack.DefinePlugin({ 33 | PLUGINS: JSON.stringify(appPlugins), 34 | }) 35 | ); 36 | 37 | module.exports = { 38 | mode: 'development', 39 | devtool: 'eval', 40 | entry: entries, 41 | output: { 42 | path: '/', 43 | filename: '[name].bundle-[hash].js', 44 | publicPath: '/', 45 | }, 46 | plugins, 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.jsx?$/, 51 | loader: 'babel-loader', 52 | include: path.join(__dirname, 'src'), 53 | }, 54 | { 55 | test: /\.(jpg|png|svg|eot|woff|woff2|ttf|gif)$/, 56 | use: { 57 | loader: 'file-loader', 58 | options: { 59 | name: '[path][name].[ext]', 60 | }, 61 | }, 62 | }, 63 | { 64 | test: /\.s[ac]ss$/i, 65 | use: [ 66 | // Creates `style` nodes from JS strings 67 | 'style-loader', 68 | // Translates CSS into CommonJS 69 | 'css-loader', 70 | // Compiles Sass to CSS 71 | 'sass-loader', 72 | ], 73 | }, 74 | { 75 | test: /\.css$/, 76 | use: [ 77 | 'style-loader', 78 | { loader: 'css-loader', options: { importLoaders: 1 } }, 79 | { 80 | loader: 'postcss-loader', 81 | options: { 82 | ident: 'postcss', 83 | plugins: () => [ 84 | require('postcss-import')({ 85 | addDependencyTo: webpack, 86 | path: ['node_modules', 'src/assets'], 87 | }), 88 | require('postcss-color-function'), 89 | require('postcss-url')(), 90 | require('precss')(), 91 | require('autoprefixer')({ browsers: ['last 2 versions'] }), 92 | ], 93 | }, 94 | }, 95 | ], 96 | }, 97 | { 98 | test: /\.html$/, 99 | loader: 'html-loader', 100 | }, 101 | { 102 | test: /\.json$/, 103 | loader: 'json-loader', 104 | }, 105 | ], 106 | }, 107 | resolve: { 108 | extensions: ['.js', '.json'], 109 | alias: { 110 | '@plugins': path.resolve('./plugins'), 111 | }, 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /examples/basic/webpack.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | var fs = require('fs'); 6 | 7 | const plugins = [ 8 | new webpack.DefinePlugin({ 9 | 'process.env': { 10 | NODE_ENV: JSON.stringify('production'), 11 | }, 12 | }), 13 | new HtmlWebpackPlugin({ 14 | template: './index.html', 15 | }), 16 | new CopyWebpackPlugin([{ from: './plugins/**', to: './', ignore: ['*.md'] }]), 17 | ]; 18 | 19 | if (!fs.existsSync(path.join(__dirname, 'dist/plugins.js'))) { 20 | plugins.push( 21 | new webpack.DefinePlugin({ 22 | PLUGINS: JSON.stringify([]), 23 | }) 24 | ); 25 | } 26 | 27 | module.exports = { 28 | mode: 'production', 29 | devtool: 'cheap-module-source-map', 30 | entry: ['@babel/polyfill', './src/index.js', './favicon.png'], 31 | output: { 32 | path: path.join(__dirname, 'dist'), 33 | filename: 'bundle-[hash].js', 34 | publicPath: '/', 35 | }, 36 | plugins, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | loader: 'babel-loader', 42 | include: path.join(__dirname, 'src'), 43 | }, 44 | { 45 | test: /\.(jpg|png|svg|eot|woff|woff2|ttf|gif)$/, 46 | exclude: /\w*(logo)\w*\.(jpg|png)$/, 47 | use: { 48 | loader: 'file-loader', 49 | options: { 50 | name: '[path][name].[hash].[ext]', 51 | }, 52 | }, 53 | }, 54 | { 55 | test: /\w*(logo)\w*\.(jpg|png)$/, 56 | use: { 57 | loader: 'file-loader', 58 | options: { 59 | name: '[path][name].[ext]', 60 | }, 61 | }, 62 | }, 63 | { 64 | test: /\.s[ac]ss$/i, 65 | use: [ 66 | // Creates `style` nodes from JS strings 67 | 'style-loader', 68 | // Translates CSS into CommonJS 69 | 'css-loader', 70 | // Compiles Sass to CSS 71 | 'sass-loader', 72 | ], 73 | }, 74 | { 75 | test: /\.css$/, 76 | use: [ 77 | 'style-loader', 78 | { loader: 'css-loader', options: { importLoaders: 1 } }, 79 | { 80 | loader: 'postcss-loader', 81 | options: { 82 | ident: 'postcss', 83 | plugins: () => [ 84 | require('postcss-import')({ 85 | addDependencyTo: webpack, 86 | path: ['node_modules'], 87 | }), 88 | require('postcss-color-function'), 89 | require('postcss-url')(), 90 | require('autoprefixer')({ browsers: ['last 2 versions'] }), 91 | require('precss')(), 92 | ], 93 | }, 94 | }, 95 | ], 96 | }, 97 | { 98 | test: /\.html$/, 99 | loader: 'html-loader', 100 | }, 101 | ], 102 | }, 103 | resolve: { 104 | extensions: ['.js', '.json'], 105 | alias: { 106 | '@plugins$': path.resolve('./plugins'), 107 | }, 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-plugin-system-basic-example", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "description": "Basic example running react-plugin-system for ReactJS", 6 | "author": "Kuba Siemiatkowski ", 7 | "scripts": { 8 | "build-prod": "webpack --config webpack.prod.js --bail --display-error-details", 9 | "lint": "eslint .", 10 | "start": "node server.js", 11 | "stylelint": "stylelint 'src/assets/**/*.css'" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "7.7.7", 15 | "@babel/plugin-proposal-class-properties": "7.7.4", 16 | "@babel/plugin-proposal-object-rest-spread": "7.7.7", 17 | "@babel/plugin-syntax-dynamic-import": "7.7.4", 18 | "@babel/plugin-transform-async-to-generator": "7.7.4", 19 | "@babel/plugin-transform-modules-commonjs": "7.7.5", 20 | "@babel/polyfill": "7.7.0", 21 | "@babel/preset-env": "7.7.7", 22 | "@babel/preset-react": "7.7.4", 23 | "@babel/register": "7.7.7", 24 | "autoprefixer": "~9.7.3", 25 | "babel-eslint": "10.0.3", 26 | "babel-loader": "8.0.6", 27 | "babel-plugin-dynamic-import-node": "2.3.0", 28 | "css-loader": "3.4.1", 29 | "eslint": "6.8.0", 30 | "eslint-config-prettier": "6.9.0", 31 | "eslint-plugin-import": "2.19.1", 32 | "eslint-plugin-jest": "23.2.0", 33 | "eslint-plugin-jsx-a11y": "6.2.3", 34 | "eslint-plugin-prettier": "3.1.2", 35 | "eslint-plugin-react": "7.17.0", 36 | "file-loader": "5.0.2", 37 | "html-loader": "0.5.5", 38 | "html-webpack-plugin": "3.2.0", 39 | "json-loader": "0.5.7", 40 | "node-sass": "^4.13.1", 41 | "postcss-color-function": "4.1.0", 42 | "postcss-import": "12.0.1", 43 | "postcss-loader": "3.0.0", 44 | "postcss-simple-vars": "5.0.2", 45 | "postcss-url": "8.0.0", 46 | "precss": "4.0.0", 47 | "prettier": "1.19.1", 48 | "react-hot-loader": "4.12.18", 49 | "redux-devtools": "3.5.0", 50 | "sass-loader": "^8.0.0", 51 | "style-loader": "1.1.2", 52 | "stylelint": "12.0.1", 53 | "webpack": "^4.41.5", 54 | "webpack-cli": "3.3.10", 55 | "webpack-dev-server": "3.10.1", 56 | "webpack-node-externals": "1.7.2", 57 | "why-did-you-update": "^1.0.6" 58 | }, 59 | "dependencies": { 60 | "@types/react": "^16.9.17", 61 | "axios": "0.19.0", 62 | "bootstrap": "4.4.1", 63 | "classnames": "2.2.6", 64 | "connected-react-router": "^6.6.1", 65 | "history": "^4.10.1", 66 | "lodash": "^4.17.19", 67 | "prop-types": "15.7.2", 68 | "react": "16.12.0", 69 | "react-bootstrap": "^1.0.0-beta.5", 70 | "react-dom": "16.12.0", 71 | "react-redux": "7.1.3", 72 | "react-router": "5.1.2", 73 | "react-router-bootstrap": "^0.25.0", 74 | "react-router-dom": "^5.1.2", 75 | "redux": "4.0.5", 76 | "redux-actions": "2.6.5", 77 | "redux-dynamic-reducer": "2.0.2", 78 | "redux-promise": "0.6.0", 79 | "redux-thunk": "2.3.0", 80 | "uuid": "3.3.3" 81 | }, 82 | "browserslist": { 83 | "production": [ 84 | ">0.2%", 85 | "not dead", 86 | "not op_mini all" 87 | ], 88 | "development": [ 89 | "last 1 chrome version", 90 | "last 1 firefox version", 91 | "last 1 safari version" 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plugin system for ReactJS apps with Redux datalayer 2 | 3 | Although plugin systems are usually custom and application-specific it's nice to have some inspiration. When I had to build one I was having trouble finding anything that worked/looked clean/was easily extendable. So here's my take on this problem. 4 | 5 | ## Requirements 6 | 7 | This is a pretty simple extension to your typical React application. What you'll need is : 8 | * ReactJS 16+ 9 | * Redux 3+ 10 | * react-router 4.x+ 11 | 12 | ## Contents of this repository 13 | 14 | The structure of this repo can be divided into two separate parts: 15 | 16 | #### Essentials 17 | 18 | * webpack.config.js.dist - starter development webpack config 19 | * webpack.prod.js.dist - starter production webpack config 20 | * plugins.dist.js - config file for loading plugins 21 | * .babelrc - minimal Babel config to load plugins properly 22 | * src/ - javascript to start building your app. Needs to be extended. 23 | * actions/PluginActions.js - plugin-specific action-creators 24 | * components/App.js - wrapper around redux's Provider and react-router Routes (which is basically the root of the app - just render it to an html element) that loads plugins from config on init 25 | * components/CustomRouter.js - wrapper around react-router's Routes component that extends it's functionality 26 | * reducers/index.js - merges reducers 27 | * reducers/pluginsHandler.js - redux reducer responsible for storing plugins info in the store 28 | * services/PluginsRegistry.js - stores info about loaded plugins 29 | * configureStore.js - initialize redux store 30 | * routes.js - define your react-router routes here + load plugin routes 31 | 32 | #### Examples 33 | 34 | Complete applications showing how to integrate plugins to achieve certain results: 35 | 36 | * basic - application that loads another (almost) separate application using a plugin. This is just to focus on the most basic boilerplate to get you started without the apps really communicating between themselves. 37 | * (in the works) advanced - a more complex application that extends it's basic functionality with plugin 38 | * (planned) multiple plugins example 39 | 40 | # Howto 41 | 42 | This part describes the requirements and process of creating and loading a custom plugin. 43 | 44 | ## Loading plugins in the application 45 | 46 | All plugins are dynamically loaded on application start from separate script files. In order to pickup new plugins, user must : 47 | 48 | 1. Provide a plugins.js file, that will be loaded by the app. If file does not exist it can be created by copying the default config: 49 | 50 | > cp plugins.js.dist plugins.js 51 | 52 | or in case of production build: 53 | 54 | > cp plugins.js.dist ./dist/plugins.js 55 | 56 | 2. Add plugins names to the array inside the config, ie : 57 | 58 | ```javascript 59 | //plugins.js 60 | 61 | module.exports = { 62 | PLUGINS: [], 63 | };; 64 | ``` 65 | 66 | 3. Copy your plugins scripts to folders named after values inserted in the config array and placed in the main plugins folder. Mind scripts are expected to have `index.js` name. 67 | 68 | > cp index.js ./plugins/plugin1/ 69 | > cp index.js ./plugins/plugin2/ 70 | 71 | 4. Build the application 72 | 73 | `npm start` 74 | 75 | Runs the app in the development mode.
    76 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 77 | 78 | The page will reload if you make edits.
    79 | You will also see any lint errors in the console. 80 | 81 | `npm run build` 82 | 83 | Builds the app for production to the `build` folder.
    84 | It correctly bundles React in production mode and optimizes the build for the best performance. 85 | 86 | The build is minified and the filenames include the hashes.
    87 | Your app is ready to be deployed! 88 | 89 | # Building custom plugins 90 | 91 | Since the plugins are imported as separate modules, they must be built in such a way that dynamic commonjs imports will work. Here's a short guide on how to configure [Webpack](https://webpack.js.org/) bundler with [Babel](https://babeljs.io/) precompiler in production mode (right now code minification is not supported for the plugins). 92 | 93 | 94 | ## Build configuration 95 | 96 | For the purpose of this guide we will be using configurations as required by Babel 6.20.x and Webpack 4.7.x . 97 | 98 | ### Babel 99 | 100 | To compile javascript using Babel compiler you will need two plugins : `babel-plugin-add-module-exports` and `babel-plugin-syntax-dynamic-import`. Both can be installed via yarn or npm : 101 | 102 | > npm install --save-dev babel-plugin-add-module-exports babel-plugin-syntax-dynamic-import 103 | 104 | and then added to the `.babelrc` config file: 105 | 106 | ```javascript 107 | { 108 | "plugins": [ 109 | "add-module-exports", 110 | "babel-plugin-syntax-dynamic-import", 111 | ] 112 | } 113 | ``` 114 | 115 | ### Webpack 116 | 117 | This is a basic config for Webpack. One important thing to notice is the `libraryTarget` option for the output code. 118 | 119 | ```javascript 120 | var path = require('path'); 121 | 122 | module.exports = { 123 | mode: 'production', 124 | entry: [ 125 | './index.jsx' 126 | ], 127 | optimization: { 128 | minimize: false, 129 | }, 130 | output: { 131 | path: path.join(__dirname, 'dist'), 132 | filename: 'index.js', 133 | publicPath: '/', 134 | libraryTarget: 'commonjs2' 135 | }, 136 | module: { 137 | rules: [{ 138 | test: /\.jsx?$/, 139 | loader: 'babel-loader', 140 | include: path.join(__dirname, 'src') 141 | }, 142 | ]}, 143 | resolve: { 144 | extensions: ['.js', '.json'] 145 | } 146 | }; 147 | ``` 148 | 149 | ## Plugin architecture basics 150 | 151 | This section describes how the plugins code should be structured, available options, handling data etc. 152 | 153 | ### API 154 | 155 | Plugins need to provide a certain API to properly work with the application. This is a sample module code we will use to describe each of the options. 156 | 157 | ```javascript 158 | const api = { 159 | routes: [ 160 | { 161 | path: '/myplugin', 162 | component: Main, 163 | // optional 164 | indexRoute: { 165 | component: IndexComponent, 166 | }, 167 | childRoutes: [ 168 | { 169 | path: '/myplugin/child-route', 170 | component: ChildComponent, 171 | }, 172 | ], 173 | }, 174 | ], 175 | reducers: { 176 | name: 'myplugin', 177 | reducer, 178 | }, 179 | }; 180 | ``` 181 | 182 | **routes** 183 | 184 | Right now this system supports `react-router` (v3) for routing, which plugins can further extend. This option expects an array of [static routes](https://reacttraining.com/react-router/core/guides/philosophy/static-routing), which support all of the functionality provided by `react-router`. Nested child routes require a full path, with parent's prefix, ie `/myparent/child`. 185 | 186 | **reducers** 187 | 188 | This setting is used for extending the parent application's [redux](https://redux.js.org/) reducer. All plugins reducers will be branched on the main reducer tree under `plugins` key. It expects an object with two keys: 189 | * name - reducer name 190 | * reducer - reducer function 191 | 192 | ### Data handling 193 | 194 | Data layer is powered by the well respected [redux](https://redux.js.org/) store. This stays true for the plugins, as the main plugins component is wrapped in the redux's Provider (wich gives access to the store). Please check the official [guide](https://redux.js.org/basics/usage-with-react) for details on how to connect components with the store. Here's a minimal example showing how to provide user's id to your component's props: 195 | 196 | ```javascript 197 | class MyComponent extends Component { 198 | } 199 | 200 | function mapStateToProps({ plugins }) { 201 | return { 202 | userId: plugins[pluginName].userId, 203 | }; 204 | } 205 | 206 | export default connect(mapStateToProps)(MyComponent); 207 | ``` 208 | 209 | For simplicity (or in case of using functional components) there are two additional properties available: 210 | * store - handler for the redux store 211 | * dispatch - store's function for dispatching actions 212 | 213 | Remember about the fact, that plugin's are added to the redux tree under `plugins` key. So any data query should be prepended with `plugins` followed by the plugin name. 214 | 215 | ## Learn More 216 | -------------------------------------------------------------------------------- /examples/basic/plugins/example/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 41 | /******/ } 42 | /******/ }; 43 | /******/ 44 | /******/ // define __esModule on exports 45 | /******/ __webpack_require__.r = function(exports) { 46 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 47 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 48 | /******/ } 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // create a fake namespace object 53 | /******/ // mode & 1: value is a module id, require it 54 | /******/ // mode & 2: merge all properties of value into the ns 55 | /******/ // mode & 4: return value when already ns object 56 | /******/ // mode & 8|1: behave like require 57 | /******/ __webpack_require__.t = function(value, mode) { 58 | /******/ if(mode & 1) value = __webpack_require__(value); 59 | /******/ if(mode & 8) return value; 60 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 61 | /******/ var ns = Object.create(null); 62 | /******/ __webpack_require__.r(ns); 63 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 64 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 65 | /******/ return ns; 66 | /******/ }; 67 | /******/ 68 | /******/ // getDefaultExport function for compatibility with non-harmony modules 69 | /******/ __webpack_require__.n = function(module) { 70 | /******/ var getter = module && module.__esModule ? 71 | /******/ function getDefault() { return module['default']; } : 72 | /******/ function getModuleExports() { return module; }; 73 | /******/ __webpack_require__.d(getter, 'a', getter); 74 | /******/ return getter; 75 | /******/ }; 76 | /******/ 77 | /******/ // Object.prototype.hasOwnProperty.call 78 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 79 | /******/ 80 | /******/ // __webpack_public_path__ 81 | /******/ __webpack_require__.p = "/"; 82 | /******/ 83 | /******/ 84 | /******/ // Load entry module and return exports 85 | /******/ return __webpack_require__(__webpack_require__.s = 4); 86 | /******/ }) 87 | /************************************************************************/ 88 | /******/ ([ 89 | /* 0 */ 90 | /***/ (function(module, exports) { 91 | 92 | module.exports = function(module) { 93 | if (!module.webpackPolyfill) { 94 | module.deprecate = function() {}; 95 | module.paths = []; 96 | // module.parent = undefined by default 97 | if (!module.children) module.children = []; 98 | Object.defineProperty(module, "loaded", { 99 | enumerable: true, 100 | get: function() { 101 | return module.l; 102 | } 103 | }); 104 | Object.defineProperty(module, "id", { 105 | enumerable: true, 106 | get: function() { 107 | return module.i; 108 | } 109 | }); 110 | module.webpackPolyfill = 1; 111 | } 112 | return module; 113 | }; 114 | 115 | 116 | /***/ }), 117 | /* 1 */ 118 | /***/ (function(module, exports) { 119 | 120 | module.exports = require("react"); 121 | 122 | /***/ }), 123 | /* 2 */ 124 | /***/ (function(module, exports) { 125 | 126 | module.exports = require("react-router-dom"); 127 | 128 | /***/ }), 129 | /* 3 */ 130 | /***/ (function(module, exports) { 131 | 132 | module.exports = require("react-redux"); 133 | 134 | /***/ }), 135 | /* 4 */ 136 | /***/ (function(module, exports, __webpack_require__) { 137 | 138 | module.exports = __webpack_require__(5); 139 | 140 | 141 | /***/ }), 142 | /* 5 */ 143 | /***/ (function(module, exports, __webpack_require__) { 144 | 145 | "use strict"; 146 | /* WEBPACK VAR INJECTION */(function(module) { 147 | 148 | Object.defineProperty(exports, "__esModule", { 149 | value: true 150 | }); 151 | exports.default = void 0; 152 | 153 | var _react = _interopRequireDefault(__webpack_require__(1)); 154 | 155 | var _reactRouterDom = __webpack_require__(2); 156 | 157 | var _App = _interopRequireDefault(__webpack_require__(6)); 158 | 159 | var _Start = _interopRequireDefault(__webpack_require__(11)); 160 | 161 | var _Cats = _interopRequireDefault(__webpack_require__(12)); 162 | 163 | var _Posts = _interopRequireDefault(__webpack_require__(13)); 164 | 165 | var _reducers = _interopRequireDefault(__webpack_require__(17)); 166 | 167 | (function () { 168 | var enterModule = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.enterModule : undefined; 169 | enterModule && enterModule(module); 170 | })(); 171 | 172 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 173 | 174 | var __signature__ = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.default.signature : function (a) { 175 | return a; 176 | }; 177 | 178 | var api = { 179 | routes: _react.default.createElement(_App.default, null, _react.default.createElement(_reactRouterDom.Route, { 180 | exact: true, 181 | path: "/plugins", 182 | component: _Start.default 183 | }), _react.default.createElement(_reactRouterDom.Route, { 184 | path: "/plugins/cats", 185 | component: _Cats.default 186 | }), _react.default.createElement(_reactRouterDom.Route, { 187 | path: "/plugins/posts", 188 | component: _Posts.default 189 | })), 190 | reducers: { 191 | name: 'example', 192 | reducer: _reducers.default 193 | } 194 | }; 195 | var _default = api; 196 | var _default2 = _default; 197 | exports.default = _default2; 198 | ; 199 | 200 | (function () { 201 | var reactHotLoader = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.default : undefined; 202 | 203 | if (!reactHotLoader) { 204 | return; 205 | } 206 | 207 | reactHotLoader.register(api, "api", "/minadmin/js/rps-basic/src/index.jsx"); 208 | reactHotLoader.register(_default, "default", "/minadmin/js/rps-basic/src/index.jsx"); 209 | })(); 210 | 211 | ; 212 | 213 | (function () { 214 | var leaveModule = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.leaveModule : undefined; 215 | leaveModule && leaveModule(module); 216 | })(); 217 | 218 | module.exports = exports.default; 219 | /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(0)(module))) 220 | 221 | /***/ }), 222 | /* 6 */ 223 | /***/ (function(module, exports, __webpack_require__) { 224 | 225 | "use strict"; 226 | /* WEBPACK VAR INJECTION */(function(module) { 227 | 228 | Object.defineProperty(exports, "__esModule", { 229 | value: true 230 | }); 231 | exports.default = void 0; 232 | 233 | var _react = _interopRequireDefault(__webpack_require__(1)); 234 | 235 | var _reactRouterDom = __webpack_require__(2); 236 | 237 | __webpack_require__(7); 238 | 239 | (function () { 240 | var enterModule = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.enterModule : undefined; 241 | enterModule && enterModule(module); 242 | })(); 243 | 244 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 245 | 246 | var __signature__ = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.default.signature : function (a) { 247 | return a; 248 | }; 249 | 250 | var App = function App(_ref) { 251 | var children = _ref.children; 252 | return _react.default.createElement("div", { 253 | className: "container cats-posts-application" 254 | }, _react.default.createElement("div", { 255 | className: "row" 256 | }, _react.default.createElement("div", { 257 | className: "col-12" 258 | }, _react.default.createElement("header", null, _react.default.createElement("ul", { 259 | className: "nav" 260 | }, _react.default.createElement("li", { 261 | className: "nav-item" 262 | }, _react.default.createElement(_reactRouterDom.Link, { 263 | to: '/' 264 | }, "Home")), _react.default.createElement("li", { 265 | className: "nav-item" 266 | }, _react.default.createElement(_reactRouterDom.Link, { 267 | to: '/plugins/cats' 268 | }, "Cats")), _react.default.createElement("li", { 269 | className: "nav-item" 270 | }, _react.default.createElement(_reactRouterDom.Link, { 271 | to: '/plugins/posts' 272 | }, "Posts")))))), _react.default.createElement("div", { 273 | className: "row" 274 | }, _react.default.createElement("div", { 275 | className: "col-12" 276 | }, children))); 277 | }; 278 | 279 | var _default = App; 280 | var _default2 = _default; 281 | exports.default = _default2; 282 | ; 283 | 284 | (function () { 285 | var reactHotLoader = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.default : undefined; 286 | 287 | if (!reactHotLoader) { 288 | return; 289 | } 290 | 291 | reactHotLoader.register(App, "App", "/minadmin/js/rps-basic/src/components/App.js"); 292 | reactHotLoader.register(_default, "default", "/minadmin/js/rps-basic/src/components/App.js"); 293 | })(); 294 | 295 | ; 296 | 297 | (function () { 298 | var leaveModule = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.leaveModule : undefined; 299 | leaveModule && leaveModule(module); 300 | })(); 301 | 302 | module.exports = exports.default; 303 | /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(0)(module))) 304 | 305 | /***/ }), 306 | /* 7 */ 307 | /***/ (function(module, exports, __webpack_require__) { 308 | 309 | var api = __webpack_require__(8); 310 | var content = __webpack_require__(9); 311 | 312 | content = content.__esModule ? content.default : content; 313 | 314 | if (typeof content === 'string') { 315 | content = [[module.i, content, '']]; 316 | } 317 | 318 | var options = {}; 319 | 320 | options.insert = "head"; 321 | options.singleton = false; 322 | 323 | var update = api(module.i, content, options); 324 | 325 | var exported = content.locals ? content.locals : {}; 326 | 327 | 328 | 329 | module.exports = exported; 330 | 331 | /***/ }), 332 | /* 8 */ 333 | /***/ (function(module, exports, __webpack_require__) { 334 | 335 | "use strict"; 336 | 337 | 338 | var isOldIE = function isOldIE() { 339 | var memo; 340 | return function memorize() { 341 | if (typeof memo === 'undefined') { 342 | // Test for IE <= 9 as proposed by Browserhacks 343 | // @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805 344 | // Tests for existence of standard globals is to allow style-loader 345 | // to operate correctly into non-standard environments 346 | // @see https://github.com/webpack-contrib/style-loader/issues/177 347 | memo = Boolean(window && document && document.all && !window.atob); 348 | } 349 | 350 | return memo; 351 | }; 352 | }(); 353 | 354 | var getTarget = function getTarget() { 355 | var memo = {}; 356 | return function memorize(target) { 357 | if (typeof memo[target] === 'undefined') { 358 | var styleTarget = document.querySelector(target); // Special case to return head of iframe instead of iframe itself 359 | 360 | if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) { 361 | try { 362 | // This will throw an exception if access to iframe is blocked 363 | // due to cross-origin restrictions 364 | styleTarget = styleTarget.contentDocument.head; 365 | } catch (e) { 366 | // istanbul ignore next 367 | styleTarget = null; 368 | } 369 | } 370 | 371 | memo[target] = styleTarget; 372 | } 373 | 374 | return memo[target]; 375 | }; 376 | }(); 377 | 378 | var stylesInDom = {}; 379 | 380 | function modulesToDom(moduleId, list, options) { 381 | for (var i = 0; i < list.length; i++) { 382 | var part = { 383 | css: list[i][1], 384 | media: list[i][2], 385 | sourceMap: list[i][3] 386 | }; 387 | 388 | if (stylesInDom[moduleId][i]) { 389 | stylesInDom[moduleId][i](part); 390 | } else { 391 | stylesInDom[moduleId].push(addStyle(part, options)); 392 | } 393 | } 394 | } 395 | 396 | function insertStyleElement(options) { 397 | var style = document.createElement('style'); 398 | var attributes = options.attributes || {}; 399 | 400 | if (typeof attributes.nonce === 'undefined') { 401 | var nonce = true ? __webpack_require__.nc : undefined; 402 | 403 | if (nonce) { 404 | attributes.nonce = nonce; 405 | } 406 | } 407 | 408 | Object.keys(attributes).forEach(function (key) { 409 | style.setAttribute(key, attributes[key]); 410 | }); 411 | 412 | if (typeof options.insert === 'function') { 413 | options.insert(style); 414 | } else { 415 | var target = getTarget(options.insert || 'head'); 416 | 417 | if (!target) { 418 | throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid."); 419 | } 420 | 421 | target.appendChild(style); 422 | } 423 | 424 | return style; 425 | } 426 | 427 | function removeStyleElement(style) { 428 | // istanbul ignore if 429 | if (style.parentNode === null) { 430 | return false; 431 | } 432 | 433 | style.parentNode.removeChild(style); 434 | } 435 | /* istanbul ignore next */ 436 | 437 | 438 | var replaceText = function replaceText() { 439 | var textStore = []; 440 | return function replace(index, replacement) { 441 | textStore[index] = replacement; 442 | return textStore.filter(Boolean).join('\n'); 443 | }; 444 | }(); 445 | 446 | function applyToSingletonTag(style, index, remove, obj) { 447 | var css = remove ? '' : obj.css; // For old IE 448 | 449 | /* istanbul ignore if */ 450 | 451 | if (style.styleSheet) { 452 | style.styleSheet.cssText = replaceText(index, css); 453 | } else { 454 | var cssNode = document.createTextNode(css); 455 | var childNodes = style.childNodes; 456 | 457 | if (childNodes[index]) { 458 | style.removeChild(childNodes[index]); 459 | } 460 | 461 | if (childNodes.length) { 462 | style.insertBefore(cssNode, childNodes[index]); 463 | } else { 464 | style.appendChild(cssNode); 465 | } 466 | } 467 | } 468 | 469 | function applyToTag(style, options, obj) { 470 | var css = obj.css; 471 | var media = obj.media; 472 | var sourceMap = obj.sourceMap; 473 | 474 | if (media) { 475 | style.setAttribute('media', media); 476 | } else { 477 | style.removeAttribute('media'); 478 | } 479 | 480 | if (sourceMap && btoa) { 481 | css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */"); 482 | } // For old IE 483 | 484 | /* istanbul ignore if */ 485 | 486 | 487 | if (style.styleSheet) { 488 | style.styleSheet.cssText = css; 489 | } else { 490 | while (style.firstChild) { 491 | style.removeChild(style.firstChild); 492 | } 493 | 494 | style.appendChild(document.createTextNode(css)); 495 | } 496 | } 497 | 498 | var singleton = null; 499 | var singletonCounter = 0; 500 | 501 | function addStyle(obj, options) { 502 | var style; 503 | var update; 504 | var remove; 505 | 506 | if (options.singleton) { 507 | var styleIndex = singletonCounter++; 508 | style = singleton || (singleton = insertStyleElement(options)); 509 | update = applyToSingletonTag.bind(null, style, styleIndex, false); 510 | remove = applyToSingletonTag.bind(null, style, styleIndex, true); 511 | } else { 512 | style = insertStyleElement(options); 513 | update = applyToTag.bind(null, style, options); 514 | 515 | remove = function remove() { 516 | removeStyleElement(style); 517 | }; 518 | } 519 | 520 | update(obj); 521 | return function updateStyle(newObj) { 522 | if (newObj) { 523 | if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) { 524 | return; 525 | } 526 | 527 | update(obj = newObj); 528 | } else { 529 | remove(); 530 | } 531 | }; 532 | } 533 | 534 | module.exports = function (moduleId, list, options) { 535 | options = options || {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of