@feature-hub/core
Version:
Create scalable web applications using micro frontends.
194 lines • 8.63 kB
JavaScript
import { AsyncValue } from './async-value';
import * as Messages from './internal/feature-app-manager-messages';
import { isFeatureAppModule } from './internal/is-feature-app-module';
/**
* The `FeatureAppManager` manages the lifecycle of Feature Apps.
*/
export class FeatureAppManager {
constructor(featureServiceRegistry, options = {}) {
this.featureServiceRegistry = featureServiceRegistry;
this.options = options;
this.asyncFeatureAppDefinitions = new Map();
this.featureAppDefinitionsWithRegisteredOwnFeatureServices = new WeakSet();
this.featureAppRetainers = new Map();
this.logger = options.logger || console;
}
/**
* Load a [[FeatureAppDefinition]] using the module loader the
* [[FeatureAppManager]] was initilized with.
*
* @throws Throws an error if no module loader was provided on initilization.
*
* @param url A URL pointing to a [[FeatureAppDefinition]] bundle in a module
* format compatible with the module loader.
*
* @param moduleType The module type of the [[FeatureAppDefinition]] bundle.
* This value can be used by the provided
* [[FeatureAppManagerOptions.moduleLoader]].
*
* @returns An [[AsyncValue]] containing a promise that resolves with the
* loaded [[FeatureAppDefinition]]. If called again with the same URL it
* returns the same [[AsyncValue]]. The promise rejects when loading fails, or
* when the loaded bundle doesn't export a [[FeatureAppDefinition]] as
* default.
*/
getAsyncFeatureAppDefinition(url, moduleType) {
const key = `${url}${moduleType}`;
let asyncFeatureAppDefinition = this.asyncFeatureAppDefinitions.get(key);
if (!asyncFeatureAppDefinition) {
asyncFeatureAppDefinition = this.createAsyncFeatureAppDefinition(url, moduleType);
this.asyncFeatureAppDefinitions.set(key, asyncFeatureAppDefinition);
}
return asyncFeatureAppDefinition;
}
/**
* Create a [[FeatureAppScope]] which includes validating externals, binding
* all available Feature Service dependencies, and calling the `create` method
* of the [[FeatureAppDefinition]].
*
* @throws Throws an error if Feature Services that the
* [[FeatureAppDefinition]] provides with its `ownFeatureServices` key fail to
* be registered.
* @throws Throws an error if the required externals can't be satisfied.
* @throws Throws an error if the required Feature Services can't be
* satisfied.
* @throws Throws an error the [[FeatureAppDefinition]]'s `create` method
* throws.
*
* @param featureAppID The ID of the Feature App to create a scope for.
* @param featureAppDefinition The definition of the Feature App to create a
* scope for.
*
* @returns A [[FeatureAppScope]] for the provided Feature App ID and
* [[FeatureAppDefinition]]. A new scope is created for every call of
* `createFeatureAppScope`, even with the same ID and definiton.
*/
createFeatureAppScope(featureAppId, featureAppDefinition, options = {}) {
const featureAppRetainer = this.getFeatureAppRetainer(featureAppId, featureAppDefinition, options);
let released = false;
return {
featureApp: featureAppRetainer.featureApp,
release: () => {
if (released) {
this.logger.warn(`The Feature App with the ID ${JSON.stringify(featureAppId)} has already been released for this scope.`);
}
else {
released = true;
featureAppRetainer.release();
}
},
};
}
/**
* Preload a [[FeatureAppDefinition]] using the module loader the
* [[FeatureAppManager]] was initilized with. Useful before hydration of a
* server rendered page to avoid render result mismatch between client and
* server due missing [[FeatureAppDefinition]]s.
*
* @throws Throws an error if no module loader was provided on initilization.
*
* @param url A URL pointing to a [[FeatureAppDefinition]] bundle in a module
* format compatible with the module loader.
*
* @param moduleType The module type of the [[FeatureAppDefinition]] bundle.
* This value can be used by the provided
* [[FeatureAppManagerOptions.moduleLoader]].
*/
async preloadFeatureApp(url, moduleType) {
await this.getAsyncFeatureAppDefinition(url, moduleType).promise;
}
createAsyncFeatureAppDefinition(url, moduleType) {
const { moduleLoader: loadModule } = this.options;
if (!loadModule) {
throw new Error('No module loader provided.');
}
return new AsyncValue(loadModule(url, moduleType).then((featureAppModule) => {
if (!isFeatureAppModule(featureAppModule)) {
throw new Error(Messages.invalidFeatureAppModule(url, moduleType, this.options.moduleLoader));
}
this.logger.info(`The Feature App module at the url ${JSON.stringify(url)} has been successfully loaded.`);
return featureAppModule.default;
}));
}
registerOwnFeatureServices(featureAppId, featureAppDefinition) {
if (this.featureAppDefinitionsWithRegisteredOwnFeatureServices.has(featureAppDefinition)) {
return;
}
if (featureAppDefinition.ownFeatureServiceDefinitions) {
this.featureServiceRegistry.registerFeatureServices(featureAppDefinition.ownFeatureServiceDefinitions, featureAppId);
}
this.featureAppDefinitionsWithRegisteredOwnFeatureServices.add(featureAppDefinition);
}
getFeatureAppRetainer(featureAppId, featureAppDefinition, options) {
let featureAppRetainer = this.featureAppRetainers.get(featureAppId);
if (featureAppRetainer) {
featureAppRetainer.retain();
}
else {
this.registerOwnFeatureServices(featureAppId, featureAppDefinition);
featureAppRetainer = this.createFeatureAppRetainer(featureAppDefinition, featureAppId, options);
}
return featureAppRetainer;
}
createFeatureAppRetainer(featureAppDefinition, featureAppId, options) {
var _a, _b;
this.validateExternals(featureAppDefinition, featureAppId);
const { featureAppName, baseUrl, beforeCreate, config, done, parentFeatureApp, } = options;
const binding = this.featureServiceRegistry.bindFeatureServices(featureAppDefinition, featureAppId, featureAppName);
try {
(_b = (_a = this.options).onBind) === null || _b === void 0 ? void 0 : _b.call(_a, {
featureAppDefinition,
featureAppId,
featureAppName,
parentFeatureApp,
});
}
catch (error) {
this.logger.error('Failed to execute onBind callback.', error);
}
const env = {
baseUrl,
config,
featureAppId,
featureAppName,
featureServices: binding.featureServices,
done,
};
try {
beforeCreate === null || beforeCreate === void 0 ? void 0 : beforeCreate(env);
const featureApp = featureAppDefinition.create(env);
this.logger.info(`The Feature App with the ID ${JSON.stringify(featureAppId)} has been successfully created.`);
let retainCount = 1;
const featureAppRetainer = {
featureApp,
retain: () => {
retainCount += 1;
},
release: () => {
retainCount -= 1;
if (retainCount === 0) {
this.featureAppRetainers.delete(featureAppId);
binding.unbind();
}
},
};
this.featureAppRetainers.set(featureAppId, featureAppRetainer);
return featureAppRetainer;
}
catch (error) {
binding.unbind();
throw error;
}
}
validateExternals(featureAppDefinition, featureAppId) {
const { externalsValidator } = this.options;
if (!externalsValidator) {
return;
}
const { dependencies } = featureAppDefinition;
if (dependencies && dependencies.externals) {
externalsValidator.validate(dependencies.externals, featureAppId);
}
}
}
//# sourceMappingURL=feature-app-manager.js.map