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 }