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 {