Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/cli/domain/get-mocked-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down Expand Up @@ -52,6 +54,7 @@ const registerStaticMocks = (
logger.ok(`├── ${pluginName} () => ${mockedValue}`);

return {
context: false,
name: pluginName,
register: {
register: defaultRegister,
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/cli/facade/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/oc-client/package.json
Original file line number Diff line number Diff line change
@@ -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 <matteofigus@gmail.com>",
"oc": {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion src/registry/domain/options-sanitiser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type CompileOptions = Omit<
>;

export interface RegistryOptions<T = any>
extends Partial<Omit<Config<T>, 'beforePublish' | 'discovery'>> {
extends Partial<Omit<Config<T>, 'beforePublish' | 'discovery' | 'plugins'>> {
/**
* Configuration object to enable/disable the HTML discovery page and the API
*
Expand Down
27 changes: 14 additions & 13 deletions src/registry/domain/plugins-initialiser.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -46,12 +50,8 @@ function checkDependencies(plugins: Plugin[]) {

let deferredLoads: Plugin[] = [];

type PluginFunctions = Record<string, (...args: unknown[]) => void>;

export async function init(
pluginsToRegister: unknown[]
): Promise<PluginFunctions> {
const registered: PluginFunctions = {};
export async function init(pluginsToRegister: unknown[]): Promise<Plugins> {
const registered: Plugins = {};

validatePlugins(pluginsToRegister);
checkDependencies(pluginsToRegister);
Expand All @@ -71,7 +71,7 @@ export async function init(
return present;
};

const loadPlugin = async (plugin: Plugin): Promise<void> => {
const loadPlugin = async (plugin: PluginWithCallback): Promise<void> => {
if (registered[plugin.name]) {
return;
}
Expand All @@ -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<PluginFunctions> => {
const terminator = async (): Promise<Plugins> => {
if (deferredLoads.length > 0) {
const deferredPlugins = [...deferredLoads];
deferredLoads = [];
Expand Down
4 changes: 2 additions & 2 deletions src/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function registry<T = any>(inputOptions: RegistryOptions<T>) {
};

const register = <T = any>(
plugin: Omit<Plugin<T>, 'callback'>,
plugin: Plugin<T>,
callback?: (...args: any[]) => void
) => {
plugins.push(Object.assign(plugin, { callback }));
Expand All @@ -48,10 +48,10 @@ export default function registry<T = any>(inputOptions: RegistryOptions<T>) {
) => 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);

Expand Down
58 changes: 56 additions & 2 deletions src/registry/routes/helpers/get-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, (...args: any[]) => 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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/registry/routes/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
);

Expand Down
4 changes: 2 additions & 2 deletions src/registry/views/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ ${indexJS}</script>`;
<Dependencies availableDependencies={vm.availableDependencies} />
<Plugins
availablePlugins={Object.fromEntries(
Object.entries(vm.availablePlugins).map(([name, fn]) => [
Object.entries(vm.availablePlugins).map(([name, { description }]) => [
name,
fn.toString() as string
description
])
)}
/>
Expand Down
1 change: 0 additions & 1 deletion src/registry/views/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ export default function Info(vm: Vm) {
class="w-100"
id="href"
placeholder="Insert component href here"
readonly
>
{componentHref}
</textarea>
Expand Down
74 changes: 69 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export interface VM {
version: string;
link: string;
}>;
availablePlugins: Record<string, (...args: unknown[]) => void>;
availablePlugins: Plugins;
components: ParsedComponent[];
componentsList: ComponentList[];
componentsReleases: number;
Expand Down Expand Up @@ -275,7 +275,7 @@ export interface Config<T = any> {
* Collection of plugins initialised for this registry instance.
* Populated via `registry.register(...)`.
*/
plugins: Record<string, (...args: unknown[]) => void>;
plugins: Plugins;
/**
* Seconds between each poll of the storage adapter for changes.
*
Expand Down Expand Up @@ -394,8 +394,7 @@ export interface Template {
render: (options: any, cb: (err: Error | null, data: string) => void) => void;
}

export interface Plugin<T = any> {
callback?: (error?: unknown) => void;
interface BasePLugin<T = any> {
description?: string;
name: string;
options?: T;
Expand All @@ -405,11 +404,76 @@ export interface Plugin<T = any> {
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<T = any> = BasePLugin<T> &
(
| {
/**
* 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 {
Expand Down
Loading