├── 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 |
7 | {todos.map(todo => (
8 | onTodoClick(todo.id)} />
9 | ))}
10 |
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 |
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 |
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 |
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