votive 0.2.1 → 0.3.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.
@@ -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,6 +1,5 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises"
2
2
  import path from "node:path"
3
- import { checkFile } from "./utils/index.js"
4
3
 
5
4
  /** @import {Abstract, Abstracts, VotiveConfig} from "./bundle.js" */
6
5
  /** @import {Database} from "./createDatabase.js" */
@@ -12,46 +11,27 @@ import { checkFile } from "./utils/index.js"
12
11
  */
13
12
  async function writeDestinations(config, database) {
14
13
  const writeProcessors = config && config.plugins && config.plugins.flatMap(plugin => (
15
- plugin.processors && plugin.processors.map(processor => processor.writeFile && processor).filter(a => a)
14
+ plugin.processors && plugin.processors.map(({ write, syntax }) => write && { write, syntax }).filter(a => a)
16
15
  )).filter(a => a)
17
16
 
18
17
  if (!writeProcessors || !writeProcessors.length) throw "No write processor provided"
19
18
 
20
- const targets = database.target.getStale()
19
+ const destinations = database.getStaleDestinations()
20
+ if (!destinations) return
21
21
 
22
- if (!targets) return
23
-
24
- const writing = targets.filter(({ path }) => path !== "0").flatMap(destination => {
25
-
26
- /* FIXME the following line doesn't make sense */
27
- if (!destination.path) database.target.markFresh(destination.path)
28
- if (!Object.keys(destination.abstract).length) return
22
+ const writing = destinations.flatMap(destination => {
29
23
  const destinationPath = path.join(config.destinationFolder, String(destination.path))
30
24
  const { dir } = path.parse(destinationPath)
31
- return writeProcessors.map(async processor => {
32
- if (processor.extensions.includes(destination.syntax)) {
33
- const writeInfo = await processor.writeFile(destination, database, config)
34
- if (!writeInfo) return
35
- const { data, encoding = 'utf-8' } = writeInfo
25
+ return writeProcessors.map(processor => {
26
+ if (processor.syntax === destination.syntax) {
27
+ const { data, encoding = 'utf-8' } = processor.write(destination, database, config)
36
28
 
37
29
  async function write() {
38
- const destinationExists = checkFile(dir)
39
-
40
- if (!destinationExists) {
41
- await mkdir(dir, { recursive: true })
42
- }
43
-
44
- if (data) {
45
- if (processor.format === "text") {
46
- await writeFile(destinationPath, data, "utf-8")
47
- database.target.markFresh(destination.path)
48
- } else {
49
- await writeFile(destinationPath, data)
50
- database.target.markFresh(destination.path)
51
- }
52
- }
30
+ await mkdir(dir, { recursive: true })
31
+ await writeFile(destinationPath, data, encoding)
32
+ database.freshenDependency(destination.path)
53
33
  }
54
-
34
+
55
35
  return write()
56
36
  }
57
37
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "votive",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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"