mikser-io 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ import handlebars from 'handlebars'
2
+ import helpers from 'handlebars-helpers'
3
+ import { readFile } from 'fs/promises'
4
+
5
+ export function load({ config, runtime, context }) {
6
+ handlebars.registerHelper(helpers(config?.helpers || [
7
+ 'array',
8
+ 'collection',
9
+ 'object',
10
+ 'comparison',
11
+ 'date',
12
+ 'markdown',
13
+ 'match',
14
+ 'math',
15
+ 'number',
16
+ 'regex',
17
+ 'string',
18
+ 'url'
19
+ ]))
20
+ for (let partial in context.layouts) {
21
+ if (context.layouts[partial].template == 'hbs' && partial.indexOf('partials') == 0) {
22
+ const partialLayout = readFile(context.layouts[partial].uri, 'utf8')
23
+ handlebars.registerPartial(partial, partialLayout)
24
+ }
25
+ }
26
+ handlebars.registerHelper('url', function(obj, options) {
27
+ // Called as {{url}} with no args — obj is the Handlebars options object,
28
+ // so read url from the current context (this)
29
+ if (!options) return this?.url ?? ''
30
+ if (!obj) return ''
31
+ if (typeof obj === 'string') return obj
32
+ return obj.url ?? ''
33
+ })
34
+
35
+ runtime.hbs = (source, sandbox) => {
36
+ const template = handlebars.compile(source)
37
+ return template(sandbox)
38
+ }
39
+ }
40
+
41
+ export async function render({ entity, runtime }) {
42
+ const source = await readFile(entity.layout.uri, 'utf8')
43
+ const sandbox = {}
44
+ for (let helper in runtime) {
45
+ if (typeof (runtime[helper]) == 'function') {
46
+ handlebars.registerHelper(helper, runtime[helper])
47
+ } else {
48
+ sandbox[helper] = runtime[helper]
49
+ }
50
+ }
51
+ return runtime.hbs(source, sandbox)
52
+ }
@@ -0,0 +1,45 @@
1
+ import path from 'node:path'
2
+
3
+ export function load({ entity, runtime, state, options }) {
4
+ const { clear } = options
5
+
6
+ runtime.hrefLang = (href) => {
7
+ const { sitemap } = state.layouts
8
+ return sitemap[href]
9
+ }
10
+
11
+ runtime.hrefLangPage = (href, page) => {
12
+ if (page > 1) href += `.${page}`
13
+ return sitemap[href]
14
+ }
15
+
16
+ runtime.href = (href, lang) => {
17
+ if (!href || typeof href != 'string') return
18
+ if (typeof lang != 'string') lang = undefined
19
+
20
+ if (href.indexOf('http') == 0) return href
21
+ lang ||= entity.meta?.lang
22
+
23
+ let found = runtime.hrefLang(href)
24
+ if (!found) {
25
+ const from = path.dirname(entity.destination || '/')
26
+ return { url: path.relative(from, href) }
27
+ } else {
28
+ if (!found.id) {
29
+ found = found[lang]
30
+ }
31
+ if (found?.destination) {
32
+ const destination = clear ? found.destination.replace('index.html', '') : found.destination
33
+ const from = path.dirname(entity.destination || '/')
34
+ found.url = path.relative(from, destination)
35
+ }
36
+ return found
37
+ }
38
+ }
39
+
40
+ runtime.hrefPage = (href, page, lang) => {
41
+ }
42
+
43
+ runtime.prev = entity.page > 1 ? entity.page - 1 : false
44
+ runtime.next = entity.page + 1 < entity.pages ? entity.page + 1 : false
45
+ }
@@ -0,0 +1,13 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ export async function load({ entity, runtime }) {
5
+ const preset = await import(`${entity.preset.uri}?stamp=${Date.now()}`)
6
+ runtime.preset = preset.default
7
+ }
8
+
9
+ export async function render({ entity, options, config, context, plugins, runtime, state, logger }) {
10
+ await mkdir(path.dirname(entity.destination), { recursive: true })
11
+ await runtime.preset({ entity, options, config, context, plugins, runtime, state, logger })
12
+ return entity.destination
13
+ }
@@ -0,0 +1,17 @@
1
+ import path from 'node:path'
2
+
3
+ export function load({ runtime, entity, state, options }) {
4
+ runtime.resource = (url) => {
5
+ const { resourceLib } = state.resources
6
+ for (let library in resourceLib) {
7
+ if (url.match(library)) {
8
+ const { origin } = new URL(url)
9
+ const name = url.replace(origin, `${resourceLib[library]}`)
10
+ const relative = url.replace(origin, `${state.resources.resourcesFolder}/${resourceLib[library]}`)
11
+ const destination = '/' + relative
12
+ const from = path.dirname(entity.destination || '/')
13
+ return { url: path.relative(from, destination), name }
14
+ }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,216 @@
1
+ import { mkdir, symlink, rename, unlink } from 'node:fs/promises'
2
+ import { createWriteStream } from 'node:fs'
3
+ import lodash from 'lodash'
4
+ import deepdash from 'deepdash'
5
+ import axios from 'axios'
6
+ import path from 'node:path'
7
+ import { globby } from 'globby'
8
+ import escapeStringRegexp from 'escape-string-regexp'
9
+ import * as stream from 'stream'
10
+ import { promisify } from 'util'
11
+ import isUrl from 'is-url'
12
+ import map from 'p-map'
13
+
14
+ export default ({
15
+ useLogger,
16
+ useJournal,
17
+ onLoaded,
18
+ runtime,
19
+ stopProgress,
20
+ createEntity,
21
+ onProcessed,
22
+ onFinalize,
23
+ checksum,
24
+ trackProgress,
25
+ updateProgress,
26
+ updateEntry,
27
+ constants: { OPERATION },
28
+ }) => {
29
+ const collection = 'resources'
30
+ const type = 'resource'
31
+
32
+ const _ = deepdash(lodash)
33
+
34
+ const finishedDownload = promisify(stream.finished)
35
+
36
+ onLoaded(async () => {
37
+ const logger = useLogger()
38
+
39
+ const resourcesName = runtime.config.resources?.resourcesFolder || collection
40
+ runtime.state.resources = {
41
+ resourceLib: {},
42
+ resourceMap: {},
43
+ resourcesFolder: runtime.config.resources?.outputFolder
44
+ ? path.join(runtime.config.resources.outputFolder, resourcesName)
45
+ : resourcesName,
46
+ }
47
+
48
+ runtime.options.resources = runtime.config.resources?.resourcesFolder || collection
49
+ runtime.options.resourcesFolder = path.join(runtime.options.workingFolder, runtime.options.resources)
50
+ logger.info('Resources folder: %s', runtime.options.resourcesFolder)
51
+
52
+ for (let library in (runtime.config.resources?.libraries || [])) {
53
+ let resource = runtime.config.resources.libraries[library]
54
+ runtime.state.resources.resourceLib[resource.match || escapeStringRegexp(resource.url)] = library
55
+ }
56
+ })
57
+
58
+ onProcessed(async (signal) => {
59
+ const logger = useLogger()
60
+ const { resourceLib, resourceMap } = runtime.state.resources
61
+
62
+ for await (let { id, entity } of useJournal('Resources provision', [OPERATION.CREATE, OPERATION.UPDATE], signal)) {
63
+ if (entity.collection != collection && entity.meta) {
64
+ resourceMap[entity.id] = []
65
+ _.eachDeep(entity.meta, resource => {
66
+ if (typeof resource == 'string') {
67
+ for (let library in resourceLib) {
68
+ const match = new RegExp(library)
69
+ if (resource.match(match)) {
70
+ resourceMap[entity.id].push({ library, resource, entity })
71
+ }
72
+ }
73
+ }
74
+ })
75
+ entity.resources = resourceMap[entity.id].map(({ resource }) => resource)
76
+ await updateEntry({ id, entity })
77
+ }
78
+ }
79
+ const resources = [].concat(...Object.values(resourceMap))
80
+ resources.length && logger.info('Resources: %d', resources.length)
81
+
82
+ const resourceDownloads = {}
83
+ const localResources = new Set()
84
+ trackProgress('Resources processing', resources.length)
85
+ for (let { library, resource, entity } of resources) {
86
+ if (signal?.aborted) {
87
+ stopProgress()
88
+ break
89
+ }
90
+
91
+ library = resourceLib[library]
92
+ if (isUrl(resource)) {
93
+ if (!resourceDownloads[resource]) {
94
+ resourceDownloads[resource] = { library, entity }
95
+ }
96
+ } else {
97
+ try {
98
+ const id = resource.indexOf(`/${library}`) == 0 ? resource : path.join(`/${library}`, resource)
99
+ if (!localResources.has(id)) {
100
+ await createEntity({
101
+ id,
102
+ uri: path.join(runtime.options.workingFolder, resource),
103
+ collection,
104
+ type,
105
+ format: path.extname(resource).substring(1).toLowerCase(),
106
+ name: resource.indexOf('/') == 0 ? resource.substring(1) : resource,
107
+ source: path.join(runtime.options.workingFolder, resource),
108
+ checksum: await checksum(path.join(runtime.options.workingFolder, resource))
109
+ })
110
+ logger.debug('Resource: %s %s', id, resource)
111
+ localResources.add(id)
112
+ }
113
+ } catch (err) {
114
+ logger.error('Resource error: %s %s %s', entity.id, resource, err.message)
115
+ }
116
+ }
117
+ updateProgress()
118
+ }
119
+
120
+ const resourceFiles = await globby('**/*', { cwd: runtime.options.resourcesFolder })
121
+ const resourceFilesMap = new Set()
122
+ for (let resourceFile of resourceFiles) {
123
+ resourceFilesMap.add(resourceFile)
124
+ }
125
+
126
+ const downloads = Object.keys(resourceDownloads)
127
+ if (downloads.length) {
128
+ trackProgress('Resources download', downloads.length)
129
+ await mkdir(runtime.options.resourcesFolder, { recursive: true })
130
+ let link = path.join(runtime.options.outputFolder, runtime.options.resources)
131
+ if (runtime.config.resources?.outputFolder) link = path.join(runtime.options.outputFolder, runtime.config.resources?.outputFolder, runtime.options.resources)
132
+ try {
133
+ await mkdir(path.dirname(link), { recursive: true })
134
+ await symlink(path.resolve(runtime.options.resourcesFolder), link, 'dir')
135
+ } catch (err) {
136
+ if (err.code != 'EEXIST')
137
+ throw err
138
+ }
139
+ let count = 0
140
+ await map(downloads, async url => {
141
+ const { library, entity } = resourceDownloads[url]
142
+ let { pathname } = new URL(url)
143
+ pathname = decodeURI(pathname)
144
+ const resource = path.join(runtime.options.resourcesFolder, library, pathname)
145
+ const uri = path.join(runtime.options.outputFolder, library, pathname)
146
+
147
+ let success = true
148
+ if (!resourceFilesMap.has(path.join(library, pathname))) {
149
+ const resourceTemp = path.join(runtime.options.resourcesFolder, library, pathname + '.temp')
150
+ logger.debug('Downloading resource: %s %s', entity.id, url)
151
+ const config = runtime.config.resources.libraries[library]
152
+ const request = {
153
+ method: 'get',
154
+ ...config,
155
+ url,
156
+ responseType: 'stream',
157
+ signal
158
+ }
159
+
160
+ try {
161
+ count++
162
+ var response = await axios(request)
163
+ } catch (err) {
164
+ success == false
165
+ if (axios.isCancel(err)) {
166
+ logger.trace('Downloading canceled')
167
+ } else {
168
+ logger.error('Resource error: %s %s %s', entity.id, url, err.message)
169
+ }
170
+ return
171
+ }
172
+
173
+ if (response && success) {
174
+ await mkdir(path.dirname(resource), { recursive: true })
175
+ const writer = createWriteStream(resourceTemp)
176
+ response.data.pipe(writer)
177
+ await finishedDownload(writer)
178
+
179
+ logger.debug('Resource: %s %s', entity.id, url)
180
+ await rename(resourceTemp, resource)
181
+ }
182
+ }
183
+
184
+ if (success) {
185
+ await createEntity({
186
+ id: path.join('/resources', library, pathname),
187
+ uri,
188
+ collection,
189
+ type,
190
+ format: path.extname(resource).substring(1).toLowerCase(),
191
+ name: path.join(library, pathname),
192
+ source: resource,
193
+ checksum: await checksum(resource)
194
+ })
195
+ }
196
+ updateProgress()
197
+ }, { concurrency: 10, signal })
198
+ count && logger.info('Downloaded: %d', count)
199
+ }
200
+ })
201
+
202
+ onFinalize(async () => {
203
+ runtime.state.resources.resourceMap = {}
204
+
205
+ const paths = await globby('**/*.temp', { cwd: runtime.options.resourcesFolder })
206
+ for (let relativePath of paths) {
207
+ let resourceTemp = path.join(runtime.options.resourcesFolder, relativePath)
208
+ await unlink(resourceTemp)
209
+ }
210
+ })
211
+
212
+ return {
213
+ collection,
214
+ type
215
+ }
216
+ }
@@ -0,0 +1,143 @@
1
+ import path from 'node:path'
2
+ import { writeFile, unlink, mkdir, access } from 'node:fs/promises'
3
+ import { updateEntity } from '../lifecycle.js'
4
+ import { findEntities } from '../catalog.js'
5
+
6
+ export default ({
7
+ runtime,
8
+ onLoaded,
9
+ useLogger,
10
+ }) => {
11
+ onLoaded(async () => {
12
+ const logger = useLogger()
13
+
14
+ const { default: express } = await import('express').catch(() => {
15
+ throw new Error('express is required for the rest plugin — run: npm install express')
16
+ })
17
+
18
+ const ownApp = !runtime.options.app
19
+ const app = runtime.options.app ?? express()
20
+
21
+ const router = express.Router()
22
+ router.use(express.json())
23
+
24
+ const token = runtime.config.rest?.token
25
+ const auth = (req, res, next) => {
26
+ if (!token) return next()
27
+ if (req.headers.authorization === `Bearer ${token}`) return next()
28
+ res.status(401).json({ error: 'Unauthorized' })
29
+ }
30
+
31
+ function collectionFolder(collection) {
32
+ return runtime.options[`${collection}Folder`]
33
+ }
34
+
35
+ router.get('/entities', async (req, res) => {
36
+ try {
37
+ const { page: rawPage, limit: rawLimit, ...filter } = req.query
38
+ const page = Math.max(1, parseInt(rawPage) || 1)
39
+ const limit = Math.min(100, Math.max(1, parseInt(rawLimit) || runtime.config.rest?.pageSize ?? 10))
40
+ const query = Object.keys(filter).length ? filter : undefined
41
+
42
+ const all = await findEntities(query)
43
+ const total = all.length
44
+ const totalPages = Math.ceil(total / limit)
45
+ const items = all.slice((page - 1) * limit, page * limit)
46
+
47
+ res.json({
48
+ items,
49
+ page,
50
+ limit,
51
+ total,
52
+ totalPages,
53
+ hasNext: page < totalPages,
54
+ hasPrev: page > 1,
55
+ })
56
+ } catch (err) {
57
+ logger.error('REST list error: %s', err.message)
58
+ res.status(500).json({ error: err.message })
59
+ }
60
+ })
61
+
62
+ router.put('/entities', auth, async (req, res) => {
63
+ try {
64
+ const { collection, relativePath, content = '' } = req.body
65
+ const folder = collectionFolder(collection)
66
+ if (!folder) return res.status(400).json({ error: `Unknown collection: ${collection}` })
67
+ const uri = path.join(folder, relativePath)
68
+ await mkdir(path.dirname(uri), { recursive: true })
69
+ await writeFile(uri, content, 'utf8')
70
+ res.status(202).json({ ok: true })
71
+ } catch (err) {
72
+ logger.error('REST update error: %s', err.message)
73
+ res.status(500).json({ error: err.message })
74
+ }
75
+ })
76
+
77
+ router.delete('/entities', auth, async (req, res) => {
78
+ try {
79
+ const { collection, relativePath } = req.body
80
+ const folder = collectionFolder(collection)
81
+ if (!folder) return res.status(400).json({ error: `Unknown collection: ${collection}` })
82
+ const uri = path.join(folder, relativePath)
83
+ await unlink(uri)
84
+ res.status(202).json({ ok: true })
85
+ } catch (err) {
86
+ logger.error('REST delete error: %s', err.message)
87
+ res.status(500).json({ error: err.message })
88
+ }
89
+ })
90
+
91
+ router.post('/render', async (req, res) => {
92
+ try {
93
+ const entity = { ...req.body, _correlationId: crypto.randomUUID() }
94
+ const timeout = runtime.config.rest?.renderTimeout ?? 30_000
95
+
96
+ const output = await new Promise((resolve, reject) => {
97
+ const timer = setTimeout(() => {
98
+ runtime.removeHook('completed', hook)
99
+ reject(new Error(`Render timeout for ${entity.id}`))
100
+ }, timeout)
101
+
102
+ const hook = async (entry) => {
103
+ if (entry.entity._correlationId !== entity._correlationId) return
104
+ clearTimeout(timer)
105
+ runtime.removeHook('completed', hook)
106
+ resolve(entry.output)
107
+ }
108
+ runtime.addHook('completed', hook)
109
+
110
+ updateEntity(entity)
111
+ .then(() => runtime.process())
112
+ .catch(reject)
113
+ })
114
+
115
+ if (output?.result != null) {
116
+ const isFile = await access(output.result).then(() => true).catch(() => false)
117
+ if (isFile) {
118
+ res.sendFile(output.result)
119
+ } else {
120
+ res.send(output.result)
121
+ }
122
+ } else {
123
+ res.status(204).send()
124
+ }
125
+ } catch (err) {
126
+ logger.error('REST render error: %s', err.message)
127
+ res.status(500).json({ error: err.message })
128
+ }
129
+ })
130
+
131
+ const base = runtime.config.rest?.base ?? '/mikser'
132
+ app.use(base, router)
133
+
134
+ if (ownApp) {
135
+ const port = runtime.config.rest?.port ?? 3001
136
+ app.listen(port, () => {
137
+ logger.info('REST plugin listening on port %d', port)
138
+ })
139
+ } else {
140
+ logger.info('REST plugin mounted on %s', base)
141
+ }
142
+ })
143
+ }
@@ -0,0 +1,40 @@
1
+ import { mkdir, symlink, stat } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ export default ({
5
+ useLogger,
6
+ onLoaded,
7
+ runtime
8
+ }) => {
9
+ onLoaded(async () => {
10
+ const logger = useLogger()
11
+
12
+ for (let item of (runtime.config.shares?.locations || [])) {
13
+ let source, destination
14
+ if (typeof item == 'string') {
15
+ source = destination = item
16
+ } else {
17
+ source = item.source
18
+ destination = item.destination
19
+ }
20
+
21
+ let destinationLocation = path.join(runtime.options.outputFolder, destination)
22
+ let destinationFolder = path.dirname(destinationLocation)
23
+ try {
24
+ const sourceLocation = path.join(runtime.options.workingFolder, source)
25
+ const sourceStat = await stat(sourceLocation)
26
+
27
+ await mkdir(destinationFolder, { recursive: true })
28
+ logger.info('Sharing: %s → %s', sourceLocation, destinationLocation)
29
+ if (sourceStat.isDirectory()) {
30
+ await symlink(path.resolve(sourceLocation), destinationLocation, 'dir')
31
+ } else {
32
+ await symlink(path.resolve(sourceLocation), destinationLocation, 'file')
33
+ }
34
+ } catch (err) {
35
+ if (err.code != 'EEXIST')
36
+ throw err
37
+ }
38
+ }
39
+ })
40
+ }
@@ -0,0 +1,19 @@
1
+ import _ from 'lodash'
2
+
3
+ export default ({
4
+ onLoad,
5
+ onValidate,
6
+ runtime,
7
+ matchEntity,
8
+ constants: { OPERATION },
9
+ }) => {
10
+ onLoad(() => {
11
+ for (let { match, validate, operations = [OPERATION.CREATE, OPERATION.UPDATE] } of runtime.config.validator?.validators || []) {
12
+ onValidate(operations, async entry => {
13
+ if (entity.meta && matchEntity(entry.entity, match)) {
14
+ return await validate(entry.entity)
15
+ }
16
+ })
17
+ }
18
+ })
19
+ }
@@ -0,0 +1,26 @@
1
+ import YAML from 'yaml'
2
+
3
+ export default ({
4
+ onProcess,
5
+ useLogger,
6
+ useJournal,
7
+ updateEntry,
8
+ constants: { OPERATION },
9
+ }) => {
10
+ onProcess(async (signal) => {
11
+ const logger = useLogger()
12
+
13
+ for await (let { id, entity } of useJournal('Yaml', [OPERATION.CREATE, OPERATION.UPDATE], signal)) {
14
+ if (entity.content && (entity.format == 'yml' || entity.format == 'yaml')) {
15
+ try {
16
+ entity.meta = Object.assign(entity.meta || {}, YAML.parse(entity.content))
17
+ delete entity.content
18
+ logger.trace('Yaml %s: %s', entity.collection, entity.id)
19
+ await updateEntry({ id, entity })
20
+ } catch (err) {
21
+ logger.error('Yaml error %s: %s %s', entity.collection, entity.id, err.message)
22
+ }
23
+ }
24
+ }
25
+ })
26
+ }
package/src/plugins.js ADDED
@@ -0,0 +1,54 @@
1
+ import { useLogger } from './engine.js'
2
+ import { onLoad } from './lifecycle.js'
3
+ import runtime from './runtime.js'
4
+ import path from 'node:path'
5
+ import fs from 'fs'
6
+
7
+ import * as core from '../index.js'
8
+
9
+ export async function loadPlugin(pluginName) {
10
+ const logger = useLogger()
11
+
12
+ const resolveLocations = [
13
+ path.join(path.dirname(import.meta.url), 'plugins', `${pluginName}.js`),
14
+ path.join(runtime.options.workingFolder, 'plugins', `${pluginName}.js`),
15
+ path.join(runtime.options.workingFolder, 'node_modules', `mikser-io-${pluginName}`, 'index.js'),
16
+ ]
17
+ for (let index = 0; index < resolveLocations.length; index++) {
18
+ const resolveLocation = resolveLocations[index]
19
+ if (fs.existsSync(resolveLocation.replace('file:', ''))) {
20
+ try {
21
+ const plugin = await import(resolveLocation)
22
+ const pluginRuntime = plugin.default(core)
23
+ runtime.engine[pluginName] = pluginRuntime
24
+ if (pluginRuntime) {
25
+ logger.trace('Loaded %s plugin: %s', pluginName, pluginRuntime)
26
+ } else {
27
+ logger.trace('Loaded %s plugin', pluginName)
28
+ }
29
+ return
30
+ } catch (err) {
31
+ logger.error('Plugin load error: [%s] %s', pluginName, err.message)
32
+ return
33
+ }
34
+ }
35
+ }
36
+ logger.error('Plugin not loaded: %s', pluginName)
37
+ }
38
+
39
+ onLoad(async () => {
40
+ const logger = useLogger()
41
+
42
+ runtime.options.plugins = runtime.options.plugins.concat(runtime.config.plugins).filter(plugin => plugin)
43
+
44
+ const userPlugins = runtime.options.plugins.filter(plugin => plugin.indexOf('render-') != 0 && plugin.indexOf('post-') != 0)
45
+ if (!userPlugins.length) {
46
+ logger.info('No plugins loaded')
47
+ } else {
48
+ logger.info('Loading plugins: %s', userPlugins)
49
+
50
+ for (let plugin of userPlugins) {
51
+ await loadPlugin(plugin)
52
+ }
53
+ }
54
+ })
@@ -0,0 +1,47 @@
1
+ import path from 'node:path'
2
+ import _ from 'lodash'
3
+
4
+ export async function loadPlugin(pluginName, workingFolder) {
5
+ const resolveLocations = [
6
+ path.join(workingFolder, 'node_modules', `mikser-core-${pluginName}/index.js`),
7
+ path.join(workingFolder, 'plugins', `${pluginName}.js`),
8
+ path.join(path.dirname(import.meta.url), 'plugins', 'post', `${pluginName.replace('post-', '')}.js`)
9
+ ]
10
+ for (let resolveLocation of resolveLocations) {
11
+ try {
12
+ return await import(resolveLocation)
13
+ } catch (err) {
14
+ if (err.code != 'ERR_MODULE_NOT_FOUND') throw err
15
+ }
16
+ }
17
+ }
18
+
19
+ export default async ({ entity, options, config, context, state, logger }) => {
20
+
21
+ const { postprocessor } = options
22
+ const plugins = {}
23
+ let pluginsToLoad = [...context.plugins || []]
24
+ pluginsToLoad.push(`post-${postprocessor}`)
25
+ if (entity.meta?.plugins) {
26
+ pluginsToLoad.push(...entity.meta.plugins)
27
+ }
28
+ pluginsToLoad.push(...options.plugins)
29
+ pluginsToLoad = _.uniq(pluginsToLoad.filter(pluginName => pluginName && pluginName.indexOf('post-') == 0))
30
+
31
+ const runtime = {
32
+ [entity.type]: entity,
33
+ entity,
34
+ plugins,
35
+ config: config[`post-${postprocessor}`],
36
+ data: context.data,
37
+ }
38
+
39
+ for (let pluginName of pluginsToLoad) {
40
+ const plugin = await loadPlugin(pluginName, options.workingFolder)
41
+ plugins[pluginName] = plugin
42
+ if (plugin?.load) await plugin.load({ entity, options, config: config[pluginName], context, runtime, state, logger })
43
+ }
44
+
45
+ const postprocessorPlugin = plugins[`post-${postprocessor}`]
46
+ return await postprocessorPlugin?.postprocess({ entity, options, config, context, plugins, runtime, state, logger })
47
+ }