@readme/cli 0.0.27 → 0.0.30
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.
- package/package.json +5 -5
- package/src/cli.js +3 -3
- package/src/commands/import-oas.test.js +277 -0
- package/src/commands/import.classify.test.js +172 -0
- package/src/commands/import.js +2848 -443
- package/src/commands/import.landing.test.js +217 -0
- package/src/commands/import.slug-planner.test.js +86 -0
- package/src/commands/import.staging.test.js +158 -0
- package/src/commands/import.test.js +382 -0
- package/src/commands/oas-sync.js +362 -92
- package/src/prompts/index.js +101 -13
- package/src/utils/lint.js +2 -2
- package/src/utils/llms.js +224 -0
- package/src/utils/llms.test.js +302 -0
- package/src/utils/reporter.js +2 -1
- package/src/utils/slug-for-entry.js +16 -0
- package/src/utils/url-segs.js +124 -0
- package/src/utils/url-segs.test.js +239 -0
- package/src/validators/components.js +11 -7
- package/src/validators/duplicates.js +8 -8
- package/src/validators/frontmatter.js +11 -0
- package/src/validators/links.js +2 -8
- package/src/validators/oas-reference.js +13 -38
- package/src/validators/ordering.js +169 -83
- package/vendor/TOOLS.md +1 -1
- package/src/dev/.next/app-build-manifest.json +0 -20
- package/src/dev/.next/build-manifest.json +0 -31
- package/src/dev/.next/cache/.rscinfo +0 -1
- package/src/dev/.next/cache/next-devtools-config.json +0 -1
- package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/package.json +0 -1
- package/src/dev/.next/prerender-manifest.json +0 -11
- package/src/dev/.next/react-loadable-manifest.json +0 -1
- package/src/dev/.next/routes-manifest.json +0 -1
- package/src/dev/.next/server/app/[...slug]/page.js +0 -360
- package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +0 -1
- package/src/dev/.next/server/app/page.js +0 -349
- package/src/dev/.next/server/app/page_client-reference-manifest.js +0 -1
- package/src/dev/.next/server/app-paths-manifest.json +0 -3
- package/src/dev/.next/server/edge-runtime-webpack.js +0 -1151
- package/src/dev/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/src/dev/.next/server/middleware-build-manifest.js +0 -33
- package/src/dev/.next/server/middleware-manifest.json +0 -32
- package/src/dev/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/src/dev/.next/server/middleware.js +0 -1113
- package/src/dev/.next/server/next-font-manifest.js +0 -1
- package/src/dev/.next/server/next-font-manifest.json +0 -1
- package/src/dev/.next/server/pages-manifest.json +0 -5
- package/src/dev/.next/server/server-reference-manifest.js +0 -1
- package/src/dev/.next/server/server-reference-manifest.json +0 -5
- package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/src/dev/.next/server/vendor-chunks/@readme.js +0 -25
- package/src/dev/.next/server/vendor-chunks/@swc.js +0 -55
- package/src/dev/.next/server/vendor-chunks/next.js +0 -3659
- package/src/dev/.next/server/webpack-runtime.js +0 -209
- package/src/dev/.next/static/chunks/app/[...slug]/loading.js +0 -28
- package/src/dev/.next/static/chunks/app/[...slug]/page.js +0 -28
- package/src/dev/.next/static/chunks/app/layout.js +0 -171
- package/src/dev/.next/static/chunks/app/page.js +0 -28
- package/src/dev/.next/static/chunks/app-pages-internals.js +0 -182
- package/src/dev/.next/static/chunks/main-app.js +0 -1882
- package/src/dev/.next/static/chunks/polyfills.js +0 -1
- package/src/dev/.next/static/chunks/webpack.js +0 -1393
- package/src/dev/.next/static/css/app/layout.css +0 -559
- package/src/dev/.next/static/development/_buildManifest.js +0 -1
- package/src/dev/.next/static/development/_ssgManifest.js +0 -1
- package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +0 -1
- package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +0 -12
- package/src/dev/.next/trace +0 -21
- package/src/dev/.next/types/app/[...slug]/page.ts +0 -84
- package/src/dev/.next/types/app/layout.ts +0 -84
- package/src/dev/.next/types/app/page.ts +0 -84
- package/src/dev/.next/types/cache-life.d.ts +0 -141
- package/src/dev/.next/types/package.json +0 -1
- package/src/dev/.next/types/routes.d.ts +0 -55
- package/src/validators/numbering.js +0 -134
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { stripSegmentExtensions, urlTrieSegs, extractUrlPathSegments, normalizePath, compareCanonicalUrlPreference, llmsPageDedupeKey } from './url-segs.js'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// stripSegmentExtensions
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
test('stripSegmentExtensions: strips single .md', () => {
|
|
10
|
+
assert.equal(stripSegmentExtensions('index.md'), 'index')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('stripSegmentExtensions: strips single .html', () => {
|
|
14
|
+
assert.equal(stripSegmentExtensions('index.html'), 'index')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('stripSegmentExtensions: strips single .htm', () => {
|
|
18
|
+
assert.equal(stripSegmentExtensions('index.htm'), 'index')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('stripSegmentExtensions: strips double .html.md (plaid pattern)', () => {
|
|
22
|
+
assert.equal(stripSegmentExtensions('index.html.md'), 'index')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('stripSegmentExtensions: strips .mdx', () => {
|
|
26
|
+
assert.equal(stripSegmentExtensions('page.mdx'), 'page')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('stripSegmentExtensions: strips .txt', () => {
|
|
30
|
+
assert.equal(stripSegmentExtensions('terminal.txt'), 'terminal')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('stripSegmentExtensions: leaves bare txt segment untouched (no dot prefix)', () => {
|
|
34
|
+
assert.equal(stripSegmentExtensions('txt'), 'txt')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('stripSegmentExtensions: leaves plain segments untouched', () => {
|
|
38
|
+
assert.equal(stripSegmentExtensions('quickstart'), 'quickstart')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('stripSegmentExtensions: leaves kebab segments untouched', () => {
|
|
42
|
+
assert.equal(stripSegmentExtensions('getting-started'), 'getting-started')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// urlTrieSegs — the double-extension plaid.com pattern
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
test('urlTrieSegs: .html.md index collapses to parent path (plaid pattern)', () => {
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
urlTrieSegs('https://plaid.com/docs/account/index.html.md'),
|
|
52
|
+
['docs', 'account'],
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('urlTrieSegs: .html index collapses to parent path', () => {
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
urlTrieSegs('https://plaid.com/docs/account/index.html'),
|
|
59
|
+
['docs', 'account'],
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('urlTrieSegs: trailing slash collapses correctly', () => {
|
|
64
|
+
assert.deepEqual(
|
|
65
|
+
urlTrieSegs('https://plaid.com/docs/account/'),
|
|
66
|
+
['docs', 'account'],
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('urlTrieSegs: non-index .html.md page keeps its segment', () => {
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
urlTrieSegs('https://plaid.com/docs/account/billing/index.html.md'),
|
|
73
|
+
['docs', 'account', 'billing'],
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('urlTrieSegs: orval.dev docs/index.html.md collapses correctly', () => {
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
urlTrieSegs('https://orval.dev/docs/index.html.md'),
|
|
80
|
+
['docs'],
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('urlTrieSegs: plain URL without extension passes through', () => {
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
urlTrieSegs('https://example.com/docs/quickstart'),
|
|
87
|
+
['docs', 'quickstart'],
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('urlTrieSegs: returns null on unparseable input', () => {
|
|
92
|
+
assert.equal(urlTrieSegs('not-a-url'), null)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('urlTrieSegs: .txt segment strips extension (warp.dev pattern)', () => {
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
urlTrieSegs('https://docs.warp.dev/_llms-txt/terminal.txt'),
|
|
98
|
+
['_llms-txt', 'terminal'],
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// extractUrlPathSegments — slug source
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
test('extractUrlPathSegments: .html.md index → parent segments only (plaid pattern)', () => {
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
extractUrlPathSegments('https://plaid.com/docs/account/index.html.md'),
|
|
109
|
+
['docs', 'account'],
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('extractUrlPathSegments: slug from deepened path', () => {
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
extractUrlPathSegments('https://plaid.com/docs/account/billing/index.html.md'),
|
|
116
|
+
['docs', 'account', 'billing'],
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('extractUrlPathSegments: strips leading numeric prefix', () => {
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
extractUrlPathSegments('https://example.com/docs/01-intro.md'),
|
|
123
|
+
['docs', 'intro'],
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('extractUrlPathSegments: plain .md page', () => {
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
extractUrlPathSegments('https://example.com/docs/quickstart.md'),
|
|
130
|
+
['docs', 'quickstart'],
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('extractUrlPathSegments: returns [] for empty/null input', () => {
|
|
135
|
+
assert.deepEqual(extractUrlPathSegments(''), [])
|
|
136
|
+
assert.deepEqual(extractUrlPathSegments(null), [])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// normalizePath — deduplication keys
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
test('normalizePath: .html.md and .html and bare path all produce same key', () => {
|
|
144
|
+
const a = normalizePath('https://plaid.com/docs/account/index.html.md')
|
|
145
|
+
const b = normalizePath('https://plaid.com/docs/account/index.html')
|
|
146
|
+
const c = normalizePath('https://plaid.com/docs/account/index')
|
|
147
|
+
assert.equal(a, b)
|
|
148
|
+
assert.equal(b, c)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('normalizePath: trailing slash and bare path produce same key', () => {
|
|
152
|
+
const a = normalizePath('https://example.com/docs/foo/')
|
|
153
|
+
const b = normalizePath('https://example.com/docs/foo')
|
|
154
|
+
assert.equal(a, b)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('normalizePath: index variants and bare path produce same key', () => {
|
|
158
|
+
const bare = normalizePath('https://example.com/docs/get-started')
|
|
159
|
+
assert.equal(normalizePath('https://example.com/docs/get-started/'), bare)
|
|
160
|
+
assert.equal(normalizePath('https://example.com/docs/get-started/index.html'), bare)
|
|
161
|
+
assert.equal(normalizePath('https://example.com/docs/get-started/index.htm'), bare)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('normalizePath: lowercases result', () => {
|
|
165
|
+
assert.equal(normalizePath('https://example.com/Docs/QuickStart'), '/docs/quickstart')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('normalizePath: falls back for unparseable input', () => {
|
|
169
|
+
assert.equal(normalizePath('not-a-url'), 'not-a-url')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('normalizePath: strips .txt extension (warp.dev pattern)', () => {
|
|
173
|
+
assert.equal(
|
|
174
|
+
normalizePath('https://docs.warp.dev/_llms-txt/terminal.txt'),
|
|
175
|
+
'/_llms-txt/terminal',
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// llmsPageDedupeKey — llms.txt page-row deduplication
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
test('llmsPageDedupeKey: includes origin so same paths on different hosts stay distinct', () => {
|
|
184
|
+
assert.notEqual(
|
|
185
|
+
llmsPageDedupeKey('https://docs.example.com/docs/get-started.md'),
|
|
186
|
+
llmsPageDedupeKey('https://example.com/docs/get-started.md'),
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('llmsPageDedupeKey: preserves query params by default', () => {
|
|
191
|
+
assert.notEqual(
|
|
192
|
+
llmsPageDedupeKey('https://example.com/docs/search?product=api'),
|
|
193
|
+
llmsPageDedupeKey('https://example.com/docs/search?product=sdk'),
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('llmsPageDedupeKey: normalizes query param order', () => {
|
|
198
|
+
assert.equal(
|
|
199
|
+
llmsPageDedupeKey('https://example.com/docs/search?b=2&a=1'),
|
|
200
|
+
llmsPageDedupeKey('https://example.com/docs/search?a=1&b=2'),
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('llmsPageDedupeKey: ignores fragments but preserves router query state', () => {
|
|
205
|
+
assert.equal(
|
|
206
|
+
llmsPageDedupeKey('https://example.com/docs/get-started.md?tab=js#install'),
|
|
207
|
+
llmsPageDedupeKey('https://example.com/docs/get-started?tab=js#auth'),
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('llmsPageDedupeKey: collapses doc URL spellings for the same page', () => {
|
|
212
|
+
const bare = llmsPageDedupeKey('https://example.com/docs/get-started')
|
|
213
|
+
assert.equal(llmsPageDedupeKey('https://example.com/docs/get-started.md'), bare)
|
|
214
|
+
assert.equal(llmsPageDedupeKey('https://example.com/docs/get-started.txt'), bare)
|
|
215
|
+
assert.equal(llmsPageDedupeKey('https://example.com/docs/get-started/index.html'), bare)
|
|
216
|
+
assert.equal(llmsPageDedupeKey('https://example.com/docs/get-started/'), bare)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// compareCanonicalUrlPreference — retained URL selection
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
test('compareCanonicalUrlPreference: prefers md over txt, bare, trailing slash, and index', () => {
|
|
224
|
+
const urls = [
|
|
225
|
+
'https://example.com/docs/get-started/index.html',
|
|
226
|
+
'https://example.com/docs/get-started/',
|
|
227
|
+
'https://example.com/docs/get-started',
|
|
228
|
+
'https://example.com/docs/get-started.txt',
|
|
229
|
+
'https://example.com/docs/get-started.md',
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
assert.deepEqual([...urls].sort(compareCanonicalUrlPreference), [
|
|
233
|
+
'https://example.com/docs/get-started.md',
|
|
234
|
+
'https://example.com/docs/get-started.txt',
|
|
235
|
+
'https://example.com/docs/get-started',
|
|
236
|
+
'https://example.com/docs/get-started/',
|
|
237
|
+
'https://example.com/docs/get-started/index.html',
|
|
238
|
+
])
|
|
239
|
+
})
|
|
@@ -135,19 +135,22 @@ export function validate({ content, relativePath }) {
|
|
|
135
135
|
|
|
136
136
|
const results = [];
|
|
137
137
|
|
|
138
|
-
// .md snippet files: filename
|
|
138
|
+
// .md snippet files: the filename becomes a component tag <Name />, so it must
|
|
139
|
+
// be a valid JS identifier starting with an uppercase letter. Underscores and
|
|
140
|
+
// digits are fine (<Helm_v4_error /> is valid); hyphens and a lowercase initial
|
|
141
|
+
// are not.
|
|
139
142
|
if (isMd) {
|
|
140
143
|
const slug = filename.replace(/\.md$/, "");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
if (!/^[A-Z][A-Za-z0-9_$]*$/.test(slug)) {
|
|
145
|
+
const suggestion = slug
|
|
146
|
+
.split(/[-_]/)
|
|
147
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
148
|
+
.join("");
|
|
146
149
|
results.push({
|
|
147
150
|
file: relativePath,
|
|
148
151
|
rule: name,
|
|
149
152
|
severity: "warning",
|
|
150
|
-
message: `Bad filename:
|
|
153
|
+
message: `Bad filename: "${slug}" can't be used as a component tag — rename to "${suggestion}.md" so it can be used as <${suggestion} />`,
|
|
151
154
|
});
|
|
152
155
|
}
|
|
153
156
|
return results.length > 0 ? results : null;
|
|
@@ -220,6 +223,7 @@ export function validateAll(files, gitRoot) {
|
|
|
220
223
|
results.push({
|
|
221
224
|
file: relPath,
|
|
222
225
|
rule: name,
|
|
226
|
+
severity: 'warning',
|
|
223
227
|
message: `Unknown component: <${comp}> is not a built-in or custom block`,
|
|
224
228
|
});
|
|
225
229
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { slugForEntry } from '../utils/slug-for-entry.js';
|
|
2
3
|
|
|
3
4
|
export const name = 'duplicates';
|
|
4
5
|
|
|
@@ -8,27 +9,26 @@ const CHECKED_DIRS = ['docs', 'reference'];
|
|
|
8
9
|
export function validateAll(files) {
|
|
9
10
|
const results = [];
|
|
10
11
|
|
|
11
|
-
// Group files by slug.
|
|
12
|
+
// Group files by "<topDir>:<slug>" so slugs only collide within the same section.
|
|
12
13
|
const slugMap = new Map();
|
|
13
14
|
for (const relPath of files) {
|
|
14
15
|
const topDir = relPath.split('/')[0];
|
|
15
16
|
if (!CHECKED_DIRS.includes(topDir)) continue;
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!slugMap.has(slug)) slugMap.set(slug, []);
|
|
22
|
-
slugMap.get(slug).push(relPath);
|
|
18
|
+
const slug = slugForEntry(relPath);
|
|
19
|
+
const key = `${topDir}:${slug}`;
|
|
20
|
+
if (!slugMap.has(key)) slugMap.set(key, []);
|
|
21
|
+
slugMap.get(key).push(relPath);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
for (const [
|
|
24
|
+
for (const [key, paths] of slugMap) {
|
|
26
25
|
if (paths.length < 2) continue;
|
|
27
26
|
|
|
28
27
|
// TODO: figure out why ReadMeConfig causes duplicate slugs and handle properly.
|
|
29
28
|
// For now, skip any duplicate set that involves a ReadMeConfig path.
|
|
30
29
|
if (paths.some((p) => p.includes('ReadMeConfig/'))) continue;
|
|
31
30
|
|
|
31
|
+
const slug = key.slice(key.indexOf(':') + 1);
|
|
32
32
|
const others = paths.slice(1);
|
|
33
33
|
for (const relPath of others) {
|
|
34
34
|
const otherLocations = paths.filter((p) => p !== relPath).map((p) => path.dirname(p)).join(', ');
|
|
@@ -113,6 +113,17 @@ export function validate({ content, filePath, relativePath, fix }) {
|
|
|
113
113
|
const valid = validateFn(data);
|
|
114
114
|
if (!valid) {
|
|
115
115
|
for (const err of validateFn.errors) {
|
|
116
|
+
// Reference pages are OAS-backed: title/excerpt come from the spec, so a
|
|
117
|
+
// missing title is not an error for pages that declare an api.file.
|
|
118
|
+
if (
|
|
119
|
+
dir === 'reference' &&
|
|
120
|
+
data?.api?.file &&
|
|
121
|
+
err.keyword === 'required' &&
|
|
122
|
+
err.params?.missingProperty === 'title'
|
|
123
|
+
) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
if (err.keyword === 'not' && err.schema?.properties) {
|
|
117
128
|
const target = err.instancePath ? getNestedValue(data, err.instancePath) : data;
|
|
118
129
|
if (!target || !Object.keys(err.schema.properties).some((k) => k in target)) continue;
|
package/src/validators/links.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
|
+
import { slugForEntry } from '../utils/slug-for-entry.js';
|
|
4
5
|
|
|
5
6
|
export const name = 'links';
|
|
6
7
|
|
|
@@ -19,14 +20,7 @@ function collectSlugs(gitRoot) {
|
|
|
19
20
|
const entries = fs.readdirSync(dirPath, { recursive: true });
|
|
20
21
|
for (const entry of entries) {
|
|
21
22
|
if (!/\.(md|mdx)$/.test(entry)) continue;
|
|
22
|
-
|
|
23
|
-
if (filename === 'index.md' || filename === 'index.mdx') {
|
|
24
|
-
// index.md represents the parent directory, e.g. plans-and-pricing/index.md -> slug "plans-and-pricing"
|
|
25
|
-
const parentDir = path.basename(path.dirname(entry));
|
|
26
|
-
if (parentDir && parentDir !== '.') slugs.add(parentDir);
|
|
27
|
-
} else {
|
|
28
|
-
slugs.add(filename.replace(/\.(md|mdx)$/, ''));
|
|
29
|
-
}
|
|
23
|
+
slugs.add(slugForEntry(entry));
|
|
30
24
|
}
|
|
31
25
|
}
|
|
32
26
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
|
-
import { findOasFiles, extractOperations, collectExistingPages, syncOas } from '../commands/oas-sync.js';
|
|
4
|
+
import { findOasFiles, extractOperations, collectExistingPages, syncOas, operationKey } from '../commands/oas-sync.js';
|
|
5
5
|
|
|
6
6
|
export const name = 'oas-reference';
|
|
7
7
|
|
|
@@ -34,6 +34,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
34
34
|
|
|
35
35
|
const oasFilename = data.api.file;
|
|
36
36
|
const operationId = data.api.operationId;
|
|
37
|
+
const isWebhook = !!data.api.webhook;
|
|
37
38
|
const oas = oasMap.get(oasFilename);
|
|
38
39
|
|
|
39
40
|
// Check: OAS file doesn't exist.
|
|
@@ -50,7 +51,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
50
51
|
if (!operationId) continue;
|
|
51
52
|
|
|
52
53
|
// Check: operationId doesn't exist in the spec.
|
|
53
|
-
if (!oas.ops.has(operationId)) {
|
|
54
|
+
if (!oas.ops.has(operationKey({ operationId, isWebhook }))) {
|
|
54
55
|
results.push({
|
|
55
56
|
file: relPath,
|
|
56
57
|
rule: name,
|
|
@@ -60,52 +61,26 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
60
61
|
continue;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
// Check both the spec title and the page's directory path.
|
|
65
|
-
const isReadMeConfig = oas.spec.info?.title === 'ReadMeConfig'
|
|
66
|
-
|| relPath.startsWith('reference/ReadMeConfig/');
|
|
67
|
-
if (isReadMeConfig) continue;
|
|
68
|
-
|
|
69
|
-
// Check: title or excerpt out of sync.
|
|
70
|
-
const op = oas.ops.get(operationId);
|
|
71
|
-
const expectedTitle = op.summary || operationId;
|
|
72
|
-
const expectedExcerpt = op.description || null;
|
|
73
|
-
|
|
74
|
-
if (data.title !== expectedTitle) {
|
|
75
|
-
results.push({
|
|
76
|
-
file: relPath,
|
|
77
|
-
rule: name,
|
|
78
|
-
severity: 'warning',
|
|
79
|
-
message: `Out of sync: title is "${data.title}" but spec summary is "${expectedTitle}"`,
|
|
80
|
-
fixable: true,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const currentExcerpt = data.excerpt || null;
|
|
85
|
-
if (currentExcerpt !== expectedExcerpt) {
|
|
86
|
-
results.push({
|
|
87
|
-
file: relPath,
|
|
88
|
-
rule: name,
|
|
89
|
-
severity: 'warning',
|
|
90
|
-
message: `Out of sync: excerpt does not match spec description for "${operationId}"`,
|
|
91
|
-
fixable: true,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
64
|
+
// Title/excerpt are owned by the OAS spec at render time — no sync check here.
|
|
94
65
|
}
|
|
95
66
|
|
|
96
67
|
// Check for missing pages: operations in the spec with no corresponding page.
|
|
97
68
|
const existingPages = collectExistingPages(refDir);
|
|
98
69
|
for (const [oasFilename, { ops }] of oasMap) {
|
|
99
70
|
const pagesForOas = existingPages.filter((p) => p.data.api.file === oasFilename);
|
|
100
|
-
const coveredOps = new Set(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
71
|
+
const coveredOps = new Set(
|
|
72
|
+
pagesForOas.map((p) =>
|
|
73
|
+
operationKey({ operationId: p.data.api.operationId, isWebhook: !!p.data.api.webhook }),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
for (const op of ops.values()) {
|
|
78
|
+
if (!coveredOps.has(operationKey(op))) {
|
|
104
79
|
results.push({
|
|
105
80
|
file: `reference/${oasFilename}`,
|
|
106
81
|
rule: name,
|
|
107
82
|
severity: 'warning',
|
|
108
|
-
message: `Missing page: no reference page found for operation "${
|
|
83
|
+
message: `Missing page: no reference page found for operation "${op.operationId}"`,
|
|
109
84
|
fixable: true,
|
|
110
85
|
});
|
|
111
86
|
}
|