diff --git a/package-lock.json b/package-lock.json index fa93418a..17e17f4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "fluent-ffmpeg": "2.1.2", "image-size": "1.0.2", "locale": "0.1.0", + "logger": "file:extensions/logger", "node-geocoder": "4.2.0", "nodemailer": "6.9.4", "reflect-metadata": "0.1.13", @@ -131,6 +132,12 @@ "mysql": "2.18.1" } }, + "extensions/logger": { + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.21" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -15585,8 +15592,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", @@ -15843,6 +15849,10 @@ "node": ">=8.0" } }, + "node_modules/logger": { + "resolved": "extensions/logger", + "link": true + }, "node_modules/loupe": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", @@ -35932,8 +35942,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.21", @@ -36137,6 +36146,12 @@ "streamroller": "^3.0.6" } }, + "logger": { + "version": "file:extensions/logger", + "requires": { + "lodash": "4.17.21" + } + }, "loupe": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", diff --git a/package.json b/package.json index 8d75c330..e47b1596 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "fluent-ffmpeg": "2.1.2", "image-size": "1.0.2", "locale": "0.1.0", + "logger": "file:extensions/logger", "node-geocoder": "4.2.0", "nodemailer": "6.9.4", "reflect-metadata": "0.1.13", diff --git a/src/backend/model/extension/ExtensionManager.ts b/src/backend/model/extension/ExtensionManager.ts index c98a6e69..1d9ee898 100644 --- a/src/backend/model/extension/ExtensionManager.ts +++ b/src/backend/model/extension/ExtensionManager.ts @@ -11,6 +11,9 @@ import * as express from 'express'; import {SQLConnection} from '../database/SQLConnection'; import {ExtensionObject} from './ExtensionObject'; import {ExtensionDecoratorObject} from './ExtensionDecorator'; +import * as util from 'util'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const exec = util.promisify(require('child_process').exec); const LOG_TAG = '[ExtensionManager]'; @@ -68,43 +71,60 @@ export class ExtensionManager implements IObjectManager { } Config.Extensions.list = fs - .readdirSync(ProjectPath.ExtensionFolder) - .filter((f): boolean => - fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory() - ); + .readdirSync(ProjectPath.ExtensionFolder) + .filter((f): boolean => + fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory() + ); Config.Extensions.list.sort(); Logger.debug(LOG_TAG, 'Extensions found ', JSON.stringify(Config.Extensions.list)); } - private async callServerFN(fn: (ext: IServerExtension, extName: string) => Promise) { - for (let i = 0; i < Config.Extensions.list.length; ++i) { - const extName = Config.Extensions.list[i]; - const extPath = path.join(ProjectPath.ExtensionFolder, extName); - const serverExt = path.join(extPath, 'server.js'); - if (!fs.existsSync(serverExt)) { - Logger.silly(LOG_TAG, `Skipping ${extName} server initiation. server.js does not exists`); - continue; + private createUniqueExtensionObject(name: string, folder: string): IExtensionObject { + let id = name; + if (this.extObjects[id]) { + let i = 0; + while (this.extObjects[`${name}_${++i}`]) { /* empty */ } - // eslint-disable-next-line @typescript-eslint/no-var-requires - const ext = require(serverExt); - await fn(ext, extName); + id = `${name}_${++i}`; } - } - - private createExtensionObject(name: string): IExtensionObject { - if (!this.extObjects[name]) { - this.extObjects[name] = new ExtensionObject(name, this.router, this.events); + if (!this.extObjects[id]) { + this.extObjects[id] = new ExtensionObject(id, name, folder, this.router, this.events); } - return this.extObjects[name]; + return this.extObjects[id]; } private async initExtensions() { - await this.callServerFN(async (ext, extName) => { - if (typeof ext?.init === 'function') { - Logger.debug(LOG_TAG, 'Running init on extension: ' + extName); - await ext?.init(this.createExtensionObject(extName)); + + for (let i = 0; i < Config.Extensions.list.length; ++i) { + const extFolder = Config.Extensions.list[i]; + let extName = extFolder; + const extPath = path.join(ProjectPath.ExtensionFolder, extFolder); + const serverExtPath = path.join(extPath, 'server.js'); + const packageJsonPath = path.join(extPath, 'package.json'); + if (!fs.existsSync(serverExtPath)) { + Logger.silly(LOG_TAG, `Skipping ${extFolder} server initiation. server.js does not exists`); + continue; } - }); + + if (fs.existsSync(packageJsonPath)) { + Logger.silly(LOG_TAG, `Running: "npm install --omit=dev" in ${extPath}`); + await exec('npm install --omit=dev' ,{ + cwd:extPath + }); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkg = require(packageJsonPath); + if (pkg.name) { + extName = pkg.name; + } + } + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ext = require(serverExtPath); + if (typeof ext?.init === 'function') { + Logger.debug(LOG_TAG, 'Running init on extension: ' + extFolder); + await ext?.init(this.createUniqueExtensionObject(extName, extPath)); + } + } if (Config.Extensions.cleanUpUnusedTables) { // Clean up tables after all Extension was initialized. await SQLConnection.removeUnusedTables(); @@ -112,12 +132,15 @@ export class ExtensionManager implements IObjectManager { } private async cleanUpExtensions() { - await this.callServerFN(async (ext, extName) => { + for (const extObj of Object.values(this.extObjects)) { + const serverExt = path.join(extObj.folder, 'server.js'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ext = require(serverExt); if (typeof ext?.cleanUp === 'function') { - Logger.debug(LOG_TAG, 'Running Init on extension:' + extName); - await ext?.cleanUp(this.createExtensionObject(extName)); + Logger.debug(LOG_TAG, 'Running Init on extension:' + extObj.extensionName); + await ext?.cleanUp(ext); } - }); + } } diff --git a/src/backend/model/extension/ExtensionObject.ts b/src/backend/model/extension/ExtensionObject.ts index f21e28ae..dcc35ed1 100644 --- a/src/backend/model/extension/ExtensionObject.ts +++ b/src/backend/model/extension/ExtensionObject.ts @@ -17,7 +17,11 @@ export class ExtensionObject implements IExtensionObject { public readonly events; public readonly RESTApi; - constructor(public readonly extensionId: string, extensionRouter: express.Router, events: IExtensionEvents) { + constructor(public readonly extensionId: string, + public readonly extensionName: string, + public readonly folder: string, + extensionRouter: express.Router, + events: IExtensionEvents) { const logger = createLoggerWrapper(`[Extension][${extensionId}]`); this._app = new ExtensionApp(); this.config = new ExtensionConfig(extensionId); diff --git a/src/backend/model/extension/IExtension.ts b/src/backend/model/extension/IExtension.ts index 3375caaf..038cdf7a 100644 --- a/src/backend/model/extension/IExtension.ts +++ b/src/backend/model/extension/IExtension.ts @@ -117,6 +117,16 @@ export interface IExtensionConfig { } export interface IExtensionObject { + /** + * ID of the extension that is internally used. By default the name and ID matches if there is no collision. + */ + extensionId: string, + + /** + * Name of the extension + */ + extensionName: string, + /** * Inner functionality of the app. Use this with caution. * If you want to go deeper than the standard exposed APIs, you can try doing so here.