config: disable_plugins and plugins_config
Massimo Melina committed
Jan 12, 2022 at 15:00 UTC
51cd1ec0bbde55db26189c45adc84c7ea700c678
10 files changed
+52
-36
README.md
+12
-12
@@ -118,21 +118,21 @@ but nothing is preventing a single plug-in from doing both tasks.
118
119
## For plug-in makers
120
121
-A plug-in must have a `plugin.yaml` file, even if empty.
122
-Supported keys are:
121
+A plug-in must have a `plugin.js` file in its own folder.
122
+This file is javascript module that is supposed to expose one or more of the supported keys:
123
124
-- `frontend_css` path to one or more css files that you want the frontend to load.
124
+- `frontend_css` path to one or more css files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
125
126
-- `frontend_js` path to one or more js files that you want the frontend to load.
126
+- `frontend_js` path to one or more js files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
127
128
-Each plug-in can have a `public` folder, and its files will be accessible at `/~/plugins/PLPUGIN_NAME/FILENAME`.
128
+- `middleware: function(Context): undefined | true` a function that will be used as a middleware: it can interfere with http activity.
129
130
-If a file `plugin.js` is also present, it will be required as a javascript module.
131
-The js module can export some properties. Supported ones are:
130
+ To know what the Context object contains please refer to [Koa's documentation](https://github.com/koajs/koa/blob/master/docs/api/context.md).
131
+ You don't get the `next` parameter as in standard Koa's middlewares because this is different, but we are now explaining how to achieve the same results.
132
+ To interrupt other middlewares on this http request, return `true`.
133
+ If you want to execute something in the "upstream" of middlewares, return a function.
134
133
-- `middleware: function(Context): undefined | true` a function that will be used as a middleware: it can interfere with http activity.
135
+Each plug-in can have a `public` folder, and its files will be accessible at `/~/plugins/PLUGIN_NAME/FILENAME`.
136
135
- To know what the Context contains please refer to [Koa's documentation](https://github.com/koajs/koa/blob/master/docs/api/context.md).
136
- If the function returns `true`, other executions on this http request will be interrupted.
137
- Return another function if you want to execute it in the "upstream" of middlewares.
138
-
137
+If your plugin need to get some configuration, it should require the `getPluginConfig(pluginName:string)` function.
138
+The content will be read from the main config file, under the `plugins_config` property.
config.yaml
+1
@@ -5,6 +5,7 @@
5
#error_log:
6
mime:
7
"*.jpg|*.png|*.mp3|*.txt": auto
8
+disable_plugins: [ 'theme-example', 'middleware-example' ]
9
vfs:
10
children:
11
- name: f1
plugins/middleware-example-disabled/plugin.js
deleted
-4
@@ -1,4 +0,0 @@
1
-exports.middleware = function(ctx) {
2
- ctx.body = 'This plugin is stopping you ;)'
3
- return true // true = please stop
4
-}
plugins/middleware-example/plugin.js
new
+6
@@ -0,0 +1,6 @@
1
+const api = exports.api = {}
2
+
3
+exports.middleware = function(ctx) {
4
+ ctx.body = 'This plugin is stopping you: ' + api.getConfig('message')
5
+ return true // true = please stop
6
+}
plugins/theme-example/plugin.js
renamed
plugins/theme-example/public/star.svg
renamed
plugins/theme-example/public/style.css
renamed
plugins/theme-example/public/test.js
renamed
src/misc.ts
+1
-1
@@ -44,7 +44,7 @@ export async function readFileBusy(path: string): Promise<string> {
44
})
45
}
46
47
-export function wantArray(x:any) {
47
+export function wantArray<T>(x?: void | T | T[]) {
48
return x == null ? [] : Array.isArray(x) ? x : [x]
49
}
50
src/plugins.ts
+32
-19
@@ -2,11 +2,12 @@ import { createReadStream, watch } from 'fs'
2
import glob from 'fast-glob'
3
import { watchLoad } from './watchLoad'
4
import _ from 'lodash'
5
-import path from 'path'
5
+import { resolve } from 'path'
6
import { PLUGINS_PUB_URI } from './const'
7
import mime from 'mime-types'
8
import Koa from 'koa'
9
import { getOrSet, onProcessExit, wantArray } from './misc'
10
+import { getConfig, subscribeConfig } from './config'
11
12
const PATH = 'plugins'
13
@@ -18,7 +19,6 @@ export function mapPlugins<T>(cb:(plugin:Readonly<Plugin>, pluginKey:string)=> T
19
20
export function pluginsMiddleware(): Koa.Middleware {
21
return async (ctx, next) => {
21
- const { path } = ctx
22
const after = []
23
// run middleware plugins
24
for (const k in plugins)
@@ -31,14 +31,18 @@ export function pluginsMiddleware(): Koa.Middleware {
31
after.push(res)
32
}
33
catch(e){
34
- console.log('error middleware plugin', k)
34
+ console.log('error middleware plugin', k, String(e))
35
+ console.debug(e)
36
}
37
// expose public plugins' files
37
- if (path.startsWith(PLUGINS_PUB_URI)) {
38
+ const { path } = ctx
39
+ if (!ctx.pluginStopped && path.startsWith(PLUGINS_PUB_URI)) {
40
const a = path.substring(PLUGINS_PUB_URI.length).split('/')
39
- a.splice(1,0,'public')
40
- ctx.type = mime.lookup(path) || ''
41
- return ctx.body = createReadStream(PATH + '/' + a.join('/'))
41
+ if (plugins.hasOwnProperty(a[0])) { // do it only if the plugin is loaded
42
+ a.splice(1,0,'public')
43
+ ctx.type = mime.lookup(path) || ''
44
+ ctx.body = createReadStream(resolve(PATH, a.join('/')))
45
+ }
46
}
47
if (!ctx.pluginStopped)
48
await next()
@@ -47,25 +51,28 @@ export function pluginsMiddleware(): Koa.Middleware {
51
}
52
}
53
50
-try {
51
- const debounced = _.debounce(rescan, 1000)
52
- watch(PATH, debounced)
53
- debounced()
54
-}
55
-catch(e){
56
- console.debug('plugins not found')
57
-}
54
+subscribeConfig({ k:'disable_plugins', defaultValue:[] }, () => {
55
+ try {
56
+ const debounced = _.debounce(rescan, 1000)
57
+ watch(PATH, debounced)
58
+ debounced()
59
+ }
60
+ catch(e){
61
+ console.debug('plugins not found')
62
+ }
63
+})
64
65
class Plugin {
66
js: any
67
constructor(readonly k:string, private data:any, private unwatch:()=>void){
62
- if (!data) return
68
+ if (!data) throw 'invalid data'
69
// if a previous instance is present, we are going to overwrite it, but first call its unload callback
70
try { plugins[k]?.data?.unload?.() }
71
catch(e){
72
console.debug('error unloading plugin', k, String(e))
73
}
74
plugins[k] = this // track this
75
+ this.data = data = { ...data } // clone to make object modifiable. Objects coming from import are not.
76
// some validation
77
for (const k of ['frontend_css', 'frontend_js']) {
78
const v = data[k]
@@ -105,21 +112,27 @@ type CallMeAfter = ()=>void
112
async function rescan() {
113
console.debug('scanning plugins')
114
const found = []
115
+ const disable_plugins = wantArray(getConfig('disable_plugins'))
116
for (let f of await glob(PATH+'/*/plugin.js')) {
117
const k = f.split('/').slice(-2)[0]
110
- if (k.endsWith('-disabled')) continue
118
+ if (k.endsWith('-disabled') || disable_plugins.includes(k)) continue
119
found.push(k)
120
if (plugins[k]) // already loaded
121
continue
114
- f = path.resolve(f) // without this, import won't work
122
+ f = resolve(f) // without this, import won't work
123
const unwatch = watchLoad(f, async () => {
124
try {
125
console.log('loading plugin', k)
126
const data = await import(f)
127
deleteModule(require.resolve(f)) // avoid caching
128
new Plugin(k, data, unwatch)
129
+ if (data.api)
130
+ Object.assign(data.api, {
131
+ getConfig: (cfgKey: string) =>
132
+ getConfig('plugins_config')?.[k]?.[cfgKey]
133
+ })
134
} catch (e) {
122
- console.log('plugin error importing', k, e)
135
+ console.log('plugin error:', e)
136
}
137
})
138
}