@readme/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/bin/git-format.js +80 -0
  2. package/bin/readme.js +1 -1
  3. package/package.json +54 -26
  4. package/src/bootstrap.js +9 -13
  5. package/src/cli.js +17 -11
  6. package/src/commands/components.js +35 -18
  7. package/src/commands/dev.js +12 -8
  8. package/src/commands/eyes.js +12 -8
  9. package/src/commands/import.js +2200 -2049
  10. package/src/commands/lint.js +33 -29
  11. package/src/commands/oas-bundle.js +6 -2
  12. package/src/commands/oas-sync.js +271 -161
  13. package/src/commands/oas-validate.js +10 -7
  14. package/src/commands/pretty.js +188 -165
  15. package/src/commands/setup.js +24 -26
  16. package/src/commands/versions.js +6 -2
  17. package/src/dev/app/[...slug]/page.js +1 -0
  18. package/src/dev/app/layout.js +18 -11
  19. package/src/dev/app/page.js +1 -0
  20. package/src/dev/lib/docs.js +27 -19
  21. package/src/dev/next.config.mjs +2 -2
  22. package/src/gha.js +7 -7
  23. package/src/prompts/index.js +16 -6
  24. package/src/utils/containment.js +87 -0
  25. package/src/utils/eyes.js +143 -51
  26. package/src/utils/git.js +3 -3
  27. package/src/utils/lint.js +39 -38
  28. package/src/utils/llms.js +94 -91
  29. package/src/utils/markdown.js +256 -0
  30. package/src/utils/marketplace.js +12 -5
  31. package/src/utils/multi-spinner.js +5 -7
  32. package/src/utils/promise-pool.js +7 -5
  33. package/src/utils/reporter.js +88 -94
  34. package/src/utils/setup-templates.js +12 -6
  35. package/src/utils/styles.js +2 -1
  36. package/src/utils/tamagotchi.js +257 -91
  37. package/src/utils/tips.js +18 -9
  38. package/src/utils/url-segs.js +46 -41
  39. package/src/validators/components.js +55 -63
  40. package/src/validators/content.js +16 -14
  41. package/src/validators/duplicates.js +6 -2
  42. package/src/validators/frontmatter.js +21 -29
  43. package/src/validators/links.js +4 -8
  44. package/src/validators/oas-reference.js +25 -18
  45. package/src/validators/oas-schema.js +2 -0
  46. package/src/validators/ordering.js +5 -5
  47. package/src/validators/recipes.js +4 -12
  48. package/vendor/git-format/CLAUDE.md +352 -0
  49. package/vendor/git-format/frontmatter.schema.json +199 -0
  50. package/src/commands/import-oas.test.js +0 -277
  51. package/src/commands/import.classify.test.js +0 -172
  52. package/src/commands/import.landing.test.js +0 -217
  53. package/src/commands/import.slug-planner.test.js +0 -86
  54. package/src/commands/import.staging.test.js +0 -230
  55. package/src/commands/import.test.js +0 -382
  56. package/src/utils/llms.test.js +0 -302
  57. package/src/utils/url-segs.test.js +0 -239
