NodeBB/NodeBB

View on GitHub
src/plugins/load.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

const semver = require('semver');
const async = require('async');
const winston = require('winston');
const nconf = require('nconf');
const _ = require('lodash');

const meta = require('../meta');
const { themeNamePattern } = require('../constants');

module.exports = function (Plugins) {
    async function registerPluginAssets(pluginData, fields) {
        function add(dest, arr) {
            dest.push(...(arr || []));
        }

        const handlers = {
            staticDirs: function (next) {
                Plugins.data.getStaticDirectories(pluginData, next);
            },
            cssFiles: function (next) {
                Plugins.data.getFiles(pluginData, 'css', next);
            },
            lessFiles: function (next) {
                Plugins.data.getFiles(pluginData, 'less', next);
            },
            acpLessFiles: function (next) {
                Plugins.data.getFiles(pluginData, 'acpLess', next);
            },
            clientScripts: function (next) {
                Plugins.data.getScripts(pluginData, 'client', next);
            },
            acpScripts: function (next) {
                Plugins.data.getScripts(pluginData, 'acp', next);
            },
            modules: function (next) {
                Plugins.data.getModules(pluginData, next);
            },
            languageData: function (next) {
                Plugins.data.getLanguageData(pluginData, next);
            },
        };

        let methods = {};
        if (Array.isArray(fields)) {
            fields.forEach((field) => {
                methods[field] = handlers[field];
            });
        } else {
            methods = handlers;
        }

        const results = await async.parallel(methods);

        Object.assign(Plugins.staticDirs, results.staticDirs || {});
        add(Plugins.cssFiles, results.cssFiles);
        add(Plugins.lessFiles, results.lessFiles);
        add(Plugins.acpLessFiles, results.acpLessFiles);
        add(Plugins.clientScripts, results.clientScripts);
        add(Plugins.acpScripts, results.acpScripts);
        Object.assign(meta.js.scripts.modules, results.modules || {});
        if (results.languageData) {
            Plugins.languageData.languages = _.union(Plugins.languageData.languages, results.languageData.languages);
            Plugins.languageData.namespaces = _.union(Plugins.languageData.namespaces, results.languageData.namespaces);
            pluginData.languageData = results.languageData;
        }
        Plugins.pluginsData[pluginData.id] = pluginData;
    }

    Plugins.prepareForBuild = async function (targets) {
        const map = {
            'plugin static dirs': ['staticDirs'],
            'requirejs modules': ['modules'],
            'client js bundle': ['clientScripts'],
            'admin js bundle': ['acpScripts'],
            'client side styles': ['cssFiles', 'lessFiles'],
            'admin control panel styles': ['cssFiles', 'lessFiles', 'acpLessFiles'],
            languages: ['languageData'],
        };

        const fields = _.uniq(_.flatMap(targets, target => map[target] || []));

        // clear old data before build
        fields.forEach((field) => {
            switch (field) {
                case 'clientScripts':
                case 'acpScripts':
                case 'cssFiles':
                case 'lessFiles':
                case 'acpLessFiles':
                    Plugins[field].length = 0;
                    break;
                case 'languageData':
                    Plugins.languageData.languages = [];
                    Plugins.languageData.namespaces = [];
                    break;
            // do nothing for modules and staticDirs
            }
        });

        winston.verbose(`[plugins] loading the following fields from plugin data: ${fields.join(', ')}`);
        const plugins = await Plugins.data.getActive();
        await Promise.all(plugins.map(p => registerPluginAssets(p, fields)));
    };

    Plugins.loadPlugin = async function (pluginPath) {
        let pluginData;
        try {
            pluginData = await Plugins.data.loadPluginInfo(pluginPath);
        } catch (err) {
            if (err.message === '[[error:parse-error]]') {
                return;
            }
            if (!themeNamePattern.test(pluginPath)) {
                throw err;
            }
            return;
        }
        checkVersion(pluginData);

        try {
            registerHooks(pluginData);
            await registerPluginAssets(pluginData);
        } catch (err) {
            winston.error(err.stack);
            winston.verbose(`[plugins] Could not load plugin : ${pluginData.id}`);
            return;
        }

        if (!pluginData.private) {
            Plugins.loadedPlugins.push({
                id: pluginData.id,
                version: pluginData.version,
            });
        }

        winston.verbose(`[plugins] Loaded plugin: ${pluginData.id}`);
    };

    function checkVersion(pluginData) {
        function add() {
            if (!Plugins.versionWarning.includes(pluginData.id)) {
                Plugins.versionWarning.push(pluginData.id);
            }
        }

        if (pluginData.nbbpm && pluginData.nbbpm.compatibility && semver.validRange(pluginData.nbbpm.compatibility)) {
            if (!semver.satisfies(nconf.get('version'), pluginData.nbbpm.compatibility)) {
                add();
            }
        } else {
            add();
        }
    }

    function registerHooks(pluginData) {
        try {
            if (!Plugins.libraries[pluginData.id]) {
                Plugins.requireLibrary(pluginData);
            }

            if (Array.isArray(pluginData.hooks)) {
                pluginData.hooks.forEach(hook => Plugins.hooks.register(pluginData.id, hook));
            }
        } catch (err) {
            winston.warn(`[plugins] Unable to load library for: ${pluginData.id}`);
            throw err;
        }
    }
};