livemark 0.24.0 → 0.24.1

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 (125) hide show
  1. package/.agents/skills/change-review/SKILL.md +47 -0
  2. package/{skills/configuration → .agents/skills/livemark-configuration}/SKILL.md +1 -1
  3. package/build/actions/changelog/build.d.ts +21 -9
  4. package/build/actions/changelog/build.js +29 -17
  5. package/build/actions/changelog/build.unit.js +156 -2
  6. package/build/components/Callout.js +2 -2
  7. package/build/components/LinkButton.js +2 -2
  8. package/build/components/Section.js +2 -2
  9. package/build/components/SectionHeader.js +2 -2
  10. package/build/content-collections.js +5 -4
  11. package/build/elements/accordion.js +2 -2
  12. package/build/elements/alert-dialog.js +2 -2
  13. package/build/elements/alert.js +2 -2
  14. package/build/elements/aspect-ratio.js +2 -2
  15. package/build/elements/avatar.js +2 -2
  16. package/build/elements/badge.js +2 -2
  17. package/build/elements/breadcrumb.js +2 -2
  18. package/build/elements/button-group.js +2 -2
  19. package/build/elements/button.js +2 -2
  20. package/build/elements/calendar.js +2 -2
  21. package/build/elements/card.js +2 -2
  22. package/build/elements/carousel.js +2 -2
  23. package/build/elements/chart.js +2 -2
  24. package/build/elements/checkbox.js +2 -2
  25. package/build/elements/command.js +2 -2
  26. package/build/elements/context-menu.js +2 -2
  27. package/build/elements/dialog.js +2 -2
  28. package/build/elements/drawer.js +2 -2
  29. package/build/elements/dropdown-menu.js +2 -2
  30. package/build/elements/empty.js +2 -2
  31. package/build/elements/field.js +2 -2
  32. package/build/elements/hover-card.js +2 -2
  33. package/build/elements/input-group.js +2 -2
  34. package/build/elements/input-otp.js +2 -2
  35. package/build/elements/input.js +2 -2
  36. package/build/elements/item.js +2 -2
  37. package/build/elements/kbd.js +2 -2
  38. package/build/elements/label.js +2 -2
  39. package/build/elements/menubar.js +2 -2
  40. package/build/elements/native-select.js +2 -2
  41. package/build/elements/navigation-menu.js +2 -2
  42. package/build/elements/pagination.js +2 -2
  43. package/build/elements/popover.js +2 -2
  44. package/build/elements/progress.js +2 -2
  45. package/build/elements/radio-group.js +2 -2
  46. package/build/elements/resizable.js +2 -2
  47. package/build/elements/scroll-area.js +2 -2
  48. package/build/elements/select.js +2 -2
  49. package/build/elements/separator.js +2 -2
  50. package/build/elements/sheet.js +2 -2
  51. package/build/elements/sidebar.js +2 -2
  52. package/build/elements/skeleton.js +2 -2
  53. package/build/elements/slider.js +2 -2
  54. package/build/elements/spinner.js +2 -2
  55. package/build/elements/switch.js +2 -2
  56. package/build/elements/table.js +2 -2
  57. package/build/elements/tabs.js +2 -2
  58. package/build/elements/textarea.js +2 -2
  59. package/build/elements/toggle-group.js +2 -2
  60. package/build/elements/toggle.js +2 -2
  61. package/build/elements/tooltip.js +2 -2
  62. package/package.json +17 -19
  63. package/source/actions/changelog/build.ts +29 -16
  64. package/source/actions/changelog/build.unit.ts +191 -1
  65. package/source/components/Callout.tsx +1 -1
  66. package/source/components/LinkButton.tsx +1 -1
  67. package/source/components/Section.tsx +1 -1
  68. package/source/components/SectionHeader.tsx +1 -1
  69. package/source/content-collections.ts +4 -3
  70. package/source/elements/accordion.tsx +1 -1
  71. package/source/elements/alert-dialog.tsx +1 -1
  72. package/source/elements/alert.tsx +1 -1
  73. package/source/elements/aspect-ratio.tsx +1 -1
  74. package/source/elements/avatar.tsx +1 -1
  75. package/source/elements/badge.tsx +1 -1
  76. package/source/elements/breadcrumb.tsx +1 -1
  77. package/source/elements/button-group.tsx +1 -1
  78. package/source/elements/button.tsx +1 -1
  79. package/source/elements/calendar.tsx +1 -1
  80. package/source/elements/card.tsx +1 -1
  81. package/source/elements/carousel.tsx +1 -1
  82. package/source/elements/chart.tsx +1 -1
  83. package/source/elements/checkbox.tsx +1 -1
  84. package/source/elements/command.tsx +1 -1
  85. package/source/elements/context-menu.tsx +1 -1
  86. package/source/elements/dialog.tsx +1 -1
  87. package/source/elements/drawer.tsx +1 -1
  88. package/source/elements/dropdown-menu.tsx +1 -1
  89. package/source/elements/empty.tsx +1 -1
  90. package/source/elements/field.tsx +1 -1
  91. package/source/elements/hover-card.tsx +1 -1
  92. package/source/elements/input-group.tsx +1 -1
  93. package/source/elements/input-otp.tsx +1 -1
  94. package/source/elements/input.tsx +1 -1
  95. package/source/elements/item.tsx +1 -1
  96. package/source/elements/kbd.tsx +1 -1
  97. package/source/elements/label.tsx +1 -1
  98. package/source/elements/menubar.tsx +1 -1
  99. package/source/elements/native-select.tsx +1 -1
  100. package/source/elements/navigation-menu.tsx +1 -1
  101. package/source/elements/pagination.tsx +1 -1
  102. package/source/elements/popover.tsx +1 -1
  103. package/source/elements/progress.tsx +1 -1
  104. package/source/elements/radio-group.tsx +1 -1
  105. package/source/elements/resizable.tsx +1 -1
  106. package/source/elements/scroll-area.tsx +1 -1
  107. package/source/elements/select.tsx +1 -1
  108. package/source/elements/separator.tsx +1 -1
  109. package/source/elements/sheet.tsx +1 -1
  110. package/source/elements/sidebar.tsx +1 -1
  111. package/source/elements/skeleton.tsx +1 -1
  112. package/source/elements/slider.tsx +1 -1
  113. package/source/elements/spinner.tsx +1 -1
  114. package/source/elements/switch.tsx +1 -1
  115. package/source/elements/table.tsx +1 -1
  116. package/source/elements/tabs.tsx +1 -1
  117. package/source/elements/textarea.tsx +1 -1
  118. package/source/elements/toggle-group.tsx +1 -1
  119. package/source/elements/toggle.tsx +1 -1
  120. package/source/elements/tooltip.tsx +1 -1
  121. package/build/utils/style.d.ts +0 -2
  122. package/build/utils/style.js +0 -6
  123. package/source/utils/style.ts +0 -6
  124. /package/{skills/authoring → .agents/skills/livemark-authoring}/SKILL.md +0 -0
  125. /package/{skills/customization → .agents/skills/livemark-customization}/SKILL.md +0 -0
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "livemark",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Livemark description",
5
5
  "license": "MIT",
