|
| 1 | +import { existsSync, readFileSync } from 'fs' |
| 2 | +import { dirname, join } from 'path' |
| 3 | +import { parse } from 'yaml' |
| 4 | +import { ExtensionFileV1 } from './_dto/extension.dto' |
| 5 | +import { ExtensionsFileV1, ExtensionsListV1 } from './_dto/extensions.dto' |
| 6 | +import { plainToInstance } from 'class-transformer' |
| 7 | +import { validateOrReject } from 'class-validator' |
| 8 | +import * as process from 'process' |
| 9 | +import { DynamicModule, Logger } from '@nestjs/common' |
| 10 | + |
| 11 | +const serviceList: DynamicModule[] = [] |
| 12 | + |
| 13 | +export const EXTENSION_FILE_INFO = 'extension.yml' |
| 14 | +export const EXTENSIONS_FILE_PATH = join(process.cwd(), '/extensions/list.yml') |
| 15 | + |
| 16 | +export async function parseExtensionsList(): Promise<ExtensionsListV1[]> { |
| 17 | + const data = readFileSync(EXTENSIONS_FILE_PATH, 'utf8') |
| 18 | + const yml = parse(data) |
| 19 | + const schema = plainToInstance(ExtensionsFileV1, yml) |
| 20 | + try { |
| 21 | + await validateOrReject(schema, { |
| 22 | + whitelist: true, |
| 23 | + }) |
| 24 | + } catch (errors) { |
| 25 | + const err = new Error(`Invalid extensions`) |
| 26 | + err.message = errors.map((e) => e.toString()).join(', ') //TODO: improve error message |
| 27 | + throw err |
| 28 | + } |
| 29 | + |
| 30 | + return yml.list |
| 31 | +} |
| 32 | + |
| 33 | +export async function extensionParseFile(path: string): Promise<ExtensionFileV1> { |
| 34 | + Logger.log('Extension file found, validating...', 'extensionParseFile') |
| 35 | + const data = readFileSync(`${path}/${EXTENSION_FILE_INFO}`, 'utf8') |
| 36 | + const yml = parse(data) |
| 37 | + return plainToInstance(ExtensionFileV1, yml) |
| 38 | +} |
| 39 | + |
| 40 | +export default async function (): Promise<DynamicModule[]> { |
| 41 | + try { |
| 42 | + if (existsSync(EXTENSIONS_FILE_PATH)) { |
| 43 | + Logger.log('Extensions file found, validating...', 'parsingAppExtensions') |
| 44 | + const list = await parseExtensionsList() |
| 45 | + |
| 46 | + for (const extension of list) { |
| 47 | + if (!extension.enabled) { |
| 48 | + Logger.log(`Extension ${extension.path} is disabled`, 'ExtensionServiceSetup') |
| 49 | + continue |
| 50 | + } |
| 51 | + const extensionPath = `${process.cwd()}/extensions/${extension.path}` |
| 52 | + const extensionFile = await extensionParseFile(extensionPath) |
| 53 | + if (!extensionFile.settings.service.target) { |
| 54 | + Logger.warn(`Extension ${extensionFile.information.name} has no service target`, 'ExtensionServiceSetup') |
| 55 | + continue |
| 56 | + } |
| 57 | + const extensionServiceTarget = `${extensionPath}/${extensionFile.settings.service.target}` |
| 58 | + await import(extensionServiceTarget) |
| 59 | + .then((module) => { |
| 60 | + if (module[extensionFile.settings.service.mainModule]) { |
| 61 | + serviceList.push(module[extensionFile.settings.service.mainModule]) |
| 62 | + Logger.log(`Extension ${extensionFile.information.name} is enabled`, 'ExtensionServiceSetup') |
| 63 | + return |
| 64 | + } |
| 65 | + Logger.warn(`Extension <${extensionFile.information.name}> module <${extensionFile.settings.service.mainModule}> has no main module`, 'ExtensionServiceSetup') |
| 66 | + }) |
| 67 | + .catch((err) => { |
| 68 | + Logger.error(`Extension <${extensionFile.information.name}> located in <${extensionServiceTarget}> failed to load`, 'ExtensionServiceSetup') |
| 69 | + console.error(err) |
| 70 | + console.trace(err.stack) |
| 71 | + }) |
| 72 | + } |
| 73 | + } |
| 74 | + return serviceList |
| 75 | + } catch (err) { |
| 76 | + Logger.error('Failed to load extensions', 'ExtensionServiceSetup') |
| 77 | + console.error(err) |
| 78 | + process.exit(1) |
| 79 | + } |
| 80 | +} |
0 commit comments