diff --git a/package.json b/package.json index 0ac4a9ce..56e2f26c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oc", - "version": "0.50.18", + "version": "0.50.19", "description": "A framework for developing and distributing html components", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/cli/domain/get-mocked-plugins.ts b/src/cli/domain/get-mocked-plugins.ts index 728e2923..da8af191 100644 --- a/src/cli/domain/get-mocked-plugins.ts +++ b/src/cli/domain/get-mocked-plugins.ts @@ -9,9 +9,11 @@ import type { Logger } from '../logger'; interface MockedPlugin { register: (options: unknown, dependencies: unknown, next: () => void) => void; execute: (...args: unknown[]) => unknown; + context?: boolean; } interface PluginMock { + context: boolean; name: string; register: { register: ( @@ -52,6 +54,7 @@ const registerStaticMocks = ( logger.ok(`├── ${pluginName} () => ${mockedValue}`); return { + context: false, name: pluginName, register: { register: defaultRegister, @@ -83,12 +86,14 @@ const registerDynamicMocks = ( const register = (pluginMock as MockedPlugin).register || defaultRegister; const execute = (pluginMock as MockedPlugin).execute || pluginMock; + const context = (pluginMock as MockedPlugin).context || false; logger.ok(`├── ${pluginName} () => [Function]`); return { name: pluginName, - register: { execute, register } + register: { execute, register }, + context }; }) .filter((pluginMock): pluginMock is PluginMock => !!pluginMock); diff --git a/src/cli/facade/dev.ts b/src/cli/facade/dev.ts index 9f72c3ca..e8e50a4d 100644 --- a/src/cli/facade/dev.ts +++ b/src/cli/facade/dev.ts @@ -113,7 +113,7 @@ const dev = ({ local, logger }: { logger: Logger; local: Local }) => const registerPlugins = (registry: Registry) => { const mockedPlugins = getMockedPlugins(logger, componentsDir); for (const plugin of mockedPlugins) { - registry.register(plugin); + registry.register(plugin as any); } registry.on('request', (data) => { diff --git a/src/components/oc-client/package.json b/src/components/oc-client/package.json index 0722684b..e5ae6da0 100644 --- a/src/components/oc-client/package.json +++ b/src/components/oc-client/package.json @@ -1,7 +1,7 @@ { "name": "oc-client", "description": "The OpenComponents client-side javascript client", - "version": "0.50.18", + "version": "0.50.19", "repository": "https://github.com/opencomponents/oc/tree/master/components/oc-client", "author": "Matteo Figus ", "oc": { diff --git a/src/index.ts b/src/index.ts index fd40d3c2..c336bebf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ export { default as Client } from 'oc-client'; export { default as cli } from './cli/programmatic-api'; export { default as Registry, RegistryOptions } from './registry'; + +export type { Plugin, PluginContext } from './types'; diff --git a/src/registry/domain/options-sanitiser.ts b/src/registry/domain/options-sanitiser.ts index 7c8fcdcf..c486d79d 100644 --- a/src/registry/domain/options-sanitiser.ts +++ b/src/registry/domain/options-sanitiser.ts @@ -12,7 +12,7 @@ type CompileOptions = Omit< >; export interface RegistryOptions - extends Partial, 'beforePublish' | 'discovery'>> { + extends Partial, 'beforePublish' | 'discovery' | 'plugins'>> { /** * Configuration object to enable/disable the HTML discovery page and the API * diff --git a/src/registry/domain/plugins-initialiser.ts b/src/registry/domain/plugins-initialiser.ts index f4494629..18e78f9f 100644 --- a/src/registry/domain/plugins-initialiser.ts +++ b/src/registry/domain/plugins-initialiser.ts @@ -1,9 +1,13 @@ import { promisify } from 'node:util'; import { DepGraph } from 'dependency-graph'; import strings from '../../resources'; -import type { Plugin } from '../../types'; +import type { Plugin, Plugins } from '../../types'; import pLimit from '../../utils/pLimit'; +type PluginWithCallback = Plugin & { + callback?: (error?: unknown) => void; +}; + function validatePlugins(plugins: unknown[]): asserts plugins is Plugin[] { for (let idx = 0; idx < plugins.length; idx++) { const plugin = plugins[idx] as Plugin; @@ -46,12 +50,8 @@ function checkDependencies(plugins: Plugin[]) { let deferredLoads: Plugin[] = []; -type PluginFunctions = Record void>; - -export async function init( - pluginsToRegister: unknown[] -): Promise { - const registered: PluginFunctions = {}; +export async function init(pluginsToRegister: unknown[]): Promise { + const registered: Plugins = {}; validatePlugins(pluginsToRegister); checkDependencies(pluginsToRegister); @@ -71,7 +71,7 @@ export async function init( return present; }; - const loadPlugin = async (plugin: Plugin): Promise => { + const loadPlugin = async (plugin: PluginWithCallback): Promise => { if (registered[plugin.name]) { return; } @@ -98,14 +98,15 @@ export async function init( pluginCallback(err); throw err; }); - // Overriding toString so implementation details of plugins do not - // leak to OC consumers - plugin.register.execute.toString = () => plugin.description || ''; - registered[plugin.name] = plugin.register.execute; + registered[plugin.name] = { + handler: plugin.register.execute as any, + description: plugin.description || '', + context: plugin.context || false + }; pluginCallback(); }; - const terminator = async (): Promise => { + const terminator = async (): Promise => { if (deferredLoads.length > 0) { const deferredPlugins = [...deferredLoads]; deferredLoads = []; diff --git a/src/registry/index.ts b/src/registry/index.ts index f55d72fd..1e0e0af1 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -35,7 +35,7 @@ export default function registry(inputOptions: RegistryOptions) { }; const register = ( - plugin: Omit, 'callback'>, + plugin: Plugin, callback?: (...args: any[]) => void ) => { plugins.push(Object.assign(plugin, { callback })); @@ -48,10 +48,10 @@ export default function registry(inputOptions: RegistryOptions) { ) => void ) => { const ok = (msg: string) => console.log(colors.green(msg)); - createRouter(app, options, repository); try { options.plugins = await pluginsInitialiser.init(plugins); + createRouter(app, options, repository); const componentsInfo = await repository.init(); await appStart(repository, options); diff --git a/src/registry/routes/helpers/get-component.ts b/src/registry/routes/helpers/get-component.ts index fa9e30ae..1805fee9 100644 --- a/src/registry/routes/helpers/get-component.ts +++ b/src/registry/routes/helpers/get-component.ts @@ -9,7 +9,13 @@ import emptyResponseHandler from 'oc-empty-response-handler'; import { fromPromise } from 'universalify'; import strings from '../../../resources'; import settings from '../../../resources/settings'; -import type { Component, Config, Template } from '../../../types'; +import type { + Component, + Config, + PluginContext, + Plugins, + Template +} from '../../../types'; import isTemplateLegacy from '../../../utils/is-template-legacy'; import eventsHandler from '../../domain/events-handler'; import NestedRenderer from '../../domain/nested-renderer'; @@ -62,12 +68,57 @@ const noopConsole = Object.fromEntries( Object.keys(console).map((key) => [key, noop]) ); +/** + * Converts the plugins to a function that returns a record of plugins with the context applied + * Caches the plugins without context and applies the context to the plugins that need it + * to avoid creating a new function for each component if possible + * @param plugins - The plugins to convert + * @returns A function that returns a record of plugins with the context applied + */ +function pluginConverter(plugins: Plugins = {}) { + const pluginsMap = { + withoutContext: {} as Record any>, + withContext: {} as Record< + string, + (ctx: PluginContext) => (...args: any[]) => any + >, + needsContext: false + }; + for (const [name, { handler, context }] of Object.entries(plugins)) { + if (context) { + pluginsMap.withContext[name] = handler as any; + pluginsMap.needsContext = true; + } else { + pluginsMap.withoutContext[name] = handler; + } + } + + return (ctx: PluginContext) => { + if (!pluginsMap.needsContext) { + return pluginsMap.withoutContext; + } + const pluginsWithContextApplied = {} as Record< + string, + (...args: any[]) => any + >; + for (const [name, handler] of Object.entries(pluginsMap.withContext)) { + pluginsWithContextApplied[name] = handler(ctx); + } + + return { + ...pluginsMap.withoutContext, + ...pluginsWithContextApplied + }; + }; +} + export default function getComponent(conf: Config, repository: Repository) { const client = Client({ templates: conf.templates }); const cache = new Cache({ verbose: !!conf.verbosity, refreshInterval: conf.refreshInterval }); + const convertPlugins = pluginConverter(conf.plugins); const getEnv = async ( component: Component @@ -504,7 +555,10 @@ export default function getComponent(conf: Config, repository: Repository) { baseUrl: conf.baseUrl, env: { ...conf.env, ...env }, params, - plugins: conf.plugins, + plugins: convertPlugins({ + name: component.name, + version: component.version + }), renderComponent: fromPromise(nestedRenderer.renderComponent), renderComponents: fromPromise(nestedRenderer.renderComponents), requestHeaders: options.headers, diff --git a/src/registry/routes/plugins.ts b/src/registry/routes/plugins.ts index ff3bc8da..6eb9d4da 100644 --- a/src/registry/routes/plugins.ts +++ b/src/registry/routes/plugins.ts @@ -5,9 +5,9 @@ export default function plugins(conf: Config) { return (_req: Request, res: Response): void => { if (res.conf.discovery.ui) { const plugins = Object.entries(conf.plugins).map( - ([pluginName, pluginFn]) => ({ + ([pluginName, plugin]) => ({ name: pluginName, - description: pluginFn.toString() + description: plugin.description }) ); diff --git a/src/registry/views/index.tsx b/src/registry/views/index.tsx index 58c80a83..44aefe12 100644 --- a/src/registry/views/index.tsx +++ b/src/registry/views/index.tsx @@ -75,9 +75,9 @@ ${indexJS}`; [ + Object.entries(vm.availablePlugins).map(([name, { description }]) => [ name, - fn.toString() as string + description ]) )} /> diff --git a/src/registry/views/info.tsx b/src/registry/views/info.tsx index 5125e3ae..73856fbd 100644 --- a/src/registry/views/info.tsx +++ b/src/registry/views/info.tsx @@ -201,7 +201,6 @@ export default function Info(vm: Vm) { class="w-100" id="href" placeholder="Insert component href here" - readonly > {componentHref} diff --git a/src/types.ts b/src/types.ts index dc8db54e..561347de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,7 +112,7 @@ export interface VM { version: string; link: string; }>; - availablePlugins: Record void>; + availablePlugins: Plugins; components: ParsedComponent[]; componentsList: ComponentList[]; componentsReleases: number; @@ -275,7 +275,7 @@ export interface Config { * Collection of plugins initialised for this registry instance. * Populated via `registry.register(...)`. */ - plugins: Record void>; + plugins: Plugins; /** * Seconds between each poll of the storage adapter for changes. * @@ -394,8 +394,7 @@ export interface Template { render: (options: any, cb: (err: Error | null, data: string) => void) => void; } -export interface Plugin { - callback?: (error?: unknown) => void; +interface BasePLugin { description?: string; name: string; options?: T; @@ -405,11 +404,76 @@ export interface Plugin { dependencies: any, next: (error?: Error) => void ) => void; - execute: (...args: any[]) => any; dependencies?: string[]; }; } +/** + * The context object passed to the plugin's execute function + */ +export type PluginContext = { + /** + * The name of the component calling the plugin + */ + name: string; + /** + * The version of the component calling the plugin + */ + version: string; +}; + +export type Plugin = BasePLugin & + ( + | { + /** + * When false or undefined, the plugin's execute function will be called directly. + * The execute function should accept any parameters and return any value. + */ + context?: false | undefined; + register: { + register: ( + options: T, + dependencies: any, + next: (error?: Error) => void + ) => void; + execute: (...args: any[]) => any; + dependencies?: string[]; + }; + } + | { + /** + * When true, the plugin's execute function will be called with a context object containing + * the component name and version. It should return a function that accepts parameters and returns any value. + * @example + * ```ts + * { + * register: { + * execute: (...args: any[]) => any + * } + * } + * ``` + */ + context: true; + register: { + register: ( + options: T, + dependencies: any, + next: (error?: Error) => void + ) => void; + execute: (context: PluginContext) => (params: any) => any; + dependencies?: string[]; + }; + } + ); + +export interface Plugins { + [pluginName: string]: { + handler: (...args: unknown[]) => void; + description: string; + context: boolean; + }; +} + declare global { namespace Express { interface Request { diff --git a/test/unit/registry-domain-plugins-initialiser.js b/test/unit/registry-domain-plugins-initialiser.js index d1d98d75..aa725474 100644 --- a/test/unit/registry-domain-plugins-initialiser.js +++ b/test/unit/registry-domain-plugins-initialiser.js @@ -149,18 +149,18 @@ describe('registry : domain : plugins-initialiser', () => { }); it('should expose the functionalities using the plugin names', () => { - expect(result.getValue).to.be.a('function'); - expect(result.isFlagged).to.be.a('function'); + expect(result.getValue.handler).to.be.a('function'); + expect(result.isFlagged.handler).to.be.a('function'); }); it('should expose descriptions on the plugin functions if defined', () => { - expect(result.getValue.toString()).to.equal('Function description'); - expect(result.isFlagged.toString()).to.equal(''); + expect(result.getValue.description).to.equal('Function description'); + expect(result.isFlagged.description).to.equal(''); }); it('should be make the functionality usable', () => { - const a = result.getValue('a'); - const flagged = result.isFlagged(); + const a = result.getValue.handler('a'); + const flagged = result.isFlagged.handler(); expect(a).to.equal(123); expect(flagged).to.equal(true); @@ -203,7 +203,7 @@ describe('registry : domain : plugins-initialiser', () => { }); it('should provide the getValue register method with the required dependent plugins', () => { - expect(passedDeps.isFlagged()).to.eql(true); + expect(passedDeps.isFlagged.handler()).to.eql(true); }); }); @@ -331,7 +331,7 @@ describe('registry : domain : plugins-initialiser', () => { }); it('should defer the initalisation of the plugin until all dependencies have bee registered', () => { - expect(result.doSomething()).to.eql(true); + expect(result.doSomething.handler()).to.eql(true); }); }); }); diff --git a/test/unit/registry-routes-component.js b/test/unit/registry-routes-component.js index ee485973..86dc2b4b 100644 --- a/test/unit/registry-routes-component.js +++ b/test/unit/registry-routes-component.js @@ -133,7 +133,15 @@ describe('registry : routes : component', () => { describe('when getting a component with server.js execution errors', () => { before(() => { initialise(mockedComponents['error-component']); - componentRoute = ComponentRoute({}, mockedRepository); + const plugins = { + a: { + handler: () => '', + description: '' + } + }; + componentRoute = ComponentRoute({ + plugins + }, mockedRepository); componentRoute( { @@ -143,9 +151,7 @@ describe('registry : routes : component', () => { { conf: { baseUrl: 'http://components.com/', - plugins: { - a: () => '' - } + plugins, }, status: statusStub } @@ -323,27 +329,33 @@ describe('registry : routes : component', () => { }); describe('when plugin declared in package.json', () => { + const conf = { + baseUrl: 'http://components.com/', + plugins: { + doSomething: { + handler: () => 'hello hello hello my friend', + description: '' + } + } + }; beforeEach(() => { const component = { ...mockedComponents['plugin-component'] }; component.package.oc.plugins = ['doSomething']; initialise(component); - componentRoute = ComponentRoute({}, mockedRepository); + componentRoute = ComponentRoute(conf, mockedRepository); }); describe('when registry implements plugin', () => { beforeEach((done) => { + componentRoute( { headers: {}, + conf, params: { componentName: 'plugin-component' } }, { - conf: { - baseUrl: 'http://components.com/', - plugins: { - doSomething: () => 'hello hello hello my friend' - } - }, + conf, status: statusStub } ); diff --git a/test/unit/registry-routes-plugins.js b/test/unit/registry-routes-plugins.js index a7210bd2..3a37a30a 100644 --- a/test/unit/registry-routes-plugins.js +++ b/test/unit/registry-routes-plugins.js @@ -11,13 +11,15 @@ describe('registry : routes : plugins', () => { const initialise = () => { resJsonStub = sinon.stub(); statusStub = sinon.stub().returns({ json: resJsonStub }); - const plugin1 = () => {}; - plugin1.toString = () => 'Description plugin 1'; - const plugin2 = () => {}; - plugin2.toString = () => ''; plugins = { - plugin1, - plugin2 + plugin1: { + handler: () => {}, + description: 'Description plugin 1' + }, + plugin2: { + handler: () => {}, + description: '' + } }; };