votive 0.3.2 → 0.4.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/README.md +11 -4
- package/lib/bundle.js +1 -2
- package/lib/createDatabase.js +38 -37
- package/lib/readAbstracts.js +12 -9
- package/lib/readFolders.js +18 -15
- package/lib/readSources.js +168 -41
- package/lib/runJobs.js +26 -8
- package/lib/utils/index.js +13 -5
- package/lib/writeDestinations.js +0 -1
- package/package.json +2 -1
- package/tests/index.js +302 -245
- package/lib/cleanDeletions.js +0 -32
package/README.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
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
|
+
|
|
3
8
|
## Roadmap
|
|
4
9
|
|
|
5
|
-
- [ ] ReadPaths
|
|
6
10
|
- [ ] Flesh out job runners
|
|
7
|
-
- [ ]
|
|
11
|
+
- [ ] Rename jobs and paths
|
|
12
|
+
- [ ] Better dependency tracking
|
|
13
|
+
- [ ] Better query filters
|
|
14
|
+
- [x] File deletion handling
|
|
8
15
|
|
|
9
|
-
##
|
|
16
|
+
## Project: Jobs
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
Jobs was originally a generic concept, but after working it's clear that there are two main categories of jobs: async writes and data fetching. We can probably handle async writes with inbuilt logic. So, instead of a "job", we should have "read uri"? That way we can cache all the uris.
|
package/lib/bundle.js
CHANGED
|
@@ -245,8 +245,6 @@ 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
|
-
|
|
250
248
|
writeTime()
|
|
251
249
|
|
|
252
250
|
// database.commit()
|
|
@@ -282,6 +280,7 @@ async function bundler(config) {
|
|
|
282
280
|
queue.push(bundle(config))
|
|
283
281
|
cache = await queue[0]
|
|
284
282
|
} else {
|
|
283
|
+
"cache"
|
|
285
284
|
queue.push(bundle(config, cache))
|
|
286
285
|
await queue[0]
|
|
287
286
|
}
|
package/lib/createDatabase.js
CHANGED
|
@@ -264,43 +264,43 @@ function prepareStatements(database) {
|
|
|
264
264
|
ORDER BY d.path ASC;
|
|
265
265
|
`))),
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
)
|
|
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
|
|
281
280
|
)
|
|
282
281
|
)
|
|
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
282
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
*/
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
306
|
* @callback SQLiteTargetGetAll
|
|
@@ -746,13 +746,14 @@ function createDatabase(databasePath = ".votive.db") {
|
|
|
746
746
|
* @param {string} filePath
|
|
747
747
|
*/
|
|
748
748
|
delete(filePath) {
|
|
749
|
-
|
|
750
|
-
prepared.target.delete.get(filePath)
|
|
749
|
+
console.log("deleting")
|
|
750
|
+
const deleted = prepared.target.delete.get(filePath)
|
|
751
751
|
prepared.metadata.deleteByTarget.all(filePath)
|
|
752
752
|
const deps = queries.dependency.getAllByTarget(filePath)
|
|
753
753
|
deps.forEach(dep => {
|
|
754
754
|
queries.target.markStale(dep.dependent)
|
|
755
755
|
})
|
|
756
|
+
return deleted
|
|
756
757
|
},
|
|
757
758
|
|
|
758
759
|
/**
|
|
@@ -816,7 +817,7 @@ function createDatabase(databasePath = ".votive.db") {
|
|
|
816
817
|
getWithTrackers(filePath, dependent) {
|
|
817
818
|
const target = queries.target.get(filePath)
|
|
818
819
|
|
|
819
|
-
if
|
|
820
|
+
if(!target) return
|
|
820
821
|
|
|
821
822
|
const trackedTarget = { metadata: {} }
|
|
822
823
|
|
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 {ReadSourceFilesResult} from "./readSources.js"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {object} ReadAbstractsResult
|
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* @param {
|
|
16
|
+
* @param {ReadSourceFilesResult} files
|
|
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(files, config, database, processors) {
|
|
23
23
|
|
|
24
|
-
const processed =
|
|
24
|
+
const processed = files.flatMap(file => {
|
|
25
|
+
|
|
26
|
+
const { abstract: unprocessedAbstract } = file
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* @param {FlatProcessors} processors
|
|
@@ -32,13 +34,14 @@ function readAbstracts(abstracts, config, database, processors) {
|
|
|
32
34
|
const [flatProcessor, ...rest] = processors
|
|
33
35
|
if (!flatProcessor) return { abstract, jobs }
|
|
34
36
|
const { processor, plugin } = flatProcessor
|
|
35
|
-
if (processor.syntax
|
|
36
|
-
|| !processor.
|
|
37
|
-
|| !processor.read.abstract
|
|
37
|
+
if (!processor.extensions.includes(file.syntax)
|
|
38
|
+
|| !processor.transformFile
|
|
38
39
|
) return recursiveProcess(rest, abstract, jobs)
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const settings = database.setting.getByFolder(file.dir)
|
|
42
|
+
|
|
43
|
+
const processed = processor.transformFile(abstract, database, config)
|
|
44
|
+
processed.jobs && processed.jobs.forEach(job => job.syntax = file.syntax)
|
|
42
45
|
return recursiveProcess(rest, processed.abstract, [...jobs, ...(processed.jobs || [])])
|
|
43
46
|
}
|
|
44
47
|
|
package/lib/readFolders.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
1
3
|
/** @import {VotiveConfig, FlatProcessors} from "./bundle.js" */
|
|
2
4
|
/** @import {Database} from "./createDatabase.js" */
|
|
3
5
|
/** @import {Dirent} from "node:fs" */
|
|
4
6
|
|
|
5
|
-
import path from "node:path"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @param {Dirent[]} folders
|
|
@@ -12,33 +13,35 @@ import path from "node:path"
|
|
|
12
13
|
*/
|
|
13
14
|
function readFolders(folders = [], config, database, processors) {
|
|
14
15
|
|
|
15
|
-
const folderProcessors = processors.filter(({ processor }) => processor.
|
|
16
|
+
const folderProcessors = processors.filter(({ processor }) => processor.readFolder)
|
|
16
17
|
|
|
17
18
|
const processed = folderProcessors.flatMap(({ processor, plugin }) => {
|
|
18
19
|
|
|
19
20
|
const jobs = folders.flatMap(folder => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const {
|
|
24
|
-
jobs.forEach(job => job.
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
database.
|
|
21
|
+
let folderPath = path.join(folder.parentPath, folder.name)
|
|
22
|
+
if(folderPath) folderPath += path.sep
|
|
23
|
+
|
|
24
|
+
const { targets, jobs } = processor.readFolder(folderPath, database, config)
|
|
25
|
+
jobs.forEach(job => job.syntax = processor.syntax)
|
|
26
|
+
if (targets) {
|
|
27
|
+
targets.forEach(target => {
|
|
28
|
+
database.target.create(target)
|
|
28
29
|
})
|
|
29
30
|
return jobs
|
|
30
31
|
}
|
|
31
32
|
})
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
const rootPath = path.relative(config.sourceFolder, "")
|
|
36
|
+
const rootFolder = processor.readFolder(rootPath, database, config, true)
|
|
34
37
|
|
|
35
|
-
if (rootFolder.
|
|
36
|
-
rootFolder.
|
|
37
|
-
database.
|
|
38
|
+
if (rootFolder.targets) {
|
|
39
|
+
rootFolder.targets.forEach(target => {
|
|
40
|
+
database.target.create(target)
|
|
38
41
|
})
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
rootFolder.jobs.forEach(job => job.
|
|
44
|
+
rootFolder.jobs.forEach(job => job.syntax = processor.syntax)
|
|
42
45
|
|
|
43
46
|
if(rootFolder.jobs) jobs.push(... rootFolder.jobs)
|
|
44
47
|
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 pLimit from "p-limit"
|
|
5
5
|
|
|
6
|
-
/** @import {VotiveConfig, VotivePlugin, VotiveProcessor, FlatProcessors, Abstracts, Abstract, Jobs, ProcessorSyntax} from "./bundle.js" */
|
|
6
|
+
/** @import {VotiveConfig, VotivePlugin, VotiveProcessor, FlatProcessors, Abstracts, Abstract, Jobs, ProcessorSyntax, Router} 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 {ReadSourceFileResult[]} sources
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -25,24 +25,56 @@ async function readSources(config, database, processors) {
|
|
|
25
25
|
recursive: true
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
const filteredDirents = (dirents || []).filter(fileFilter(config
|
|
28
|
+
const filteredDirents = (dirents || []).filter(fileFilter(config))
|
|
29
|
+
|
|
29
30
|
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 })))
|
|
30
32
|
if (!files) return { folders, sources: [] }
|
|
33
|
+
const limit = pLimit(5)
|
|
31
34
|
const readingSourceFiles = files.flatMap(readSourceFile(processors, database, config))
|
|
32
|
-
|
|
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
|
+
|
|
33
43
|
return {
|
|
34
44
|
folders,
|
|
35
|
-
sources
|
|
45
|
+
sources: [...sources, ...deletedSources]
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/**
|
|
40
50
|
* @param {VotiveConfig} config
|
|
41
51
|
* @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
|
|
42
72
|
*/
|
|
43
|
-
function fileFilter(config
|
|
73
|
+
function fileFilter(config) {
|
|
44
74
|
/** @param {Dirent} dirent */
|
|
45
75
|
return (dirent) => {
|
|
76
|
+
const isDestinationFolder = !path.relative(config.destinationFolder, path.join(dirent.parentPath, dirent.name))
|
|
77
|
+
|
|
46
78
|
if (dirent.parentPath === config.destinationFolder) {
|
|
47
79
|
return false
|
|
48
80
|
} else if (dirent.parentPath.startsWith(config.destinationFolder + path.sep)) {
|
|
@@ -51,21 +83,28 @@ function fileFilter(config, database) {
|
|
|
51
83
|
return false // Ignore hidden files
|
|
52
84
|
} else if (dirent.parentPath.includes(path.sep + ".")) {
|
|
53
85
|
return false // Ignore hidden folders
|
|
86
|
+
} else if (dirent.parentPath.match(/^\.\w/)) {
|
|
87
|
+
return false // Ignore hidden folders
|
|
88
|
+
} if (isDestinationFolder) {
|
|
89
|
+
return false
|
|
54
90
|
}
|
|
55
91
|
return true
|
|
56
92
|
}
|
|
57
93
|
}
|
|
58
94
|
|
|
59
95
|
/**
|
|
60
|
-
* @typedef {
|
|
96
|
+
* @typedef {ReadSourceFileResult[]} ReadSourceFilesResult
|
|
61
97
|
*/
|
|
62
98
|
|
|
63
99
|
/**
|
|
64
|
-
* @typedef {object}
|
|
100
|
+
* @typedef {object} ReadSourceFileResult
|
|
65
101
|
* @property {Abstract} abstract
|
|
66
102
|
* @property {ProcessorSyntax} syntax
|
|
103
|
+
* @property {object} [metadata]
|
|
104
|
+
* @property {string} [targetFilePath]
|
|
105
|
+
* @property {string} [dir]
|
|
67
106
|
* @property {Jobs} jobs
|
|
68
|
-
* @property {string}
|
|
107
|
+
* @property {string} sourceFilePath
|
|
69
108
|
*/
|
|
70
109
|
|
|
71
110
|
/**
|
|
@@ -76,12 +115,12 @@ function fileFilter(config, database) {
|
|
|
76
115
|
function readSourceFile(processors, database, config) {
|
|
77
116
|
/**
|
|
78
117
|
* @param {import("node:fs").Dirent} dirent
|
|
79
|
-
* @returns {Promise<
|
|
118
|
+
* @returns {Promise<ReadSourceFileResult>[]}
|
|
80
119
|
*/
|
|
81
120
|
return (dirent) => {
|
|
82
121
|
const { name, parentPath } = dirent
|
|
83
|
-
const
|
|
84
|
-
const
|
|
122
|
+
const sourceFilePath = path.join(parentPath, name)
|
|
123
|
+
const sourceFileInfo = path.parse(sourceFilePath)
|
|
85
124
|
|
|
86
125
|
const processing = processors.flatMap(({ plugin, processor }) => process(plugin, processor, config))
|
|
87
126
|
|
|
@@ -91,43 +130,98 @@ function readSourceFile(processors, database, config) {
|
|
|
91
130
|
* @param {VotiveConfig} config
|
|
92
131
|
*/
|
|
93
132
|
async function process(plugin, processor, config) {
|
|
94
|
-
const { read, filter,
|
|
95
|
-
if (filter.
|
|
133
|
+
const { readFile: read, extensions: filter, format } = processor
|
|
134
|
+
if (filter.includes(sourceFileInfo.ext) && read) {
|
|
96
135
|
|
|
97
136
|
// Check modified time
|
|
98
|
-
const stat = await fs.stat(
|
|
99
|
-
const source = database.
|
|
137
|
+
const stat = await fs.stat(sourceFilePath)
|
|
138
|
+
const source = database.source.get(sourceFilePath)
|
|
139
|
+
const diff = source && source.lastModified - Number(Math.floor(stat.mtimeMs))
|
|
100
140
|
|
|
141
|
+
if (source && diff > -1) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
database.deleteSettings(filePath)
|
|
145
|
+
database.setting.deleteBySource(sourceFilePath)
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
const
|
|
147
|
+
const targetFilePath = route(sourceFilePath, plugin, config)
|
|
148
|
+
const targetFileExtension = path.extname(targetFilePath)
|
|
108
149
|
|
|
109
150
|
// Set URL if exists
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
158
|
+
|
|
159
|
+
const target = database.target.create({
|
|
160
|
+
metadata,
|
|
161
|
+
abstract,
|
|
162
|
+
path: targetFilePath,
|
|
163
|
+
})
|
|
114
164
|
|
|
115
|
-
const timeStamp = stat.mtimeMs.toFixed()
|
|
116
165
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
database.createSource(filePath, destinationPath, Number(timeStamp))
|
|
166
|
+
if (Array.isArray(jobs)) {
|
|
167
|
+
allJobs.push(...jobs)
|
|
168
|
+
}
|
|
121
169
|
}
|
|
122
170
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
171
|
+
if (format === "text") {
|
|
172
|
+
const stats = await fs.stat(sourceFilePath)
|
|
173
|
+
const data = await fs.readFile(sourceFilePath, { encoding: "utf-8" })
|
|
174
|
+
|
|
175
|
+
const { jobs, abstract, metadata } = read(data, sourceFilePath, targetFilePath, database, config)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(jobs)) {
|
|
179
|
+
allJobs.push(...jobs)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const target = database.target.create({
|
|
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
|
+
}
|
|
201
|
+
|
|
202
|
+
// FIXME Job syntax
|
|
203
|
+
allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
|
|
204
|
+
updateSource()
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
jobs: allJobs,
|
|
208
|
+
abstract: null,
|
|
209
|
+
targetFilePath: null,
|
|
210
|
+
sourceFilePath: null,
|
|
211
|
+
metadata: null,
|
|
212
|
+
syntax: null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function updateSource() {
|
|
216
|
+
const timeStamp = stat.mtimeMs.toFixed()
|
|
217
|
+
|
|
218
|
+
if (source) {
|
|
219
|
+
database.source.updateTimestamp(sourceFilePath, Number(timeStamp))
|
|
220
|
+
} else {
|
|
221
|
+
database.source.create(sourceFilePath, targetFilePath, Number(timeStamp))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
129
224
|
|
|
130
|
-
return { abstract, destinationPath, syntax, jobs }
|
|
131
225
|
}
|
|
132
226
|
}
|
|
133
227
|
|
|
@@ -138,10 +232,43 @@ function readSourceFile(processors, database, config) {
|
|
|
138
232
|
/**
|
|
139
233
|
* @param {string} filePath
|
|
140
234
|
* @param {VotivePlugin} plugin
|
|
235
|
+
* @param {VotiveConfig} config
|
|
141
236
|
*/
|
|
142
|
-
function route(filePath, plugin) {
|
|
143
|
-
|
|
144
|
-
|
|
237
|
+
function route(filePath, plugin, config) {
|
|
238
|
+
const { dir, ...parsedPath } = path.parse(filePath)
|
|
239
|
+
const rooty = !path.relative(config.sourceFolder, dir)
|
|
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))
|
|
145
272
|
}
|
|
146
273
|
|
|
147
274
|
export default readSources
|
package/lib/runJobs.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import workerpool from "workerpool"
|
|
2
|
-
|
|
3
1
|
/** @import {Job, Jobs, Database, VotiveConfig} from "./bundle.js" */
|
|
4
2
|
|
|
5
|
-
const pool = workerpool.pool()
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* @param {Jobs} jobs
|
|
@@ -10,14 +7,35 @@ const pool = workerpool.pool()
|
|
|
10
7
|
* @param {Database} database
|
|
11
8
|
*/
|
|
12
9
|
async function runJobs(jobs, config, database) {
|
|
13
|
-
const
|
|
10
|
+
const processors = config.plugins.flatMap(plugin => plugin.processors.flatMap(processor => processor.read?.url && ({ fetcher: processor.read.url, syntax: processor.syntax }))).filter(a => a)
|
|
11
|
+
const running = jobs.flatMap(async job => {
|
|
14
12
|
if (!job) return
|
|
15
|
-
const
|
|
16
|
-
|
|
13
|
+
const cachedURL = database.getURL(job.data)
|
|
14
|
+
if (cachedURL) return
|
|
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)
|
|
17
35
|
})
|
|
18
36
|
|
|
19
|
-
const
|
|
20
|
-
|
|
37
|
+
const settled = await Promise.allSettled(running)
|
|
38
|
+
return settled
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
export default runJobs
|
package/lib/utils/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { styleText } from "node:util"
|
|
2
2
|
import { statSync } from "node:fs"
|
|
3
|
+
import path from "path"
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} label
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {boolean} verbose
|
|
9
|
+
*/
|
|
10
|
+
export function stopwatch(label, message, verbose) {
|
|
6
11
|
const start = performance.now()
|
|
7
12
|
|
|
8
13
|
function stop() {
|
|
9
14
|
const end = performance.now()
|
|
10
15
|
const duration = end - start
|
|
11
|
-
console.info(`${label}
|
|
16
|
+
if(verbose) console.info(`${styleText("dim", label + ":")} ${styleText("magenta", message)} ${styleText("magenta", duration.toFixed(2) + "ms")}`)
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
return stop
|
|
@@ -18,8 +23,11 @@ export function stopwatch(label) {
|
|
|
18
23
|
* @param {string} urlPath
|
|
19
24
|
*/
|
|
20
25
|
export function splitURL(urlPath) {
|
|
21
|
-
const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").reverse()
|
|
22
|
-
|
|
26
|
+
const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").filter(a => a).reverse()
|
|
27
|
+
const segments = urlDirSegmentsReversed.reverse()
|
|
28
|
+
segments.push('')
|
|
29
|
+
const folder = path.relative("", segments.join(path.sep))
|
|
30
|
+
return folder
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
/** @param {string} dbPath */
|
package/lib/writeDestinations.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "votive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A file processor.",
|
|
5
5
|
"homepage": "https://github.com/samlfair/votive#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"encoding-sniffer": "^0.2.1",
|
|
27
|
+
"p-limit": "^7.3.0",
|
|
27
28
|
"unified": "^11.0.5",
|
|
28
29
|
"unist-util-visit": "^5.0.0",
|
|
29
30
|
"workerpool": "^10.0.1"
|
package/tests/index.js
CHANGED
|
@@ -13,21 +13,84 @@ import { checkFile } from "./../lib/utils/index.js"
|
|
|
13
13
|
|
|
14
14
|
process.chdir("./tests")
|
|
15
15
|
|
|
16
|
-
test("
|
|
17
|
-
const
|
|
16
|
+
test("database creation in memory", (t) => {
|
|
17
|
+
const database = votive.createDatabase(":memory:")
|
|
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-")
|
|
18
58
|
const tempSource = fs.mkdtempDisposableSync("source-")
|
|
19
59
|
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(tempSource.path, "abc.in"),
|
|
62
|
+
JSON.stringify({ abstract: ["abc"], metadata: { color: "blue" } }),
|
|
63
|
+
"utf-8"
|
|
64
|
+
)
|
|
65
|
+
|
|
20
66
|
try {
|
|
21
|
-
const stop = stopwatch("Bundle
|
|
22
|
-
await votive.bundle({
|
|
67
|
+
const stop = stopwatch("Bundle folder")
|
|
68
|
+
const database = await votive.bundle({
|
|
23
69
|
sourceFolder: tempSource.path,
|
|
24
70
|
destinationFolder: tempDest.path,
|
|
25
|
-
plugins: [
|
|
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
|
+
|
|
26
85
|
})
|
|
27
86
|
|
|
87
|
+
const sources = database.source.getAll()
|
|
88
|
+
|
|
89
|
+
const targets = database.target.getAll()
|
|
90
|
+
|
|
28
91
|
stop()
|
|
29
92
|
|
|
30
|
-
const dir = fs.readdirSync(tempDest.path)
|
|
93
|
+
const dir = fs.readdirSync(tempDest.path, { recursive: true })
|
|
31
94
|
assert(dir.length === 0, "Destination directory is not empty.")
|
|
32
95
|
} catch (error) {
|
|
33
96
|
tempDest.remove()
|
|
@@ -40,281 +103,275 @@ test("empty directory", async () => {
|
|
|
40
103
|
tempSource.remove()
|
|
41
104
|
})
|
|
42
105
|
|
|
43
|
-
test("internals", async (t) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
106
|
+
// test("internals", async (t) => {
|
|
107
|
+
// const dbExists = checkFile(".votive.db")
|
|
108
|
+
// if (dbExists) await rm(".votive.db")
|
|
109
|
+
|
|
110
|
+
// const temp = fs.mkdtempDisposableSync("destination-")
|
|
111
|
+
|
|
112
|
+
// /** @returns {Job} */
|
|
113
|
+
// function createExampleJob() {
|
|
114
|
+
// return {
|
|
115
|
+
// data: Math.floor(Math.random() * 1000),
|
|
116
|
+
// runner: "exampleRunner"
|
|
117
|
+
// }
|
|
118
|
+
// }
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// /** @type {ReadText} */
|
|
122
|
+
// function exampleTextReader(text, filePath, destinationPath, database, config) {
|
|
123
|
+
// const matches = text.match(/\b\w+\b/)
|
|
124
|
+
// const title = matches ? matches[0] : "Untitled"
|
|
125
|
+
|
|
126
|
+
// /** @type {ReadTextResult} */
|
|
127
|
+
// return {
|
|
128
|
+
// abstract: {
|
|
129
|
+
// content: text,
|
|
130
|
+
// },
|
|
131
|
+
// metadata: {
|
|
132
|
+
// title
|
|
133
|
+
// },
|
|
134
|
+
// jobs: [
|
|
135
|
+
// createExampleJob()
|
|
136
|
+
// ]
|
|
137
|
+
// }
|
|
138
|
+
// }
|
|
139
|
+
|
|
140
|
+
// /** @type {ReadAbstract} */
|
|
141
|
+
// function exampleAbstractReader(abstract, database, config) {
|
|
142
|
+
// abstract.exampleAppend = true
|
|
143
|
+
// return { abstract, jobs: [createExampleJob()] }
|
|
144
|
+
// }
|
|
145
|
+
|
|
146
|
+
// /** @type {ReadFolder} */
|
|
147
|
+
// function exampleFolderReader(folder, database, config) {
|
|
148
|
+
// return {
|
|
149
|
+
// jobs: [createExampleJob()],
|
|
150
|
+
// destinations: [
|
|
151
|
+
// {
|
|
152
|
+
// path: "index.html",
|
|
153
|
+
// abstract: { content: "" },
|
|
154
|
+
// metadata: {
|
|
155
|
+
// title: "home"
|
|
156
|
+
// },
|
|
157
|
+
// syntax: "md"
|
|
158
|
+
// }
|
|
159
|
+
// ]
|
|
160
|
+
// }
|
|
161
|
+
// }
|
|
162
|
+
|
|
163
|
+
// /** @type {VotiveProcessor} */
|
|
164
|
+
// const exampleProcessor = {
|
|
165
|
+
// syntax: "md",
|
|
166
|
+
// filter: {
|
|
167
|
+
// extensions: [".md"]
|
|
168
|
+
// },
|
|
169
|
+
// read: {
|
|
170
|
+
// text: exampleTextReader,
|
|
171
|
+
// abstract: exampleAbstractReader,
|
|
172
|
+
// folder: exampleFolderReader
|
|
173
|
+
// },
|
|
174
|
+
// write: (destination, database, config) => {
|
|
175
|
+
// return {
|
|
176
|
+
// data: "lorem ipsum",
|
|
177
|
+
// }
|
|
178
|
+
// }
|
|
179
|
+
// }
|
|
180
|
+
|
|
181
|
+
// /** @param {string} sourcePath */
|
|
182
|
+
// function router({ base, ...parsed }) {
|
|
183
|
+
// return { ...parsed, ext: ".html" }
|
|
184
|
+
// }
|
|
185
|
+
|
|
186
|
+
// /** @type {VotivePlugin} */
|
|
187
|
+
// const examplePlugin = {
|
|
188
|
+
// name: "example plugin",
|
|
189
|
+
// runners: {
|
|
190
|
+
// exampleRunner: exampleRunner
|
|
191
|
+
// },
|
|
192
|
+
// router,
|
|
193
|
+
// processors: [exampleProcessor]
|
|
194
|
+
// }
|
|
195
|
+
|
|
196
|
+
// /** @type {Runner} */
|
|
197
|
+
// async function exampleRunner(data, database) {
|
|
198
|
+
// return await new Promise((resolve) => setTimeout(() => resolve(data), 1))
|
|
199
|
+
// }
|
|
200
|
+
|
|
201
|
+
// /** @type {VotiveConfig} */
|
|
202
|
+
// const config = {
|
|
203
|
+
// sourceFolder: "./markdown",
|
|
204
|
+
// destinationFolder: temp.path,
|
|
205
|
+
// plugins: [examplePlugin]
|
|
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)
|
|
145
245
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
]
|
|
246
|
+
// const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
|
|
247
|
+
|
|
248
|
+
// t.test('transformation succeeded', () => {
|
|
249
|
+
// assert(processedAbstracts.find(abstract => abstract.exampleAppend))
|
|
250
|
+
// })
|
|
153
251
|
|
|
154
|
-
|
|
155
|
-
|
|
252
|
+
// t.test('abstract jobs exist', () => {
|
|
253
|
+
// assert(abstractsJobs.length > 0)
|
|
254
|
+
// assert(abstractsJobs.every(job => job.data && job.runner))
|
|
255
|
+
// })
|
|
156
256
|
|
|
157
|
-
|
|
158
|
-
const sourcesOne = database.getAllSources()
|
|
257
|
+
// const foldersJobs = readFolders(folders, config, database, processors)
|
|
159
258
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
})
|
|
259
|
+
// t.test("folders jobs exist", () => {
|
|
260
|
+
// assert(foldersJobs.length > 0)
|
|
261
|
+
// assert(foldersJobs.every(job => job.data && job.runner))
|
|
262
|
+
// })
|
|
165
263
|
|
|
264
|
+
// database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.html", abstract: { c: 3 }, syntax: "md" })
|
|
265
|
+
// const firstDestination = database.getDestinationIndependently("abc.html", [])
|
|
166
266
|
|
|
267
|
+
// t.test('first destination created', () => {
|
|
268
|
+
// assert(firstDestination.path === 'abc.html')
|
|
269
|
+
// })
|
|
167
270
|
|
|
168
|
-
|
|
271
|
+
// database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "abc/def.html", abstract: { c: 5 }, syntax: "md" })
|
|
169
272
|
|
|
170
|
-
|
|
273
|
+
// /* TODO: Test that dependencies are working */
|
|
171
274
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
275
|
+
// const setting = database.setSetting("abc.html", "theme", "blue", "markdown/prunee.md")
|
|
276
|
+
// const newSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
|
|
277
|
+
// const sameSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
|
|
278
|
+
// const folderSetting = database.setSetting("abc", "theme", "red", "markdown/prunee.md")
|
|
175
279
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
280
|
+
// t.test('settings created', () => {
|
|
281
|
+
// assert(setting.value === 'blue')
|
|
282
|
+
// assert(newSetting.value === 'Dog')
|
|
283
|
+
// assert(!sameSetting)
|
|
284
|
+
// })
|
|
179
285
|
|
|
180
|
-
|
|
286
|
+
// const abcSettings = database.getSettings("abc.html")
|
|
181
287
|
|
|
182
|
-
|
|
288
|
+
// t.test('settings retrieved', () => {
|
|
289
|
+
// assert(abcSettings.theme[0] === "blue")
|
|
290
|
+
// })
|
|
183
291
|
|
|
184
|
-
|
|
292
|
+
// const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
|
|
293
|
+
// const defSettings = database.getSettings("abc/def.html")
|
|
185
294
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
295
|
+
// t.test('settings retrieved', () => {
|
|
296
|
+
// assert(abcSettings.theme[0] === "blue")
|
|
297
|
+
// assert(defSettings.theme[1] === "green")
|
|
298
|
+
// })
|
|
189
299
|
|
|
190
|
-
|
|
191
|
-
assert(abstractsJobs.length > 0)
|
|
192
|
-
assert(abstractsJobs.every(job => job.data && job.runner))
|
|
193
|
-
})
|
|
300
|
+
// const staleDestinations = database.getStaleDestinations()
|
|
194
301
|
|
|
195
|
-
|
|
302
|
+
// t.test('all destinations are stale', () => {
|
|
303
|
+
// assert(staleDestinations.length === 7)
|
|
304
|
+
// })
|
|
196
305
|
|
|
197
|
-
|
|
198
|
-
assert(foldersJobs.length > 0)
|
|
199
|
-
assert(foldersJobs.every(job => job.data && job.runner))
|
|
200
|
-
})
|
|
306
|
+
// const written = await votive.writeDestinations(config, database)
|
|
201
307
|
|
|
202
|
-
|
|
203
|
-
const firstDestination = database.getDestinationIndependently("abc.html", [])
|
|
308
|
+
// const staleDestinationsAfterWriting = database.getStaleDestinations()
|
|
204
309
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
310
|
+
// t.test('all destinations are fresh', () => {
|
|
311
|
+
// assert(staleDestinationsAfterWriting.length === 0)
|
|
312
|
+
// })
|
|
208
313
|
|
|
209
|
-
|
|
210
|
-
|
|
314
|
+
// const updated = database.createOrUpdateDestination({ metadata: { a: 1, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
|
|
315
|
+
// const staleAfterAbstractUpdate = database.getStaleDestinations()
|
|
211
316
|
|
|
212
|
-
|
|
317
|
+
// t.test('updated document with no side effects is stale', () => {
|
|
318
|
+
// assert(staleAfterAbstractUpdate.length === 1)
|
|
319
|
+
// })
|
|
213
320
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
})
|
|
321
|
+
// await votive.writeDestinations(config, database)
|
|
322
|
+
// const staleAfterSecondWrite = database.getStaleDestinations()
|
|
217
323
|
|
|
324
|
+
// t.test('everything fresh again', () => {
|
|
325
|
+
// assert(staleAfterSecondWrite.length === 0)
|
|
326
|
+
// })
|
|
218
327
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const sameSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
|
|
222
|
-
const folderSetting = database.setSetting("abc", "theme", "red", "markdown/prunee.md")
|
|
328
|
+
// const updatedWithSideEffects = database.createOrUpdateDestination({ metadata: { a: 4, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
|
|
329
|
+
// const staleAfterSideEffects = database.getStaleDestinations()
|
|
223
330
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
assert(!sameSetting)
|
|
228
|
-
})
|
|
331
|
+
// t.test('side effects work', () => {
|
|
332
|
+
// assert(staleAfterSideEffects.length === 2)
|
|
333
|
+
// })
|
|
229
334
|
|
|
230
|
-
const abcSettings = database.getSettings("abc.html")
|
|
231
335
|
|
|
232
|
-
|
|
233
|
-
assert(abcSettings.theme[0] === "blue")
|
|
234
|
-
})
|
|
336
|
+
// await votive.writeDestinations(config, database)
|
|
235
337
|
|
|
236
|
-
|
|
237
|
-
const defSettings = database.getSettings("abc/def.html")
|
|
338
|
+
// const oldSetting = database.setSetting("abc", "theme", "green")
|
|
238
339
|
|
|
239
|
-
|
|
240
|
-
assert(abcSettings.theme[0] === "blue")
|
|
241
|
-
assert(defSettings.theme[1] === "green")
|
|
242
|
-
})
|
|
340
|
+
// const staleAfterSettingsChange = database.getStaleDestinations()
|
|
243
341
|
|
|
244
|
-
|
|
342
|
+
// t.test('setting affects descendents', () => {
|
|
343
|
+
// assert(staleAfterSettingsChange.length === 1)
|
|
344
|
+
// })
|
|
245
345
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
346
|
+
// // TODO: Write tests for jobs
|
|
347
|
+
// const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
|
|
348
|
+
// runJobs(jobs, config, database)
|
|
249
349
|
|
|
250
|
-
|
|
350
|
+
// fs.rmSync("markdown/prunee.md")
|
|
251
351
|
|
|
252
|
-
|
|
352
|
+
// await votive.pruneSources(config, database)
|
|
253
353
|
|
|
254
|
-
|
|
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()
|
|
354
|
+
// const result = database.getDestinationIndependently("markdown/prunee.html")
|
|
260
355
|
|
|
261
|
-
|
|
262
|
-
assert(staleAfterAbstractUpdate.length === 1)
|
|
263
|
-
})
|
|
356
|
+
// /* TODO: Check pruning metadata works (I removed it) */
|
|
264
357
|
|
|
265
|
-
|
|
266
|
-
|
|
358
|
+
// const finalDestinations = database.getDestinations({
|
|
359
|
+
// filter: [
|
|
360
|
+
// {
|
|
361
|
+
// property: "a",
|
|
362
|
+
// operator: "gt",
|
|
363
|
+
// value: 3
|
|
364
|
+
// }
|
|
365
|
+
// ]
|
|
366
|
+
// }, "markdown/prunee.html")
|
|
267
367
|
|
|
268
|
-
|
|
269
|
-
assert(staleAfterSecondWrite.length === 0)
|
|
270
|
-
})
|
|
368
|
+
// // TODO: Write a test for get destinations
|
|
271
369
|
|
|
272
|
-
|
|
273
|
-
const staleAfterSideEffects = database.getStaleDestinations()
|
|
370
|
+
// await database.saveDB()
|
|
274
371
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
})
|
|
372
|
+
// temp.remove()
|
|
373
|
+
// } catch (error) {
|
|
374
|
+
// temp.remove()
|
|
375
|
+
// throw error
|
|
376
|
+
// }
|
|
377
|
+
// })
|
package/lib/cleanDeletions.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|