move extensions-api to libs
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import type { ManifestElement } from '../models';
|
||||
import { hasDefaultExport } from './has-default-export.function';
|
||||
import { isManifestElementNameType } from './is-manifest-element-name-type.function';
|
||||
import { loadExtension } from './load-extension.function';
|
||||
|
||||
export async function createExtensionElement(manifest: ManifestElement): Promise<HTMLElement | undefined> {
|
||||
|
||||
//TODO: Write tests for these extension options:
|
||||
const js = await loadExtension(manifest);
|
||||
|
||||
if (isManifestElementNameType(manifest)) {
|
||||
// created by manifest method providing HTMLElement
|
||||
return document.createElement(manifest.elementName);
|
||||
}
|
||||
|
||||
// TODO: Do we need this except for the default() loader?
|
||||
if (js) {
|
||||
if (hasDefaultExport(js)) {
|
||||
// created by default class
|
||||
return new js.default();
|
||||
}
|
||||
|
||||
console.error('-- Extension did not succeed creating an element, missing a default export of the served JavaScript file', manifest);
|
||||
|
||||
// If some JS was loaded and it did not at least contain a default export, then we are safe to assume that it executed as a module and does not need to be returned
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.error('-- Extension did not succeed creating an element, missing a default export or `elementName` in the manifest.', manifest);
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { HTMLElementConstructor } from '../models';
|
||||
|
||||
export function hasDefaultExport(object: unknown): object is { default: HTMLElementConstructor } {
|
||||
return typeof object === 'object' && object !== null && 'default' in object;
|
||||
}
|
||||
8
src/Umbraco.Web.UI.Client/libs/extensions-api/index.ts
Normal file
8
src/Umbraco.Web.UI.Client/libs/extensions-api/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './registry/extension.registry';
|
||||
export * from './create-extension-element.function';
|
||||
export * from './has-default-export.function';
|
||||
export * from './is-manifest-element-name-type.function';
|
||||
export * from './is-manifest-elementable-type.function';
|
||||
export * from './is-manifest-js-type.function';
|
||||
export * from './is-manifest-loader-type.function';
|
||||
export * from './load-extension.function';
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ManifestElement, ManifestElementWithElementName } from '../models';
|
||||
|
||||
export function isManifestElementNameType(manifest: unknown): manifest is ManifestElementWithElementName {
|
||||
return (
|
||||
typeof manifest === 'object' && manifest !== null && (manifest as ManifestElement).elementName !== undefined
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { isManifestElementNameType } from './is-manifest-element-name-type.function';
|
||||
import { isManifestJSType } from './is-manifest-js-type.function';
|
||||
import { isManifestLoaderType } from './is-manifest-loader-type.function';
|
||||
import type { ManifestElement, ManifestBase } from '@umbraco-cms/extensions-registry';
|
||||
|
||||
export function isManifestElementableType(manifest: ManifestBase): manifest is ManifestElement {
|
||||
return isManifestElementNameType(manifest) || isManifestLoaderType(manifest) || isManifestJSType(manifest);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ManifestJSType } from './load-extension.function';
|
||||
import type { ManifestBase } from '@umbraco-cms/extensions-registry';
|
||||
|
||||
export function isManifestJSType(manifest: ManifestBase): manifest is ManifestJSType {
|
||||
return (manifest as ManifestJSType).js !== undefined;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ManifestLoaderType } from './load-extension.function';
|
||||
import type { ManifestBase } from '@umbraco-cms/extensions-registry';
|
||||
|
||||
export function isManifestLoaderType(manifest: ManifestBase): manifest is ManifestLoaderType {
|
||||
return typeof (manifest as ManifestLoaderType).loader === 'function';
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ManifestElement } from '../models';
|
||||
import { isManifestJSType } from './is-manifest-js-type.function';
|
||||
import { isManifestLoaderType } from './is-manifest-loader-type.function';
|
||||
|
||||
export type ManifestLoaderType = ManifestElement & { loader: () => Promise<object | HTMLElement> };
|
||||
export type ManifestJSType = ManifestElement & { js: string };
|
||||
|
||||
export async function loadExtension(manifest: ManifestElement): Promise<object | HTMLElement | null> {
|
||||
try {
|
||||
if (isManifestLoaderType(manifest)) {
|
||||
return manifest.loader();
|
||||
}
|
||||
|
||||
if (isManifestJSType(manifest) && manifest.js) {
|
||||
return await import(/* @vite-ignore */ manifest.js);
|
||||
}
|
||||
} catch {
|
||||
console.warn('-- Extension failed to load script', manifest);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
16
src/Umbraco.Web.UI.Client/libs/extensions-api/package.json
Normal file
16
src/Umbraco.Web.UI.Client/libs/extensions-api/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@umbraco-cms/extensions-api",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"module": "index.js",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Umbraco HQ",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import type { ManifestTypes } from '../../models';
|
||||
import { UmbExtensionRegistry } from './extension.registry';
|
||||
|
||||
describe('UmbContextRequestEvent', () => {
|
||||
let extensionRegistry: UmbExtensionRegistry;
|
||||
let manifests: Array<ManifestTypes>;
|
||||
|
||||
beforeEach(() => {
|
||||
extensionRegistry = new UmbExtensionRegistry();
|
||||
manifests = [
|
||||
{
|
||||
type: 'section',
|
||||
name: 'test-section-1',
|
||||
alias: 'Umb.Test.Section.1',
|
||||
weight: 1,
|
||||
meta: {
|
||||
label: 'Test Section 1',
|
||||
pathname: 'test-section-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'test-section-2',
|
||||
alias: 'Umb.Test.Section.2',
|
||||
weight: 200,
|
||||
meta: {
|
||||
label: 'Test Section 2',
|
||||
pathname: 'test-section-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'test-section-3',
|
||||
alias: 'Umb.Test.Section.3',
|
||||
weight: 25,
|
||||
meta: {
|
||||
label: 'Test Section 3',
|
||||
pathname: 'test-section-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'workspace',
|
||||
name: 'test-editor-1',
|
||||
alias: 'Umb.Test.Editor.1',
|
||||
meta: {
|
||||
entityType: 'testEntity',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
manifests.forEach((manifest) => extensionRegistry.register(manifest));
|
||||
});
|
||||
|
||||
it('should register an extension', () => {
|
||||
const registeredExtensions = extensionRegistry['_extensions'].getValue();
|
||||
expect(registeredExtensions).to.have.lengthOf(4);
|
||||
expect(registeredExtensions?.[0]?.name).to.eq('test-section-1');
|
||||
});
|
||||
|
||||
it('should get an extension by alias', (done) => {
|
||||
const alias = 'Umb.Test.Section.1';
|
||||
extensionRegistry.getByAlias(alias).subscribe((extension) => {
|
||||
expect(extension?.alias).to.eq(alias);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByType', () => {
|
||||
const type = 'section';
|
||||
|
||||
it('should get all extensions by type', (done) => {
|
||||
extensionRegistry.extensionsOfType(type).subscribe((extensions) => {
|
||||
expect(extensions).to.have.lengthOf(3);
|
||||
expect(extensions?.[0]?.type).to.eq(type);
|
||||
expect(extensions?.[1]?.type).to.eq(type);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return extensions ordered by weight', (done) => {
|
||||
extensionRegistry.extensionsOfType(type).subscribe((extensions) => {
|
||||
expect(extensions?.[0]?.weight).to.eq(200);
|
||||
expect(extensions?.[1]?.weight).to.eq(25);
|
||||
expect(extensions?.[2]?.weight).to.eq(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { BehaviorSubject, map, Observable } from 'rxjs';
|
||||
import type { ManifestTypes, ManifestTypeMap, ManifestBase } from '../../models';
|
||||
import { hasDefaultExport } from '../has-default-export.function';
|
||||
import { loadExtension } from '../load-extension.function';
|
||||
|
||||
type SpecificManifestTypeOrManifestBase<T extends keyof ManifestTypeMap | string> = T extends keyof ManifestTypeMap
|
||||
? ManifestTypeMap[T]
|
||||
: ManifestBase;
|
||||
|
||||
export class UmbExtensionRegistry {
|
||||
|
||||
// TODO: Use UniqueBehaviorSubject, as we don't want someone to edit data of extensions.
|
||||
private _extensions = new BehaviorSubject<Array<ManifestBase>>([]);
|
||||
public readonly extensions = this._extensions.asObservable();
|
||||
|
||||
register(manifest: ManifestTypes & { loader?: () => Promise<object | HTMLElement> }): void {
|
||||
const extensionsValues = this._extensions.getValue();
|
||||
const extension = extensionsValues.find((extension) => extension.alias === manifest.alias);
|
||||
|
||||
if (extension) {
|
||||
console.error(`Extension with alias ${manifest.alias} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._extensions.next([...extensionsValues, manifest]);
|
||||
|
||||
// If entrypoint extension, we should load and run it immediately
|
||||
if (manifest.type === 'entrypoint') {
|
||||
loadExtension(manifest).then((js) => {
|
||||
if (hasDefaultExport(js)) {
|
||||
new js.default();
|
||||
} else {
|
||||
console.error(
|
||||
`Extension with alias '${manifest.alias}' of type 'entrypoint' must have a default export of its JavaScript module.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unregister(alias: string): void {
|
||||
const oldExtensionsValues = this._extensions.getValue();
|
||||
const newExtensionsValues = oldExtensionsValues.filter((extension) => extension.alias !== alias);
|
||||
|
||||
// TODO: Maybe its not needed to fire an console.error. as you might want to call this method without needing to check the existence first.
|
||||
if (oldExtensionsValues.length === newExtensionsValues.length) {
|
||||
console.error(`Unable to unregister extension with alias ${alias}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._extensions.next(newExtensionsValues);
|
||||
}
|
||||
|
||||
isRegistered(alias: string): boolean {
|
||||
const values = this._extensions.getValue();
|
||||
return values.some((ext) => ext.alias === alias);
|
||||
}
|
||||
|
||||
getByAlias(alias: string) {
|
||||
// TODO: make pipes prettier/simpler/reuseable
|
||||
return this.extensions.pipe(map((dataTypes) => dataTypes.find((extension) => extension.alias === alias) || null));
|
||||
}
|
||||
|
||||
getByTypeAndAlias<Key extends keyof ManifestTypeMap>(type: Key, alias: string) {
|
||||
return this.extensionsOfType(type).pipe(
|
||||
map((extensions) => extensions.find((extension) => extension.alias === alias) || null)
|
||||
);
|
||||
}
|
||||
|
||||
extensionsOfType<Key extends keyof ManifestTypeMap | string, T = SpecificManifestTypeOrManifestBase<Key>>(type: Key) {
|
||||
return this.extensions.pipe(
|
||||
map((exts) => exts.filter((ext) => ext.type === type).sort((a, b) => (b.weight || 0) - (a.weight || 0)))
|
||||
) as Observable<Array<T>>;
|
||||
}
|
||||
|
||||
extensionsOfTypes<ExtensionType = ManifestBase>(types: string[]): Observable<Array<ExtensionType>> {
|
||||
return this.extensions.pipe(
|
||||
map((exts) =>
|
||||
exts.filter((ext) => types.indexOf(ext.type) !== -1).sort((a, b) => (b.weight || 0) - (a.weight || 0))
|
||||
)
|
||||
) as Observable<Array<ExtensionType>>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import config from '../../utils/rollup.config.js';
|
||||
export default {
|
||||
...config,
|
||||
};
|
||||
Reference in New Issue
Block a user