votive 0.3.2 → 0.4.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,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
@@ -10,6 +10,10 @@ import { styleText } from "node:util"
10
10
 
11
11
  /** @import {Database} from "./createDatabase.js" */
12
12
 
13
+ /**
14
+ * @typedef {Database} Database
15
+ */
16
+
13
17
  /**
14
18
  * @typedef {object} VotivePlugin
15
19
  * @property {string} name
@@ -245,8 +249,6 @@ async function bundle(config, cache) {
245
249
  // Write destination files
246
250
  await writeDestinations(config, database)
247
251
 
248
- const stale = database.target.getStale()
249
-
250
252
  writeTime()
251
253
 
252
254
  // database.commit()
@@ -282,6 +284,7 @@ async function bundler(config) {
282
284
  queue.push(bundle(config))
283
285
  cache = await queue[0]
284
286
  } else {
287
+ "cache"
285
288
  queue.push(bundle(config, cache))
286
289
  await queue[0]
287
290
  }
@@ -659,7 +659,9 @@ function createDatabase(databasePath = ".votive.db") {
659
659
  const type = typeof value
660
660
  const safeValue = type === "object"
661
661
  ? JSON.stringify(value)
662
- : value
662
+ : type === "boolean"
663
+ ? Number(value)
664
+ : value
663
665
 
664
666
  const descendents = folder === ""
665
667
  ? "%"
@@ -746,13 +748,13 @@ function createDatabase(databasePath = ".votive.db") {
746
748
  * @param {string} filePath
747
749
  */
748
750
  delete(filePath) {
749
- // Note: Need to run clean-up after
750
- prepared.target.delete.get(filePath)
751
+ const deleted = prepared.target.delete.get(filePath)
751
752
  prepared.metadata.deleteByTarget.all(filePath)
752
753
  const deps = queries.dependency.getAllByTarget(filePath)
753
754
  deps.forEach(dep => {
754
755
  queries.target.markStale(dep.dependent)
755
756
  })
757
+ return deleted
756
758
  },
757
759
 
758
760
  /**
@@ -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
42
53
  */
43
- function fileFilter(config, database) {
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
+ */
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,102 @@ 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)
100
-
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))
101
140
 
102
- if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
141
+ if (source && diff > -1) {
142
+ return null
143
+ }
103
144
 
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
114
158
 
115
- const timeStamp = stat.mtimeMs.toFixed()
159
+ const target = database.target.create({
160
+ metadata,
161
+ abstract,
162
+ path: targetFilePath,
163
+ })
116
164
 
117
- if(source) {
118
- database.updateSource(filePath, Number(timeStamp))
119
- } else {
120
- database.createSource(filePath, destinationPath, Number(timeStamp))
165
+
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 content = read(data, sourceFilePath, targetFilePath, database, config)
176
+ if (content) {
177
+
178
+ const { jobs, abstract, metadata } = read(data, sourceFilePath, targetFilePath, database, config)
179
+
180
+
181
+ if (Array.isArray(jobs)) {
182
+ allJobs.push(...jobs)
183
+ }
184
+
185
+ const target = database.target.create({
186
+ abstract,
187
+ path: targetFilePath,
188
+ metadata,
189
+ })
190
+
191
+ // FIXME this won't work with the refactor
192
+ allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
193
+ updateSource()
194
+ return {
195
+ abstract,
196
+ metadata,
197
+ syntax: targetFileExtension,
198
+ targetFilePath: target.path,
199
+ dir: target.dir,
200
+ sourceFilePath,
201
+ jobs: allJobs
202
+ }
203
+ }
204
+ }
205
+
206
+ // FIXME Job syntax
207
+ allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
208
+ updateSource()
209
+
210
+ return {
211
+ jobs: allJobs,
212
+ abstract: null,
213
+ targetFilePath: null,
214
+ sourceFilePath: null,
215
+ metadata: null,
216
+ syntax: null
217
+ }
218
+
219
+ function updateSource() {
220
+ const timeStamp = stat.mtimeMs.toFixed()
221
+
222
+ if (source) {
223
+ database.source.updateTimestamp(sourceFilePath, Number(timeStamp))
224
+ } else {
225
+ database.source.create(sourceFilePath, targetFilePath, Number(timeStamp))
226
+ }
227
+ }
129
228
 
130
- return { abstract, destinationPath, syntax, jobs }
131
229
  }
132
230
  }
133
231
 
@@ -138,10 +236,43 @@ function readSourceFile(processors, database, config) {
138
236
  /**
139
237
  * @param {string} filePath
140
238
  * @param {VotivePlugin} plugin
239
+ * @param {VotiveConfig} config
141
240
  */
142
- function route(filePath, plugin) {
143
- if (!plugin.router) return ""
144
- return plugin.router(filePath) || ""
241
+ function route(filePath, plugin, config) {
242
+ const { dir, ...parsedPath } = path.parse(filePath)
243
+ const rooty = !path.relative(config.sourceFolder, dir)
244
+ const segments = ["", ...dir.split(path.sep).filter(a => a)]
245
+ const pathInfo = {
246
+ inRootDir: rooty,
247
+ dir: segments,
248
+ ...parsedPath
249
+ }
250
+
251
+ if (!plugin.router) return "0"
252
+
253
+ const routedPath = plugin.router(pathInfo)
254
+
255
+ if (!routedPath) return "0"
256
+
257
+ if (routedPath.hasOwnProperty("dir") && Array.isArray(routedPath.dir)) {
258
+ return path.normalize(path.format({
259
+ dir: path.join(...routedPath.dir),
260
+ root: routedPath.root || "",
261
+ base: routedPath.base,
262
+ name: routedPath.name,
263
+ ext: routedPath.ext
264
+ }))
265
+ }
266
+
267
+ const routedInfo = {
268
+ dir: routedPath.dir || "",
269
+ root: routedPath.root || "",
270
+ base: routedPath.base,
271
+ name: routedPath.name,
272
+ ext: routedPath.ext
273
+ }
274
+
275
+ return path.normalize(path.format(routedInfo))
145
276
  }
146
277
 
147
278
  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 */
@@ -39,7 +39,6 @@ async function writeDestinations(config, database) {
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.2",
3
+ "version": "0.4.1",
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