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 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
- - [ ] Rename jobs and paths
12
- - [ ] Better dependency tracking
13
- - [ ] Better query filters
14
- - [x] File deletion handling
7
+ - [ ] Destination query filters
15
8
 
16
- ## Project: Jobs
9
+ ## Helpers
17
10
 
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.
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
@@ -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 j.value = m.value
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 = "%", recursive, dependent, query = {} } = params
854
+ const { folder = "", recursive, dependent, query = {} } = params
794
855
 
795
856
  const recursivePath = [folder, "%"].filter(a => a).join("/")
796
857
 
@@ -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 {ReadSourceFilesResult} from "./readSources.js"
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 {ReadSourceFilesResult} files
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(files, config, database, processors) {
22
+ function readAbstracts(abstracts, config, database, processors) {
23
23
 
24
- const processed = files.flatMap(file => {
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 (!processor.extensions.includes(file.syntax)
38
- || !processor.transformFile
35
+ if (processor.syntax !== syntax
36
+ || !processor.read
37
+ || !processor.read.abstract
39
38
  ) return recursiveProcess(rest, abstract, jobs)
40
39
 
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)
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
 
@@ -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.readFolder)
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
- 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)
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.targets) {
39
- rootFolder.targets.forEach(target => {
40
- database.target.create(target)
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.syntax = processor.syntax)
41
+ rootFolder.jobs.forEach(job => job.plugin = plugin.name)
45
42
 
46
43
  if(rootFolder.jobs) jobs.push(... rootFolder.jobs)
47
44
  return jobs
@@ -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 pLimit from "p-limit"
4
+ import { visit } from "unist-util-visit"
5
5
 
6
- /** @import {VotiveConfig, VotivePlugin, VotiveProcessor, FlatProcessors, Abstracts, Abstract, Jobs, ProcessorSyntax, Router} from "./bundle.js" */
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 {ReadSourceFileResult[]} sources
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: [...sources, ...deletedSources]
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 {ReadSourceFileResult[]} ReadSourceFilesResult
60
+ * @typedef {ReadSourceFilePlugin[]} ReadSourceFileResult
97
61
  */
98
62
 
99
63
  /**
100
- * @typedef {object} ReadSourceFileResult
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} sourceFilePath
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<ReadSourceFileResult>[]}
79
+ * @returns {Promise<ReadSourceFilePlugin>[]}
119
80
  */
120
81
  return (dirent) => {
121
82
  const { name, parentPath } = dirent
122
- const sourceFilePath = path.join(parentPath, name)
123
- const sourceFileInfo = path.parse(sourceFilePath)
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 { readFile: read, extensions: filter, format } = processor
134
- if (filter.includes(sourceFileInfo.ext) && read) {
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(sourceFilePath)
138
- const source = database.source.get(sourceFilePath)
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 (format === "text") {
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
- const { jobs, abstract, metadata } = read(data, sourceFilePath, targetFilePath, database, config)
104
+ database.deleteSettings(filePath)
176
105
 
106
+ // Get destination route
107
+ const destinationPath = route(filePath, plugin)
177
108
 
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
- }
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
- // FIXME Job syntax
203
- allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
204
- updateSource()
115
+ const timeStamp = stat.mtimeMs.toFixed()
205
116
 
206
- return {
207
- jobs: allJobs,
208
- abstract: null,
209
- targetFilePath: null,
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
- 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
- }
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, 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))
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 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 => {
13
+ const running = jobs.map(async job => {
12
14
  if (!job) return
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)
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 settled = await Promise.allSettled(running)
38
- return settled
19
+ const ran = await Promise.allSettled(running)
20
+ pool.terminate()
39
21
  }
40
22
 
41
23
  export default runJobs
@@ -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
- * @param {string} label
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
- if(verbose) console.info(`${styleText("dim", label + ":")} ${styleText("magenta", message)} ${styleText("magenta", duration.toFixed(2) + "ms")}`)
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("/").filter(a => a).reverse()
27
- const segments = urlDirSegmentsReversed.reverse()
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 */
@@ -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) return
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.2.1",
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("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-")
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
- const database = await votive.bundle({
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, { recursive: true })
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
- // 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)
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
- // const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
247
-
248
- // t.test('transformation succeeded', () => {
249
- // assert(processedAbstracts.find(abstract => abstract.exampleAppend))
250
- // })
146
+ /** @type {FlatProcessors} */
147
+ const processors = [
148
+ {
149
+ plugin: examplePlugin,
150
+ processor: exampleProcessor
151
+ }
152
+ ]
251
153
 
252
- // t.test('abstract jobs exist', () => {
253
- // assert(abstractsJobs.length > 0)
254
- // assert(abstractsJobs.every(job => job.data && job.runner))
255
- // })
154
+ try {
155
+ fs.writeFileSync("markdown/prunee.md", "A little content")
256
156
 
257
- // const foldersJobs = readFolders(folders, config, database, processors)
157
+ const database = votive.createDatabase()
158
+ const sourcesOne = database.getAllSources()
258
159
 
259
- // t.test("folders jobs exist", () => {
260
- // assert(foldersJobs.length > 0)
261
- // assert(foldersJobs.every(job => job.data && job.runner))
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
- // database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "abc/def.html", abstract: { c: 5 }, syntax: "md" })
168
+ const stop = stopwatch("Read sources")
272
169
 
273
- // /* TODO: Test that dependencies are working */
170
+ const { folders, sources } = await votive.readSources(config, database, processors)
274
171
 
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")
172
+ t.test('one folder exists', () => {
173
+ assert(folders.length === 1)
174
+ })
279
175
 
280
- // t.test('settings created', () => {
281
- // assert(setting.value === 'blue')
282
- // assert(newSetting.value === 'Dog')
283
- // assert(!sameSetting)
284
- // })
176
+ t.test('three sources exist', () => {
177
+ assert(sources.length === 4)
178
+ })
285
179
 
286
- // const abcSettings = database.getSettings("abc.html")
180
+ stop()
287
181
 
288
- // t.test('settings retrieved', () => {
289
- // assert(abcSettings.theme[0] === "blue")
290
- // })
182
+ const sourcesJobs = sources.flatMap(source => source.jobs)
291
183
 
292
- // const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
293
- // const defSettings = database.getSettings("abc/def.html")
184
+ const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
294
185
 
295
- // t.test('settings retrieved', () => {
296
- // assert(abcSettings.theme[0] === "blue")
297
- // assert(defSettings.theme[1] === "green")
298
- // })
186
+ t.test('transformation succeeded', () => {
187
+ assert(processedAbstracts.find(abstract => abstract.exampleAppend))
188
+ })
299
189
 
300
- // const staleDestinations = database.getStaleDestinations()
190
+ t.test('abstract jobs exist', () => {
191
+ assert(abstractsJobs.length > 0)
192
+ assert(abstractsJobs.every(job => job.data && job.runner))
193
+ })
301
194
 
302
- // t.test('all destinations are stale', () => {
303
- // assert(staleDestinations.length === 7)
304
- // })
195
+ const foldersJobs = readFolders(folders, config, database, processors)
305
196
 
306
- // const written = await votive.writeDestinations(config, database)
197
+ t.test("folders jobs exist", () => {
198
+ assert(foldersJobs.length > 0)
199
+ assert(foldersJobs.every(job => job.data && job.runner))
200
+ })
307
201
 
308
- // const staleDestinationsAfterWriting = database.getStaleDestinations()
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
- // t.test('all destinations are fresh', () => {
311
- // assert(staleDestinationsAfterWriting.length === 0)
312
- // })
205
+ t.test('first destination created', () => {
206
+ assert(firstDestination.path === 'abc.html')
207
+ })
313
208
 
314
- // const updated = database.createOrUpdateDestination({ metadata: { a: 1, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
315
- // const staleAfterAbstractUpdate = database.getStaleDestinations()
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
- // t.test('updated document with no side effects is stale', () => {
318
- // assert(staleAfterAbstractUpdate.length === 1)
319
- // })
212
+ const dependencies = database.getDependencies()
320
213
 
321
- // await votive.writeDestinations(config, database)
322
- // const staleAfterSecondWrite = database.getStaleDestinations()
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
- // const updatedWithSideEffects = database.createOrUpdateDestination({ metadata: { a: 4, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
329
- // const staleAfterSideEffects = database.getStaleDestinations()
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
- // t.test('side effects work', () => {
332
- // assert(staleAfterSideEffects.length === 2)
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
- // await votive.writeDestinations(config, database)
232
+ t.test('settings retrieved', () => {
233
+ assert(abcSettings.theme[0] === "blue")
234
+ })
337
235
 
338
- // const oldSetting = database.setSetting("abc", "theme", "green")
236
+ const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
237
+ const defSettings = database.getSettings("abc/def.html")
339
238
 
340
- // const staleAfterSettingsChange = database.getStaleDestinations()
239
+ t.test('settings retrieved', () => {
240
+ assert(abcSettings.theme[0] === "blue")
241
+ assert(defSettings.theme[1] === "green")
242
+ })
341
243
 
342
- // t.test('setting affects descendents', () => {
343
- // assert(staleAfterSettingsChange.length === 1)
344
- // })
244
+ const staleDestinations = database.getStaleDestinations()
345
245
 
346
- // // TODO: Write tests for jobs
347
- // const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
348
- // runJobs(jobs, config, database)
246
+ t.test('all destinations are stale', () => {
247
+ assert(staleDestinations.length === 7)
248
+ })
349
249
 
350
- // fs.rmSync("markdown/prunee.md")
250
+ const written = await votive.writeDestinations(config, database)
351
251
 
352
- // await votive.pruneSources(config, database)
252
+ const staleDestinationsAfterWriting = database.getStaleDestinations()
353
253
 
354
- // const result = database.getDestinationIndependently("markdown/prunee.html")
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
- // /* TODO: Check pruning metadata works (I removed it) */
261
+ t.test('updated document with no side effects is stale', () => {
262
+ assert(staleAfterAbstractUpdate.length === 1)
263
+ })
357
264
 
358
- // const finalDestinations = database.getDestinations({
359
- // filter: [
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
- // // TODO: Write a test for get destinations
268
+ t.test('everything fresh again', () => {
269
+ assert(staleAfterSecondWrite.length === 0)
270
+ })
369
271
 
370
- // await database.saveDB()
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
- // temp.remove()
373
- // } catch (error) {
374
- // temp.remove()
375
- // throw error
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
+ })