votive 0.2.1 → 0.3.1
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/README.md +4 -11
- package/lib/bundle.js +2 -1
- package/lib/cleanDeletions.js +32 -0
- package/lib/createDatabase.js +65 -4
- package/lib/readAbstracts.js +9 -12
- package/lib/readFolders.js +15 -18
- package/lib/readSources.js +41 -168
- package/lib/runJobs.js +8 -26
- package/lib/utils/index.js +5 -13
- package/lib/writeDestinations.js +10 -2
- package/package.json +1 -2
- package/tests/index.js +245 -302
package/README.md
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# Votive
|
|
2
2
|
|
|
3
|
-
*File processor*
|
|
4
|
-
|
|
5
|
-
- Powers [Voot](https://github.com/samlfair/voot)
|
|
6
|
-
- Bundles [Vowel](https://github.com/samlfair/vowel)
|
|
7
|
-
|
|
8
3
|
## Roadmap
|
|
9
4
|
|
|
5
|
+
- [ ] ReadPaths
|
|
10
6
|
- [ ] Flesh out job runners
|
|
11
|
-
- [ ]
|
|
12
|
-
- [ ] Better dependency tracking
|
|
13
|
-
- [ ] Better query filters
|
|
14
|
-
- [x] File deletion handling
|
|
7
|
+
- [ ] Destination query filters
|
|
15
8
|
|
|
16
|
-
##
|
|
9
|
+
## Helpers
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
- Read
|
package/lib/bundle.js
CHANGED
|
@@ -245,6 +245,8 @@ async function bundle(config, cache) {
|
|
|
245
245
|
// Write destination files
|
|
246
246
|
await writeDestinations(config, database)
|
|
247
247
|
|
|
248
|
+
const stale = database.target.getStale()
|
|
249
|
+
|
|
248
250
|
writeTime()
|
|
249
251
|
|
|
250
252
|
// database.commit()
|
|
@@ -280,7 +282,6 @@ async function bundler(config) {
|
|
|
280
282
|
queue.push(bundle(config))
|
|
281
283
|
cache = await queue[0]
|
|
282
284
|
} else {
|
|
283
|
-
"cache"
|
|
284
285
|
queue.push(bundle(config, cache))
|
|
285
286
|
await queue[0]
|
|
286
287
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdir, rm } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
/** @import {Abstract, Abstracts, VotiveConfig} from "./bundle.js" */
|
|
5
|
+
/** @import {Database} from "./createDatabase.js" */
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {VotiveConfig} config
|
|
10
|
+
* @param {Database} database
|
|
11
|
+
*/
|
|
12
|
+
async function cleanDeletions(config, database) {
|
|
13
|
+
// Must remove from sources first??
|
|
14
|
+
const dirents = new Set((await readdir(config.destinationFolder, {
|
|
15
|
+
recursive: true,
|
|
16
|
+
withFileTypes: true
|
|
17
|
+
})).map(d => d.isFile() && path.relative(config.destinationFolder, path.join(d.parentPath, d.name)))
|
|
18
|
+
.filter(a => a)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const targets = new Set(database.target.getAll().map(t => t.path))
|
|
22
|
+
|
|
23
|
+
const deletions = dirents.difference(targets)
|
|
24
|
+
|
|
25
|
+
deletions.forEach(async deletion => {
|
|
26
|
+
const deletionPath = path.join(config.destinationFolder, deletion)
|
|
27
|
+
await rm(deletionPath)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default cleanDeletions
|
package/lib/createDatabase.js
CHANGED
|
@@ -230,7 +230,18 @@ function prepareStatements(database) {
|
|
|
230
230
|
WITH matches AS (
|
|
231
231
|
SELECT m.destination, COUNT(*) AS match_count
|
|
232
232
|
FROM metadata m
|
|
233
|
-
INNER JOIN json_each(:filter) j ON j.key = m.label AND
|
|
233
|
+
INNER JOIN json_each(:filter) j ON j.key = m.label AND (
|
|
234
|
+
j.value = m.value
|
|
235
|
+
OR (
|
|
236
|
+
json_valid(m.value)
|
|
237
|
+
AND json_type(m.value) = 'array'
|
|
238
|
+
AND EXISTS (
|
|
239
|
+
SELECT 1
|
|
240
|
+
FROM json_each(m.value)
|
|
241
|
+
WHERE value = j.value
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
234
245
|
GROUP BY m.destination
|
|
235
246
|
),
|
|
236
247
|
filter_count AS (
|
|
@@ -250,9 +261,46 @@ function prepareStatements(database) {
|
|
|
250
261
|
OR d.dir LIKE :recursivePath
|
|
251
262
|
)
|
|
252
263
|
GROUP BY d.path
|
|
253
|
-
ORDER BY d.path ASC
|
|
264
|
+
ORDER BY d.path ASC;
|
|
254
265
|
`))),
|
|
255
266
|
|
|
267
|
+
/*
|
|
268
|
+
WITH matches AS (
|
|
269
|
+
SELECT m.destination, COUNT(*) AS match_count
|
|
270
|
+
FROM metadata m
|
|
271
|
+
INNER JOIN json_each('{ "breadcrumb": "Hello World" }') j ON j.key = m.label AND (
|
|
272
|
+
j.value = m.value
|
|
273
|
+
OR (
|
|
274
|
+
json_valid(m.value)
|
|
275
|
+
AND json_type(m.value) = 'array'
|
|
276
|
+
AND EXISTS (
|
|
277
|
+
SELECT 1
|
|
278
|
+
FROM json_each(m.value)
|
|
279
|
+
WHERE value = j.value
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
GROUP BY m.destination
|
|
284
|
+
),
|
|
285
|
+
filter_count AS (
|
|
286
|
+
SELECT COUNT(*) AS total FROM json_each('{ "breadcrumb": "Hello World" }')
|
|
287
|
+
)
|
|
288
|
+
SELECT d.*, json_group_object(i.label, i.value) AS metadata
|
|
289
|
+
FROM destinations d
|
|
290
|
+
INNER JOIN metadata i ON d.path = i.destination
|
|
291
|
+
LEFT JOIN matches ON matches.destination = d.path
|
|
292
|
+
CROSS JOIN filter_count f
|
|
293
|
+
WHERE (
|
|
294
|
+
f.total = 0
|
|
295
|
+
OR matches.match_count = f.total
|
|
296
|
+
)
|
|
297
|
+
AND (
|
|
298
|
+
d.dir = '/'
|
|
299
|
+
OR d.dir LIKE '%'
|
|
300
|
+
)
|
|
301
|
+
GROUP BY d.path
|
|
302
|
+
ORDER BY d.path ASC;
|
|
303
|
+
*/
|
|
256
304
|
|
|
257
305
|
/**
|
|
258
306
|
* @callback SQLiteTargetGetAll
|
|
@@ -694,6 +742,19 @@ function createDatabase(databasePath = ".votive.db") {
|
|
|
694
742
|
|
|
695
743
|
target: {
|
|
696
744
|
|
|
745
|
+
/**
|
|
746
|
+
* @param {string} filePath
|
|
747
|
+
*/
|
|
748
|
+
delete(filePath) {
|
|
749
|
+
// Note: Need to run clean-up after
|
|
750
|
+
prepared.target.delete.get(filePath)
|
|
751
|
+
prepared.metadata.deleteByTarget.all(filePath)
|
|
752
|
+
const deps = queries.dependency.getAllByTarget(filePath)
|
|
753
|
+
deps.forEach(dep => {
|
|
754
|
+
queries.target.markStale(dep.dependent)
|
|
755
|
+
})
|
|
756
|
+
},
|
|
757
|
+
|
|
697
758
|
/**
|
|
698
759
|
* @param {string} filePath
|
|
699
760
|
* @returns {TargetOutput | undefined}
|
|
@@ -755,7 +816,7 @@ function createDatabase(databasePath = ".votive.db") {
|
|
|
755
816
|
getWithTrackers(filePath, dependent) {
|
|
756
817
|
const target = queries.target.get(filePath)
|
|
757
818
|
|
|
758
|
-
if(!target) return
|
|
819
|
+
if (!target) return
|
|
759
820
|
|
|
760
821
|
const trackedTarget = { metadata: {} }
|
|
761
822
|
|
|
@@ -790,7 +851,7 @@ function createDatabase(databasePath = ".votive.db") {
|
|
|
790
851
|
|
|
791
852
|
/** @param {TargetGetManyWithTrackersParams | undefined} params */
|
|
792
853
|
getManyWithTrackers(params) {
|
|
793
|
-
const { folder = "
|
|
854
|
+
const { folder = "", recursive, dependent, query = {} } = params
|
|
794
855
|
|
|
795
856
|
const recursivePath = [folder, "%"].filter(a => a).join("/")
|
|
796
857
|
|
package/lib/readAbstracts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @import {VotiveConfig, FlatProcessors, Abstract, AbstractsWithSyntax, Jobs, FlatProcessor, Abstracts} from "./bundle.js" */
|
|
2
2
|
/** @import {Database} from "./createDatabase.js" */
|
|
3
|
-
/** @import {
|
|
3
|
+
/** @import {ReadSourceFileResult} from "./readSources.js"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {object} ReadAbstractsResult
|
|
@@ -13,17 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* @param {
|
|
16
|
+
* @param {ReadSourceFileResult} abstracts
|
|
17
17
|
* @param {VotiveConfig} config
|
|
18
18
|
* @param {Database} database
|
|
19
19
|
* @param {FlatProcessors} processors
|
|
20
20
|
* @returns {{processedAbstracts: Abstracts, abstractsJobs: Jobs}}
|
|
21
21
|
*/
|
|
22
|
-
function readAbstracts(
|
|
22
|
+
function readAbstracts(abstracts, config, database, processors) {
|
|
23
23
|
|
|
24
|
-
const processed =
|
|
25
|
-
|
|
26
|
-
const { abstract: unprocessedAbstract } = file
|
|
24
|
+
const processed = abstracts.flatMap(({ abstract: unprocessedAbstract, syntax }) => {
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
27
|
* @param {FlatProcessors} processors
|
|
@@ -34,14 +32,13 @@ function readAbstracts(files, config, database, processors) {
|
|
|
34
32
|
const [flatProcessor, ...rest] = processors
|
|
35
33
|
if (!flatProcessor) return { abstract, jobs }
|
|
36
34
|
const { processor, plugin } = flatProcessor
|
|
37
|
-
if (
|
|
38
|
-
|| !processor.
|
|
35
|
+
if (processor.syntax !== syntax
|
|
36
|
+
|| !processor.read
|
|
37
|
+
|| !processor.read.abstract
|
|
39
38
|
) return recursiveProcess(rest, abstract, jobs)
|
|
40
39
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const processed = processor.transformFile(abstract, database, config)
|
|
44
|
-
processed.jobs && processed.jobs.forEach(job => job.syntax = file.syntax)
|
|
40
|
+
const processed = processor.read.abstract(abstract, database, config)
|
|
41
|
+
processed.jobs && processed.jobs.forEach(job => job.plugin = plugin.name)
|
|
45
42
|
return recursiveProcess(rest, processed.abstract, [...jobs, ...(processed.jobs || [])])
|
|
46
43
|
}
|
|
47
44
|
|
package/lib/readFolders.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
|
|
3
1
|
/** @import {VotiveConfig, FlatProcessors} from "./bundle.js" */
|
|
4
2
|
/** @import {Database} from "./createDatabase.js" */
|
|
5
3
|
/** @import {Dirent} from "node:fs" */
|
|
6
4
|
|
|
5
|
+
import path from "node:path"
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {Dirent[]} folders
|
|
@@ -13,35 +12,33 @@ import path from "node:path"
|
|
|
13
12
|
*/
|
|
14
13
|
function readFolders(folders = [], config, database, processors) {
|
|
15
14
|
|
|
16
|
-
const folderProcessors = processors.filter(({ processor }) => processor.
|
|
15
|
+
const folderProcessors = processors.filter(({ processor }) => processor.read && processor.read.folder)
|
|
17
16
|
|
|
18
17
|
const processed = folderProcessors.flatMap(({ processor, plugin }) => {
|
|
19
18
|
|
|
20
19
|
const jobs = folders.flatMap(folder => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const {
|
|
25
|
-
jobs.forEach(job => job.
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
database.
|
|
20
|
+
const folderPath = path.relative(config.sourceFolder, path.join(folder.parentPath, folder.name))
|
|
21
|
+
|
|
22
|
+
/** @ts-ignore `.read` is throwing a warning, but it's guarded above */
|
|
23
|
+
const { destinations, jobs } = processor.read.folder(folderPath, database, config)
|
|
24
|
+
jobs.forEach(job => job.plugin = plugin.name)
|
|
25
|
+
if (destinations) {
|
|
26
|
+
destinations.forEach(destination => {
|
|
27
|
+
database.createOrUpdateDestination(destination)
|
|
29
28
|
})
|
|
30
29
|
return jobs
|
|
31
30
|
}
|
|
32
31
|
})
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
const rootPath = path.relative(config.sourceFolder, "")
|
|
36
|
-
const rootFolder = processor.readFolder(rootPath, database, config, true)
|
|
33
|
+
const rootFolder = processor.read.folder("", database, config)
|
|
37
34
|
|
|
38
|
-
if (rootFolder.
|
|
39
|
-
rootFolder.
|
|
40
|
-
database.
|
|
35
|
+
if (rootFolder.destinations) {
|
|
36
|
+
rootFolder.destinations.forEach(destination => {
|
|
37
|
+
database.createOrUpdateDestination(destination)
|
|
41
38
|
})
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
rootFolder.jobs.forEach(job => job.
|
|
41
|
+
rootFolder.jobs.forEach(job => job.plugin = plugin.name)
|
|
45
42
|
|
|
46
43
|
if(rootFolder.jobs) jobs.push(... rootFolder.jobs)
|
|
47
44
|
return jobs
|
package/lib/readSources.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { decodeBuffer } from "encoding-sniffer"
|
|
2
2
|
import fs from "node:fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
|
-
import
|
|
4
|
+
import { visit } from "unist-util-visit"
|
|
5
5
|
|
|
6
|
-
/** @import {VotiveConfig, VotivePlugin, VotiveProcessor, FlatProcessors, Abstracts, Abstract, Jobs, ProcessorSyntax
|
|
6
|
+
/** @import {VotiveConfig, VotivePlugin, VotiveProcessor, FlatProcessors, Abstracts, Abstract, Jobs, ProcessorSyntax} from "./bundle.js" */
|
|
7
7
|
/** @import {Dirent} from "node:fs" */
|
|
8
8
|
/** @import {Database} from "./createDatabase.js" */
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef {object} ReadSourcesResult
|
|
12
12
|
* @property {Dirent[]} folders
|
|
13
|
-
* @property {
|
|
13
|
+
* @property {ReadSourceFilePlugin[]} sources
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -25,56 +25,24 @@ async function readSources(config, database, processors) {
|
|
|
25
25
|
recursive: true
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
const filteredDirents = (dirents || []).filter(fileFilter(config))
|
|
29
|
-
|
|
28
|
+
const filteredDirents = (dirents || []).filter(fileFilter(config, database))
|
|
30
29
|
const { files, folders } = Object.groupBy(filteredDirents, (dirent) => dirent.isFile() ? "files" : "folders")
|
|
31
|
-
const loadingFiles = files.map(a => path.normalize(path.format({ name: a.name, dir: a.parentPath })))
|
|
32
30
|
if (!files) return { folders, sources: [] }
|
|
33
|
-
const limit = pLimit(5)
|
|
34
31
|
const readingSourceFiles = files.flatMap(readSourceFile(processors, database, config))
|
|
35
|
-
|
|
36
|
-
const reading = [
|
|
37
|
-
(await Promise.all(readingSourceFiles)).filter(a => a),
|
|
38
|
-
pruneDeletions(config, database, filteredDirents)
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
const [sources, deletedSources] = await Promise.all(reading)
|
|
42
|
-
|
|
32
|
+
const sources = readingSourceFiles && await Promise.all(readingSourceFiles)
|
|
43
33
|
return {
|
|
44
34
|
folders,
|
|
45
|
-
sources
|
|
35
|
+
sources
|
|
46
36
|
}
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
/**
|
|
50
40
|
* @param {VotiveConfig} config
|
|
51
41
|
* @param {Database} database
|
|
52
|
-
* @param {Dirent[]} dirents
|
|
53
|
-
*/
|
|
54
|
-
async function pruneDeletions(config, database, dirents) {
|
|
55
|
-
const sourceFilePaths = new Set(dirents.map(d => d.isFile() && path.join(d.parentPath, d.name)))
|
|
56
|
-
const sourceRecords = database.source.getAll()
|
|
57
|
-
const sourceRecordPaths = new Set(sourceRecords.map(r => r.path))
|
|
58
|
-
|
|
59
|
-
const deletions = sourceRecordPaths.difference(sourceFilePaths)
|
|
60
|
-
|
|
61
|
-
let deletedSources = []
|
|
62
|
-
|
|
63
|
-
deletions.forEach(deletion => {
|
|
64
|
-
deletedSources.push(database.source.delete(deletion))
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
return deletedSources.filter(a => a)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @param {VotiveConfig} config
|
|
72
42
|
*/
|
|
73
|
-
function fileFilter(config) {
|
|
43
|
+
function fileFilter(config, database) {
|
|
74
44
|
/** @param {Dirent} dirent */
|
|
75
45
|
return (dirent) => {
|
|
76
|
-
const isDestinationFolder = !path.relative(config.destinationFolder, path.join(dirent.parentPath, dirent.name))
|
|
77
|
-
|
|
78
46
|
if (dirent.parentPath === config.destinationFolder) {
|
|
79
47
|
return false
|
|
80
48
|
} else if (dirent.parentPath.startsWith(config.destinationFolder + path.sep)) {
|
|
@@ -83,28 +51,21 @@ function fileFilter(config) {
|
|
|
83
51
|
return false // Ignore hidden files
|
|
84
52
|
} else if (dirent.parentPath.includes(path.sep + ".")) {
|
|
85
53
|
return false // Ignore hidden folders
|
|
86
|
-
} else if (dirent.parentPath.match(/^\.\w/)) {
|
|
87
|
-
return false // Ignore hidden folders
|
|
88
|
-
} if (isDestinationFolder) {
|
|
89
|
-
return false
|
|
90
54
|
}
|
|
91
55
|
return true
|
|
92
56
|
}
|
|
93
57
|
}
|
|
94
58
|
|
|
95
59
|
/**
|
|
96
|
-
* @typedef {
|
|
60
|
+
* @typedef {ReadSourceFilePlugin[]} ReadSourceFileResult
|
|
97
61
|
*/
|
|
98
62
|
|
|
99
63
|
/**
|
|
100
|
-
* @typedef {object}
|
|
64
|
+
* @typedef {object} ReadSourceFilePlugin
|
|
101
65
|
* @property {Abstract} abstract
|
|
102
66
|
* @property {ProcessorSyntax} syntax
|
|
103
|
-
* @property {object} [metadata]
|
|
104
|
-
* @property {string} [targetFilePath]
|
|
105
|
-
* @property {string} [dir]
|
|
106
67
|
* @property {Jobs} jobs
|
|
107
|
-
* @property {string}
|
|
68
|
+
* @property {string} destinationPath
|
|
108
69
|
*/
|
|
109
70
|
|
|
110
71
|
/**
|
|
@@ -115,12 +76,12 @@ function fileFilter(config) {
|
|
|
115
76
|
function readSourceFile(processors, database, config) {
|
|
116
77
|
/**
|
|
117
78
|
* @param {import("node:fs").Dirent} dirent
|
|
118
|
-
* @returns {Promise<
|
|
79
|
+
* @returns {Promise<ReadSourceFilePlugin>[]}
|
|
119
80
|
*/
|
|
120
81
|
return (dirent) => {
|
|
121
82
|
const { name, parentPath } = dirent
|
|
122
|
-
const
|
|
123
|
-
const
|
|
83
|
+
const filePath = path.join(parentPath, name)
|
|
84
|
+
const fileInfo = path.parse(filePath)
|
|
124
85
|
|
|
125
86
|
const processing = processors.flatMap(({ plugin, processor }) => process(plugin, processor, config))
|
|
126
87
|
|
|
@@ -130,98 +91,43 @@ function readSourceFile(processors, database, config) {
|
|
|
130
91
|
* @param {VotiveConfig} config
|
|
131
92
|
*/
|
|
132
93
|
async function process(plugin, processor, config) {
|
|
133
|
-
const {
|
|
134
|
-
if (filter.includes(
|
|
94
|
+
const { read, filter, syntax } = processor
|
|
95
|
+
if (filter.extensions.includes(fileInfo.ext) && read && read.text) {
|
|
135
96
|
|
|
136
97
|
// Check modified time
|
|
137
|
-
const stat = await fs.stat(
|
|
138
|
-
const source = database.
|
|
139
|
-
const diff = source && source.lastModified - Number(Math.floor(stat.mtimeMs))
|
|
140
|
-
|
|
141
|
-
if (source && diff > -1) {
|
|
142
|
-
return null
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
database.setting.deleteBySource(sourceFilePath)
|
|
146
|
-
|
|
147
|
-
const targetFilePath = route(sourceFilePath, plugin, config)
|
|
148
|
-
const targetFileExtension = path.extname(targetFilePath)
|
|
149
|
-
|
|
150
|
-
// Set URL if exists
|
|
151
|
-
const allJobs = []
|
|
152
|
-
|
|
153
|
-
if (format === "buffer") {
|
|
154
|
-
|
|
155
|
-
// FIXME update this type
|
|
156
|
-
const data = read(sourceFilePath, database, config)
|
|
157
|
-
const { metadata, abstract, jobs } = data
|
|
98
|
+
const stat = await fs.stat(filePath)
|
|
99
|
+
const source = database.getSource(filePath)
|
|
158
100
|
|
|
159
|
-
const target = database.target.create({
|
|
160
|
-
metadata,
|
|
161
|
-
abstract,
|
|
162
|
-
path: targetFilePath,
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (Array.isArray(jobs)) {
|
|
167
|
-
allJobs.push(...jobs)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
101
|
|
|
171
|
-
if (
|
|
172
|
-
const stats = await fs.stat(sourceFilePath)
|
|
173
|
-
const data = await fs.readFile(sourceFilePath, { encoding: "utf-8" })
|
|
102
|
+
if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
|
|
174
103
|
|
|
175
|
-
|
|
104
|
+
database.deleteSettings(filePath)
|
|
176
105
|
|
|
106
|
+
// Get destination route
|
|
107
|
+
const destinationPath = route(filePath, plugin)
|
|
177
108
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
abstract,
|
|
184
|
-
path: targetFilePath,
|
|
185
|
-
metadata,
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
// FIXME this won't work with the refactor
|
|
189
|
-
allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
|
|
190
|
-
updateSource()
|
|
191
|
-
return {
|
|
192
|
-
abstract,
|
|
193
|
-
metadata,
|
|
194
|
-
syntax: targetFileExtension,
|
|
195
|
-
targetFilePath: target.path,
|
|
196
|
-
dir: target.dir,
|
|
197
|
-
sourceFilePath,
|
|
198
|
-
jobs: allJobs
|
|
199
|
-
}
|
|
200
|
-
}
|
|
109
|
+
// Set URL if exists
|
|
110
|
+
const buffer = await fs.readFile(path.format(fileInfo))
|
|
111
|
+
const data = decodeBuffer(buffer)
|
|
112
|
+
const { jobs, abstract, metadata } = read.text(data, filePath, database, config)
|
|
113
|
+
jobs && jobs.forEach(job => job.plugin = plugin.name)
|
|
201
114
|
|
|
202
|
-
|
|
203
|
-
allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
|
|
204
|
-
updateSource()
|
|
115
|
+
const timeStamp = stat.mtimeMs.toFixed()
|
|
205
116
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
sourceFilePath: null,
|
|
211
|
-
metadata: null,
|
|
212
|
-
syntax: null
|
|
117
|
+
if(source) {
|
|
118
|
+
database.updateSource(filePath, Number(timeStamp))
|
|
119
|
+
} else {
|
|
120
|
+
database.createSource(filePath, destinationPath, Number(timeStamp))
|
|
213
121
|
}
|
|
214
122
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
database.source.create(sourceFilePath, targetFilePath, Number(timeStamp))
|
|
222
|
-
}
|
|
223
|
-
}
|
|
123
|
+
database.createOrUpdateDestination({
|
|
124
|
+
metadata,
|
|
125
|
+
path: destinationPath,
|
|
126
|
+
abstract: abstract,
|
|
127
|
+
syntax: syntax
|
|
128
|
+
})
|
|
224
129
|
|
|
130
|
+
return { abstract, destinationPath, syntax, jobs }
|
|
225
131
|
}
|
|
226
132
|
}
|
|
227
133
|
|
|
@@ -232,43 +138,10 @@ function readSourceFile(processors, database, config) {
|
|
|
232
138
|
/**
|
|
233
139
|
* @param {string} filePath
|
|
234
140
|
* @param {VotivePlugin} plugin
|
|
235
|
-
* @param {VotiveConfig} config
|
|
236
141
|
*/
|
|
237
|
-
function route(filePath, plugin
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const segments = ["", ...dir.split(path.sep).filter(a => a)]
|
|
241
|
-
const pathInfo = {
|
|
242
|
-
inRootDir: rooty,
|
|
243
|
-
dir: segments,
|
|
244
|
-
...parsedPath
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!plugin.router) return "0"
|
|
248
|
-
|
|
249
|
-
const routedPath = plugin.router(pathInfo)
|
|
250
|
-
|
|
251
|
-
if (!routedPath) return "0"
|
|
252
|
-
|
|
253
|
-
if (routedPath.hasOwnProperty("dir") && Array.isArray(routedPath.dir)) {
|
|
254
|
-
return path.normalize(path.format({
|
|
255
|
-
dir: path.join(...routedPath.dir),
|
|
256
|
-
root: routedPath.root || "",
|
|
257
|
-
base: routedPath.base,
|
|
258
|
-
name: routedPath.name,
|
|
259
|
-
ext: routedPath.ext
|
|
260
|
-
}))
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const routedInfo = {
|
|
264
|
-
dir: routedPath.dir || "",
|
|
265
|
-
root: routedPath.root || "",
|
|
266
|
-
base: routedPath.base,
|
|
267
|
-
name: routedPath.name,
|
|
268
|
-
ext: routedPath.ext
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return path.normalize(path.format(routedInfo))
|
|
142
|
+
function route(filePath, plugin) {
|
|
143
|
+
if (!plugin.router) return ""
|
|
144
|
+
return plugin.router(filePath) || ""
|
|
272
145
|
}
|
|
273
146
|
|
|
274
147
|
export default readSources
|
package/lib/runJobs.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import workerpool from "workerpool"
|
|
2
|
+
|
|
1
3
|
/** @import {Job, Jobs, Database, VotiveConfig} from "./bundle.js" */
|
|
2
4
|
|
|
5
|
+
const pool = workerpool.pool()
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* @param {Jobs} jobs
|
|
@@ -7,35 +10,14 @@
|
|
|
7
10
|
* @param {Database} database
|
|
8
11
|
*/
|
|
9
12
|
async function runJobs(jobs, config, database) {
|
|
10
|
-
const
|
|
11
|
-
const running = jobs.flatMap(async job => {
|
|
13
|
+
const running = jobs.map(async job => {
|
|
12
14
|
if (!job) return
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const processing = processors.map(async processor => {
|
|
16
|
-
if (processor.syntax === job.syntax) {
|
|
17
|
-
try {
|
|
18
|
-
const response = await fetch(job.data)
|
|
19
|
-
if (response.status >= 200 && response.status < 300) {
|
|
20
|
-
const data = await response[job.runner]()
|
|
21
|
-
const processed = processor.fetcher(data)
|
|
22
|
-
database.createURL(job.data, processed, job.destination)
|
|
23
|
-
return
|
|
24
|
-
} else {
|
|
25
|
-
console.warn(`Error fetching URL: ${job.data}`)
|
|
26
|
-
}
|
|
27
|
-
} catch (e) {
|
|
28
|
-
console.error(e)
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
return Promise.allSettled(processing)
|
|
15
|
+
const plugin = config.plugins.find(plugin => plugin.name === job.plugin)
|
|
16
|
+
return pool.exec(plugin.runners[job.runner], [job.data])
|
|
35
17
|
})
|
|
36
18
|
|
|
37
|
-
const
|
|
38
|
-
|
|
19
|
+
const ran = await Promise.allSettled(running)
|
|
20
|
+
pool.terminate()
|
|
39
21
|
}
|
|
40
22
|
|
|
41
23
|
export default runJobs
|
package/lib/utils/index.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { styleText } from "node:util"
|
|
2
2
|
import { statSync } from "node:fs"
|
|
3
|
-
import path from "path"
|
|
4
3
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
* @param {string} message
|
|
8
|
-
* @param {boolean} verbose
|
|
9
|
-
*/
|
|
10
|
-
export function stopwatch(label, message, verbose) {
|
|
4
|
+
/** @param {string} label */
|
|
5
|
+
export function stopwatch(label) {
|
|
11
6
|
const start = performance.now()
|
|
12
7
|
|
|
13
8
|
function stop() {
|
|
14
9
|
const end = performance.now()
|
|
15
10
|
const duration = end - start
|
|
16
|
-
|
|
11
|
+
console.info(`${label}: ${styleText("red", duration.toFixed(2) + "ms")}`)
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
return stop
|
|
@@ -23,11 +18,8 @@ export function stopwatch(label, message, verbose) {
|
|
|
23
18
|
* @param {string} urlPath
|
|
24
19
|
*/
|
|
25
20
|
export function splitURL(urlPath) {
|
|
26
|
-
const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").
|
|
27
|
-
|
|
28
|
-
segments.push('')
|
|
29
|
-
const folder = path.relative("", segments.join(path.sep))
|
|
30
|
-
return folder
|
|
21
|
+
const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").reverse()
|
|
22
|
+
return [urlDirSegmentsReversed.reverse().join("/") || "/", urlFileName]
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
/** @param {string} dbPath */
|
package/lib/writeDestinations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
1
|
+
import { mkdir, writeFile, rm } from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { checkFile } from "./utils/index.js"
|
|
4
4
|
|
|
@@ -31,7 +31,15 @@ async function writeDestinations(config, database) {
|
|
|
31
31
|
return writeProcessors.map(async processor => {
|
|
32
32
|
if (processor.extensions.includes(destination.syntax)) {
|
|
33
33
|
const writeInfo = await processor.writeFile(destination, database, config)
|
|
34
|
-
if (!writeInfo)
|
|
34
|
+
if (!writeInfo) {
|
|
35
|
+
try {
|
|
36
|
+
database.target.delete(destination.path)
|
|
37
|
+
return await rm(destinationPath)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(e)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
const { data, encoding = 'utf-8' } = writeInfo
|
|
36
44
|
|
|
37
45
|
async function write() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "votive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A file processor.",
|
|
5
5
|
"homepage": "https://github.com/samlfair/votive#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"encoding-sniffer": "^0.2.1",
|
|
27
|
-
"p-limit": "^7.3.0",
|
|
28
27
|
"unified": "^11.0.5",
|
|
29
28
|
"unist-util-visit": "^5.0.0",
|
|
30
29
|
"workerpool": "^10.0.1"
|
package/tests/index.js
CHANGED
|
@@ -13,84 +13,21 @@ import { checkFile } from "./../lib/utils/index.js"
|
|
|
13
13
|
|
|
14
14
|
process.chdir("./tests")
|
|
15
15
|
|
|
16
|
-
test("
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
t.test("database functions", () => {
|
|
20
|
-
const databaseKeys = new Set(Object.keys(database))
|
|
21
|
-
const expectedKeys = new Set(["saveDB", "source", "setting", "dependency", "target", "url"])
|
|
22
|
-
const difference = databaseKeys.difference(expectedKeys)
|
|
23
|
-
assert(difference.size === 0)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
t.test("create sources", () => {
|
|
27
|
-
database.source.create("abc.in", "abc.out", 1)
|
|
28
|
-
database.source.create("def.in", "def.out", 2)
|
|
29
|
-
const sources = database.source.getAll()
|
|
30
|
-
assert(sources.length === 2)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
t.test("create targets", () => {
|
|
34
|
-
database.target.create({
|
|
35
|
-
path: "abc.out",
|
|
36
|
-
abstract: ["abc"],
|
|
37
|
-
metadata: {
|
|
38
|
-
color: "blue",
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
database.target.create({
|
|
43
|
-
path: "def.out",
|
|
44
|
-
abstract: ["def"],
|
|
45
|
-
metadata: {
|
|
46
|
-
color: "red",
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const targets = database.target.getAll()
|
|
51
|
-
|
|
52
|
-
assert(targets.length === 2)
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test("bundle", async () => {
|
|
57
|
-
const tempDest = fs.mkdtempDisposableSync("target-")
|
|
16
|
+
test("empty directory", async () => {
|
|
17
|
+
const tempDest = fs.mkdtempDisposableSync("destination-")
|
|
58
18
|
const tempSource = fs.mkdtempDisposableSync("source-")
|
|
59
19
|
|
|
60
|
-
fs.writeFileSync(
|
|
61
|
-
path.join(tempSource.path, "abc.in"),
|
|
62
|
-
JSON.stringify({ abstract: ["abc"], metadata: { color: "blue" } }),
|
|
63
|
-
"utf-8"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
20
|
try {
|
|
67
|
-
const stop = stopwatch("Bundle folder")
|
|
68
|
-
|
|
21
|
+
const stop = stopwatch("Bundle empty folder")
|
|
22
|
+
await votive.bundle({
|
|
69
23
|
sourceFolder: tempSource.path,
|
|
70
24
|
destinationFolder: tempDest.path,
|
|
71
|
-
plugins: [
|
|
72
|
-
name: "test-plugin",
|
|
73
|
-
processors: [{
|
|
74
|
-
format: "text",
|
|
75
|
-
extensions: [".in"],
|
|
76
|
-
// readResource: null,
|
|
77
|
-
writeFile: (destination) => ({ data: destination.abstract[0] }),
|
|
78
|
-
readFile: (data) => ({ abstract: [data], metadata: { color: "green" } }),
|
|
79
|
-
// readFolder: null,
|
|
80
|
-
// transformFile: null
|
|
81
|
-
}],
|
|
82
|
-
router: ({ base, dir, root }) => ({ base, root, ext: ".out" }),
|
|
83
|
-
}]
|
|
84
|
-
|
|
25
|
+
plugins: []
|
|
85
26
|
})
|
|
86
27
|
|
|
87
|
-
const sources = database.source.getAll()
|
|
88
|
-
|
|
89
|
-
const targets = database.target.getAll()
|
|
90
|
-
|
|
91
28
|
stop()
|
|
92
29
|
|
|
93
|
-
const dir = fs.readdirSync(tempDest.path
|
|
30
|
+
const dir = fs.readdirSync(tempDest.path)
|
|
94
31
|
assert(dir.length === 0, "Destination directory is not empty.")
|
|
95
32
|
} catch (error) {
|
|
96
33
|
tempDest.remove()
|
|
@@ -103,275 +40,281 @@ test("bundle", async () => {
|
|
|
103
40
|
tempSource.remove()
|
|
104
41
|
})
|
|
105
42
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// /** @type {FlatProcessors} */
|
|
209
|
-
// const processors = [
|
|
210
|
-
// {
|
|
211
|
-
// plugin: examplePlugin,
|
|
212
|
-
// processor: exampleProcessor
|
|
213
|
-
// }
|
|
214
|
-
// ]
|
|
215
|
-
|
|
216
|
-
// try {
|
|
217
|
-
// fs.writeFileSync("markdown/prunee.md", "A little content")
|
|
218
|
-
|
|
219
|
-
// const database = votive.createDatabase()
|
|
220
|
-
// const sourcesOne = database.getAllSources()
|
|
221
|
-
|
|
222
|
-
// t.test("no sources on startup", () => {
|
|
223
|
-
// const isArray = Array.isArray(sourcesOne)
|
|
224
|
-
// const isEmpty = !sourcesOne.length
|
|
225
|
-
// assert(isArray && isEmpty)
|
|
226
|
-
// })
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// const stop = stopwatch("Read sources")
|
|
231
|
-
|
|
232
|
-
// const { folders, sources } = await votive.readSources(config, database, processors)
|
|
233
|
-
|
|
234
|
-
// t.test('one folder exists', () => {
|
|
235
|
-
// assert(folders.length === 1)
|
|
236
|
-
// })
|
|
237
|
-
|
|
238
|
-
// t.test('three sources exist', () => {
|
|
239
|
-
// assert(sources.length === 4)
|
|
240
|
-
// })
|
|
241
|
-
|
|
242
|
-
// stop()
|
|
243
|
-
|
|
244
|
-
// const sourcesJobs = sources.flatMap(source => source.jobs)
|
|
43
|
+
test("internals", async (t) => {
|
|
44
|
+
const dbExists = checkFile(".votive.db")
|
|
45
|
+
if (dbExists) await rm(".votive.db")
|
|
46
|
+
|
|
47
|
+
const temp = fs.mkdtempDisposableSync("destination-")
|
|
48
|
+
|
|
49
|
+
/** @returns {Job} */
|
|
50
|
+
function createExampleJob() {
|
|
51
|
+
return {
|
|
52
|
+
data: Math.floor(Math.random() * 1000),
|
|
53
|
+
runner: "exampleRunner"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/** @type {ReadText} */
|
|
59
|
+
function exampleTextReader(text, filePath, database, config) {
|
|
60
|
+
const matches = text.match(/\b\w+\b/)
|
|
61
|
+
const title = matches ? matches[0] : "Untitled"
|
|
62
|
+
|
|
63
|
+
/** @type {ReadTextResult} */
|
|
64
|
+
return {
|
|
65
|
+
abstract: {
|
|
66
|
+
content: text,
|
|
67
|
+
},
|
|
68
|
+
metadata: {
|
|
69
|
+
title
|
|
70
|
+
},
|
|
71
|
+
jobs: [
|
|
72
|
+
createExampleJob()
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @type {ReadAbstract} */
|
|
78
|
+
function exampleAbstractReader(abstract, database, config) {
|
|
79
|
+
abstract.exampleAppend = true
|
|
80
|
+
return { abstract, jobs: [createExampleJob()] }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @type {ReadFolder} */
|
|
84
|
+
function exampleFolderReader(folder, database, config) {
|
|
85
|
+
return {
|
|
86
|
+
jobs: [createExampleJob()],
|
|
87
|
+
destinations: [
|
|
88
|
+
{
|
|
89
|
+
path: "/index.html",
|
|
90
|
+
abstract: { content: "" },
|
|
91
|
+
metadata: {
|
|
92
|
+
title: "home"
|
|
93
|
+
},
|
|
94
|
+
syntax: "md"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @type {VotiveProcessor} */
|
|
101
|
+
const exampleProcessor = {
|
|
102
|
+
syntax: "md",
|
|
103
|
+
filter: {
|
|
104
|
+
extensions: [".md"]
|
|
105
|
+
},
|
|
106
|
+
read: {
|
|
107
|
+
text: exampleTextReader,
|
|
108
|
+
abstract: exampleAbstractReader,
|
|
109
|
+
folder: exampleFolderReader
|
|
110
|
+
},
|
|
111
|
+
write: (destination, database, config) => {
|
|
112
|
+
return {
|
|
113
|
+
data: "lorem ipsum",
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @param {string} sourcePath */
|
|
119
|
+
function router(sourcePath) {
|
|
120
|
+
const { base, ...parsed }= path.parse(sourcePath)
|
|
121
|
+
return path.format({ ...parsed, ext: ".html" })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @type {VotivePlugin} */
|
|
125
|
+
const examplePlugin = {
|
|
126
|
+
name: "example plugin",
|
|
127
|
+
runners: {
|
|
128
|
+
exampleRunner: exampleRunner
|
|
129
|
+
},
|
|
130
|
+
router,
|
|
131
|
+
processors: [exampleProcessor]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @type {Runner} */
|
|
135
|
+
async function exampleRunner(data, database) {
|
|
136
|
+
return await new Promise((resolve) => setTimeout(() => resolve(data), 1))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @type {VotiveConfig} */
|
|
140
|
+
const config = {
|
|
141
|
+
sourceFolder: "./markdown",
|
|
142
|
+
destinationFolder: temp.path,
|
|
143
|
+
plugins: [examplePlugin]
|
|
144
|
+
}
|
|
245
145
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
146
|
+
/** @type {FlatProcessors} */
|
|
147
|
+
const processors = [
|
|
148
|
+
{
|
|
149
|
+
plugin: examplePlugin,
|
|
150
|
+
processor: exampleProcessor
|
|
151
|
+
}
|
|
152
|
+
]
|
|
251
153
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// assert(abstractsJobs.every(job => job.data && job.runner))
|
|
255
|
-
// })
|
|
154
|
+
try {
|
|
155
|
+
fs.writeFileSync("markdown/prunee.md", "A little content")
|
|
256
156
|
|
|
257
|
-
|
|
157
|
+
const database = votive.createDatabase()
|
|
158
|
+
const sourcesOne = database.getAllSources()
|
|
258
159
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
160
|
+
t.test("no sources on startup", () => {
|
|
161
|
+
const isArray = Array.isArray(sourcesOne)
|
|
162
|
+
const isEmpty = !sourcesOne.length
|
|
163
|
+
assert(isArray && isEmpty)
|
|
164
|
+
})
|
|
263
165
|
|
|
264
|
-
// database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.html", abstract: { c: 3 }, syntax: "md" })
|
|
265
|
-
// const firstDestination = database.getDestinationIndependently("abc.html", [])
|
|
266
166
|
|
|
267
|
-
// t.test('first destination created', () => {
|
|
268
|
-
// assert(firstDestination.path === 'abc.html')
|
|
269
|
-
// })
|
|
270
167
|
|
|
271
|
-
|
|
168
|
+
const stop = stopwatch("Read sources")
|
|
272
169
|
|
|
273
|
-
|
|
170
|
+
const { folders, sources } = await votive.readSources(config, database, processors)
|
|
274
171
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// const folderSetting = database.setSetting("abc", "theme", "red", "markdown/prunee.md")
|
|
172
|
+
t.test('one folder exists', () => {
|
|
173
|
+
assert(folders.length === 1)
|
|
174
|
+
})
|
|
279
175
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// assert(!sameSetting)
|
|
284
|
-
// })
|
|
176
|
+
t.test('three sources exist', () => {
|
|
177
|
+
assert(sources.length === 4)
|
|
178
|
+
})
|
|
285
179
|
|
|
286
|
-
|
|
180
|
+
stop()
|
|
287
181
|
|
|
288
|
-
|
|
289
|
-
// assert(abcSettings.theme[0] === "blue")
|
|
290
|
-
// })
|
|
182
|
+
const sourcesJobs = sources.flatMap(source => source.jobs)
|
|
291
183
|
|
|
292
|
-
|
|
293
|
-
// const defSettings = database.getSettings("abc/def.html")
|
|
184
|
+
const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
|
|
294
185
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// })
|
|
186
|
+
t.test('transformation succeeded', () => {
|
|
187
|
+
assert(processedAbstracts.find(abstract => abstract.exampleAppend))
|
|
188
|
+
})
|
|
299
189
|
|
|
300
|
-
|
|
190
|
+
t.test('abstract jobs exist', () => {
|
|
191
|
+
assert(abstractsJobs.length > 0)
|
|
192
|
+
assert(abstractsJobs.every(job => job.data && job.runner))
|
|
193
|
+
})
|
|
301
194
|
|
|
302
|
-
|
|
303
|
-
// assert(staleDestinations.length === 7)
|
|
304
|
-
// })
|
|
195
|
+
const foldersJobs = readFolders(folders, config, database, processors)
|
|
305
196
|
|
|
306
|
-
|
|
197
|
+
t.test("folders jobs exist", () => {
|
|
198
|
+
assert(foldersJobs.length > 0)
|
|
199
|
+
assert(foldersJobs.every(job => job.data && job.runner))
|
|
200
|
+
})
|
|
307
201
|
|
|
308
|
-
|
|
202
|
+
database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.html", abstract: { c: 3 }, syntax: "md" })
|
|
203
|
+
const firstDestination = database.getDestinationIndependently("abc.html", [])
|
|
309
204
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
205
|
+
t.test('first destination created', () => {
|
|
206
|
+
assert(firstDestination.path === 'abc.html')
|
|
207
|
+
})
|
|
313
208
|
|
|
314
|
-
|
|
315
|
-
|
|
209
|
+
database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "abc/def.html", abstract: { c: 5 }, syntax: "md" })
|
|
210
|
+
const destination = database.getDestinationDependently("abc.html", ["a"], "abc/def.html")
|
|
316
211
|
|
|
317
|
-
|
|
318
|
-
// assert(staleAfterAbstractUpdate.length === 1)
|
|
319
|
-
// })
|
|
212
|
+
const dependencies = database.getDependencies()
|
|
320
213
|
|
|
321
|
-
|
|
322
|
-
|
|
214
|
+
t.test('dependency created', () => {
|
|
215
|
+
assert(dependencies[0].dependent === 'abc/def.html')
|
|
216
|
+
})
|
|
323
217
|
|
|
324
|
-
// t.test('everything fresh again', () => {
|
|
325
|
-
// assert(staleAfterSecondWrite.length === 0)
|
|
326
|
-
// })
|
|
327
218
|
|
|
328
|
-
|
|
329
|
-
|
|
219
|
+
const setting = database.setSetting("abc.html", "theme", "blue", "markdown/prunee.md")
|
|
220
|
+
const newSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
|
|
221
|
+
const sameSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
|
|
222
|
+
const folderSetting = database.setSetting("abc", "theme", "red", "markdown/prunee.md")
|
|
330
223
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
224
|
+
t.test('settings created', () => {
|
|
225
|
+
assert(setting.value === 'blue')
|
|
226
|
+
assert(newSetting.value === 'Dog')
|
|
227
|
+
assert(!sameSetting)
|
|
228
|
+
})
|
|
334
229
|
|
|
230
|
+
const abcSettings = database.getSettings("abc.html")
|
|
335
231
|
|
|
336
|
-
|
|
232
|
+
t.test('settings retrieved', () => {
|
|
233
|
+
assert(abcSettings.theme[0] === "blue")
|
|
234
|
+
})
|
|
337
235
|
|
|
338
|
-
|
|
236
|
+
const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
|
|
237
|
+
const defSettings = database.getSettings("abc/def.html")
|
|
339
238
|
|
|
340
|
-
|
|
239
|
+
t.test('settings retrieved', () => {
|
|
240
|
+
assert(abcSettings.theme[0] === "blue")
|
|
241
|
+
assert(defSettings.theme[1] === "green")
|
|
242
|
+
})
|
|
341
243
|
|
|
342
|
-
|
|
343
|
-
// assert(staleAfterSettingsChange.length === 1)
|
|
344
|
-
// })
|
|
244
|
+
const staleDestinations = database.getStaleDestinations()
|
|
345
245
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
246
|
+
t.test('all destinations are stale', () => {
|
|
247
|
+
assert(staleDestinations.length === 7)
|
|
248
|
+
})
|
|
349
249
|
|
|
350
|
-
|
|
250
|
+
const written = await votive.writeDestinations(config, database)
|
|
351
251
|
|
|
352
|
-
|
|
252
|
+
const staleDestinationsAfterWriting = database.getStaleDestinations()
|
|
353
253
|
|
|
354
|
-
|
|
254
|
+
t.test('all destinations are fresh', () => {
|
|
255
|
+
assert(staleDestinationsAfterWriting.length === 0)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const updated = database.createOrUpdateDestination({ metadata: { a: 1, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
|
|
259
|
+
const staleAfterAbstractUpdate = database.getStaleDestinations()
|
|
355
260
|
|
|
356
|
-
|
|
261
|
+
t.test('updated document with no side effects is stale', () => {
|
|
262
|
+
assert(staleAfterAbstractUpdate.length === 1)
|
|
263
|
+
})
|
|
357
264
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// {
|
|
361
|
-
// property: "a",
|
|
362
|
-
// operator: "gt",
|
|
363
|
-
// value: 3
|
|
364
|
-
// }
|
|
365
|
-
// ]
|
|
366
|
-
// }, "markdown/prunee.html")
|
|
265
|
+
await votive.writeDestinations(config, database)
|
|
266
|
+
const staleAfterSecondWrite = database.getStaleDestinations()
|
|
367
267
|
|
|
368
|
-
|
|
268
|
+
t.test('everything fresh again', () => {
|
|
269
|
+
assert(staleAfterSecondWrite.length === 0)
|
|
270
|
+
})
|
|
369
271
|
|
|
370
|
-
|
|
272
|
+
const updatedWithSideEffects = database.createOrUpdateDestination({ metadata: { a: 4, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
|
|
273
|
+
const staleAfterSideEffects = database.getStaleDestinations()
|
|
371
274
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
275
|
+
t.test('side effects work', () => {
|
|
276
|
+
assert(staleAfterSideEffects.length === 2)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const freshDestinations = database.getAllDestinations()
|
|
280
|
+
|
|
281
|
+
await votive.writeDestinations(config, database)
|
|
282
|
+
|
|
283
|
+
const oldSetting = database.setSetting("abc", "theme", "green")
|
|
284
|
+
|
|
285
|
+
const staleAfterSettingsChange = database.getStaleDestinations()
|
|
286
|
+
|
|
287
|
+
t.test('setting affects descendents', () => {
|
|
288
|
+
assert(staleAfterSettingsChange.length === 1)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// TODO: Write tests for jobs
|
|
292
|
+
const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
|
|
293
|
+
runJobs(jobs, config, database)
|
|
294
|
+
|
|
295
|
+
const destinations = database.getAllDestinations()
|
|
296
|
+
database.getDestinationDependently("markdown/prunee.html", ["title"], "abc/def.html")
|
|
297
|
+
const newDependencies = database.getDependencies()
|
|
298
|
+
|
|
299
|
+
fs.rmSync("markdown/prunee.md")
|
|
300
|
+
|
|
301
|
+
await votive.pruneSources(config, database)
|
|
302
|
+
|
|
303
|
+
const result = database.getDestinationIndependently("markdown/prunee.html")
|
|
304
|
+
const prunedDependencies = database.getDependencies()
|
|
305
|
+
const prunedMetadata = database.getAllMetadataByPath("markdown/prunee.html")
|
|
306
|
+
|
|
307
|
+
t.test('source pruned', () => {
|
|
308
|
+
assert(!result)
|
|
309
|
+
assert(newDependencies.length - prunedDependencies.length === 1)
|
|
310
|
+
assert(!prunedMetadata)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
await database.saveDB()
|
|
314
|
+
|
|
315
|
+
temp.remove()
|
|
316
|
+
} catch (error) {
|
|
317
|
+
temp.remove()
|
|
318
|
+
throw error
|
|
319
|
+
}
|
|
320
|
+
})
|