6
6
  "author": "Evgeny Karev",
7
- "repository": "https://github.com/datisthq/livemark",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/datisthq/livemark"
10
+ },
8
11
  "bin": {
9
12
  "livemark": "./build/entrypoints/main.js"
10
13
  },
11
14
  "files": [
12
15
  "build",
13
16
  "source",
14
- "skills"
17
+ ".agents/skills"
15
18
  ],
16
19
  "type": "module",
17
20
  "sideEffects": false,
@@ -20,8 +23,7 @@
20
23
  "./components/*": "./source/components/*.tsx",
21
24
  "./elements/*": "./source/elements/*.tsx",
22
25
  "./hooks/*": "./source/hooks/*.ts",
23
- "./styles/*": "./source/styles/*.css",
24
- "./utils/*": "./source/utils/*.ts"
26
+ "./styles/*": "./source/styles/*.css"
25
27
  },
26
28
  "dependencies": {
27
29
  "@base-ui/react": "1.3.0",
@@ -50,14 +52,14 @@
50
52
  "@vitejs/plugin-react": "6.0.1",
51
53
  "chokidar": "^5.0.0",
52
54
  "class-variance-authority": "0.7.1",
53
- "clsx": "2.1.1",
54
55
  "cmdk": "1.1.1",
56
+ "cn": "0.2.4",
55
57
  "commander": "14.0.3",
56
58
  "embla-carousel-react": "8.6.0",
57
59
  "es-toolkit": "1.44.0",
58
60
  "github-slugger": "^2.0.0",
59
61
  "input-otp": "1.4.2",
60
- "katex": "^0.16.44",
62
+ "katex": "0.18.9",
61
63
  "lucide-react": "0.577.0",
62
64
  "mdast-util-directive": "^3.1.0",
63
65
  "mdast-util-to-string": "^4.0.0",
@@ -81,31 +83,28 @@
81
83
  "shadcn": "3.7.0",
82
84
  "shiki": "4.0.2",
83
85
  "sonner": "2.0.7",
84
- "tailwind-merge": "3.5.0",
85
86
  "tailwindcss": "4.2.2",
86
87
  "tw-animate-css": "1.4.0",
87
88
  "unified": "^11.0.5",
88
89
  "unist-util-visit": "^5.1.0",
89
90
  "vaul": "1.1.2",
90
- "vite": "8.0.13",
91
+ "vite": "8.3.1",
91
92
  "vite-plugin-svgr": "5.2.0",
92
93
  "zod": "4.3.6"
93
94
  },
