Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"eslint-print-config": "yarn eslint --print-config",
"jest": "TZ=UTC jest --passWithNoTests",
"jest-print-config": "jest --showConfig",
"sample-app-install-cypress": "yarn workspace @monorepo/sample-app run cypress install",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a helper script to make test env. preparation easier when running in a container.

"sample-app-test-component": "yarn workspace @monorepo/sample-app run cypress run --component",
"sample-app-test-e2e": "yarn workspace @monorepo/sample-app run cypress run --e2e",
"api-extractor": "api-extractor run --typescript-compiler-folder ./node_modules/typescript/lib"
Expand Down
20 changes: 10 additions & 10 deletions packages/lib-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# Changelog for `@openshift/dynamic-plugin-sdk`

> Changes prefixed with [!] refer to API breaking changes.

## 6.0.0 - TODO

> TODO highlight major changes for this release here.

- Add support for optional dependencies ([#273])
- [!] Move plugin manifest extension post-processing to `PluginLoader.loadPlugin` ([#280])
- Add support for loading plugins from local manifests ([#281])
- BREAKING: Move plugin manifest extension post-processing to `PluginLoader.loadPlugin` ([#280])
- Improve code reference types and make them support optional chaining ([#274])

## 5.0.1 - 2024-01-15
Expand All @@ -20,7 +19,7 @@
> the `PluginStore`. Note that the `useResolvedExtensions` hook does not automatically disable
> plugins whose extensions have code reference resolution errors.

- [!] Rename `postProcessManifest` loader option to `transformPluginManifest` ([#236])
- BREAKING: Rename `postProcessManifest` loader option to `transformPluginManifest` ([#236])
- Support passing custom plugin loader implementation to `PluginStore` ([#232])
- Add `TestPluginStore` intended for React component testing purposes ([#232])
- Add options to `useResolvedExtensions` hook to customize its default behavior ([#241])
Expand All @@ -30,10 +29,10 @@
> This release removes the `PluginLoader` export. Pass the former `PluginLoader`
> options object as `loaderOptions` when creating the `PluginStore`.

- [!] Modify `PluginStore.loadPlugin` signature to accept plugin manifest ([#212])
- [!] Ensure `PluginStore.loadPlugin` returns the same Promise for pending plugins ([#212])
- [!] Treat `PluginLoader` as an implementation detail of `PluginStore` ([#212])
- [!] Replace `entryCallbackName` loader option with `entryCallbackSettings.name` ([#212])
- BREAKING: Modify `PluginStore.loadPlugin` signature to accept plugin manifest ([#212])
- BREAKING: Ensure `PluginStore.loadPlugin` returns the same Promise for pending plugins ([#212])
- BREAKING: Treat `PluginLoader` as an implementation detail of `PluginStore` ([#212])
- BREAKING: Replace `entryCallbackName` loader option with `entryCallbackSettings.name` ([#212])
- Add `entryCallbackSettings.autoRegisterCallback` loader option ([#212])
- Support tracking pending plugins via `PluginStore.getPluginInfo` ([#212])
- Provide access to raw plugin manifest in all `PluginInfoEntry` objects ([#212])
Expand All @@ -47,7 +46,7 @@
- Allow plugins to pass custom properties via plugin manifest ([#204])
- Add `sdkVersion` to `PluginStore` for better runtime diagnostics ([#200])
- Provide direct access to raw plugin manifest data ([#207])
- [!] Remove `PluginStore` option `postProcessExtensions` ([#207])
- BREAKING: Remove `PluginStore` option `postProcessExtensions` ([#207])
- Add technical compatibility with React 18 ([#208])

## 2.0.1 - 2023-01-27
Expand All @@ -63,7 +62,7 @@
- Allow reloading plugins which are already loaded ([#182])
- Allow providing custom manifest object in `PluginStore.loadPlugin` ([#182])
- Provide direct access to plugin modules via `PluginStore.getExposedModule` ([#180])
- [!] Fix `useResolvedExtensions` hook to reset result before restarting resolution ([#182])
- BREAKING: Fix `useResolvedExtensions` hook to reset result before restarting resolution ([#182])
- Ensure that all APIs referenced through the package index are exported ([#184])

## 1.0.0 - 2022-10-27
Expand All @@ -87,3 +86,4 @@
[#273]: https://github.com/openshift/dynamic-plugin-sdk/pull/273
[#274]: https://github.com/openshift/dynamic-plugin-sdk/pull/274
[#280]: https://github.com/openshift/dynamic-plugin-sdk/pull/280
[#281]: https://github.com/openshift/dynamic-plugin-sdk/pull/281
3 changes: 3 additions & 0 deletions packages/lib-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { useFeatureFlag, UseFeatureFlagResult } from './runtime/useFeatureFlag';

// Core utilities
export { applyCodeRefSymbol } from './runtime/coderefs';
export { isLocalPluginManifest, isStandardPluginManifest } from './runtime/plugin-manifest';

// Testing utilities
export { TestPluginStore } from './testing/TestPluginStore';
Expand All @@ -69,6 +70,8 @@ export {
PluginRegistrationMethod,
PluginRuntimeMetadata,
PluginManifest,
LocalPluginManifest,
AnyPluginManifest,
PendingPlugin,
LoadedPlugin,
FailedPlugin,
Expand Down
60 changes: 36 additions & 24 deletions packages/lib-core/src/runtime/PluginLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { DEFAULT_REMOTE_ENTRY_CALLBACK } from '../constants';
import type { LoadedExtension } from '../types/extension';
import type { ResourceFetch } from '../types/fetch';
import type { PluginLoadResult, PluginLoaderInterface } from '../types/loader';
import type { PluginManifest } from '../types/plugin';
import type { PluginManifest, AnyPluginManifest } from '../types/plugin';
import type { PluginEntryModule, PluginEntryCallback } from '../types/runtime';
import { basicFetch } from '../utils/basic-fetch';
import { settleAllPromises } from '../utils/promise';
import { injectScriptElement, getScriptElement } from '../utils/scripts';
import { resolveURL } from '../utils/url';
import { pluginManifestSchema } from '../yup-schemas';
import { decodeCodeRefs } from './coderefs';
import { isStandardPluginManifest } from './plugin-manifest';

declare global {
interface Window {
Expand All @@ -24,7 +25,7 @@ declare global {

type PluginLoadData = {
status: 'pending' | 'loaded' | 'failed';
manifest: PluginManifest;
manifest: AnyPluginManifest;
entryCallbackFired?: boolean;
entryCallbackModule?: PluginEntryModule;
};
Expand All @@ -46,7 +47,7 @@ export type PluginLoaderOptions = Partial<{
*
* By default, all plugins are allowed to be loaded and reloaded.
*/
canLoadPlugin: (manifest: PluginManifest, reload: boolean) => boolean;
canLoadPlugin: (manifest: AnyPluginManifest, reload: boolean) => boolean;

/**
* Control whether the given plugin script can be reloaded when attempting to reload
Expand Down Expand Up @@ -121,7 +122,7 @@ export type PluginLoaderOptions = Partial<{
*
* By default, no transformation is performed on the manifest.
*/
transformPluginManifest: (manifest: PluginManifest) => PluginManifest;
transformPluginManifest: <T extends AnyPluginManifest>(manifest: T) => T;

/**
* Provide access to the plugin's entry module.
Expand Down Expand Up @@ -174,10 +175,10 @@ export class PluginLoader implements PluginLoaderInterface {

pluginManifestSchema.validateSync(manifest, { strict: true, abortEarly: false });

return manifest;
return manifest as PluginManifest;
}

transformPluginManifest(manifest: PluginManifest) {
transformPluginManifest<T extends AnyPluginManifest>(manifest: T) {
return this.options.transformPluginManifest(manifest);
}

Expand All @@ -191,15 +192,17 @@ export class PluginLoader implements PluginLoaderInterface {
* For plugins using the `custom` registration method, the `getPluginEntryModule` function
* is expected to return the entry module of the given plugin. If not implemented properly,
* plugins using the `custom` registration method will fail to load.
*
* For plugins loaded from a local plugin manifest, the `entryModule` will be `undefined`.
*/
async loadPlugin(manifest: PluginManifest): Promise<PluginLoadResult> {
async loadPlugin(manifest: AnyPluginManifest): Promise<PluginLoadResult> {
const pluginName = manifest.name;
const reload = this.plugins.has(pluginName);

const data: PluginLoadData = { status: 'pending', manifest };
let entryModule: PluginEntryModule;
let entryModule: PluginEntryModule | undefined;

if (manifest.registrationMethod === 'callback') {
if (isStandardPluginManifest(manifest) && manifest.registrationMethod === 'callback') {
data.entryCallbackFired = false;
}

Expand Down Expand Up @@ -229,7 +232,9 @@ export class PluginLoader implements PluginLoaderInterface {
}

try {
await this.loadPluginScripts(manifest, data);
if (isStandardPluginManifest(manifest)) {
await this.loadPluginScripts(manifest, data);
}
} catch (e) {
data.status = 'failed';
this.invokeLoadListeners();
Expand All @@ -242,7 +247,9 @@ export class PluginLoader implements PluginLoaderInterface {
}

try {
entryModule = await this.initSharedModules(manifest, data);
if (isStandardPluginManifest(manifest)) {
entryModule = await this.initSharedModules(manifest, data);
}
} catch (e) {
data.status = 'failed';
this.invokeLoadListeners();
Expand All @@ -254,21 +261,25 @@ export class PluginLoader implements PluginLoaderInterface {
};
}

const loadedExtensions = cloneDeep(manifest.extensions).map<LoadedExtension>((e, index) =>
decodeCodeRefs(
{
...e,
pluginName,
uid: `${pluginName}[${index}]_${manifest.buildHash ?? uuidv4()}`,
},
entryModule,
),
);
const pluginBuildHash = isStandardPluginManifest(manifest)
? manifest.buildHash ?? uuidv4()
: uuidv4();

let loadedExtensions = cloneDeep(manifest.extensions).map<LoadedExtension>((e, index) => ({
...e,
pluginName,
uid: `${pluginName}[${index}]_${pluginBuildHash}`,
}));

if (isStandardPluginManifest(manifest)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loadedExtensions = loadedExtensions.map((e) => decodeCodeRefs(e, entryModule!));
}

data.status = 'loaded';
this.invokeLoadListeners();

return { success: true, entryModule, loadedExtensions };
return { success: true, loadedExtensions, entryModule };
}

/**
Expand Down Expand Up @@ -361,7 +372,7 @@ export class PluginLoader implements PluginLoaderInterface {
*
* Fail early if there are any unsuccessful or unmet dependency resolutions.
*/
private resolvePluginDependencies(manifest: PluginManifest) {
private resolvePluginDependencies(manifest: AnyPluginManifest) {
return new Promise<void>((resolve, reject) => {
const pluginName = manifest.name;
const requiredDependencies = manifest.dependencies ?? {};
Expand Down Expand Up @@ -458,7 +469,6 @@ export class PluginLoader implements PluginLoaderInterface {
const callbackName = this.options.entryCallbackSettings.name ?? DEFAULT_REMOTE_ENTRY_CALLBACK;

if (!registerCallback) {
consoleLogger.info(`Plugin entry callback ${callbackName} will not be registered`);
return;
}

Expand All @@ -468,5 +478,7 @@ export class PluginLoader implements PluginLoaderInterface {
}

window[callbackName] = this.createPluginEntryCallback();

consoleLogger.info(`Plugin entry callback ${callbackName} has been registered`);
}
}
43 changes: 29 additions & 14 deletions packages/lib-core/src/runtime/PluginStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { compact, isEqual, noop, pickBy } from 'lodash';
import { version as sdkVersion } from '../../package.json';
import type { LoadedExtension } from '../types/extension';
import type { PluginLoaderInterface } from '../types/loader';
import type { PluginManifest, PendingPlugin, LoadedPlugin, FailedPlugin } from '../types/plugin';
import type { AnyPluginManifest, PendingPlugin, LoadedPlugin, FailedPlugin } from '../types/plugin';
import type { PluginEntryModule } from '../types/runtime';
import type { PluginInfoEntry, PluginStoreInterface, FeatureFlags } from '../types/store';
import { PluginEventType } from '../types/store';
Expand Down Expand Up @@ -160,12 +160,15 @@ export class PluginStore implements PluginStoreInterface {
}
}

async loadPlugin(manifest: PluginManifest | string, forceReload?: boolean) {
let loadedManifest: PluginManifest;
async loadPlugin(manifest: AnyPluginManifest | string, forceReload?: boolean) {
let loadedManifest: AnyPluginManifest;

try {
loadedManifest =
typeof manifest === 'string' ? await this.loader.loadPluginManifest(manifest) : manifest;
if (typeof manifest === 'string') {
loadedManifest = await this.loader.loadPluginManifest(manifest);
} else {
loadedManifest = manifest;
}
} catch (e) {
throw new ErrorWithCause('Failed to load plugin manifest', e);
}
Expand All @@ -190,9 +193,9 @@ export class PluginStore implements PluginStoreInterface {
const result = await this.loader.loadPlugin(loadedManifest);

if (result.success) {
const { entryModule, loadedExtensions } = result;
const { loadedExtensions, entryModule } = result;

this.addLoadedPlugin(loadedManifest, entryModule, loadedExtensions);
this.addLoadedPlugin(loadedManifest, loadedExtensions, entryModule);

consoleLogger.info(`Plugin ${pluginName} has been loaded`);

Expand Down Expand Up @@ -288,10 +291,11 @@ export class PluginStore implements PluginStoreInterface {
}
}

protected addPendingPlugin(manifest: PluginManifest) {
protected addPendingPlugin(manifest: AnyPluginManifest) {
const pluginName = manifest.name;
const pendingPlugin: PendingPlugin = { manifest };

this.pendingPlugins.set(pluginName, { manifest });
this.pendingPlugins.set(pluginName, pendingPlugin);
this.loadedPlugins.delete(pluginName);
this.failedPlugins.delete(pluginName);

Expand All @@ -300,14 +304,14 @@ export class PluginStore implements PluginStoreInterface {
}

/**
* Add a loaded plugin to the {@link PluginStore}.
* @remarks
*
* Once added, the plugin is disabled by default. Enable it to put its extensions into use.
*/
protected addLoadedPlugin(
manifest: PluginManifest,
entryModule: PluginEntryModule,
manifest: AnyPluginManifest,
loadedExtensions: LoadedExtension[],
entryModule?: PluginEntryModule,
) {
const pluginName = manifest.name;

Expand All @@ -327,12 +331,17 @@ export class PluginStore implements PluginStoreInterface {
this.updateExtensions();
}

protected addFailedPlugin(manifest: PluginManifest, errorMessage: string, errorCause?: unknown) {
protected addFailedPlugin(
manifest: AnyPluginManifest,
errorMessage: string,
errorCause?: unknown,
) {
const pluginName = manifest.name;
const failedPlugin: FailedPlugin = { manifest, errorMessage, errorCause };

this.pendingPlugins.delete(pluginName);
this.loadedPlugins.delete(pluginName);
this.failedPlugins.set(pluginName, { manifest, errorMessage, errorCause });
this.failedPlugins.set(pluginName, failedPlugin);

this.invokeListeners(PluginEventType.PluginInfoChanged);
this.updateExtensions();
Expand All @@ -348,6 +357,12 @@ export class PluginStore implements PluginStoreInterface {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const plugin = this.loadedPlugins.get(pluginName)!;

if (!plugin.entryModule) {
throw new Error(
`Attempt to get module '${moduleName}' of plugin ${pluginName} which has no entry module`,
);
}

const referencedModule = await getPluginModule<TModule>(
moduleName,
plugin.entryModule,
Expand Down
8 changes: 8 additions & 0 deletions packages/lib-core/src/runtime/plugin-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PluginManifest, LocalPluginManifest, AnyPluginManifest } from '../types/plugin';

export const isLocalPluginManifest = (
manifest: AnyPluginManifest,
): manifest is LocalPluginManifest => (manifest as LocalPluginManifest).$local === true;

export const isStandardPluginManifest = (manifest: AnyPluginManifest): manifest is PluginManifest =>
!isLocalPluginManifest(manifest);
19 changes: 6 additions & 13 deletions packages/lib-core/src/testing/TestPluginStore.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import { PluginStore } from '../runtime/PluginStore';
import type { LoadedExtension } from '../types/extension';
import type { PluginManifest } from '../types/plugin';
import type { PluginEntryModule } from '../types/runtime';

/**
* `PluginStore` implementation intended for testing purposes.
*/
export class TestPluginStore extends PluginStore {
// Override to change access to public
override addPendingPlugin(manifest: PluginManifest) {
super.addPendingPlugin(manifest);
override addPendingPlugin(...args: Parameters<PluginStore['addPendingPlugin']>) {
super.addPendingPlugin(...args);
}

// Override to change access to public
override addLoadedPlugin(
manifest: PluginManifest,
entryModule: PluginEntryModule,
loadedExtensions: LoadedExtension[],
) {
super.addLoadedPlugin(manifest, entryModule, loadedExtensions);
override addLoadedPlugin(...args: Parameters<PluginStore['addLoadedPlugin']>) {
super.addLoadedPlugin(...args);
}

// Override to change access to public
override addFailedPlugin(manifest: PluginManifest, errorMessage: string, errorCause?: unknown) {
super.addFailedPlugin(manifest, errorMessage, errorCause);
override addFailedPlugin(...args: Parameters<PluginStore['addFailedPlugin']>) {
super.addFailedPlugin(...args);
}
}
Loading