@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
@@ -0,0 +1,199 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "ReadMe frontmatter shapes",
4
+ "oneOf": [
5
+ {
6
+ "title": "Page",
7
+ "description": "This is the shape for files within the `docs/` directory.",
8
+ "$ref": "#/defs/filetype-page"
9
+ },
10
+ {
11
+ "title": "Custom Page",
12
+ "description": "This is the shape for files within the `custom_pages/` directory.",
13
+ "allOf": [
14
+ { "$ref": "#/defs/common-page" },
15
+ { "type": "object", "properties": { "fullscreen": { "type": ["boolean", "null"] } } }
16
+ ],
17
+ "required": ["title"],
18
+ "not": {
19
+ "type": "object",
20
+ "properties": { "deprecated": { "type": "boolean" } },
21
+ "description": "Custom pages have everything that a common page has except that they can't be deprecated."
22
+ }
23
+ },
24
+ {
25
+ "title": "API Reference",
26
+ "description": "This is the shape for files within the `reference/` directory.",
27
+ "allOf": [
28
+ { "$ref": "#/defs/filetype-page" },
29
+ {
30
+ "type": "object",
31
+ "properties": {
32
+ "api": {
33
+ "anyOf": [
34
+ {
35
+ "type": "object",
36
+ "properties": {
37
+ "file": { "type": "string" },
38
+ "operationId": { "type": "string" },
39
+ "webhook": { "type": ["boolean", "null"] }
40
+ },
41
+ "required": ["file", "operationId"]
42
+ },
43
+ { "type": "null" }
44
+ ],
45
+ "description": "Information about the API operation that this page is documenting. Setting this to `null` will detach it from the API operation and make it a standard page."
46
+ },
47
+ "api_config": {
48
+ "anyOf": [
49
+ { "type": "string", "enum": ["authentication", "getting-started", "my-requests"] },
50
+ { "type": "null" }
51
+ ],
52
+ "description": "Describes a Reference config page of a particular type."
53
+ },
54
+ "recipes": {
55
+ "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "null" }],
56
+ "description": "A collection of recipes that are related to this API operation."
57
+ }
58
+ }
59
+ }
60
+ ]
61
+ },
62
+ {
63
+ "title": "Recipe",
64
+ "description": "This is the shape for files within the `recipes/` directory.",
65
+ "allOf": [
66
+ { "$ref": "#/defs/core" },
67
+ {
68
+ "type": "object",
69
+ "properties": {
70
+ "description": { "type": ["string", "null"] },
71
+ "recipe": {
72
+ "type": "object",
73
+ "properties": {
74
+ "color": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" },
75
+ "icon": { "type": "string" }
76
+ },
77
+ "required": ["color", "icon"]
78
+ }
79
+ }
80
+ }
81
+ ],
82
+ "required": ["title", "recipe"]
83
+ },
84
+ {
85
+ "title": "Custom Block",
86
+ "description": "This is the shape for custom blocks in the `custom_blocks/` directory.",
87
+ "type": "object",
88
+ "properties": { "name": { "type": ["string", "null"] } },
89
+ "required": ["name"]
90
+ }
91
+ ],
92
+ "defs": {
93
+ "core": {
94
+ "title": "Core properties that are available on all file types.",
95
+ "type": "object",
96
+ "properties": { "hidden": { "type": ["boolean", "null"] }, "title": { "type": "string" } }
97
+ },
98
+ "metadata": {
99
+ "title": "Properties that control file metadata.",
100
+ "type": "object",
101
+ "properties": {
102
+ "description": { "type": ["string", "null"] },
103
+ "image": { "type": ["string", "null"] },
104
+ "keywords": { "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "null" }] },
105
+ "robots": { "anyOf": [{ "type": "string", "enum": ["index", "noindex"] }, { "type": "null" }] },
106
+ "title": { "type": ["string", "null"], "description": "This is the title that's used for SEO metadata." }
107
+ }
108
+ },
109
+ "common-page": {
110
+ "allOf": [
111
+ { "$ref": "#/defs/core" },
112
+ {
113
+ "type": "object",
114
+ "properties": {
115
+ "deprecated": { "type": ["boolean", "null"] },
116
+ "excerpt": { "type": ["string", "null"] },
117
+ "icon": { "type": ["string", "null"] },
118
+ "table_of_contents": { "type": ["boolean", "null"] },
119
+ "x-import": {
120
+ "type": ["string", "null"],
121
+ "description": "Set during the docs site import flow. This is a link to the external page that needs to be imported and signifies this page has not yet been imported. It is removed once the page content is imported."
122
+ },
123
+ "metadata": { "$ref": "#/defs/metadata" },
124
+ "next": {
125
+ "type": "object",
126
+ "properties": {
127
+ "description": { "type": ["string", "null"] },
128
+ "pages": {
129
+ "anyOf": [
130
+ {
131
+ "type": "array",
132
+ "items": {
133
+ "anyOf": [
134
+ {
135
+ "type": "object",
136
+ "properties": {
137
+ "slug": { "type": "string" },
138
+ "title": { "type": ["string", "null"] },
139
+ "type": { "type": "string", "enum": ["basic", "endpoint", "changelog", "custom_page"] }
140
+ },
141
+ "description": "A page within the developer site. Does not support multiguides and is generally deprecated in favor of `type: page`."
142
+ },
143
+ {
144
+ "type": "object",
145
+ "properties": {
146
+ "slug": { "type": "string" },
147
+ "directory": {
148
+ "type": "string",
149
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,29}$",
150
+ "description": "The section directory of the page."
151
+ },
152
+ "title": { "type": ["string", "null"] },
153
+ "type": { "type": "string", "const": "page" }
154
+ },
155
+ "description": "A page within the developer site. Multiguides compatible."
156
+ },
157
+ {
158
+ "type": "object",
159
+ "properties": {
160
+ "title": { "type": ["string", "null"] },
161
+ "type": { "type": "string", "const": "link" },
162
+ "url": { "type": "string" }
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ },
168
+ { "type": "null" }
169
+ ]
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ ]
176
+ },
177
+ "filetype-page": {
178
+ "allOf": [
179
+ { "$ref": "#/defs/common-page" },
180
+ {
181
+ "type": "object",
182
+ "properties": {
183
+ "link": {
184
+ "anyOf": [
185
+ {
186
+ "type": "object",
187
+ "properties": { "new_tab": { "type": ["boolean", "null"] }, "url": { "type": ["string", "null"] } }
188
+ },
189
+ { "type": "null" }
190
+ ],
191
+ "description": "Information about where this page should redirect to. Setting this to `null` will make the page a standard page."
192
+ }
193
+ }
194
+ }
195
+ ],
196
+ "required": ["title"]
197
+ }
198
+ }
199
+ }
@@ -1,277 +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 { __test__ } from './import.js'
7
-
8
- const {
9
- sniffOasContent,
10
- buildWellKnownOasProbeUrls,
11
- probeWellKnownOasPaths,
12
- extractOasUrlsFromHtml,
13
- extractMintlifyOasCandidates,
14
- sitemapUrlsToKnownUrls,
15
- extractOasSpecUrlsFromParsed,
16
- downloadOasSpecs,
17
- } = __test__
18
-
19
- const originalFetch = globalThis.fetch
20
-
21
- afterEach(() => {
22
- globalThis.fetch = originalFetch
23
- })
24
-
25
- const JSON_SPEC = JSON.stringify({ openapi: '3.0.0', info: { title: 'T', version: '1' }, paths: {} })
26
- const YAML_SPEC = 'openapi: 3.0.0\ninfo:\n title: T\n version: "1"\npaths: {}\n'
27
- const HTML_SHELL = '<!DOCTYPE html><html><head><title>Docs</title></head><body>hi</body></html>'
28
-
29
- function toArrayBuffer(str) {
30
- const buf = Buffer.from(str)
31
- return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
32
- }
33
-
34
- function mockProbeFetch(routes) {
35
- const seen = []
36
- globalThis.fetch = async (url) => {
37
- const href = String(url)
38
- seen.push(href)
39
- const entry = routes[href] || routes['*']
40
- if (!entry) return { ok: false, status: 404, url: href, arrayBuffer: async () => new ArrayBuffer(0) }
41
- return {
42
- ok: true,
43
- status: 200,
44
- url: entry.finalUrl || href,
45
- arrayBuffer: async () => toArrayBuffer(entry.body),
46
- }
47
- }
48
- return seen
49
- }
50
-
51
- test('sniffOasContent accepts JSON and YAML specs, rejects HTML and random content', () => {
52
- assert.equal(sniffOasContent(JSON_SPEC), true)
53
- assert.equal(sniffOasContent(JSON.stringify({ swagger: '2.0', paths: {} })), true)
54
- assert.equal(sniffOasContent(YAML_SPEC), true)
55
- assert.equal(sniffOasContent('swagger: "2.0"\npaths: {}\n'), true)
56
- assert.equal(sniffOasContent("'openapi': 3.1.0\n"), true)
57
- assert.equal(sniffOasContent(HTML_SHELL), false)
58
- assert.equal(sniffOasContent(' <svg></svg>'), false)
59
- assert.equal(sniffOasContent(JSON.stringify({ name: 'pkg', version: '1.0.0' })), false)
60
- assert.equal(sniffOasContent(''), false)
61
- assert.equal(sniffOasContent('just some prose about openapi stuff'), false)
62
- })
63
-
64
- test('buildWellKnownOasProbeUrls covers both origins and path-kind docs bases without duplicates', () => {
65
- const docsBase = { kind: 'path', url: new URL('https://foo.com/docs/') }
66
- const urls = buildWellKnownOasProbeUrls(new URL('https://foo.com/docs/'), docsBase, 'https://foo.com')
67
- assert(urls.includes('https://foo.com/openapi.json'))
68
- assert(urls.includes('https://foo.com/v3/api-docs'))
69
- assert(urls.includes('https://foo.com/docs/openapi.yaml'))
70
- assert(!urls.includes('https://foo.com/docs/openapi.yml'))
71
- assert.equal(new Set(urls).size, urls.length)
72
-
73
- const subdomain = buildWellKnownOasProbeUrls(
74
- new URL('https://docs.foo.com/'),
75
- { kind: 'subdomain', url: new URL('https://docs.foo.com/') },
76
- 'https://foo.com',
77
- )
78
- assert(subdomain.includes('https://docs.foo.com/openapi.json'))
79
- assert(subdomain.includes('https://foo.com/openapi.json'))
80
- })
81
-
82
- test('probeWellKnownOasPaths returns zero hits when every path 200s with HTML', async () => {
83
- const seen = mockProbeFetch({ '*': { body: HTML_SHELL } })
84
- const hits = await probeWellKnownOasPaths(new URL('https://foo.com/'), null, 'https://foo.com')
85
- assert.equal(hits.length, 0)
86
- assert(seen.length <= 27)
87
- })
88
-
89
- test('probeWellKnownOasPaths returns a hit with prefetched body for a real YAML spec', async () => {
90
- mockProbeFetch({ 'https://foo.com/openapi.yaml': { body: YAML_SPEC } })
91
- const hits = await probeWellKnownOasPaths(new URL('https://foo.com/'), null, 'https://foo.com')
92
- assert.equal(hits.length, 1)
93
- assert.equal(hits[0].url, 'https://foo.com/openapi.yaml')
94
- assert.equal(hits[0].source, 'probe')
95
- assert(Buffer.isBuffer(hits[0].body))
96
- assert.equal(hits[0].body.toString('utf8'), YAML_SPEC)
97
- })
98
-
99
- test('probeWellKnownOasPaths skips hits that redirect off the probe origins', async () => {
100
- mockProbeFetch({
101
- 'https://foo.com/openapi.json': { body: JSON_SPEC, finalUrl: 'https://other.example.com/openapi.json' },
102
- 'https://foo.com/swagger.json': { body: JSON_SPEC },
103
- })
104
- const hits = await probeWellKnownOasPaths(new URL('https://foo.com/'), null, 'https://foo.com')
105
- assert.deepEqual(hits.map((h) => h.url), ['https://foo.com/swagger.json'])
106
- })
107
-
108
- test('extractMintlifyOasCandidates handles string, string[], {source}, and nested nav nodes', () => {
109
- const config = {
110
- openapi: '/specs/root.json',
111
- navigation: {
112
- tabs: [
113
- {
114
- tab: 'API',
115
- openapi: ['https://cdn.foo.com/a.yaml', '/specs/b.json'],
116
- groups: [{ group: 'Endpoints', openapi: { source: 'specs/c.json' }, pages: ['api/list'] }],
117
- },
118
- ],
119
- },
120
- }
121
- const items = extractMintlifyOasCandidates(config, 'https://foo.com')
122
- const urls = items.map((i) => i.url)
123
- assert.deepEqual(urls, [
124
- 'https://foo.com/specs/root.json',
125
- 'https://cdn.foo.com/a.yaml',
126
- 'https://foo.com/specs/b.json',
127
- 'https://foo.com/specs/c.json',
128
- ])
129
- assert(items.every((i) => i.source === 'mintlify' && i.text === null))
130
- })
131
-
132
- test('extractMintlifyOasCandidates ignores non-url values and dedupes', () => {
133
- const config = {
134
- navigation: {
135
- anchors: [{ openapi: '/spec.json' }, { openapi: '/spec.json' }, { openapi: { notSource: true } }],
136
- },
137
- }
138
- const items = extractMintlifyOasCandidates(config, 'https://foo.com')
139
- assert.deepEqual(items.map((i) => i.url), ['https://foo.com/spec.json'])
140
- })
141
-
142
- test('extractOasUrlsFromHtml finds Swagger UI single url and urls arrays', () => {
143
- const single = `<script>SwaggerUIBundle({ dom_id: '#swagger', url: "/openapi.json" })</script>`
144
- assert.deepEqual(extractOasUrlsFromHtml(single, 'https://foo.com/reference'), ['https://foo.com/openapi.json'])
145
-
146
- const multi = `<script>SwaggerUI({ urls: [{ url: "https://foo.com/v1.json", name: "v1" }, { url: '/v2.yaml', name: "v2" }] })</script>`
147
- assert.deepEqual(extractOasUrlsFromHtml(multi, 'https://foo.com/reference'), [
148
- 'https://foo.com/v1.json',
149
- 'https://foo.com/v2.yaml',
150
- ])
151
- })
152
-
153
- test('extractOasUrlsFromHtml finds redoc in every quoting variant plus Redoc.init', () => {
154
- for (const html of [
155
- '<redoc spec-url="/spec.yaml"></redoc>',
156
- "<redoc spec-url='/spec.yaml'></redoc>",
157
- '<redoc lazy-rendering spec-url=/spec.yaml></redoc>',
158
- ]) {
159
- assert.deepEqual(extractOasUrlsFromHtml(html, 'https://foo.com/api'), ['https://foo.com/spec.yaml'])
160
- }
161
- const init = `<script>Redoc.init("https://foo.com/openapi.json", {}, document.getElementById('redoc'))</script>`
162
- assert.deepEqual(extractOasUrlsFromHtml(init, 'https://foo.com/api'), ['https://foo.com/openapi.json'])
163
- })
164
-
165
- test('extractOasUrlsFromHtml finds Scalar script tags in both attribute orders', () => {
166
- const a = '<script id="api-reference" data-url="/openapi.json"></script>'
167
- const b = "<script data-url='/openapi.json' id='api-reference'></script>"
168
- assert.deepEqual(extractOasUrlsFromHtml(a, 'https://foo.com/'), ['https://foo.com/openapi.json'])
169
- assert.deepEqual(extractOasUrlsFromHtml(b, 'https://foo.com/'), ['https://foo.com/openapi.json'])
170
- const noId = '<script data-url="/openapi.json"></script>'
171
- assert.deepEqual(extractOasUrlsFromHtml(noId, 'https://foo.com/'), [])
172
- })
173
-
174
- test('extractOasUrlsFromHtml finds Stoplight apiDescriptionUrl and skips marketing pages', () => {
175
- const stoplight = '<elements-api apiDescriptionUrl="https://foo.com/api.yaml" router="hash" />'
176
- assert.deepEqual(extractOasUrlsFromHtml(stoplight, 'https://foo.com/'), ['https://foo.com/api.yaml'])
177
- const marketing = '<html><body><a href="/pricing">Pricing</a><script>analytics.track("url: nope")</script></body></html>'
178
- assert.deepEqual(extractOasUrlsFromHtml(marketing, 'https://foo.com/'), [])
179
- })
180
-
181
- test('sitemapUrlsToKnownUrls routes spec-shaped locs to the sink instead of known pages', () => {
182
- const sink = []
183
- const known = sitemapUrlsToKnownUrls(
184
- [
185
- 'https://foo.com/docs/getting-started',
186
- 'https://foo.com/openapi.json',
187
- 'https://foo.com/specs/api.yaml',
188
- 'https://foo.com/specs/api.yml',
189
- ],
190
- sink,
191
- )
192
- assert.deepEqual(known.map((k) => k.url), ['https://foo.com/docs/getting-started'])
193
- assert.deepEqual(
194
- sink.map((s) => s.url),
195
- ['https://foo.com/openapi.json', 'https://foo.com/specs/api.yaml', 'https://foo.com/specs/api.yml'],
196
- )
197
- assert(sink.every((s) => s.source === 'sitemap'))
198
- })
199
-
200
- test('extractOasSpecUrlsFromParsed captures .yaml links too and prunes emptied sections', () => {
201
- const parsed = {
202
- sections: [
203
- { title: 'Specs', items: [{ text: 'OpenAPI', url: 'https://foo.com/openapi.yaml' }] },
204
- {
205
- title: 'Guides',
206
- items: [
207
- { text: 'Intro', url: 'https://foo.com/docs/intro' },
208
- { text: 'Spec', url: 'https://foo.com/spec.json' },
209
- ],
210
- },
211
- ],
212
- }
213
- const captured = extractOasSpecUrlsFromParsed(parsed)
214
- assert.deepEqual(captured, [
215
- { url: 'https://foo.com/openapi.yaml', text: 'OpenAPI', source: 'llms' },
216
- { url: 'https://foo.com/spec.json', text: 'Spec', source: 'llms' },
217
- ])
218
- assert.equal(parsed.sections.length, 1)
219
- assert.deepEqual(parsed.sections[0].items.map((i) => i.url), ['https://foo.com/docs/intro'])
220
- })
221
-
222
- test('downloadOasSpecs writes prefetched bodies without fetching and preserves extensions', async () => {
223
- const seen = []
224
- globalThis.fetch = async (url) => {
225
- seen.push(String(url))
226
- return { ok: true, status: 200, url: String(url), arrayBuffer: async () => toArrayBuffer(YAML_SPEC) }
227
- }
228
- const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'import-oas-test-'))
229
- const written = await downloadOasSpecs(
230
- [
231
- { url: 'https://foo.com/openapi.json', text: null, source: 'probe', body: Buffer.from(JSON_SPEC) },
232
- { url: 'https://foo.com/specs/api.yaml', text: null, source: 'llms' },
233
- ],
234
- stagingDir,
235
- )
236
- assert.equal(written, 2)
237
- assert.deepEqual(seen, ['https://foo.com/specs/api.yaml'])
238
- const files = fs.readdirSync(path.join(stagingDir, 'oas')).sort()
239
- assert.deepEqual(files, ['api.yaml', 'openapi.json'])
240
- assert.equal(fs.readFileSync(path.join(stagingDir, 'oas', 'openapi.json'), 'utf8'), JSON_SPEC)
241
- assert.equal(fs.readFileSync(path.join(stagingDir, 'oas', 'api.yaml'), 'utf8'), YAML_SPEC)
242
- })
243
-
244
- test('downloadOasSpecs sniffs an extension for extensionless URLs', async () => {
245
- const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'import-oas-test-'))
246
- const written = await downloadOasSpecs(
247
- [
248
- { url: 'https://foo.com/v3/api-docs', text: null, source: 'probe', body: Buffer.from(JSON_SPEC) },
249
- { url: 'https://foo.com/spec/latest', text: null, source: 'probe', body: Buffer.from(YAML_SPEC) },
250
- ],
251
- stagingDir,
252
- )
253
- assert.equal(written, 2)
254
- const files = fs.readdirSync(path.join(stagingDir, 'oas')).sort()
255
- assert.deepEqual(files, ['api-docs.json', 'latest.yaml'])
256
- })
257
-
258
- test('downloadOasSpecs skips HTML bodies and suffixes filename collisions', async () => {
259
- globalThis.fetch = async (url) => ({
260
- ok: true,
261
- status: 200,
262
- url: String(url),
263
- arrayBuffer: async () => toArrayBuffer(HTML_SHELL),
264
- })
265
- const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'import-oas-test-'))
266
- const written = await downloadOasSpecs(
267
- [
268
- { url: 'https://foo.com/a/openapi.json', text: null, source: 'probe', body: Buffer.from(JSON_SPEC) },
269
- { url: 'https://foo.com/b/openapi.json', text: null, source: 'probe', body: Buffer.from(JSON_SPEC) },
270
- { url: 'https://foo.com/shell.json', text: null, source: 'html' },
271
- ],
272
- stagingDir,
273
- )
274
- assert.equal(written, 2)
275
- const files = fs.readdirSync(path.join(stagingDir, 'oas')).sort()
276
- assert.deepEqual(files, ['openapi-2.json', 'openapi.json'])
277
- })
@@ -1,172 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { __test__ } from './import.js'
4
-
5
- const {
6
- isDiscoverableLink,
7
- isAuthChromeUrl,
8
- isDocsUnderApiPath,
9
- filterUrlPagesTree,
10
- urlIsApiReference,
11
- urlIsChangelog,
12
- reclassifyReferencePages,
13
- extractChangelogFromSections,
14
- decodeEntities,
15
- stripTags,
16
- } = __test__
17
-
18
- test('isDocsUnderApiPath treats /api/docs guides as docs, not reference', () => {
19
- assert.equal(isDocsUnderApiPath('https://example.com/api/docs/guides/intro'), true)
20
- assert.equal(isDocsUnderApiPath('https://example.com/api/docs/pricing'), true)
21
- assert.equal(isDocsUnderApiPath('https://example.com/api/docs'), true)
22
- assert.equal(isDocsUnderApiPath('https://example.com/API/DOC/guides'), true)
23
- assert.equal(isDocsUnderApiPath('https://example.com/api/docs/api'), false)
24
- assert.equal(isDocsUnderApiPath('https://example.com/api/docs/reference'), false)
25
- assert.equal(isDocsUnderApiPath('https://example.com/api/docs/api-reference'), false)
26
- assert.equal(isDocsUnderApiPath('https://example.com/docs/guides'), false)
27
- assert.equal(isDocsUnderApiPath('https://example.com/api/reference/pets'), false)
28
- assert.equal(isDocsUnderApiPath('not a url'), false)
29
- })
30
-
31
- test('filterUrlPagesTree keeps matching URL pages and empty parents with survivors', () => {
32
- const tree = [
33
- { title: 'Guides', url: 'https://example.com/api/docs/guides' },
34
- { title: 'Pets', url: 'https://example.com/api/reference/pets' },
35
- {
36
- title: 'Mixed',
37
- pages: [
38
- { title: 'Pricing', url: 'https://example.com/api/docs/pricing' },
39
- { title: 'Endpoint', url: 'https://example.com/api/reference/create' },
40
- ],
41
- },
42
- { title: 'Empty leftover', pages: [{ title: 'Gone', url: 'https://example.com/api/reference/gone' }] },
43
- ]
44
-
45
- const kept = filterUrlPagesTree(tree, isDocsUnderApiPath)
46
- assert.deepEqual(
47
- kept.map((p) => p.title),
48
- ['Guides', 'Mixed'],
49
- )
50
- assert.deepEqual(
51
- kept[1].pages.map((p) => p.title),
52
- ['Pricing'],
53
- )
54
- })
55
-
56
- test('urlIsApiReference matches api-reference / endpoints segments only', () => {
57
- assert.equal(urlIsApiReference('https://example.com/api-reference/pets'), true)
58
- assert.equal(urlIsApiReference('https://example.com/api_reference/pets'), true)
59
- assert.equal(urlIsApiReference('https://example.com/endpoints/create'), true)
60
- assert.equal(urlIsApiReference('https://example.com/endpoint/create'), true)
61
- assert.equal(urlIsApiReference('https://example.com/api/docs/guides'), false)
62
- assert.equal(urlIsApiReference('https://example.com/docs/api'), false)
63
- assert.equal(urlIsApiReference('://bad'), false)
64
- })
65
-
66
- test('urlIsChangelog matches changelog-ish slugs without sweeping release-pipeline', () => {
67
- assert.equal(urlIsChangelog('https://example.com/docs/changelog'), true)
68
- assert.equal(urlIsChangelog('https://example.com/docs/changelog-javascript-agent'), true)
69
- assert.equal(urlIsChangelog('https://example.com/docs/release-notes'), true)
70
- assert.equal(urlIsChangelog('https://example.com/docs/whats-new'), true)
71
- assert.equal(urlIsChangelog('https://example.com/docs/releases'), true)
72
- assert.equal(urlIsChangelog('https://example.com/docs/release-2026-2'), true)
73
- assert.equal(urlIsChangelog('https://example.com/docs/release-pipeline'), false)
74
- assert.equal(urlIsChangelog('https://example.com/docs/getting-started'), false)
75
- assert.equal(urlIsChangelog('://bad'), false)
76
- })
77
-
78
- test('extractChangelogFromSections pulls changelog rows out of llms sections in place', () => {
79
- const sections = [
80
- {
81
- title: 'Guides',
82
- items: [
83
- { text: 'Intro', url: 'https://example.com/docs/intro' },
84
- { text: 'Notes', url: 'https://example.com/docs/changelog', description: 'What shipped' },
85
- ],
86
- },
87
- {
88
- title: 'History',
89
- items: [{ text: 'v1', url: 'https://example.com/docs/release-notes/v1' }],
90
- },
91
- ]
92
-
93
- const extracted = extractChangelogFromSections(sections)
94
- assert.deepEqual(extracted, [
95
- { title: 'Notes', url: 'https://example.com/docs/changelog', description: 'What shipped' },
96
- { title: 'v1', url: 'https://example.com/docs/release-notes/v1' },
97
- ])
98
- assert.deepEqual(sections[0].items.map((i) => i.url), ['https://example.com/docs/intro'])
99
- assert.deepEqual(sections[1].items, [])
100
- })
101
-
102
- test('reclassifyReferencePages sweeps api-reference URLs into one category and drops emptied ones', () => {
103
- const scraped = {
104
- categories: [
105
- {
106
- title: 'Developers',
107
- pages: [
108
- { title: 'Guide', url: 'https://example.com/docs/guide' },
109
- {
110
- title: 'List pets',
111
- url: 'https://example.com/api-reference/pets',
112
- pages: [{ title: 'Create pet', url: 'https://example.com/endpoints/create' }],
113
- },
114
- ],
115
- },
116
- {
117
- title: 'Only endpoints',
118
- pages: [{ title: 'Delete pet', url: 'https://example.com/endpoints/delete' }],
119
- },
120
- ],
121
- }
122
-
123
- assert.equal(reclassifyReferencePages(scraped), 3)
124
- assert.deepEqual(
125
- scraped.categories.map((c) => c.title),
126
- ['Developers', 'API Reference'],
127
- )
128
- assert.deepEqual(
129
- scraped.categories[0].pages.map((p) => p.title),
130
- ['Guide'],
131
- )
132
- assert.deepEqual(
133
- scraped.categories[1].pages.map((p) => p.url),
134
- [
135
- 'https://example.com/api-reference/pets',
136
- 'https://example.com/endpoints/create',
137
- 'https://example.com/endpoints/delete',
138
- ],
139
- )
140
- })
141
-
142
- test('isAuthChromeUrl matches leading login/account routes only', () => {
143
- assert.equal(isAuthChromeUrl('https://example.com/auth'), true)
144
- assert.equal(isAuthChromeUrl('https://example.com/login/callback'), true)
145
- assert.equal(isAuthChromeUrl('https://example.com/sign-in'), true)
146
- assert.equal(isAuthChromeUrl('https://example.com/signup'), true)
147
- assert.equal(isAuthChromeUrl('https://example.com/oauth2/authorize'), true)
148
- assert.equal(isAuthChromeUrl('https://example.com/sso'), true)
149
- assert.equal(isAuthChromeUrl('https://example.com/docs/auth'), false)
150
- assert.equal(isAuthChromeUrl('https://example.com/authenticate'), false)
151
- assert.equal(isAuthChromeUrl('://bad'), false)
152
- })
153
-
154
- test('isDiscoverableLink keeps same-origin doc paths and drops assets, chrome, and off-origin', () => {
155
- const base = new URL('https://example.com/docs')
156
- assert.equal(isDiscoverableLink('https://example.com/docs/intro', base), true)
157
- assert.equal(isDiscoverableLink('https://other.com/docs/intro', base), false)
158
- assert.equal(isDiscoverableLink('https://example.com/', base), false)
159
- assert.equal(isDiscoverableLink('https://example.com/logo.png', base), false)
160
- assert.equal(isDiscoverableLink('https://example.com/_next/static/chunk.js', base), false)
161
- assert.equal(isDiscoverableLink('https://example.com/assets/app.js', base), false)
162
- assert.equal(isDiscoverableLink('not a url', base), false)
163
- })
164
-
165
- test('decodeEntities and stripTags clean sidebar titles that would otherwise miss category routing', () => {
166
- assert.equal(decodeEntities('New Features &amp; Upgrade Changes'), 'New Features & Upgrade Changes')
167
- assert.equal(decodeEntities('A &#38; B'), 'A & B')
168
- assert.equal(decodeEntities('hex &#x26; amp'), 'hex & amp')
169
- assert.equal(decodeEntities('plain'), 'plain')
170
- assert.equal(stripTags('<span>API\u200B Reference</span>'), 'API Reference')
171
- assert.equal(stripTags('Guides &amp; <b>Recipes</b>'), 'Guides & Recipes')
172
- })