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,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
|
+
}
|