@readme/cli 0.1.0 → 0.2.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 (53) hide show
  1. package/bin/readme.js +1 -1
  2. package/package.json +33 -24
  3. package/src/bootstrap.js +9 -13
  4. package/src/cli.js +17 -11
  5. package/src/commands/components.js +35 -18
  6. package/src/commands/dev.js +12 -8
  7. package/src/commands/eyes.js +12 -8
  8. package/src/commands/import.js +2190 -2048
  9. package/src/commands/lint.js +33 -29
  10. package/src/commands/oas-bundle.js +6 -2
  11. package/src/commands/oas-sync.js +96 -93
  12. package/src/commands/oas-validate.js +10 -7
  13. package/src/commands/pretty.js +177 -164
  14. package/src/commands/setup.js +24 -26
  15. package/src/commands/versions.js +6 -2
  16. package/src/dev/app/[...slug]/page.js +1 -0
  17. package/src/dev/app/layout.js +18 -11
  18. package/src/dev/app/page.js +1 -0
  19. package/src/dev/lib/docs.js +27 -19
  20. package/src/dev/next.config.mjs +2 -2
  21. package/src/gha.js +7 -7
  22. package/src/prompts/index.js +16 -6
  23. package/src/utils/eyes.js +143 -51
  24. package/src/utils/git.js +3 -3
  25. package/src/utils/lint.js +39 -38
  26. package/src/utils/llms.js +94 -91
  27. package/src/utils/markdown.js +256 -0
  28. package/src/utils/marketplace.js +12 -5
  29. package/src/utils/multi-spinner.js +5 -7
  30. package/src/utils/promise-pool.js +7 -5
  31. package/src/utils/reporter.js +88 -91
  32. package/src/utils/setup-templates.js +12 -6
  33. package/src/utils/styles.js +2 -1
  34. package/src/utils/tamagotchi.js +257 -91
  35. package/src/utils/tips.js +18 -9
  36. package/src/utils/url-segs.js +46 -41
  37. package/src/validators/components.js +55 -63
  38. package/src/validators/content.js +5 -10
  39. package/src/validators/duplicates.js +6 -2
  40. package/src/validators/frontmatter.js +19 -21
  41. package/src/validators/links.js +4 -8
  42. package/src/validators/oas-reference.js +7 -14
  43. package/src/validators/oas-schema.js +2 -0
  44. package/src/validators/ordering.js +5 -5
  45. package/src/validators/recipes.js +4 -12
  46. package/src/commands/import-oas.test.js +0 -277
  47. package/src/commands/import.classify.test.js +0 -172
  48. package/src/commands/import.landing.test.js +0 -217
  49. package/src/commands/import.slug-planner.test.js +0 -86
  50. package/src/commands/import.staging.test.js +0 -230
  51. package/src/commands/import.test.js +0 -382
  52. package/src/utils/llms.test.js +0 -302
  53. package/src/utils/url-segs.test.js +0 -239
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import matter from 'gray-matter';
3
+
4
4
  import {
5
5
  findOasFiles,
6
6
  extractOperations,
@@ -9,6 +9,7 @@ import {
9
9
  operationKey,
10
10
  findIgnoredInternalExtensions,
11
11
  } from '../commands/oas-sync.js';
12
+ import { parseFrontmatter } from '../utils/markdown.js';
12
13
 
13
14
  export const name = 'oas-reference';
14
15
 
@@ -37,17 +38,11 @@ export function validateAll(files, gitRoot, { fix } = {}) {
37
38
  }
38
39
 
39
40
  // Collect all reference pages with api frontmatter.
40
- const refPages = files.filter((f) => f.startsWith('reference/') && f.endsWith('.md'));
41
+ const refPages = files.filter(f => f.startsWith('reference/') && f.endsWith('.md'));
41
42
 
42
43
  for (const relPath of refPages) {
43
44
  const filePath = path.join(gitRoot, relPath);
44
- let data;
45
- try {
46
- ({ data } = matter(fs.readFileSync(filePath, 'utf-8')));
47
- } catch {
48
- continue;
49
- }
50
-
45
+ const { data } = parseFrontmatter(fs.readFileSync(filePath, 'utf-8'));
51
46
  if (!data.api || !data.api.file) continue;
52
47
 
53
48
  const oasFilename = data.api.file;
@@ -85,11 +80,9 @@ export function validateAll(files, gitRoot, { fix } = {}) {
85
80
  // Check for missing pages: operations in the spec with no corresponding page.
86
81
  const existingPages = collectExistingPages(refDir);
87
82
  for (const [oasFilename, { ops }] of oasMap) {
88
- const pagesForOas = existingPages.filter((p) => p.data.api.file === oasFilename);
83
+ const pagesForOas = existingPages.filter(p => p.data.api.file === oasFilename);
89
84
  const coveredOps = new Set(
90
- pagesForOas.map((p) =>
91
- operationKey({ operationId: p.data.api.operationId, isWebhook: !!p.data.api.webhook }),
92
- ),
85
+ pagesForOas.map(p => operationKey({ operationId: p.data.api.operationId, isWebhook: !!p.data.api.webhook })),
93
86
  );
94
87
 
95
88
  for (const op of ops.values()) {
@@ -109,7 +102,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
109
102
  // is actually fixable. The sync adds, deletes, moves, hides and reorders
110
103
  // reference files, which is far too much to do on the strength of an
111
104
  // unfixable warning (`x-readme.internal`, a page pointing at a missing spec).
112
- if (fix && results.some((r) => r.fixable)) {
105
+ if (fix && results.some(r => r.fixable)) {
113
106
  const syncResults = syncOas(gitRoot);
114
107
  if (syncResults) {
115
108
  for (const r of results) {
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+
3
4
  import OASNormalize from 'oas-normalize';
5
+
4
6
  import { findOasFiles } from '../commands/oas-sync.js';
5
7
 
6
8
  export const name = 'oas-schema';
@@ -33,7 +33,7 @@ function parseOrderYaml(content) {
33
33
  return content
34
34
  .split('\n')
35
35
  .map(parseOrderEntry)
36
- .filter((entry) => entry !== null);
36
+ .filter(entry => entry !== null);
37
37
  }
38
38
 
39
39
  function isIndex(name) {
@@ -108,7 +108,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
108
108
 
109
109
  // On disk but not ordered.
110
110
  if (requireOrder) {
111
- const missing = [...onDisk].filter((slug) => !ordered.has(slug)).sort();
111
+ const missing = [...onDisk].filter(slug => !ordered.has(slug)).sort();
112
112
  if (missing.length > 0) {
113
113
  results.push({
114
114
  file: relOrder,
@@ -152,7 +152,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
152
152
  for (const r of results) {
153
153
  if (!r._fixAdd) continue;
154
154
  const { orderPath, missing } = r._fixAdd;
155
- const newEntries = missing.map((slug) => `- ${yamlSafeSlug(slug)}`).join('\n');
155
+ const newEntries = missing.map(slug => `- ${yamlSafeSlug(slug)}`).join('\n');
156
156
 
157
157
  if (fs.existsSync(orderPath)) {
158
158
  const existing = fs.readFileSync(orderPath, 'utf-8');
@@ -180,11 +180,11 @@ export function validateAll(files, gitRoot, { fix } = {}) {
180
180
  const kept = fs
181
181
  .readFileSync(orderPath, 'utf-8')
182
182
  .split('\n')
183
- .filter((line) => {
183
+ .filter(line => {
184
184
  const entry = parseOrderEntry(line);
185
185
  return entry === null || !toRemove.has(entry);
186
186
  });
187
- if (kept.some((line) => parseOrderEntry(line) !== null)) {
187
+ if (kept.some(line => parseOrderEntry(line) !== null)) {
188
188
  fs.writeFileSync(orderPath, kept.join('\n'));
189
189
  } else {
190
190
  fs.unlinkSync(orderPath);
@@ -1,4 +1,4 @@
1
- import matter from 'gray-matter';
1
+ import { parseFrontmatter } from '../utils/markdown.js';
2
2
 
3
3
  export const name = 'recipes';
4
4
 
@@ -76,16 +76,8 @@ export function validate({ content, relativePath }) {
76
76
  if (!relativePath.endsWith('.md')) return null;
77
77
 
78
78
  const results = [];
79
- let data;
80
- let body;
81
-
82
- try {
83
- ({ data, content: body } = matter(content));
84
- } catch {
85
- return null;
86
- }
87
-
88
- if (!data.recipe) return null;
79
+ const { data, content: body, error } = parseFrontmatter(content);
80
+ if (error || !data.recipe) return null;
89
81
 
90
82
  const codeBlocks = parseCodeBlocks(body);
91
83
  const snippetRefs = parseSnippetRefs(body);
@@ -118,7 +110,7 @@ export function validate({ content, relativePath }) {
118
110
  const block = blocksByLang.get(ref.lang);
119
111
 
120
112
  if (!block) {
121
- const available = codeBlocks.map((b) => b.lang).join(', ');
113
+ const available = codeBlocks.map(b => b.lang).join(', ');
122
114
  results.push({
123
115
  file: relativePath,
124
116
  rule: name,
@@ -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
- })