@samitouri / QOSami-HFS / commits / 6e2b1822

plugins: beforePlugin, afterPlugin

Massimo Melina committed Nov 13, 2024 at 22:18 UTC 6e2b182257e1a1c9c2527ae93ff5dab44e23bb02
2 files changed +47 -24
dev-plugins.md
+6 -2
@@ -55,6 +55,8 @@ All the following properties are optional unless otherwise specified.
55 Special values "light" and "dark" to declare the theme is (for example) dark and forces HFS to use dark-theme as a base.
56 - `preview: string | string[]` one or more URLs to images you want to show before your plugin is downloaded. (JSON syntax)
57 - `depend: { repo: string, version: number }[]` declare what other plugins this depends on. (JSON syntax)
58 +- `beforePlugin: string` control the order this plugin is executed relative to another
59 +- `afterPlugin: string` control the order this plugin is executed relative to another
60 - `repo: string | object` pointer to a GitHub repo where this plugin is hosted. (JSON syntax)
61 - the string form is for GitHub repos. Example: "rejetto/file-icons"
62 - the object form will point to other custom repo. Object properties:
@@ -130,7 +132,7 @@ used must be strictly JSON (thus, no single quotes, only double quotes for strin
132 ### FieldDescriptor
133
134 Currently, these properties are supported:
133 -- `type: 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'real_path' | 'vfs_path' | 'array' | 'username'` . Default is `string`.
135 +- `type: 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'real_path' | 'vfs_path' | 'array' | 'username' | 'color'` . Default is `string`.
136 - `label: string` what name to display next to the field. Default is based on `key`.
137 - `defaultValue: any` value to be used when nothing is set.
138 - `helperText: string` extra text printed next to the field.
@@ -676,7 +678,9 @@ If you want to override a text regardless of the language, use the special langu
678 ## API version history
679
680 - 10 (v0.55.0)
679 - - HFS.copyTextToClipboard
681 + - HFS.copyTextToClipboard
682 + - exports.beforePlugin + afterPlugin
683 + - config.type: color
684 - 9.6 (v0.54.0)
685 - frontend event: showPlay
686 - api.addBlock
src/plugins.ts
+41 -22
@@ -32,10 +32,10 @@ export const PATH = 'plugins'
32 export const DISABLING_SUFFIX = '-disabled'
33 export const STORAGE_FOLDER = 'storage'
34
35 -const plugins: Record<string, Plugin> = {}
35 +const plugins = new Map<string, Plugin>() // now that we care about the order, a simple object wouldn't do, because numbers are always at the beginning
36
37 export function isPluginRunning(id: string) {
38 - return Boolean(plugins[id]?.started)
38 + return Boolean(plugins.get(id)?.started)
39 }
40
41 export function isPluginEnabled(id: string) {
@@ -88,13 +88,15 @@ export function setPluginConfig(id: string, changes: Dict | null) {
88 }
89
90 export function getPluginInfo(id: string) {
91 - const running = plugins[id]?.getData()
91 + const running = plugins.get(id)?.getData()
92 return running && Object.assign(running, {id}) || availablePlugins[id]
93 }
94
95 export function findPluginByRepo<T>(repo: string) {
96 - return _.find(plugins, pl => match(pl.getData()))
97 - || _.find(availablePlugins, match)
96 + for (const pl of plugins.values())
97 + if (match(pl.getData()))
98 + return pl
99 + return _.find(availablePlugins, match)
100
101 function match(rec: any) {
102 return repo === (rec?.repo?.main ?? rec?.repo)
@@ -102,7 +104,7 @@ export function findPluginByRepo<T>(repo: string) {
104 }
105
106 export function getPluginConfigFields(id: string) {
105 - return plugins[id]?.getData().config
107 + return plugins.get(id)?.getData().config
108 }
109
110 async function initPlugin<T>(pl: any, morePassedToInit?: T) {
@@ -160,10 +162,10 @@ export const pluginsMiddleware: Koa.Middleware = async (ctx, next) => {
162 if (path.startsWith(PLUGINS_PUB_URI)) {
163 const a = path.substring(PLUGINS_PUB_URI.length).split('/')
164 const name = a.shift()!
163 - if (plugins.hasOwnProperty(name)) { // do it only if the plugin is loaded
165 + if (plugins.has(name)) { // do it only if the plugin is loaded
166 if (ctx.get('referer')?.endsWith('/'))
167 ctx.state.considerAsGui = true
166 - await serveFile(ctx, plugins[name]!.folder + '/public/' + a.join('/'), MIME_AUTO)
168 + await serveFile(ctx, plugins.get(name)!.folder + '/public/' + a.join('/'), MIME_AUTO)
169 }
170 return
171 }
@@ -211,7 +213,22 @@ export class Plugin implements CommonPluginInterface {
213 console.warn('invalid', k)
214 }
215 }
214 - plugins[id] = this
216 + plugins.set(id, this)
217 +
218 + const keys = Array.from(plugins.keys())
219 + const idx = keys.indexOf(id)
220 + const moveDown = onlyTruthy(mapPlugins(((pl, plId, plIdx) => pl.afterPlugin === id && plIdx < idx && plId)))
221 + const {beforePlugin, afterPlugin} = data // or this plugin that wants to be considered before another
222 + if (afterPlugin && keys.indexOf(afterPlugin) > idx)
223 + moveDown.push(id)
224 + if (beforePlugin && keys.indexOf(beforePlugin) < idx)
225 + moveDown.push(beforePlugin)
226 + for (const k of moveDown) {
227 + const temp = plugins.get(k)
228 + if (!temp) continue
229 + plugins.delete(k)
230 + plugins.set(k, temp)
231 + }
232 }
233 get version(): undefined | number { return this.data?.version }
234 get description(): undefined | string { return this.data?.description }
@@ -219,6 +236,8 @@ export class Plugin implements CommonPluginInterface {
236 get isTheme(): undefined | boolean { return this.data?.isTheme }
237 get repo(): undefined | Repo { return this.data?.repo }
238 get depend(): undefined | Depend { return this.data?.depend }
239 + get afterPlugin(): undefined | string { return this.data?.afterPlugin }
240 + get beforePlugin(): undefined | string { return this.data?.beforePlugin }
241
242 get middleware(): undefined | PluginMiddleware {
243 return this.data?.middleware
@@ -269,13 +288,11 @@ const serverCode = defineConfig('server_code', '', async (script, { k }) => {
288 }
289 })
290
272 -let serverCodePlugin: void | Plugin
273 -serverCode.sub(() => serverCode.compiled()?.then(x => serverCodePlugin = x))
274 -export function mapPlugins<T>(cb:(plugin:Readonly<Plugin>, pluginName:string)=> T, includeServerCode=true) {
275 - const entries = Object.entries(plugins)
276 - return entries.map(([plName,pl]) => {
291 +export function mapPlugins<T>(cb:(plugin:Readonly<Plugin>, pluginName:string, idx:number)=> T, includeServerCode=true) {
292 + let i = 0
293 + return Array.from(plugins).map(([plName,pl]) => {
294 if (!includeServerCode && plName === SERVER_CODE_ID) return
278 - try { return cb(pl,plName) }
295 + try { return cb(pl,plName,i++) }
296 catch(e) {
297 console.log('plugin error', plName, String(e))
298 }
@@ -283,7 +300,7 @@ export function mapPlugins<T>(cb:(plugin:Readonly<Plugin>, pluginName:string)=>
300 }
301
302 export function firstPlugin<T>(cb:(plugin:Readonly<Plugin>, pluginName:string)=> T, includeServerCode=true) {
286 - for (const [plName,pl] of Object.entries(plugins)) {
303 + for (const [plName, pl] of plugins.entries()) {
304 if (!includeServerCode && plName === SERVER_CODE_ID) continue
305 try {
306 const ret = cb(pl,plName)
@@ -367,10 +384,11 @@ function watchPlugin(id: string, path: string) {
384 const unsub = enablePlugins.sub(() => { // we take care of enabled-state after it was loaded
385 if (!getPluginInfo(id)) return // not loaded yet
386 const enabled = isPluginEnabled(id)
370 - if (enabled !== isPluginRunning(id))
371 - return enabled ? start() : stop()
387 + if (enabled === isPluginRunning(id)) return
388 + if (enabled) start()
389 + else stop()
390 })
373 - const { unwatch } = watchLoad(module, async (source) => {
391 + const { unwatch } = watchLoad(module, async source => {
392 const notRunning = availablePlugins[id]
393 if (!source)
394 return onUninstalled()
@@ -396,7 +414,7 @@ function watchPlugin(id: string, path: string) {
414 }
415
416 async function markItAvailable() {
399 - delete plugins[id]
417 + plugins.delete(id)
418 availablePlugins[id] = await parsePlugin()
419 }
420
@@ -406,7 +424,7 @@ function watchPlugin(id: string, path: string) {
424
425 async function stop() {
426 await starting
409 - const p = plugins[id]
427 + const p = plugins.get(id)
428 if (!p) return
429 await p.unload()
430 await markItAvailable()
@@ -423,7 +441,7 @@ function watchPlugin(id: string, path: string) {
441 throw Error("plugin missing dependencies: " + _.map(getMissingDependencies(info), x => x.repo).join(', '))
442 if (getPluginInfo(id))
443 setError(id, '')
426 - const alreadyRunning = plugins[id]
444 + const alreadyRunning = plugins.get(id)
445 console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id)
446 const pluginData = require(module)
447 deleteModule(require.resolve(module)) // avoid caching at next import
@@ -479,6 +497,7 @@ function watchPlugin(id: string, path: string) {
497 await Promise.allSettled(dbs.map(x => x.close()))
498 dbs.length = 0
499 })
500 +
501 if (alreadyRunning)
502 events.emit('pluginUpdated', Object.assign(_.pick(plugin, 'started'), getPluginInfo(id)))
503 else {