diff --git a/.changeset/afraid-jokes-trade.md b/.changeset/afraid-jokes-trade.md new file mode 100644 index 000000000..175b4f407 --- /dev/null +++ b/.changeset/afraid-jokes-trade.md @@ -0,0 +1,11 @@ +--- +"@equinor/fusion-framework-module": minor +--- + +__Feat(module)__ add base module class + +As a module developer there should be a base provider class to extend, which handles basic wireing. + +Some aspects of providers should be the same for all, like `version` handling. + +These new features does not change any existing code, only tooling for future development diff --git a/.changeset/smooth-chicken-promise.md b/.changeset/smooth-chicken-promise.md new file mode 100644 index 000000000..49b349c28 --- /dev/null +++ b/.changeset/smooth-chicken-promise.md @@ -0,0 +1,20 @@ +--- +"@equinor/fusion-framework-module": minor +--- + +__Feat(module): add semver__ + +In some cases other modules might require features in sibling modules +```ts +if (modules.context.version.satisfies('>=7.2')) { + // do some code +} else { + throw Error('this feature requires ContextModule of 7.2 or higher, please update depencies') +} +``` + +Usage: +- log telemetry about module usage and outdated application +- debug code runtime by knowing version of implementation +- write inter-opt when breaking changes accour + diff --git a/packages/modules/module/package.json b/packages/modules/module/package.json index d84f6aec8..de42657ff 100644 --- a/packages/modules/module/package.json +++ b/packages/modules/module/package.json @@ -23,7 +23,8 @@ "directory": "packages/modules/module" }, "dependencies": { - "rxjs": "^7.5.7" + "rxjs": "^7.5.7", + "semver": "^7.5.0" }, "devDependencies": { "typescript": "^4.9.3" diff --git a/packages/modules/module/src/configurator.ts b/packages/modules/module/src/configurator.ts index b0a31c60d..2823ce960 100644 --- a/packages/modules/module/src/configurator.ts +++ b/packages/modules/module/src/configurator.ts @@ -16,6 +16,9 @@ import type { ModuleType, } from './types'; +import { SemanticVersion } from './lib/semantic-version'; +import { BaseModuleProvider, type IModuleProvider } from './lib/provider'; + export interface IModulesConfigurator< TModules extends Array = Array, TRef = any @@ -254,10 +257,31 @@ export class ModulesConfigurator = Array { + if ( + !(instance instanceof BaseModuleProvider) && + !(instance.version instanceof SemanticVersion) + ) { + // TODO change to warn in future + logger.debug( + `🤷 module does not extends the [BaseModuleProvider] or exposes [SemanticVersion]` + ); + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + instance.version = + module.version instanceof SemanticVersion + ? module.version + : new SemanticVersion( + module.version ?? '0.0.0-unknown' + ); + } catch (err) { + logger.error(`🚨 failed to set module version`); + } + } logger.debug(`🚀 initialized ${logger.formatModuleName(module)}`); return [key, instance]; }) diff --git a/packages/modules/module/src/lib/provider/BaseModuleProvider.ts b/packages/modules/module/src/lib/provider/BaseModuleProvider.ts new file mode 100644 index 000000000..a4b88f0b5 --- /dev/null +++ b/packages/modules/module/src/lib/provider/BaseModuleProvider.ts @@ -0,0 +1,47 @@ +import SemanticVersion from '../semantic-version'; + +import { type IModuleProvider } from './IModuleProvider'; + +import { Subscription, type TeardownLogic } from 'rxjs'; + +type BaseModuleProviderCtorArgs = { + version: string; + config: TConfig; +}; + +/** + * Base class for creating module provider + * + * this is the interface which is returned after enabling a module + */ +export abstract class BaseModuleProvider implements IModuleProvider { + #version: SemanticVersion; + #subscriptions: Subscription; + + public get version() { + return this.#version; + } + + constructor(args: BaseModuleProviderCtorArgs) { + this.#version = new SemanticVersion(args.version); + this.#subscriptions = new Subscription(); + this._init(args.config); + } + + protected abstract _init(config: TConfig): void; + + /** + * Add teardown down function, which is called on dispose. + * + * @param teardown dispose callback function + * @returns callback for removing the teardown + */ + protected _addTeardown(teardown: Exclude): VoidFunction { + this.#subscriptions.add(teardown); + return () => this.#subscriptions.remove(teardown); + } + + public dispose() { + this.#subscriptions.unsubscribe(); + } +} diff --git a/packages/modules/module/src/lib/provider/IModuleProvider.ts b/packages/modules/module/src/lib/provider/IModuleProvider.ts new file mode 100644 index 000000000..0caf93823 --- /dev/null +++ b/packages/modules/module/src/lib/provider/IModuleProvider.ts @@ -0,0 +1,6 @@ +import SemanticVersion from '../semantic-version'; + +export interface IModuleProvider { + get version(): SemanticVersion; + dispose: VoidFunction; +} diff --git a/packages/modules/module/src/lib/provider/index.ts b/packages/modules/module/src/lib/provider/index.ts new file mode 100644 index 000000000..715f13999 --- /dev/null +++ b/packages/modules/module/src/lib/provider/index.ts @@ -0,0 +1,2 @@ +export type { IModuleProvider } from './IModuleProvider'; +export { BaseModuleProvider } from './BaseModuleProvider'; diff --git a/packages/modules/module/src/lib/semantic-version.ts b/packages/modules/module/src/lib/semantic-version.ts new file mode 100644 index 000000000..0fef8ecab --- /dev/null +++ b/packages/modules/module/src/lib/semantic-version.ts @@ -0,0 +1,13 @@ +import { SemVer, satisfies } from 'semver'; + +/** + * Extension of {@link SemVer} to expose `satisfies` + * @see {@link [SemVer](https://www.npmjs.com/package/semver)} + */ +export class SemanticVersion extends SemVer { + public satisfies(arg: Parameters[1]) { + return satisfies(this, arg); + } +} + +export default SemanticVersion; diff --git a/packages/modules/module/src/types.ts b/packages/modules/module/src/types.ts index 9a4db9f88..0b4a8447f 100644 --- a/packages/modules/module/src/types.ts +++ b/packages/modules/module/src/types.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import SemanticVersion from './lib/semantic-version'; import { ObservableInput } from 'rxjs'; export type ModuleInitializerArgs = []> = { @@ -13,18 +14,87 @@ export type ModuleInitializerArgs = []> // | ((key: Extract, string>) => boolean) }; +/** + * Interface which describes the structure of a module + * + * @TODO create a BaseModule which implements this interface + * + * @template TKey name of the module + * @template TType module instance type + * @template TConfig module configurator type + * @template TDeps optional peer module dependencies + */ export interface Module = []> { + /** + * package version + */ + version?: SemanticVersion; + + /** + * uniq name of module, used as attribute name on module instances + */ name: TKey; + + /** + * _[optional]_ + * + * Create a configurator builder which the consumer can use for configuring + * + * @TODO change return type to `ObservableInput` + * @TODO add reference to `IConfigurationBuilder` + * + * @param ref this would normally be the parent instance + * @returns a configurator build + */ configure?: (ref?: any) => TConfig | Promise; + + /** + * _[optional]_ + * + * This method is called after the module initiator has created config. + * @see {@link Module.configure} + * + * @TODO change return type to `ObservableInput` + * + * @param config + * @returns void + */ postConfigure?: ( config: Record & ModulesConfigType> ) => void | Promise; + + /** + * __[required]__ + * + * Called after all configuration is done. + * + * Creates the instance to interact with + * + * @param args @see {@link ModuleInitializerArgs} + * @returns a provider instance which the consumer interact with + */ initialize: (args: ModuleInitializerArgs) => TType | Promise; + + /** + * _[optional]_ + * + * Called after the module is initialized + * + * @param args @see {@link ModuleInitializerArgs} + */ postInitialize?: (args: { ref?: any; instance: TType; modules: ModuleInstance; // Record & ModulesInstanceType>; }) => ObservableInput; + + /** + * _[optional]_ + * + * Cleanup callback + * + * @param args + */ dispose?: (args: { ref?: any; instance: TType; diff --git a/yarn.lock b/yarn.lock index 245f14307..14eaf559f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11298,6 +11298,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.5.0: + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz"