vowel 0.2.4 → 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.
Files changed (74) hide show
  1. package/README.md +134 -26
  2. package/bin.js +209 -1
  3. package/config.js +20 -0
  4. package/docs-source/.votive.db +0 -0
  5. package/docs-source/blog/url-ui.md +1 -5
  6. package/docs-source/blog.md +5 -0
  7. package/docs-source/docs/items.md +1 -1
  8. package/docs-source/home.md +1 -1
  9. package/docs-source/output/about.html +1 -0
  10. package/docs-source/output/blog/url-ui.html +8 -0
  11. package/docs-source/output/blog.html +1 -0
  12. package/docs-source/output/default.css +1 -0
  13. package/docs-source/output/docs/deploy.html +34 -0
  14. package/docs-source/output/docs/file-structure.html +24 -0
  15. package/docs-source/output/docs/folder-settings.html +20 -0
  16. package/docs-source/output/docs/images.html +2 -0
  17. package/docs-source/output/docs/items.html +6 -0
  18. package/docs-source/output/docs/pages.html +101 -0
  19. package/docs-source/output/docs/styling.html +18 -0
  20. package/docs-source/output/docs/taxonomies.html +20 -0
  21. package/docs-source/output/docs.html +28 -0
  22. package/docs-source/output/features/cards.html +1 -0
  23. package/docs-source/output/features/editing.html +1 -0
  24. package/docs-source/output/features/emoji.html +1 -0
  25. package/docs-source/output/features/frontmatter.html +1 -0
  26. package/docs-source/output/features/lists.html +1 -0
  27. package/docs-source/output/features/navigation.html +1 -0
  28. package/docs-source/output/features/rich-previews.html +1 -0
  29. package/docs-source/output/features/robots.html +1 -0
  30. package/docs-source/output/features/rss.html +1 -0
  31. package/docs-source/output/features/sitemap.html +1 -0
  32. package/docs-source/output/features/speed.html +1 -0
  33. package/docs-source/output/features/static.html +1 -0
  34. package/docs-source/output/features/taxonomies.html +1 -0
  35. package/docs-source/output/features.html +1 -0
  36. package/docs-source/output/feed.xml +1 -0
  37. package/docs-source/output/index.html +21 -0
  38. package/docs-source/output/reset.css +1 -0
  39. package/docs-source/output/roadmap.html +87 -0
  40. package/docs-source/output/robots.txt +14 -0
  41. package/docs-source/output/sitemap.xml +16 -0
  42. package/docs-source/output/typography.css +1 -0
  43. package/docs-source/settings.md +1 -0
  44. package/getMetadata.js +1 -1
  45. package/index.js +5 -660
  46. package/package.json +18 -3
  47. package/plugins/fonts/index.js +26 -0
  48. package/plugins/icons/index.js +26 -0
  49. package/plugins/images/index.js +45 -0
  50. package/plugins/markdown/index.js +1097 -0
  51. package/plugins/robots/index.js +23 -0
  52. package/plugins/styles/index.js +69 -0
  53. package/plugins/vectors/index.js +38 -0
  54. package/plugins/xml/index.js +196 -0
  55. package/stylesheets/DefaultStyles.css +329 -263
  56. package/stylesheets/ResetStyles.css +119 -123
  57. package/stylesheets/TypographyStyles.css +259 -242
  58. package/docs-source/.vercel/README.txt +0 -11
  59. package/docs-source/.vercel/project.json +0 -1
  60. package/docs-source/assets/styles.css +0 -51
  61. package/docs-source/blog/home.md +0 -5
  62. /package/docs-source/{$features → features}/cards.md +0 -0
  63. /package/docs-source/{$features → features}/editing.md +0 -0
  64. /package/docs-source/{$features → features}/emoji.md +0 -0
  65. /package/docs-source/{$features → features}/frontmatter.md +0 -0
  66. /package/docs-source/{$features → features}/lists.md +0 -0
  67. /package/docs-source/{$features → features}/navigation.md +0 -0
  68. /package/docs-source/{$features → features}/rich-previews.md +0 -0
  69. /package/docs-source/{$features → features}/robots.md +0 -0
  70. /package/docs-source/{$features → features}/rss.md +0 -0
  71. /package/docs-source/{$features → features}/sitemap.md +0 -0
  72. /package/docs-source/{$features → features}/speed.md +0 -0
  73. /package/docs-source/{$features → features}/static.md +0 -0
  74. /package/docs-source/{$features → features}/taxonomies.md +0 -0