94
95
  "devDependencies": {
95
96
  "@plausible-analytics/tracker": "^0.4.4",
96
- "@semantic-release/exec": "7.1.0",
97
97
  "@types/node": "24.2.0",
98
98
  "@typescript/native-preview": "7.0.0-dev.20260316.1",
99
- "@vitest/coverage-v8": "4.1.6",
100
- "conventional-changelog-conventionalcommits": "9.1.0",
99
+ "@vitest/coverage-v8": "5.0.1",
101
100
  "lefthook": "2.1.1",
102
101
  "livemark": "link:.",
103
- "npm-check-updates": "18.0.1",
102
+ "npm-check-updates": "23.1.0",
104
103
  "react": "19.2.4",
105
104
  "react-dom": "19.2.4",
106
- "semantic-release": "24.2.9",
107
105
  "tempy": "3.2.0",
108
- "vite-plus": "0.1.24",
106
+ "vite-plus": "1.0.0",
107
+ "vitest": "5.0.1",
109
108
  "wrangler": "4.85.0"
110
109
  },
111
110
  "peerDependencies": {
@@ -113,15 +112,15 @@
113
112
  "react-dom": "^19.0.0"
114
113
  },
115
114
  "engines": {
116
- "node": "^24.0.0",
115
+ "node": "^24.11.0",
117
116
  "pnpm": "^11.0.0"
118
117
  },
119
118
  "scripts": {
120
119
  "build": "tsgo --project tsconfig.build.json",
121
- "deps": "ncu -u",
120
+ "deps": "ncu -u --cooldown 7d",
122
121
  "docs:build": "node source/entrypoints/main.ts build",
123
122
  "docs:start": "node source/entrypoints/main.ts start",
124
- "format": "vp fmt",
123
+ "format": "vp fmt --no-error-on-unmatched-pattern",
125
124
  "leaks": "gitleaks git --config=.gitleaks.toml --verbose --redact",
126
125
  "lint": "vp fmt --check && vp lint",
127
126
  "release": "pnpm publish --access public --ignore-scripts --no-git-checks",
@@ -130,7 +129,6 @@
130
129
  "test": "pnpm lint && pnpm type && pnpm unit",
131
130
  "type": "tsgo --noEmit",
132
131
  "unit": "vp test",
133
- "vers": "pnpm version --no-git-tag-version",
134
132
  "vulns": "semgrep --config=p/typescript --config=p/nodejs --config=p/owasp-top-ten --config=p/secrets --exclude=\"**/fixtures/**\" --metrics=off"
135
133
  }
136
134
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto"
1
2
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
2
3
  import { dirname, join, resolve } from "node:path"
3
4
  import slugify from "@sindresorhus/slugify"
@@ -42,21 +43,31 @@ const stringifier = unified().use(remarkStringify, {
42
43
  emphasis: "_",
43
44
  })
44
45
 
