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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/app.js +7 -0
- package/index.js +11 -0
- package/mikser-lockup-stacked.svg +14 -0
- package/package.json +64 -0
- package/src/catalog.js +56 -0
- package/src/config.js +37 -0
- package/src/constants.js +20 -0
- package/src/engine.js +317 -0
- package/src/journal.js +108 -0
- package/src/lifecycle.js +299 -0
- package/src/manager.js +119 -0
- package/src/plugins/api.js +176 -0
- package/src/plugins/assets.js +346 -0
- package/src/plugins/commands.js +75 -0
- package/src/plugins/data.js +138 -0
- package/src/plugins/documents.js +96 -0
- package/src/plugins/files.js +153 -0
- package/src/plugins/front-matter.js +24 -0
- package/src/plugins/json.js +20 -0
- package/src/plugins/layouts.js +368 -0
- package/src/plugins/mapper.js +29 -0
- package/src/plugins/post/pdf.js +60 -0
- package/src/plugins/render/asset.js +11 -0
- package/src/plugins/render/file.js +16 -0
- package/src/plugins/render/hbs.js +52 -0
- package/src/plugins/render/href.js +45 -0
- package/src/plugins/render/preset.js +13 -0
- package/src/plugins/render/resource.js +17 -0
- package/src/plugins/resources.js +216 -0
- package/src/plugins/rest.js +143 -0
- package/src/plugins/shares.js +40 -0
- package/src/plugins/validator.js +19 -0
- package/src/plugins/yaml.js +26 -0
- package/src/plugins.js +54 -0
- package/src/postprocess.js +47 -0
- package/src/render.js +71 -0
- package/src/runtime.js +153 -0
- package/src/tracking.js +74 -0
- package/src/utils.js +65 -0
|
@@ -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
|
+
}
|