@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
@@ -0,0 +1,147 @@
1
+ const require_errors = require("./errors.cjs");
2
+ const require_frontmatter = require("./frontmatter.cjs");
3
+ const require_render = require("./render.cjs");
4
+ const require_util = require("./util.cjs");
5
+ let effect = require("effect");
6
+ let _effect_platform = require("@effect/platform");
7
+ //#region src/content.ts
8
+ const MD_RE = /\.md$/;
9
+ const orderKey = (order, slug) => `${String(order ?? 9999).padStart(4, "0")} ${slug}`;
10
+ const buildPost = (config, absPath, rel) => effect.Effect.gen(function* () {
11
+ const fs = yield* _effect_platform.FileSystem.FileSystem;
12
+ const { data, content } = yield* require_frontmatter.parseFrontmatter(rel, yield* fs.readFileString(absPath));
13
+ const isDocs = config.content.type === "docs";
14
+ const isChangelog = config.content.type === "changelog";
15
+ const segments = rel.replace(MD_RE, "").split(/[\\/]/).filter(Boolean);
16
+ const fileBase = segments.pop() ?? rel;
17
+ const dirOrders = [];
18
+ const dirSlugs = segments.map((seg) => {
19
+ const { order, rest } = isDocs ? require_util.splitOrderPrefix(seg) : {
20
+ order: void 0,
21
+ rest: seg
22
+ };
23
+ dirOrders.push(order);
24
+ return require_util.slugify(rest);
25
+ });
26
+ const { date: filenameDate, rest: dateRest } = require_util.splitDatePrefix(fileBase);
27
+ const { order: fileOrder, rest } = isDocs ? require_util.splitOrderPrefix(dateRest) : {
28
+ order: void 0,
29
+ rest: dateRest
30
+ };
31
+ const baseSlug = data.slug ? require_util.slugify(data.slug) : require_util.slugify(rest);
32
+ const isIndex = isDocs && !data.slug && baseSlug === "index";
33
+ const path = isDocs ? isIndex ? dirSlugs : [...dirSlugs, baseSlug] : [baseSlug];
34
+ const slug = path[path.length - 1] ?? "";
35
+ const order = data.order ?? fileOrder ?? (isIndex ? dirOrders[dirOrders.length - 1] : void 0);
36
+ const version = isChangelog ? data.version ?? require_util.parseVersion(dateRest) : data.version;
37
+ const baseUrl = require_util.joinPath(config.content.basePath, "/").replace(/(.)\/+$/, "$1");
38
+ const urlPath = path.join("/");
39
+ const url = isChangelog ? `${baseUrl}#${slug}` : urlPath ? require_util.joinPath(config.content.basePath, urlPath) : baseUrl;
40
+ const levelKeys = dirSlugs.map((s, i) => orderKey(dirOrders[i], s));
41
+ if (isIndex) {
42
+ if (levelKeys.length > 0) levelKeys[levelKeys.length - 1] = orderKey(order, slug);
43
+ } else levelKeys.push(orderKey(order, slug));
44
+ const sortKey = levelKeys.join("/");
45
+ const dirRel = rel.split(/[\\/]/).slice(0, -1).join("/");
46
+ const { html, toc } = yield* require_render.renderMarkdownEffect(content, config, { assetBase: dirRel ? require_util.joinPath(config.content.basePath, dirRel) : config.content.basePath });
47
+ let date = data.date ?? filenameDate;
48
+ if (!date) {
49
+ if (!isDocs) yield* effect.Effect.logWarning(`${rel}: no date in frontmatter or filename — falling back to the file's creation time, which is not stable across checkouts.`);
50
+ const info = yield* fs.stat(absPath);
51
+ date = (effect.Option.getOrUndefined(info.birthtime) ?? effect.Option.getOrUndefined(info.mtime) ?? /* @__PURE__ */ new Date()).toISOString();
52
+ }
53
+ return {
54
+ post: {
55
+ slug,
56
+ path,
57
+ url,
58
+ title: data.title,
59
+ description: data.description,
60
+ date,
61
+ updated: data.updated,
62
+ tags: [...data.tags],
63
+ category: data.category,
64
+ order,
65
+ version,
66
+ draft: data.draft,
67
+ image: data.image,
68
+ author: data.author,
69
+ excerpt: data.excerpt ?? require_util.deriveExcerpt(content),
70
+ readingTimeMinutes: require_util.readingTimeMinutes(content),
71
+ html,
72
+ toc,
73
+ content
74
+ },
75
+ sortKey
76
+ };
77
+ });
78
+ /**
79
+ * Reads all Markdown files from the configured content directory,
80
+ * renders them, and returns sorted posts.
81
+ *
82
+ * - **docs** — sorted by numeric directory/file order prefix.
83
+ * - **changelog** — sorted by date descending, then semver descending.
84
+ * - **blog** — sorted by date descending.
85
+ *
86
+ * @param config - Resolved Voxx config.
87
+ * @param opts - Optional collection filter and draft visibility.
88
+ */
89
+ const getPostsEffect = (config, opts = {}) => effect.Effect.gen(function* () {
90
+ if (opts.collection) {
91
+ const active = config.collections?.find((c) => c.name === opts.collection);
92
+ if (!active) return yield* new require_errors.ConfigError({ message: `Unknown collection "${opts.collection}" — defined: ${(config.collections ?? []).map((c) => c.name).join(", ")}` });
93
+ config = {
94
+ ...config,
95
+ content: active
96
+ };
97
+ }
98
+ const fs = yield* _effect_platform.FileSystem.FileSystem;
99
+ const path = yield* _effect_platform.Path.Path;
100
+ const dir = config.content.dir;
101
+ if (!(yield* fs.exists(dir).pipe(effect.Effect.orElseSucceed(() => false)))) return yield* new require_errors.ContentDirMissing({ dir });
102
+ const files = (yield* fs.readDirectory(dir, { recursive: true })).filter((f) => MD_RE.test(f));
103
+ const includeDrafts = opts.includeDrafts ?? config.content.drafts;
104
+ const built = yield* effect.Effect.forEach(files, (rel) => buildPost(config, path.join(dir, rel), rel), { concurrency: 8 });
105
+ const seen = /* @__PURE__ */ new Map();
106
+ for (let i = 0; i < built.length; i++) {
107
+ const key = built[i].post.path.join("/");
108
+ const previous = seen.get(key);
109
+ if (previous !== void 0) yield* effect.Effect.logWarning(`Duplicate slug "${key}": ${files[i]} collides with ${previous} — only one will be reachable.`);
110
+ else seen.set(key, files[i]);
111
+ }
112
+ const visible = built.filter((b) => includeDrafts || !b.post.draft);
113
+ if (config.content.type === "docs") return visible.sort((a, b) => a.sortKey.localeCompare(b.sortKey)).map((b) => b.post);
114
+ const isChangelog = config.content.type === "changelog";
115
+ return visible.map((b) => b.post).sort((a, b) => {
116
+ const byDate = b.date.localeCompare(a.date);
117
+ if (byDate !== 0) return byDate;
118
+ if (isChangelog && a.version && b.version) return require_util.compareVersions(b.version, a.version);
119
+ return a.slug.localeCompare(b.slug);
120
+ });
121
+ });
122
+ /**
123
+ * Finds a post in an already-loaded array by slug or path.
124
+ *
125
+ * @param posts - Array returned by `getPosts`.
126
+ * @param slug - Slash-separated path, e.g. `"getting-started/install"`.
127
+ * @returns The matching post, or `undefined` if not found.
128
+ */
129
+ function findPost(posts, slug) {
130
+ const wanted = slug.split("/").filter(Boolean).map(require_util.slugify).join("/");
131
+ return posts.find((p) => p.path.join("/") === wanted || p.path.length <= 1 && p.slug === wanted);
132
+ }
133
+ /**
134
+ * Loads all posts and returns the one matching `slug`.
135
+ * Throws `PostNotFound` if no match exists.
136
+ */
137
+ const getPostEffect = (config, slug, opts = {}) => effect.Effect.gen(function* () {
138
+ const post = findPost(yield* getPostsEffect(config, opts), slug);
139
+ if (!post) return yield* new require_errors.PostNotFound({ slug });
140
+ return post;
141
+ });
142
+ //#endregion
143
+ exports.findPost = findPost;
144
+ exports.getPostEffect = getPostEffect;
145
+ exports.getPostsEffect = getPostsEffect;
146
+
147
+ //# sourceMappingURL=content.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.cjs","names":["Effect","FileSystem","parseFrontmatter","splitOrderPrefix","slugify","splitDatePrefix","parseVersion","joinPath","renderMarkdownEffect","Option","deriveExcerpt","readingTimeMinutes","ConfigError","Path","ContentDirMissing","compareVersions","PostNotFound"],"sources":["../src/content.ts"],"sourcesContent":["import { Effect, Option } from \"effect\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { parseFrontmatter } from \"./frontmatter\";\nimport { renderMarkdownEffect } from \"./render\";\nimport { ConfigError, ContentDirMissing, PostNotFound } from \"./errors\";\nimport {\n compareVersions,\n deriveExcerpt,\n joinPath,\n parseVersion,\n readingTimeMinutes,\n slugify,\n splitDatePrefix,\n splitOrderPrefix,\n} from \"./util\";\nimport type { Post, VoxxConfig } from \"./types\";\n\nconst MD_RE = /\\.md$/;\n\n/** Options for filtering posts returned by `getPostsEffect`. */\nexport interface GetPostsEffectOptions {\n /** When `true`, includes posts whose frontmatter sets `draft: true`. */\n includeDrafts?: boolean;\n /** Restricts results to a named collection defined in `config.collections`. */\n collection?: string;\n}\nconst orderKey = (order: number | undefined, slug: string) =>\n `${String(order ?? 9999).padStart(4, \"0\")} ${slug}`;\n\ninterface BuiltPost {\n post: Post;\n sortKey: string;\n}\n\nconst buildPost = (config: VoxxConfig, absPath: string, rel: string) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const raw = yield* fs.readFileString(absPath);\n const { data, content } = yield* parseFrontmatter(rel, raw);\n const isDocs = config.content.type === \"docs\";\n const isChangelog = config.content.type === \"changelog\";\n\n const segments = rel.replace(MD_RE, \"\").split(/[\\\\/]/).filter(Boolean);\n const fileBase = segments.pop() ?? rel;\n\n const dirOrders: Array<number | undefined> = [];\n const dirSlugs = segments.map((seg) => {\n const { order, rest } = isDocs\n ? splitOrderPrefix(seg)\n : { order: undefined, rest: seg };\n dirOrders.push(order);\n return slugify(rest);\n });\n\n const { date: filenameDate, rest: dateRest } = splitDatePrefix(fileBase);\n const { order: fileOrder, rest } = isDocs\n ? splitOrderPrefix(dateRest)\n : { order: undefined, rest: dateRest };\n const baseSlug = data.slug ? slugify(data.slug) : slugify(rest);\n\n const isIndex = isDocs && !data.slug && baseSlug === \"index\";\n const path = isDocs\n ? isIndex\n ? dirSlugs\n : [...dirSlugs, baseSlug]\n : [baseSlug];\n const slug = path[path.length - 1] ?? \"\";\n const order =\n data.order ??\n fileOrder ??\n (isIndex ? dirOrders[dirOrders.length - 1] : undefined);\n\n const version = isChangelog\n ? (data.version ?? parseVersion(dateRest))\n : data.version;\n\n const baseUrl = joinPath(config.content.basePath, \"/\").replace(\n /(.)\\/+$/,\n \"$1\",\n );\n const urlPath = path.join(\"/\");\n const url = isChangelog\n ? `${baseUrl}#${slug}`\n : urlPath\n ? joinPath(config.content.basePath, urlPath)\n : baseUrl;\n\n const levelKeys = dirSlugs.map((s, i) => orderKey(dirOrders[i], s));\n if (isIndex) {\n if (levelKeys.length > 0)\n levelKeys[levelKeys.length - 1] = orderKey(order, slug);\n } else {\n levelKeys.push(orderKey(order, slug));\n }\n const sortKey = levelKeys.join(\"/\");\n\n const dirRel = rel.split(/[\\\\/]/).slice(0, -1).join(\"/\");\n const assetBase = dirRel\n ? joinPath(config.content.basePath, dirRel)\n : config.content.basePath;\n const { html, toc } = yield* renderMarkdownEffect(content, config, {\n assetBase,\n });\n\n let date = data.date ?? filenameDate;\n if (!date) {\n if (!isDocs) {\n yield* Effect.logWarning(\n `${rel}: no date in frontmatter or filename — falling back to the file's creation time, which is not stable across checkouts.`,\n );\n }\n const info = yield* fs.stat(absPath);\n const created =\n Option.getOrUndefined(info.birthtime) ??\n Option.getOrUndefined(info.mtime);\n date = (created ?? new Date()).toISOString();\n }\n\n const post: Post = {\n slug,\n path,\n url,\n title: data.title,\n description: data.description,\n date,\n updated: data.updated,\n tags: [...data.tags],\n category: data.category,\n order,\n version,\n draft: data.draft,\n image: data.image,\n author: data.author,\n excerpt: data.excerpt ?? deriveExcerpt(content),\n readingTimeMinutes: readingTimeMinutes(content),\n html,\n toc,\n content,\n };\n return { post, sortKey } satisfies BuiltPost;\n });\n\n/**\n * Reads all Markdown files from the configured content directory,\n * renders them, and returns sorted posts.\n *\n * - **docs** — sorted by numeric directory/file order prefix.\n * - **changelog** — sorted by date descending, then semver descending.\n * - **blog** — sorted by date descending.\n *\n * @param config - Resolved Voxx config.\n * @param opts - Optional collection filter and draft visibility.\n */\nexport const getPostsEffect = (\n config: VoxxConfig,\n opts: GetPostsEffectOptions = {},\n) =>\n Effect.gen(function* () {\n if (opts.collection) {\n const active = config.collections?.find(\n (c) => c.name === opts.collection,\n );\n if (!active) {\n return yield* new ConfigError({\n message: `Unknown collection \"${opts.collection}\" — defined: ${(\n config.collections ?? []\n )\n .map((c) => c.name)\n .join(\", \")}`,\n });\n }\n config = { ...config, content: active };\n }\n\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const dir = config.content.dir;\n\n const exists = yield* fs\n .exists(dir)\n .pipe(Effect.orElseSucceed(() => false));\n if (!exists) return yield* new ContentDirMissing({ dir });\n\n const entries = yield* fs.readDirectory(dir, { recursive: true });\n const files = entries.filter((f) => MD_RE.test(f));\n const includeDrafts = opts.includeDrafts ?? config.content.drafts;\n\n const built = yield* Effect.forEach(\n files,\n (rel) => buildPost(config, path.join(dir, rel), rel),\n { concurrency: 8 },\n );\n\n const seen = new Map<string, string>();\n for (let i = 0; i < built.length; i++) {\n const key = built[i]!.post.path.join(\"/\");\n const previous = seen.get(key);\n if (previous !== undefined) {\n yield* Effect.logWarning(\n `Duplicate slug \"${key}\": ${files[i]} collides with ${previous} — only one will be reachable.`,\n );\n } else {\n seen.set(key, files[i]!);\n }\n }\n\n const visible = built.filter((b) => includeDrafts || !b.post.draft);\n\n if (config.content.type === \"docs\") {\n return visible\n .sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n .map((b) => b.post);\n }\n\n const isChangelog = config.content.type === \"changelog\";\n\n return visible\n .map((b) => b.post)\n .sort((a, b) => {\n const byDate = b.date.localeCompare(a.date);\n if (byDate !== 0) return byDate;\n if (isChangelog && a.version && b.version) {\n return compareVersions(b.version, a.version);\n }\n return a.slug.localeCompare(b.slug);\n });\n });\n\n/**\n * Finds a post in an already-loaded array by slug or path.\n *\n * @param posts - Array returned by `getPosts`.\n * @param slug - Slash-separated path, e.g. `\"getting-started/install\"`.\n * @returns The matching post, or `undefined` if not found.\n */\nexport function findPost(posts: Post[], slug: string): Post | undefined {\n const wanted = slug.split(\"/\").filter(Boolean).map(slugify).join(\"/\");\n return posts.find(\n (p) =>\n p.path.join(\"/\") === wanted || (p.path.length <= 1 && p.slug === wanted),\n );\n}\n\n/**\n * Loads all posts and returns the one matching `slug`.\n * Throws `PostNotFound` if no match exists.\n */\nexport const getPostEffect = (\n config: VoxxConfig,\n slug: string,\n opts: GetPostsEffectOptions = {},\n) =>\n Effect.gen(function* () {\n const posts = yield* getPostsEffect(config, opts);\n const post = findPost(posts, slug);\n if (!post) return yield* new PostNotFound({ slug });\n return post;\n });\n"],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { ConfigError, ContentDirMissing, InvalidFrontmatter, PostNotFound, RenderError } from "./errors.cjs";
2
+ import { Post, VoxxConfig } from "./types.cjs";
3
+ import { Effect } from "effect";
4
+ import { FileSystem, Path } from "@effect/platform";
5
+
6
+ //#region src/content.d.ts
7
+ /** Options for filtering posts returned by `getPostsEffect`. */
8
+ interface GetPostsEffectOptions {
9
+ /** When `true`, includes posts whose frontmatter sets `draft: true`. */
10
+ includeDrafts?: boolean;
11
+ /** Restricts results to a named collection defined in `config.collections`. */
12
+ collection?: string;
13
+ }
14
+ /**
15
+ * Reads all Markdown files from the configured content directory,
16
+ * renders them, and returns sorted posts.
17
+ *
18
+ * - **docs** — sorted by numeric directory/file order prefix.
19
+ * - **changelog** — sorted by date descending, then semver descending.
20
+ * - **blog** — sorted by date descending.
21
+ *
22
+ * @param config - Resolved Voxx config.
23
+ * @param opts - Optional collection filter and draft visibility.
24
+ */
25
+ declare const getPostsEffect: (config: VoxxConfig, opts?: GetPostsEffectOptions) => Effect.Effect<Post[], ConfigError | InvalidFrontmatter | ContentDirMissing | RenderError | import("@effect/platform/Error").PlatformError, Path.Path | FileSystem.FileSystem>;
26
+ /**
27
+ * Finds a post in an already-loaded array by slug or path.
28
+ *
29
+ * @param posts - Array returned by `getPosts`.
30
+ * @param slug - Slash-separated path, e.g. `"getting-started/install"`.
31
+ * @returns The matching post, or `undefined` if not found.
32
+ */
33
+ declare function findPost(posts: Post[], slug: string): Post | undefined;
34
+ /**
35
+ * Loads all posts and returns the one matching `slug`.
36
+ * Throws `PostNotFound` if no match exists.
37
+ */
38
+ declare const getPostEffect: (config: VoxxConfig, slug: string, opts?: GetPostsEffectOptions) => Effect.Effect<Post, ConfigError | InvalidFrontmatter | PostNotFound | ContentDirMissing | RenderError | import("@effect/platform/Error").PlatformError, Path.Path | FileSystem.FileSystem>;
39
+ //#endregion
40
+ export { GetPostsEffectOptions, findPost, getPostEffect, getPostsEffect };
41
+ //# sourceMappingURL=content.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.cts","names":[],"sources":["../src/content.ts"],"mappings":";;;;;;;UAoBiB,qBAAA;EAAA;EAEf,aAAA;;EAEA,UAAU;AAAA;AAiIZ;;;;;;;;;;;AAAA,cAAa,cAAA,GACX,MAAA,EAAQ,UAAA,EACR,IAAA,GAAM,qBAAA,KAA0B,MAAA,CAAA,MAAA,CAAA,IAAA,IAAA,WAAA,GAAA,kBAAA,GAAA,iBAAA,GAAA,WAAA,oCAAA,aAAA,EAAA,IAAA,CAAA,IAAA,GAAA,UAAA,CAAA,UAAA;;;;;;;;iBAgFlB,QAAA,CAAS,KAAA,EAAO,IAAA,IAAQ,IAAA,WAAe,IAAI;;;;;cAY9C,aAAA,GACX,MAAA,EAAQ,UAAA,EACR,IAAA,UACA,IAAA,GAAM,qBAAA,KAA0B,MAAA,CAAA,MAAA,CAAA,IAAA,EAAA,WAAA,GAAA,kBAAA,GAAA,YAAA,GAAA,iBAAA,GAAA,WAAA,oCAAA,aAAA,EAAA,IAAA,CAAA,IAAA,GAAA,UAAA,CAAA,UAAA"}
@@ -0,0 +1,41 @@
1
+ import { ConfigError, ContentDirMissing, InvalidFrontmatter, PostNotFound, RenderError } from "./errors.mjs";
2
+ import { Post, VoxxConfig } from "./types.mjs";
3
+ import { Effect } from "effect";
4
+ import { FileSystem, Path } from "@effect/platform";
5
+
6
+ //#region src/content.d.ts
7
+ /** Options for filtering posts returned by `getPostsEffect`. */
8
+ interface GetPostsEffectOptions {
9
+ /** When `true`, includes posts whose frontmatter sets `draft: true`. */
10
+ includeDrafts?: boolean;
11
+ /** Restricts results to a named collection defined in `config.collections`. */
12
+ collection?: string;
13
+ }
14
+ /**
15
+ * Reads all Markdown files from the configured content directory,
16
+ * renders them, and returns sorted posts.
17
+ *
18
+ * - **docs** — sorted by numeric directory/file order prefix.
19
+ * - **changelog** — sorted by date descending, then semver descending.
20
+ * - **blog** — sorted by date descending.
21
+ *
22
+ * @param config - Resolved Voxx config.
23
+ * @param opts - Optional collection filter and draft visibility.
24
+ */
25
+ declare const getPostsEffect: (config: VoxxConfig, opts?: GetPostsEffectOptions) => Effect.Effect<Post[], ConfigError | InvalidFrontmatter | ContentDirMissing | RenderError | import("@effect/platform/Error").PlatformError, Path.Path | FileSystem.FileSystem>;
26
+ /**
27
+ * Finds a post in an already-loaded array by slug or path.
28
+ *
29
+ * @param posts - Array returned by `getPosts`.
30
+ * @param slug - Slash-separated path, e.g. `"getting-started/install"`.
31
+ * @returns The matching post, or `undefined` if not found.
32
+ */
33
+ declare function findPost(posts: Post[], slug: string): Post | undefined;
34
+ /**
35
+ * Loads all posts and returns the one matching `slug`.
36
+ * Throws `PostNotFound` if no match exists.
37
+ */
38
+ declare const getPostEffect: (config: VoxxConfig, slug: string, opts?: GetPostsEffectOptions) => Effect.Effect<Post, ConfigError | InvalidFrontmatter | PostNotFound | ContentDirMissing | RenderError | import("@effect/platform/Error").PlatformError, Path.Path | FileSystem.FileSystem>;
39
+ //#endregion
40
+ export { GetPostsEffectOptions, findPost, getPostEffect, getPostsEffect };
41
+ //# sourceMappingURL=content.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.mts","names":[],"sources":["../src/content.ts"],"mappings":";;;;;;;UAoBiB,qBAAA;EAAA;EAEf,aAAA;;EAEA,UAAU;AAAA;AAiIZ;;;;;;;;;;;AAAA,cAAa,cAAA,GACX,MAAA,EAAQ,UAAA,EACR,IAAA,GAAM,qBAAA,KAA0B,MAAA,CAAA,MAAA,CAAA,IAAA,IAAA,WAAA,GAAA,kBAAA,GAAA,iBAAA,GAAA,WAAA,oCAAA,aAAA,EAAA,IAAA,CAAA,IAAA,GAAA,UAAA,CAAA,UAAA;;;;;;;;iBAgFlB,QAAA,CAAS,KAAA,EAAO,IAAA,IAAQ,IAAA,WAAe,IAAI;;;;;cAY9C,aAAA,GACX,MAAA,EAAQ,UAAA,EACR,IAAA,UACA,IAAA,GAAM,qBAAA,KAA0B,MAAA,CAAA,MAAA,CAAA,IAAA,EAAA,WAAA,GAAA,kBAAA,GAAA,YAAA,GAAA,iBAAA,GAAA,WAAA,oCAAA,aAAA,EAAA,IAAA,CAAA,IAAA,GAAA,UAAA,CAAA,UAAA"}
@@ -0,0 +1,145 @@
1
+ import { ConfigError, ContentDirMissing, PostNotFound } from "./errors.mjs";
2
+ import { parseFrontmatter } from "./frontmatter.mjs";
3
+ import { renderMarkdownEffect } from "./render.mjs";
4
+ import { compareVersions, deriveExcerpt, joinPath, parseVersion, readingTimeMinutes, slugify, splitDatePrefix, splitOrderPrefix } from "./util.mjs";
5
+ import { Effect, Option } from "effect";
6
+ import { FileSystem, Path } from "@effect/platform";
7
+ //#region src/content.ts
8
+ const MD_RE = /\.md$/;
9
+ const orderKey = (order, slug) => `${String(order ?? 9999).padStart(4, "0")} ${slug}`;
10
+ const buildPost = (config, absPath, rel) => Effect.gen(function* () {
11
+ const fs = yield* FileSystem.FileSystem;
12
+ const { data, content } = yield* parseFrontmatter(rel, yield* fs.readFileString(absPath));
13
+ const isDocs = config.content.type === "docs";
14
+ const isChangelog = config.content.type === "changelog";
15
+ const segments = rel.replace(MD_RE, "").split(/[\\/]/).filter(Boolean);
16
+ const fileBase = segments.pop() ?? rel;
17
+ const dirOrders = [];
18
+ const dirSlugs = segments.map((seg) => {
19
+ const { order, rest } = isDocs ? splitOrderPrefix(seg) : {
20
+ order: void 0,
21
+ rest: seg
22
+ };
23
+ dirOrders.push(order);
24
+ return slugify(rest);
25
+ });
26
+ const { date: filenameDate, rest: dateRest } = splitDatePrefix(fileBase);
27
+ const { order: fileOrder, rest } = isDocs ? splitOrderPrefix(dateRest) : {
28
+ order: void 0,
29
+ rest: dateRest
30
+ };
31
+ const baseSlug = data.slug ? slugify(data.slug) : slugify(rest);
32
+ const isIndex = isDocs && !data.slug && baseSlug === "index";
33
+ const path = isDocs ? isIndex ? dirSlugs : [...dirSlugs, baseSlug] : [baseSlug];
34
+ const slug = path[path.length - 1] ?? "";
35
+ const order = data.order ?? fileOrder ?? (isIndex ? dirOrders[dirOrders.length - 1] : void 0);
36
+ const version = isChangelog ? data.version ?? parseVersion(dateRest) : data.version;
37
+ const baseUrl = joinPath(config.content.basePath, "/").replace(/(.)\/+$/, "$1");
38
+ const urlPath = path.join("/");
39
+ const url = isChangelog ? `${baseUrl}#${slug}` : urlPath ? joinPath(config.content.basePath, urlPath) : baseUrl;
40
+ const levelKeys = dirSlugs.map((s, i) => orderKey(dirOrders[i], s));
41
+ if (isIndex) {
42
+ if (levelKeys.length > 0) levelKeys[levelKeys.length - 1] = orderKey(order, slug);
43
+ } else levelKeys.push(orderKey(order, slug));
44
+ const sortKey = levelKeys.join("/");
45
+ const dirRel = rel.split(/[\\/]/).slice(0, -1).join("/");
46
+ const { html, toc } = yield* renderMarkdownEffect(content, config, { assetBase: dirRel ? joinPath(config.content.basePath, dirRel) : config.content.basePath });
47
+ let date = data.date ?? filenameDate;
48
+ if (!date) {
49
+ if (!isDocs) yield* Effect.logWarning(`${rel}: no date in frontmatter or filename — falling back to the file's creation time, which is not stable across checkouts.`);
50
+ const info = yield* fs.stat(absPath);
51
+ date = (Option.getOrUndefined(info.birthtime) ?? Option.getOrUndefined(info.mtime) ?? /* @__PURE__ */ new Date()).toISOString();
52
+ }
53
+ return {
54
+ post: {
55
+ slug,
56
+ path,
57
+ url,
58
+ title: data.title,
59
+ description: data.description,
60
+ date,
61
+ updated: data.updated,
62
+ tags: [...data.tags],
63
+ category: data.category,
64
+ order,
65
+ version,
66
+ draft: data.draft,
67
+ image: data.image,
68
+ author: data.author,
69
+ excerpt: data.excerpt ?? deriveExcerpt(content),
70
+ readingTimeMinutes: readingTimeMinutes(content),
71
+ html,
72
+ toc,
73
+ content
74
+ },
75
+ sortKey
76
+ };
77
+ });
78
+ /**
79
+ * Reads all Markdown files from the configured content directory,
80
+ * renders them, and returns sorted posts.
81
+ *
82
+ * - **docs** — sorted by numeric directory/file order prefix.
83
+ * - **changelog** — sorted by date descending, then semver descending.
84
+ * - **blog** — sorted by date descending.
85
+ *
86
+ * @param config - Resolved Voxx config.
87
+ * @param opts - Optional collection filter and draft visibility.
88
+ */
89
+ const getPostsEffect = (config, opts = {}) => Effect.gen(function* () {
90
+ if (opts.collection) {
91
+ const active = config.collections?.find((c) => c.name === opts.collection);
92
+ if (!active) return yield* new ConfigError({ message: `Unknown collection "${opts.collection}" — defined: ${(config.collections ?? []).map((c) => c.name).join(", ")}` });
93
+ config = {
94
+ ...config,
95
+ content: active
96
+ };
97
+ }
98
+ const fs = yield* FileSystem.FileSystem;
99
+ const path = yield* Path.Path;
100
+ const dir = config.content.dir;
101
+ if (!(yield* fs.exists(dir).pipe(Effect.orElseSucceed(() => false)))) return yield* new ContentDirMissing({ dir });
102
+ const files = (yield* fs.readDirectory(dir, { recursive: true })).filter((f) => MD_RE.test(f));
103
+ const includeDrafts = opts.includeDrafts ?? config.content.drafts;
104
+ const built = yield* Effect.forEach(files, (rel) => buildPost(config, path.join(dir, rel), rel), { concurrency: 8 });
105
+ const seen = /* @__PURE__ */ new Map();
106
+ for (let i = 0; i < built.length; i++) {
107
+ const key = built[i].post.path.join("/");
108
+ const previous = seen.get(key);
109
+ if (previous !== void 0) yield* Effect.logWarning(`Duplicate slug "${key}": ${files[i]} collides with ${previous} — only one will be reachable.`);
110
+ else seen.set(key, files[i]);
111
+ }
112
+ const visible = built.filter((b) => includeDrafts || !b.post.draft);
113
+ if (config.content.type === "docs") return visible.sort((a, b) => a.sortKey.localeCompare(b.sortKey)).map((b) => b.post);
114
+ const isChangelog = config.content.type === "changelog";
115
+ return visible.map((b) => b.post).sort((a, b) => {
116
+ const byDate = b.date.localeCompare(a.date);
117
+ if (byDate !== 0) return byDate;
118
+ if (isChangelog && a.version && b.version) return compareVersions(b.version, a.version);
119
+ return a.slug.localeCompare(b.slug);
120
+ });
121
+ });
122
+ /**
123
+ * Finds a post in an already-loaded array by slug or path.
124
+ *
125
+ * @param posts - Array returned by `getPosts`.
126
+ * @param slug - Slash-separated path, e.g. `"getting-started/install"`.
127
+ * @returns The matching post, or `undefined` if not found.
128
+ */
129
+ function findPost(posts, slug) {
130
+ const wanted = slug.split("/").filter(Boolean).map(slugify).join("/");
131
+ return posts.find((p) => p.path.join("/") === wanted || p.path.length <= 1 && p.slug === wanted);
132
+ }
133
+ /**
134
+ * Loads all posts and returns the one matching `slug`.
135
+ * Throws `PostNotFound` if no match exists.
136
+ */
137
+ const getPostEffect = (config, slug, opts = {}) => Effect.gen(function* () {
138
+ const post = findPost(yield* getPostsEffect(config, opts), slug);
139
+ if (!post) return yield* new PostNotFound({ slug });
140
+ return post;
141
+ });
142
+ //#endregion
143
+ export { findPost, getPostEffect, getPostsEffect };
144
+
145
+ //# sourceMappingURL=content.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.mjs","names":[],"sources":["../src/content.ts"],"sourcesContent":["import { Effect, Option } from \"effect\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { parseFrontmatter } from \"./frontmatter\";\nimport { renderMarkdownEffect } from \"./render\";\nimport { ConfigError, ContentDirMissing, PostNotFound } from \"./errors\";\nimport {\n compareVersions,\n deriveExcerpt,\n joinPath,\n parseVersion,\n readingTimeMinutes,\n slugify,\n splitDatePrefix,\n splitOrderPrefix,\n} from \"./util\";\nimport type { Post, VoxxConfig } from \"./types\";\n\nconst MD_RE = /\\.md$/;\n\n/** Options for filtering posts returned by `getPostsEffect`. */\nexport interface GetPostsEffectOptions {\n /** When `true`, includes posts whose frontmatter sets `draft: true`. */\n includeDrafts?: boolean;\n /** Restricts results to a named collection defined in `config.collections`. */\n collection?: string;\n}\nconst orderKey = (order: number | undefined, slug: string) =>\n `${String(order ?? 9999).padStart(4, \"0\")} ${slug}`;\n\ninterface BuiltPost {\n post: Post;\n sortKey: string;\n}\n\nconst buildPost = (config: VoxxConfig, absPath: string, rel: string) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const raw = yield* fs.readFileString(absPath);\n const { data, content } = yield* parseFrontmatter(rel, raw);\n const isDocs = config.content.type === \"docs\";\n const isChangelog = config.content.type === \"changelog\";\n\n const segments = rel.replace(MD_RE, \"\").split(/[\\\\/]/).filter(Boolean);\n const fileBase = segments.pop() ?? rel;\n\n const dirOrders: Array<number | undefined> = [];\n const dirSlugs = segments.map((seg) => {\n const { order, rest } = isDocs\n ? splitOrderPrefix(seg)\n : { order: undefined, rest: seg };\n dirOrders.push(order);\n return slugify(rest);\n });\n\n const { date: filenameDate, rest: dateRest } = splitDatePrefix(fileBase);\n const { order: fileOrder, rest } = isDocs\n ? splitOrderPrefix(dateRest)\n : { order: undefined, rest: dateRest };\n const baseSlug = data.slug ? slugify(data.slug) : slugify(rest);\n\n const isIndex = isDocs && !data.slug && baseSlug === \"index\";\n const path = isDocs\n ? isIndex\n ? dirSlugs\n : [...dirSlugs, baseSlug]\n : [baseSlug];\n const slug = path[path.length - 1] ?? \"\";\n const order =\n data.order ??\n fileOrder ??\n (isIndex ? dirOrders[dirOrders.length - 1] : undefined);\n\n const version = isChangelog\n ? (data.version ?? parseVersion(dateRest))\n : data.version;\n\n const baseUrl = joinPath(config.content.basePath, \"/\").replace(\n /(.)\\/+$/,\n \"$1\",\n );\n const urlPath = path.join(\"/\");\n const url = isChangelog\n ? `${baseUrl}#${slug}`\n : urlPath\n ? joinPath(config.content.basePath, urlPath)\n : baseUrl;\n\n const levelKeys = dirSlugs.map((s, i) => orderKey(dirOrders[i], s));\n if (isIndex) {\n if (levelKeys.length > 0)\n levelKeys[levelKeys.length - 1] = orderKey(order, slug);\n } else {\n levelKeys.push(orderKey(order, slug));\n }\n const sortKey = levelKeys.join(\"/\");\n\n const dirRel = rel.split(/[\\\\/]/).slice(0, -1).join(\"/\");\n const assetBase = dirRel\n ? joinPath(config.content.basePath, dirRel)\n : config.content.basePath;\n const { html, toc } = yield* renderMarkdownEffect(content, config, {\n assetBase,\n });\n\n let date = data.date ?? filenameDate;\n if (!date) {\n if (!isDocs) {\n yield* Effect.logWarning(\n `${rel}: no date in frontmatter or filename — falling back to the file's creation time, which is not stable across checkouts.`,\n );\n }\n const info = yield* fs.stat(absPath);\n const created =\n Option.getOrUndefined(info.birthtime) ??\n Option.getOrUndefined(info.mtime);\n date = (created ?? new Date()).toISOString();\n }\n\n const post: Post = {\n slug,\n path,\n url,\n title: data.title,\n description: data.description,\n date,\n updated: data.updated,\n tags: [...data.tags],\n category: data.category,\n order,\n version,\n draft: data.draft,\n image: data.image,\n author: data.author,\n excerpt: data.excerpt ?? deriveExcerpt(content),\n readingTimeMinutes: readingTimeMinutes(content),\n html,\n toc,\n content,\n };\n return { post, sortKey } satisfies BuiltPost;\n });\n\n/**\n * Reads all Markdown files from the configured content directory,\n * renders them, and returns sorted posts.\n *\n * - **docs** — sorted by numeric directory/file order prefix.\n * - **changelog** — sorted by date descending, then semver descending.\n * - **blog** — sorted by date descending.\n *\n * @param config - Resolved Voxx config.\n * @param opts - Optional collection filter and draft visibility.\n */\nexport const getPostsEffect = (\n config: VoxxConfig,\n opts: GetPostsEffectOptions = {},\n) =>\n Effect.gen(function* () {\n if (opts.collection) {\n const active = config.collections?.find(\n (c) => c.name === opts.collection,\n );\n if (!active) {\n return yield* new ConfigError({\n message: `Unknown collection \"${opts.collection}\" — defined: ${(\n config.collections ?? []\n )\n .map((c) => c.name)\n .join(\", \")}`,\n });\n }\n config = { ...config, content: active };\n }\n\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const dir = config.content.dir;\n\n const exists = yield* fs\n .exists(dir)\n .pipe(Effect.orElseSucceed(() => false));\n if (!exists) return yield* new ContentDirMissing({ dir });\n\n const entries = yield* fs.readDirectory(dir, { recursive: true });\n const files = entries.filter((f) => MD_RE.test(f));\n const includeDrafts = opts.includeDrafts ?? config.content.drafts;\n\n const built = yield* Effect.forEach(\n files,\n (rel) => buildPost(config, path.join(dir, rel), rel),\n { concurrency: 8 },\n );\n\n const seen = new Map<string, string>();\n for (let i = 0; i < built.length; i++) {\n const key = built[i]!.post.path.join(\"/\");\n const previous = seen.get(key);\n if (previous !== undefined) {\n yield* Effect.logWarning(\n `Duplicate slug \"${key}\": ${files[i]} collides with ${previous} — only one will be reachable.`,\n );\n } else {\n seen.set(key, files[i]!);\n }\n }\n\n const visible = built.filter((b) => includeDrafts || !b.post.draft);\n\n if (config.content.type === \"docs\") {\n return visible\n .sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n .map((b) => b.post);\n }\n\n const isChangelog = config.content.type === \"changelog\";\n\n return visible\n .map((b) => b.post)\n .sort((a, b) => {\n const byDate = b.date.localeCompare(a.date);\n if (byDate !== 0) return byDate;\n if (isChangelog && a.version && b.version) {\n return compareVersions(b.version, a.version);\n }\n return a.slug.localeCompare(b.slug);\n });\n });\n\n/**\n * Finds a post in an already-loaded array by slug or path.\n *\n * @param posts - Array returned by `getPosts`.\n * @param slug - Slash-separated path, e.g. `\"getting-started/install\"`.\n * @returns The matching post, or `undefined` if not found.\n */\nexport function findPost(posts: Post[], slug: string): Post | undefined {\n const wanted = slug.split(\"/\").filter(Boolean).map(slugify).join(\"/\");\n return posts.find(\n (p) =>\n p.path.join(\"/\") === wanted || (p.path.length <= 1 && p.slug === wanted),\n );\n}\n\n/**\n * Loads all posts and returns the one matching `slug`.\n * Throws `PostNotFound` if no match exists.\n */\nexport const getPostEffect = (\n config: VoxxConfig,\n slug: string,\n opts: GetPostsEffectOptions = {},\n) =>\n Effect.gen(function* () {\n const posts = yield* getPostsEffect(config, opts);\n const post = findPost(posts, slug);\n if (!post) return yield* new PostNotFound({ slug });\n return post;\n });\n"],"mappings":";;;;;;;AAiBA,MAAM,QAAQ;AASd,MAAM,YAAY,OAA2B,SAC3C,GAAG,OAAO,SAAS,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG;AAO/C,MAAM,aAAa,QAAoB,SAAiB,QACtD,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;CAE7B,MAAM,EAAE,MAAM,YAAY,OAAO,iBAAiB,KAAK,OADpC,GAAG,eAAe,OAAO,CACc;CAC1D,MAAM,SAAS,OAAO,QAAQ,SAAS;CACvC,MAAM,cAAc,OAAO,QAAQ,SAAS;CAE5C,MAAM,WAAW,IAAI,QAAQ,OAAO,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,CAAC,OAAO,OAAO;CACrE,MAAM,WAAW,SAAS,IAAI,KAAK;CAEnC,MAAM,YAAuC,CAAC;CAC9C,MAAM,WAAW,SAAS,KAAK,QAAQ;EACrC,MAAM,EAAE,OAAO,SAAS,SACpB,iBAAiB,GAAG,IACpB;GAAE,OAAO,KAAA;GAAW,MAAM;EAAI;EAClC,UAAU,KAAK,KAAK;EACpB,OAAO,QAAQ,IAAI;CACrB,CAAC;CAED,MAAM,EAAE,MAAM,cAAc,MAAM,aAAa,gBAAgB,QAAQ;CACvE,MAAM,EAAE,OAAO,WAAW,SAAS,SAC/B,iBAAiB,QAAQ,IACzB;EAAE,OAAO,KAAA;EAAW,MAAM;CAAS;CACvC,MAAM,WAAW,KAAK,OAAO,QAAQ,KAAK,IAAI,IAAI,QAAQ,IAAI;CAE9D,MAAM,UAAU,UAAU,CAAC,KAAK,QAAQ,aAAa;CACrD,MAAM,OAAO,SACT,UACE,WACA,CAAC,GAAG,UAAU,QAAQ,IACxB,CAAC,QAAQ;CACb,MAAM,OAAO,KAAK,KAAK,SAAS,MAAM;CACtC,MAAM,QACJ,KAAK,SACL,cACC,UAAU,UAAU,UAAU,SAAS,KAAK,KAAA;CAE/C,MAAM,UAAU,cACX,KAAK,WAAW,aAAa,QAAQ,IACtC,KAAK;CAET,MAAM,UAAU,SAAS,OAAO,QAAQ,UAAU,GAAG,CAAC,CAAC,QACrD,WACA,IACF;CACA,MAAM,UAAU,KAAK,KAAK,GAAG;CAC7B,MAAM,MAAM,cACR,GAAG,QAAQ,GAAG,SACd,UACE,SAAS,OAAO,QAAQ,UAAU,OAAO,IACzC;CAEN,MAAM,YAAY,SAAS,KAAK,GAAG,MAAM,SAAS,UAAU,IAAI,CAAC,CAAC;CAClE,IAAI;MACE,UAAU,SAAS,GACrB,UAAU,UAAU,SAAS,KAAK,SAAS,OAAO,IAAI;CAAA,OAExD,UAAU,KAAK,SAAS,OAAO,IAAI,CAAC;CAEtC,MAAM,UAAU,UAAU,KAAK,GAAG;CAElC,MAAM,SAAS,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,GAAG;CAIvD,MAAM,EAAE,MAAM,QAAQ,OAAO,qBAAqB,SAAS,QAAQ,EACjE,WAJgB,SACd,SAAS,OAAO,QAAQ,UAAU,MAAM,IACxC,OAAO,QAAQ,SAGnB,CAAC;CAED,IAAI,OAAO,KAAK,QAAQ;CACxB,IAAI,CAAC,MAAM;EACT,IAAI,CAAC,QACH,OAAO,OAAO,WACZ,GAAG,IAAI,uHACT;EAEF,MAAM,OAAO,OAAO,GAAG,KAAK,OAAO;EAInC,QAFE,OAAO,eAAe,KAAK,SAAS,KACpC,OAAO,eAAe,KAAK,KAAK,qBACf,IAAI,KAAK,EAAA,CAAG,YAAY;CAC7C;CAuBA,OAAO;EAAE,MAAA;GApBP;GACA;GACA;GACA,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB;GACA,SAAS,KAAK;GACd,MAAM,CAAC,GAAG,KAAK,IAAI;GACnB,UAAU,KAAK;GACf;GACA;GACA,OAAO,KAAK;GACZ,OAAO,KAAK;GACZ,QAAQ,KAAK;GACb,SAAS,KAAK,WAAW,cAAc,OAAO;GAC9C,oBAAoB,mBAAmB,OAAO;GAC9C;GACA;GACA;EAEU;EAAG;CAAQ;AACzB,CAAC;;;;;;;;;;;;AAaH,MAAa,kBACX,QACA,OAA8B,CAAC,MAE/B,OAAO,IAAI,aAAa;CACtB,IAAI,KAAK,YAAY;EACnB,MAAM,SAAS,OAAO,aAAa,MAChC,MAAM,EAAE,SAAS,KAAK,UACzB;EACA,IAAI,CAAC,QACH,OAAO,OAAO,IAAI,YAAY,EAC5B,SAAS,uBAAuB,KAAK,WAAW,gBAC9C,OAAO,eAAe,CAAC,EAAA,CAEtB,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,KAAK,IAAI,IACd,CAAC;EAEH,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAO;CACxC;CAEA,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,MAAM,OAAO,QAAQ;CAK3B,IAAI,EAAC,OAHiB,GACnB,OAAO,GAAG,CAAC,CACX,KAAK,OAAO,oBAAoB,KAAK,CAAC,IAC5B,OAAO,OAAO,IAAI,kBAAkB,EAAE,IAAI,CAAC;CAGxD,MAAM,SAAQ,OADS,GAAG,cAAc,KAAK,EAAE,WAAW,KAAK,CAAC,EAAA,CAC1C,QAAQ,MAAM,MAAM,KAAK,CAAC,CAAC;CACjD,MAAM,gBAAgB,KAAK,iBAAiB,OAAO,QAAQ;CAE3D,MAAM,QAAQ,OAAO,OAAO,QAC1B,QACC,QAAQ,UAAU,QAAQ,KAAK,KAAK,KAAK,GAAG,GAAG,GAAG,GACnD,EAAE,aAAa,EAAE,CACnB;CAEA,MAAM,uBAAO,IAAI,IAAoB;CACrC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,MAAM,MAAM,EAAE,CAAE,KAAK,KAAK,KAAK,GAAG;EACxC,MAAM,WAAW,KAAK,IAAI,GAAG;EAC7B,IAAI,aAAa,KAAA,GACf,OAAO,OAAO,WACZ,mBAAmB,IAAI,KAAK,MAAM,GAAG,iBAAiB,SAAS,+BACjE;OAEA,KAAK,IAAI,KAAK,MAAM,EAAG;CAE3B;CAEA,MAAM,UAAU,MAAM,QAAQ,MAAM,iBAAiB,CAAC,EAAE,KAAK,KAAK;CAElE,IAAI,OAAO,QAAQ,SAAS,QAC1B,OAAO,QACJ,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC,CAAC,CAClD,KAAK,MAAM,EAAE,IAAI;CAGtB,MAAM,cAAc,OAAO,QAAQ,SAAS;CAE5C,OAAO,QACJ,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,MAAM,GAAG,MAAM;EACd,MAAM,SAAS,EAAE,KAAK,cAAc,EAAE,IAAI;EAC1C,IAAI,WAAW,GAAG,OAAO;EACzB,IAAI,eAAe,EAAE,WAAW,EAAE,SAChC,OAAO,gBAAgB,EAAE,SAAS,EAAE,OAAO;EAE7C,OAAO,EAAE,KAAK,cAAc,EAAE,IAAI;CACpC,CAAC;AACL,CAAC;;;;;;;;AASH,SAAgB,SAAS,OAAe,MAAgC;CACtE,MAAM,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,GAAG;CACpE,OAAO,MAAM,MACV,MACC,EAAE,KAAK,KAAK,GAAG,MAAM,UAAW,EAAE,KAAK,UAAU,KAAK,EAAE,SAAS,MACrE;AACF;;;;;AAMA,MAAa,iBACX,QACA,MACA,OAA8B,CAAC,MAE/B,OAAO,IAAI,aAAa;CAEtB,MAAM,OAAO,SAAS,OADD,eAAe,QAAQ,IAAI,GACnB,IAAI;CACjC,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,aAAa,EAAE,KAAK,CAAC;CAClD,OAAO;AACT,CAAC"}
package/dist/dev.cjs ADDED
@@ -0,0 +1,82 @@
1
+ const require_config = require("./config.cjs");
2
+ let effect = require("effect");
3
+ let _effect_platform_node = require("@effect/platform-node");
4
+ let node_fs = require("node:fs");
5
+ let node_path = require("node:path");
6
+ //#region src/dev.ts
7
+ const run = (effect$1) => effect.Effect.runPromise(effect.Effect.provide(effect$1, _effect_platform_node.NodeContext.layer));
8
+ const SKIP_DIRS = new Set([
9
+ "node_modules",
10
+ ".next",
11
+ ".git",
12
+ ".turbo",
13
+ "dist",
14
+ "out"
15
+ ]);
16
+ const VERSION_FILE = "content-version.ts";
17
+ function writeVersion(file, value) {
18
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(file), { recursive: true });
19
+ (0, node_fs.writeFileSync)(file, `export const CONTENT_VERSION = ${value};\n`);
20
+ }
21
+ function findVersionModules(root, depth = 6) {
22
+ const found = [];
23
+ const walk = (dir, left) => {
24
+ let entries;
25
+ try {
26
+ entries = (0, node_fs.readdirSync)(dir, { withFileTypes: true });
27
+ } catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) if (entry.isDirectory()) {
31
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
32
+ if (left > 0) walk((0, node_path.join)(dir, entry.name), left - 1);
33
+ } else if (entry.name === VERSION_FILE && (0, node_path.basename)(dir) === "_voxx") found.push((0, node_path.join)(dir, entry.name));
34
+ };
35
+ walk(root, depth);
36
+ return found;
37
+ }
38
+ let registered = false;
39
+ /**
40
+ * Watches content directories and `voxx.json` for changes, then bumps a
41
+ * `CONTENT_VERSION` timestamp in each discovered `_voxx/content-version.ts`
42
+ * file to trigger Next.js hot reload.
43
+ *
44
+ * No-ops outside `NODE_ENV=development` and in non-Node runtimes (e.g. Edge).
45
+ * Safe to call multiple times — only the first call registers watchers.
46
+ *
47
+ * @param opts - Optional cwd, explicit version module paths, and debounce delay.
48
+ */
49
+ async function registerContentWatcher(opts = {}) {
50
+ if (process.env["NEXT_RUNTIME"] && process.env["NEXT_RUNTIME"] !== "nodejs") return;
51
+ if (process.env.NODE_ENV !== "development") return;
52
+ if (registered) return;
53
+ registered = true;
54
+ const cwd = opts.cwd ?? process.cwd();
55
+ const versionModules = opts.versionModules && opts.versionModules.length > 0 ? opts.versionModules : findVersionModules(cwd);
56
+ if (versionModules.length === 0) return;
57
+ const dirs = /* @__PURE__ */ new Set();
58
+ try {
59
+ const config = await run(require_config.loadConfigEffect({ cwd }));
60
+ for (const collection of config.collections) dirs.add(collection.dir);
61
+ } catch {}
62
+ const paths = new Set([(0, node_path.join)(cwd, "voxx.json"), ...dirs]);
63
+ let timer;
64
+ const bump = () => {
65
+ clearTimeout(timer);
66
+ timer = setTimeout(() => {
67
+ const value = Date.now();
68
+ for (const file of versionModules) writeVersion(file, value);
69
+ }, opts.debounceMs ?? 80);
70
+ };
71
+ const watchers = [];
72
+ for (const path of paths) {
73
+ if (!(0, node_fs.existsSync)(path)) continue;
74
+ try {
75
+ watchers.push((0, node_fs.watch)(path, { recursive: true }, bump));
76
+ } catch {}
77
+ }
78
+ }
79
+ //#endregion
80
+ exports.registerContentWatcher = registerContentWatcher;
81
+
82
+ //# sourceMappingURL=dev.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.cjs","names":["Effect","effect","NodeContext","loadConfigEffect"],"sources":["../src/dev.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n readdirSync,\n watch,\n writeFileSync,\n type Dirent,\n type FSWatcher,\n} from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { Effect } from \"effect\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { NodeContext } from \"@effect/platform-node\";\nimport { loadConfigEffect } from \"./config\";\n\ntype Services = FileSystem.FileSystem | Path.Path;\n\nconst run = <A, E>(effect: Effect.Effect<A, E, Services>): Promise<A> =>\n Effect.runPromise(Effect.provide(effect, NodeContext.layer));\n\n/** Options for `registerContentWatcher`. */\nexport interface ContentWatcherOptions {\n /** Working directory used to locate `voxx.json` and content dirs. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit paths to `content-version.ts` files to bump on change. Auto-discovered when omitted. */\n versionModules?: string[];\n /** Milliseconds to wait before writing after the last change event. Defaults to `80`. */\n debounceMs?: number;\n}\n\nconst SKIP_DIRS = new Set([\n \"node_modules\",\n \".next\",\n \".git\",\n \".turbo\",\n \"dist\",\n \"out\",\n]);\nconst VERSION_FILE = \"content-version.ts\";\n\nfunction writeVersion(file: string, value: number): void {\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, `export const CONTENT_VERSION = ${value};\\n`);\n}\n\nfunction findVersionModules(root: string, depth = 6): string[] {\n const found: string[] = [];\n const walk = (dir: string, left: number): void => {\n let entries: Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(\".\")) continue;\n if (left > 0) walk(join(dir, entry.name), left - 1);\n } else if (entry.name === VERSION_FILE && basename(dir) === \"_voxx\") {\n found.push(join(dir, entry.name));\n }\n }\n };\n walk(root, depth);\n return found;\n}\n\nlet registered = false;\n\n/**\n * Watches content directories and `voxx.json` for changes, then bumps a\n * `CONTENT_VERSION` timestamp in each discovered `_voxx/content-version.ts`\n * file to trigger Next.js hot reload.\n *\n * No-ops outside `NODE_ENV=development` and in non-Node runtimes (e.g. Edge).\n * Safe to call multiple times — only the first call registers watchers.\n *\n * @param opts - Optional cwd, explicit version module paths, and debounce delay.\n */\nexport async function registerContentWatcher(\n opts: ContentWatcherOptions = {},\n): Promise<void> {\n if (process.env[\"NEXT_RUNTIME\"] && process.env[\"NEXT_RUNTIME\"] !== \"nodejs\") {\n return;\n }\n if (process.env.NODE_ENV !== \"development\") return;\n if (registered) return;\n registered = true;\n\n const cwd = opts.cwd ?? process.cwd();\n\n const versionModules =\n opts.versionModules && opts.versionModules.length > 0\n ? opts.versionModules\n : findVersionModules(cwd);\n if (versionModules.length === 0) return;\n\n const dirs = new Set<string>();\n try {\n const config = await run(loadConfigEffect({ cwd }));\n for (const collection of config.collections) dirs.add(collection.dir);\n } catch {}\n\n const paths = new Set<string>([join(cwd, \"voxx.json\"), ...dirs]);\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const bump = () => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n const value = Date.now();\n for (const file of versionModules) writeVersion(file, value);\n }, opts.debounceMs ?? 80);\n };\n\n const watchers: FSWatcher[] = [];\n for (const path of paths) {\n if (!existsSync(path)) continue;\n try {\n watchers.push(watch(path, { recursive: true }, bump));\n } catch {}\n }\n}\n"],"mappings":";;;;;;AAiBA,MAAM,OAAa,aACjBA,OAAAA,OAAO,WAAWA,OAAAA,OAAO,QAAQC,UAAQC,sBAAAA,YAAY,KAAK,CAAC;AAY7D,MAAM,YAAY,IAAI,IAAI;CACxB;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,eAAe;AAErB,SAAS,aAAa,MAAc,OAAqB;CACvD,CAAA,GAAA,QAAA,UAAA,EAAA,GAAA,UAAA,QAAA,CAAkB,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CAC5C,CAAA,GAAA,QAAA,cAAA,CAAc,MAAM,kCAAkC,MAAM,IAAI;AAClE;AAEA,SAAS,mBAAmB,MAAc,QAAQ,GAAa;CAC7D,MAAM,QAAkB,CAAC;CACzB,MAAM,QAAQ,KAAa,SAAuB;EAChD,IAAI;EACJ,IAAI;GACF,WAAA,GAAA,QAAA,YAAA,CAAsB,KAAK,EAAE,eAAe,KAAK,CAAC;EACpD,QAAQ;GACN;EACF;EACA,KAAK,MAAM,SAAS,SAClB,IAAI,MAAM,YAAY,GAAG;GACvB,IAAI,UAAU,IAAI,MAAM,IAAI,KAAK,MAAM,KAAK,WAAW,GAAG,GAAG;GAC7D,IAAI,OAAO,GAAG,MAAA,GAAA,UAAA,KAAA,CAAU,KAAK,MAAM,IAAI,GAAG,OAAO,CAAC;EACpD,OAAO,IAAI,MAAM,SAAS,iBAAA,GAAA,UAAA,SAAA,CAAyB,GAAG,MAAM,SAC1D,MAAM,MAAA,GAAA,UAAA,KAAA,CAAU,KAAK,MAAM,IAAI,CAAC;CAGtC;CACA,KAAK,MAAM,KAAK;CAChB,OAAO;AACT;AAEA,IAAI,aAAa;;;;;;;;;;;AAYjB,eAAsB,uBACpB,OAA8B,CAAC,GAChB;CACf,IAAI,QAAQ,IAAI,mBAAmB,QAAQ,IAAI,oBAAoB,UACjE;CAEF,IAAI,QAAQ,IAAI,aAAa,eAAe;CAC5C,IAAI,YAAY;CAChB,aAAa;CAEb,MAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;CAEpC,MAAM,iBACJ,KAAK,kBAAkB,KAAK,eAAe,SAAS,IAChD,KAAK,iBACL,mBAAmB,GAAG;CAC5B,IAAI,eAAe,WAAW,GAAG;CAEjC,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI;EACF,MAAM,SAAS,MAAM,IAAIC,eAAAA,iBAAiB,EAAE,IAAI,CAAC,CAAC;EAClD,KAAK,MAAM,cAAc,OAAO,aAAa,KAAK,IAAI,WAAW,GAAG;CACtE,QAAQ,CAAC;CAET,MAAM,QAAQ,IAAI,IAAY,EAAA,GAAA,UAAA,KAAA,CAAM,KAAK,WAAW,GAAG,GAAG,IAAI,CAAC;CAE/D,IAAI;CACJ,MAAM,aAAa;EACjB,aAAa,KAAK;EAClB,QAAQ,iBAAiB;GACvB,MAAM,QAAQ,KAAK,IAAI;GACvB,KAAK,MAAM,QAAQ,gBAAgB,aAAa,MAAM,KAAK;EAC7D,GAAG,KAAK,cAAc,EAAE;CAC1B;CAEA,MAAM,WAAwB,CAAC;CAC/B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,EAAA,GAAA,QAAA,WAAA,CAAY,IAAI,GAAG;EACvB,IAAI;GACF,SAAS,MAAA,GAAA,QAAA,MAAA,CAAW,MAAM,EAAE,WAAW,KAAK,GAAG,IAAI,CAAC;EACtD,QAAQ,CAAC;CACX;AACF"}
package/dist/dev.d.cts ADDED
@@ -0,0 +1,24 @@
1
+ //#region src/dev.d.ts
2
+ /** Options for `registerContentWatcher`. */
3
+ interface ContentWatcherOptions {
4
+ /** Working directory used to locate `voxx.json` and content dirs. Defaults to `process.cwd()`. */
5
+ cwd?: string;
6
+ /** Explicit paths to `content-version.ts` files to bump on change. Auto-discovered when omitted. */
7
+ versionModules?: string[];
8
+ /** Milliseconds to wait before writing after the last change event. Defaults to `80`. */
9
+ debounceMs?: number;
10
+ }
11
+ /**
12
+ * Watches content directories and `voxx.json` for changes, then bumps a
13
+ * `CONTENT_VERSION` timestamp in each discovered `_voxx/content-version.ts`
14
+ * file to trigger Next.js hot reload.
15
+ *
16
+ * No-ops outside `NODE_ENV=development` and in non-Node runtimes (e.g. Edge).
17
+ * Safe to call multiple times — only the first call registers watchers.
18
+ *
19
+ * @param opts - Optional cwd, explicit version module paths, and debounce delay.
20
+ */
21
+ declare function registerContentWatcher(opts?: ContentWatcherOptions): Promise<void>;
22
+ //#endregion
23
+ export { ContentWatcherOptions, registerContentWatcher };
24
+ //# sourceMappingURL=dev.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.cts","names":[],"sources":["../src/dev.ts"],"mappings":";;UAqBiB,qBAAA;EAAqB;EAEpC,GAAA;EAFoC;EAIpC,cAAA;EAAA;EAEA,UAAA;AAAA;AAAU;AAoDZ;;;;;;;;AAEU;AAtDE,iBAoDU,sBAAA,CACpB,IAAA,GAAM,qBAAA,GACL,OAAO"}
package/dist/dev.d.mts ADDED
@@ -0,0 +1,24 @@
1
+ //#region src/dev.d.ts
2
+ /** Options for `registerContentWatcher`. */
3
+ interface ContentWatcherOptions {
4
+ /** Working directory used to locate `voxx.json` and content dirs. Defaults to `process.cwd()`. */
5
+ cwd?: string;
6
+ /** Explicit paths to `content-version.ts` files to bump on change. Auto-discovered when omitted. */
7
+ versionModules?: string[];
8
+ /** Milliseconds to wait before writing after the last change event. Defaults to `80`. */
9
+ debounceMs?: number;
10
+ }
11
+ /**
12
+ * Watches content directories and `voxx.json` for changes, then bumps a
13
+ * `CONTENT_VERSION` timestamp in each discovered `_voxx/content-version.ts`
14
+ * file to trigger Next.js hot reload.
15
+ *
16
+ * No-ops outside `NODE_ENV=development` and in non-Node runtimes (e.g. Edge).
17
+ * Safe to call multiple times — only the first call registers watchers.
18
+ *
19
+ * @param opts - Optional cwd, explicit version module paths, and debounce delay.
20
+ */
21
+ declare function registerContentWatcher(opts?: ContentWatcherOptions): Promise<void>;
22
+ //#endregion
23
+ export { ContentWatcherOptions, registerContentWatcher };
24
+ //# sourceMappingURL=dev.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.mts","names":[],"sources":["../src/dev.ts"],"mappings":";;UAqBiB,qBAAA;EAAqB;EAEpC,GAAA;EAFoC;EAIpC,cAAA;EAAA;EAEA,UAAA;AAAA;AAAU;AAoDZ;;;;;;;;AAEU;AAtDE,iBAoDU,sBAAA,CACpB,IAAA,GAAM,qBAAA,GACL,OAAO"}