45
- /** Glob (absolute path) covering all cached changelog markdown files for
46
- * a given consumer. Used by content-collections to include them in the
47
- * article scan. */
48
- export function cacheIncludeGlob(config: Config) {
49
- return join(targetDirFor(config.configPath), CACHE_SUBDIR, "**", "*.md")
46
+ /** Glob (absolute path) covering the cached changelog markdown files of
47
+ * one changelog section. Used by content-collections to include them in
48
+ * the article scan. */
49
+ export function cacheIncludeGlob(section: ChangelogSection, config: Config) {
50
+ return join(changelogCacheDir(section, config), "*.md")
50
51
  }
51
52
 
52
- /** Build the per-version changelog cache files. Livemark allows one
53
- * changelog per project, so all cache state lives in a single
54
- * `cache/changelog/`: the rendered `.md` entries plus a `meta.json`
55
- * holding the etag for conditional GETs. On refresh we wipe only the
56
- * `.md` files, leaving meta intact. */
53
+ /** Directory holding one changelog section's cache state: the rendered
54
+ * `.md` entries plus a `meta.json` holding the etag for conditional
55
+ * GETs. Each section gets its own subdirectory of `cache/changelog/`,
56
+ * keyed by its prefix's slug plus a hash of the raw prefix
57
+ * (`/python/changelog/` → `python-changelog-<hash>`), so several
58
+ * changelogs never overwrite each other's entries even when their
59
+ * prefixes slugify alike. */
60
+ export function changelogCacheDir(section: ChangelogSection, config: Config) {
61
+ const slug = slugify(section.prefix) || "index"
62
+ const hash = createHash("sha256").update(section.prefix).digest("hex").slice(0, 8)
63
+ return join(targetDirFor(config.configPath), CACHE_SUBDIR, `${slug}-${hash}`)
64
+ }
65
+
66
+ /** Build the per-version changelog cache files of one changelog section
67
+ * into its own cache directory. On refresh we wipe only that
68
+ * directory's `.md` files, leaving meta intact. */
57
69
  export async function buildChangelog(section: ChangelogSection, config: Config) {
58
- const targetDir = targetDirFor(config.configPath)
59
- const cacheDir = join(targetDir, CACHE_SUBDIR)
70
+ const cacheDir = changelogCacheDir(section, config)
60
71
  const metaPath = join(cacheDir, "meta.json")
61
72
 
62
73
  const entries = isGitHubUrl(section.source)
@@ -137,10 +148,12 @@ function processBody(body: string) {
137
148
  return escapeMdxAngles(stringifier.stringify(tree).trimEnd())
138
149
  }
139
150
 
140
- /** Escape `<` to `\<` outside fenced code blocks and inline backtick spans
151
+ /** Escape `<` to `&lt;` outside fenced code blocks and inline backtick spans
141
152
  * so commit messages with placeholders like `<subdir>` don't get parsed as
142
- * JSX by MDX. */
143
- function escapeMdxAngles(markdown: string) {
153
+ * JSX by MDX. The entity is used rather than a `\<` backslash escape because
154
+ * MDX's JSX-aware parser does not honour the backslash and still reads
155
+ * `\<name>` as an unclosed tag. */
156
+ export function escapeMdxAngles(markdown: string) {
144
157
  let inFence = false
145
158
  return markdown
146
159
  .split("\n")
@@ -150,7 +163,7 @@ function escapeMdxAngles(markdown: string) {
150
163
  return line
151
164
  }
152
165
  if (inFence) return line
153
- return line.replace(/`[^`]*`|</g, match => (match === "<" ? "\\<" : match))
166
+ return line.replace(/`[^`]*`|</g, match => (match === "<" ? "&lt;" : match))
154
167
  })
155
168
  .join("\n")
156
169
  }
@@ -1,5 +1,159 @@
1
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
2
+ import { basename, dirname, join } from "node:path"
3
+ import { temporaryDirectoryTask } from "tempy"
1
4
  import { describe, expect, it } from "vite-plus/test"
2
- import { splitLocalChangelog } from "./build.ts"
5
+ import { partitionBySection } from "../../helpers/section.ts"
6
+ import { Config } from "../../models/config.ts"
7
+ import type { ChangelogSection } from "../../models/section.ts"
8
+ import { targetDirFor } from "../target/build.ts"
9
+ import {
10
+ buildChangelog,
11
+ cacheIncludeGlob,
12
+ changelogCacheDir,
13
+ escapeMdxAngles,
14
+ splitLocalChangelog,
15
+ } from "./build.ts"
16
+
17
+ describe("buildChangelog", () => {
18
+ it("keeps several changelog sections apart when they share a version", () =>
19
+ temporaryDirectoryTask(async root => {
20
+ const configPath = join(root, "livemark.config.ts")
21
+ const standard: ChangelogSection = {
22
+ type: "changelog",
23
+ title: "Changelog",
24
+ prefix: "/changelog/",
25
+ source: "standard/CHANGELOG.md",
26
+ version: true,
27
+ position: "sidebar",
28
+ }
29
+ const python: ChangelogSection = {
30
+ type: "changelog",
31
+ title: "Changelog",
32
+ prefix: "/python/changelog/",
33
+ source: "python/CHANGELOG.md",
34
+ version: true,
35
+ position: "sidebar",
36
+ }
37
+ const config = makeConfig(root, [standard, python])
38
+
39
+ await writeChangelog(
40
+ root,
41
+ "standard",
42
+ "## 0.2.0\n\nStandard two.\n\n## 0.1.0\n\nStandard one.\n",
43
+ )
44
+ await writeChangelog(root, "python", "## 0.2.0\n\nPython two.\n")
45
+
46
+ try {
47
+ await buildChangelog(standard, config)
48
+ await buildChangelog(python, config)
49
+
50
+ const standardDir = changelogCacheDir(standard, config)
51
+ const pythonDir = changelogCacheDir(python, config)
52
+ const cacheRoot = join(targetDirFor(configPath), "cache/changelog")
53
+ expect(dirname(standardDir)).toBe(cacheRoot)
54
+ expect(dirname(pythonDir)).toBe(cacheRoot)
55
+ expect(basename(standardDir)).toMatch(/^changelog-[0-9a-f]{8}$/)
56
+ expect(basename(pythonDir)).toMatch(/^python-changelog-[0-9a-f]{8}$/)
57
+ expect(cacheIncludeGlob(python, config)).toBe(join(pythonDir, "*.md"))
58
+
59
+ expect((await readdir(standardDir)).sort()).toEqual(["0-1-0.md", "0-2-0.md"])
60
+ expect(await readdir(pythonDir)).toEqual(["0-2-0.md"])
61
+
62
+ const standardEntry = await readFile(join(standardDir, "0-2-0.md"), "utf-8")
63
+ const pythonEntry = await readFile(join(pythonDir, "0-2-0.md"), "utf-8")
64
+ expect(standardEntry).toContain("path: /changelog/0-2-0/")
65
+ expect(standardEntry).toContain("Standard two.")
66
+ expect(pythonEntry).toContain("path: /python/changelog/0-2-0/")
67
+ expect(pythonEntry).toContain("Python two.")
68
+
69
+ const olderEntry = await readFile(join(standardDir, "0-1-0.md"), "utf-8")
70
+ const articles = [standardEntry, olderEntry, pythonEntry].map(markdown => ({
71
+ path: markdown.match(/^path: (.+)$/m)?.[1] ?? "",
72
+ }))
73
+ const buckets = partitionBySection(articles, [standard, python])
74
+ expect(buckets.get("/changelog/")?.map(a => a.path)).toEqual([
75
+ "/changelog/0-2-0/",
76
+ "/changelog/0-1-0/",
77
+ ])
78
+ expect(buckets.get("/python/changelog/")?.map(a => a.path)).toEqual([
79
+ "/python/changelog/0-2-0/",
80
+ ])
81
+ } finally {
82
+ await rm(targetDirFor(configPath), { recursive: true, force: true })
83
+ }
84
+ }))
85
+
86
+ it("cleans only its own cache directory on rebuild", () =>
87
+ temporaryDirectoryTask(async root => {
88
+ const configPath = join(root, "livemark.config.ts")
89
+ const standard: ChangelogSection = {
90
+ type: "changelog",
91
+ title: "Changelog",
92
+ prefix: "/changelog/",
93
+ source: "standard/CHANGELOG.md",
94
+ position: "sidebar",
95
+ }
96
+ const python: ChangelogSection = {
97
+ type: "changelog",
98
+ title: "Changelog",
99
+ prefix: "/python/changelog/",
100
+ source: "python/CHANGELOG.md",
101
+ position: "sidebar",
102
+ }
103
+ const config = makeConfig(root, [standard, python])
104
+
105
+ await writeChangelog(root, "standard", "## 0.2.0\n\nStandard two.\n")
106
+ await writeChangelog(root, "python", "## 0.3.0\n\nPython three.\n")
107
+
108
+ try {
109
+ await buildChangelog(standard, config)
110
+ await buildChangelog(python, config)
111
+ await writeChangelog(root, "standard", "## 0.4.0\n\nStandard four.\n")
112
+ await buildChangelog(standard, config)
113
+
114
+ expect(await readdir(changelogCacheDir(standard, config))).toEqual(["0-4-0.md"])
115
+ expect(await readdir(changelogCacheDir(python, config))).toEqual(["0-3-0.md"])
116
+ } finally {
117
+ await rm(targetDirFor(configPath), { recursive: true, force: true })
118
+ }
119
+ }))
120
+
121
+ it("keeps sections apart when their prefixes slugify to the same key", () =>
122
+ temporaryDirectoryTask(async root => {
123
+ const configPath = join(root, "livemark.config.ts")
124
+ const nested: ChangelogSection = {
125
+ type: "changelog",
126
+ title: "Changelog",
127
+ prefix: "/python/changelog/",
128
+ source: "nested/CHANGELOG.md",
129
+ position: "sidebar",
130
+ }
131
+ const flat: ChangelogSection = {
132
+ type: "changelog",
133
+ title: "Changelog",
134
+ prefix: "/python-changelog/",
135
+ source: "flat/CHANGELOG.md",
136
+ position: "sidebar",
137
+ }
138
+ const config = makeConfig(root, [nested, flat])
139
+
140
+ await writeChangelog(root, "nested", "## 0.1.0\n\nNested one.\n")
141
+ await writeChangelog(root, "flat", "## 0.2.0\n\nFlat two.\n")
142
+
143
+ try {
144
+ await buildChangelog(nested, config)
145
+ await buildChangelog(flat, config)
146
+
147
+ const nestedDir = changelogCacheDir(nested, config)
148
+ const flatDir = changelogCacheDir(flat, config)
149
+ expect(nestedDir).not.toBe(flatDir)
150
+ expect(await readdir(nestedDir)).toEqual(["0-1-0.md"])
151
+ expect(await readdir(flatDir)).toEqual(["0-2-0.md"])
152
+ } finally {
153
+ await rm(targetDirFor(configPath), { recursive: true, force: true })
154
+ }
155
+ }))
156
+ })
3
157
 
4
158
  describe("splitLocalChangelog", () => {
5
159
  it("splits by h2 headings", () => {
@@ -67,3 +221,39 @@ Release notes.
67
221
  expect(entries[0]?.date).toBeUndefined()
68
222
  })
69
223
  })
224
+
225
+ describe("escapeMdxAngles", () => {
226
+ it("escapes a bare angle placeholder as an HTML entity", () => {
227
+ // `\<` is honoured by remark but NOT by MDX's JSX-aware parser, which still
228
+ // reads `\<name>` as an unclosed tag. Only the entity survives both.
229
+ expect(escapeMdxAngles("a <name> b")).toBe("a &lt;name> b")
230
+ })
231
+
232
+ it("escapes a placeholder that follows a bare URL", () => {
233
+ const input = "See https://fairspec.org/profiles/latest/<name>.json, ok."
234
+ expect(escapeMdxAngles(input)).toBe(
235
+ "See https://fairspec.org/profiles/latest/&lt;name>.json, ok.",
236
+ )
237
+ })
238
+
239
+ it("leaves inline code spans alone", () => {
240
+ expect(escapeMdxAngles("use `--resource <name>` here")).toBe(
241
+ "use `--resource <name>` here",
242
+ )
243
+ })
244
+
245
+ it("leaves fenced code blocks alone", () => {
246
+ const input = "```sh\nfoo <bar>\n```"
247
+ expect(escapeMdxAngles(input)).toBe(input)
248
+ })
249
+ })
250
+
251
+ function makeConfig(root: string, sections: ChangelogSection[]) {
252
+ const configPath = join(root, "livemark.config.ts")
253
+ return { ...Config.parse({ root, configPath, sections }), base: undefined }
254
+ }
255
+
256
+ async function writeChangelog(root: string, dir: string, body: string) {
257
+ await mkdir(join(root, dir), { recursive: true })
258
+ await writeFile(join(root, dir, "CHANGELOG.md"), body, "utf-8")
259
+ }
@@ -1,5 +1,5 @@
1
1
  import { AlertTriangle, Flame, Info, Lightbulb } from "lucide-react"
2
- import { cn } from "../utils/style.ts"
2
+ import { cn } from "cn"
3
3
 
4
4
  type CalloutType = "note" | "tip" | "info" | "warning" | "danger"
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { buttonVariants } from "../elements/button.tsx"
2
- import { cn } from "../utils/style.ts"
2
+ import { cn } from "cn"
3
3
 
4
4
  /** Styled link button rendered from ::button leaf directives */
5
5
  export function LinkButton(props: {
@@ -1,4 +1,4 @@
1
- import { cn } from "../utils/style.ts"
1
+ import { cn } from "cn"
2
2
  import { useInView } from "../hooks/in-view.ts"
3
3
 
4
4
  export function Section(props: {
@@ -1,4 +1,4 @@
1
- import { cn } from "../utils/style.ts"
1
+ import { cn } from "cn"
2
2
 
3
3
  export function SectionHeader(props: {
4
4
  label?: string
@@ -59,9 +59,10 @@ for (const section of changelogSections) {
59
59
  }
60
60
 
61
61
  const configInclude = Array.isArray(config.include) ? config.include : [config.include]
62
- const include = changelogSections.length
63
- ? [...configInclude, cacheIncludeGlob(config)]
64
- : configInclude
62
+ const include = [
63
+ ...configInclude,
64
+ ...changelogSections.map(section => cacheIncludeGlob(section, config)),
65
+ ]
65
66
 
66
67
  const userExclude = config.exclude
67
68
  ? Array.isArray(config.exclude)
@@ -1,7 +1,7 @@
1
1
  import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
2
2
  import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
3
3
 
4
- import { cn } from "../utils/style.ts"
4
+ import { cn } from "cn"
5
5
 
6
6
  function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
7
7
  return (
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
3
3
 
4
- import { cn } from "../utils/style.ts"
4
+ import { cn } from "cn"
5
5
  import { Button } from "./button.tsx"
6
6
 
7
7
  function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
@@ -1,5 +1,5 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority"
2
- import { cn } from "../utils/style.ts"
2
+ import { cn } from "cn"
3
3
 
4
4
  const alertVariants = cva(
5
5
  "grid gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert",
@@ -1,4 +1,4 @@
1
- import { cn } from "../utils/style.ts"
1
+ import { cn } from "cn"
2
2
 
3
3
  function AspectRatio({
4
4
  ratio,
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
3
3
 
4
- import { cn } from "../utils/style.ts"
4
+ import { cn } from "cn"
5
5
 
6
6
  function Avatar({
7
7
  className,
@@ -1,7 +1,7 @@
1
1
  import { mergeProps } from "@base-ui/react/merge-props"
2
2
  import { useRender } from "@base-ui/react/use-render"
3
3
  import { cva, type VariantProps } from "class-variance-authority"
4
- import { cn } from "../utils/style.ts"
4
+ import { cn } from "cn"
5
5
 
6
6
  const badgeVariants = cva(
7
7
  "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge",
@@ -1,7 +1,7 @@
1
1
  import { mergeProps } from "@base-ui/react/merge-props"
2
2
  import { useRender } from "@base-ui/react/use-render"
3
3
  import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
4
- import { cn } from "../utils/style.ts"
4
+ import { cn } from "cn"
5
5
 
6
6
  function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
7
7
  return (
@@ -2,7 +2,7 @@ import { mergeProps } from "@base-ui/react/merge-props"
2
2
  import { useRender } from "@base-ui/react/use-render"
3
3
  import { cva, type VariantProps } from "class-variance-authority"
4
4
 
5
- import { cn } from "../utils/style.ts"
5
+ import { cn } from "cn"
6
6
  import { Separator } from "./separator.tsx"
7
7
 
8
8
  const buttonGroupVariants = cva(
@@ -1,6 +1,6 @@
1
1
  import { Button as ButtonPrimitive } from "@base-ui/react/button"
2
2
  import { cva, type VariantProps } from "class-variance-authority"
3
- import { cn } from "../utils/style.ts"
3
+ import { cn } from "cn"
4
4
 
5
5
  const buttonVariants = cva(
6
6
  "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
@@ -7,7 +7,7 @@ import {
7
7
  } from "react-day-picker"
8
8
  import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
9
9
 
10
- import { cn } from "../utils/style.ts"
10
+ import { cn } from "cn"
11
11
  import { Button, buttonVariants } from "./button.tsx"
12
12
 
13
13
  function Calendar({
@@ -1,5 +1,5 @@
1
1
  import type * as React from "react"
2
- import { cn } from "../utils/style.ts"
2
+ import { cn } from "cn"
3
3
 
4
4
  function Card({
5
5
  className,
@@ -2,7 +2,7 @@ import * as React from "react"
2
2
  import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react"
3
3
  import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
4
4
 
5
- import { cn } from "../utils/style.ts"
5
+ import { cn } from "cn"
6
6
  import { Button } from "./button.tsx"
7
7
 
8
8
  type CarouselApi = UseEmblaCarouselType[1]
@@ -2,7 +2,7 @@ import * as React from "react"
2
2
  import * as RechartsPrimitive from "recharts"
3
3
  import type { TooltipValueType } from "recharts"
4
4
 
5
- import { cn } from "../utils/style.ts"
5
+ import { cn } from "cn"
6
6
 
7
7
  // Format: { THEME_NAME: CSS_SELECTOR }
8
8
  const THEMES = { light: "", dark: ".dark" } as const
@@ -1,7 +1,7 @@
1
1
  import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
2
2
  import { CheckIcon } from "lucide-react"
3
3
 
4
- import { cn } from "../utils/style.ts"
4
+ import { cn } from "cn"
5
5
 
6
6
  function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
7
7
  return (
@@ -2,7 +2,7 @@ import * as React from "react"
2
2
  import { Command as CommandPrimitive } from "cmdk"
3
3
  import { CheckIcon, SearchIcon, XIcon } from "lucide-react"
4
4
 
5
- import { cn } from "../utils/style.ts"
5
+ import { cn } from "cn"
6
6
  import {
7
7
  Dialog,
8
8
  DialogContent,
@@ -2,7 +2,7 @@ import * as React from "react"
2
2
  import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
3
3
  import { CheckIcon, ChevronRightIcon } from "lucide-react"
4
4
 
5
- import { cn } from "../utils/style.ts"
5
+ import { cn } from "cn"
6
6
 
7
7
  function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
8
8
  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
@@ -1,6 +1,6 @@
1
1
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
2
2
  import { XIcon } from "lucide-react"
3
- import { cn } from "../utils/style.ts"
3
+ import { cn } from "cn"
4
4
  import { Button } from "./button.tsx"
5
5
 
6
6
  function Dialog({ ...props }: DialogPrimitive.Root.Props) {
@@ -1,5 +1,5 @@
1
1
  import { Drawer as DrawerPrimitive } from "vaul"
2
- import { cn } from "../utils/style.ts"
2
+ import { cn } from "cn"
3
3
 
4
4
  function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
5
5
  return <DrawerPrimitive.Root data-slot="drawer" {...props} />
@@ -1,6 +1,6 @@
1
1
  import { Menu as MenuPrimitive } from "@base-ui/react/menu"
2
2
  import { CheckIcon, ChevronRightIcon } from "lucide-react"
3
- import { cn } from "../utils/style.ts"
3
+ import { cn } from "cn"
4
4
 
5
5
  function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
6
6
  return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
@@ -1,6 +1,6 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority"
2
2
 
3
- import { cn } from "../utils/style.ts"
3
+ import { cn } from "cn"
4
4
 
5
5
  function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
6
  return (
@@ -1,6 +1,6 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority"
2
2
  import { useMemo } from "react"
3
- import { cn } from "../utils/style.ts"
3
+ import { cn } from "cn"
4
4
  import { Label } from "./label.tsx"
5
5
  import { Separator } from "./separator.tsx"
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"
2
2
 
3
- import { cn } from "../utils/style.ts"
3
+ import { cn } from "cn"
4
4
 
5
5
  function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
6
6
  return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />