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
11 changes: 8 additions & 3 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
scopeconfig:
- scope: node
fileignoreconfig:
- filename: .github/workflows/secrets-scan.yml
ignore_detectors:
- filecontent
- filename: package-lock.json
ignore_detectors:
- filecontent
- filename: .github/workflows/secrets-scan.yml
ignore_detectors:
- filecontent
version: "1.0"
2 changes: 1 addition & 1 deletion __test__/uiLocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ describe("UI Location", () => {
const config = await uiLocation.getConfig();
expect(config).toEqual({});
expect(postRobotSendToParentMock).toHaveBeenLastCalledWith(
"getConfig"
"getConfig", {"context": {"extensionUID": "extension_uid", "installationUID": "installation_uid"}}
);
});
});
Expand Down
177 changes: 108 additions & 69 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/app-sdk",
"version": "2.3.2",
"version": "2.3.3",
"types": "dist/src/index.d.ts",
"description": "The Contentstack App SDK allows you to customize your Contentstack applications.",
"main": "dist/index.js",
Expand Down Expand Up @@ -62,6 +62,7 @@
"jsonfile": "^6.1.0",
"loader-utils": "^3.2.1",
"post-robot": "^8.0.31",
"rxjs": "^7.8.1",
"ssri": "^12.0.0",
"wolfy87-eventemitter": "^5.2.9"
},
Expand Down
6 changes: 4 additions & 2 deletions src/RTE/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "slate";

import { RTEPlugin } from "./index";
import UiLocation from "../uiLocation";

declare interface TransformOptions {
at?: Location;
Expand Down Expand Up @@ -48,7 +49,7 @@ export declare interface IRteParam {
voids?: boolean;
}
) => Point | undefined;

sdk: UiLocation;
isPointEqual: (point: Point, another: Point) => boolean;
};

Expand Down Expand Up @@ -140,6 +141,7 @@ export declare interface IRteParam {
getVariable: <T = unknown>(name: string, defaultValue: any) => T;
setVariable: <T = unknown>(name: string, value: T) => void;
getConfig: <T>() => { [key: string]: T };
sdk: UiLocation;
}

export declare type IRteParamWithPreventDefault = {
Expand Down Expand Up @@ -199,7 +201,7 @@ export declare interface IRteElementType {
children: Array<IRteElementType | IRteTextType>;
}

type IDynamicFunction = (
export type IDynamicFunction = (
element: IRteElementType
) =>
| Exclude<IElementTypeOptions, "text">
Expand Down
62 changes: 60 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import postRobot from "post-robot";

import { InitializationData } from "./types";
import { IRteParam } from "./RTE/types";
import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin";
import UiLocation from "./uiLocation";
import { version } from "../package.json";
import { InitializationData } from "./types";

postRobot.CONFIG.LOG_LEVEL = "error";

Expand Down Expand Up @@ -43,6 +45,55 @@ class ContentstackAppSDK {
.catch((e: Error) => Promise.reject(e));
}

/**
* Registers RTE plugins with the Contentstack platform.
* This method is the primary entry point for defining and registering custom RTE plugins
* built using the PluginBuilder pattern. It returns a function that the Contentstack
* platform will invoke at runtime, providing the necessary context.
*
* @example
* // In your plugin's entry file (e.g., src/index.ts):
* import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk';
*
* const MyCustomPlugin = new PluginBuilder("my-plugin-id")
* .title("My Plugin")
* .icon(<MyIconComponent />)
* .elementType("block")
* .display("toolbar")
* .render(()=>{return <Comment />})
* .on("exec", (rte: IRteParam) => {
* // Access SDK via rte.sdk if needed:
* const sdk = rte.sdk;
* // ... plugin execution logic ...
* })
* .build();
*
* export default ContentstackAppSDK.registerRTEPlugins(
* MyCustomPlugin
* );
*
* @param {...PluginDefinition} pluginDefinitions - One or more plugin definitions created using the `PluginBuilder`.
* Each `PluginDefinition` describes the plugin's configuration, callbacks, and any child plugins.
* @returns {Promise<{ __isPluginBuilder__: boolean; version: string; plugins: (context: RTEContext, rte: IRteParam) => Promise<{ [key: string]: RTEPlugin; }>; }>}
* A Promise that resolves to an object containing:
* - `__isPluginBuilder__`: A boolean flag indicating this is a builder-based plugin export.
* - `version`: The version of the SDK that registered the plugins.
* - `plugins`: An asynchronous function. This function is designed to be invoked by the
* Contentstack platform loader, providing the `context` (initialization data) and
* the `rte` instance. When called, it materializes and returns a map of the
* registered `RTEPlugin` instances, keyed by their IDs.
*/

static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) {
return {
__isPluginBuilder__: true,
version,
plugins: (context: InitializationData, rte: IRteParam) => {
return registerPlugins(...pluginDefinitions)(context, rte);
}
};
}

/**
* Version of Contentstack App SDK.
*/
Expand All @@ -52,4 +103,11 @@ class ContentstackAppSDK {
}

export default ContentstackAppSDK;
module.exports = ContentstackAppSDK;
export { PluginBuilder };

// CommonJS compatibility
if (typeof module !== 'undefined' && module.exports) {
module.exports = ContentstackAppSDK;
module.exports.default = ContentstackAppSDK;
module.exports.PluginBuilder = PluginBuilder;
}
171 changes: 171 additions & 0 deletions src/rtePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE";
import {
IConfig,
IDisplayOnOptions,
IDynamicFunction,
IElementTypeOptions,
IOnFunction,
IRteElementType,
IRteParam,
} from "./RTE/types";
import { InitializationData } from "./types";
import UiLocation from "./uiLocation";

type PluginConfigCallback = (sdk: UiLocation) => Promise<IConfig> | IConfig;

interface PluginDefinition {
id: string;
config: Partial<IConfig>;
callbacks: Partial<IOnFunction>;
asyncConfigCallback?: PluginConfigCallback;
childBuilders: PluginBuilder[];
}

class PluginBuilder {
private id: string;
private _config: Partial<IConfig> = {};
private _callbacks: Partial<IOnFunction> = {};
private _asyncConfigCallback?: PluginConfigCallback;
private _childBuilders: PluginBuilder[] = [];

constructor(id: string) {
this.id = id;
this._config.title = id;
}

title(title: string): PluginBuilder {
this._config.title = title;
return this;
}
icon(icon: React.ReactElement | null): PluginBuilder {
this._config.icon = icon;
return this;
}
display(display: IDisplayOnOptions | IDisplayOnOptions[]): PluginBuilder {
this._config.display = display;
return this;
}
elementType(
elementType:
| IElementTypeOptions
| IElementTypeOptions[]
| IDynamicFunction
): PluginBuilder {
this._config.elementType = elementType;
return this;
}
render(renderFn: (element: React.ReactElement, attrs: { [key: string]: any }, path: number[], rte: IRteParam) => React.ReactElement): PluginBuilder {
this._config.render = renderFn;
return this;
}
shouldOverride(
shouldOverrideFn: (element: IRteElementType) => boolean
): PluginBuilder {
this._config.shouldOverride = shouldOverrideFn;
return this;
}
on<T extends keyof IOnFunction>(
type: T,
callback: IOnFunction[T]
): PluginBuilder {
this._callbacks[type] = callback;
return this;
}
configure(callback: PluginConfigCallback): PluginBuilder {
this._asyncConfigCallback = callback;
return this;
}
addPlugins(...builders: PluginBuilder[]): PluginBuilder {
this._childBuilders.push(...builders);
return this;
}

/**
* Builds and returns a definition of the RTE Plugin, ready to be materialized
* into a concrete RTEPlugin instance later when the SDK and Plugin Factory are available.
* This method no longer performs the actual creation of RTEPlugin instances.
*/
build(): PluginDefinition {
return {
id: this.id,
config: this._config,
callbacks: this._callbacks,
asyncConfigCallback: this._asyncConfigCallback,
childBuilders: this._childBuilders,
};
}
}

async function materializePlugin(
pluginDef: PluginDefinition,
sdk: UiLocation
): Promise<Plugin> {
let finalConfig: Partial<IConfig> = { ...pluginDef.config };
if (pluginDef.asyncConfigCallback) {
const dynamicConfig = await Promise.resolve(
pluginDef.asyncConfigCallback(sdk)
);
finalConfig = { ...finalConfig, ...dynamicConfig };
}

const plugin = rtePluginInitializer(
pluginDef.id,
(rte: IRteParam | void) => {
return finalConfig;
}
);

Object.entries(pluginDef.callbacks).forEach(([type, callback]) => {
plugin.on(type as keyof IOnFunction, callback);
});

if (pluginDef.childBuilders.length > 0) {
const childPlugins = await Promise.all(
pluginDef.childBuilders.map((childBuilder) =>
materializePlugin(childBuilder.build(), sdk)
)
);
plugin.addPlugins(...childPlugins);
}

return plugin;
}

function registerPlugins(
...pluginDefinitions: PluginDefinition[]
): (
context: InitializationData,
rte: IRteParam
) => Promise<{ [key: string]: Plugin }> {
const definitionsToProcess = [...pluginDefinitions];
const plugins = async (context: InitializationData, rte: IRteParam) => {
try {
const sdk = new UiLocation(context);
const materializedPlugins: { [key: string]: Plugin } = {};
for (const def of definitionsToProcess) {
const pluginInstance = await materializePlugin(def, sdk);
materializedPlugins[def.id] = pluginInstance;
}
rte.sdk = sdk;
return materializedPlugins;
} catch (err) {
console.error("Error during plugin registration:", err);
throw err;
}
};
return plugins;
}

export {
IConfig,
IDisplayOnOptions,
IDynamicFunction,
IElementTypeOptions,
IOnFunction,
IRteElementType,
IRteParam,
Plugin,
PluginBuilder,
PluginDefinition,
registerPlugins
};
6 changes: 3 additions & 3 deletions src/uiLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ class UiLocation {
return Promise.resolve(this.config);
}
return this.postRobot
.sendToParent("getConfig")
.sendToParent("getConfig", {context:{installationUID:this.installationUID, extensionUID:this.locationUID}})
.then(onData)
.catch(onError);
};
Expand Down Expand Up @@ -485,7 +485,7 @@ class UiLocation {

api = (url: string, option?: RequestInit): Promise<Response> =>
dispatchApiRequest(url, option) as Promise<Response>;

/**
* Method used to create an adapter for management sdk.
*/
Expand Down Expand Up @@ -519,4 +519,4 @@ class UiLocation {
}
}

export default UiLocation;
export default UiLocation;
Loading
Loading