Skip to content

Commit

Permalink
Move Kompakkt/Extender to this repository (#1)
Browse files Browse the repository at this point in the history
* Create extender lib to later integrate Kompakkt/Extender in this workspace

* Copy over extender library from Kompakkt/Extender

* Remove temporary generic testing from extender

* Add extender to lerna configuration
  • Loading branch information
HeyItsBATMAN authored Mar 27, 2024
1 parent d787f45 commit 08d0398
Show file tree
Hide file tree
Showing 21 changed files with 1,208 additions and 17 deletions.
40 changes: 40 additions & 0 deletions extender/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"extends": ["../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "lib",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "lib",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nx/angular-template"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": "error"
}
}
]
}
661 changes: 661 additions & 0 deletions extender/LICENSE

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions extender/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Kompakkt.Extender

<p align="center">
<img src="https://github.com/Kompakkt/Assets/raw/main/extender-logo.png" alt="Kompakkt ExtenderLogo" width="600">
</p>

Extension system for modularizing instances of Kompakkt.

## How to use Extender in Kompakkt Viewer or Repo

Use the `provideExtender`-method in the `providers` array of your ApplicationConfig (`app.config.ts`):

```ts
import { ApplicationConfig } from '@angular/core';
import { HelloWorldPlugin, provideExtender } from '@kompakkt/extender';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
provideExtender({
// Array of Extender Plugins
plugins: [new HelloWorldPlugin()],
// Name of the ComponentSet, either 'viewerComponents' or 'repoComponents'
componentSet: 'repoComponents',
}),
],
};
```

## Creating a basic plugin and how to consume it

### Plugin creating using factory pattern

The Extender library provides a factory for creating plugin classes.
The plugin requires some basic metadata, and can optionally receive components for the Repo and the Viewer, as well as a token which can be consumed as a `ProviderToken`, allowing to inject the plugin instance anywhere in the application.

```ts
import { createExtenderPlugin } from '../plugin-factory';
import { HelloWorldComponent } from './hello-world.component';

// Create a plugin class extending the factory class
export class HelloWorldPlugin extends createExtenderPlugin({
// Required
name: 'Hello World',
description: 'Hello World plugin',
version: '1.0.0',
// Optional
tokenName: 'HelloWorldPlugin',
viewerComponents: {
'hello-world': [HelloWorldComponent],
},
repoComponents: {
'hello-world': [HelloWorldComponent],
},
}) {
// Custom logic
}
```

### Consume plugin components using slots and ExtenderSlotDirective

Notice the `slot` names like `hello-world` in the plugin definition?

Using these slots, we can inject the components of the plugin anywhere in Kompakkt, by using the `ExtenderSlotDirective`.
Simply import the `ExtenderSlotDirective`, add it to a components' imports and use it on any HTML-tag in the template with the slot name you wish to use.

```ts
import { ExtenderSlotDirective } from '@kompakkt/extender';

@Component({
/* ... */
imports: [ExtenderSlotDirective]
})
```

```html
<div extendSlot="hello-world"></div>
```

### Passing data to components

Each element with the `ExtenderSlotDirective` also has an input called `slotData`.
Using this input, we can pass any data to our components.

```html
<div extendSlot="hello-world" [slotData]="{ name: 'World' }" ]></div>
```

When creating a component, you can access the `slotData` and use type guards to validate it.

```ts
@Component({
selector: 'lib-hello-world',
standalone: true,
template: '<p>Hello {{ name() }}</p>',
styles: '',
})
export class HelloWorldComponent extends createExtenderComponent() {
name = computed(() => {
const slotData = this.slotData();

const isHelloWorldData = (obj: unknown): obj is { name: string } => {
return typeof obj === 'object' && obj !== null && 'name' in obj;
};

return isHelloWorldData(slotData) ? slotData.name : 'World';
});
}
```

### Injecting plugin using the plugin token

If a token was given to the factory, the plugin can be injected anywhere in the application.

```ts
import { Component, inject } from '@angular/core';
import { HelloWorldPlugin } from '@kompakkt/extender';

@Component({
/*...*/
})
class ExampleComponent {
pluginReference = inject<HelloWorldPlugin>(HelloWorldPlugin.providerToken);
}
```
7 changes: 7 additions & 0 deletions extender/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../dist/extender",
"lib": {
"entryFile": "src/index.ts"
}
}
17 changes: 17 additions & 0 deletions extender/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@kompakkt/extender",
"license": "AGPL-3.0-only",
"repository": {
"type": "git",
"url": "https://github.com/Kompakkt/Extender.git"
},
"version": "0.0.9",
"peerDependencies": {
"@angular/common": "^17.3.0",
"@angular/core": "^17.3.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}
29 changes: 29 additions & 0 deletions extender/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "extender",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "extender/src",
"prefix": "lib",
"tags": [],
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/angular:package",
"outputs": ["{workspaceRoot}/dist/{projectRoot}"],
"options": {
"project": "extender/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "extender/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "extender/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
8 changes: 8 additions & 0 deletions extender/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
ExtenderOptions,
PLUGIN_COMPONENT_SET,
PLUGIN_MANAGER,
provideExtender,
} from './lib/extender';
export { createExtenderComponent, createExtenderPlugin } from './lib/factory';
export { ExtenderSlotDirective } from './lib/slot.directive';
45 changes: 45 additions & 0 deletions extender/src/lib/extender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
EnvironmentProviders,
InjectionToken,
Type,
makeEnvironmentProviders,
} from '@angular/core';
import { ExtenderPluginManager } from './manager';
import { ExtenderProviderPlugin } from './provider';

