votive 0.2.0 → 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,64 +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
-
43
- // const sources = readingSourceFiles && (await Promise.all(readingSourceFiles)).filter(a => a)
44
-
45
- console.log({ sources })
46
-
47
- // const deletedSources = await pruneDeletions(config, database, filteredDirents)
48
-
49
- console.log({ deletedSources })
50
-
32
+ const sources = readingSourceFiles && await Promise.all(readingSourceFiles)
51
33
  return {
52
34
  folders,
53
- sources: [...sources, ...deletedSources]
35
+ sources
54
36
  }
55
37
  }
56
38
 
57
39
  /**
58
40
  * @param {VotiveConfig} config
59
41
  * @param {Database} database
60
- * @param {Dirent[]} dirents
61
- */
62
- async function pruneDeletions(config, database, dirents) {
63
- const sourceFilePaths = new Set(dirents.map(d => d.isFile() && path.join(d.parentPath, d.name)))
64
- const sourceRecords = database.source.getAll()
65
- const sourceRecordPaths = new Set(sourceRecords.map(r => r.path))
66
-
67
- const deletions = sourceRecordPaths.difference(sourceFilePaths)
68
-
69
- let deletedSources = []
70
-
71
- deletions.forEach(deletion => {
72
- deletedSources.push(database.source.delete(deletion))
73
- })
74
-
75
- return deletedSources.filter(a => a)
76
- }
77
-
78
- /**
79
- * @param {VotiveConfig} config
80
42
  */