@@ -1,217 +0,0 @@
1
- import { afterEach, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { __test__ } from './import.js'
4
-
5
- const {
6
- injectSectionLandingPages,
7
- parseMetaRefreshTarget,
8
- parseHtmlCanonical,
9
- } = __test__
10
-
11
- const originalFetch = globalThis.fetch
12
-
13
- afterEach(() => {
14
- globalThis.fetch = originalFetch
15
- })
16
-
17
- function mockLandingFetch(routes) {
18
- globalThis.fetch = async (url) => {
19
- const href = String(url)
20
- const entry = routes[href]
21
- if (!entry) return { ok: false, status: 404, url: href, text: async () => '' }
22
- if (entry instanceof Error) throw entry
23
- return {
24
- ok: entry.ok !== false,
25
- status: entry.status ?? 200,
26
- url: entry.finalUrl || href,
27
- text: async () => entry.body || '',
28
- }
29
- }
30
- }
31
-
32
- test('parseMetaRefreshTarget resolves relative, quoted, and entity-encoded targets', () => {
33
- const base = 'https://example.com/docs/guides/'
34
- assert.equal(
35
- parseMetaRefreshTarget('<meta http-equiv="refresh" content="0;url=/docs/guides/intro">', base),
36
- 'https://example.com/docs/guides/intro',
37
- )
38
- assert.equal(
39
- parseMetaRefreshTarget("<meta http-equiv='refresh' content='0; url=intro'>", base),
40
- 'https://example.com/docs/guides/intro',
41
- )
42
- assert.equal(
43
- parseMetaRefreshTarget('<meta http-equiv="refresh" content="0;url=/docs/guides?tab=js&amp;x=1">', base),
44
- 'https://example.com/docs/guides?tab=js&x=1',
45
- )
46
- assert.equal(parseMetaRefreshTarget('<html><title>Guides</title></html>', base), null)
47
- assert.equal(parseMetaRefreshTarget('', base), null)
48
- assert.equal(parseMetaRefreshTarget('<meta http-equiv="refresh" content="5">', base), null)
49
- })
50
-
51
- test('parseHtmlCanonical resolves relative and entity-encoded hrefs', () => {
52
- const base = 'https://example.com/docs/guides/'
53
- assert.equal(
54
- parseHtmlCanonical('<link rel="canonical" href="/docs/guides/intro">', base),
55
- 'https://example.com/docs/guides/intro',
56
- )
57
- assert.equal(
58
- parseHtmlCanonical("<link rel='canonical' href='intro'>", base),
59
- 'https://example.com/docs/guides/intro',
60
- )
61
- assert.equal(
62
- parseHtmlCanonical('<link rel="canonical" href="/docs/guides?tab=js&amp;x=1">', base),
63
- 'https://example.com/docs/guides?tab=js&x=1',
64
- )
65
- assert.equal(parseHtmlCanonical('<html></html>', base), null)
66
- assert.equal(parseHtmlCanonical('', base), null)
67
- })
68
-
69
- test('injectSectionLandingPages prepends a real common-prefix landing page', async () => {
70
- mockLandingFetch({
71
- 'https://example.com/docs/guides': { body: '<title>Guides</title>' },
72
- })
73
- const organized = {
74
- categories: [
75
- {
76
- title: 'Guides',
77
- pages: [
78
- { title: 'Intro', url: 'https://example.com/docs/guides/intro' },
79
- { title: 'Install', url: 'https://example.com/docs/guides/install' },
80
- ],
81
- },
82
- ],
83
- }
84
-
85
- await injectSectionLandingPages(organized, 'https://example.com/docs')
86
-
87
- assert.equal(organized.categories[0].pages[0].url, 'https://example.com/docs/guides')
88
- assert.equal(organized.categories[0].pages[0].title, 'Guides')
89
- assert.equal(organized.categories[0].pages.length, 3)
90
- })
91
-
92
- test('injectSectionLandingPages skips client-redirect stub landings via meta refresh', async () => {
93
- mockLandingFetch({
94
- 'https://example.com/docs/guides': {
95
- body: '<meta http-equiv="refresh" content="0;url=/docs/guides/intro">',
96
- },
97
- })
98
- const organized = {
99
- categories: [
100
- {
101
- title: 'Guides',
102
- pages: [
103
- { title: 'Intro', url: 'https://example.com/docs/guides/intro' },
104
- { title: 'Install', url: 'https://example.com/docs/guides/install' },
105
- ],
106
- },
107
- ],
108
- }
109
-
110
- await injectSectionLandingPages(organized, 'https://example.com/docs')
111
- assert.deepEqual(
112
- organized.categories[0].pages.map((p) => p.url),
113
- ['https://example.com/docs/guides/intro', 'https://example.com/docs/guides/install'],
114
- )
115
- })
116
-
117
- test('injectSectionLandingPages skips landings whose canonical points at a known child', async () => {
118
- mockLandingFetch({
119
- 'https://example.com/docs/guides': {
120
- body: '<link rel="canonical" href="/docs/guides/intro">',
121
- },
122
- })
123
- const organized = {
124
- categories: [
125
- {
126
- title: 'Guides',
127
- pages: [
128
- { title: 'Intro', url: 'https://example.com/docs/guides/intro' },
129
- { title: 'Install', url: 'https://example.com/docs/guides/install' },
130
- ],
131
- },
132
- ],
133
- }
134
-
135
- await injectSectionLandingPages(organized, 'https://example.com/docs')
136
- assert.equal(organized.categories[0].pages.length, 2)
137
- })
138
-
139
- test('injectSectionLandingPages skips landings that HTTP-redirect onto a known child', async () => {
140
- mockLandingFetch({
141
- 'https://example.com/docs/guides': {
142
- finalUrl: 'https://example.com/docs/guides/intro',
143
- body: '<title>Intro</title>',
144
- },
145
- })
146
- const organized = {
147
- categories: [
148
- {
149
- title: 'Guides',
150
- pages: [
151
- { title: 'Intro', url: 'https://example.com/docs/guides/intro' },
152
- { title: 'Install', url: 'https://example.com/docs/guides/install' },
153
- ],
154
- },
155
- ],
156
- }
157
-
158
- await injectSectionLandingPages(organized, 'https://example.com/docs')
159
- assert.equal(organized.categories[0].pages.length, 2)
160
- })
161
-
162
- test('injectSectionLandingPages does not probe the import base or categories with a single child', async () => {
163
- const seen = []
164
- globalThis.fetch = async (url) => {
165
- seen.push(String(url))
166
- return { ok: true, status: 200, url: String(url), text: async () => '<title>Docs</title>' }
167
- }
168
- const organized = {
169
- categories: [
170
- {
171
- title: 'Docs',
172
- pages: [
173
- { title: 'A', url: 'https://example.com/docs/a' },
174
- { title: 'B', url: 'https://example.com/docs/b' },
175
- ],
176
- },
177
- {
178
- title: 'Lonely',
179
- pages: [{ title: 'Only', url: 'https://example.com/docs/guides/only' }],
180
- },
181
- ],
182
- }
183
-
184
- await injectSectionLandingPages(organized, 'https://example.com/docs')
185
- assert.deepEqual(seen, [])
186
- assert.equal(organized.categories[0].pages.length, 2)
187
- assert.equal(organized.categories[1].pages.length, 1)
188
- })
189
-
190
- test('injectSectionLandingPages ignores 404s and fetch failures', async () => {
191
- mockLandingFetch({
192
- 'https://example.com/docs/guides': new Error('network down'),
193
- 'https://example.com/docs/api': { ok: false, status: 404, body: '' },
194
- })
195
- const organized = {
196
- categories: [
197
- {
198
- title: 'Guides',
199
- pages: [
200
- { title: 'Intro', url: 'https://example.com/docs/guides/intro' },
201
- { title: 'Install', url: 'https://example.com/docs/guides/install' },
202
- ],
203
- },
204
- {
205
- title: 'API',
206
- pages: [
207
- { title: 'Auth', url: 'https://example.com/docs/api/auth' },
208
- { title: 'Errors', url: 'https://example.com/docs/api/errors' },
209
- ],
210
- },
211
- ],
212
- }
213
-
214
- await injectSectionLandingPages(organized, 'https://example.com/docs')
215
- assert.equal(organized.categories[0].pages.length, 2)
216
- assert.equal(organized.categories[1].pages.length, 2)
217
- })
@@ -1,86 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { ensureUniqueSlugs } from './import.js'
4
-
5
- function assignedSlug(categories, page) {
6
- return ensureUniqueSlugs(categories).get(page)
7
- }
8
-
9
- test('ensureUniqueSlugs: generic docs terminal paths fall back to introduction', () => {
10
- for (const segment of ['doc', 'docs', 'documentation', 'documentations']) {
11
- const page = { title: segment, url: `https://example.com/${segment}` }
12
- assert.equal(
13
- assignedSlug([{ title: 'Documentation', pages: [page] }], page),
14
- 'introduction',
15
- )
16
- }
17
- })
18
-
19
- test('ensureUniqueSlugs: terminal generic docs paths use introduction under non-generic categories', () => {
20
- const page = { title: 'Docs', url: 'https://example.com/docs' }
21
-
22
- assert.equal(
23
- assignedSlug([{ title: 'Guides', pages: [page] }], page),
24
- 'introduction',
25
- )
26
- })
27
-
28
- test('ensureUniqueSlugs: category segment disambiguates introduction collisions', () => {
29
- const guidesPage = { title: 'Docs', url: 'https://example.com/docs' }
30
- const apiPage = { title: 'Docs', url: 'https://example.com/docs' }
31
- const slugs = ensureUniqueSlugs([
32
- { title: 'Guides', pages: [guidesPage] },
33
- { title: 'API', pages: [apiPage] },
34
- ])
35
-
36
- assert.equal(slugs.get(guidesPage), 'guides-introduction')
37
- assert.equal(slugs.get(apiPage), 'api-introduction')
38
- })
39
-
40
- test('ensureUniqueSlugs: generic docs category does not prefix an available page slug', () => {
41
- const page = { title: 'Getting Started', url: 'https://example.com/docs/getting-started' }
42
-
43
- assert.equal(
44
- assignedSlug([{ title: 'Docs', pages: [page] }], page),
45
- 'getting-started',
46
- )
47
- })
48
-
49
- test('ensureUniqueSlugs: generic docs categories use existing numeric suffix fallback for irreducible collisions', () => {
50
- const first = { title: 'Getting Started', url: 'https://example.com/docs/getting-started' }
51
- const second = { title: 'Getting Started', url: 'https://example.com/documentation/getting-started' }
52
- const slugs = ensureUniqueSlugs([
53
- { title: 'Docs', pages: [first] },
54
- { title: 'Documentation', pages: [second] },
55
- ])
56
-
57
- assert.equal(slugs.get(first), 'getting-started')
58
- assert.equal(slugs.get(second), 'getting-started-2')
59
- })
60
-
61
- test('ensureUniqueSlugs: generic docs category is assigned before non-generic categories', () => {
62
- const docsPage = { title: 'Getting Started', url: 'https://example.com/docs/getting-started' }
63
- const aiPage = { title: 'Getting Started', url: 'https://example.com/ai/getting-started' }
64
- const slugs = ensureUniqueSlugs([
65
- { title: 'AI', pages: [aiPage] },
66
- { title: 'Docs', pages: [docsPage] },
67
- ])
68
-
69
- assert.equal(slugs.get(docsPage), 'getting-started')
70
- assert.equal(slugs.get(aiPage), 'ai-getting-started')
71
- })
72
-
73
- test('ensureUniqueSlugs: non-generic docs paths keep current base slug behavior', () => {
74
- const api = { title: 'API', url: 'https://example.com/docs/api' }
75
- const gettingStarted = { title: 'Getting Started', url: 'https://example.com/docs/getting-started' }
76
- const docsApi = { title: 'Docs API', url: 'https://example.com/docs-api' }
77
- const documentationSettings = { title: 'Documentation Settings', url: 'https://example.com/documentation-settings' }
78
- const slugs = ensureUniqueSlugs([
79
- { title: 'Guides', pages: [api, gettingStarted, docsApi, documentationSettings] },
80
- ])
81
-
82
- assert.equal(slugs.get(api), 'api')
83
- assert.equal(slugs.get(gettingStarted), 'getting-started')
84
- assert.equal(slugs.get(docsApi), 'docs-api')
85
- assert.equal(slugs.get(documentationSettings), 'documentation-settings')
86
- })
@@ -1,230 +0,0 @@
1
- import { afterEach, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import fs from 'node:fs'
4
- import os from 'node:os'
5
- import path from 'node:path'
6
- import matter from 'gray-matter'
7
- import { Command } from 'commander'
8
- import { args, __test__ } from './import.js'
9
-
10
- const { stageOrganized, finalizeChangelogs, allocateChangelogFilenames } = __test__
11
-
12
- const originalLog = console.log
13
- afterEach(() => {
14
- console.log = originalLog
15
- })
16
-
17
- function withStaging(fn) {
18
- const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'import-staging-'))
19
- console.log = () => {}
20
- try {
21
- return fn(stagingDir)
22
- } finally {
23
- fs.rmSync(stagingDir, { recursive: true, force: true })
24
- }
25
- }
26
-
27
- function readFm(filePath) {
28
- return matter(fs.readFileSync(filePath, 'utf8')).data
29
- }
30
-
31
- test('stageOrganized writes parent pages as index.md and leaves as slug.md', () => {
32
- withStaging((stagingDir) => {
33
- const parent = {
34
- title: 'Getting Started',
35
- url: 'https://example.com/docs/getting-started',
36
- pages: [{ title: 'Install', url: 'https://example.com/docs/getting-started/install' }],
37
- }
38
- const leaf = { title: 'Pricing', url: 'https://example.com/docs/pricing' }
39
-
40
- const result = stageOrganized(
41
- { categories: [{ title: 'Guides', pages: [parent, leaf] }] },
42
- stagingDir,
43
- )
44
-
45
- const parentIndex = path.join(stagingDir, 'docs', 'Guides', 'getting-started', 'index.md')
46
- const child = path.join(stagingDir, 'docs', 'Guides', 'getting-started', 'install.md')
47
- const leafFile = path.join(stagingDir, 'docs', 'Guides', 'pricing.md')
48
- const siblingParent = path.join(stagingDir, 'docs', 'Guides', 'getting-started.md')
49
-
50
- assert.equal(fs.existsSync(parentIndex), true)
51
- assert.equal(fs.existsSync(child), true)
52
- assert.equal(fs.existsSync(leafFile), true)
53
- assert.equal(fs.existsSync(siblingParent), false)
54
-
55
- assert.equal(readFm(parentIndex).title, 'Getting Started')
56
- assert.equal(readFm(parentIndex)['x-import'], 'https://example.com/docs/getting-started')
57
- assert.equal(readFm(child).title, 'Install')
58
- assert.equal(readFm(leafFile).title, 'Pricing')
59
-
60
- const guidesOrder = fs.readFileSync(path.join(stagingDir, 'docs', 'Guides', '_order.yaml'), 'utf8')
61
- assert.match(guidesOrder, /^- getting-started$/m)
62
- assert.match(guidesOrder, /^- pricing$/m)
63
- assert.doesNotMatch(guidesOrder, /^- index$/m)
64
-
65
- const nestedOrder = fs.readFileSync(
66
- path.join(stagingDir, 'docs', 'Guides', 'getting-started', '_order.yaml'),
67
- 'utf8',
68
- )
69
- assert.match(nestedOrder, /^- install$/m)
70
- assert.equal(result.fileCount, 3)
71
- })
72
- })
73
-
74
- test('stageOrganized rescues /api/docs pages when skipping API reference categories', () => {
75
- withStaging((stagingDir) => {
76
- const result = stageOrganized(
77
- {
78
- categories: [
79
- {
80
- title: 'API Reference',
81
- pages: [
82
- { title: 'List pets', url: 'https://example.com/api/reference/pets' },
83
- { title: 'Guides', url: 'https://example.com/api/docs/guides/intro' },
84
- {
85
- title: 'Mixed',
86
- pages: [{ title: 'Pricing', url: 'https://example.com/api/docs/pricing' }],
87
- },
88
- ],
89
- },
90
- ],
91
- },
92
- stagingDir,
93
- { skipApiReference: true },
94
- )
95
-
96
- assert.equal(result.skippedApiRef, 1)
97
- assert.equal(fs.existsSync(path.join(stagingDir, 'reference')), false)
98
-
99
- const rescuedDir = path.join(stagingDir, 'docs', 'API Docs')
100
- const guides = path.join(rescuedDir, 'intro.md')
101
- const pricing = path.join(rescuedDir, 'introduction', 'pricing.md')
102
- assert.equal(fs.existsSync(guides), true)
103
- assert.equal(fs.existsSync(pricing), true)
104
- assert.equal(readFm(guides)['x-import'], 'https://example.com/api/docs/guides/intro')
105
- assert.equal(readFm(pricing)['x-import'], 'https://example.com/api/docs/pricing')
106
- })
107
- })
108
-
109
- test('stageOrganized namespaces nested category folders that would otherwise share a basename', () => {
110
- withStaging((stagingDir) => {
111
- stageOrganized(
112
- {
113
- categories: [
114
- { title: 'Fundamentals', pages: [{ title: 'Hello', url: 'https://example.com/docs/hello' }] },
115
- { title: 'cockroach/Fundamentals', pages: [{ title: 'SQL', url: 'https://example.com/docs/sql' }] },
116
- ],
117
- },
118
- stagingDir,
119
- )
120
-
121
- const topLevel = path.join(stagingDir, 'docs', 'Fundamentals', 'hello.md')
122
- const nestedDir = path.join(stagingDir, 'docs', 'cockroach', 'cockroach-fundamentals')
123
- const nestedPage = path.join(nestedDir, 'sql.md')
124
- const nestedIndex = path.join(nestedDir, 'index.md')
125
-
126
- assert.equal(fs.existsSync(topLevel), true)
127
- assert.equal(fs.existsSync(nestedPage), true)
128
- assert.equal(fs.existsSync(nestedIndex), true)
129
- assert.equal(readFm(nestedIndex).title, 'Fundamentals')
130
- assert.equal('x-import' in readFm(nestedIndex), false)
131
-
132
- const docsOrder = fs.readFileSync(path.join(stagingDir, 'docs', '_order.yaml'), 'utf8')
133
- assert.match(docsOrder, /^- Fundamentals$/m)
134
- assert.match(docsOrder, /^- cockroach\/cockroach-fundamentals$/m)
135
- })
136
- })
137
-
138
- test('finalizeChangelogs emits flat canonical changelogs without an order file and suffixes case-insensitive collisions', () => {
139
- withStaging((stagingDir) => {
140
- const changelogDir = path.join(stagingDir, 'docs', 'Changelog')
141
- fs.mkdirSync(path.join(changelogDir, 'foo'), { recursive: true })
142
- fs.writeFileSync(
143
- path.join(changelogDir, 'foo', 'Bar.md'),
144
- matter.stringify('', { title: 'Nested', icon: 'fa-book', 'x-import': 'https://example.com/nested' }),
145
- )
146
- fs.writeFileSync(
147
- path.join(changelogDir, 'foo-bar.md'),
148
- matter.stringify('', { title: 'Root', icon: 'fa-book', 'x-import': 'https://example.com/root' }),
149
- )
150
- fs.writeFileSync(
151
- path.join(changelogDir, 'unchanged.md'),
152
- matter.stringify('', { title: 'Unchanged', icon: 'fa-book', 'x-import': 'https://example.com/unchanged' }),
153
- )
154
- fs.writeFileSync(path.join(stagingDir, 'docs', '_order.yaml'), '- Changelog\n')
155
-
156
- assert.equal(finalizeChangelogs(stagingDir), 3)
157
-
158
- const outputDir = path.join(stagingDir, 'changelogs')
159
- assert.deepEqual(fs.readdirSync(outputDir).sort(), ['foo-Bar.md', 'foo-bar-2.md', 'unchanged.md'])
160
- assert.equal(fs.existsSync(path.join(outputDir, '_order.yaml')), false)
161
- assert.equal(fs.existsSync(path.join(stagingDir, 'changelog')), false)
162
- assert.equal(fs.existsSync(path.join(stagingDir, 'docs', 'Changelog')), false)
163
- assert.equal(fs.existsSync(path.join(stagingDir, 'docs', '_order.yaml')), false)
164
- assert.equal(readFm(path.join(outputDir, 'foo-Bar.md')).icon, undefined)
165
- assert.equal(readFm(path.join(outputDir, 'foo-Bar.md'))['x-import'], 'https://example.com/nested')
166
- assert.equal(readFm(path.join(outputDir, 'foo-bar-2.md'))['x-import'], 'https://example.com/root')
167
- assert.equal(readFm(path.join(outputDir, 'unchanged.md'))['x-import'], 'https://example.com/unchanged')
168
- })
169
- })
170
-
171
- test('stageOrganized retains case-only changelog names before finalization', () => {
172
- withStaging((stagingDir) => {
173
- const upper = { title: 'Upper A', url: 'https://example.com/A' }
174
- const lower = { title: 'Lower a', url: 'https://example.com/a' }
175
- const independent = { title: 'Independent a-2', url: 'https://example.com/a-2' }
176
- stageOrganized(
177
- { categories: [{ title: 'Changelog', pages: [upper, lower, independent] }] },
178
- stagingDir,
179
- { slugFor: new Map([[upper, 'A'], [lower, 'a'], [independent, 'a-2']]) },
180
- )
181
- assert.equal(finalizeChangelogs(stagingDir), 3)
182
-
183
- const outputDir = path.join(stagingDir, 'changelogs')
184
- assert.deepEqual(fs.readdirSync(outputDir).sort(), ['A.md', 'a-2.md', 'a-3.md'])
185
- assert.equal(readFm(path.join(outputDir, 'A.md')).title, 'Upper A')
186
- assert.equal(readFm(path.join(outputDir, 'a-3.md')).title, 'Lower a')
187
- assert.equal(readFm(path.join(outputDir, 'a-2.md')).title, 'Independent a-2')
188
- })
189
- })
190
-
191
- test('allocateChangelogFilenames preserves an independent numeric-suffix filename', () => {
192
- assert.deepEqual(
193
- allocateChangelogFilenames([
194
- { ancestors: [], slug: 'A' },
195
- { ancestors: [], slug: 'a' },
196
- { ancestors: [], slug: 'a-2' },
197
- ]),
198
- ['A', 'a-3', 'a-2'],
199
- )
200
- })
201
-
202
- test('import command has no conditional changelog layout option', () => {
203
- const cmd = new Command()
204
- args(cmd)
205
-
206
- assert.equal(cmd.options.some((option) => option.long === '--separate-changelog'), false)
207
- })
208
-
209
- test('stageOrganized quotes YAML-unsafe slugs in _order.yaml', () => {
210
- withStaging((stagingDir) => {
211
- stageOrganized(
212
- {
213
- categories: [
214
- {
215
- title: 'Guides',
216
- pages: [
217
- { title: 'True', url: 'https://example.com/docs/true' },
218
- { title: 'Null', url: 'https://example.com/docs/null' },
219
- ],
220
- },
221
- ],
222
- },
223
- stagingDir,
224
- )
225
-
226
- const order = fs.readFileSync(path.join(stagingDir, 'docs', 'Guides', '_order.yaml'), 'utf8')
227
- assert.match(order, /^- "true"$/m)
228
- assert.match(order, /^- "null"$/m)
229
- })
230
- })