43 |
50 | >
51 | ));
52 | }, 5000);
53 | }
54 | deactivate() {
55 | this.pluginStore.removeFunction('ClickMe.sendAlert');
56 | }
57 | }
58 |
59 | export default ClickMePlugin;
60 |
61 | type PluginStoreClickMe = {
62 | executeFunction(functionName: `ClickMe.add`, msg: string): void;
63 | executeFunction(functionName: 'ClickMe.remove', msg: string): void;
64 | };
65 |
66 | export { PluginStoreClickMe };
67 |
--------------------------------------------------------------------------------
/src/plugins/RendererPlugin/index.tsx:
--------------------------------------------------------------------------------
1 | import { IPlugin } from '../../interfaces/IPlugin';
2 | import { PluginStore } from '../../PluginStore';
3 | import { Renderer } from './components/Renderer';
4 | import ComponentUpdatedEvent from './events/ComponentUpdatedEvent';
5 | import randomString from './randomString';
6 |
7 | export class RendererPlugin implements IPlugin {
8 | public pluginStore: PluginStore = new PluginStore();
9 | private componentMap = new Map<
10 | string,
11 | Array<{
12 | component: React.ComponentClass;
13 | key?: string;
14 | }>
15 | >();
16 |
17 | getPluginName() {
18 | return 'Renderer@1.0.0';
19 | }
20 | getDependencies() {
21 | return [];
22 | }
23 |
24 | init(pluginStore: PluginStore) {
25 | this.pluginStore = pluginStore;
26 | }
27 |
28 | addToComponentMap(
29 | position: string,
30 | component: React.ComponentClass,
31 | key?: string
32 | ) {
33 | let array = this.componentMap.get(position);
34 | let componentKey = key ? key : randomString(8);
35 | if (!array) {
36 | array = [{ component, key: componentKey }];
37 | } else {
38 | array.push({ component, key: componentKey });
39 | }
40 | this.componentMap.set(position, array);
41 | this.pluginStore.dispatchEvent(
42 | new ComponentUpdatedEvent('Renderer.componentUpdated', position)
43 | );
44 | }
45 |
46 | removeFromComponentMap(position: string, component: React.ComponentClass) {
47 | let array = this.componentMap.get(position);
48 | if (array) {
49 | array.splice(
50 | array.findIndex(item => item.component === component),
51 | 1
52 | );
53 | }
54 | this.pluginStore.dispatchEvent(
55 | new ComponentUpdatedEvent('Renderer.componentUpdated', position)
56 | );
57 | }
58 |
59 | getRendererComponent() {
60 | return Renderer;
61 | }
62 |
63 | getComponentsInPosition(position: string) {
64 | let componentArray = this.componentMap.get(position);
65 | if (!componentArray) return [];
66 |
67 | return componentArray;
68 | }
69 |
70 | activate() {
71 | this.pluginStore.addFunction(
72 | 'Renderer.add',
73 | this.addToComponentMap.bind(this)
74 | );
75 |
76 | this.pluginStore.addFunction(
77 | 'Renderer.getComponentsInPosition',
78 | this.getComponentsInPosition.bind(this)
79 | );
80 |
81 | this.pluginStore.addFunction(
82 | 'Renderer.getRendererComponent',
83 | this.getRendererComponent.bind(this)
84 | );
85 |
86 | this.pluginStore.addFunction(
87 | 'Renderer.remove',
88 | this.removeFromComponentMap.bind(this)
89 | );
90 | }
91 |
92 | deactivate() {
93 | this.pluginStore.removeFunction('Renderer.add');
94 |
95 | this.pluginStore.removeFunction('Renderer.getComponentsInPosition');
96 |
97 | this.pluginStore.removeFunction('Renderer.getRendererComponent');
98 |
99 | this.pluginStore.removeFunction('Renderer.remove');
100 | }
101 | }
102 |
103 | export type PluginStoreRenderer = {
104 | executeFunction(
105 | functionName: 'Renderer.getComponentsInPosition',
106 | position: string
107 | ): Array;
108 | };
109 |
--------------------------------------------------------------------------------
/src/PluginStore.tsx:
--------------------------------------------------------------------------------
1 | import { Event } from './Event';
2 | import { EventCallableRegsitry } from './EventCallableRegsitry';
3 | import { IPlugin } from './interfaces/IPlugin';
4 | import dependencyValid from './utils/dependencyValid';
5 |
6 | export class PluginStore {
7 | private functionArray: Map;
8 | private pluginMap: Map;
9 | private _eventCallableRegistry: EventCallableRegsitry = new EventCallableRegsitry();
10 |
11 | constructor() {
12 | this.functionArray = new Map();
13 | this.pluginMap = new Map();
14 | }
15 |
16 | install(plugin: IPlugin) {
17 | const pluginNameAndVer = plugin.getPluginName();
18 | const [pluginName] = pluginNameAndVer.split('@');
19 | const pluginDependencies = plugin.getDependencies() || [];
20 |
21 | let installationErrors: string[] = [];
22 | pluginDependencies.forEach((dep: string) => {
23 | const [depName, depVersion] = dep.split('@');
24 | const installedNameAndVer = this.getInstalledPluginNameWithVersion(
25 | depName
26 | );
27 | const [, installedVersion] = installedNameAndVer
28 | ? installedNameAndVer.split('@')
29 | : [null, ''];
30 | if (!installedNameAndVer) {
31 | installationErrors.push(
32 | `Error installing ${pluginNameAndVer}. Could not find dependency ${dep}.`
33 | );
34 | } else if (!dependencyValid(installedVersion, depVersion)) {
35 | installationErrors.push(
36 | `Error installing ${pluginNameAndVer}.\n${installedNameAndVer} doesn't satisfy the required dependency ${dep}.`
37 | );
38 | }
39 | });
40 |
41 | if (installationErrors.length === 0) {
42 | this.pluginMap.set(pluginName, plugin);
43 | plugin.init(this);
44 | plugin.activate();
45 | } else {
46 | installationErrors.forEach(err => {
47 | console.error(err);
48 | });
49 | }
50 | }
51 |
52 | getInstalledPluginNameWithVersion(name: string) {
53 | const plugin = this.pluginMap.get(name);
54 | if (!plugin) {
55 | return null;
56 | }
57 |
58 | return plugin.getPluginName();
59 | }
60 |
61 | addFunction(key: string, fn: any) {
62 | this.functionArray.set(key, fn);
63 | }
64 |
65 | executeFunction(key: string, ...args: any): any {
66 | let fn = this.functionArray.get(key);
67 | if (fn) {
68 | return fn(...args);
69 | }
70 | console.error('No function added for the key ' + key + '.');
71 | }
72 |
73 | removeFunction(key: string): void {
74 | this.functionArray.delete(key);
75 | }
76 |
77 | uninstall(key: string) {
78 | let plugin = this.pluginMap.get(key);
79 |
80 | if (plugin) {
81 | plugin.deactivate();
82 | this.pluginMap.delete(key);
83 | }
84 | }
85 |
86 | addEventListener(
87 | name: string,
88 | callback: (event: EventType) => void
89 | ) {
90 | this._eventCallableRegistry.addEventListener(name, callback);
91 | }
92 | removeEventListener(
93 | name: string,
94 | callback: (event: EventType) => void
95 | ) {
96 | this._eventCallableRegistry.removeEventListener(name, callback);
97 | }
98 | dispatchEvent(event: EventType) {
99 | // @ts-ignore
100 | this._eventCallableRegistry.dispatchEvent(event);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at strapui-support@sahusoft.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We want to make contributing to this project as easy and transparent as possible and we are grateful for, any contributions made by the community. By contributing to React Pluggable, you agree to abide by the [code of conduct](https://github.com/GeekyAnts/react-pluggable/blob/master/CODE_OF_CONDUCT.md).
4 |
5 | ## Reporting Issues and Asking Questions
6 |
7 | Before opening an issue, please search the [issue tracker](https://github.com/GeekyAnts/react-pluggable/issues) to make sure your issue hasn't already been reported.
8 |
9 | ### Bugs and Improvements
10 |
11 | We use the issue tracker to keep track of bugs and improvements to React Pluggable itself, its examples, and the documentation. We encourage you to open issues to discuss improvements, architecture, theory, internal implementation, etc. If a topic has been discussed before, we will ask you to join the previous discussion.
12 |
13 | ## Development
14 |
15 | Visit the [issue tracker](https://github.com/GeekyAnts/react-pluggable/issues) to find a list of open issues that need attention.
16 |
17 | Fork, then clone the repo:
18 |
19 | ```sh
20 | git clone https://github.com/GeekyAnts/react-pluggable.git
21 | ```
22 |
23 | ### Building
24 |
25 | #### Building React Pluggable
26 |
27 | ```sh
28 | yarn build
29 | ```
30 |
31 | ### Testing and Linting
32 |
33 | To only run linting:
34 |
35 | ```sh
36 | yarn lint
37 | ```
38 |
39 | To only run tests:
40 |
41 | ```sh
42 | yarn test
43 | ```
44 |
45 | ### Docs
46 |
47 | Improvements to the documentation are always welcome. You can find them in the on [`react-pluggable.github.io`](https://github.com/react-pluggable/react-pluggable.github.io) repository. We use [Docusaurus](https://docusaurus.io/) to build our documentation website. The website is published automatically whenever the `master` branch is updated.
48 |
49 | ### Examples
50 |
51 | React Pluggabel comes with a Todo App example to demonstrate various concepts and best practices.
52 |
53 | When adding a new example, please adhere to the style and format of the existing examples, and try to reuse as much code as possible.
54 |
55 | #### Testing the Examples
56 |
57 | To test the official React Pluggabel examples, run the following:
58 |
59 | Install dependencies using yarn
60 |
61 | ```sh
62 | yarn
63 | ```
64 |
65 | Then run the example using
66 |
67 | ```sh
68 | yarn start
69 | ```
70 |
71 | Not all examples have tests. If you see an example project without tests, you are very welcome to add them in a way consistent with the examples that have tests.
72 |
73 | Please visit the [Examples page](https://react-pluggable.github.io/docs/hello-world-example) for information on running individual examples.
74 |
75 | ### Sending a Pull Request
76 |
77 | For non-trivial changes, please open an issue with a proposal for a new feature or refactoring before starting on the work. We don't want you to waste your efforts on a pull request that we won't want to accept.
78 |
79 | In general, the contribution workflow looks like this:
80 |
81 | - Open a new issue in the [Issue tracker](https://github.com/GeekyAnts/react-pluggable/issues)
82 | - Fork the repo.
83 | - Create a new feature branch based off the `master` branch.
84 | - Make sure all tests pass and there are no linting errors.
85 | - Submit a pull request, referencing any issues it addresses.
86 |
87 | Please try to keep your pull request focused in scope and avoid including unrelated commits.
88 |
89 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements.
90 |
91 | Thank you for contributing!
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Pluggable
2 |
3 | ### 1) Introduction
4 |
5 | [React Pluggable](https://react-pluggable.github.io/?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README): A plugin system for JS & React apps
6 |
7 | While React itself is a plugin system in a way, it focuses on the abstraction of the UI. It is inherently declarative which makes it intuitive for developers when building UI. With the help of React Pluggable, we can think of our app as a **set of features** instead of a **set of components**. It provides a mixed approach to solve this problem.
8 |
9 | We at [GeekyAnts](https://geekyants.com/?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) have used React Pluggable for large & complex apps like [BuilderX](https://builderx.io/?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) to add independent and dependent features over time, and it has worked wonderfully for us. Find out more on our [official documentation](https://react-pluggable.github.io/?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README).
10 |
11 | ### 2) Motivation
12 |
13 | In React, we think of everything as components. If we want to add a new feature, we make a new component and add it to our app. Every time we have to enable/disable a feature, we have to add/remove that component from the entire app and this becomes cumbersome when working on a complex app where there are lots of features contributed by different developers.
14 |
15 | We are a huge fan of [Laravel](https://laravel.com/) and love how Service Provider works in it. Motivated by how we register any service for the entire app from one place, we built a plugin system that has all your features and can be enabled/disabled with a single line of code.
16 |
17 | React Pluggable simplifies the problem in 3 simple steps:
18 |
19 | 1. To add a new feature in your app, you write it's logic and install it in the plugin store.
20 | 2. You can use that feature anywhere in the app by calling that feature using PluginStore rather than importing that feature directly.
21 | 3. If you do not want a particular plugin in your app, you can uninstall it from your plugin store or just comment out the installation.
22 |
23 | ### 3) Features
24 |
25 | - **Open-closed Principle**
26 |
27 | The O in SOLID stands for Open-closed principle which means that entities should be **open for extension** but **closed for modification**. With React Pluggable, we can add plugins and extend a system without modifying existing files and features.
28 |
29 | - **Imperative APIs for Extensibility**
30 |
31 | React is inherently declarative which makes it intuitive for developers when building UI but it also makes extensibility hard.
32 |
33 | - **Thinking Features over Components**
34 |
35 | React abstracts components very well but a feature may have more than just components. React Pluggable pushes you to a **feature mindset** instead of a **component mindset**.
36 |
37 | ### 4) Installation
38 |
39 | Use npm or yarn to install this to your application:
40 |
41 | ```
42 | npm install react-pluggable
43 | yarn add react-pluggable
44 | ```
45 |
46 | ### 5) Usage
47 |
48 | - **Making a plugin**
49 |
50 | _ShowAlertPlugin.tsx_
51 |
52 | ```tsx
53 | import React from 'react';
54 | import { IPlugin, PluginStore } from 'react-pluggable';
55 |
56 | class ShowAlertPlugin implements IPlugin {
57 | public pluginStore: any;
58 |
59 | getPluginName(): string {
60 | return 'ShowAlert';
61 | }
62 |
63 | getDependencies(): string[] {
64 | return [];
65 | }
66 |
67 | init(pluginStore: PluginStore): void {
68 | this.pluginStore = pluginStore;
69 | }
70 |
71 | activate(): void {
72 | this.pluginStore.addFunction('sendAlert', () => {
73 | alert('Hello from the ShowAlert Plugin');
74 | });
75 | }
76 |
77 | deactivate(): void {
78 | this.pluginStore.removeFunction('sendAlert');
79 | }
80 | }
81 |
82 | export default ShowAlertPlugin;`
83 | ```
84 |
85 | - **Adding it to your app**
86 |
87 | _App.tsx_
88 |
89 | ```tsx
90 | import React from 'react';
91 | import './App.css';
92 | import { createPluginStore, PluginProvider } from 'react-pluggable';
93 | import ShowAlertPlugin from './plugins/ShowAlertPlugin';
94 | import Test from './components/Test';
95 |
96 | const pluginStore = createPluginStore();
97 | pluginStore.install(new ShowAlertPlugin());
98 |
99 | const App = () => {
100 | return (
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default App;
108 | ```
109 |
110 | - **Using the plugin**
111 |
112 | _Test.tsx_
113 |
114 | ```tsx
115 | import * as React from 'react';
116 | import { usePluginStore } from 'react-pluggable';
117 |
118 | const Test = () => {
119 | const pluginStore = usePluginStore();
120 |
121 | return (
122 | <>
123 |
130 | >
131 | );
132 | };
133 |
134 | export default Test;
135 | ```
136 |
137 | - **Using the inbuilt renderer**
138 |
139 | Sometimes a plugin has a UI component associated with it. You can implement this functionality by simply building a plugin of your own or using the default plugin provided by the package.
140 |
141 | _SharePlugin.tsx_
142 |
143 | ```tsx
144 | import React from 'react';
145 | import { IPlugin, PluginStore } from 'react-pluggable';
146 |
147 | class SharePlugin implements IPlugin {
148 | public pluginStore: any;
149 |
150 | getPluginName(): string {
151 | return 'Share plugin';
152 | }
153 |
154 | getDependencies(): string[] {
155 | return [];
156 | }
157 |
158 | init(pluginStore: PluginStore): void {
159 | this.pluginStore = pluginStore;
160 | }
161 |
162 | activate(): void {
163 | this.pluginStore.executeFunction('RendererPlugin.add', 'top', () => (
164 |
165 | ));
166 | }
167 |
168 | deactivate(): void {
169 | //
170 | }
171 | }
172 |
173 | export default SharePlugin;
174 | ```
175 |
176 | You can add the inbuilt renderer plugin by importing and installing `RendererPlugin` provided in the package.
177 |
178 | - **Importing the plugin**
179 |
180 | _App.tsx_
181 |
182 | ```tsx
183 | import \* as React from 'react';
184 | import { usePluginStore } from 'react-pluggable';
185 | import {
186 | createPluginStore,
187 | PluginProvider,
188 | RendererPlugin,
189 | } from 'react-pluggable';
190 | import SharePlugin from './plugins/SharePlugin';
191 | import Test from './components/Test';
192 |
193 | const pluginStore = createPluginStore();
194 | pluginStore.install(new RendererPlugin());
195 | pluginStore.install(new SharePlugin());
196 |
197 | function App() {
198 | return (
199 |
200 |
201 |
202 | );
203 | }
204 |
205 | export default App;
206 | ```
207 |
208 | _Test.tsx_
209 |
210 | ```tsx
211 | import \* as React from 'react';
212 | import { usePluginStore } from 'react-pluggable';
213 |
214 | const Test = (props: any) => {
215 | const pluginStore: any = usePluginStore();
216 |
217 | let Renderer = pluginStore.executeFunction(
218 | 'RendererPlugin.getRendererComponent'
219 | );
220 |
221 | return (
222 | <>
223 |