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,346 @@
1
+ import path from 'node:path'
2
+ import { mkdir, writeFile, unlink, rm, readFile, symlink, } from 'fs/promises'
3
+ import { globby } from 'globby'
4
+ import _ from 'lodash'
5
+ import map from 'p-map'
6
+
7
+ export default ({
8
+ runtime,
9
+ onLoaded,
10
+ useLogger,
11
+ onImport,
12
+ watch,
13
+ onProcessed,
14
+ onBeforeRender,
15
+ useJournal,
16
+ createEntity,
17
+ updateEntity,
18
+ deleteEntity,
19
+ renderEntities,
20
+ onComplete,
21
+ onSync,
22
+ onFinalize,
23
+ findEntity,
24
+ matchEntity,
25
+ changeExtension,
26
+ constants: { ACTION, OPERATION },
27
+ }) => {
28
+ const collection = 'presets'
29
+ const type = 'preset'
30
+ const checksumMap = new Set()
31
+
32
+ async function getEntityPresets(entity) {
33
+ const entityPresets = []
34
+ for (let preset in (runtime.config.assets?.presets || {})) {
35
+ const matches = Array.isArray(runtime.config.assets.presets[preset]) ? runtime.config.assets.presets[preset] : [runtime.config.assets.presets[preset]]
36
+ for (let match of matches) {
37
+ if (matchEntity(entity, match)) {
38
+ entityPresets.push(preset)
39
+ }
40
+ }
41
+ }
42
+ return entityPresets
43
+ }
44
+
45
+ async function getRevisions(entity) {
46
+ let revisions = await globby(`${entity.destination.replaceAll('(', '\\(').replaceAll(')', '\\)')}.*.md5`, {
47
+ cwd: path.join(runtime.options.assetsFolder, entity.preset.name),
48
+ expandDirectories: false,
49
+ onlyFiles: true
50
+ })
51
+ return revisions
52
+ }
53
+
54
+ async function isPresetRendered(entity) {
55
+ let result = false
56
+ let revisions = []
57
+ const assetChecksum = `${entity.destination}.${entity.preset.checksum}.md5`
58
+ if (checksumMap.has(assetChecksum)) {
59
+ revisions.push(assetChecksum)
60
+ } else {
61
+ revisions = await getRevisions(entity)
62
+ }
63
+
64
+ for (let revision of revisions) {
65
+ const [assetsRevision] = revision.split('.').slice(-2, -1)
66
+ if (entity.preset.checksum <= Number.parseInt(assetsRevision)) {
67
+ if (entity.preset.options?.checksum === false) {
68
+ result = true
69
+ break
70
+ }
71
+
72
+ let checksum = await readFile(revision, 'utf8')
73
+ result ||= checksum == entity.checksum
74
+ if (result) break
75
+ }
76
+ }
77
+ return result
78
+ }
79
+
80
+ async function renderPresets(entities) {
81
+ const { presets, assetsMap } = runtime.state.assets
82
+
83
+ const tasks = []
84
+ for (let entityToRender of entities) {
85
+ for (let entityPreset of assetsMap[entityToRender.id] || []) {
86
+ const entity = _.cloneDeep(entityToRender)
87
+ entity.preset = presets[entityPreset]
88
+ let destination = entity.name
89
+ if (entity.preset.format) {
90
+ destination = changeExtension(destination, entity.preset.format)
91
+ }
92
+ entity.destination = path.join(runtime.options.assetsFolder, entityPreset, destination)
93
+ const ignore = await isPresetRendered(entity)
94
+ tasks.push({
95
+ entity,
96
+ options: {
97
+ ...entity.preset.options,
98
+ renderer: 'preset',
99
+ ignore
100
+ }
101
+ })
102
+ }
103
+ }
104
+ await renderEntities(tasks)
105
+
106
+ }
107
+
108
+ onLoaded(async () => {
109
+ const logger = useLogger()
110
+
111
+ const assetsName = runtime.config.assets?.assetsFolder || 'assets'
112
+ runtime.state.assets = {
113
+ presets: {},
114
+ assetsMap: {},
115
+ assetsFolder: runtime.config.assets?.outputFolder
116
+ ? path.join(runtime.config.assets.outputFolder, assetsName)
117
+ : assetsName,
118
+ }
119
+
120
+ runtime.options.presets = runtime.config.presets?.presetsFolder || collection
121
+ runtime.options.presetsFolder = path.join(runtime.options.workingFolder, runtime.options.presets)
122
+ logger.info('Presets folder: %s', runtime.options.presetsFolder)
123
+ await mkdir(runtime.options.presetsFolder, { recursive: true })
124
+
125
+ runtime.options.assets = runtime.config.assets?.assetsFolder || 'assets'
126
+ runtime.options.assetsFolder = path.join(runtime.options.workingFolder, runtime.options.assets)
127
+ logger.info('Assets folder: %s', runtime.options.assetsFolder)
128
+ await mkdir(runtime.options.assetsFolder, { recursive: true })
129
+
130
+ let link = path.join(runtime.options.outputFolder, runtime.options.assets)
131
+ if (runtime.config.assets?.outputFolder) link = path.join(runtime.options.outputFolder, runtime.config.assets?.outputFolder, runtime.options.assets)
132
+ try {
133
+ await mkdir(path.dirname(link), { recursive: true })
134
+ await symlink(path.resolve(runtime.options.assetsFolder), link, 'dir')
135
+ } catch (err) {
136
+ if (err.code != 'EEXIST')
137
+ throw err
138
+ }
139
+
140
+ watch(collection, runtime.options.presetsFolder)
141
+ })
142
+
143
+ onSync(collection, async ({ action, context }) => {
144
+ if (!context.relativePath) return false
145
+ const { relativePath } = context
146
+
147
+ const logger = useLogger()
148
+ const { presets } = runtime.state.assets
149
+
150
+ const name = relativePath.replace(path.extname(relativePath), '')
151
+ const uri = path.join(runtime.options.presetsFolder, relativePath)
152
+ const source = uri
153
+
154
+ let synced = true
155
+ switch (action) {
156
+ case ACTION.CREATE:
157
+ try {
158
+ const { revision = 1, format, options } = await import(`${uri}?stamp=${Date.now()}`)
159
+ const preset = {
160
+ id: path.join('/presets', relativePath),
161
+ collection,
162
+ type,
163
+ uri,
164
+ name: relativePath.replace(path.extname(relativePath), ''),
165
+ source,
166
+ format,
167
+ checksum: revision,
168
+ options
169
+ }
170
+ presets[name] = preset
171
+ await createEntity(preset)
172
+ } catch (err) {
173
+ synced = false
174
+ logger.error('Preset loading error: %s %s', uri, err.message)
175
+ }
176
+ break
177
+ case ACTION.UPDATE:
178
+ try {
179
+ const { revision = 1, format, options } = await import(`${uri}?stamp=${Date.now()}`)
180
+ const preset = {
181
+ id: path.join('/presets', relativePath),
182
+ collection,
183
+ type,
184
+ uri,
185
+ name: relativePath.replace(path.extname(relativePath), ''),
186
+ checksum: revision,
187
+ source,
188
+ format,
189
+ options
190
+ }
191
+ if (!preset[name]) {
192
+ presets[name] = preset
193
+ await createEntity(preset)
194
+ } else if (presets[name].checksum != preset.checksum) {
195
+ presets[name] = preset
196
+ await updateEntity(preset)
197
+ } else {
198
+ synced = false
199
+ }
200
+ } catch (err) {
201
+ synced = false
202
+ logger.error('Preset loading error: %s %s', uri, err.message)
203
+ }
204
+ break
205
+ case ACTION.DELETE:
206
+ delete presets[name]
207
+ await deleteEntity({
208
+ id: path.join('/presets', relativePath),
209
+ collection,
210
+ type,
211
+ })
212
+ break
213
+ }
214
+ return synced
215
+ })
216
+
217
+ onImport(async () => {
218
+ const logger = useLogger()
219
+ const { presets } = runtime.state.assets
220
+
221
+ const paths = await globby('*.js', { cwd: runtime.options.presetsFolder })
222
+ for (let relativePath of paths) {
223
+ const uri = path.join(runtime.options.presetsFolder, relativePath)
224
+ const source = uri
225
+ try {
226
+ const { revision = 1, format, options } = await import(`${uri}?stamp=${Date.now()}`)
227
+ const name = relativePath.replace(path.extname(relativePath), '')
228
+
229
+ const preset = {
230
+ id: path.join('/presets', relativePath),
231
+ collection,
232
+ type,
233
+ uri,
234
+ name: relativePath.replace(path.extname(relativePath), ''),
235
+ source,
236
+ format,
237
+ checksum: revision,
238
+ options,
239
+ options
240
+ }
241
+
242
+ await createEntity(preset)
243
+ presets[name] = preset
244
+ } catch (err) {
245
+ logger.error(err, 'Preset loading error: %s', uri)
246
+ }
247
+ }
248
+ })
249
+
250
+ onProcessed(async (signal) => {
251
+ const logger = useLogger()
252
+ const { assetsMap } = runtime.state.assets
253
+
254
+ for await (let { entity, operation } of useJournal('Assets processing', [OPERATION.CREATE, OPERATION.UPDATE, OPERATION.DELETE], signal)) {
255
+ if (entity.collection != collection) {
256
+ switch (operation) {
257
+ case OPERATION.CREATE:
258
+ case OPERATION.UPDATE:
259
+ const entityPresets = await getEntityPresets(entity)
260
+ if (entityPresets.length) {
261
+ logger.debug('Presets matched for: %s %s', entity.collection, entity.id, entityPresets.length)
262
+ assetsMap[entity.id] = entityPresets
263
+ }
264
+ break
265
+ case OPERATION.DELETE:
266
+ delete assetsMap[entity.id]
267
+ break
268
+ }
269
+ }
270
+ }
271
+ })
272
+
273
+ onBeforeRender(async signal => {
274
+ const { assetsMap } = runtime.state.assets
275
+
276
+ checksumMap.clear()
277
+ const checksumFiles = await globby('**/*.md5', { cwd: runtime.options.assetsFolder })
278
+ for (let checksumFile of checksumFiles) {
279
+ checksumMap.add(path.join(runtime.options.assetsFolder, checksumFile))
280
+ }
281
+
282
+ const entitiesToRender = new Map()
283
+ await map(useJournal('Assets provision', [OPERATION.CREATE, OPERATION.UPDATE], signal), async ({ entity }) => {
284
+ if (entity.collection == collection) {
285
+ for (let entityId in assetsMap) {
286
+ if (assetsMap[entityId].find(preset => preset == entity.name)) {
287
+ const entityToRender = await findEntity({
288
+ id: entityId
289
+ })
290
+ if (!entitiesToRender.has(entityToRender.id)) {
291
+ entitiesToRender.set(entityToRender.id, entityToRender)
292
+ }
293
+ }
294
+ }
295
+ } else {
296
+ if (assetsMap[entity.id] && !entitiesToRender.has(entity.id)) {
297
+ entitiesToRender.set(entity.id, entity)
298
+ }
299
+ }
300
+ }, { concurrency: 10, signal })
301
+
302
+ await renderPresets(entitiesToRender.values())
303
+ })
304
+
305
+ onComplete(async ({ entity, options }) => {
306
+ const logger = useLogger()
307
+ if (entity.preset && !options?.ignore) {
308
+ await mkdir(path.dirname(entity.destination), { recursive: true })
309
+ const assetChecksum = `${entity.destination}.${entity.preset.checksum}.md5`
310
+ await writeFile(assetChecksum, entity.checksum, 'utf8')
311
+ logger.debug('Asset render finished: [%s] %s', assetChecksum, entity.destination.replace(runtime.options.workingFolder, ''))
312
+ }
313
+ })
314
+
315
+ onFinalize(async () => {
316
+ const logger = useLogger()
317
+ const { presets } = runtime.state.assets
318
+
319
+ let revisions = await globby('**/*.md5', { cwd: runtime.options.assetsFolder })
320
+ for (let revision of revisions) {
321
+ const [preset] = revision.split(path.sep)
322
+ const [assetsRevision] = revision.split('.').slice(-2, -1)
323
+
324
+ if (!presets[preset]) {
325
+ const assetsPresetFolder = path.join(runtime.options.assetsFolder, preset)
326
+ const assetsPresetRemoved = false
327
+ try {
328
+ await rm(assetsPresetFolder, { recursive: true, force: true })
329
+ assetsPresetRemoved = true
330
+ } catch { }
331
+ if (assetsPresetRemoved) {
332
+ logger.debug('Assets preset removed: %s', assetsPresetFolder)
333
+ }
334
+ } else {
335
+ if (Number.parseInt(assetsRevision) < presets[preset].checksum) {
336
+ await unlink(path.join(runtime.options.assetsFolder, revision))
337
+ }
338
+ }
339
+ }
340
+ })
341
+
342
+ return {
343
+ collection,
344
+ type
345
+ }
346
+ }
@@ -0,0 +1,75 @@
1
+ import { execaCommand } from 'execa'
2
+ import lineReader from 'line-reader'
3
+ import { promisify } from 'util'
4
+ import _ from 'lodash'
5
+
6
+ export default ({
7
+ runtime,
8
+ useLogger,
9
+ onLoad,
10
+ onLoaded,
11
+ onImport,
12
+ onImported,
13
+ onProcess,
14
+ onProcessed,
15
+ onPersist,
16
+ onPersisted,
17
+ onCancel,
18
+ onCancelled,
19
+ onBeforeRender,
20
+ onRender,
21
+ onAfterRender,
22
+ onFinalize,
23
+ onFinalized
24
+ }) => {
25
+ const eachLine = promisify(lineReader.eachLine)
26
+ const running = {}
27
+
28
+ async function executeCommand(command) {
29
+ const logger = useLogger()
30
+ if (_.endsWith(command, '&')) {
31
+ command = command.slice(0, -1)
32
+ if (!running[command]) {
33
+ logger.info('Command: %s', command, runtime.options.wokrkingFolder)
34
+ const subprocess = execaCommand(command, { cwd: runtime.options.wokrkingFolder, all: true })
35
+ eachLine(subprocess.all, line => logger.info(line))
36
+ running[command] = subprocess
37
+ .then(() => delete running[command])
38
+ .catch(err => logger.error(err, 'Command error'))
39
+ }
40
+ } else {
41
+ logger.info('Command: %s', command, runtime.options.wokrkingFolder)
42
+ const subprocess = execaCommand(command, { cwd: runtime.options.wokrkingFolder, all: true })
43
+ await eachLine(subprocess.all, line => logger.debug(line))
44
+ await subprocess
45
+ }
46
+ }
47
+
48
+ async function executeCommands(hook) {
49
+ let commands = runtime.config.commands && runtime.config.commands[hook] || []
50
+ if (typeof commands == 'function') commands = await commands()
51
+ if (typeof commands == 'string') commands = [commands]
52
+
53
+ for (let command of commands) {
54
+ await executeCommand(command)
55
+ }
56
+ }
57
+
58
+ onLoad(async () => await executeCommands('load'))
59
+ onLoaded(async () => await executeCommands('loaded'))
60
+ onImport(async () => await executeCommands('import'))
61
+ onImported(async () => await executeCommands('imported'))
62
+ onProcess(async () => await executeCommands('process'))
63
+ onProcessed(async () => await executeCommands('processed'))
64
+ onPersist(async () => await executeCommands('persist'))
65
+ onPersisted(async () => await executeCommands('persisted'))
66
+ onBeforeRender(async () => await executeCommands('beforeRender'))
67
+ onRender(async () => await executeCommands('render'))
68
+ onAfterRender(async () => await executeCommands('afterRender'))
69
+ onCancel(async () => await executeCommands('cancel'))
70
+ onCancelled(async () => await executeCommands('canceled'))
71
+ onFinalize(async () => await executeCommands('finalize'))
72
+ onFinalized(async () => await executeCommands('finalized'))
73
+
74
+ return { executeCommand }
75
+ }
@@ -0,0 +1,138 @@
1
+ import path from 'path'
2
+ import { mkdir, writeFile, unlink } from 'fs/promises'
3
+ import _ from 'lodash'
4
+ import pMap from 'p-map'
5
+
6
+ export default ({
7
+ onLoaded,
8
+ useLogger,
9
+ runtime,
10
+ useJournal,
11
+ normalize,
12
+ findEntities,
13
+ onAfterRender,
14
+ onFinalize,
15
+ onBeforeRender,
16
+ constants: { OPERATION },
17
+ }) => {
18
+ onLoaded(async () => {
19
+ const logger = useLogger()
20
+ runtime.options.data = runtime.config.data?.dataFolder || 'data'
21
+ runtime.options.dataFolder = path.join(runtime.options.outputFolder, runtime.options.data)
22
+
23
+ logger.info('Data folder: %s', runtime.options.dataFolder)
24
+ await mkdir(runtime.options.dataFolder, { recursive: true })
25
+ })
26
+
27
+ onBeforeRender(async () => {
28
+ const logger = useLogger()
29
+
30
+ let entitiesConfig = runtime.config.data?.entities
31
+ if (entitiesConfig === undefined) {
32
+ entitiesConfig = {
33
+ document: {
34
+ query: entity => entity.type == 'document'
35
+ }
36
+ }
37
+ }
38
+ for (let entitiesName in entitiesConfig) {
39
+ const {
40
+ query,
41
+ map,
42
+ pick,
43
+ save: saveEntity = async entity => {
44
+ if (!entity.name) {
45
+ logger.warn('Entity name is missing: %o', entity)
46
+ return
47
+ }
48
+ const dump = JSON.stringify(normalize(entity))
49
+ const entityFile = path.join(runtime.options.dataFolder, `${entity.name}.${entitiesName}.json`)
50
+ await mkdir(path.dirname(entityFile), { recursive: true })
51
+ await writeFile(entityFile, dump, 'utf8')
52
+ },
53
+ delete: deleteEntity = async entity => {
54
+ const entityFile = path.join(runtime.options.dataFolder, `${entity.name}.json`)
55
+ await unlink(entityFile)
56
+ }
57
+ } = entitiesConfig[entitiesName]
58
+
59
+ for await (let { operation, entity } of useJournal('Data entities', [OPERATION.CREATE, OPERATION.UPDATE, OPERATION.DELETE])) {
60
+ if (query(entity)) {
61
+ switch (operation) {
62
+ case OPERATION.CREATE:
63
+ case OPERATION.UPDATE:
64
+ logger.debug('Data export entity %s %s: %s', entity.collection, operation, entity.id)
65
+ await saveEntity(map ? await map(entity) : {
66
+ refId: ('/' + entity.name.replaceAll('\\', '/')).replace(/\/index$/g, '/'),
67
+ name: entity.name,
68
+ date: new Date(entity.time),
69
+ data: _.pick(entity, pick || ['collection', 'format', 'type', 'destination', 'stamp', 'meta', 'id',])
70
+ })
71
+ break
72
+ case OPERATION.DELETE:
73
+ await deleteEntity(entity)
74
+ break
75
+ }
76
+ }
77
+ }
78
+ }
79
+ })
80
+
81
+ onAfterRender(async () => {
82
+ const logger = useLogger()
83
+
84
+ let contextConfig = runtime.config.data?.context
85
+ if (contextConfig === undefined) {
86
+ contextConfig = {
87
+ context: {
88
+ query: entity => entity.type == 'document'
89
+ }
90
+ }
91
+ }
92
+ for (let contextName in contextConfig) {
93
+ const {
94
+ query,
95
+ map,
96
+ pick,
97
+ save: saveConext = async (entity, context) => {
98
+ if (context?.data) {
99
+ const entityName = entity.name
100
+ const contextFile = path.join(runtime.options.dataFolder, `${entityName}.${contextName}.json`)
101
+ await mkdir(path.dirname(contextFile), { recursive: true })
102
+ await writeFile(contextFile, JSON.stringify(context), 'utf8')
103
+ }
104
+ }
105
+ } = contextConfig[contextName]
106
+
107
+ for await (let { entity, context } of useJournal('Data context', [OPERATION.RENDER])) {
108
+ if (query(entity)) {
109
+ logger.debug('Data export context: %s', entity.name)
110
+ await saveConext(entity, map ? await map(entity, context) : _.pick(context, pick || ['data']))
111
+ }
112
+ }
113
+ }
114
+ })
115
+
116
+ onFinalize(async () => {
117
+ const logger = useLogger()
118
+ for (let catalogName in runtime.config.data?.catalog || {}) {
119
+ const {
120
+ query: queryEntities = entity => entity.type == 'document',
121
+ map,
122
+ pick,
123
+ save: saveEntities = async entities => {
124
+ const entitiesFile = path.join(runtime.options.dataFolder, `${catalogName}.json`)
125
+ logger.debug('Data export catalog %s %s: %s', catalogName, entities.length, entitiesFile)
126
+ await writeFile(entitiesFile, JSON.stringify(entities), 'utf8')
127
+ }
128
+ } = runtime.config.data?.catalog[catalogName]
129
+ const entities = await findEntities(queryEntities)
130
+ await saveEntities(await pMap(entities, async entity => map ? await map(entity) : {
131
+ refId: ('/' + entity.name.replaceAll('\\', '/')).replace(/\/index$/g, '/'),
132
+ name: entity.name,
133
+ date: new Date(entity.time),
134
+ data: _.pick(entity, pick || ['collection', 'format', 'type', 'destination', 'stamp', 'meta', 'id',])
135
+ }))
136
+ }
137
+ })
138
+ }
@@ -0,0 +1,96 @@
1
+ import path from 'node:path'
2
+ import { mkdir, readFile } from '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
+ onSync,
16
+ trackProgress,
17
+ updateProgress,
18
+ constants: { ACTION }
19
+ }) => {
20
+ const collection = 'documents'
21
+ const type = 'document'
22
+
23
+ onSync(collection, async ({ action, context }) => {
24
+ if (!context.relativePath) return false
25
+ const { relativePath } = context
26
+ const id = path.join(`/${collection}`, relativePath)
27
+ const uri = path.join(runtime.options.documentsFolder, relativePath)
28
+ switch (action) {
29
+ case ACTION.CREATE:
30
+ await createEntity({
31
+ id,
32
+ uri,
33
+ name: relativePath.replace(path.extname(relativePath), ''),
34
+ collection,
35
+ type,
36
+ format: path.extname(relativePath).substring(1).toLowerCase(),
37
+ content: await readFile(uri, 'utf8')
38
+ })
39
+ break
40
+ case ACTION.UPDATE:
41
+ await updateEntity({
42
+ id,
43
+ uri,
44
+ name: relativePath.replace(path.extname(relativePath), ''),
45
+ collection,
46
+ type,
47
+ format: path.extname(relativePath).substring(1).toLowerCase(),
48
+ content: await readFile(uri, 'utf8')
49
+ })
50
+ break
51
+ case ACTION.DELETE:
52
+ await deleteEntity({
53
+ id,
54
+ collection,
55
+ type,
56
+ format: path.extname(relativePath).substring(1).toLowerCase(),
57
+ })
58
+ break
59
+ }
60
+ })
61
+
62
+ onLoaded(async () => {
63
+ const logger = useLogger()
64
+ runtime.options.documents = runtime.config.documents?.documentsFolder || collection
65
+ runtime.options.documentsFolder = path.join(runtime.options.workingFolder, runtime.options.documents)
66
+
67
+ logger.info('Documents folder: %s', runtime.options.documentsFolder)
68
+ await mkdir(runtime.options.documentsFolder, { recursive: true })
69
+
70
+ watch(collection, runtime.options.documentsFolder)
71
+ })
72
+
73
+ onImport(async () => {
74
+ const paths = await globby('**/*', { cwd: runtime.options.documentsFolder })
75
+
76
+ trackProgress('Documents import', paths.length)
77
+ return Promise.all(paths.map(async relativePath => {
78
+ const uri = path.join(runtime.options.documentsFolder, relativePath)
79
+ await createEntity({
80
+ id: path.join(`/${collection}`, relativePath),
81
+ uri,
82
+ name: relativePath.replace(path.extname(relativePath), ''),
83
+ collection,
84
+ type,
85
+ format: path.extname(relativePath).substring(1).toLowerCase(),
86
+ content: await readFile(uri, 'utf8')
87
+ })
88
+ updateProgress()
89
+ }))
90
+ })
91
+
92
+ return {
93
+ collection,
94
+ type
95
+ }
96
+ }