package/index.js CHANGED
@@ -1,667 +1,12 @@
1
1
  import voot from "voot"
2
- import { h } from 'hastscript'
3
- import fs from "fs/promises"
4
- import { readFileSync } from "fs"
5
- import { rehype } from "rehype"
6
- import { find } from 'unist-util-find'
7
- import { toHast } from 'mdast-util-to-hast'
8
- import { fromHtml } from 'hast-util-from-html'
9
- import { fromMarkdown } from 'mdast-util-from-markdown'
10
- import { frontmatter } from "micromark-extension-frontmatter"
11
- import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
12
- import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"
13
- import { visit } from "unist-util-visit"
14
- import { gfmFootnote } from "micromark-extension-gfm-footnote"
15
- import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough'
16
- import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
17
- import { gfmTable } from 'micromark-extension-gfm-table'
18
- import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
19
- import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item' // TODO: Add
20
- import { gfmTaskListItemFromMarkdown } from 'mdast-util-gfm-task-list-item' // TODO: Add
21
- import { normalizeHeadings } from 'mdast-normalize-headings'
22
- import { toString as mdastToString } from 'mdast-util-to-string'
23
- import { toString as hastToString } from 'hast-util-to-string'
24
- import yaml from 'yaml'
25
- import extractDate from "./extractDate.js"
26
- import { testURL } from "./utils.js"
27
- import path from "node:path"
28
- import { styleText } from "node:util"
29
-
30
- const VOWEL_DIR = import.meta.dirname
31
-
32
-
33
- /** @import {Runner, ReadPath, VotiveConfig, VotivePlugin, VotiveProcessor, ReadText, ReadAbstract, ReadFolder, ProcessorWrite, Router} from "votive" */
34
-
35
- /** @type {VotiveProcessor} */
36
- const cssWriter = {
37
- syntax: "css",
38
- write: writeCSS
39
- }
40
-
41
- /** @type {VotiveProcessor} */
42
- const jpegLoader = {
43
- syntax: "jpeg",
44
- filter: {
45
- extensions: [".jpeg", ".jpg"]
46
- },
47
- read: {
48
- path: readImagePath,
49
- },
50
- write: writeImage
51
- }
52
-
53
-
54
- /** @type {ProcessorWrite} */
55
- function writeCSS(destination, database, config) {
56
- return {
57
- data: destination.abstract.css,
58
- encoding: "utf-8"
59
- }
60
- }
61
-
62
- function readURL(data) {
63
- const hast = fromHtml(data)
64
- const metadata = {}
65
-
66
- visit(hast, (node) => {
67
- if (node.tagName === "meta") {
68
- if (node.properties && node.properties.property) {
69
- metadata[node.properties.property] = node.properties.content
70
- }
71
- } else if (node.tagName === "title") {
72
- metadata.title = hastToString(node)
73
- } else if (node.tagName === "link") {
74
- if (node.properties?.rel?.includes("me")) {
75
- metadata.me = node.properties.href
76
- } else if (node.properties?.rel?.includes("webmention")) {
77
- metadata.webmention = node.properties.href
78
- } else if (node.properties?.rel?.includes("icon")) {
79
- metadata.icon = node.properties.href
80
- }
81
- }
82
- })
83
-
84
- return metadata
85
- }
86
-
87
- /** @type {VotiveProcessor} */
88
- const markdownReader = {
89
- syntax: "mdast",
90
- filter: { extensions: [".md"] },
91
- read: {
92
- text: readMarkdown,
93
- url: readURL,
94
- abstract: readAbstract,
95
- folder: readFolder
96
- },
97
- write: writeHTML
98
- }
99
-
100
-
101
- async function removeCache() {
102
- try {
103
- await fs.rm("./.votive.db")
104
- console.info(styleText("yellow", "Cache cleared"))
105
- } catch (e) {
106
- console.info(styleText("yellow", "No database cache found"))
107
- }
108
- }
109
-
110
- async function removeDB() {
111
- try {
112
- await fs.rmdir("./output")
113
- console.info(styleText("yellow", "Output cleared"))
114
- } catch (e) {
115
- console.info(styleText("yellow", "No output cache found"))
116
- }
117
- }
118
-
119
- await removeCache()
120
- await removeDB()
121
-
122
- /** @type {ReadText} */
123
- function readMarkdown(string, filePath, destinationPath, database, config) {
124
- const mdast = fromMarkdown(string, {
125
- // Micromark extensions
126
- extensions: [
127
- frontmatter(),
128
- gfmFootnote(),
129
- gfmStrikethrough(),
130
- gfmTable()
131
- ],
132
- mdastExtensions: [
133
- frontmatterFromMarkdown(),
134
- gfmFootnoteFromMarkdown(),
135
- gfmStrikethroughFromMarkdown(),
136
- gfmTableFromMarkdown()
137
- ]
138
- })
139
-
140
- normalizeHeadings(mdast)
141
- const pathInfo = path.parse(filePath)
142
- const metadata = getMetadata(mdast)
143
- metadata.inferred_label = pathInfo.name
144
- const destinationInfo = path.parse(destinationPath)
145
- metadata.prettyURL = (new URL(`${destinationInfo.dir}/${destinationInfo.name}`, "thismessage:/")).pathname
146
- const jobs = []
147
-
148
- selectMetadata(metadata)
149
-
150
- if (pathInfo.base === "settings.md") {
151
- for (const key in metadata) {
152
- database.setSetting(
153
- pathInfo.dir,
154
- key,
155
- metadata[key],
156
- filePath
157
- )
158
- }
159
- }
160
-
161
- visit(mdast, (node, index, parent) => {
162
- if (node.type === "text" && parent.children.length === 1 && parent.type === "paragraph") {
163
- const validURL = testURL(node.value)
164
- if (validURL) {
165
-
166
- jobs.push({
167
- data: node.value,
168
- runner: "text",
169
- destination: destinationPath
170
- })
171
- }
172
- }
173
- })
174
-
175
- return { abstract: mdast, metadata, jobs }
176
- }
177
-
178
- /** @param {ReturnType<getMetadata>} metadata */
179
- function selectMetadata(metadata) {
180
- const date =
181
- metadata.fm_date
182
- || metadata.inferred_date
183
-
184
- if (date) {
185
- metadata.date = date
186
- }
187
-
188
- const title =
189
- metadata.fm_title
190
- || metadata.inferred_title
191
- || metadata.inferred_label
192
-
193
- if (title) {
194
- metadata.title = title
195
- }
196
-
197
- const breadcrumb =
198
- metadata.fm_breadcrumb
199
- || metadata.title
200
- || metadata.inferred_label
201
-
202
- if (breadcrumb) {
203
- metadata.breadcrumb = breadcrumb
204
- }
205
-
206
- const description =
207
- metadata.fm_description
208
- || metadata.inferred_description
209
-
210
- if (description) {
211
- metadata.description = description
212
- }
213
-
214
- const image =
215
- metadata.fm_image
216
- || metadata.inferred_image
217
-
218
- if (image) {
219
- metadata.image = image
220
- }
221
- }
222
-
2
+ import { config } from "./config.js"
223
3
 
