@prudentbird/voxx-core 1.0.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  4. package/dist/config.cjs +151 -0
  5. package/dist/config.cjs.map +1 -0
  6. package/dist/config.d.cts +122 -0
  7. package/dist/config.d.cts.map +1 -0
  8. package/dist/config.d.mts +122 -0
  9. package/dist/config.d.mts.map +1 -0
  10. package/dist/config.mjs +149 -0
  11. package/dist/config.mjs.map +1 -0
  12. package/dist/content.cjs +147 -0
  13. package/dist/content.cjs.map +1 -0
  14. package/dist/content.d.cts +41 -0
  15. package/dist/content.d.cts.map +1 -0
  16. package/dist/content.d.mts +41 -0
  17. package/dist/content.d.mts.map +1 -0
  18. package/dist/content.mjs +145 -0
  19. package/dist/content.mjs.map +1 -0
  20. package/dist/dev.cjs +82 -0
  21. package/dist/dev.cjs.map +1 -0
  22. package/dist/dev.d.cts +24 -0
  23. package/dist/dev.d.cts.map +1 -0
  24. package/dist/dev.d.mts +24 -0
  25. package/dist/dev.d.mts.map +1 -0
  26. package/dist/dev.mjs +82 -0
  27. package/dist/dev.mjs.map +1 -0
  28. package/dist/effect.cjs +23 -0
  29. package/dist/effect.d.cts +8 -0
  30. package/dist/effect.d.mts +8 -0
  31. package/dist/effect.mjs +8 -0
  32. package/dist/errors.cjs +20 -0
  33. package/dist/errors.cjs.map +1 -0
  34. package/dist/errors.d.cts +45 -0
  35. package/dist/errors.d.cts.map +1 -0
  36. package/dist/errors.d.mts +45 -0
  37. package/dist/errors.d.mts.map +1 -0
  38. package/dist/errors.mjs +16 -0
  39. package/dist/errors.mjs.map +1 -0
  40. package/dist/feeds.cjs +97 -0
  41. package/dist/feeds.cjs.map +1 -0
  42. package/dist/feeds.d.cts +49 -0
  43. package/dist/feeds.d.cts.map +1 -0
  44. package/dist/feeds.d.mts +49 -0
  45. package/dist/feeds.d.mts.map +1 -0
  46. package/dist/feeds.mjs +94 -0
  47. package/dist/feeds.mjs.map +1 -0
  48. package/dist/frontmatter.cjs +22 -0
  49. package/dist/frontmatter.cjs.map +1 -0
  50. package/dist/frontmatter.d.cts +30 -0
  51. package/dist/frontmatter.d.cts.map +1 -0
  52. package/dist/frontmatter.d.mts +30 -0
  53. package/dist/frontmatter.d.mts.map +1 -0
  54. package/dist/frontmatter.mjs +20 -0
  55. package/dist/frontmatter.mjs.map +1 -0
  56. package/dist/index.cjs +90 -0
  57. package/dist/index.cjs.map +1 -0
  58. package/dist/index.d.cts +50 -0
  59. package/dist/index.d.cts.map +1 -0
  60. package/dist/index.d.mts +50 -0
  61. package/dist/index.d.mts.map +1 -0
  62. package/dist/index.mjs +59 -0
  63. package/dist/index.mjs.map +1 -0
  64. package/dist/llms.cjs +81 -0
  65. package/dist/llms.cjs.map +1 -0
  66. package/dist/llms.d.cts +43 -0
  67. package/dist/llms.d.cts.map +1 -0
  68. package/dist/llms.d.mts +43 -0
  69. package/dist/llms.d.mts.map +1 -0
  70. package/dist/llms.mjs +78 -0
  71. package/dist/llms.mjs.map +1 -0
  72. package/dist/nav.cjs +50 -0
  73. package/dist/nav.cjs.map +1 -0
  74. package/dist/nav.d.cts +16 -0
  75. package/dist/nav.d.cts.map +1 -0
  76. package/dist/nav.d.mts +16 -0
  77. package/dist/nav.d.mts.map +1 -0
  78. package/dist/nav.mjs +50 -0
  79. package/dist/nav.mjs.map +1 -0
  80. package/dist/render.cjs +152 -0
  81. package/dist/render.cjs.map +1 -0
  82. package/dist/render.d.cts +29 -0
  83. package/dist/render.d.cts.map +1 -0
  84. package/dist/render.d.mts +29 -0
  85. package/dist/render.d.mts.map +1 -0
  86. package/dist/render.mjs +143 -0
  87. package/dist/render.mjs.map +1 -0
  88. package/dist/schema.cjs +78 -0
  89. package/dist/schema.cjs.map +1 -0
  90. package/dist/schema.d.cts +93 -0
  91. package/dist/schema.d.cts.map +1 -0
  92. package/dist/schema.d.mts +93 -0
  93. package/dist/schema.d.mts.map +1 -0
  94. package/dist/schema.mjs +77 -0
  95. package/dist/schema.mjs.map +1 -0
  96. package/dist/seo.cjs +77 -0
  97. package/dist/seo.cjs.map +1 -0
  98. package/dist/seo.d.cts +15 -0
  99. package/dist/seo.d.cts.map +1 -0
  100. package/dist/seo.d.mts +15 -0
  101. package/dist/seo.d.mts.map +1 -0
  102. package/dist/seo.mjs +77 -0
  103. package/dist/seo.mjs.map +1 -0
  104. package/dist/types.cjs +45 -0
  105. package/dist/types.cjs.map +1 -0
  106. package/dist/types.d.cts +138 -0
  107. package/dist/types.d.cts.map +1 -0
  108. package/dist/types.d.mts +138 -0
  109. package/dist/types.d.mts.map +1 -0
  110. package/dist/types.mjs +45 -0
  111. package/dist/types.mjs.map +1 -0
  112. package/dist/util.cjs +185 -0
  113. package/dist/util.cjs.map +1 -0
  114. package/dist/util.d.cts +98 -0
  115. package/dist/util.d.cts.map +1 -0
  116. package/dist/util.d.mts +98 -0
  117. package/dist/util.d.mts.map +1 -0
  118. package/dist/util.mjs +171 -0
  119. package/dist/util.mjs.map +1 -0
  120. package/package.json +106 -0
  121. package/theme/demo-globals.css +61 -0
  122. package/theme/voxx.css +915 -0
  123. package/voxx.schema.json +186 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 prudentbird
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @prudentbird/voxx-core
2
+
3
+ The portable content engine behind [Voxx](https://github.com/prudentbird/voxx): point it at a folder of markdown and get SEO-ready data for a blog, a docs site, or a changelog.
4
+
5
+ ```ts
6
+ import { getPosts, getPost, buildNavTree, buildSeo } from "@prudentbird/voxx-core";
7
+
8
+ const posts = await getPosts(); // reads voxx.json, renders markdown
9
+ const post = await getPost("getting-started/install");
10
+ const nav = buildNavTree(posts); // sidebar tree for docs collections
11
+ ```
12
+
13
+ ## What it does
14
+
15
+ - **Markdown in, structured data out** — frontmatter validation (Effect Schema), GFM + raw HTML rendering, Shiki highlighting, heading slugs/anchors, table of contents, excerpts, reading time. `.md` only — there's no MDX compiler, so `.mdx` is ignored rather than silently stripped.
16
+ - **Three content types** — `blog` (flat, newest-first), `docs` (folder tree → nested URLs, `01-` order prefixes, `index.md` section pages), `changelog` (versioned releases with anchor URLs).
17
+ - **SEO out of the box** — canonical/OG/Twitter/JSON-LD via `buildSeo`, plus `renderRss`, `renderSitemap`, `renderRobotsTxt`, `renderLlmsTxt`, and `renderLlmsFull`.
18
+ - **Drafts stay private** — `draft: true` hides a post from `getPosts` _and_ `getPost` until you opt in with `includeDrafts: true` (or `drafts: true` in config).
19
+ - **Configured by `voxx.json`** — validated against the shipped `voxx.schema.json`; mount one collection via `content` or several via `collections`.
20
+
21
+ ```jsonc
22
+ {
23
+ "site": { "title": "Acme", "url": "https://acme.dev" },
24
+ "collections": [
25
+ { "name": "blog" },
26
+ { "name": "docs", "type": "docs" },
27
+ { "name": "releases", "type": "changelog", "basePath": "/changelog" },
28
+ ],
29
+ }
30
+ ```
31
+
32
+ ```ts
33
+ const docs = await getPosts({ collection: "docs" });
34
+ ```
35
+
36
+ ## Entry points
37
+
38
+ - `@prudentbird/voxx-core` — plain Promise API.
39
+ - `@prudentbird/voxx-core/effect` — the raw Effect programs, schemas, and tagged errors.
40
+ - `@prudentbird/voxx-core/theme/voxx.css` — token-aware default styles (inherits shadcn design tokens when present).
41
+
42
+ Scaffolding lives in the `voxx` CLI; this package has no opinion about your framework.
@@ -0,0 +1,23 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ exports.__toESM = __toESM;
@@ -0,0 +1,151 @@
1
+ const require_schema = require("./schema.cjs");
2
+ const require_errors = require("./errors.cjs");
3
+ const require_types = require("./types.cjs");
4
+ let effect = require("effect");
5
+ let _effect_platform = require("@effect/platform");
6
+ //#region src/config.ts
7
+ const TYPE_FEATURE_DEFAULTS = {
8
+ blog: {},
9
+ docs: {
10
+ rss: false,
11
+ readingTime: false,
12
+ tags: false
13
+ },
14
+ changelog: {
15
+ toc: false,
16
+ sitemap: false,
17
+ readingTime: false,
18
+ tags: false
19
+ }
20
+ };
21
+ /**
22
+ * Fills in missing fields on a collection definition with type-appropriate defaults.
23
+ *
24
+ * @param c - Partial collection input from config.
25
+ * @returns A fully resolved `CollectionConfig`.
26
+ */
27
+ function resolveCollectionDefaults(c) {
28
+ const type = c.type ?? "blog";
29
+ const name = c.name ?? type;
30
+ return {
31
+ name,
32
+ type,
33
+ dir: c.dir ?? `content/${name}`,
34
+ basePath: c.basePath ?? `/${name}`,
35
+ drafts: c.drafts ?? false
36
+ };
37
+ }
38
+ function mergeCollections(input) {
39
+ const d = require_types.DEFAULT_CONFIG;
40
+ if (input.collections && input.collections.length > 0) return input.collections.map(resolveCollectionDefaults);
41
+ const type = input.content?.type ?? d.content.type;
42
+ return [{
43
+ name: type,
44
+ type,
45
+ dir: input.content?.dir ?? d.content.dir,
46
+ basePath: input.content?.basePath ?? d.content.basePath,
47
+ drafts: input.content?.drafts ?? d.content.drafts
48
+ }];
49
+ }
50
+ function mergeConfig(input) {
51
+ const d = require_types.DEFAULT_CONFIG;
52
+ const collections = mergeCollections(input);
53
+ const first = collections[0];
54
+ const featureDefaults = {
55
+ ...d.features,
56
+ ...TYPE_FEATURE_DEFAULTS[first.type]
57
+ };
58
+ return {
59
+ site: {
60
+ title: input.site.title,
61
+ description: input.site.description ?? d.site.description,
62
+ url: input.site.url,
63
+ author: input.site.author ? {
64
+ name: input.site.author.name,
65
+ url: input.site.author.url
66
+ } : d.site.author,
67
+ locale: input.site.locale ?? d.site.locale
68
+ },
69
+ content: {
70
+ type: first.type,
71
+ dir: first.dir,
72
+ basePath: first.basePath,
73
+ drafts: first.drafts
74
+ },
75
+ collections,
76
+ theme: {
77
+ preset: input.theme?.preset ?? d.theme.preset,
78
+ css: input.theme?.css ?? d.theme.css,
79
+ codeTheme: input.theme?.codeTheme ?? d.theme.codeTheme
80
+ },
81
+ features: {
82
+ toc: input.features?.toc ?? featureDefaults.toc,
83
+ rss: input.features?.rss ?? featureDefaults.rss,
84
+ sitemap: input.features?.sitemap ?? featureDefaults.sitemap,
85
+ llmsTxt: input.features?.llmsTxt ?? featureDefaults.llmsTxt,
86
+ tags: input.features?.tags ?? featureDefaults.tags,
87
+ readingTime: input.features?.readingTime ?? featureDefaults.readingTime
88
+ },
89
+ seo: {
90
+ openGraph: input.seo?.openGraph ?? d.seo.openGraph,
91
+ twitter: input.seo?.twitter ?? d.seo.twitter,
92
+ jsonLd: input.seo?.jsonLd ?? d.seo.jsonLd,
93
+ defaultImage: input.seo?.defaultImage ?? d.seo.defaultImage
94
+ }
95
+ };
96
+ }
97
+ /**
98
+ * Validates raw JSON data against the config schema and resolves all paths
99
+ * relative to `cwd`.
100
+ */
101
+ const resolveConfig = (data, cwd) => effect.Effect.gen(function* () {
102
+ const path = yield* _effect_platform.Path.Path;
103
+ const merged = mergeConfig(yield* effect.Schema.decodeUnknown(require_schema.ConfigInput)(data).pipe(effect.Effect.mapError((cause) => new require_errors.ConfigError({
104
+ message: `voxx.json failed validation:\n${effect.ParseResult.TreeFormatter.formatErrorSync(cause)}`,
105
+ cause
106
+ }))));
107
+ const resolveDir = (dir) => path.isAbsolute(dir) ? dir : path.join(cwd, dir);
108
+ return {
109
+ ...merged,
110
+ content: {
111
+ ...merged.content,
112
+ dir: resolveDir(merged.content.dir)
113
+ },
114
+ collections: merged.collections.map((c) => ({
115
+ ...c,
116
+ dir: resolveDir(c.dir)
117
+ }))
118
+ };
119
+ });
120
+ /**
121
+ * Reads, parses, and validates `voxx.json` from disk.
122
+ *
123
+ * @param opts - Optional `cwd` or explicit `path` to the config file.
124
+ */
125
+ const loadConfigEffect = (opts = {}) => effect.Effect.gen(function* () {
126
+ const fs = yield* _effect_platform.FileSystem.FileSystem;
127
+ const path = yield* _effect_platform.Path.Path;
128
+ const cwd = opts.cwd ?? process.cwd();
129
+ const configPath = opts.path ?? path.join(cwd, "voxx.json");
130
+ if (!(yield* fs.exists(configPath).pipe(effect.Effect.mapError((cause) => new require_errors.ConfigError({
131
+ message: `Could not access ${configPath}`,
132
+ cause
133
+ }))))) return yield* new require_errors.ConfigError({ message: `No voxx.json found at ${configPath}. Run \`voxx init\` to create one.` });
134
+ const raw = yield* fs.readFileString(configPath).pipe(effect.Effect.mapError((cause) => new require_errors.ConfigError({
135
+ message: `Could not read ${configPath}`,
136
+ cause
137
+ })));
138
+ return yield* resolveConfig(yield* effect.Effect.try({
139
+ try: () => JSON.parse(raw),
140
+ catch: (cause) => new require_errors.ConfigError({
141
+ message: `${configPath} is not valid JSON`,
142
+ cause
143
+ })
144
+ }), cwd);
145
+ });
146
+ //#endregion
147
+ exports.loadConfigEffect = loadConfigEffect;
148
+ exports.resolveCollectionDefaults = resolveCollectionDefaults;
149
+ exports.resolveConfig = resolveConfig;
150
+
151
+ //# sourceMappingURL=config.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.cjs","names":["DEFAULT_CONFIG","Effect","Path","Schema","ConfigInput","ConfigError","ParseResult","FileSystem"],"sources":["../src/config.ts"],"sourcesContent":["import { Effect, ParseResult, Schema } from \"effect\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { ConfigInput, type VoxxConfigInput } from \"./schema\";\nimport { ConfigError } from \"./errors\";\nimport {\n DEFAULT_CONFIG,\n type CollectionConfig,\n type ContentType,\n type VoxxConfig,\n} from \"./types\";\n\n/** Options for locating the `voxx.json` config file. */\nexport interface LoadConfigOptions {\n /** Working directory to resolve relative paths from. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit path to `voxx.json`. Overrides `cwd`. */\n path?: string;\n}\n\nconst TYPE_FEATURE_DEFAULTS: Record<\n ContentType,\n Partial<VoxxConfig[\"features\"]>\n> = {\n blog: {},\n docs: { rss: false, readingTime: false, tags: false },\n changelog: {\n toc: false,\n sitemap: false,\n readingTime: false,\n tags: false,\n },\n};\n\n/** Partial collection definition accepted in `voxx.json`. */\nexport interface CollectionInput {\n readonly name?: string;\n readonly type?: ContentType;\n readonly dir?: string;\n readonly basePath?: string;\n readonly drafts?: boolean;\n}\n\n/**\n * Fills in missing fields on a collection definition with type-appropriate defaults.\n *\n * @param c - Partial collection input from config.\n * @returns A fully resolved `CollectionConfig`.\n */\nexport function resolveCollectionDefaults(\n c: CollectionInput,\n): CollectionConfig {\n const type = c.type ?? \"blog\";\n const name = c.name ?? type;\n return {\n name,\n type,\n dir: c.dir ?? `content/${name}`,\n basePath: c.basePath ?? `/${name}`,\n drafts: c.drafts ?? false,\n };\n}\n\nfunction mergeCollections(input: VoxxConfigInput): CollectionConfig[] {\n const d = DEFAULT_CONFIG;\n if (input.collections && input.collections.length > 0) {\n return input.collections.map(resolveCollectionDefaults);\n }\n const type = input.content?.type ?? d.content.type;\n return [\n {\n name: type,\n type,\n dir: input.content?.dir ?? d.content.dir,\n basePath: input.content?.basePath ?? d.content.basePath,\n drafts: input.content?.drafts ?? d.content.drafts,\n },\n ];\n}\n\nfunction mergeConfig(input: VoxxConfigInput): VoxxConfig {\n const d = DEFAULT_CONFIG;\n const collections = mergeCollections(input);\n const first = collections[0]!;\n const featureDefaults = {\n ...d.features,\n ...TYPE_FEATURE_DEFAULTS[first.type],\n };\n return {\n site: {\n title: input.site.title,\n description: input.site.description ?? d.site.description,\n url: input.site.url,\n author: input.site.author\n ? { name: input.site.author.name, url: input.site.author.url }\n : d.site.author,\n locale: input.site.locale ?? d.site.locale,\n },\n content: {\n type: first.type,\n dir: first.dir,\n basePath: first.basePath,\n drafts: first.drafts,\n },\n collections,\n theme: {\n preset: input.theme?.preset ?? d.theme.preset,\n css: input.theme?.css ?? d.theme.css,\n codeTheme: input.theme?.codeTheme ?? d.theme.codeTheme,\n },\n features: {\n toc: input.features?.toc ?? featureDefaults.toc,\n rss: input.features?.rss ?? featureDefaults.rss,\n sitemap: input.features?.sitemap ?? featureDefaults.sitemap,\n llmsTxt: input.features?.llmsTxt ?? featureDefaults.llmsTxt,\n tags: input.features?.tags ?? featureDefaults.tags,\n readingTime: input.features?.readingTime ?? featureDefaults.readingTime,\n },\n seo: {\n openGraph: input.seo?.openGraph ?? d.seo.openGraph,\n twitter: input.seo?.twitter ?? d.seo.twitter,\n jsonLd: input.seo?.jsonLd ?? d.seo.jsonLd,\n defaultImage: input.seo?.defaultImage ?? d.seo.defaultImage,\n },\n };\n}\n\n/**\n * Validates raw JSON data against the config schema and resolves all paths\n * relative to `cwd`.\n */\nexport const resolveConfig = (data: unknown, cwd: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const decoded = yield* Schema.decodeUnknown(ConfigInput)(data).pipe(\n Effect.mapError(\n (cause) =>\n new ConfigError({\n message: `voxx.json failed validation:\\n${ParseResult.TreeFormatter.formatErrorSync(cause)}`,\n cause,\n }),\n ),\n );\n const merged = mergeConfig(decoded);\n const resolveDir = (dir: string) =>\n path.isAbsolute(dir) ? dir : path.join(cwd, dir);\n return {\n ...merged,\n content: { ...merged.content, dir: resolveDir(merged.content.dir) },\n collections: merged.collections.map((c) => ({\n ...c,\n dir: resolveDir(c.dir),\n })),\n } satisfies VoxxConfig;\n });\n\n/**\n * Reads, parses, and validates `voxx.json` from disk.\n *\n * @param opts - Optional `cwd` or explicit `path` to the config file.\n */\nexport const loadConfigEffect = (opts: LoadConfigOptions = {}) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const cwd = opts.cwd ?? process.cwd();\n const configPath = opts.path ?? path.join(cwd, \"voxx.json\");\n\n const exists = yield* fs.exists(configPath).pipe(\n Effect.mapError(\n (cause) =>\n new ConfigError({\n message: `Could not access ${configPath}`,\n cause,\n }),\n ),\n );\n if (!exists) {\n return yield* new ConfigError({\n message: `No voxx.json found at ${configPath}. Run \\`voxx init\\` to create one.`,\n });\n }\n\n const raw = yield* fs\n .readFileString(configPath)\n .pipe(\n Effect.mapError(\n (cause) =>\n new ConfigError({ message: `Could not read ${configPath}`, cause }),\n ),\n );\n\n const data = yield* Effect.try({\n try: () => JSON.parse(raw) as unknown,\n catch: (cause) =>\n new ConfigError({ message: `${configPath} is not valid JSON`, cause }),\n });\n\n return yield* resolveConfig(data, cwd);\n });\n"],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import { ConfigError } from "./errors.cjs";
2
+ import { CollectionConfig, ContentType, VoxxAuthor } from "./types.cjs";
3
+ import { Effect } from "effect";
4
+ import { FileSystem, Path } from "@effect/platform";
5
+
6
+ //#region src/config.d.ts
7
+ /** Options for locating the `voxx.json` config file. */
8
+ interface LoadConfigOptions {
9
+ /** Working directory to resolve relative paths from. Defaults to `process.cwd()`. */
10
+ cwd?: string;
11
+ /** Explicit path to `voxx.json`. Overrides `cwd`. */
12
+ path?: string;
13
+ }
14
+ /** Partial collection definition accepted in `voxx.json`. */
15
+ interface CollectionInput {
16
+ readonly name?: string;
17
+ readonly type?: ContentType;
18
+ readonly dir?: string;
19
+ readonly basePath?: string;
20
+ readonly drafts?: boolean;
21
+ }
22
+ /**
23
+ * Fills in missing fields on a collection definition with type-appropriate defaults.
24
+ *
25
+ * @param c - Partial collection input from config.
26
+ * @returns A fully resolved `CollectionConfig`.
27
+ */
28
+ declare function resolveCollectionDefaults(c: CollectionInput): CollectionConfig;
29
+ /**
30
+ * Validates raw JSON data against the config schema and resolves all paths
31
+ * relative to `cwd`.
32
+ */
33
+ declare const resolveConfig: (data: unknown, cwd: string) => Effect.Effect<{
34
+ content: {
35
+ dir: string;
36
+ type: ContentType;
37
+ basePath: string;
38
+ drafts: boolean;
39
+ };
40
+ collections: {
41
+ dir: string;
42
+ name: string;
43
+ type: ContentType;
44
+ basePath: string;
45
+ drafts: boolean;
46
+ }[];
47
+ site: {
48
+ title: string;
49
+ description: string;
50
+ url: string;
51
+ author?: VoxxAuthor;
52
+ locale: string;
53
+ };
54
+ theme: {
55
+ preset: "shadcn";
56
+ css: string | null;
57
+ codeTheme: string;
58
+ };
59
+ features: {
60
+ toc: boolean;
61
+ rss: boolean;
62
+ sitemap: boolean;
63
+ llmsTxt: boolean;
64
+ tags: boolean;
65
+ readingTime: boolean;
66
+ };
67
+ seo: {
68
+ openGraph: boolean;
69
+ twitter: string | null;
70
+ jsonLd: boolean;
71
+ defaultImage: string | null;
72
+ };
73
+ }, ConfigError, Path.Path>;
74
+ /**
75
+ * Reads, parses, and validates `voxx.json` from disk.
76
+ *
77
+ * @param opts - Optional `cwd` or explicit `path` to the config file.
78
+ */
79
+ declare const loadConfigEffect: (opts?: LoadConfigOptions) => Effect.Effect<{
80
+ content: {
81
+ dir: string;
82
+ type: ContentType;
83
+ basePath: string;
84
+ drafts: boolean;
85
+ };
86
+ collections: {
87
+ dir: string;
88
+ name: string;
89
+ type: ContentType;
90
+ basePath: string;
91
+ drafts: boolean;
92
+ }[];
93
+ site: {
94
+ title: string;
95
+ description: string;
96
+ url: string;
97
+ author?: VoxxAuthor;
98
+ locale: string;
99
+ };
100
+ theme: {
101
+ preset: "shadcn";
102
+ css: string | null;
103
+ codeTheme: string;
104
+ };
105
+ features: {
106
+ toc: boolean;
107
+ rss: boolean;
108
+ sitemap: boolean;
109
+ llmsTxt: boolean;
110
+ tags: boolean;
111
+ readingTime: boolean;
112
+ };
113
+ seo: {
114
+ openGraph: boolean;
115
+ twitter: string | null;
116
+ jsonLd: boolean;
117
+ defaultImage: string | null;
118
+ };
119
+ }, ConfigError, Path.Path | FileSystem.FileSystem>;
120
+ //#endregion
121
+ export { CollectionInput, LoadConfigOptions, loadConfigEffect, resolveCollectionDefaults, resolveConfig };
122
+ //# sourceMappingURL=config.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.cts","names":[],"sources":["../src/config.ts"],"mappings":";;;;;;;UAYiB,iBAAA;EAAA;EAEf,GAAA;;EAEA,IAAI;AAAA;AAkBN;AAAA,UAAiB,eAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA,GAAO,WAAW;EAAA,SAClB,GAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;;;;AAAM;AASjB;;iBAAgB,yBAAA,CACd,CAAA,EAAG,eAAA,GACF,gBAAgB;;;;;cAgFN,aAAA,GAAiB,IAAA,WAAe,GAAA,aAAW,MAAA,CAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8B3C,gBAAA,GAAoB,IAAA,GAAM,iBAAA,KAAsB,MAAA,CAAA,MAAA"}
@@ -0,0 +1,122 @@
1
+ import { ConfigError } from "./errors.mjs";
2
+ import { CollectionConfig, ContentType, VoxxAuthor } from "./types.mjs";
3
+ import { Effect } from "effect";
4
+ import { FileSystem, Path } from "@effect/platform";
5
+
6
+ //#region src/config.d.ts
7
+ /** Options for locating the `voxx.json` config file. */
8
+ interface LoadConfigOptions {
9
+ /** Working directory to resolve relative paths from. Defaults to `process.cwd()`. */
10
+ cwd?: string;
11
+ /** Explicit path to `voxx.json`. Overrides `cwd`. */
12
+ path?: string;
13
+ }
14
+ /** Partial collection definition accepted in `voxx.json`. */
15
+ interface CollectionInput {
16
+ readonly name?: string;
17
+ readonly type?: ContentType;
18
+ readonly dir?: string;
19
+ readonly basePath?: string;
20
+ readonly drafts?: boolean;
21
+ }
22
+ /**
23
+ * Fills in missing fields on a collection definition with type-appropriate defaults.
24
+ *
25
+ * @param c - Partial collection input from config.
26
+ * @returns A fully resolved `CollectionConfig`.
27
+ */
28
+ declare function resolveCollectionDefaults(c: CollectionInput): CollectionConfig;
29
+ /**
30
+ * Validates raw JSON data against the config schema and resolves all paths
31
+ * relative to `cwd`.
32
+ */
33
+ declare const resolveConfig: (data: unknown, cwd: string) => Effect.Effect<{
34
+ content: {
35
+ dir: string;
36
+ type: ContentType;
37
+ basePath: string;
38
+ drafts: boolean;
39
+ };
40
+ collections: {
41
+ dir: string;
42
+ name: string;
43
+ type: ContentType;
44
+ basePath: string;
45
+ drafts: boolean;
46
+ }[];
47
+ site: {
48
+ title: string;
49
+ description: string;
50
+ url: string;
51
+ author?: VoxxAuthor;
52
+ locale: string;
53
+ };
54
+ theme: {
55
+ preset: "shadcn";
56
+ css: string | null;
57
+ codeTheme: string;
58
+ };
59
+ features: {
60
+ toc: boolean;
61
+ rss: boolean;
62
+ sitemap: boolean;
63
+ llmsTxt: boolean;
64
+ tags: boolean;
65
+ readingTime: boolean;
66
+ };
67
+ seo: {
68
+ openGraph: boolean;
69
+ twitter: string | null;
70
+ jsonLd: boolean;
71
+ defaultImage: string | null;
72
+ };
73
+ }, ConfigError, Path.Path>;
74
+ /**
75
+ * Reads, parses, and validates `voxx.json` from disk.
76
+ *
77
+ * @param opts - Optional `cwd` or explicit `path` to the config file.
78
+ */
79
+ declare const loadConfigEffect: (opts?: LoadConfigOptions) => Effect.Effect<{
80
+ content: {
81
+ dir: string;
82
+ type: ContentType;
83
+ basePath: string;
84
+ drafts: boolean;
85
+ };
86
+ collections: {
87
+ dir: string;
88
+ name: string;
89
+ type: ContentType;
90
+ basePath: string;
91
+ drafts: boolean;
92
+ }[];
93
+ site: {
94
+ title: string;
95
+ description: string;
96
+ url: string;
97
+ author?: VoxxAuthor;
98
+ locale: string;
99
+ };
100
+ theme: {
101
+ preset: "shadcn";
102
+ css: string | null;
103
+ codeTheme: string;
104
+ };
105
+ features: {
106
+ toc: boolean;
107
+ rss: boolean;
108
+ sitemap: boolean;
109
+ llmsTxt: boolean;
110
+ tags: boolean;
111
+ readingTime: boolean;
112
+ };
113
+ seo: {
114
+ openGraph: boolean;
115
+ twitter: string | null;
116
+ jsonLd: boolean;
117
+ defaultImage: string | null;
118
+ };
119
+ }, ConfigError, Path.Path | FileSystem.FileSystem>;
120
+ //#endregion
121
+ export { CollectionInput, LoadConfigOptions, loadConfigEffect, resolveCollectionDefaults, resolveConfig };
122
+ //# sourceMappingURL=config.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";;;;;;;UAYiB,iBAAA;EAAA;EAEf,GAAA;;EAEA,IAAI;AAAA;AAkBN;AAAA,UAAiB,eAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA,GAAO,WAAW;EAAA,SAClB,GAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;;;;AAAM;AASjB;;iBAAgB,yBAAA,CACd,CAAA,EAAG,eAAA,GACF,gBAAgB;;;;;cAgFN,aAAA,GAAiB,IAAA,WAAe,GAAA,aAAW,MAAA,CAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8B3C,gBAAA,GAAoB,IAAA,GAAM,iBAAA,KAAsB,MAAA,CAAA,MAAA"}
@@ -0,0 +1,149 @@
1
+ import { ConfigInput } from "./schema.mjs";
2
+ import { ConfigError } from "./errors.mjs";
3
+ import { DEFAULT_CONFIG } from "./types.mjs";
4
+ import { Effect, ParseResult, Schema } from "effect";
5
+ import { FileSystem, Path } from "@effect/platform";
6
+ //#region src/config.ts
7
+ const TYPE_FEATURE_DEFAULTS = {
8
+ blog: {},
9
+ docs: {
10
+ rss: false,
11
+ readingTime: false,
12
+ tags: false
13
+ },
14
+ changelog: {
15
+ toc: false,
16
+ sitemap: false,
17
+ readingTime: false,
18
+ tags: false
19
+ }
20
+ };
21
+ /**
22
+ * Fills in missing fields on a collection definition with type-appropriate defaults.
23
+ *
24
+ * @param c - Partial collection input from config.
25
+ * @returns A fully resolved `CollectionConfig`.
26
+ */
27
+ function resolveCollectionDefaults(c) {
28
+ const type = c.type ?? "blog";
29
+ const name = c.name ?? type;
30
+ return {
31
+ name,
32
+ type,
33
+ dir: c.dir ?? `content/${name}`,
34
+ basePath: c.basePath ?? `/${name}`,
35
+ drafts: c.drafts ?? false
36
+ };
37
+ }
38
+ function mergeCollections(input) {
39
+ const d = DEFAULT_CONFIG;
40
+ if (input.collections && input.collections.length > 0) return input.collections.map(resolveCollectionDefaults);
41
+ const type = input.content?.type ?? d.content.type;
42
+ return [{
43
+ name: type,
44
+ type,
45
+ dir: input.content?.dir ?? d.content.dir,
46
+ basePath: input.content?.basePath ?? d.content.basePath,
47
+ drafts: input.content?.drafts ?? d.content.drafts
48
+ }];
49
+ }
50
+ function mergeConfig(input) {
51
+ const d = DEFAULT_CONFIG;
52
+ const collections = mergeCollections(input);
53
+ const first = collections[0];
54
+ const featureDefaults = {
55
+ ...d.features,
56
+ ...TYPE_FEATURE_DEFAULTS[first.type]
57
+ };
58
+ return {
59
+ site: {
60
+ title: input.site.title,
61
+ description: input.site.description ?? d.site.description,
62
+ url: input.site.url,
63
+ author: input.site.author ? {
64
+ name: input.site.author.name,
65
+ url: input.site.author.url
66
+ } : d.site.author,
67
+ locale: input.site.locale ?? d.site.locale
68
+ },
69
+ content: {
70
+ type: first.type,
71
+ dir: first.dir,
72
+ basePath: first.basePath,
73
+ drafts: first.drafts
74
+ },
75
+ collections,
76
+ theme: {
77
+ preset: input.theme?.preset ?? d.theme.preset,
78
+ css: input.theme?.css ?? d.theme.css,
79
+ codeTheme: input.theme?.codeTheme ?? d.theme.codeTheme
80
+ },
81
+ features: {
82
+ toc: input.features?.toc ?? featureDefaults.toc,
83
+ rss: input.features?.rss ?? featureDefaults.rss,
84
+ sitemap: input.features?.sitemap ?? featureDefaults.sitemap,
85
+ llmsTxt: input.features?.llmsTxt ?? featureDefaults.llmsTxt,
86
+ tags: input.features?.tags ?? featureDefaults.tags,
87
+ readingTime: input.features?.readingTime ?? featureDefaults.readingTime
88
+ },
89
+ seo: {
90
+ openGraph: input.seo?.openGraph ?? d.seo.openGraph,
91
+ twitter: input.seo?.twitter ?? d.seo.twitter,
92
+ jsonLd: input.seo?.jsonLd ?? d.seo.jsonLd,
93
+ defaultImage: input.seo?.defaultImage ?? d.seo.defaultImage
94
+ }
95
+ };
96
+ }
97
+ /**
98
+ * Validates raw JSON data against the config schema and resolves all paths
99
+ * relative to `cwd`.
100
+ */
101
+ const resolveConfig = (data, cwd) => Effect.gen(function* () {
102
+ const path = yield* Path.Path;
103
+ const merged = mergeConfig(yield* Schema.decodeUnknown(ConfigInput)(data).pipe(Effect.mapError((cause) => new ConfigError({
104
+ message: `voxx.json failed validation:\n${ParseResult.TreeFormatter.formatErrorSync(cause)}`,
105
+ cause
106
+ }))));
107
+ const resolveDir = (dir) => path.isAbsolute(dir) ? dir : path.join(cwd, dir);
108
+ return {
109
+ ...merged,
110
+ content: {
111
+ ...merged.content,
112
+ dir: resolveDir(merged.content.dir)
113
+ },
114
+ collections: merged.collections.map((c) => ({
115
+ ...c,
116
+ dir: resolveDir(c.dir)
117
+ }))
118
+ };
119
+ });
120
+ /**
121
+ * Reads, parses, and validates `voxx.json` from disk.
122
+ *
123
+ * @param opts - Optional `cwd` or explicit `path` to the config file.
124
+ */
125
+ const loadConfigEffect = (opts = {}) => Effect.gen(function* () {
126
+ const fs = yield* FileSystem.FileSystem;
127
+ const path = yield* Path.Path;
128
+ const cwd = opts.cwd ?? process.cwd();
129
+ const configPath = opts.path ?? path.join(cwd, "voxx.json");
130
+ if (!(yield* fs.exists(configPath).pipe(Effect.mapError((cause) => new ConfigError({
131
+ message: `Could not access ${configPath}`,
132
+ cause
133
+ }))))) return yield* new ConfigError({ message: `No voxx.json found at ${configPath}. Run \`voxx init\` to create one.` });
134
+ const raw = yield* fs.readFileString(configPath).pipe(Effect.mapError((cause) => new ConfigError({
135
+ message: `Could not read ${configPath}`,
136
+ cause
137
+ })));
138
+ return yield* resolveConfig(yield* Effect.try({
139
+ try: () => JSON.parse(raw),
140
+ catch: (cause) => new ConfigError({
141
+ message: `${configPath} is not valid JSON`,
142
+ cause
143
+ })
144
+ }), cwd);
145
+ });
146
+ //#endregion
147
+ export { loadConfigEffect, resolveCollectionDefaults, resolveConfig };
148
+
149
+ //# sourceMappingURL=config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { Effect, ParseResult, Schema } from \"effect\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { ConfigInput, type VoxxConfigInput } from \"./schema\";\nimport { ConfigError } from \"./errors\";\nimport {\n DEFAULT_CONFIG,\n type CollectionConfig,\n type ContentType,\n type VoxxConfig,\n} from \"./types\";\n\n/** Options for locating the `voxx.json` config file. */\nexport interface LoadConfigOptions {\n /** Working directory to resolve relative paths from. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit path to `voxx.json`. Overrides `cwd`. */\n path?: string;\n}\n\nconst TYPE_FEATURE_DEFAULTS: Record<\n ContentType,\n Partial<VoxxConfig[\"features\"]>\n> = {\n blog: {},\n docs: { rss: false, readingTime: false, tags: false },\n changelog: {\n toc: false,\n sitemap: false,\n readingTime: false,\n tags: false,\n },\n};\n\n/** Partial collection definition accepted in `voxx.json`. */\nexport interface CollectionInput {\n readonly name?: string;\n readonly type?: ContentType;\n readonly dir?: string;\n readonly basePath?: string;\n readonly drafts?: boolean;\n}\n\n/**\n * Fills in missing fields on a collection definition with type-appropriate defaults.\n *\n * @param c - Partial collection input from config.\n * @returns A fully resolved `CollectionConfig`.\n */\nexport function resolveCollectionDefaults(\n c: CollectionInput,\n): CollectionConfig {\n const type = c.type ?? \"blog\";\n const name = c.name ?? type;\n return {\n name,\n type,\n dir: c.dir ?? `content/${name}`,\n basePath: c.basePath ?? `/${name}`,\n drafts: c.drafts ?? false,\n };\n}\n\nfunction mergeCollections(input: VoxxConfigInput): CollectionConfig[] {\n const d = DEFAULT_CONFIG;\n if (input.collections && input.collections.length > 0) {\n return input.collections.map(resolveCollectionDefaults);\n }\n const type = input.content?.type ?? d.content.type;\n return [\n {\n name: type,\n type,\n dir: input.content?.dir ?? d.content.dir,\n basePath: input.content?.basePath ?? d.content.basePath,\n drafts: input.content?.drafts ?? d.content.drafts,\n },\n ];\n}\n\nfunction mergeConfig(input: VoxxConfigInput): VoxxConfig {\n const d = DEFAULT_CONFIG;\n const collections = mergeCollections(input);\n const first = collections[0]!;\n const featureDefaults = {\n ...d.features,\n ...TYPE_FEATURE_DEFAULTS[first.type],\n };\n return {\n site: {\n title: input.site.title,\n description: input.site.description ?? d.site.description,\n url: input.site.url,\n author: input.site.author\n ? { name: input.site.author.name, url: input.site.author.url }\n : d.site.author,\n locale: input.site.locale ?? d.site.locale,\n },\n content: {\n type: first.type,\n dir: first.dir,\n basePath: first.basePath,\n drafts: first.drafts,\n },\n collections,\n theme: {\n preset: input.theme?.preset ?? d.theme.preset,\n css: input.theme?.css ?? d.theme.css,\n codeTheme: input.theme?.codeTheme ?? d.theme.codeTheme,\n },\n features: {\n toc: input.features?.toc ?? featureDefaults.toc,\n rss: input.features?.rss ?? featureDefaults.rss,\n sitemap: input.features?.sitemap ?? featureDefaults.sitemap,\n llmsTxt: input.features?.llmsTxt ?? featureDefaults.llmsTxt,\n tags: input.features?.tags ?? featureDefaults.tags,\n readingTime: input.features?.readingTime ?? featureDefaults.readingTime,\n },\n seo: {\n openGraph: input.seo?.openGraph ?? d.seo.openGraph,\n twitter: input.seo?.twitter ?? d.seo.twitter,\n jsonLd: input.seo?.jsonLd ?? d.seo.jsonLd,\n defaultImage: input.seo?.defaultImage ?? d.seo.defaultImage,\n },\n };\n}\n\n/**\n * Validates raw JSON data against the config schema and resolves all paths\n * relative to `cwd`.\n */\nexport const resolveConfig = (data: unknown, cwd: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const decoded = yield* Schema.decodeUnknown(ConfigInput)(data).pipe(\n Effect.mapError(\n (cause) =>\n new ConfigError({\n message: `voxx.json failed validation:\\n${ParseResult.TreeFormatter.formatErrorSync(cause)}`,\n cause,\n }),\n ),\n );\n const merged = mergeConfig(decoded);\n const resolveDir = (dir: string) =>\n path.isAbsolute(dir) ? dir : path.join(cwd, dir);\n return {\n ...merged,\n content: { ...merged.content, dir: resolveDir(merged.content.dir) },\n collections: merged.collections.map((c) => ({\n ...c,\n dir: resolveDir(c.dir),\n })),\n } satisfies VoxxConfig;\n });\n\n/**\n * Reads, parses, and validates `voxx.json` from disk.\n *\n * @param opts - Optional `cwd` or explicit `path` to the config file.\n */\nexport const loadConfigEffect = (opts: LoadConfigOptions = {}) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const cwd = opts.cwd ?? process.cwd();\n const configPath = opts.path ?? path.join(cwd, \"voxx.json\");\n\n const exists = yield* fs.exists(configPath).pipe(\n Effect.mapError(\n (cause) =>\n new ConfigError({\n message: `Could not access ${configPath}`,\n cause,\n }),\n ),\n );\n if (!exists) {\n return yield* new ConfigError({\n message: `No voxx.json found at ${configPath}. Run \\`voxx init\\` to create one.`,\n });\n }\n\n const raw = yield* fs\n .readFileString(configPath)\n .pipe(\n Effect.mapError(\n (cause) =>\n new ConfigError({ message: `Could not read ${configPath}`, cause }),\n ),\n );\n\n const data = yield* Effect.try({\n try: () => JSON.parse(raw) as unknown,\n catch: (cause) =>\n new ConfigError({ message: `${configPath} is not valid JSON`, cause }),\n });\n\n return yield* resolveConfig(data, cwd);\n });\n"],"mappings":"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"}