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,153 @@
1
+ import path from 'node:path'
2
+ import { mkdir, symlink, unlink, lstat, realpath } from 'fs/promises'
3
+ import { globby } from 'globby'
4
+
5
+ export default ({
6
+ runtime,
7
+ onLoaded,
8
+ useLogger,
9
+ onImport,
10
+ createEntity,
11
+ updateEntity,
12
+ deleteEntity,
13
+ watch,
14
+ onSync,
15
+ findEntity,
16
+ checksum,
17
+ trackProgress,
18
+ updateProgress,
19
+ constants: { ACTION },
20
+ }) => {
21
+ const collection = 'files'
22
+ const type = 'file'
23
+
24
+ async function ensureLink(relativePath) {
25
+ const source = path.join(runtime.options.filesFolder, relativePath)
26
+ let uri = path.join(runtime.options.outputFolder, relativePath)
27
+ if (runtime.config.files?.outputFolder) uri = path.join(runtime.options.outputFolder, runtime.config.files.outputFolder, relativePath)
28
+ try {
29
+ await mkdir(path.dirname(uri), { recursive: true })
30
+ await symlink(path.resolve(source), uri, 'file')
31
+ } catch (err) {
32
+ if (err.code != 'EEXIST')
33
+ throw err
34
+ }
35
+ return { uri, source }
36
+ }
37
+
38
+ async function removeLink(relativePath) {
39
+ let uri = path.join(runtime.options.outputFolder, relativePath)
40
+ if (runtime.config.files?.outputFolder) uri = path.join(runtime.options.outputFolder, runtime.config.files.outputFolder, relativePath)
41
+ await unlink(path.resolve(uri))
42
+ }
43
+
44
+ async function link(source) {
45
+ const stat = await lstat(source)
46
+ if (stat.isSymbolicLink()) {
47
+ return await realpath(source)
48
+ }
49
+ }
50
+
51
+ onSync(collection, async ({ action, context }) => {
52
+ if (!context.relativePath) return false
53
+ const { relativePath } = context
54
+
55
+ const source = path.join(runtime.options.filesFolder, relativePath)
56
+ const format = path.extname(relativePath).substring(1).toLowerCase()
57
+ const id = path.join(`/${collection}`, relativePath)
58
+ let uri = path.join(runtime.options.outputFolder, relativePath)
59
+ let name = relativePath
60
+ if (runtime.config.files?.outputFolder) {
61
+ uri = path.join(runtime.options.outputFolder, runtime.config.files.outputFolder, relativePath)
62
+ name = path.join(runtime.config.files.outputFolder, relativePath)
63
+ }
64
+
65
+ let synced = true
66
+ switch (action) {
67
+ case ACTION.CREATE:
68
+ await ensureLink(relativePath)
69
+ await createEntity({
70
+ id,
71
+ uri,
72
+ name,
73
+ collection,
74
+ type,
75
+ format,
76
+ source,
77
+ checksum: await checksum(source),
78
+ link: await link(source)
79
+ })
80
+ break
81
+ case ACTION.UPDATE:
82
+ const current = await findEntity({ id })
83
+ if (current?.checksum != checksum) {
84
+ await updateEntity({
85
+ id,
86
+ uri,
87
+ name: relativePath,
88
+ collection,
89
+ type,
90
+ format,
91
+ source,
92
+ checksum: await checksum(source),
93
+ link: await link(source)
94
+ })
95
+ } else {
96
+ synced = false
97
+ }
98
+ break
99
+ case ACTION.DELETE:
100
+ await removeLink(relativePath)
101
+ await deleteEntity({
102
+ id,
103
+ collection,
104
+ type,
105
+ })
106
+ break
107
+ }
108
+ return synced
109
+ })
110
+
111
+ onLoaded(async () => {
112
+ const logger = useLogger()
113
+ runtime.options.files = runtime.config.files?.filesFolder || collection
114
+ runtime.options.filesFolder = path.join(runtime.options.workingFolder, runtime.options.files)
115
+
116
+ logger.info('Files folder: %s', runtime.options.filesFolder)
117
+ await mkdir(runtime.options.filesFolder, { recursive: true })
118
+
119
+ watch(collection, runtime.options.filesFolder)
120
+ })
121
+
122
+ onImport(async () => {
123
+ await mkdir(runtime.options.outputFolder, { recursive: true })
124
+ if (runtime.config.files?.outputFolder) await mkdir(path.join(runtime.options.outputFolder, runtime.config.files.outputFolder), { recursive: true })
125
+
126
+ const paths = await globby('**/*', { cwd: runtime.options.filesFolder })
127
+ trackProgress('Files import', paths.length)
128
+ return Promise.all(paths.map(async relativePath => {
129
+ const { uri, source } = await ensureLink(relativePath)
130
+ let name = relativePath
131
+ if (runtime.config.files?.outputFolder) {
132
+ name = path.join(runtime.config.files.outputFolder, relativePath)
133
+ }
134
+ await createEntity({
135
+ id: path.join(`/${collection}`, relativePath),
136
+ uri,
137
+ collection,
138
+ type,
139
+ format: path.extname(relativePath).substring(1).toLowerCase(),
140
+ name,
141
+ source,
142
+ checksum: await checksum(source),
143
+ link: await link(source)
144
+ })
145
+ updateProgress()
146
+ }))
147
+ })
148
+
149
+ return {
150
+ collection,
151
+ type
152
+ }
153
+ }
@@ -0,0 +1,24 @@
1
+ import fm from 'front-matter'
2
+
3
+ export default ({
4
+ onProcess,
5
+ useLogger,
6
+ useJournal,
7
+ updateEntry,
8
+ constants: { OPERATION }
9
+ }) => {
10
+ onProcess(async () => {
11
+ const logger = useLogger()
12
+ for await (let { id, entity } of useJournal('Fron matter', [OPERATION.CREATE, OPERATION.UPDATE])) {
13
+ if (entity.content && fm.test(entity.content)) {
14
+ const info = fm(entity.content)
15
+ if (info.attributes) {
16
+ entity.meta = Object.assign(entity.meta || {}, info.attributes)
17
+ entity.content = info.body
18
+ await updateEntry({ id, entity })
19
+ logger.trace('Front matter %s: %s', entity.collection, entity.id)
20
+ }
21
+ }
22
+ }
23
+ })
24
+ }
@@ -0,0 +1,20 @@
1
+ export default ({
2
+ onProcess,
3
+ useLogger,
4
+ useJournal,
5
+ updateEntry,
6
+ constants: { OPERATION }
7
+ }) => {
8
+ onProcess(async () => {
9
+ const logger = useLogger()
10
+
11
+ for await (let { id, entity } of useJournal('Json', [OPERATION.CREATE, OPERATION.UPDATE])) {
12
+ if (entity.content && entity.format == 'json') {
13
+ entity.meta = Object.assign(entity.meta || {}, JSON.parse(entity.content))
14
+ delete entity.content
15
+ await updateEntry({ id, entity })
16
+ logger.trace('Json %s: %s', entity.collection, entity.id)
17
+ }
18
+ }
19
+ })
20
+ }
@@ -0,0 +1,368 @@
1
+ import path from 'node:path'
2
+ import { mkdir, writeFile, unlink } from 'node:fs/promises'
3
+ import { globby } from 'globby'
4
+ import _ from 'lodash'
5
+
6
+ export default ({
7
+ runtime,
8
+ onLoaded,
9
+ useLogger,
10
+ onImport,
11
+ createEntity,
12
+ updateEntity,
13
+ deleteEntity,
14
+ watch,
15
+ onProcessed,
16
+ onBeforeRender,
17
+ useJournal,
18
+ renderEntities,
19
+ onComplete,
20
+ onSync,
21
+ matchEntity,
22
+ changeExtension,
23
+ constants: { ACTION, OPERATION, TASKS },
24
+ }) => {
25
+ const collection = 'layouts'
26
+ const type = 'layout'
27
+
28
+ function getFormatInfo(relativePath) {
29
+ const template = path.extname(relativePath).substring(1).toLowerCase()
30
+ const withoutTemplate = relativePath.replace(path.extname(relativePath), '')
31
+ const formatExt = path.extname(withoutTemplate).substring(1).toLowerCase()
32
+ const [format, postprocessor] = formatExt.split('-')
33
+ const name = formatExt ? withoutTemplate.replace(path.extname(withoutTemplate), '') : withoutTemplate
34
+ return { name, format: format || 'html', template, postprocessor }
35
+ }
36
+
37
+ function addToSitemap(entity) {
38
+ const logger = useLogger()
39
+ const { sitemap } = runtime.state.layouts
40
+ const { href = '/' + entity.name, lang } = entity.meta || {}
41
+ if (lang) {
42
+ sitemap[href] = sitemap[href] || {};
43
+ let previous = sitemap[href][lang];
44
+ if (previous && (previous.id != entity.id)) {
45
+ logger.warn('Entity with equal href: [%s] %s and %s', previous.collection, previous.id, entity.id);
46
+ }
47
+ sitemap[href][lang] = entity
48
+ }
49
+ else {
50
+ let previous = sitemap[href];
51
+ if (previous && (previous.id != entity.id)) {
52
+ logger.warn('Entity with equal href: [%s] %s and %s', previous.collection, previous.id, entity.id);
53
+ }
54
+ sitemap[href] = entity
55
+ }
56
+ }
57
+
58
+ function removeFromSitemap(entity) {
59
+ const { sitemap } = runtime.state.layouts
60
+ for (let href in sitemap) {
61
+ let entry = sitemap[href]
62
+ if (entry.id) {
63
+ if (entry.id == entity.id) {
64
+ delete sitemap[href]
65
+ return
66
+ }
67
+ } else {
68
+ for (let lang in entry) {
69
+ if (entry[lang].id == entity.id) {
70
+ delete entry[lang]
71
+ return
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ function removePagesFromSitemap(entity) {
79
+ const entities = Array.from(getSitemapEntities())
80
+ for (let current of entities) {
81
+ if (entity.uri == current.uri) {
82
+ removeFromSitemap(current)
83
+ }
84
+ }
85
+ }
86
+
87
+ function* getSitemapEntities() {
88
+ const { sitemap } = runtime.state.layouts
89
+ for (let href in sitemap) {
90
+ let entry = sitemap[href]
91
+ if (entry.id) {
92
+ if (!entry.page || entry.page <= 1) {
93
+ yield entry
94
+ }
95
+ } else {
96
+ for (let lang in entry) {
97
+ if (!entry[lang].page || entry[lang].page <= 1) {
98
+ yield entry[lang]
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ onSync(collection, async ({ action, context }) => {
106
+ if (!context.relativePath) return false
107
+ const { relativePath } = context
108
+ let id = path.join(`/${collection}`, relativePath)
109
+ if (_.endsWith(id, '.js')) id = id.replace(new RegExp('.js$'), '')
110
+
111
+ const uri = path.join(runtime.options.layoutsFolder, relativePath)
112
+ const { layouts } = runtime.state.layouts
113
+ switch (action) {
114
+ case ACTION.CREATE:
115
+ var layout = {
116
+ id,
117
+ uri,
118
+ collection,
119
+ type,
120
+ name: relativePath.replace(path.extname(relativePath), ''),
121
+ ...getFormatInfo(relativePath)
122
+ }
123
+ layouts[layout.name] = layout
124
+ await createEntity(layout)
125
+ break
126
+ case ACTION.UPDATE:
127
+ var layout = {
128
+ id,
129
+ uri,
130
+ collection,
131
+ type,
132
+ name: relativePath.replace(path.extname(relativePath), ''),
133
+ ...getFormatInfo(relativePath)
134
+ }
135
+ layouts[layout.name] = layout
136
+ await updateEntity(layout)
137
+ break
138
+ case ACTION.DELETE:
139
+ var layout = {
140
+ id,
141
+ collection,
142
+ type,
143
+ format: path.extname(relativePath).substring(1).toLowerCase(),
144
+ }
145
+ for (let name in layouts) {
146
+ if (layouts[name].id == layout.id) {
147
+ delete layouts[name]
148
+ }
149
+ }
150
+ await deleteEntity(layout)
151
+ break
152
+ }
153
+ })
154
+
155
+ onLoaded(async () => {
156
+ const logger = useLogger()
157
+
158
+ runtime.state.layouts = {
159
+ layouts: {},
160
+ sitemap: {}
161
+ }
162
+
163
+ runtime.options.layouts = runtime.config.layouts?.layoutsFolder || collection
164
+ runtime.options.layoutsFolder = path.join(runtime.options.workingFolder, runtime.options.layouts)
165
+ runtime.options.layoutsStateFolder = path.join(runtime.options.outputFolder, 'state')
166
+
167
+ logger.info('Layouts folder: %s', runtime.options.layoutsFolder)
168
+ await mkdir(runtime.options.layoutsFolder, { recursive: true })
169
+
170
+ watch(collection, runtime.options.layoutsFolder)
171
+ })
172
+
173
+ onImport(async () => {
174
+ const { layouts } = runtime.state.layouts
175
+ const paths = await globby('**/*', { cwd: runtime.options.layoutsFolder, ignore: ['**/*.js'] })
176
+ for (let relativePath of paths) {
177
+ const uri = path.join(runtime.options.layoutsFolder, relativePath)
178
+ const layout = {
179
+ id: path.join('/layouts', relativePath),
180
+ uri,
181
+ name: relativePath.replace(path.extname(relativePath), ''),
182
+ collection,
183
+ type,
184
+ }
185
+ Object.assign(layout, await getFormatInfo(relativePath))
186
+ layouts[layout.name] = layout
187
+ await createEntity(layout)
188
+ }
189
+ })
190
+
191
+ onProcessed(async (signal) => {
192
+ const logger = useLogger()
193
+ const { layouts } = runtime.state.layouts
194
+
195
+ for await (let { entity, operation } of useJournal('Layouts processing', [OPERATION.CREATE, OPERATION.UPDATE, OPERATION.DELETE], signal)) {
196
+ if (entity.collection == collection) continue
197
+ switch (operation) {
198
+ case OPERATION.CREATE:
199
+ case OPERATION.UPDATE:
200
+ removePagesFromSitemap(entity)
201
+ if (!entity.meta?.layout) {
202
+ for (let pattern in runtime.config.layouts?.match || []) {
203
+ if (matchEntity(entity, pattern)) {
204
+ const layoutName = runtime.config.layouts?.match[pattern]
205
+ entity.layout = layouts[layoutName]
206
+ break
207
+ }
208
+ }
209
+ if (!entity.layout && runtime.config.layouts?.autoLayouts && entity.name) {
210
+ const nameChunks = entity.name.split('.')
211
+ if (nameChunks?.length) {
212
+ for (let index = 0; index < nameChunks.length; index++) {
213
+ const autoLayout = [
214
+ path.basename(entity.name).split('.').slice(index).join('.'),
215
+ path.basename(entity.id)
216
+ ]
217
+ .find(layout => layouts[layout])
218
+ if (autoLayout) {
219
+ entity.layout = layouts[autoLayout]
220
+ break
221
+ }
222
+ }
223
+ }
224
+ }
225
+ } else {
226
+ entity.layout = layouts[entity.meta.layout]
227
+ }
228
+ if (entity.meta?.layout && !entity.layout) {
229
+ logger.warn('Layout not found for %s: %s', entity.collection, entity.id)
230
+ }
231
+
232
+ if (entity.layout && entity.meta?.postprocessor) {
233
+ entity.layout.postprocessor = entity.meta.postprocessor
234
+ }
235
+
236
+ if (entity.layout) {
237
+ logger.debug('Layout matched for %s: %s', entity.collection, entity.id)
238
+ addToSitemap(entity)
239
+ } else if (entity.meta?.href) {
240
+ logger.trace('Layout missing for %s: %s', entity.collection, entity.id)
241
+ addToSitemap(entity)
242
+ }
243
+ break
244
+ case OPERATION.DELETE:
245
+ removePagesFromSitemap(entity)
246
+ break
247
+ }
248
+
249
+ }
250
+ })
251
+
252
+ onBeforeRender(async (signal) => {
253
+ const tasks = []
254
+ const entities = Array.from(getSitemapEntities())
255
+ .filter(entity => entity.layout)
256
+ .sort((a, b) => b.time - a.time)
257
+
258
+ for (let original of entities) {
259
+ if (signal.aborted) return
260
+
261
+ delete original.page
262
+ delete original.pages
263
+ delete original.destination
264
+
265
+ const entity = _.cloneDeep(original)
266
+ entity.destination = '/' + entity.name
267
+ let data
268
+ try {
269
+ var { load, plugins = [] } = await import(`${path.join(runtime.options.layoutsFolder, entity.layout.name)}.js?stamp=${Date.now()}`)
270
+ if (load) {
271
+ data = await load(entity, signal)
272
+ }
273
+ } catch (err) {
274
+ if (err.code != 'ERR_MODULE_NOT_FOUND') throw err
275
+ }
276
+
277
+ if (data?.pages) {
278
+ if (!_.endsWith(entity.name, entity.format)) {
279
+ for (let page = 0; page < data.pages - 1; page++) {
280
+ const pageEntity = _.cloneDeep(entity)
281
+ pageEntity.pages = data.pages
282
+ if (page) {
283
+ pageEntity.page = page + 1
284
+ pageEntity.id = changeExtension(entity.id, `${pageEntity.page}.${entity.layout.format}`)
285
+ if (entity.meta) {
286
+ if (entity.meta.href) {
287
+ pageEntity.meta.href = `${entity.meta.href}.${pageEntity.page}`
288
+ } else {
289
+ pageEntity.meta.href = `/${entity.name}.${pageEntity.page}`
290
+ }
291
+ }
292
+
293
+ if (runtime.config.layouts?.cleanUrls && entity.layout.format == 'html') {
294
+ pageEntity.destination = path.join(entity.destination.replace('index', ''), pageEntity.page.toString(), `index.${entity.layout.format}`)
295
+ } else {
296
+ pageEntity.destination += page ? `.${pageEntity.page}.${entity.layout.format}` : `.${entity.layout.format}`
297
+ }
298
+ } else {
299
+ removePagesFromSitemap(original)
300
+ pageEntity.page = 1
301
+ if (runtime.config.layouts?.cleanUrls && !_.endsWith(entity.name, 'index') && entity.layout.format == 'html') {
302
+ pageEntity.destination = path.join(entity.destination, `index.${entity.layout.format}`)
303
+ } else {
304
+ pageEntity.destination += `.${entity.layout.format}`
305
+ }
306
+ }
307
+ addToSitemap(pageEntity)
308
+ tasks.push({
309
+ entity: pageEntity,
310
+ options: {
311
+ renderer: entity.layout.template,
312
+ postprocessor: entity.layout.postprocessor,
313
+ tasks: entity.meta?.task || TASKS.POOL
314
+ },
315
+ context: { data, plugins }
316
+ })
317
+ }
318
+ }
319
+ } else {
320
+ removePagesFromSitemap(original)
321
+ if (!_.endsWith(entity.name, entity.format)) {
322
+ if (runtime.config.layouts?.cleanUrls && !_.endsWith(entity.name, 'index') && entity.layout.format == 'html') {
323
+ entity.destination = path.join(entity.destination, `index.${entity.layout.format}`)
324
+ } else {
325
+ entity.destination += `.${entity.layout.format}`
326
+ }
327
+ }
328
+ addToSitemap(entity)
329
+ if (entity.destination) {
330
+ tasks.push({
331
+ entity,
332
+ options: {
333
+ renderer: entity.layout.template,
334
+ postprocessor: entity.layout.postprocessor,
335
+ tasks: entity.meta?.task || TASKS.POOL
336
+ },
337
+ context: { data, plugins }
338
+ })
339
+ }
340
+ }
341
+ }
342
+ await renderEntities(tasks)
343
+ })
344
+
345
+ onComplete(async ({ entity, options, output }) => {
346
+ const logger = useLogger()
347
+ if (entity.layout && !options?.ignore && output.result != null) {
348
+ const destinationFile = path.join(runtime.options.outputFolder, entity.destination)
349
+ await mkdir(path.dirname(destinationFile), { recursive: true })
350
+ try {
351
+ await unlink(destinationFile)
352
+ } catch { }
353
+ await writeFile(destinationFile, output.result)
354
+ logger.debug('Layout render finished: %s', entity.destination.replace(runtime.options.workingFolder, ''))
355
+ if (entity.origin) {
356
+ const originFile = path.join(runtime.options.outputFolder, entity.origin)
357
+ try {
358
+ await unlink(originFile)
359
+ } catch { }
360
+ }
361
+ }
362
+ })
363
+
364
+ return {
365
+ collection,
366
+ type
367
+ }
368
+ }
@@ -0,0 +1,29 @@
1
+ import _ from 'lodash'
2
+
3
+ export default ({
4
+ onProcess,
5
+ useLogger,
6
+ useJournal,
7
+ updateEntry,
8
+ matchEntity,
9
+ runtime,
10
+ constants: { OPERATION },
11
+ }) => {
12
+ onProcess(async (signal) => {
13
+ const logger = useLogger()
14
+
15
+ for (let { match, map, operations = [OPERATION.CREATE, OPERATION.UPDATE] } of runtime.config.mapper?.mappers || []) {
16
+ for await (let { id, entity } of useJournal('Mapper', operations, signal)) {
17
+ if (entity && matchEntity(entity, match)) {
18
+ logger.trace('Mapper: %s', entity.id)
19
+ try {
20
+ await map(entity)
21
+ await updateEntry({ id, entity })
22
+ } catch (err) {
23
+ logger.error('Mapper error: %s %s', entity.name || entity.id, err.message)
24
+ }
25
+ }
26
+ }
27
+ }
28
+ })
29
+ }
@@ -0,0 +1,60 @@
1
+ import path from 'node:path'
2
+
3
+ const TEARDOWN_DELAY = 60_000
4
+
5
+ let browser
6
+ let teardownTimer
7
+
8
+ export async function setup({ config, logger }) {
9
+ if (teardownTimer) {
10
+ clearTimeout(teardownTimer)
11
+ teardownTimer = undefined
12
+ logger.debug('Puppeteer browser reused')
13
+ return
14
+ }
15
+
16
+ const { default: puppeteer } = await import('puppeteer').catch(() => {
17
+ throw new Error('puppeteer is required for the pdf postprocessor — run: npm install puppeteer')
18
+ })
19
+ browser = await puppeteer.launch({
20
+ headless: true,
21
+ ...config?.launch
22
+ })
23
+ logger.debug('Puppeteer browser launched')
24
+ }
25
+
26
+ export async function postprocess({ entity, options, config, logger }) {
27
+ const sourcePath = path.join(options.outputFolder, entity.origin)
28
+
29
+ const page = await browser.newPage()
30
+ try {
31
+ await page.goto(`file://${sourcePath}`, {
32
+ waitUntil: 'networkidle0',
33
+ ...config?.navigation
34
+ })
35
+ return await page.pdf({
36
+ format: 'A4',
37
+ printBackground: true,
38
+ ...config?.pdf
39
+ })
40
+ } finally {
41
+ await page.close()
42
+ }
43
+ }
44
+
45
+ export async function teardown({ options, config, logger }) {
46
+ if (!options?.watch) {
47
+ await browser?.close()
48
+ browser = undefined
49
+ logger.debug('Puppeteer browser closed')
50
+ return
51
+ }
52
+ const delay = config?.teardownDelay ?? TEARDOWN_DELAY
53
+ teardownTimer = setTimeout(async () => {
54
+ teardownTimer = undefined
55
+ await browser?.close()
56
+ browser = undefined
57
+ logger.debug('Puppeteer browser closed')
58
+ }, delay)
59
+ logger.debug('Puppeteer browser teardown scheduled in %dms', delay)
60
+ }
@@ -0,0 +1,11 @@
1
+ import path from 'node:path'
2
+
3
+ export function load({ runtime, entity, state, options }) {
4
+ runtime.asset = (preset, url, format) => {
5
+ if (url[0] != '/') url = `/${url}`
6
+ const relative = `${state.assets.assetsFolder}/${preset}${format ? url.split('.').slice(0, -1).concat(format).join('.') : url}`
7
+ const destination = '/' + relative
8
+ const from = path.dirname(entity.destination || '/')
9
+ return { url: path.relative(from, destination) }
10
+ }
11
+ }
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { globby } from 'globby'
3
+
4
+ export function load({ runtime }) {
5
+ runtime.readFile = (file) => {
6
+ const relativePath = file.name || file
7
+ return readFileSync(relativePath, { encoding: 'utf8' })
8
+ }
9
+ runtime.jsonFile = (file) => {
10
+ const relativePath = file.name || file
11
+ return JSON.parse(readFileSync(relativePath, { encoding: 'utf8' }))
12
+ }
13
+ runtime.glob = (pattern, options = {}) => {
14
+ return globby.sync(pattern, options)
15
+ }
16
+ }