├── example ├── .npmignore ├── types.ts ├── index.html ├── Plugins │ ├── Plugin2-v4.0.0.tsx │ ├── Plugin3-v1.9.0.tsx │ ├── Plugin3-v1.9.1.tsx │ ├── Plugin1-v2.3.0.tsx │ ├── Plugin1-v2.4.0.tsx │ ├── Plugin2-v3.1.0.tsx │ └── ClickMePlugin.tsx ├── tsconfig.json ├── components │ └── Test.tsx ├── package.json └── index.tsx ├── .gitignore ├── src ├── hooks │ ├── createPluginStore.tsx │ ├── usePluginStore.tsx │ └── useForceUpdate.ts ├── PluginStoreContext.tsx ├── interfaces │ └── IPlugin.tsx ├── plugins │ └── RendererPlugin │ │ ├── events │ │ └── ComponentUpdatedEvent.ts │ │ ├── randomString.ts │ │ ├── components │ │ └── Renderer.tsx │ │ └── index.tsx ├── index.tsx ├── utils │ └── dependencyValid.ts ├── PluginProvider.tsx ├── Event.ts ├── EventCallableRegsitry.ts └── PluginStore.tsx ├── test └── blah.test.tsx ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── README.md /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /src/hooks/createPluginStore.tsx: -------------------------------------------------------------------------------- 1 | import { PluginStore } from '../PluginStore'; 2 | 3 | export function createPluginStore() { 4 | return new PluginStore(); 5 | } 6 | -------------------------------------------------------------------------------- /example/types.ts: -------------------------------------------------------------------------------- 1 | import { PluginStore } from '../src'; 2 | import { PluginStoreClickMe } from './Plugins/ClickMePlugin'; 3 | 4 | export type PluginStoreWithPlugins = PluginStore & PluginStoreClickMe; 5 | -------------------------------------------------------------------------------- /src/hooks/usePluginStore.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import PluginStoreContext from '../PluginStoreContext'; 3 | 4 | export function usePluginStore() { 5 | return useContext(PluginStoreContext); 6 | } 7 | -------------------------------------------------------------------------------- /src/PluginStoreContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PluginStore } from './PluginStore'; 3 | 4 | const PluginStoreContext = React.createContext(new PluginStore()); 5 | 6 | export default PluginStoreContext; 7 | -------------------------------------------------------------------------------- /test/blah.test.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom'; 2 | 3 | describe('it', () => { 4 | it('renders without crashing', () => { 5 | const div = document.createElement('div'); 6 | ReactDOM.unmountComponentAtNode(div); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/interfaces/IPlugin.tsx: -------------------------------------------------------------------------------- 1 | import { PluginStore } from '../PluginStore'; 2 | 3 | export interface IPlugin { 4 | pluginStore: PluginStore; 5 | getPluginName(): string; 6 | getDependencies(): string[]; 7 | init(pluginStore: PluginStore): void; 8 | activate(): void; 9 | deactivate(): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/RendererPlugin/events/ComponentUpdatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../../../Event'; 2 | 3 | export default class ComponentUpdatedEvent extends Event { 4 | position: string; 5 | constructor(name: string, position: string) { 6 | super(name); 7 | this.position = position; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { IPlugin } from './interfaces/IPlugin'; 2 | export { createPluginStore } from './hooks/createPluginStore'; 3 | export { PluginProvider } from './PluginProvider'; 4 | export { PluginStore } from './PluginStore'; 5 | export { usePluginStore } from './hooks/usePluginStore'; 6 | export { RendererPlugin } from './plugins/RendererPlugin'; 7 | export { Event } from './Event'; 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/plugins/RendererPlugin/randomString.ts: -------------------------------------------------------------------------------- 1 | export default function randomString(length: number) { 2 | var text = ''; 3 | var possible = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | 6 | text = possible.charAt(Math.floor(Math.random() * 52)); 7 | for (var i = 0; i < length - 1; i++) { 8 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 9 | } 10 | return text; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/dependencyValid.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver'; 2 | 3 | export default function dependencyValid( 4 | installedVersion: string, 5 | requiredVersion: string 6 | ) { 7 | const versionDiff = semver.diff(installedVersion, requiredVersion); 8 | return ( 9 | (versionDiff === null || 10 | versionDiff === 'patch' || 11 | versionDiff === 'minor') && 12 | semver.gte(installedVersion, requiredVersion) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /example/Plugins/Plugin2-v4.0.0.tsx: -------------------------------------------------------------------------------- 1 | import { IPlugin, PluginStore, Event } from '../../src'; 2 | 3 | export default class Plugin2 implements IPlugin { 4 | public pluginStore: PluginStore; 5 | 6 | getPluginName() { 7 | return 'Plugin2@4.0.0'; 8 | } 9 | getDependencies() { 10 | return []; 11 | } 12 | 13 | init(pluginStore) { 14 | this.pluginStore = pluginStore; 15 | } 16 | 17 | activate() {} 18 | deactivate() {} 19 | } 20 | -------------------------------------------------------------------------------- /example/Plugins/Plugin3-v1.9.0.tsx: -------------------------------------------------------------------------------- 1 | import { IPlugin, PluginStore, Event } from '../../src'; 2 | 3 | export default class Plugin3 implements IPlugin { 4 | public pluginStore: PluginStore; 5 | 6 | getPluginName() { 7 | return 'Plugin3@1.9.0'; 8 | } 9 | getDependencies() { 10 | return []; 11 | } 12 | 13 | init(pluginStore) { 14 | this.pluginStore = pluginStore; 15 | } 16 | 17 | activate() {} 18 | deactivate() {} 19 | } 20 | -------------------------------------------------------------------------------- /example/Plugins/Plugin3-v1.9.1.tsx: -------------------------------------------------------------------------------- 1 | import { IPlugin, PluginStore, Event } from '../../src'; 2 | 3 | export default class Plugin3 implements IPlugin { 4 | public pluginStore: PluginStore; 5 | 6 | getPluginName() { 7 | return 'Plugin3@1.9.1'; 8 | } 9 | getDependencies() { 10 | return []; 11 | } 12 | 13 | init(pluginStore) { 14 | this.pluginStore = pluginStore; 15 | } 16 | 17 | activate() {} 18 | deactivate() {} 19 | } 20 | -------------------------------------------------------------------------------- /src/PluginProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PluginStore } from './PluginStore'; 3 | import PluginStoreContext from './PluginStoreContext'; 4 | 5 | export const PluginProvider: React.SFC<{ 6 | pluginStore: PluginStore; 7 | children: any; 8 | }> = ({ pluginStore, children }) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /example/Plugins/Plugin1-v2.3.0.tsx: -------------------------------------------------------------------------------- 1 | import { IPlugin, PluginStore, Event } from '../../src'; 2 | 3 | export default class Plugin1 implements IPlugin { 4 | public pluginStore: PluginStore; 5 | 6 | getPluginName() { 7 | return 'Plugin1@2.3.0'; 8 | } 9 | getDependencies() { 10 | return ['Plugin2@3.0.0', 'Plugin3@1.9.0']; 11 | } 12 | 13 | init(pluginStore) { 14 | this.pluginStore = pluginStore; 15 | } 16 | 17 | activate() {} 18 | deactivate() {} 19 | } 20 | -------------------------------------------------------------------------------- /example/Plugins/Plugin1-v2.4.0.tsx: -------------------------------------------------------------------------------- 1 | import { IPlugin, PluginStore, Event } from '../../src'; 2 | 3 | export default class Plugin1 implements IPlugin { 4 | public pluginStore: PluginStore; 5 | 6 | getPluginName() { 7 | return 'Plugin1@2.4.0'; 8 | } 9 | getDependencies() { 10 | return ['Plugin2@3.0.0', 'Plugin3@1.9.1']; 11 | } 12 | 13 | init(pluginStore) { 14 | this.pluginStore = pluginStore; 15 | } 16 | 17 | activate() {} 18 | deactivate() {} 19 | } 20 | -------------------------------------------------------------------------------- /example/Plugins/Plugin2-v3.1.0.tsx: -------------------------------------------------------------------------------- 1 | import { IPlugin, PluginStore, Event } from '../../src'; 2 | import { PluginStoreWithPlugins } from '../types'; 3 | 4 | export default class Plugin2 implements IPlugin { 5 | public pluginStore: PluginStore; 6 | 7 | getPluginName() { 8 | return 'Plugin2@3.1.0'; 9 | } 10 | getDependencies() { 11 | return []; 12 | } 13 | 14 | init(pluginStore: PluginStoreWithPlugins) { 15 | this.pluginStore = pluginStore; 16 | } 17 | 18 | activate() {} 19 | deactivate() {} 20 | } 21 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES5", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "react", 18 | "esModuleInterop": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | // Returning a new object reference guarantees that a before-and-after 4 | // equivalence check will always be false, resulting in a re-render, even 5 | // when multiple calls to forceUpdate are batched. 6 | 7 | export default function useForceUpdate(): () => void { 8 | const [, dispatch] = useState<{}>(Object.create(null)); 9 | 10 | // Turn dispatch(required_parameter) into dispatch(). 11 | const memoizedDispatch = useCallback((): void => { 12 | dispatch(Object.create(null)); 13 | }, [dispatch]); 14 | return memoizedDispatch; 15 | } 16 | -------------------------------------------------------------------------------- /example/components/Test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { usePluginStore } from '../../src'; 3 | 4 | const Test = (props: any) => { 5 | const pluginStore: any = usePluginStore(); 6 | let Renderer = pluginStore.executeFunction('Renderer.getRendererComponent'); 7 | 8 | return ( 9 | <> 10 |

Working

{' '} 11 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default Test; 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Event.ts: -------------------------------------------------------------------------------- 1 | export class Event { 2 | // target: Layer; 3 | name: string; 4 | // payload: any; 5 | // currentTarget: Layer | undefined; 6 | private _propagate: boolean; 7 | private _defaults: boolean; 8 | constructor(name: string) { 9 | this.name = name; 10 | // this.target = target; 11 | // this.currentTarget = currentTarget; 12 | this._propagate = true; 13 | this._defaults = true; 14 | } 15 | get propagate() { 16 | return this._propagate; 17 | } 18 | get defaults() { 19 | return this._defaults; 20 | } 21 | stopPropagation() { 22 | this._propagate = false; 23 | } 24 | preventDefault() { 25 | this._defaults = false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Begin CI... 9 | uses: actions/checkout@v2 10 | 11 | - name: Use Node 12 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 12.x 15 | 16 | - name: Use cached node_modules 17 | uses: actions/cache@v2 18 | with: 19 | path: node_modules 20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | nodeModules- 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | env: 27 | CI: true 28 | 29 | - name: Lint 30 | run: yarn lint 31 | env: 32 | CI: true 33 | 34 | - name: Test 35 | run: yarn test --ci --coverage --maxWorkers=2 36 | env: 37 | CI: true 38 | 39 | - name: Build 40 | run: yarn build 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /src/EventCallableRegsitry.ts: -------------------------------------------------------------------------------- 1 | import { Event } from './Event'; 2 | export class EventCallableRegsitry { 3 | registry: Map {}>>; 4 | 5 | constructor() { 6 | this.registry = new Map(); 7 | } 8 | 9 | addEventListener(name: string, callback: any) { 10 | let callbacks = this.registry.get(name); 11 | if (callbacks) { 12 | callbacks.push(callback); 13 | } else { 14 | this.registry.set(name, [callback]); 15 | } 16 | } 17 | 18 | removeEventListener(name: string, callback: any) { 19 | let callbacks = this.registry.get(name); 20 | if (callbacks) { 21 | const indexOf = callbacks.indexOf(callback); 22 | if (indexOf > -1) { 23 | callbacks.splice(indexOf, 1); 24 | } 25 | } 26 | } 27 | 28 | dispatchEvent(event: Event) { 29 | // @ts-ignore 30 | if (window['debug']) { 31 | console.log('DEBUG::', 'Event:', event.name); 32 | } 33 | let callbacks = this.registry.get(event.name); 34 | for (let i in callbacks) { 35 | callbacks[i as any](event); 36 | if (!event.propagate) break; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aditya Jamuar 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. -------------------------------------------------------------------------------- /src/plugins/RendererPlugin/components/Renderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import useForceUpdate from '../../../hooks/useForceUpdate'; 3 | import { usePluginStore } from '../../../hooks/usePluginStore'; 4 | import ComponentUpdatedEvent from '../events/ComponentUpdatedEvent'; 5 | 6 | export const Renderer: React.SFC<{ 7 | placement: string; 8 | }> = ({ placement }) => { 9 | const pluginStore = usePluginStore(); 10 | const forceUpdate = useForceUpdate(); 11 | 12 | useEffect(() => { 13 | const eventListener = (event: ComponentUpdatedEvent) => { 14 | if (event.position === placement) { 15 | forceUpdate(); 16 | } 17 | }; 18 | pluginStore.addEventListener('Renderer.componentUpdated', eventListener); 19 | 20 | return () => { 21 | pluginStore.removeEventListener( 22 | 'Renderer.componentUpdated', 23 | eventListener 24 | ); 25 | }; 26 | }, [pluginStore, placement, forceUpdate]); 27 | 28 | let components = pluginStore.executeFunction( 29 | 'Renderer.getComponentsInPosition', 30 | placement 31 | ); 32 | 33 | return ( 34 | <> 35 | {components.map( 36 | (compObject: { component: React.ComponentClass; key: string }) => ( 37 | 38 | ) 39 | )} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { 5 | createPluginStore, 6 | PluginProvider, 7 | PluginStore, 8 | RendererPlugin, 9 | } from '../src'; 10 | import ClickMePlugin, { PluginStoreClickMe } from './Plugins/ClickMePlugin'; 11 | import Test from './components/Test'; 12 | import Plugin1 from './Plugins/Plugin1-v2.4.0'; 13 | import Plugin2 from './Plugins/Plugin2-v3.1.0'; 14 | import Plugin3 from './Plugins/Plugin3-v1.9.1'; 15 | import { PluginStoreWithPlugins } from './types'; 16 | 17 | const pluginStore: PluginStoreWithPlugins = createPluginStore(); 18 | pluginStore.install(new RendererPlugin()); 19 | pluginStore.install(new Plugin3()); 20 | pluginStore.install(new Plugin2()); 21 | pluginStore.install(new Plugin1()); 22 | pluginStore.install(new ClickMePlugin()); 23 | 24 | pluginStore.addEventListener('ClickMe.hello', event => { 25 | console.log('Event received: ', event); 26 | event.stopPropagation(); 27 | }); 28 | 29 | pluginStore.addEventListener('ClickMe.hello', event => { 30 | console.log('Event received second time: ', event); 31 | }); 32 | 33 | const App = () => { 34 | return ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | ReactDOM.render(, document.getElementById('root')); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.3", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test --passWithNoTests", 17 | "lint": "tsdx lint", 18 | "prepare": "tsdx build" 19 | }, 20 | "peerDependencies": { 21 | "react": ">=16" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "tsdx lint" 26 | } 27 | }, 28 | "prettier": { 29 | "printWidth": 80, 30 | "semi": true, 31 | "singleQuote": true, 32 | "trailingComma": "es5" 33 | }, 34 | "name": "react-pluggable", 35 | "author": "Aditya Jamuar", 36 | "module": "dist/react-pluggable.esm.js", 37 | "devDependencies": { 38 | "@types/react": "^16.9.49", 39 | "@types/react-dom": "^16.9.8", 40 | "@types/semver": "^7.3.4", 41 | "husky": "^4.3.0", 42 | "react": "^16.13.1", 43 | "react-dom": "^16.13.1", 44 | "tsdx": "^0.13.3", 45 | "tslib": "^2.0.1", 46 | "typescript": "^4.0.2" 47 | }, 48 | "keywords": [ 49 | "react", 50 | "react-plugins", 51 | "react-pluggable", 52 | "plugin", 53 | "plugin-ecosystem", 54 | "react-ecosystem" 55 | ], 56 | "dependencies": { 57 | "semver": "^7.3.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/Plugins/ClickMePlugin.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import React from 'react'; 3 | import { IPlugin, PluginStore, Event } from '../../src'; 4 | 5 | const namespace = 'ClickMe'; 6 | 7 | class ClickMePlugin implements IPlugin { 8 | public pluginStore: PluginStore; 9 | 10 | getPluginName() { 11 | return `${namespace}@1.0.0`; 12 | } 13 | getDependencies() { 14 | return ['Plugin1@2.3.0']; 15 | } 16 | 17 | init(pluginStore) { 18 | this.pluginStore = pluginStore; 19 | } 20 | 21 | activate() { 22 | this.pluginStore.addFunction(`${namespace}.sendAlert`, (msg: string) => { 23 | alert(msg); 24 | }); 25 | 26 | this.pluginStore.executeFunction('Renderer.add', 'top', () => ( 27 | <> 28 |

asdjkdas

29 | 36 | 37 | )); 38 | 39 | setTimeout(() => { 40 | this.pluginStore.executeFunction('Renderer.add', 'top', () => ( 41 | <> 42 |

Async text

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 Logo 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 |

I am header

224 | 225 | 226 | ); 227 | }; 228 | 229 | export default Test; 230 | ``` 231 | 232 | ### 6) Examples 233 | 234 | Here are some examples of React Pluggable: 235 | 236 | - [**Hello World**](https://react-pluggable.github.io/docs/hello-world-example?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) 237 | - [**Renderer Example**](https://react-pluggable.github.io/docs/renderer-example?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) 238 | - [**Event Example**](https://react-pluggable.github.io/docs/event-example?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) 239 | - [**Typing Example**](https://react-pluggable.github.io/docs/typing-example?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) 240 | - [**Todo App**](https://react-pluggable.github.io/docs/todo-example?utm_source=React%20Pluggable&utm_medium=GitHub&utm_campaign=README) 241 | 242 | ### 7) Tech Stack 243 | 244 | Javascript & React. 245 | 246 | ### 8) Naming Conventions 247 | 248 | Although the naming of plugins, functions, or events is not an enforced rule, we recommend a few conventions to standardize things. 249 | 250 | 1. **Plugin Name**: Let's consider a plugin which provides authentication, named as **Auth**. 251 | 2. **Class Name**: The name of the class will be your plugin name **suffixed with 'Plugin'** i.e. **AuthPlugin**. 252 | 3. **getPluginName:** When returning the name of the plugin, we add <**plugin_name**> with **@**, followed by the **version**. ex : **Auth@1.0.0** (@). 253 | 4. **Adding functions**: While functions can be added to pluginStore with any name, to ensure uniqueness across plugins, we recommend the format `.`. Ex : **Auth.authenticate**. 254 | 5. **Events:** Events can be added and dispatched from pluginStore using any name. To show which event belongs to which plugin, we recommend the format `.` Ex: **Auth.checking**. 255 | 256 | **Example :** 257 | 258 | AuthPlugin.tsx 259 | 260 | ```tsx 261 | import React from 'react'; 262 | import { IPlugin, PluginStore } from 'react-pluggable'; 263 | 264 | class AuthPlugin implements IPlugin { 265 | getPluginName(): string { 266 | return 'Auth@1.0.0'; //line 2 267 | } 268 | 269 | getDependencies(): string[] { 270 | return []; 271 | } 272 | 273 | public pluginStore: any; 274 | 275 | init(pluginStore: PluginStore): void { 276 | this.pluginStore = pluginStore; 277 | } 278 | 279 | authenticate = (credentials: object) => { 280 | // checks the credentials and returns username if matches. 281 | return { name: 'username' }; 282 | }; 283 | 284 | activate(): void { 285 | this.pluginStore.addFunction( 286 | 'Auth.authenticate', //line 3 287 | (credentials: object) => this.authenticate(credentials) 288 | ); 289 | } 290 | 291 | deactivate(): void { 292 | this.pluginStore.removeFunction('Auth.authenticate'); //line 4 293 | } 294 | } 295 | 296 | export default AuthPlugin; 297 | ``` 298 | 299 | We have seen in the class AuthPlugin that the name of the plugin is used several times. An alternate way is to define a variable that stores the name of the plugin and use that variable in the class wherever we want the plugin name. 300 | 301 | AuthPlugin 302 | 303 | ```tsx 304 | import React from 'react'; 305 | import { IPlugin, PluginStore } from 'react-pluggable'; 306 | 307 | class AuthPlugin implements IPlugin { 308 | private namespace = 'Auth'; 309 | 310 | getPluginName(): string { 311 | return `${this.namespace}@1.0.0`; //line 2 312 | } 313 | 314 | getDependencies(): string[] { 315 | return []; 316 | } 317 | 318 | public pluginStore: any; 319 | 320 | init(pluginStore: PluginStore): void { 321 | this.pluginStore = pluginStore; 322 | } 323 | 324 | authenticate = (credentials: object) => { 325 | // checks the credentials and returns username if matches. 326 | return { name: 'username' }; 327 | }; 328 | 329 | activate(): void { 330 | this.pluginStore.addFunction( 331 | `${this.namespace}.authenticate`, //line 3 332 | (credentials: object) => this.authenticate(credentials) 333 | ); 334 | } 335 | 336 | deactivate(): void { 337 | this.pluginStore.removeFunction(`${this.namespace}.authenticate`); //line 4 338 | } 339 | } 340 | 341 | export default AuthPlugin; 342 | ``` 343 | 344 | ### 9) Contributors 345 | 346 | - [Aditya Jamuar](https://twitter.com/GeekJamuar) 347 | - [Himanshu Satija](https://twitter.com/HimanshuSatija_) 348 | - [Sanket Sahu](https://twitter.com/sanketsahu) 349 | - [Amar Somani](https://twitter.com/amar_somani) 350 | 351 | ### 10) How to Contribute 352 | 353 | Thank you for your interest in contributing to React Pluggable! Pull requests are welcome. Head over to [Contribution Guidelines](https://github.com/GeekyAnts/react-pluggable/blob/master/CONTRIBUTING.md) and learn how you can be a part of a wonderful, growing community. 354 | 355 | For major changes, please open an issue first to discuss changes and update tests as appropriate. 356 | 357 | ### 11) License 358 | 359 | Licensed under the MIT License, Copyright © 2020 GeekyAnts. See [LICENSE](https://github.com/GeekyAnts/react-pluggable/blob/master/LICENSE) for more information. 360 | --------------------------------------------------------------------------------