diff --git a/Dockerfile b/Dockerfile index 35e4665b2..5bfeca4f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -108,8 +108,8 @@ FROM build as development COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/pnpm-lock.yaml ./src/ COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin -RUN bin/installDeps.sh && { [ -z "${ETHERPAD_PLUGINS}" ] || \ - pnpm install --workspace-root ${ETHERPAD_PLUGINS}; } +RUN bin/installDeps.sh && \ + { [ -z "${ETHERPAD_PLUGINS}" ] || pnpm run install-plugins ${ETHERPAD_PLUGINS}; } FROM build as production @@ -119,9 +119,9 @@ ENV ETHERPAD_PRODUCTION=true COPY --chown=etherpad:etherpad ./src ./src COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin -RUN bin/installDeps.sh && { [ -z "${ETHERPAD_PLUGINS}" ] || \ - pnpm install --workspace-root ${ETHERPAD_PLUGINS}; } && \ - rm -rf ~/.npm +RUN bin/installDeps.sh && rm -rf ~/.npm && \ + { [ -z "${ETHERPAD_PLUGINS}" ] || pnpm run install-plugins ${ETHERPAD_PLUGINS}; } + # Copy the configuration file. COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json diff --git a/README.md b/README.md index 2c9b80586..ac3a6ff68 100644 --- a/README.md +++ b/README.md @@ -139,9 +139,7 @@ Alternatively, you can install plugins from the command line: ```sh cd /path/to/etherpad-lite -# The `--no-save` and `--legacy-peer-deps` arguments are necessary to work -# around npm quirks. -npm install --no-save --legacy-peer-deps ep_${plugin_name} +pnpm run install-plugins ep_${plugin_name} ``` Also see [the plugin wiki @@ -153,7 +151,7 @@ Run the following command in your Etherpad folder to get all of the features visible in the above demo gif: ```sh -npm install --no-save --legacy-peer-deps \ +pnpm run install-plugins \ ep_align \ ep_comments_page \ ep_embedded_hyperlinks2 \ diff --git a/bin/installPlugins.ts b/bin/installPlugins.ts new file mode 100644 index 000000000..65a3b7392 --- /dev/null +++ b/bin/installPlugins.ts @@ -0,0 +1,37 @@ +'use strict'; + +import {writeFileSync} from 'fs' +import {manager, installedPluginsPath} from "ep_etherpad-lite/static/js/pluginfw/installer"; +import {PackageData} from "ep_etherpad-lite/node/types/PackageInfo"; + +const pluginsModule = require('ep_etherpad-lite/static/js/pluginfw/plugins'); +if (process.argv.length === 2) { + console.error('Expected at least one argument!'); + process.exit(1); +} + +const plugins = process.argv.slice(2); + +const persistInstalledPlugins = async () => { + const plugins:PackageData[] = [] + const installedPlugins = {plugins: plugins}; + for (const pkg of Object.values(await pluginsModule.getPackages()) as PackageData[]) { + installedPlugins.plugins.push({ + name: pkg.name, + version: pkg.version, + }); + } + installedPlugins.plugins = [...new Set(installedPlugins.plugins)]; + writeFileSync(installedPluginsPath, JSON.stringify(installedPlugins)); +}; + +async function run() { + for (const plugin of plugins) { + await manager.install(plugin); + } +} + +(async () => { + await run(); + await persistInstalledPlugins(); +})(); diff --git a/bin/package.json b/bin/package.json index de6714251..121c3dc1b 100644 --- a/bin/package.json +++ b/bin/package.json @@ -30,7 +30,8 @@ "migrateDirtyDBtoRealDB": "node --import tsx migrateDirtyDBtoRealDB.ts", "rebuildPad": "node --import tsx rebuildPad.ts", "stalePlugins": "node --import tsx ./plugins/stalePlugins.ts", - "checkPlugins": "node --import tsx ./plugins/checkPlugins.ts" + "checkPlugins": "node --import tsx ./plugins/checkPlugins.ts", + "install-plugins": "node --import tsx ./installPlugins.ts" }, "author": "", "license": "ISC" diff --git a/bin/run.sh b/bin/run.sh index 4f0b8f83a..0709b5d4e 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -29,6 +29,14 @@ fi # Prepare the environment bin/installDeps.sh "$@" || exit 1 +## Create the admin ui +if [ -z "$NODE_ENV" ] || [ "$NODE_ENV" = "development" ]; then + log "Creating the admin UI..." + (cd ../admin && pnpm run build) +else + log "Cannot create the admin UI in production mode" +fi + # Move to the node folder and start log "Starting Etherpad..." diff --git a/package.json b/package.json index d9bdf8cd3..178bfe276 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "etherpad", "description": "A free and open source realtime collaborative editor", "homepage": "https://etherpad.org", + "type": "module", "keywords": [ "etherpad", "realtime", @@ -22,7 +23,8 @@ "test-ui": "pnpm --filter ep_etherpad-lite run test-ui", "test-ui:ui": "pnpm --filter ep_etherpad-lite run test-ui:ui", "test-admin": "pnpm --filter ep_etherpad-lite run test-admin", - "test-admin:ui": "pnpm --filter ep_etherpad-lite run test-admin:ui" + "test-admin:ui": "pnpm --filter ep_etherpad-lite run test-admin:ui", + "install-plugins": "pnpm --filter bin run install-plugins" }, "dependencies": { "ep_etherpad-lite": "workspace:./src" @@ -42,3 +44,4 @@ "version": "1.9.7", "license": "Apache-2.0" } + diff --git a/src/node/eejs/index.ts b/src/node/eejs/index.ts index eb9e882f0..b7f2cf998 100644 --- a/src/node/eejs/index.ts +++ b/src/node/eejs/index.ts @@ -26,6 +26,7 @@ const hooks = require('../../static/js/pluginfw/hooks.js'); const path = require('path'); const resolve = require('resolve'); const settings = require('../utils/Settings'); +import {pluginInstallPath} from '../../static/js/pluginfw/installer' const templateCache = new Map(); @@ -82,7 +83,13 @@ exports.require = (name:string, args:{ basedir = path.dirname(mod.filename); paths = mod.paths; } - paths.push(settings.root + '/plugin_packages') + + /** + * Add the plugin install path to the paths array + */ + if (!paths.includes(pluginInstallPath)) { + paths.push(pluginInstallPath) + } const ejspath = resolve.sync(name, {paths, basedir, extensions: ['.html', '.ejs']}); diff --git a/src/node/hooks/express/adminplugins.ts b/src/node/hooks/express/adminplugins.ts index dc34cd437..502799197 100644 --- a/src/node/hooks/express/adminplugins.ts +++ b/src/node/hooks/express/adminplugins.ts @@ -1,17 +1,14 @@ 'use strict'; import {ArgsExpressType} from "../../types/ArgsExpressType"; -import {Socket} from "node:net"; import {ErrorCaused} from "../../types/ErrorCaused"; import {QueryType} from "../../types/QueryType"; -import {PluginType} from "../../types/Plugin"; -const eejs = require('../../eejs'); -const settings = require('../../utils/Settings'); -const installer = require('../../../static/js/pluginfw/installer'); +import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer"; +import {PackageData} from "../../types/PackageInfo"; + const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); -const plugins = require('../../../static/js/pluginfw/plugins'); -const semver = require('semver'); +import semver from 'semver'; exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { @@ -32,7 +29,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { socket.on('checkUpdates', async () => { // Check plugins for updates try { - const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); + const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => { if (!results[plugin]) return false; @@ -54,7 +51,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { socket.on('getAvailable', async (query:string) => { try { - const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); + const results = await getAvailablePlugins(/* maxCacheAge:*/ false); socket.emit('results:available', results); } catch (er) { console.error(er); @@ -64,7 +61,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { socket.on('search', async (query: QueryType) => { try { - const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); + const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); let res = Object.keys(results) .map((pluginName) => results[pluginName]) .filter((plugin) => !pluginDefs.plugins[plugin.name]); @@ -79,7 +76,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { }); socket.on('install', (pluginName: string) => { - installer.install(pluginName, (err: ErrorCaused) => { + install(pluginName, (err: ErrorCaused) => { if (err) console.warn(err.stack || err.toString()); socket.emit('finished:install', { @@ -91,7 +88,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { }); socket.on('uninstall', (pluginName:string) => { - installer.uninstall(pluginName, (err:ErrorCaused) => { + uninstall(pluginName, (err:ErrorCaused) => { if (err) console.warn(err.stack || err.toString()); socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null}); @@ -108,7 +105,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { * @param {String} dir The directory of the plugin * @return {Object[]} */ -const sortPluginList = (plugins:PluginType[], property:string, /* ASC?*/dir:string): object[] => plugins.sort((a, b) => { +const sortPluginList = (plugins:PackageData[], property:string, /* ASC?*/dir:string): PackageData[] => plugins.sort((a, b) => { // @ts-ignore if (a[property] < b[property]) { return dir ? -1 : 1; diff --git a/src/node/server.ts b/src/node/server.ts index 76ffd3a6a..13d35a226 100755 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -1,7 +1,5 @@ #!/usr/bin/env node -'use strict'; - /** * This module is started with src/bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. * Static file Requests are answered directly from this module, Socket.IO messages are passed @@ -26,10 +24,10 @@ import {PluginType} from "./types/Plugin"; import {ErrorCaused} from "./types/ErrorCaused"; -import {PromiseHooks} from "node:v8"; - import log4js from 'log4js'; +import {checkForMigration} from "../static/js/pluginfw/installer"; + const settings = require('./utils/Settings'); let wtfnode: any; @@ -53,7 +51,6 @@ const express = require('./hooks/express'); const hooks = require('../static/js/pluginfw/hooks'); const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); -const installer = require('../static/js/pluginfw/installer'); const {Gate} = require('./utils/promises'); const stats = require('./stats') @@ -147,7 +144,7 @@ exports.start = async () => { } await db.init(); - await installer.checkForMigration(); + await checkForMigration(); await plugins.update(); const installedPlugins = (Object.values(pluginDefs.plugins) as PluginType[]) .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') diff --git a/src/node/types/PackageInfo.ts b/src/node/types/PackageInfo.ts new file mode 100644 index 000000000..3c4a884d8 --- /dev/null +++ b/src/node/types/PackageInfo.ts @@ -0,0 +1,20 @@ +export type PackageInfo = { + from: string, + name: string, + version: string, + resolved: string, + description: string, + license: string, + author: { + name: string + }, + homepage: string, + repository: string, + path: string +} + + +export type PackageData = { + version: string, + name: string +} \ No newline at end of file diff --git a/src/package.json b/src/package.json index 9e955d21a..214d3172e 100644 --- a/src/package.json +++ b/src/package.json @@ -87,6 +87,7 @@ "@types/node": "^20.11.27", "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", + "@types/semver": "^7.5.7", "@types/underscore": "^1.11.15", "eslint": "^8.57.0", "eslint-config-etherpad": "^3.0.22", diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml index cfe6f8709..9298e5e23 100644 --- a/src/pnpm-lock.yaml +++ b/src/pnpm-lock.yaml @@ -145,6 +145,9 @@ devDependencies: '@types/node': specifier: ^20.11.19 version: 20.11.19 + '@types/semver': + specifier: ^7.5.7 + version: 7.5.7 '@types/underscore': specifier: ^1.11.15 version: 1.11.15 diff --git a/src/static/js/pluginfw/LinkInstaller.ts b/src/static/js/pluginfw/LinkInstaller.ts new file mode 100644 index 000000000..fbb5cd391 --- /dev/null +++ b/src/static/js/pluginfw/LinkInstaller.ts @@ -0,0 +1,230 @@ +import {IPluginInfo, PluginManager} from "live-plugin-manager-pnpm"; +import path from "path"; +import {node_modules, pluginInstallPath} from "./installer"; +import {accessSync, constants, rmSync, symlinkSync, unlinkSync} from "node:fs"; +import {dependencies, name} from '../../../package.json' +import {pathToFileURL} from 'node:url'; +const settings = require('../../../node/utils/Settings'); +import {readFileSync} from "fs"; + +export class LinkInstaller { + private livePluginManager: PluginManager; + private loadedPlugins: IPluginInfo[] = []; + /* + * A map of dependencies to their dependents + * + */ + private readonly dependenciesMap: Map>; + + constructor() { + this.livePluginManager = new PluginManager({ + pluginsPath: pluginInstallPath, + cwd: path.join(settings.root, 'src') + }); + this.dependenciesMap = new Map(); + + } + + + public async init() { + // Insert Etherpad lite dependencies + for (let [dependency] of Object.entries(dependencies)) { + if (this.dependenciesMap.has(dependency)) { + this.dependenciesMap.get(dependency)?.add(name) + } else { + this.dependenciesMap.set(dependency, new Set([name])) + } + } + + } + + public async installFromPath(path: string) { + const installedPlugin = await this.livePluginManager.installFromPath(path) + await this.checkLinkedDependencies(installedPlugin) + } + + + public async installPlugin(pluginName: string, version?: string) { + if (version) { + const installedPlugin = await this.livePluginManager.install(pluginName, version); + this.linkDependency(pluginName) + await this.checkLinkedDependencies(installedPlugin) + } else { + const installedPlugin = await this.livePluginManager.install(pluginName); + this.linkDependency(pluginName) + await this.checkLinkedDependencies(installedPlugin) + } + } + + public async listPlugins() { + const plugins = this.livePluginManager.list() + if (plugins && plugins.length > 0 && this.loadedPlugins.length == 0) { + this.loadedPlugins = plugins + // Check already installed plugins + for (let plugin of plugins) { + await this.checkLinkedDependencies(plugin) + } + } + return plugins + } + + public async uninstallPlugin(pluginName: string) { + const installedPlugin = this.livePluginManager.getInfo(pluginName) + if (installedPlugin) { + console.debug(`Uninstalling plugin ${pluginName}`) + await this.removeSymlink(installedPlugin) + await this.livePluginManager.uninstall(pluginName) + await this.removeSubDependencies(installedPlugin) + } + } + + private async removeSubDependencies(plugin: IPluginInfo) { + const pluginDependencies = Object.keys(plugin.dependencies) + console.debug("Removing sub dependencies",pluginDependencies) + for (let dependency of pluginDependencies) { + await this.removeSubDependency(plugin.name, dependency) + } + } + + private async removeSubDependency(_name: string, dependency:string) { + if (this.dependenciesMap.has(dependency)) { + console.debug(`Dependency ${dependency} is still being used by other plugins`) + return + } + // Read sub dependencies + try { + const json:IPluginInfo = JSON.parse( + readFileSync(pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json'))) as unknown as string); + if(json.dependencies){ + for (let [subDependency] of Object.entries(json.dependencies)) { + await this.removeSubDependency(dependency, subDependency) + } + } + } catch (e){} + this.uninstallDependency(dependency) + } + + private uninstallDependency(dependency: string) { + try { + console.debug(`Uninstalling dependency ${dependency}`) + // Check if the dependency is already installed + accessSync(path.join(pluginInstallPath, dependency), constants.F_OK) + rmSync(path.join(pluginInstallPath, dependency), { + force: true, + recursive: true + }) + } catch (err) { + // Symlink does not exist + // So nothing to do + } + } + + private async removeSymlink(plugin: IPluginInfo) { + try { + accessSync(path.join(node_modules, plugin.name), constants.F_OK) + await this.unlinkSubDependencies(plugin) + // Remove the plugin itself + this.unlinkDependency(plugin.name) + } catch (err) { + console.error(`Symlink for ${plugin.name} does not exist`) + // Symlink does not exist + // So nothing to do + } + } + + private async unlinkSubDependencies(plugin: IPluginInfo) { + const pluginDependencies = Object.keys(plugin.dependencies) + for (let dependency of pluginDependencies) { + this.dependenciesMap.get(dependency)?.delete(plugin.name) + await this.unlinkSubDependency(plugin.name, dependency) + } + } + + private async unlinkSubDependency(plugin: string, dependency: string) { + if (this.dependenciesMap.has(dependency)) { + this.dependenciesMap.get(dependency)?.delete(plugin) + if (this.dependenciesMap.get(dependency)!.size > 0) { + // We have other dependants so do not uninstall + return + } + } + this.unlinkDependency(dependency) + // Read sub dependencies + try { + const json:IPluginInfo = JSON.parse( + readFileSync(pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json'))) as unknown as string); + if(json.dependencies){ + for (let [subDependency] of Object.entries(json.dependencies)) { + await this.unlinkSubDependency(dependency, subDependency) + } + } + } catch (e){} + + console.debug("Unlinking sub dependency",dependency) + this.dependenciesMap.delete(dependency) + } + + + private async addSubDependencies(plugin: IPluginInfo) { + const pluginDependencies = Object.keys(plugin.dependencies) + for (let dependency of pluginDependencies) { + await this.addSubDependency(plugin.name, dependency) + } + } + + private async addSubDependency(plugin: string, dependency: string) { + if (this.dependenciesMap.has(dependency)) { + // We already added the sub dependency + this.dependenciesMap.get(dependency)?.add(plugin) + } else { + this.linkDependency(dependency) + // Read sub dependencies + try { + const json:IPluginInfo = JSON.parse( + readFileSync(pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json'))) as unknown as string); + if(json.dependencies){ + Object.keys(json.dependencies).forEach((subDependency: string) => { + this.addSubDependency(dependency, subDependency) + }) + } + } catch (err) { + console.error(`Error reading package.json ${err} for ${pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json')).toString()}`) + } + this.dependenciesMap.set(dependency, new Set([plugin])) + } + } + + private linkDependency(dependency: string) { + try { + // Check if the dependency is already installed + accessSync(path.join(node_modules, dependency), constants.F_OK) + } catch (err) { + symlinkSync(path.join(pluginInstallPath, dependency), path.join(node_modules, dependency), 'dir') + } + } + + private unlinkDependency(dependency: string) { + try { + // Check if the dependency is already installed + accessSync(path.join(node_modules, dependency), constants.F_OK) + unlinkSync(path.join(node_modules, dependency)) + } catch (err) { + // Symlink does not exist + // So nothing to do + } + } + + + private async checkLinkedDependencies(plugin: IPluginInfo) { + // Check if the plugin really exists at source + try { + accessSync(path.join(pluginInstallPath, plugin.name), constants.F_OK) + // Skip if the plugin is already linked + } catch (err) { + // The plugin is not installed + console.debug(`Plugin ${plugin.name} is not installed`) + } + await this.addSubDependencies(plugin) + this.dependenciesMap.set(plugin.name, new Set()) + } +} diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.ts similarity index 55% rename from src/static/js/pluginfw/installer.js rename to src/static/js/pluginfw/installer.ts index ed7b328e3..1912ed7f5 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.ts @@ -1,20 +1,28 @@ 'use strict'; -const log4js = require('log4js'); +import log4js from "log4js"; + +import axios, {AxiosResponse} from "axios"; +import {PackageData, PackageInfo} from "../../../node/types/PackageInfo"; +import {MapArrayType} from "../../../node/types/MapType"; + +import path from "path"; + +import {promises as fs} from "fs"; + const plugins = require('./plugins'); const hooks = require('./hooks'); const runCmd = require('../../../node/utils/run_cmd'); const settings = require('../../../node/utils/Settings'); -const axios = require('axios'); -const {PluginManager} = require('live-plugin-manager-pnpm'); -const {promises: fs} = require('fs'); -const path = require('path'); +import {LinkInstaller} from "./LinkInstaller"; + const {findEtherpadRoot} = require('../../../node/utils/AbsolutePaths'); const logger = log4js.getLogger('plugins'); -exports.manager = new PluginManager(); +export const pluginInstallPath = path.join(settings.root, 'src','plugin_packages'); +export const node_modules = path.join(findEtherpadRoot(),'src', 'node_modules'); -const installedPluginsPath = path.join(settings.root, 'var/installed_plugins.json'); +export const installedPluginsPath = path.join(settings.root, 'var/installed_plugins.json'); const onAllTasksFinished = async () => { await plugins.update(); @@ -30,10 +38,12 @@ const headers = { let tasks = 0; -const wrapTaskCb = (cb) => { +export const linkInstaller = new LinkInstaller(); + +const wrapTaskCb = (cb:Function|null) => { tasks++; - return (...args) => { + return (...args: any) => { cb && cb(...args); tasks--; if (tasks === 0) onAllTasksFinished(); @@ -53,18 +63,23 @@ const migratePluginsFromNodeModules = async () => { await Promise.all(Object.entries(dependencies) .filter(([pkg, info]) => pkg.startsWith(plugins.prefix) && pkg !== 'ep_etherpad-lite') .map(async ([pkg, info]) => { - if (!info.resolved) { + const _info = info as PackageInfo + if (!_info.resolved) { // Install from node_modules directory - await exports.manager.installFromPath(`${findEtherpadRoot()}/node_modules/${pkg}`); + await linkInstaller.installFromPath(`${findEtherpadRoot()}/node_modules/${pkg}`); } else { - await exports.manager.install(pkg); + await linkInstaller.installPlugin(pkg); } })); await persistInstalledPlugins(); }; -exports.checkForMigration = async () => { +export const checkForMigration = async () => { logger.info('check installed plugins for migration'); + // Initialize linkInstaller + await linkInstaller.init() + + try { await fs.access(installedPluginsPath, fs.constants.F_OK); @@ -72,19 +87,47 @@ exports.checkForMigration = async () => { await migratePluginsFromNodeModules(); } + /* + * Check if the plugin is already installed in node_modules + * If not, create a symlink to node_modules + * This is necessary as + * 1. Live Plugin Manager does not support loading plugins from the directory so that node can access them normally + * 2. Plugins can't be directly installed to node_modules otherwise upgrading Etherpad will remove them + */ + + + fs.stat(pluginInstallPath).then(async (err) => { + const files = await fs.readdir(pluginInstallPath); + + for (let file of files){ + const moduleName = path.basename(file); + try { + await fs.access(path.join(node_modules, moduleName), fs.constants.F_OK); + logger.debug(`plugin ${moduleName} already exists in node_modules`); + } catch (err) { + // Create symlink to node_modules + logger.debug(`create symlink for ${file} to ${path.join(node_modules,moduleName)}`) + await fs.symlink(path.join(pluginInstallPath,file), path.join(node_modules,moduleName), 'dir') + } + } + }).catch(()=>{ + logger.debug('plugin directory does not exist'); + }) const fileContent = await fs.readFile(installedPluginsPath); const installedPlugins = JSON.parse(fileContent.toString()); for (const plugin of installedPlugins.plugins) { if (plugin.name.startsWith(plugins.prefix) && plugin.name !== 'ep_etherpad-lite') { - await exports.manager.install(plugin.name, plugin.version); + await linkInstaller.installPlugin(plugin.name, plugin.version); } } }; const persistInstalledPlugins = async () => { - const installedPlugins = {plugins: []}; - for (const pkg of Object.values(await plugins.getPackages())) { + const installedPlugins:{ + plugins: PackageData[] + } = {plugins: []}; + for (const pkg of Object.values(await plugins.getPackages()) as PackageData[]) { installedPlugins.plugins.push({ name: pkg.name, version: pkg.version, @@ -94,50 +137,51 @@ const persistInstalledPlugins = async () => { await fs.writeFile(installedPluginsPath, JSON.stringify(installedPlugins)); }; -exports.uninstall = async (pluginName, cb = null) => { +export const uninstall = async (pluginName: string, cb:Function|null = null) => { cb = wrapTaskCb(cb); logger.info(`Uninstalling plugin ${pluginName}...`); - await exports.manager.uninstall(pluginName); + + await linkInstaller.uninstallPlugin(pluginName); logger.info(`Successfully uninstalled plugin ${pluginName}`); await hooks.aCallAll('pluginUninstall', {pluginName}); cb(null); }; -exports.install = async (pluginName, cb = null) => { +export const install = async (pluginName: string, cb:Function|null = null) => { cb = wrapTaskCb(cb); logger.info(`Installing plugin ${pluginName}...`); - await exports.manager.install(pluginName); + await linkInstaller.installPlugin(pluginName); logger.info(`Successfully installed plugin ${pluginName}`); await hooks.aCallAll('pluginInstall', {pluginName}); cb(null); }; -exports.availablePlugins = null; +export let availablePlugins:MapArrayType|null = null; let cacheTimestamp = 0; -exports.getAvailablePlugins = (maxCacheAge) => { +export const getAvailablePlugins = (maxCacheAge: number|false) => { const nowTimestamp = Math.round(Date.now() / 1000); - return new Promise(async (resolve, reject) => { + return new Promise>(async (resolve, reject) => { // check cache age before making any request - if (exports.availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) { - return resolve(exports.availablePlugins); + if (availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) { + return resolve(availablePlugins); } await axios.get('https://static.etherpad.org/plugins.json', {headers}) - .then((pluginsLoaded) => { - exports.availablePlugins = pluginsLoaded.data; + .then((pluginsLoaded:AxiosResponse>) => { + availablePlugins = pluginsLoaded.data; cacheTimestamp = nowTimestamp; - resolve(exports.availablePlugins); + resolve(availablePlugins); }) .catch(async (err) => reject(err)); }); }; -exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then( - (results) => { - const res = {}; +export const search = (searchTerm: string, maxCacheAge: number) => getAvailablePlugins(maxCacheAge).then( + (results: MapArrayType) => { + const res:MapArrayType = {}; if (searchTerm) { searchTerm = searchTerm.toLowerCase(); diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index db55c1c30..61c28ae7f 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -8,7 +8,6 @@ const runCmd = require('../../../node/utils/run_cmd'); const tsort = require('./tsort'); const pluginUtils = require('./shared'); const defs = require('./plugin_defs'); -const {manager} = require('./installer'); const settings = require('../../../node/utils/Settings'); const logger = log4js.getLogger('plugins'); @@ -122,7 +121,8 @@ exports.update = async () => { }; exports.getPackages = async () => { - const plugins = manager.list(); + const {linkInstaller} = require("./installer"); + const plugins = await linkInstaller.listPlugins(); const newDependencies = {}; for (const plugin of plugins) { diff --git a/src/tsconfig.json b/src/tsconfig.json index 179aae429..a42ef0188 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,12 +6,13 @@ /* Language and Environment */ "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "CommonJS", /* Specify what module code is generated. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ /* Completeness */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "resolveJsonModule": true } }