diff --git a/src/bin/installPlugins.js b/src/bin/installPlugins.js deleted file mode 100644 index 6c3e6393b..000000000 --- a/src/bin/installPlugins.js +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const {promises: fs} = require('fs'); -const pluginsModule = require('../static/js/pluginfw/plugins'); -const installer = require('../static/js/pluginfw/installer'); - -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 installedPlugins = {plugins: []}; - for (const pkg of Object.values(await pluginsModule.getPackages())) { - installedPlugins.plugins.push({ - name: pkg.name, - version: pkg.version, - }); - } - installedPlugins.plugins = [...new Set(installedPlugins.plugins)]; - await fs.writeFile(installer.installedPluginsPath, JSON.stringify(installedPlugins)); -}; - -async function run() { - for (const plugin of plugins) { - await installer.manager.install(plugin); - } -} - -(async () => { - await run(); - await persistInstalledPlugins(); -})() \ No newline at end of file diff --git a/src/bin/installPlugins.ts b/src/bin/installPlugins.ts new file mode 100644 index 000000000..c390ff5ff --- /dev/null +++ b/src/bin/installPlugins.ts @@ -0,0 +1,37 @@ +'use strict'; + +import {writeFileSync} from 'fs' +import {manager, installedPluginsPath} from "../static/js/pluginfw/installer"; +import {PackageData} from "../node/types/PackageInfo"; + +const pluginsModule = require('../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/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..4c9d16833 100755 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -30,6 +30,8 @@ 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 +55,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 +148,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 9f9bba12e..306c9b761 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", @@ -117,7 +118,7 @@ "test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api", "dev": "node --import tsx node/server.ts", "prod": "node --import tsx node/server.ts", - "install-plugins": "node --import tsx bin/installPlugins.js", + "install-plugins": "node --import tsx bin/installPlugins.ts", "ts-check": "tsc --noEmit", "ts-check:watch": "tsc --noEmit --watch", "test-ui": "npx playwright test tests/frontend-new/specs", 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/installer.js b/src/static/js/pluginfw/installer.ts similarity index 65% rename from src/static/js/pluginfw/installer.js rename to src/static/js/pluginfw/installer.ts index 22da6b233..3b0962148 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.ts @@ -1,20 +1,27 @@ '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'); + const {findEtherpadRoot} = require('../../../node/utils/AbsolutePaths'); const logger = log4js.getLogger('plugins'); -exports.manager = new PluginManager(); +export const manager = new PluginManager(); -exports.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 +37,10 @@ const headers = { let tasks = 0; -const wrapTaskCb = (cb) => { +const wrapTaskCb = (cb:Function|null) => { tasks++; - return (...args) => { + return (...args: any) => { cb && cb(...args); tasks--; if (tasks === 0) onAllTasksFinished(); @@ -53,91 +60,94 @@ 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 manager.installFromPath(`${findEtherpadRoot()}/node_modules/${pkg}`); } else { - await exports.manager.install(pkg); + await manager.install(pkg); } })); await persistInstalledPlugins(); }; -exports.checkForMigration = async () => { +export const checkForMigration = async () => { logger.info('check installed plugins for migration'); try { - await fs.access(exports.installedPluginsPath, fs.constants.F_OK); + await fs.access(installedPluginsPath, fs.constants.F_OK); } catch (err) { await migratePluginsFromNodeModules(); } - const fileContent = await fs.readFile(exports.installedPluginsPath); + 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 manager.install(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, }); } installedPlugins.plugins = [...new Set(installedPlugins.plugins)]; - await fs.writeFile(exports.installedPluginsPath, JSON.stringify(installedPlugins)); + 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 manager.uninstall(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 manager.install(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();