votive 0.3.1 → 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 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
- - [ ] Destination query filters
11
+ - [ ] Rename jobs and paths
12
+ - [ ] Better dependency tracking
13
+ - [ ] Better query filters
14
+ - [x] File deletion handling
8
15
 
9
- ## Helpers
16
+ ## Project: Jobs
10
17
 
11
- - Read
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
  }
@@ -264,43 +264,43 @@ function prepareStatements(database) {
264
264
  ORDER BY d.path ASC;
265
265
  `))),
266
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
- )
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
- AND (
298
- d.dir = '/'
299
- OR d.dir LIKE '%'
300
- )
301
- GROUP BY d.path
302
- ORDER BY d.path ASC;
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
- // Note: Need to run clean-up after
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 (!target) return
820
+ if(!target) return
820
821
 
821
822
  const trackedTarget = { metadata: {} }
822
823
 
@@ -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 {ReadSourceFileResult} from "./readSources.js"
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 {ReadSourceFileResult} abstracts
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(abstracts, config, database, processors) {
22
+ function readAbstracts(files, config, database, processors) {
23
23
 
24
- const processed = abstracts.flatMap(({ abstract: unprocessedAbstract, syntax }) => {
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 !== syntax
36
- || !processor.read
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 processed = processor.read.abstract(abstract, database, config)
41
- processed.jobs && processed.jobs.forEach(job => job.plugin = plugin.name)
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
 
@@ -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.read && processor.read.folder)
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
- 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)
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
- const rootFolder = processor.read.folder("", database, config)
34
+
35
+ const rootPath = path.relative(config.sourceFolder, "")
36
+ const rootFolder = processor.readFolder(rootPath, database, config, true)
34
37
 
35
- if (rootFolder.destinations) {
36
- rootFolder.destinations.forEach(destination => {
37
- database.createOrUpdateDestination(destination)
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.plugin = plugin.name)
44
+ rootFolder.jobs.forEach(job => job.syntax = processor.syntax)
42
45
 
43
46
  if(rootFolder.jobs) jobs.push(... rootFolder.jobs)
44
47
  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 { visit } from "unist-util-visit"
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 {ReadSourceFilePlugin[]} sources
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, database))
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
- const sources = readingSourceFiles && await Promise.all(readingSourceFiles)
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, database) {
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 {ReadSourceFilePlugin[]} ReadSourceFileResult
96
+ * @typedef {ReadSourceFileResult[]} ReadSourceFilesResult
61
97
  */
62
98
 
63
99
  /**
64
- * @typedef {object} ReadSourceFilePlugin
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} destinationPath
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<ReadSourceFilePlugin>[]}
118
+ * @returns {Promise<ReadSourceFileResult>[]}
80
119
  */
81
120
  return (dirent) => {
82
121
  const { name, parentPath } = dirent
83
- const filePath = path.join(parentPath, name)
84
- const fileInfo = path.parse(filePath)
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, syntax } = processor
95
- if (filter.extensions.includes(fileInfo.ext) && read && read.text) {
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(filePath)
99
- const source = database.getSource(filePath)
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
- if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
103
-
104
- database.deleteSettings(filePath)
145
+ database.setting.deleteBySource(sourceFilePath)
105
146
 
106
- // Get destination route
107
- const destinationPath = route(filePath, plugin)
147
+ const targetFilePath = route(sourceFilePath, plugin, config)
148
+ const targetFileExtension = path.extname(targetFilePath)
108
149
 
109
150
  // 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)
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
- if(source) {
118
- database.updateSource(filePath, Number(timeStamp))
119
- } else {
120
- database.createSource(filePath, destinationPath, Number(timeStamp))
166
+ if (Array.isArray(jobs)) {
167
+ allJobs.push(...jobs)
168
+ }
121
169
  }
122
170
 
123
- database.createOrUpdateDestination({
124
- metadata,
125
- path: destinationPath,
126
- abstract: abstract,
127
- syntax: syntax
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
- if (!plugin.router) return ""
144
- return plugin.router(filePath) || ""
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 running = jobs.map(async job => {
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 plugin = config.plugins.find(plugin => plugin.name === job.plugin)
16
- return pool.exec(plugin.runners[job.runner], [job.data])
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 ran = await Promise.allSettled(running)
20
- pool.terminate()
37
+ const settled = await Promise.allSettled(running)
38
+ return settled
21
39
  }
22
40
 
23
41
  export default runJobs
@@ -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
- /** @param {string} label */
5
- export function stopwatch(label) {
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}: ${styleText("red", duration.toFixed(2) + "ms")}`)
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
- return [urlDirSegmentsReversed.reverse().join("/") || "/", urlFileName]
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 */
@@ -34,12 +34,11 @@ async function writeDestinations(config, database) {
34
34
  if (!writeInfo) {
35
35
  try {
36
36
  database.target.delete(destination.path)
37
- return await rm(destinationPath)
37
+ return await rm(destination.path)
38
38
  } catch (e) {
39
39
  console.error(e)
40
40
  }
41
41
  }
42
-
43
42
  const { data, encoding = 'utf-8' } = writeInfo
44
43
 
45
44
  async function write() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "votive",
3
- "version": "0.3.1",
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("empty directory", async () => {
17
- const tempDest = fs.mkdtempDisposableSync("destination-")
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 empty folder")
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
- 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
- }
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
- /** @type {FlatProcessors} */
147
- const processors = [
148
- {
149
- plugin: examplePlugin,
150
- processor: exampleProcessor
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
- try {
155
- fs.writeFileSync("markdown/prunee.md", "A little content")
252
+ // t.test('abstract jobs exist', () => {
253
+ // assert(abstractsJobs.length > 0)
254
+ // assert(abstractsJobs.every(job => job.data && job.runner))
255
+ // })
156
256
 
157
- const database = votive.createDatabase()
158
- const sourcesOne = database.getAllSources()
257
+ // const foldersJobs = readFolders(folders, config, database, processors)
159
258
 
160
- t.test("no sources on startup", () => {
161
- const isArray = Array.isArray(sourcesOne)
162
- const isEmpty = !sourcesOne.length
163
- assert(isArray && isEmpty)
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
- const stop = stopwatch("Read sources")
271
+ // database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "abc/def.html", abstract: { c: 5 }, syntax: "md" })
169
272
 
170
- const { folders, sources } = await votive.readSources(config, database, processors)
273
+ // /* TODO: Test that dependencies are working */
171
274
 
172
- t.test('one folder exists', () => {
173
- assert(folders.length === 1)
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
- t.test('three sources exist', () => {
177
- assert(sources.length === 4)
178
- })
280
+ // t.test('settings created', () => {
281
+ // assert(setting.value === 'blue')
282
+ // assert(newSetting.value === 'Dog')
283
+ // assert(!sameSetting)
284
+ // })
179
285
 
180
- stop()
286
+ // const abcSettings = database.getSettings("abc.html")
181
287
 
182
- const sourcesJobs = sources.flatMap(source => source.jobs)
288
+ // t.test('settings retrieved', () => {
289
+ // assert(abcSettings.theme[0] === "blue")
290
+ // })
183
291
 
184
- const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
292
+ // const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
293
+ // const defSettings = database.getSettings("abc/def.html")
185
294
 
186
- t.test('transformation succeeded', () => {
187
- assert(processedAbstracts.find(abstract => abstract.exampleAppend))
188
- })
295
+ // t.test('settings retrieved', () => {
296
+ // assert(abcSettings.theme[0] === "blue")
297
+ // assert(defSettings.theme[1] === "green")
298
+ // })
189
299
 
190
- t.test('abstract jobs exist', () => {
191
- assert(abstractsJobs.length > 0)
192
- assert(abstractsJobs.every(job => job.data && job.runner))
193
- })
300
+ // const staleDestinations = database.getStaleDestinations()
194
301
 
195
- const foldersJobs = readFolders(folders, config, database, processors)
302
+ // t.test('all destinations are stale', () => {
303
+ // assert(staleDestinations.length === 7)
304
+ // })
196
305
 
197
- t.test("folders jobs exist", () => {
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
- database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.html", abstract: { c: 3 }, syntax: "md" })
203
- const firstDestination = database.getDestinationIndependently("abc.html", [])
308
+ // const staleDestinationsAfterWriting = database.getStaleDestinations()
204
309
 
205
- t.test('first destination created', () => {
206
- assert(firstDestination.path === 'abc.html')
207
- })
310
+ // t.test('all destinations are fresh', () => {
311
+ // assert(staleDestinationsAfterWriting.length === 0)
312
+ // })
208
313
 
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")
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
- const dependencies = database.getDependencies()
317
+ // t.test('updated document with no side effects is stale', () => {
318
+ // assert(staleAfterAbstractUpdate.length === 1)
319
+ // })
213
320
 
214
- t.test('dependency created', () => {
215
- assert(dependencies[0].dependent === 'abc/def.html')
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
- 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")
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
- t.test('settings created', () => {
225
- assert(setting.value === 'blue')
226
- assert(newSetting.value === 'Dog')
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
- t.test('settings retrieved', () => {
233
- assert(abcSettings.theme[0] === "blue")
234
- })
336
+ // await votive.writeDestinations(config, database)
235
337
 
236
- const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
237
- const defSettings = database.getSettings("abc/def.html")
338
+ // const oldSetting = database.setSetting("abc", "theme", "green")
238
339
 
239
- t.test('settings retrieved', () => {
240
- assert(abcSettings.theme[0] === "blue")
241
- assert(defSettings.theme[1] === "green")
242
- })
340
+ // const staleAfterSettingsChange = database.getStaleDestinations()
243
341
 
244
- const staleDestinations = database.getStaleDestinations()
342
+ // t.test('setting affects descendents', () => {
343
+ // assert(staleAfterSettingsChange.length === 1)
344
+ // })
245
345
 
246
- t.test('all destinations are stale', () => {
247
- assert(staleDestinations.length === 7)
248
- })
346
+ // // TODO: Write tests for jobs
347
+ // const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
348
+ // runJobs(jobs, config, database)
249
349
 
250
- const written = await votive.writeDestinations(config, database)
350
+ // fs.rmSync("markdown/prunee.md")
251
351
 
252
- const staleDestinationsAfterWriting = database.getStaleDestinations()
352
+ // await votive.pruneSources(config, database)
253
353
 
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()
354
+ // const result = database.getDestinationIndependently("markdown/prunee.html")
260
355
 
261
- t.test('updated document with no side effects is stale', () => {
262
- assert(staleAfterAbstractUpdate.length === 1)
263
- })
356
+ // /* TODO: Check pruning metadata works (I removed it) */
264
357
 
265
- await votive.writeDestinations(config, database)
266
- const staleAfterSecondWrite = database.getStaleDestinations()
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
- t.test('everything fresh again', () => {
269
- assert(staleAfterSecondWrite.length === 0)
270
- })
368
+ // // TODO: Write a test for get destinations
271
369
 
272
- const updatedWithSideEffects = database.createOrUpdateDestination({ metadata: { a: 4, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
273
- const staleAfterSideEffects = database.getStaleDestinations()
370
+ // await database.saveDB()
274
371
 
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
- })
372
+ // temp.remove()
373
+ // } catch (error) {
374
+ // temp.remove()
375
+ // throw error
376
+ // }
377
+ // })
@@ -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