@storm-stack/core
Version:
A build toolkit and runtime used by Storm Software in TypeScript applications
203 lines (200 loc) • 6.89 kB
JavaScript
import { __STORM_STACK_IS_PLUGIN__ } from './chunk-S6IVF7HS.js';
import { extendLog, createLog } from './chunk-K7ITYUIA.js';
import { init_esm_shims, __name } from './chunk-QH7NXH7H.js';
import { LogLevelLabel } from '@storm-software/config-tools/types';
import { isValidRange } from '@stryke/fs/semver-fns';
import { camelCase } from '@stryke/string-format/camel-case';
import { kebabCase } from '@stryke/string-format/kebab-case';
import { getPackageName, getPackageVersion } from '@stryke/string-format/package';
import { titleCase } from '@stryke/string-format/title-case';
import { isObject } from '@stryke/type-checks/is-object';
import defu from 'defu';
import { subset } from 'semver';
// src/base/plugin.ts
init_esm_shims();
var Plugin = class {
static {
__name(this, "Plugin");
}
#log;
#primaryKeyFields = [];
/**
* A list of plugin modules required as dependencies by the current Plugin.
*
* @remarks
* These plugins will be called prior to the current Plugin.
*/
dependencies = [];
/**
* The configuration options for the plugin
*
* @remarks
* This is used to store the configuration options for the plugin, which can be accessed by the plugin's methods.
*/
options;
/**
* A property to identify the object as a Storm Stack Plugin.
*/
get [__STORM_STACK_IS_PLUGIN__]() {
return true;
}
/**
* A list of dependencies that are required for the plugin to work. These dependencies will be installed when Storm Stack CLI is run.
*/
packageDeps = {};
/**
* The name of the plugin
*
* @remarks
* This is used to identify the plugin's name used in {@link Context.options}, logs, and other output.
*/
get name() {
let name = kebabCase(this.overrideName || this.constructor.name);
if (name.startsWith("plugin-")) {
name = name.replace(/^plugin-/g, "").trim();
}
if (name.endsWith("-plugin")) {
name = name.replace(/-plugin$/g, "").trim();
}
return name;
}
/**
* The primary keys for the plugin's options.
*
* @remarks
* This is used to identify when a two instances of the plugin are the same and can be de-duplicated.
*/
get primaryKeys() {
return this.primaryKeyFields.map((primaryKeyField) => this.options[primaryKeyField]);
}
/**
* Returns true if the plugin is a singleton. Singleton plugins can only be instantiated once (so whenever other plugins specify them as dependencies, they will be de-duplicated).
*
* @remarks
* A plugin is considered a singleton if it has zero primary key option fields defined.
*/
get isSingleton() {
return this.primaryKeyFields.length === 0;
}
/**
* The identifier for the plugin used in the {@link isSame} method
*
* @remarks
* Child plugins can override this to provide a more or less specific identifier. This is used to identify the plugin's options in {@link Context.options}.
*/
get identifier() {
return camelCase(`${this.primaryKeys.includes(this.name) ? "" : this.name}${this.isSingleton ? "" : `-${this.primaryKeys.join("-")}`}`);
}
/**
* A property to override the plugin's {@link name} field.
*
* @remarks
* This is useful for plugins that need to have a different name than the default one derived from the class name.
*/
get overrideName() {
return void 0;
}
/**
* A list of primary keys for the plugin's options.
*
* @remarks
* This is used to identify when a two instances of the plugin are the same and can be de-duplicated.
*/
get primaryKeyFields() {
return this.#primaryKeyFields ?? [];
}
/**
* The logger function to use
*/
get log() {
if (!this.#log) {
this.#log = this.options.log ? extendLog(this.options.log, `${titleCase(this.name)} Plugin`) : createLog(`${titleCase(this.name)} Plugin`);
}
return this.#log;
}
/**
* The constructor for the plugin
*
* @param options - The configuration options for the plugin
*/
constructor(options) {
this.options = options;
}
/**
* Checks if the current plugin is the same as another plugin based on primary key fields.
*
* @param plugin - The plugin to compare with.
* @returns `true` if the plugins are the same, `false` otherwise.
*/
isSame(plugin) {
return this.identifier === plugin.identifier || this.name === plugin.name && this.isSingleton && plugin.isSingleton;
}
/**
* Adds hooks to the engine's hook system.
*
* @param hooks - The hooks to add to the engine.
*/
addHooks(hooks) {
hooks.addHooks({
"init:begin": this.#initBegin.bind(this)
});
}
/**
* Gets the resolved options for the plugin.
*
* @param context - The context to use
* @returns The resolved options for the plugin
*/
getOptions(context) {
context.options.plugins ??= {};
context.options.plugins[this.identifier] ??= {};
return context.options.plugins[this.identifier];
}
/**
* Initializes the plugin's context with required installations.
*
* @param context - The context to initialize.
*/
async #initBegin(context) {
this.log(LogLevelLabel.TRACE, `Adding required installations for the project.`);
this.options = defu(this.options, context.options.plugins[this.identifier] ?? {});
if (Object.keys(this.packageDeps).length > 0) {
context.packageDeps = Object.entries(this.packageDeps).reduce((ret, [dependency, value]) => {
const packageName = getPackageName(dependency);
const currentVersion = getPackageVersion(dependency) || (isObject(value) ? value.version : void 0);
const currentType = isObject(value) ? value.type : value;
const match = Object.keys(ret).find((dep) => getPackageName(dep) === packageName);
if (match) {
const matchedVersion = getPackageVersion(match) || (isObject(ret[match]) ? ret[match].version : void 0);
const matchedType = isObject(ret[match]) ? ret[match].type : ret[match];
const type = currentType === "dependency" || matchedType === "dependency" ? "dependency" : currentType || matchedType;
if (currentVersion || matchedVersion) {
if (!currentVersion || !isValidRange(currentVersion)) {
ret[packageName] = {
version: matchedVersion,
type
};
} else if (!matchedVersion || !isValidRange(matchedVersion)) {
ret[packageName] = {
version: currentVersion,
type
};
} else {
ret[packageName] = {
version: subset(matchedVersion, currentVersion) ? matchedVersion : currentVersion,
type
};
}
}
if (match !== packageName) {
delete ret[match];
}
} else {
ret[dependency] = value;
}
return ret;
}, {});
}
}
};
export { Plugin };