@redpanda-data/docs-extensions-and-macros 5.0.4 → 5.1.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.
@@ -0,0 +1,479 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Diff generator for rpk command documentation
5
+ * Compares two rpk command trees and generates a detailed diff report
6
+ */
7
+
8
+ /**
9
+ * Deep equality check that handles object key ordering
10
+ * Objects with same keys in different order are considered equal
11
+ * @param {*} a - First value
12
+ * @param {*} b - Second value
13
+ * @returns {boolean} True if values are deeply equal
14
+ */
15
+ function deepEqual(a, b) {
16
+ // Handle primitives and null/undefined
17
+ if (a === b) return true
18
+ if (a == null || b == null) return a === b
19
+ if (typeof a !== typeof b) return false
20
+
21
+ // Handle arrays
22
+ if (Array.isArray(a)) {
23
+ if (!Array.isArray(b) || a.length !== b.length) return false
24
+ return a.every((item, i) => deepEqual(item, b[i]))
25
+ }
26
+
27
+ // Handle objects
28
+ if (typeof a === 'object') {
29
+ const keysA = Object.keys(a).sort()
30
+ const keysB = Object.keys(b).sort()
31
+ if (keysA.length !== keysB.length) return false
32
+ if (!keysA.every((key, i) => key === keysB[i])) return false
33
+ return keysA.every(key => deepEqual(a[key], b[key]))
34
+ }
35
+
36
+ // Primitives that aren't strictly equal
37
+ return false
38
+ }
39
+
40
+ /**
41
+ * Flatten command tree into a map of path -> command
42
+ * @param {Object} node - Tree node
43
+ * @param {string} parentPath - Parent command path
44
+ * @returns {Map} Map of command path to command object
45
+ */
46
+ function flattenToMap(node, parentPath = '') {
47
+ const result = new Map()
48
+
49
+ // Defensive check for missing or invalid node
50
+ if (!node || typeof node !== 'object') {
51
+ return result
52
+ }
53
+
54
+ // Validate node.name exists and is a non-empty string
55
+ const nodeName = node.name
56
+ if (typeof nodeName !== 'string' || nodeName.trim() === '') {
57
+ // Skip nodes without valid names but still process children
58
+ if (node.commands && Array.isArray(node.commands)) {
59
+ for (const child of node.commands) {
60
+ const childMap = flattenToMap(child, parentPath)
61
+ for (const [path, cmd] of childMap) {
62
+ result.set(path, cmd)
63
+ }
64
+ }
65
+ }
66
+ return result
67
+ }
68
+
69
+ const currentPath = parentPath ? `${parentPath} ${nodeName}` : nodeName
70
+ result.set(currentPath, node)
71
+
72
+ if (node.commands && Array.isArray(node.commands)) {
73
+ for (const child of node.commands) {
74
+ const childMap = flattenToMap(child, currentPath)
75
+ for (const [path, cmd] of childMap) {
76
+ result.set(path, cmd)
77
+ }
78
+ }
79
+ }
80
+
81
+ return result
82
+ }
83
+
84
+ /**
85
+ * Get flags as a map of name -> flag
86
+ * @param {Object} command - Command object
87
+ * @returns {Map} Map of flag name to flag object
88
+ */
89
+ function getFlagsMap(command) {
90
+ const flags = command.flags || []
91
+ return new Map(flags.map(f => [f.name, f]))
92
+ }
93
+
94
+ /**
95
+ * Compare two flags and find differences
96
+ * @param {Object} oldFlag - Old flag object
97
+ * @param {Object} newFlag - New flag object
98
+ * @returns {Object|null} Differences or null if identical
99
+ */
100
+ function compareFlags(oldFlag, newFlag) {
101
+ // Defensive checks for missing flags
102
+ if (!oldFlag || !newFlag) {
103
+ return null
104
+ }
105
+
106
+ const changes = {}
107
+
108
+ if (oldFlag.type !== newFlag.type) {
109
+ changes.type = { old: oldFlag.type, new: newFlag.type }
110
+ }
111
+
112
+ if (!deepEqual(oldFlag.default, newFlag.default)) {
113
+ changes.default = { old: oldFlag.default, new: newFlag.default }
114
+ }
115
+
116
+ if (oldFlag.description !== newFlag.description) {
117
+ changes.description = { old: oldFlag.description, new: newFlag.description }
118
+ }
119
+
120
+ if (oldFlag.required !== newFlag.required) {
121
+ changes.required = { old: oldFlag.required, new: newFlag.required }
122
+ }
123
+
124
+ return Object.keys(changes).length > 0 ? changes : null
125
+ }
126
+
127
+ /**
128
+ * Generate diff between two rpk command trees
129
+ * @param {Object} oldTree - Old command tree
130
+ * @param {Object} newTree - New command tree
131
+ * @param {Object} options - Options
132
+ * @returns {Object} Diff report
133
+ */
134
+ function generateRpkDiff(oldTree, newTree, options = {}) {
135
+ const { oldVersion = 'old', newVersion = 'new' } = options
136
+
137
+ const oldCommands = flattenToMap(oldTree)
138
+ const newCommands = flattenToMap(newTree)
139
+
140
+ const oldPaths = new Set(oldCommands.keys())
141
+ const newPaths = new Set(newCommands.keys())
142
+
143
+ // Find new commands
144
+ const newCommandPaths = [...newPaths].filter(p => !oldPaths.has(p))
145
+ const newCommandsDetails = newCommandPaths.map(path => {
146
+ const cmd = newCommands.get(path)
147
+ return {
148
+ path,
149
+ name: cmd.name,
150
+ description: cmd.description || '',
151
+ introducedInVersion: newVersion
152
+ }
153
+ })
154
+
155
+ // Find removed commands
156
+ const removedCommandPaths = [...oldPaths].filter(p => !newPaths.has(p))
157
+ const removedCommandsDetails = removedCommandPaths.map(path => {
158
+ const cmd = oldCommands.get(path)
159
+ return {
160
+ path,
161
+ name: cmd.name,
162
+ description: cmd.description || '',
163
+ removedInVersion: newVersion
164
+ }
165
+ })
166
+
167
+ // Find flag changes in existing commands
168
+ const newFlags = []
169
+ const removedFlags = []
170
+ const changedDefaults = []
171
+ const descriptionChanges = []
172
+
173
+ for (const path of newPaths) {
174
+ if (!oldPaths.has(path)) continue // Skip new commands
175
+
176
+ const oldCmd = oldCommands.get(path)
177
+ const newCmd = newCommands.get(path)
178
+
179
+ const oldFlags = getFlagsMap(oldCmd)
180
+ const newFlagsMap = getFlagsMap(newCmd)
181
+
182
+ // Find new flags
183
+ for (const [flagName, flag] of newFlagsMap) {
184
+ if (!oldFlags.has(flagName)) {
185
+ newFlags.push({
186
+ commandPath: path,
187
+ flagName,
188
+ type: flag.type,
189
+ description: flag.description,
190
+ default: flag.default,
191
+ introducedInVersion: newVersion
192
+ })
193
+ }
194
+ }
195
+
196
+ // Find removed flags
197
+ for (const [flagName, flag] of oldFlags) {
198
+ if (!newFlagsMap.has(flagName)) {
199
+ removedFlags.push({
200
+ commandPath: path,
201
+ flagName,
202
+ type: flag.type,
203
+ description: flag.description,
204
+ removedInVersion: newVersion
205
+ })
206
+ }
207
+ }
208
+
209
+ // Find changed flags
210
+ for (const [flagName, newFlag] of newFlagsMap) {
211
+ if (!oldFlags.has(flagName)) continue
212
+
213
+ const oldFlag = oldFlags.get(flagName)
214
+ const changes = compareFlags(oldFlag, newFlag)
215
+
216
+ if (changes) {
217
+ if (changes.default) {
218
+ changedDefaults.push({
219
+ commandPath: path,
220
+ flagName,
221
+ oldDefault: changes.default.old,
222
+ newDefault: changes.default.new
223
+ })
224
+ }
225
+ }
226
+ }
227
+
228
+ // Check command description changes
229
+ if (oldCmd.description !== newCmd.description) {
230
+ descriptionChanges.push({
231
+ path,
232
+ type: 'command',
233
+ oldDescription: oldCmd.description,
234
+ newDescription: newCmd.description
235
+ })
236
+ }
237
+ }
238
+
239
+ return {
240
+ comparison: {
241
+ oldVersion,
242
+ newVersion,
243
+ timestamp: new Date().toISOString()
244
+ },
245
+ summary: {
246
+ newCommands: newCommandsDetails.length,
247
+ removedCommands: removedCommandsDetails.length,
248
+ newFlags: newFlags.length,
249
+ removedFlags: removedFlags.length,
250
+ changedDefaults: changedDefaults.length,
251
+ descriptionChanges: descriptionChanges.length
252
+ },
253
+ details: {
254
+ newCommands: newCommandsDetails,
255
+ removedCommands: removedCommandsDetails,
256
+ newFlags,
257
+ removedFlags,
258
+ changedDefaults,
259
+ descriptionChanges
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Print a human-readable diff report to console
266
+ * @param {Object} diff - Diff object from generateRpkDiff
267
+ */
268
+ function printDiffReport(diff) {
269
+ console.log('\n=== rpk Documentation Diff Report ===')
270
+ console.log(`Comparing ${diff.comparison.oldVersion} → ${diff.comparison.newVersion}`)
271
+ console.log(`Generated: ${diff.comparison.timestamp}\n`)
272
+
273
+ console.log('Summary:')
274
+ console.log(` New commands: ${diff.summary.newCommands}`)
275
+ console.log(` Deprecated commands: ${diff.summary.removedCommands}`)
276
+ console.log(` New flags: ${diff.summary.newFlags}`)
277
+ console.log(` Deprecated flags: ${diff.summary.removedFlags}`)
278
+ console.log(` Changed defaults: ${diff.summary.changedDefaults}`)
279
+ console.log(` Description changes: ${diff.summary.descriptionChanges}`)
280
+
281
+ if (diff.details.newCommands.length > 0) {
282
+ console.log('\nNew Commands:')
283
+ for (const cmd of diff.details.newCommands) {
284
+ console.log(` + ${cmd.path}`)
285
+ if (cmd.description) {
286
+ const shortDesc = cmd.description.split('\n')[0].substring(0, 60)
287
+ console.log(` ${shortDesc}${cmd.description.length > 60 ? '...' : ''}`)
288
+ }
289
+ }
290
+ }
291
+
292
+ if (diff.details.removedCommands.length > 0) {
293
+ console.log('\nDeprecated Commands (no longer in command tree):')
294
+ for (const cmd of diff.details.removedCommands) {
295
+ console.log(` ⚠ ${cmd.path}`)
296
+ }
297
+ }
298
+
299
+ if (diff.details.newFlags.length > 0) {
300
+ console.log('\nNew Flags:')
301
+ for (const flag of diff.details.newFlags) {
302
+ console.log(` + ${flag.commandPath} --${flag.flagName} (${flag.type})`)
303
+ }
304
+ }
305
+
306
+ if (diff.details.removedFlags.length > 0) {
307
+ console.log('\nDeprecated Flags (no longer in command tree):')
308
+ for (const flag of diff.details.removedFlags) {
309
+ console.log(` ⚠ ${flag.commandPath} --${flag.flagName}`)
310
+ }
311
+ }
312
+
313
+ if (diff.details.changedDefaults.length > 0) {
314
+ console.log('\nChanged Defaults:')
315
+ for (const change of diff.details.changedDefaults) {
316
+ console.log(` ~ ${change.commandPath} --${change.flagName}`)
317
+ console.log(` ${JSON.stringify(change.oldDefault)} → ${JSON.stringify(change.newDefault)}`)
318
+ }
319
+ }
320
+
321
+ console.log('')
322
+ }
323
+
324
+ /**
325
+ * Generate markdown summary for PR description
326
+ * @param {Object} diff - Diff object
327
+ * @returns {string} Markdown content
328
+ */
329
+ function generateMarkdownSummary(diff) {
330
+ const lines = []
331
+
332
+ lines.push(`## rpk Documentation Changes`)
333
+ lines.push(``)
334
+ lines.push(`**Version:** ${diff.comparison.oldVersion} → ${diff.comparison.newVersion}`)
335
+ lines.push(``)
336
+
337
+ lines.push(`### Summary`)
338
+ lines.push(``)
339
+ lines.push(`| Category | Count |`)
340
+ lines.push(`|----------|-------|`)
341
+ lines.push(`| New commands | ${diff.summary.newCommands} |`)
342
+ lines.push(`| Deprecated commands | ${diff.summary.removedCommands} |`)
343
+ lines.push(`| New flags | ${diff.summary.newFlags} |`)
344
+ lines.push(`| Deprecated flags | ${diff.summary.removedFlags} |`)
345
+ lines.push(`| Changed defaults | ${diff.summary.changedDefaults} |`)
346
+ lines.push(``)
347
+
348
+ if (diff.details.newCommands.length > 0) {
349
+ lines.push(`### New Commands`)
350
+ lines.push(``)
351
+ for (const cmd of diff.details.newCommands) {
352
+ lines.push(`- \`${cmd.path}\``)
353
+ }
354
+ lines.push(``)
355
+ }
356
+
357
+ if (diff.details.removedCommands.length > 0) {
358
+ lines.push(`### Deprecated Commands`)
359
+ lines.push(``)
360
+ lines.push(`> Commands no longer in the active command tree. These may still work but are deprecated.`)
361
+ lines.push(``)
362
+ for (const cmd of diff.details.removedCommands) {
363
+ lines.push(`- ~~\`${cmd.path}\`~~`)
364
+ }
365
+ lines.push(``)
366
+ }
367
+
368
+ if (diff.details.newFlags.length > 0 && diff.details.newFlags.length <= 20) {
369
+ lines.push(`### New Flags`)
370
+ lines.push(``)
371
+ for (const flag of diff.details.newFlags) {
372
+ lines.push(`- \`${flag.commandPath}\`: \`--${flag.flagName}\` (${flag.type})`)
373
+ }
374
+ lines.push(``)
375
+ } else if (diff.details.newFlags.length > 20) {
376
+ lines.push(`### New Flags`)
377
+ lines.push(``)
378
+ lines.push(`${diff.details.newFlags.length} new flags added. See diff JSON for details.`)
379
+ lines.push(``)
380
+ }
381
+
382
+ return lines.join('\n')
383
+ }
384
+
385
+ /**
386
+ * Convert command path to xref path
387
+ * @param {string} commandPath - Command path like "rpk cluster info"
388
+ * @returns {string} Xref path like "rpk-cluster/rpk-cluster-info.adoc"
389
+ */
390
+ function commandPathToXref(commandPath) {
391
+ const parts = commandPath.split(' ')
392
+ if (parts.length === 1) {
393
+ return `rpk.adoc`
394
+ }
395
+ const dashified = commandPath.replace(/ /g, '-')
396
+ if (parts.length === 2) {
397
+ return `${dashified}.adoc`
398
+ }
399
+ // For deeper commands: rpk cluster info -> rpk-cluster/rpk-cluster-info.adoc
400
+ const parentDir = parts.slice(0, 2).join('-')
401
+ return `${parentDir}/${dashified}.adoc`
402
+ }
403
+
404
+ /**
405
+ * Generate AsciiDoc content for what's-new file
406
+ * @param {Object} diff - Diff object from generateRpkDiff
407
+ * @param {Object} options - Options
408
+ * @param {string} options.version - Version string to display
409
+ * @returns {string} AsciiDoc content
410
+ */
411
+ function generateWhatsNewSection(diff, options = {}) {
412
+ const lines = []
413
+ const version = options.version || diff.comparison.newVersion
414
+
415
+ // Check if there are any changes worth documenting
416
+ const hasNewCommands = diff.details.newCommands.length > 0
417
+ const hasNewFlags = diff.details.newFlags.length > 0
418
+ const hasChangedDefaults = diff.details.changedDefaults.length > 0
419
+
420
+ if (!hasNewCommands && !hasNewFlags && !hasChangedDefaults) {
421
+ return '' // No changes to document
422
+ }
423
+
424
+ lines.push(`== Redpanda CLI`)
425
+ lines.push(``)
426
+
427
+ if (hasNewCommands) {
428
+ lines.push(`=== New commands`)
429
+ lines.push(``)
430
+ for (const cmd of diff.details.newCommands) {
431
+ const xrefPath = commandPathToXref(cmd.path)
432
+ const desc = cmd.description ? ` - ${cmd.description}` : ''
433
+ lines.push(`* xref:reference:rpk/${xrefPath}[\`${cmd.path}\`]${desc}`)
434
+ }
435
+ lines.push(``)
436
+ }
437
+
438
+ if (hasNewFlags) {
439
+ lines.push(`=== New flags`)
440
+ lines.push(``)
441
+ // Group flags by command
442
+ const flagsByCommand = {}
443
+ for (const flag of diff.details.newFlags) {
444
+ if (!flagsByCommand[flag.commandPath]) {
445
+ flagsByCommand[flag.commandPath] = []
446
+ }
447
+ flagsByCommand[flag.commandPath].push(flag)
448
+ }
449
+
450
+ for (const [cmdPath, flags] of Object.entries(flagsByCommand)) {
451
+ const xrefPath = commandPathToXref(cmdPath)
452
+ const flagList = flags.map(f => `\`--${f.flagName}\``).join(', ')
453
+ lines.push(`* xref:reference:rpk/${xrefPath}[\`${cmdPath}\`]: Added ${flagList}`)
454
+ }
455
+ lines.push(``)
456
+ }
457
+
458
+ if (hasChangedDefaults) {
459
+ lines.push(`=== Changed defaults`)
460
+ lines.push(``)
461
+ for (const change of diff.details.changedDefaults) {
462
+ const xrefPath = commandPathToXref(change.commandPath)
463
+ lines.push(`* xref:reference:rpk/${xrefPath}[\`${change.commandPath}\`]: \`--${change.flagName}\` default changed from \`${change.oldDefault}\` to \`${change.newDefault}\``)
464
+ }
465
+ lines.push(``)
466
+ }
467
+
468
+ return lines.join('\n')
469
+ }
470
+
471
+ module.exports = {
472
+ generateRpkDiff,
473
+ printDiffReport,
474
+ generateMarkdownSummary,
475
+ generateWhatsNewSection,
476
+ flattenToMap,
477
+ getFlagsMap,
478
+ compareFlags
479
+ }