224
4
  /**
225
- * @param {string} text
5
+ * @param {object} options
6
+ * @property {"verbose" | "essential" | "silent"} logging
226
7
  */
227
- function truncateText(text) {
228
- const match = text.match(/^(?<desc>((\b.+?){28})\S)\s(?<etc>.+)$/)
229
- if (!match) {
230
- return text
231
- } else if (match.groups.etc) {
232
- return match.groups.desc + "..."
233
- } else {
234
- return match.groups.desc
235
- }
236
- }
237
-
238
- /** @param {object} tree */
239
- function getMetadata(tree) {
240
- const metadata = {}
241
-
242
- for (let i = 0; i < tree.children.length; i++) {
243
- const child = tree.children[i]
244
- const text = mdastToString(child)
245
- switch (child.type) {
246
- case "paragraph":
247
- if (child.children.length !== 1) {
248
- if (!metadata.fm_description && !metadata.inferred_description) {
249
- const description = truncateText(text)
250
- metadata.inferred_description = description
251
- }
252
- i = Infinity
253
- break
254
- } else if (child.children[0].type === "image") {
255
- metadata.inferred_image = child.children[0].url
256
- metadata.inferred_alt_text = child.children[0].alt
257
- break
258
- } else if (testURL(text)) {
259
- const url = new URL(text)
260
- if (text.match(/\.(jpeg|jpg|png)$/)) {
261
- metadata.inferred_image = url
262
- break
263
- } else {
264
- metadata.inferred_link = url
265
- break
266
- }
267
- } else {
268
- const inferred_date = extractDate(mdastToString(child))
269
- if (inferred_date) metadata.inferred_date = inferred_date
270
- else i = Infinity
271
- break
272
- }
273
- case "heading":
274
- if (child.depth === 1) {
275
- metadata.inferred_title = mdastToString(child)
276
- }
277
- break
278
- case "yaml":
279
- const frontmatter = yaml.parse(child.value)
280
- for (const key in frontmatter) {
281
- metadata["fm_" + key] = frontmatter[key]
282
- }
283
- break
284
- default:
285
- i = Infinity
286
- break
287
- }
288
- }
289
-
290
- // TODO: Extract links!
291
- // TODO: Extract backlinks
292
- return metadata
293
-
294
- }
295
-
296
- /** @type {ReadAbstract} */
297
- function readAbstract(abstract, database, config) {
298
- const jobs = []
299
- return { abstract, jobs }
300
- }
301
-
302
- /** @type {ReadFolder} */
303
- function readFolder(folder, database, config, isRoot) {
304
- const folderInfo = path.parse(folder)
305
-
306
- const indexPath = path.format({
307
- dir: path.join(folderInfo.dir, folderInfo.name),
308
- name: "index",
309
- ext: ".html"
310
- })
311
-
312
- const aliasPath = path.format({
313
- dir: path.join(folderInfo.dir),
314
- name: folderInfo.name,
315
- ext: ".html"
316
- })
317
-
318
- const indexFile = database.getDestinationIndependently(indexPath)
319
- const aliasFile = database.getDestinationIndependently(aliasPath)
320
-
321
- if (!indexFile && !aliasFile) {
322
- database.createOrUpdateDestination({
323
- metadata: {
324
- title: folderInfo.name
325
- },
326
- path: aliasPath,
327
- abstract: {},
328
- syntax: "mdast"
329
- })
330
- }
331
-
332
-
333
- if (isRoot) {
334
- // TODO: Create theme job
335
- const settings = database.getSettings(config.sourceFolder)
336
- if (!settings.theme || settings.theme[0] === "default") {
337
- database.setSetting(folder, "theme", "default")
338
- // TODO: Save theme file (reset, typography, default)
339
-
340
- const resetStylesPath = path.join(VOWEL_DIR, "stylesheets", "ResetStyles.css")
341
- const typeStylesPath = path.join(VOWEL_DIR, "stylesheets", "TypographyStyles.css")
342
- const defaultStylesPath = path.join(VOWEL_DIR, "stylesheets", "DefaultStyles.css")
343
-
344
- const resetStyles = readFileSync(resetStylesPath, "utf-8")
345
- const typeStyles = readFileSync(typeStylesPath, "utf-8")
346
- const defaultStyles = readFileSync(defaultStylesPath, "utf-8")
347
-
348
- database.createOrUpdateDestination({
349
- path: "reset.css",
350
- abstract: { css: resetStyles },
351
- metadata: {},
352
- syntax: "css"
353
- })
354
-
355
- database.createOrUpdateDestination({
356
- path: "typography.css",
357
- abstract: { css: typeStyles },
358
- metadata: {},
359
- syntax: "css"
360
- })
361
-
362
- database.createOrUpdateDestination({
363
- path: "default.css",
364
- abstract: { css: defaultStyles },
365
- metadata: {},
366
- syntax: "css"
367
- })
368
- }
369
- // TODO: Create robots
370
- // TODO: Create RSS
371
- // TODO: Create sitemap
372
- }
373
-
374
-
375
-
376
- return {
377
- jobs: [],
378
- destinations: []
379
- }
380
- }
381
-
382
- /** @type {ProcessorWrite} */
383
- function writeHTML(destination, database, config) {
384
- const settings = database.getSettings(destination.dir)
385
- const { abstract, metadata, ...rest } = destination
386
-
387
- /** @param {string} filePath */
388
- function listFolders(filePath) {
389
- if (!filePath) return []
390
-
391
- const pathInfo = path.parse(filePath)
392
- const dir = pathInfo.dir && pathInfo.dir + path.sep
393
-
394
- return [filePath, ...listFolders(
395
- dir
396
- )]
397
- }
398
-
399
- const ancestorFolders = listFolders(rest.dir)
400
-
401
- const family = database.getDestinations({
402
- filter: [
403
- {
404
- property: "dir",
405
- operator: "in",
406
- value: ancestorFolders
407
- }
408
- ]
409
- }, destination.path)
410
-
411
- const treeStyleSheets = []
412
-
413
- !settings.theme || settings.theme.includes("default") && treeStyleSheets.push(
414
- h('link', {
415
- rel: "stylesheet",
416
- href: "/default.css"
417
- }),
418
- h('link', {
419
- rel: "stylesheet",
420
- href: "/typography.css"
421
- }),
422
- h('link', {
423
- rel: "stylesheet",
424
- href: "/reset.css"
425
- })
426
- )
427
-
428
-
429
- function createTitle() {
430
- if (metadata.title && settings.title) {
431
- const titles = [metadata.title, ...settings.title.reverse()]
432
- return titles.join(" - ")
433
- }
434
-
435
- if (metadata.title || settings.title) {
436
- return metadata.title || settings.title.reverse()
437
- }
438
-
439
- return "Website"
440
- }
441
-
442
- const treeHead = h('head', [
443
- h('title', createTitle()),
444
- h('meta', {
445
- property: "og:description",
446
- content: metadata.description,
447
- }),
448
- ...treeStyleSheets,
449
- // TODO: Favicon
450
- // TODO: Site name
451
- // TODO: og:url
452
- // TODO: Canonical URL
453
- // TODO: REL=ME (github)
454
- // TODO: Webmention URL
455
- // TODO: Image
456
- ])
457
-
458
-
459
- function treeNavItems(navItem) {
460
- return h('a', {
461
- href: navItem.metadata.prettyURL,
462
- "aria-current": metadata.prettyURL === navItem.metadata.prettyURL ? 'page' : null
463
- }, navItem.metadata.breadcrumb)
464
- }
465
-
466
- function navItemFilter(nav_item) {
467
- return !nav_item.date
468
- }
469
-
470
-
471
- function treeNavFolder(navFolder) {
472
- return navFolder.length > 1
473
- ? h('nav.primary', navFolder.filter(navItemFilter).map(treeNavItems))
474
- : null
475
- }
476
-
477
- // TODO: Filter singleton navs
478
- // TODO: Filter ephemeral content
479
-
480
- const groupedNavs = Object.groupBy(family, ({ dir }) => dir)
481
-
482
- const treeNav = Object.values(groupedNavs).map(treeNavFolder)
483
-
484
- let treeBreadcrumbs = []
485
-
486
- if (settings.breadcrumbs) {
487
- treeBreadcrumbs.push(
488
- ...settings.breadcrumb.map((b, i) => h('a', {
489
- href: settings.prettyURL[i]
490
- }, b))
491
- )
492
- }
493
-
494
- treeBreadcrumbs.push(
495
- h('a', {
496
- href: destination.prettyURL,
497
- 'aria-current': 'page'
498
- }, metadata.breadcrumb)
499
- )
500
-
501
-
502
- const headerElements = []
503
-
504
- if (settings.title) {
505
- headerElements.push(h('a.site-title', settings.title[0]))
506
- }
507
-
508
- if (settings.description) {
509
- headerElements.push(h('p.subtitle', settings.description[0]))
510
- }
511
-
512
- const treeHeader = h('header', [
513
- // TODO: Logo
514
- ...headerElements,
515
- ...treeNav,
516
- h('nav.breadcrumbs', {
517
- 'aria-label': 'Breadcrumb'
518
- }, treeBreadcrumbs)
519
- ])
520
-
521
- // TODO: Add page ID as a class
522
-
523
- const treeMain = h('main.h-entry', toHast(abstract).children)
524
-
525
- visit(treeMain, (node, index, parent) => {
526
- if (node.type === "text" && parent.tagName === 'p' && parent.children.length === 1) {
527
- const validURL = testURL(node.value)
528
- if (validURL) {
529
- const metadata = database.getURL(node.value)
530
- if (metadata) {
531
- parent.tagName = "article"
532
- // TODO: Add url to metadata
533
- parent.children = [
534
- h("a", { href: node.value },
535
- h("h2", metadata.title)
536
- )
537
- ]
538
- }
539
- }
540
- }
541
- })
542
-
543
- // TODO: Write sidebar
544
- const treeSidebar = h('nav.secondary')
545
- const treeFooter = h('footer', `© ${new Date().getFullYear()}`)
546
-
547
- const treeBody = h('body.page', [
548
- treeHeader,
549
- treeMain,
550
- treeSidebar,
551
- treeFooter
552
- ])
553
-
554
- const tree = h(
555
- null,
556
- [
557
- // TODO: Add doctype
558
- h('html', [
559
- treeHead,
560
- treeBody
561
- ])
562
- ]
563
- )
564
-
565
- // Mutate
566
- // Get links
567
- // - section, article.thumbnail
568
-
569
- // TODO: WRITE HTML
570
-
571
- const data = rehype()
572
- .stringify(tree)
573
-
574
-
575
- return {
576
- data
577
- }
578
- }
579
-
580
- /** @type {Router} */
581
- function router({ name, dir, inRootDir }) {
582
- if (name.startsWith("$")) return false
583
- if (dir.find(segment => segment.startsWith("$"))) return false
584
-
585
- switch (name) {
586
- case "settings":
587
- return false
588
- case "home":
589
- if (inRootDir) {
590
- return {
591
- dir,
592
- name: "index",
593
- ext: ".html"
594
- }
595
- }
596
- return {
597
- dir: dir.slice(0, -1).map(segment => segment.replaceAll(/[^\w\/]/g, "-").replaceAll(/--+/g, "-").toLowerCase()),
598
- name: dir.at(-1),
599
- ext: ".html"
600
- }
601
- default:
602
- return {
603
- dir: dir.map(segment => segment.replaceAll(/[^\w\/]/g, "-").replaceAll(/--+/g, "-").toLowerCase()),
604
- name: name.replaceAll(/[^\w\/]/g, "-").replaceAll(/--+/g, "-").toLowerCase(),
605
- ext: "html"
606
- }
607
- }
608
- }
609
-
610
- /** @type {ReadPath} */
611
- async function readImagePath(string, database) {
612
- // TODO: Resize and optimize images
613
-
614
- return {
615
- metadata: {},
616
- abstract: { path: string }
617
- }
618
- }
619
-
620
- /** @type {ProcessorWrite} */
621
- async function writeImage(destination, database, config) {
622
- const buffer = await fs.readFile(destination.path)
623
- return {
624
- buffer
625
- }
626
- }
627
-
628
- /** @type {VotivePlugin} */
629
- const vowelMarkdown = {
630
- name: "vowel",
631
- processors: [markdownReader, cssWriter],
632
- router
633
- }
634
-
635
- /** @type {VotivePlugin} */
636
- const vowelJpeg = {
637
- name: "vowel-jpeg",
638
- processors: [jpegLoader],
639
- router: ({ name, dir, ext }) => {
640
- return {
641
- name, dir, ext
642
- }
643
- }
644
- }
645
-
646
-
647
-
648
- /** @type {VotiveConfig} */
649
- const config = {
650
- sourceFolder: ".",
651
- destinationFolder: "output",
652
- plugins: [
653
- vowelMarkdown,
654
- vowelJpeg
655
- ]
656
- }
657
-
658
-
659
- async function init() {
660
- const then = performance.now()
661
- // await fs.rm(config.destinationFolder, { recursive: true, force: true })
662
- // await fs.mkdir(config.destinationFolder, { recursive: true })
663
- const cache = await voot(config)
664
- console.log(styleText("red", (performance.now() - then).toFixed(4) + "ms"))
8
+ async function init(options) {
9
+ const cache = await voot({ ...config, ...options })
665
10
  }
666
11
 
667
12