export type ExtenderOptions<T> = {
componentSet: 'viewerComponents' | 'repoComponents';
plugins: ExtenderProviderPlugin[];
services: Record<string, Type<T>>;
};

export const PLUGIN_MANAGER = new InjectionToken<ExtenderPluginManager<unknown>>(
'KOMPAKKT_EXTENDER_PLUGIN_MANAGER',
);

export const PLUGIN_COMPONENT_SET = new InjectionToken<ExtenderOptions<unknown>['componentSet']>(
'KOMPAKKT_EXTENDER_PLUGIN_COMPONENT_SET',
);

export const provideExtender = <T>({
componentSet,
plugins,
services,
}: ExtenderOptions<T>): EnvironmentProviders => {
return makeEnvironmentProviders([
{
provide: PLUGIN_MANAGER,
useFactory: () => new ExtenderPluginManager<T>(plugins, services),
},
{
provide: PLUGIN_COMPONENT_SET,
useValue: componentSet,
},
...plugins
.filter(p => !!(p.constructor as any)?.providerToken)
.map(p => {
const provide = (p.constructor as any).providerToken;
return { provide, useValue: p };
}),
]);
};
44 changes: 44 additions & 0 deletions extender/src/lib/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Directive, InjectionToken, Type, input, output } from '@angular/core';
import { ExtenderPluginManager } from './manager';
import { ExtenderAddonProviderPlugin } from './provider';

@Directive()
class ExtenderAddonProviderPluginBase {}

export const createExtenderPlugin = (options: {
name: string;
description: string;
version: `${number}.${number}.${number}`;
viewerComponents?: Record<string, Type<ExtenderPluginBaseComponent>[]>;
repoComponents?: Record<string, Type<ExtenderPluginBaseComponent>[]>;
tokenName?: string;
}) => {
const providerToken = options.tokenName
? new InjectionToken<ExtenderAddonProviderPlugin>(
`KOMPAKKT_EXTENDER_PLUGIN_${options.tokenName}`,
)
: undefined;

return class extends ExtenderAddonProviderPluginBase implements ExtenderAddonProviderPlugin {
readonly type = 'addon-provider' as const;
readonly tokenName = options.tokenName;
readonly name = options.name;
readonly description = options.description;
readonly version = options.version;
readonly viewerComponents = options.viewerComponents ?? {};
readonly repoComponents = options.repoComponents ?? {};

static readonly providerToken = providerToken;
};
};

@Directive({})
export class ExtenderPluginBaseComponent {
readonly slotData = input<unknown>();
readonly event = output<Event>();
readonly pluginManager = input<ExtenderPluginManager<unknown>>();
}

export const createExtenderComponent = () => {
return class extends ExtenderPluginBaseComponent {};
};
64 changes: 64 additions & 0 deletions extender/src/lib/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Injector, Type, inject, runInInjectionContext } from '@angular/core';
import { ExtenderPluginBaseComponent } from './factory';
import {
ExtenderAddonProviderPlugin,
ExtenderDataProviderPlugin,
ExtenderLoginProviderPlugin,
ExtenderProviderPlugin,
} from './provider';

export class ExtenderPluginManager<T> {
readonly plugins: ExtenderProviderPlugin[] = [];
readonly serviceMap = new Map<string, Type<T>>();
readonly injectedServicesMap = new Map<string, T>();

#injector = inject(Injector);

constructor(plugins: ExtenderProviderPlugin[], services: Record<string, Type<T>>) {
this.plugins.push(...plugins);
for (const [key, service] of Object.entries(services)) {
this.serviceMap.set(key, service);
}

runInInjectionContext(this.#injector, () => {
for (const [key, service] of this.serviceMap) {
const injected = inject(service);
this.injectedServicesMap.set(key, injected);
}
});
}

get dataProvider(): ExtenderDataProviderPlugin | undefined {
return this.plugins.find(
(plugin): plugin is ExtenderDataProviderPlugin => plugin.type === 'data-provider',
);
}

get loginProviders(): ExtenderLoginProviderPlugin[] {
return this.plugins.filter(
(plugin): plugin is ExtenderLoginProviderPlugin => plugin.type === 'login-provider',
);
}

get addonProviders(): ExtenderAddonProviderPlugin[] {
return this.plugins.filter(
(plugin): plugin is ExtenderAddonProviderPlugin => plugin.type === 'addon-provider',
);
}

public getComponentsForSlot(slot: string, componentSet: 'viewerComponents' | 'repoComponents') {
return new Map<ExtenderAddonProviderPlugin, Type<ExtenderPluginBaseComponent>[]>(
this.addonProviders.map(p => [p, p?.[componentSet]?.[slot] ?? []] as const),
);
}

public findAddonForComponent(
slot: string,
componentSet: 'viewerComponents' | 'repoComponents',
component: Type<ExtenderPluginBaseComponent>,
): ExtenderAddonProviderPlugin | undefined {
return this.addonProviders.find(p =>
p?.[componentSet]?.[slot]?.find(c => c.name === component.name),
);
}
}
Loading

0 comments on commit 08d0398

Please sign in to comment.