move extensions-api to libs

This commit is contained in:
Jacob Overgaard
2023-01-23 14:56:21 +01:00
parent 7ae95ff7ae
commit 45252017f1
14 changed files with 25 additions and 5 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View 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';

View File

@@ -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
);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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';
}

View File

@@ -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);
}

View 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"
}

View File

@@ -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();
});
});
});
});

View File

@@ -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>>;
}
}

View File

@@ -0,0 +1,4 @@
import config from '../../utils/rollup.config.js';
export default {
...config,
};