81
- function fileFilter(config) {
43
+ function fileFilter(config, database) {
82
44
  /** @param {Dirent} dirent */
83
45
  return (dirent) => {
84
- const isDestinationFolder = !path.relative(config.destinationFolder, path.join(dirent.parentPath, dirent.name))
85
-
86
46
  if (dirent.parentPath === config.destinationFolder) {
87
47
  return false
88
48
  } else if (dirent.parentPath.startsWith(config.destinationFolder + path.sep)) {
@@ -91,28 +51,21 @@ function fileFilter(config) {
91
51
  return false // Ignore hidden files
92
52
  } else if (dirent.parentPath.includes(path.sep + ".")) {
93
53
  return false // Ignore hidden folders
94
- } else if (dirent.parentPath.match(/^\.\w/)) {
95
- return false // Ignore hidden folders
96
- } if (isDestinationFolder) {
97
- return false
98
54
  }
99
55
  return true
100
56
  }
101
57
  }
102
58
 
103
59
  /**
104
- * @typedef {ReadSourceFileResult[]} ReadSourceFilesResult
60
+ * @typedef {ReadSourceFilePlugin[]} ReadSourceFileResult
105
61
  */
106
62
 
107
63
  /**
108
- * @typedef {object} ReadSourceFileResult
64
+ * @typedef {object} ReadSourceFilePlugin
109
65
  * @property {Abstract} abstract
110
66
  * @property {ProcessorSyntax} syntax
111
- * @property {object} [metadata]
112
- * @property {string} [targetFilePath]
113
- * @property {string} [dir]
114
67
  * @property {Jobs} jobs
115
- * @property {string} sourceFilePath
68
+ * @property {string} destinationPath
116
69
  */
117
70
 
118
71
  /**
@@ -123,12 +76,12 @@ function fileFilter(config) {
123
76
  function readSourceFile(processors, database, config) {
124
77
  /**
125
78
  * @param {import("node:fs").Dirent} dirent
126
- * @returns {Promise<ReadSourceFileResult>[]}
79
+ * @returns {Promise<ReadSourceFilePlugin>[]}
127
80
  */
128
81
  return (dirent) => {
129
82
  const { name, parentPath } = dirent
130
- const sourceFilePath = path.join(parentPath, name)
131
- const sourceFileInfo = path.parse(sourceFilePath)
83
+ const filePath = path.join(parentPath, name)
84
+ const fileInfo = path.parse(filePath)
132
85
 
133
86
  const processing = processors.flatMap(({ plugin, processor }) => process(plugin, processor, config))
134
87
 
@@ -138,98 +91,43 @@ function readSourceFile(processors, database, config) {
138
91
  * @param {VotiveConfig} config
139
92
  */
140
93
  async function process(plugin, processor, config) {
141
- const { readFile: read, extensions: filter, format } = processor
142
- if (filter.includes(sourceFileInfo.ext) && read) {
94
+ const { read, filter, syntax } = processor
95
+ if (filter.extensions.includes(fileInfo.ext) && read && read.text) {
143
96
 
144
97
  // Check modified time
145
- const stat = await fs.stat(sourceFilePath)
146
- const source = database.source.get(sourceFilePath)
147
- const diff = source && source.lastModified - Number(Math.floor(stat.mtimeMs))
148
-
149
- if (source && diff > -1) {
150
- return null
151
- }
152
-
153
- database.setting.deleteBySource(sourceFilePath)
98
+ const stat = await fs.stat(filePath)
99
+ const source = database.getSource(filePath)
154
100
 
155
- const targetFilePath = route(sourceFilePath, plugin, config)
156
- const targetFileExtension = path.extname(targetFilePath)
157
101
 
158
- // Set URL if exists
159
- const allJobs = []
160
-
161
- if (format === "buffer") {
162
-
163
- // FIXME update this type
164
- const data = read(sourceFilePath, database, config)
165
- const { metadata, abstract, jobs } = data
166
-
167
- const target = database.target.create({
168
- metadata,
169
- abstract,
170
- path: targetFilePath,
171
- })
172
-
173
-
174
- if (Array.isArray(jobs)) {
175
- allJobs.push(...jobs)
176
- }
177
- }
178
-
179
- if (format === "text") {
180
- const stats = await fs.stat(sourceFilePath)
181
- const data = await fs.readFile(sourceFilePath, { encoding: "utf-8" })
182
-
183
- const { jobs, abstract, metadata } = read(data, sourceFilePath, targetFilePath, database, config)
102
+ if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
184
103
 
104
+ database.deleteSettings(filePath)
185
105
 
186
- if (Array.isArray(jobs)) {
187
- allJobs.push(...jobs)
188
- }
106
+ // Get destination route
107
+ const destinationPath = route(filePath, plugin)
189
108
 
190
- const target = database.target.create({
191
- abstract,
192
- path: targetFilePath,
193
- metadata,
194
- })
195
-
196
- // FIXME this won't work with the refactor
197
- allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
198
- updateSource()
199
- return {
200
- abstract,
201
- metadata,
202
- syntax: targetFileExtension,
203
- targetFilePath: target.path,
204
- dir: target.dir,
205
- sourceFilePath,
206
- jobs: allJobs
207
- }
208
- }
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)
209
114
 
210
- // FIXME Job syntax
211
- allJobs && allJobs.forEach(job => job.syntax = processor.extensions[0])
212
- updateSource()
115
+ const timeStamp = stat.mtimeMs.toFixed()
213
116
 
214
- return {
215
- jobs: allJobs,
216
- abstract: null,
217
- targetFilePath: null,
218
- sourceFilePath: null,
219
- metadata: null,
220
- syntax: null
117
+ if(source) {
118
+ database.updateSource(filePath, Number(timeStamp))
119
+ } else {
120
+ database.createSource(filePath, destinationPath, Number(timeStamp))
221
121
  }
222
122
 
223
- function updateSource() {
224
- const timeStamp = stat.mtimeMs.toFixed()
225
-
226
- if (source) {
227
- database.source.updateTimestamp(sourceFilePath, Number(timeStamp))
228
- } else {
229
- database.source.create(sourceFilePath, targetFilePath, Number(timeStamp))
230
- }
231
- }
123
+ database.createOrUpdateDestination({
124
+ metadata,
125
+ path: destinationPath,
126
+ abstract: abstract,
127
+ syntax: syntax
128
+ })
232
129
 
130
+ return { abstract, destinationPath, syntax, jobs }
233
131
  }
234
132
  }
235
133
 
@@ -240,43 +138,10 @@ function readSourceFile(processors, database, config) {
240
138
  /**
241
139
  * @param {string} filePath
242
140
  * @param {VotivePlugin} plugin
243
- * @param {VotiveConfig} config
244
141
  */
245
- function route(filePath, plugin, config) {
246
- const { dir, ...parsedPath } = path.parse(filePath)
247
- const rooty = !path.relative(config.sourceFolder, dir)
248
- const segments = ["", ...dir.split(path.sep).filter(a => a)]
249
- const pathInfo = {
250
- inRootDir: rooty,
251
- dir: segments,
252
- ...parsedPath
253
- }
254
-
255
- if (!plugin.router) return "0"
256
-
257
- const routedPath = plugin.router(pathInfo)
258
-
259
- if (!routedPath) return "0"
260
-
261
- if (routedPath.hasOwnProperty("dir") && Array.isArray(routedPath.dir)) {
262
- return path.normalize(path.format({
263
- dir: path.join(...routedPath.dir),
264
- root: routedPath.root || "",
265
- base: routedPath.base,
266
- name: routedPath.name,
267
- ext: routedPath.ext
268
- }))
269
- }
270
-
271
- const routedInfo = {
272
- dir: routedPath.dir || "",
273
- root: routedPath.root || "",
274
- base: routedPath.base,
275
- name: routedPath.name,
276
- ext: routedPath.ext
277
- }
278
-
279
- return path.normalize(path.format(routedInfo))
142
+ function route(filePath, plugin) {
143
+ if (!plugin.router) return ""
144
+ return plugin.router(filePath) || ""
280
145
  }
281
146
 
282
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.0",
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"