module.exports.uninstallPlugin = function()

in src/plugman/uninstall.js [93:227]


module.exports.uninstallPlugin = function (id, plugins_dir, options) {
    plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir);

    options = options || {};
    options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider();
    const pluginInfoProvider = options.pluginInfoProvider;

    const plugin_dir = path.join(plugins_dir, id);

    // @tests - important this event is checked spec/uninstall.spec.js
    events.emit('log', 'Removing "' + id + '"');

    // If already removed, skip.
    if (!fs.existsSync(plugin_dir)) {
        events.emit('verbose', 'Plugin "' + id + '" already removed (' + plugin_dir + ')');
        return Promise.resolve();
    }

    /*
     * Deletes plugin from plugins directory and node_modules directory.
     *
     * @param {String} id   the id of the plugin being removed
     *
     * @return {Promise||Error} Returns a empty promise or a promise of doing the npm uninstall
     */
    const doDelete = function (id) {
        const plugin_dir = path.join(plugins_dir, id);
        if (!fs.existsSync(plugin_dir)) {
            events.emit('verbose', 'Plugin "' + id + '" already removed (' + plugin_dir + ')');
            return Promise.resolve();
        }

        fs.removeSync(plugin_dir);
        events.emit('verbose', 'Deleted plugin "' + id + '"');

        // remove plugin from node_modules directory
        return npmUninstall(id, options.projectRoot, options);
    };

    // We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info.
    // Instead, we list all dependencies of the target plugin, and check the remaining metadata to see if
    // anything depends on them, or if they're listed as top-level.
    // If neither, they can be deleted.
    const top_plugin_id = id;

    // Recursively remove plugins which were installed as dependents (that are not top-level)
    const toDelete = [];
    function findDependencies (pluginId) {
        const depPluginDir = path.join(plugins_dir, pluginId);
        // Skip plugin check for dependencies if it does not exist (CB-7846).
        if (!fs.existsSync(depPluginDir)) {
            events.emit('verbose', 'Plugin "' + pluginId + '" does not exist (' + depPluginDir + ')');
            return;
        }
        const pluginInfo = pluginInfoProvider.get(depPluginDir);
        // TODO: Should remove dependencies in a separate step, since dependencies depend on platform.
        const deps = pluginInfo.getDependencies();
        deps.forEach(function (d) {
            if (toDelete.indexOf(d.id) === -1) {
                toDelete.push(d.id);
                findDependencies(d.id);
            }
        });
    }
    findDependencies(top_plugin_id);
    toDelete.push(top_plugin_id);

    // Okay, now we check if any of these are depended on, or top-level.
    // Find the installed platforms by whether they have a metadata file.
    const platforms = Object.keys(platform_modules).filter(function (platform) {
        return fs.existsSync(path.join(plugins_dir, platform + '.json'));
    });

    // Can have missing plugins on some platforms when not supported..
    const dependList = {};
    platforms.forEach(function (platform) {
        const platformJson = PlatformJson.load(plugins_dir, platform);
        const depsInfo = dependencies.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider);
        const tlps = depsInfo.top_level_plugins;
        let deps;

        // Top-level deps must always be explicitely asked to remove by user
        tlps.forEach(function (plugin_id) {
            if (top_plugin_id === plugin_id) { return; }

            const i = toDelete.indexOf(plugin_id);
            if (i >= 0) { toDelete.splice(i, 1); }
        });

        toDelete.forEach(function (plugin) {
            deps = dependencies.dependents(plugin, depsInfo, platformJson, pluginInfoProvider);

            const i = deps.indexOf(top_plugin_id);
            if (i >= 0) { deps.splice(i, 1); } // remove current/top-level plugin as blocking uninstall

            if (deps.length) {
                dependList[plugin] = deps.join(', ');
            }
        });
    });

    const dependPluginIds = toDelete.filter(function (plugin_id) {
        return dependList[plugin_id];
    });
    const createMsg = function (plugin_id) {
        return 'Plugin "' + plugin_id + '" is required by (' + dependList[plugin_id] + ')';
    };
    const createMsg2 = function (plugin_id) {
        return createMsg(plugin_id) + ' and cannot be removed (hint: use -f or --force)';
    };
    if (!options.force && dependPluginIds.includes(top_plugin_id)) {
        const msg = createMsg2(top_plugin_id);
        return Promise.reject(new CordovaError(msg));
    }

    // action emmiting events.
    if (options.force) {
        dependPluginIds.forEach(function (plugin_id) {
            const msg = createMsg(plugin_id);
            events.emit('log', msg + ' but forcing removal.');
        });
    } else {
        dependPluginIds.forEach(function (plugin_id) {
            const msg = createMsg2(plugin_id);
            events.emit('warn', msg);
        });
    }
    const deletePluginIds = options.force ? toDelete : toDelete.filter(function (plugin_id) { return !dependList[plugin_id]; });
    const deleteExecList = deletePluginIds.map(function (plugin_id) {
        return function () { return doDelete(plugin_id); };
    });
    return deleteExecList.reduce(function (acc, deleteExec) {
        return acc.then(deleteExec);
    }, Promise.resolve());
};