You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vizality/renderer/src/managers/Addon.js

976 lines
27 KiB

/**
*
*/
import { toKebabCase, toTitleCase, toHash, toPlural } from '@vizality/util/string';
import fs, { stat, renameSync, readFileSync, existsSync, lstatSync, readdirSync } from 'fs';
import { AddonInfoMessage, AddonUninstallModal } from '@vizality/components/addon';
import { removeDirRecursive } from '@vizality/util/file';
import { log, warn, error } from '@vizality/util/logger';
import { openModal } from '@vizality/modal';
import { isArray } from '@vizality/util/array';
import { Avatars, Events as _Events, Protocols } from '@vizality/constants';
import http from 'isomorphic-git/http/node';
import { join, resolve, sep, extname } from 'path';
import { clone } from 'isomorphic-git';
import { Messages } from '@vizality/i18n';
import { watch } from 'chokidar';
import Events from 'events';
import React from 'react';
const requiredManifestKeys = [ 'name', 'version', 'description', 'author' ];
const ErrorTypes = Object.freeze({
ADDON_ALREADY_INSTALLED: 'ADDON_ALREADY_INSTALLED',
ADDON_ALREADY_INSTALLING: 'ADDON_ALREADY_INSTALLING',
DIRECTORY_ALREADY_EXISTS: 'DIRECTORY_ALREADY_EXISTS'
});
/**
* @extends Events
*/
export default class AddonManager extends Events {
constructor (type, dir) {
super();
this.dir = dir;
this.type = type;
this._items = new Map();
this._watcherEnabled = null;
this._watcher = {};
this._labels = [ 'Manager', this.type ];
}
/**
*
*/
get count () {
return this._items.size;
}
/**
*
*/
get values () {
return this._items.values();
}
/**
*
* @returns
*/
get keys () {
return [ ...this._items.keys() ];
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
has (addonId) {
return this._items.has(addonId);
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
get (addonId) {
return this._items.get(addonId);
}
/**
*
* @returns
*/
getAll () {
return this._items;
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
isInstalled (addonId) {
return this.has(addonId);
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
isEnabled (addonId) {
return !vizality.settings.get(`disabled${toTitleCase(toPlural(this.type))}`, [])
.filter(addon => this.isInstalled(addon))
.includes(addonId);
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
isDisabled (addonId) {
return !this.isEnabled(addonId);
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
hasSettings (addonId) {
try {
const addon = this.get(addonId);
return Boolean(addon?.sections?.settings);
} catch (err) {
return this._error(`An error occurred while checking for settings for "${addonId}"!`, err);
}
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
hasScreenshots (addonId) {
try {
const addon = this.get(addonId);
return Boolean(addon?.manifest?.screenshots);
} catch (err) {
return this._error(`An error occurred while checking for screenshots for "${addonId}"!`, err);
}
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
hasReadme (addonId) {
try {
const addon = this.get(addonId);
return Boolean(addon?.sections?.readme);
} catch (err) {
return this._error(`An error occurred while checking for readme for "${addonId}"!`, err);
}
}
/**
*
* @param {string} addonId Addon ID
* @returns
*/
hasChangelog (addonId) {
try {
const addon = this.get(addonId);
return Boolean(addon?.sections?.changelog);
} catch (err) {
return this._error(`An error occurred while checking for changelog for "${addonId}"!`, err);
}
}
/**
*
* @returns
*/
getEnabledKeys () {
const addons = this.keys;
return addons.filter(addon => this.isEnabled(addon));
}
/**
*
* @returns
*/
getEnabled () {
const enabled = new Map();
this.getEnabledKeys()
.sort((a, b) => a - b)
.map(addon => this.get(addon))
.forEach(addon => enabled.set(addon.addonId, addon));
return enabled;
}
/**
*
* @returns
*/
getDisabledKeys () {
const addons = this.keys;
return addons.filter(addon => this.isDisabled(addon));
}
/**
*
* @returns
*/
getDisabled () {
const disabled = new Map();
this.getDisabledKeys()
.sort((a, b) => a - b)
.map(addon => this.get(addon))
.forEach(addon => disabled.set(addon.addonId, addon));
return disabled;
}
/**
* Initializes an addon.
* @param {string} addonId Addon ID
*/
async mount (addonId) {
let manifest;
try {
manifest = Object.assign({
appMode: 'app'
}, await import(resolve(this.dir, addonId, 'manifest.json')));
} catch (err) {
return this._error(`${toTitleCase(this.type)} "${addonId}" doesn't have a valid manifest. Initialization aborted.`);
}
if (!requiredManifestKeys.every(key => manifest.hasOwnProperty(key))) {
return this._error(`${toTitleCase(this.type)} "${addonId}" doesn't have a valid manifest. Initialization aborted.`);
}
try {
const addonModule = await import(resolve(this.dir, addonId));
const Addon = addonModule?.__esModule ? addonModule.default : addonModule;
Object.defineProperties(Addon?.prototype, {
addonId: {
get: () => addonId,
set: () => {
throw new Error(`${toTitleCase(toPlural(this.type))} cannot update their ID at runtime!`);
}
},
manifest: {
get: () => manifest,
set: () => {
throw new Error(`${toTitleCase(toPlural(this.type))} cannot update manifest at runtime!`);
}
}
});
this._setAddonIcon(addonId, manifest);
this._setScreenshotImages(addonId, manifest);
this._items.set(addonId, new Addon());
} catch (err) {
return this._error(`An error occurred while initializing "${addonId}"!`, err);
}
}
/**
* Uninitializes an addon.
* @param {string} addonId Addon ID
* @param {boolean} showLogs Whether to show unloading log messages
*/
async unmount (addonId, showLogs = true) {
try {
const addon = this.get(addonId);
if (!addon) {
throw new Error(`Tried to unmount a non-installed ${this.type}: "${addon}"!`);
}
await addon._unload(showLogs);
Object.keys(require.cache).forEach(key => {
if (key.includes(addonId)) {
delete require.cache[key];
}
});
this._items.delete(addonId);
} catch (err) {
return this._error(`An error occurred while unmounting "${addonId}"!`, err);
}
}
/**
* Re-initializes an addon.
* @param {string} addonId Addon ID
* @param {boolean} showLogs Whether to show loading and unloading log messages
*/
async remount (addonId, showLogs = true) {
try {
await this.unmount(addonId, showLogs);
} catch (err) {
return this._error(`An error occurred while remounting "${addonId}"!`, err);
}
/*
* @note I have these separated like this because it seems to cause problems if they're
* not separated for some reason. Not confirmed yet though.
*/
try {
await this.mount(addonId);
await this.get(addonId)?._load(showLogs);
} catch (err) {
return this._error(`An error occurred while remounting "${addonId}"!`, err);
}
}
/**
* Reinitializes all addons.
*/
async remountAll () {
try {
const addons = this.getEnabledKeys();
for (const addon of addons) {
await this.remount(addon, false);
}
} catch (err) {
return this._error(`An error occurred while remounting all ${toPlural(this.type)}!`, err);
}
return this._log(`All ${toPlural(this.type)} have been re-initialized!`);
}
/**
* Initializes all addons.
*/
async initialize () {
let addonId;
try {
this._enableWatcher();
if (this._watcherEnabled) {
await this._watchFiles();
}
const ignorePath = join(this.dir, '.vzignore');
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, 'utf-8').trim().split(/\r?\n/) : [];
const files = readdirSync(this.dir).sort(this._sortBuiltins);
for (const filename of files) {
/**
* Remove any leftover addons that were being installed.
*/
if (filename.startsWith('__installing__')) {
await removeDirRecursive(resolve(this.dir, filename));
continue;
}
addonId = filename;
// If it's a file or listed in .vzignore, skip it
if (lstatSync(join(this.dir, addonId)).isFile() || ignore.includes(addonId)) {
continue;
}
// Mount the addon
await this.mount(addonId);
// If addon didn't mount, skip it
if (!this.get(addonId)) {
continue;
}
/**
* If the addon is not disabled in the user's settings, load it.
*/
if (!this.getDisabledKeys().includes(addonId)) {
await this.get(addonId)?._load();
}
}
this.emit(_Events.VIZALITY_ADDONS_READY, { type: this.type });
} catch (err) {
return this._error(`An error occurred while initializing "${addonId}"!`, err);
}
}
/**
* Shuts down and unloads all addons.
*/
async stop () {
try {
this._disableWatcher();
const addons = this.keys;
for (const addon of addons) {
if (this.isEnabled(addon)) {
await this.unmount(addon, false);
} else {
Object.keys(require.cache).forEach(key => {
if (key.includes(addon)) {
delete require.cache[key];
}
});
this._items.delete(addon);
}
}
} catch (err) {
return this._error(`There was a problem shutting down ${toPlural(this.type)}!`, err);
}
return this._log(`All ${toPlural(this.type)} have been unloaded!`);
}
/**
* Toggles an addon with a given ID.
* @param {string} addonId Addon ID
*/
async toggle (addonId, sendEvent = true) {
try {
const addon = this.get(addonId);
/**
* Make sure the addon is installed.
*/
if (!addon) {
throw new Error(`Tried to toggle a non-installed ${this.type}: "${addonId}"!`);
}
/**
* Toggle the addon.
*/
let enabled;
if (this.isEnabled(addonId)) {
await this.disable(addonId, false);
enabled = false;
} else if (this.isDisabled(addonId, false)) {
await this.enable(addonId);
enabled = true;
}
/**
* Check if we should send the event.
*/
if (sendEvent) {
this.emit(_Events.VIZALITY_ADDON_TOGGLE, { addonId, type: this.type, enabled });
}
} catch (err) {
return this._error(err);
}
}
/**
* Enables an addon with a given ID.
* @param {string} addonId Addon ID
*/
async enable (addonId, sendEvent = true) {
try {
const addon = this.get(addonId);
if (!addon) {
throw new Error(`Tried to enable a non-installed ${this.type}: "${addonId}"!`);
}
if ((this.type === 'plugin' || this.type === 'builtin') && addon._ready) {
throw new Error(`Tried to enable an already-loaded ${this.type}: "${addonId}"!`);
}
vizality.settings.set(`disabled${toTitleCase(toPlural(this.type))}`,
vizality.settings.get(`disabled${toTitleCase(toPlural(this.type))}`, [])
.filter(addon => addon !== addonId));
await addon._load(true);
/**
* Check if we should send the event.
*/
if (sendEvent) {
this.emit(_Events.VIZALITY_ADDON_ENABLE, { addonId, type: this.type });
}
} catch (err) {
return this._error(err);
}
}
/**
* Disables an addon with a given ID.
* @param {string} addonId Addon ID
*/
async disable (addonId) {
try {
const addon = this.get(addonId);
if (!addon) {
throw new Error(`Tried to disable a non-installed ${this.type}: "${addonId}"!`);
}
if ((this.type !== 'theme') && !addon._ready) {
throw new Error(`Tried to disable a non-loaded ${this.type}: "${addon}"!`);
}
vizality.settings.set(`disabled${toTitleCase(toPlural(this.type))}`, [
...vizality.settings.get(`disabled${toTitleCase(toPlural(this.type))}`, []),
addonId
]);
await addon._unload(true);
this.emit(_Events.VIZALITY_ADDON_DISABLE, { addonId, type: this.type });
} catch (err) {
return this._error(err);
}
}
/**
* Reloads an addon.
* @param {string} addonId Addon ID
*/
async reload (addonId) {
try {
await this.disable(addonId);
await this.enable(addonId);
} catch (err) {
return this._error(err);
}
}
/**
* Reloads all addons.
*/
async reloadAll () {
try {
const addons = this.getEnabledKeys();
for (const addon of addons) {
await this.reload(addon);
}
} catch (err) {
return this._error(err);
}
}
/**
* Enables all addons.
*/
async enableAll () {
try {
const addons = this.getDisabledKeys();
for (const addon of addons) {
await this.enable(addon);
}
} catch (err) {
return this._error(err);
}
}
/**
* Disables all addons.
*/
async disableAll () {
try {
const addons = this.getEnabledKeys();
for (const addon of addons) {
await this.disable(addon);
}
} catch (err) {
return this._error(err);
}
}
/**
* Installs an addon.
* @param {string} addonId Addon ID or GitHub repository URL
*/
async install (addonId) {
try {
let git;
if (vizality.manager.community[toPlural(this.type)].has(addonId)) {
({ git } = vizality.manager.community[toPlural(this.type)].get(addonId));
}
if (!new RegExp(/^(((https?:\/\/)(((([a-zA-Z0-9][a-zA-Z0-9\-_]{1,252})\.){1,8}[a-zA-Z]{2,63})\/))|((ssh:\/\/)?git@)(((([a-zA-Z0-9][a-zA-Z0-9\-_]{1,252})\.){1,8}[a-zA-Z]{2,63})(:)))([a-zA-Z0-9][a-zA-Z0-9_-]{1,36})(\/)([a-zA-Z0-9][a-zA-Z0-9_-]{1,36})((\.git)?)$/).test(git || addonId)) {
throw new Error('You must provide a valid GitHub repository URL or an addon ID from https://github.com/vizality-community!');
} else if (vizality.manager.community.plugins.has(addonId)) {
git = `https://github.com/vizality-community/${addonId}`;
}
/**
* The addonId supplied might be a GitHub repository URL
*/
if (!git) {
git = addonId;
}
/**
* The URL must end in git to get processed by isomorphic-git below.
*/
if (!git.endsWith('.git')) {
git = `${git}.git`;
}
addonId = git.split('.git')[0].split('/')[git.split('.git')[0].split('/').length - 1];
addonId = toKebabCase(addonId);
if (this.isInstalled(addonId)) {
throw {
name: ErrorTypes.ADDON_ALREADY_INSTALLED,
message: Messages.VIZALITY_ADDON_ALREADY_INSTALLED_DESC.format({ type: this.type, addonId }),
stack: `\n${(new Error()).stack.replace('Error\n', '')}`,
addonId
};
}
if (existsSync(join(this.dir, `__installing__${addonId}`)) && lstatSync(join(this.dir, `__installing__${addonId}`))?.isDirectory()) {
throw {
name: ErrorTypes.ADDON_ALREADY_INSTALLING,
message: Messages.VIZALITY_ADDON_ALREADY_INSTALLING_DESC.format({ type: this.type, addonId }),
stack: `\n${(new Error()).stack.replace('Error\n', '')}`,
addonId
};
}
if (existsSync(join(this.dir, addonId)) && lstatSync(join(this.dir, addonId))?.isDirectory()) {
throw {
name: ErrorTypes.DIRECTORY_ALREADY_EXISTS,
message: Messages.VIZALITY_ADDON_DIRECTORY_ALREADY_EXISTS_DESC.format({ type: this.type, addonId }),
stack: `\n${(new Error()).stack.replace('Error\n', '')}`,
addonId
};
}
try {
await clone({
fs,
http,
singleBranch: true,
depth: 1,
dir: join(this.dir, `__installing__${addonId}`),
url: git,
onProgress: evt => {
// console.log(evt);
}
});
} catch (err) {
/**
* @note isomorphic-git creates the directory before it checks anything, whether there is
* a response or not, so let's remove it if there's an error here.
*/
await removeDirRecursive(resolve(this.dir, addonId));
throw new Error(`There was a problem while attempting to install "${addonId}"!`, err);
}
try {
renameSync(join(this.dir, `__installing__${addonId}`), join(this.dir, addonId));
} catch (err) {
console.log(err);
}
/**
* Send a success toast.
*/
vizality.api.notifications.sendToast({
id: 'addon-installed',
header: Messages.VIZALITY_ADDON_SUCCESSFULLY_INSTALLED.format({ type: toTitleCase(this.type) }),
content: <AddonInfoMessage addon={this.get(addonId) || { manifest: { name: addonId } }} message={Messages.VIZALITY_ADDON_SUCCESSFULLY_INSTALLED_DESC.format({ type: toTitleCase(this.type) })} />,
icon: this.type,
buttons: [
{
text: 'View',
onClick: () => vizality.api.routes.navigateTo(`/vizality/${this.type}/${addonId}`)
}
]
});
} catch (err) {
let addon;
if (err.addonId) {
addon = this.get(err.addonId);
}
switch (err.name) {
/**
* Addon Already Installed
*/
case ErrorTypes.ADDON_ALREADY_INSTALLED: {
this._error(err.message, err.addonId, err.stack);
vizality.api.notifications.sendToast({
id: ErrorTypes.ADDON_ALREADY_INSTALLED,
header: Messages.VIZALITY_ADDON_ALREADY_INSTALLED_DESC.format({ type: toTitleCase(this.type) }),
content: <AddonInfoMessage addon={addon} message={err.message} />,
icon: this.type,
buttons: [
{
text: Messages.VIZALITY_UNINSTALL,
color: 'red',
onClick: () => {
vizality.api.notifications.closeToast(ErrorTypes.ADDON_ALREADY_INSTALLED);
this.uninstall(err.addonId);
}
},
{
text: 'View',
onClick: () => vizality.api.routes.navigateTo(`/${toPlural(this.type)}/${err.addonId}`)
}
]
});
break;
}
/**
* Addon Already Installing
*/
case ErrorTypes.ADDON_ALREADY_INSTALLING: {
this._error(err.message, err.addonId, err.stack);
vizality.api.notifications.sendToast({
id: ErrorTypes.ADDON_ALREADY_INSTALLING,
header: Messages.VIZALITY_ADDON_ALREADY_INSTALLING_DESC.format({ type: toTitleCase(this.type) }),
content: <AddonInfoMessage addon={addon} message={err.message} />,
icon: this.type,
buttons: [
{
text: 'Cancel',
color: 'red',
onClick: async () => {
vizality.api.notifications.closeToast(ErrorTypes.ADDON_ALREADY_INSTALLING);
await removeDirRecursive(resolve(this.dir, `__installing__${err.addonId}`));
}
}
]
});
break;
}
/**
* Addon Directory Already Exists
*/
case ErrorTypes.ADDON_DIRECTORY_ALREADY_EXISTS: {
this._error(err.message, err.addonId, err.stack);
vizality.api.notifications.sendToast({
id: ErrorTypes.ADDON_DIRECTORY_ALREADY_EXISTS,
header: Messages.ADDON_DIRECTORY_ALREADY_EXISTS_DESC.format({ type: toTitleCase(this.type) }),
content: <AddonInfoMessage addon={addon} message={err.message} />,
icon: this.type
});
break;
}
}
}
}
/**
* Uninstalls a plugin or theme.
* @param {string} addonId Addon ID
* @returns {Promise<void>}
* @private
*/
async _uninstall (addonId) {
try {
if (!this.isInstalled(addonId)) {
console.error(`Can't uninstall non-installed thing`);
}
await this.unmount(addonId);
await removeDirRecursive(resolve(this.dir, addonId));
this.emit(_Events.VIZALITY_ADDON_UNINSTALL, { addonId, type: this.type });
} catch (err) {
return this._error(err);
}
}
/**
* Sets an addon's icon image URL.
* @private
* @param {string} addonId Addon ID
* @param {object} manifest Addon manifest
* @returns {Promise<void>}
*/
async _setAddonIcon (addonId, manifest) {
try {
const validExtensions = [ '.png', '.jpg', '.jpeg' ];
if (manifest.icon) {
if (!validExtensions.some(ext => manifest.icon.endsWith(ext))) {
this._warn(`${toTitleCase(this.type)} icon must be of type .png, .jpg, or .jpeg.`);
} else {
return manifest.icon = `${Protocols[toPlural(this.type).toUpperCase()]}/${addonId}/${manifest.icon}`;
}
}
if (validExtensions.some(ext => existsSync(resolve(this.dir, addonId, 'assets', `icon${ext}`)))) {
for (const ext of validExtensions) {
if (existsSync(resolve(this.dir, addonId, 'assets', `icon${ext}`))) {
manifest.icon = `${Protocols[toPlural(this.type).toUpperCase()]}/${addonId}/assets/icon${ext}`;
break;
}
}
} else {
return manifest.icon = this._getDefaultAddonIcon(addonId);
}
} catch (err) {
return this._error(err);
}
}
/**
* Gets a default addon icon image URL.
* @private
* @param {string} addonId Addon ID
* @returns {string}
*/
_getDefaultAddonIcon (addonId) {
try {
const addonIdHash = toHash(addonId);
return Avatars[`DEFAULT_${this.type.toUpperCase()}_${(addonIdHash % 5) + 1}`];
} catch (err) {
return this._error(err);
}
}
/**
*
* @param {string} addonId Addon ID
* @param {AddonManifest} manifest Addon manifest
* @returns {void}
*/
_setScreenshotImages (addonId, manifest) {
try {
const validExtensions = [ '.png', '.jpg', '.jpeg', '.gif', '.webp' ];
const screenshotUrls = [];
if (isArray(manifest.screenshots)) {
manifest.screenshot.forEach(screenshot => {
if (!validExtensions.some(ext => screenshot.endsWith(ext))) {
this._warn(`${toTitleCase(this.type)} screenshots must be of type .png, .jpg, .jpeg, .gif, or .webp`);
} else {
screenshotUrls.push(`${Protocols[toPlural(this.type).toUpperCase()]}/${addonId}/${screenshot}`);
}
});
} else {
const screenshotsDir = join(this.dir, addonId, 'screenshots');
const hasScreenshots = existsSync(screenshotsDir) && lstatSync(screenshotsDir)?.isDirectory();
if (hasScreenshots) {
const previewImages = [];
const validExtensions = [ '.png', '.gif', '.jpg', '.jpeg', '.webp' ];
readdirSync(screenshotsDir)
.filter(file => validExtensions.indexOf(extname(file).toLowerCase()) !== -1)
.map(file => previewImages.push(`${Protocols[toPlural(this.type).toUpperCase()]}/${addonId}/screenshots/${file}`));
return manifest.screenshots = previewImages;
}
}
} catch (err) {
return this._error(err);
}
}
/**
* Enables the addon directory watcher.
* @private
* @returns {void}
*/
_enableWatcher () {
this._watcherEnabled = true;
}
/**
* Disables the addon directory watcher.
* @private
* @returns {Promise<void>}
*/
async _disableWatcher () {
this._watcherEnabled = false;
if (this._watcher?.close) {
await this._watcher.close();
this._watcher = {};
}
}
/**
* Initiates the addon directory watcher.
* @private
* @returns {Promise<void>}
*/
async _watchFiles () {
this._watcher = watch(this.dir, {
ignored: [ /.exists/, /__installing__/ ],
ignoreInitial: true,
depth: 0
});
/**
* Makes sure that the directory added has been completely copied by the operating
* system before it attempts to do anything with the addon.
* @see {@link https://memorytin.com/2015/07/08/node-js-chokidar-wait-for-file-copy-to-complete-before-modifying/}
* @param {string} path Addon folder path
* @param {object} prev Previous folder stats info @see {@link https://nodejs.org/api/fs.html#fs_class_fs_stats}
* @returns {void}
*/
const checkAddDirComplete = (path, prev) => {
try {
stat(path, async (err, stat) => {
if (err) {
throw err;
}
if (stat.mtime.getTime() === prev.mtime.getTime()) {
const addonId = path.replace(this.dir + sep, '');
if (addonId !== toKebabCase(addonId)) {
renameSync(path, join(this.dir, toKebabCase(addonId)));
}
await this.mount(addonId);
await this.get(addonId)?._load();
} else {
setTimeout(checkAddDirComplete, 2000, path, stat);
}
});
} catch (err) {
this._error(err);
}
};
/**
*
*/
this._watcher
/**
*
*/
.on('addDir', (path, stat) => {
setTimeout(checkAddDirComplete, 2000, path, stat);
})
/**
*
*/
.on('unlinkDir', path => {
const addonId = path.replace(this.dir + sep, '');
Object.keys(require.cache).forEach(key => {
if (key.includes(addonId)) {
delete require.cache[key];
}
});
this._items.delete(addonId);
});
}
/**
*
* @param {string} addonId Addon ID
* @returns {void}
*/
uninstall (addonId) {
try {
if (!this.isInstalled(addonId)) {
console.error(`Can't uninstall non-installed thing`);
}
const addon = this.get(addonId);
if (!addon) {
return;
}
openModal(() => props => <AddonUninstallModal {...props} addon={addon} type={this.type} />);
} catch (err) {
return this._error(err);
}
}
/**
* @private
*/
_log (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
log({ labels: message[0], message: _message });
} else {
log({ labels: this._labels, message });
}
}
/**
* @private
*/
_warn (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
warn({ labels: message[0], message: _message });
} else {
warn({ labels: this._labels, message });
}
}
/**
* @private
*/
_error (...message) {
// In case the addon wants to provide their own labels
if (isArray(message[0])) {
const _message = message.slice(1);
error({ labels: message[0], message: _message });
} else {
error({ labels: this._labels, message });
}
}
}