@redpanda-data/docs-extensions-and-macros 5.12.1 → 5.12.3

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.
@@ -11,6 +11,7 @@ const handlebars = require('handlebars')
11
11
 
12
12
  // Import and register Redpanda Connect helpers
13
13
  const helpers = require('../tools/redpanda-connect/helpers')
14
+ const { flattenConnectFields } = require('../tools/redpanda-connect/helpers/flattenConnectFields')
14
15
  Object.entries(helpers).forEach(([name, fn]) => {
15
16
  if (typeof fn === 'function') {
16
17
  handlebars.registerHelper(name, fn)
@@ -25,18 +26,11 @@ handlebars.registerHelper('renderConnectFieldsTable', function (children) {
25
26
 
26
27
  const rows = []
27
28
 
28
- function collectFields (fieldsList, pathPrefix = '') {
29
- if (!Array.isArray(fieldsList)) return
30
-
31
- fieldsList.forEach(field => {
32
- if (field.is_deprecated || !field.name) return
33
-
34
- const isArray = field.kind === 'array'
35
- const nameWithArray = (typeof field.name === 'string' && isArray && !field.name.endsWith('[]'))
36
- ? `${field.name}[]`
37
- : field.name
38
- const currentPath = pathPrefix ? `${pathPrefix}.${nameWithArray}` : nameWithArray
39
-
29
+ // The dotted paths, the `[]` array marker and the deprecated-field skipping
30
+ // all come from the shared walker, so this table, the reference-page
31
+ // headings and the connector delta report cannot drift apart.
32
+ flattenConnectFields(children, { arrayMarker: true, skipDeprecated: true })
33
+ .forEach(({ path: currentPath, field }) => {
40
34
  // Normalize type
41
35
  let displayType
42
36
  const isArrayTitle = typeof field.name === 'string' && field.name.endsWith('[]')
@@ -88,15 +82,7 @@ handlebars.registerHelper('renderConnectFieldsTable', function (children) {
88
82
  default: defaultValue || '-',
89
83
  description: desc || '-'
90
84
  })
91
-
92
- // Recurse for children
93
- if (field.children && Array.isArray(field.children) && field.children.length > 0) {
94
- collectFields(field.children, currentPath)
95
- }
96
85
  })
97
- }
98
-
99
- collectFields(children, '')
100
86
 
101
87
  if (rows.length === 0) return 'No configuration fields available.\n\n'
102
88
 
@@ -273,7 +273,14 @@ function buildComponentVersionNavMap(components) {
273
273
  map.set(component.name, {
274
274
  component,
275
275
  versionMap,
276
- latestVersion: component.latestVersion || component.versions[0],
276
+ // component.latest, not component.latestVersion: the latter is not part of
277
+ // Antora's component model (content-catalog.js sets `latest` and
278
+ // `latestPrerelease`), so it was always undefined and this always fell
279
+ // through to versions[0]. Antora stores versions newest-first WITH
280
+ // prereleases at the front, so on any build containing a prerelease the
281
+ // whole bucket -- component metadata, nav tree, version dropdown -- was
282
+ // read from the unreleased version. Nothing threw, so nothing showed it.
283
+ latestVersion: component.latest || component.versions[0],
277
284
  })
278
285
  }
279
286
 
@@ -550,7 +557,7 @@ function buildBucketsFromConfig(
550
557
  return null // Component not found
551
558
  }
552
559
 
553
- const latestVersion = component.latestVersion || component.versions[0]
560
+ const latestVersion = component.latest || component.versions[0]
554
561
  if (!latestVersion) {
555
562
  return null
556
563
  }
@@ -646,7 +653,7 @@ function findComponentPath(configTree, targetComponent, contentCatalog) {
646
653
 
647
654
  if (!component) continue
648
655
 
649
- const latestVersion = component.latestVersion || component.versions[0]
656
+ const latestVersion = component.latest || component.versions[0]
650
657
  const metadata = latestVersion ? getHeaderData(latestVersion) : null
651
658
 
652
659
  const breadcrumbItem = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.12.1",
3
+ "version": "5.12.3",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared walker for Redpanda Connect field trees.
5
+ *
6
+ * This is a plain module, not a Handlebars helper, so it is deliberately not
7
+ * exported from helpers/index.js. It exists because the same recursion was
8
+ * written four times (the reference-page renderer, the fields-only table
9
+ * extension and the connector delta report), and the copies disagreed about
10
+ * what a field path looks like. The renderer is the authority: the path this
11
+ * module produces must match the `=== ` heading renderConnectFields emits,
12
+ * because the what's-new tables link to those headings.
13
+ */
14
+
15
+ /**
16
+ * Field name as it appears in the rendered docs: an array-of-object field
17
+ * carries an `[]` marker so the path is a valid config path.
18
+ * @param {object} field - Field definition
19
+ * @returns {string} Display name for the field
20
+ */
21
+ function connectFieldName (field) {
22
+ const name = field && field.name;
23
+ if (typeof name === 'string' && field.kind === 'array' && !name.endsWith('[]')) {
24
+ return `${name}[]`;
25
+ }
26
+ return name;
27
+ }
28
+
29
+ /**
30
+ * Walk a field tree depth-first and return one entry per field, in the order
31
+ * the fields appear in the source data.
32
+ *
33
+ * A field nested under an existing group (for example
34
+ * `sqs.zero_key_warn_interval` under aws_s3's pre-existing `sqs` group) is
35
+ * only visible to a set-based diff if the tree is flattened first: a map of
36
+ * top-level names alone treats that as no change at all, because `sqs` itself
37
+ * is not new.
38
+ *
39
+ * Nameless nodes are skipped along with their subtree, matching
40
+ * renderConnectFields: a node with no name gets no heading, so nothing under
41
+ * it is addressable in the published docs.
42
+ *
43
+ * @param {Array} children - Field definitions (each may itself have `children`)
44
+ * @param {object} [opts] - { arrayMarker, skipDeprecated, prefix }
45
+ * @param {boolean} [opts.arrayMarker=false] - Append `[]` to array-of-object names
46
+ * @param {boolean} [opts.skipDeprecated=false] - Skip deprecated fields and their subtrees
47
+ * @param {string} [opts.prefix=''] - Dotted path of the parent, if any
48
+ * @returns {Array<{path: string, name: string, parentPath: (string|null), field: object}>}
49
+ */
50
+ function flattenConnectFields (children, opts = {}) {
51
+ const { arrayMarker = false, skipDeprecated = false, prefix = '' } = opts;
52
+ const result = [];
53
+
54
+ const walk = (list, currentPrefix, parentPath) => {
55
+ if (!Array.isArray(list)) return;
56
+ list.forEach(field => {
57
+ if (!field || !field.name) return;
58
+ if (skipDeprecated && field.is_deprecated) return;
59
+
60
+ const name = arrayMarker ? connectFieldName(field) : field.name;
61
+ const path = currentPrefix ? `${currentPrefix}.${name}` : `${name}`;
62
+
63
+ result.push({ path, name, parentPath, field });
64
+ walk(field.children, path, path);
65
+ });
66
+ };
67
+
68
+ walk(children, prefix, prefix || null);
69
+
70
+ return result;
71
+ }
72
+
73
+ module.exports = { flattenConnectFields, connectFieldName };
@@ -1,6 +1,7 @@
1
1
  const yaml = require('yaml');
2
2
  const renderYamlList = require('./renderYamlList');
3
3
  const handlebars = require('handlebars');
4
+ const { connectFieldName } = require('./flattenConnectFields');
4
5
 
5
6
  /**
6
7
  * Renders the children of a configuration object into AsciiDoc.
@@ -56,11 +57,10 @@ module.exports = function renderConnectFields(children, prefix = '') {
56
57
  }
57
58
 
58
59
  let block = '';
59
- const isArray = child.kind === 'array';
60
- // Only append [] if kind is array and name does not already end with []
61
- const nameWithArray = (typeof child.name === 'string' && isArray && !child.name.endsWith('[]'))
62
- ? `${child.name}[]`
63
- : child.name;
60
+ // Only append [] if kind is array and name does not already end with [].
61
+ // Shared with the delta report and the fields-only table so all three
62
+ // producers agree on what a field path looks like.
63
+ const nameWithArray = connectFieldName(child);
64
64
  const currentPath = prefix
65
65
  ? `${prefix}.${nameWithArray}`
66
66
  : `${nameWithArray}`;
@@ -1,4 +1,5 @@
1
1
  const { execSync } = require('child_process');
2
+ const { flattenConnectFields } = require('./helpers/flattenConnectFields');
2
3
 
3
4
  /**
4
5
  * Generate a JSON diff report between two connector index objects.
@@ -54,22 +55,29 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
54
55
  if (!(cKey in oldMap)) return;
55
56
  const oldFields = new Set(oldMap[cKey].fields || []);
56
57
  const newFieldsArr = newMap[cKey].fields || [];
57
- newFieldsArr.forEach(fName => {
58
- if (!oldFields.has(fName)) {
59
- const [type, compName] = cKey.split(':');
60
- let rawFieldObj = null;
61
- if (type === 'config') {
62
- rawFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
63
- } else {
64
- rawFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
65
- }
66
- newFields.push({
67
- component: cKey,
68
- field: fName,
69
- introducedIn: rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version),
70
- description: rawFieldObj && rawFieldObj.description
71
- });
72
- }
58
+ const added = collapseToTopmost(
59
+ newFieldsArr.filter(fName => !oldFields.has(fName)),
60
+ newMap[cKey].parents
61
+ );
62
+ added.forEach(fName => {
63
+ const rawFieldObj = newMap[cKey].fieldMap.get(fName) || null;
64
+ // A field that arrives already deprecated -- or sits under a
65
+ // deprecated ancestor group -- gets no heading on the reference page,
66
+ // so advertising it under "New field support" would link to a fragment
67
+ // that never exists.
68
+ if (isDeprecatedOrDescendsFromDeprecated(fName, newMap[cKey].fieldMap, newMap[cKey].parents)) return;
69
+ newFields.push({
70
+ component: cKey,
71
+ field: fName,
72
+ // Most fields carry no version of their own, so fall back to the
73
+ // version this diff is comparing against: the field is new as of
74
+ // exactly that release. Some fields do carry `version` (268 entries
75
+ // in connect-4.105.0), and that wins, because it can predate the
76
+ // span being compared: inputs:oracledb_cdc/snapshot_mode reports
77
+ // 4.99.0 in the 4.98.0 to 4.103.1 diff.
78
+ introducedIn: (rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version)) || opts.newVersion || null,
79
+ description: rawFieldObj && rawFieldObj.description
80
+ });
73
81
  });
74
82
  });
75
83
 
@@ -79,13 +87,15 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
79
87
  if (!(cKey in newMap)) return;
80
88
  const newFieldsSet = new Set(newMap[cKey].fields || []);
81
89
  const oldFieldsArr = oldMap[cKey].fields || [];
82
- oldFieldsArr.forEach(fName => {
83
- if (!newFieldsSet.has(fName)) {
84
- removedFields.push({
85
- component: cKey,
86
- field: fName
87
- });
88
- }
90
+ const removed = collapseToTopmost(
91
+ oldFieldsArr.filter(fName => !newFieldsSet.has(fName)),
92
+ oldMap[cKey].parents
93
+ );
94
+ removed.forEach(fName => {
95
+ removedFields.push({
96
+ component: cKey,
97
+ field: fName
98
+ });
89
99
  });
90
100
  });
91
101
 
@@ -172,26 +182,11 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
172
182
  // Check fields that exist in both versions
173
183
  const commonFields = oldFieldsArr.filter(f => newFieldsArr.includes(f));
174
184
  commonFields.forEach(fName => {
175
- const [type, compName] = cKey.split(':');
185
+ const oldFieldObj = oldMap[cKey].fieldMap.get(fName) || null;
186
+ const newFieldObj = newMap[cKey].fieldMap.get(fName) || null;
176
187
 
177
- // Get old field object
178
- let oldFieldObj = null;
179
- if (type === 'config') {
180
- oldFieldObj = (oldMap[cKey].raw.children || []).find(f => f.name === fName);
181
- } else {
182
- oldFieldObj = (oldMap[cKey].raw.config?.children || []).find(f => f.name === fName);
183
- }
184
-
185
- // Get new field object
186
- let newFieldObj = null;
187
- if (type === 'config') {
188
- newFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
189
- } else {
190
- newFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
191
- }
192
-
193
- const oldDeprecated = oldFieldObj && (oldFieldObj.is_deprecated === true || oldFieldObj.deprecated === true || (oldFieldObj.status || '').toLowerCase() === 'deprecated');
194
- const newDeprecated = newFieldObj && (newFieldObj.is_deprecated === true || newFieldObj.deprecated === true || (newFieldObj.status || '').toLowerCase() === 'deprecated');
188
+ const oldDeprecated = isDeprecatedField(oldFieldObj);
189
+ const newDeprecated = isDeprecatedField(newFieldObj);
195
190
 
196
191
  if (!oldDeprecated && newDeprecated) {
197
192
  deprecatedFields.push({
@@ -380,6 +375,67 @@ function discoverComponentKeys(obj) {
380
375
  return Object.keys(obj).filter(key => Array.isArray(obj[key]));
381
376
  }
382
377
 
378
+ /**
379
+ * Whether a field is marked deprecated.
380
+ *
381
+ * Three markers have been used by the source data over time. Only
382
+ * `is_deprecated` appears in current releases (verified across connect-4.63.0,
383
+ * 4.104.0 and 4.105.0: 85, 75 and 75 fields, zero uses of the other two), and
384
+ * it is the one renderConnectFields keys on when it decides not to render a
385
+ * heading for a field.
386
+ *
387
+ * @param {object} field - Field definition, or null
388
+ * @returns {boolean} True when the field is deprecated
389
+ */
390
+ function isDeprecatedField(field) {
391
+ if (!field) return false;
392
+ return field.is_deprecated === true ||
393
+ field.deprecated === true ||
394
+ (field.status || '').toLowerCase() === 'deprecated';
395
+ }
396
+
397
+ /**
398
+ * True if `fName` or any of its ancestor fields is deprecated.
399
+ *
400
+ * renderConnectFields never renders a field under a deprecated group (the
401
+ * whole group is hidden), so a leaf-only check reports a field added under an
402
+ * existing-but-deprecated group in "New field support" even though it never
403
+ * appears on the page. Walking the parent chain via `parents` is what
404
+ * catches that.
405
+ */
406
+ function isDeprecatedOrDescendsFromDeprecated(fName, fieldMap, parents) {
407
+ let current = fName;
408
+ while (current) {
409
+ if (isDeprecatedField(fieldMap.get(current))) return true;
410
+ current = parents && parents.get(current);
411
+ }
412
+ return false;
413
+ }
414
+
415
+ /**
416
+ * Drop paths whose parent changed in the same way, keeping only the topmost
417
+ * field of each changed subtree.
418
+ *
419
+ * Adding or removing one group adds or removes every field under it, and the
420
+ * flattened field list contains all of them. Reporting each descendant turns
421
+ * a single change into one row per node: removing `batching` from
422
+ * outputs:redpanda_migrator would list `batching` plus batching.count,
423
+ * .byte_size, .period, .check and .processors, and the group row already says
424
+ * all of that. Only the fields added to or removed from a group that still
425
+ * exists are news.
426
+ *
427
+ * @param {Array<string>} changedPaths - Paths that are new (or removed)
428
+ * @param {Map<string, (string|null)>} parents - Path to parent path
429
+ * @returns {Array<string>} The topmost changed path of each subtree, in order
430
+ */
431
+ function collapseToTopmost(changedPaths, parents) {
432
+ const changed = new Set(changedPaths);
433
+ return changedPaths.filter(path => {
434
+ const parent = parents && parents.get(path);
435
+ return !(parent && changed.has(parent));
436
+ });
437
+ }
438
+
383
439
  function buildComponentMap(indexObj) {
384
440
  const map = {};
385
441
  const types = discoverComponentKeys(indexObj);
@@ -402,7 +458,14 @@ function buildComponentMap(indexObj) {
402
458
  }
403
459
  }
404
460
 
405
- const fieldNames = childArray.map(f => f.name);
461
+ // arrayMarker keeps the paths identical to the headings
462
+ // renderConnectFields emits, so a field name in the what's-new table is
463
+ // a config path a reader can paste into YAML (`sasl[].aws.tcp`, not the
464
+ // invalid `sasl.aws.tcp`).
465
+ const flattened = flattenConnectFields(childArray, { arrayMarker: true });
466
+ const fieldNames = flattened.map(f => f.path);
467
+ const fieldMap = new Map(flattened.map(f => [f.path, f.field]));
468
+ const parents = new Map(flattened.map(f => [f.path, f.parentPath]));
406
469
 
407
470
  // Preserve platform metadata for accurate diff comparison
408
471
  const metadata = {
@@ -414,6 +477,8 @@ function buildComponentMap(indexObj) {
414
477
  map[lookupKey] = {
415
478
  raw: component,
416
479
  fields: fieldNames,
480
+ fieldMap: fieldMap,
481
+ parents: parents,
417
482
  metadata: metadata
418
483
  };
419
484
  });
@@ -499,8 +564,11 @@ function getRpkConnectVersion() {
499
564
  * • which connectors/components are brand-new
500
565
  * • which new fields appeared under existing connectors (including “config” entries)
501
566
  * • for each new component/field, if the raw object contains “version” or “introducedInVersion” or “requiresVersion” metadata, print it
567
+ * @param {object} oldIndex - Previous version connector index
568
+ * @param {object} newIndex - Current version connector index
569
+ * @param {object} [opts] - { newVersion } used as the introducedIn fallback
502
570
  */
503
- function printDeltaReport(oldIndex, newIndex) {
571
+ function printDeltaReport(oldIndex, newIndex, opts = {}) {
504
572
  const oldMap = buildComponentMap(oldIndex);
505
573
  const newMap = buildComponentMap(newIndex);
506
574
 
@@ -513,27 +581,24 @@ function printDeltaReport(oldIndex, newIndex) {
513
581
  if (!(cKey in oldMap)) return; // skip brand-new components here
514
582
  const oldFields = new Set(oldMap[cKey].fields || []);
515
583
  const newFieldsArr = newMap[cKey].fields || [];
516
- newFieldsArr.forEach(fName => {
517
- if (!oldFields.has(fName)) {
518
- // fetch raw field metadata if available
519
- const [type, compName] = cKey.split(':');
520
- let rawFieldObj = null;
521
- if (type === 'config') {
522
- rawFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
523
- } else {
524
- rawFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
525
- }
526
-
527
- let introducedIn = rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version);
528
- let requiresVer = rawFieldObj && rawFieldObj.requiresVersion;
529
-
530
- newFields.push({
531
- component: cKey,
532
- field: fName,
533
- introducedIn,
534
- requiresVersion: requiresVer,
535
- });
536
- }
584
+ const added = collapseToTopmost(
585
+ newFieldsArr.filter(fName => !oldFields.has(fName)),
586
+ newMap[cKey].parents
587
+ );
588
+ added.forEach(fName => {
589
+ const rawFieldObj = newMap[cKey].fieldMap.get(fName) || null;
590
+ if (isDeprecatedField(rawFieldObj)) return;
591
+ // Same fallback as generateConnectorDiffJson, so the console report and
592
+ // the published table cannot disagree about the same input.
593
+ const introducedIn = (rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version)) || opts.newVersion || null;
594
+ const requiresVer = rawFieldObj && rawFieldObj.requiresVersion;
595
+
596
+ newFields.push({
597
+ component: cKey,
598
+ field: fName,
599
+ introducedIn,
600
+ requiresVersion: requiresVer,
601
+ });
537
602
  });
538
603
  });
539
604
 
@@ -560,21 +625,10 @@ function printDeltaReport(oldIndex, newIndex) {
560
625
  const commonFields = oldFieldsArr.filter(f => newFieldsArr.includes(f));
561
626
 
562
627
  commonFields.forEach(fName => {
563
- const [type, compName] = cKey.split(':');
564
- let oldFieldObj = null;
565
- if (type === 'config') {
566
- oldFieldObj = (oldMap[cKey].raw.children || []).find(f => f.name === fName);
567
- } else {
568
- oldFieldObj = (oldMap[cKey].raw.config?.children || []).find(f => f.name === fName);
569
- }
570
- let newFieldObj = null;
571
- if (type === 'config') {
572
- newFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
573
- } else {
574
- newFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
575
- }
576
- const oldDeprecated = oldFieldObj && (oldFieldObj.is_deprecated === true || oldFieldObj.deprecated === true || (oldFieldObj.status || '').toLowerCase() === 'deprecated');
577
- const newDeprecated = newFieldObj && (newFieldObj.is_deprecated === true || newFieldObj.deprecated === true || (newFieldObj.status || '').toLowerCase() === 'deprecated');
628
+ const oldFieldObj = oldMap[cKey].fieldMap.get(fName) || null;
629
+ const newFieldObj = newMap[cKey].fieldMap.get(fName) || null;
630
+ const oldDeprecated = isDeprecatedField(oldFieldObj);
631
+ const newDeprecated = isDeprecatedField(newFieldObj);
578
632
  if (!oldDeprecated && newDeprecated) {
579
633
  deprecatedFieldsList.push({ component: cKey, field: fName });
580
634
  }
@@ -413,7 +413,7 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
413
413
  if (regularFields.length > 0) {
414
414
  section += '\n=== New field support\n\n'
415
415
  section += 'This release adds support for the following new fields:\n\n'
416
- section += buildFieldsTable(regularFields, capToTwoSentences)
416
+ section += buildFieldsTable(regularFields, capToTwoSentences, { showIntroducedIn: true })
417
417
  }
418
418
  }
419
419
 
@@ -553,24 +553,35 @@ function fieldAnchor (fieldName) {
553
553
  * @param {Function} capFn - Caption function
554
554
  * @returns {string} AsciiDoc table
555
555
  */
556
- function buildFieldsTable (fields, capFn) {
556
+ function buildFieldsTable (fields, capFn, opts = {}) {
557
+ const { showIntroducedIn = false } = opts
557
558
  const byField = {}
558
559
  for (const field of fields) {
559
560
  const [type, compName] = field.component.split(':')
560
- if (!byField[field.field]) {
561
- byField[field.field] = {
561
+ // Two components can gain the same field path in different releases
562
+ // (`urls` arrived in 3.58.0 for one and 4.23.0 for another). Merging those
563
+ // into a single row would publish the first version for both, so when the
564
+ // version is on show it is part of the row's identity.
565
+ const key = showIntroducedIn
566
+ ? `${field.field}\u0000${field.introducedIn || ''}`
567
+ : field.field
568
+ if (!byField[key]) {
569
+ byField[key] = {
570
+ field: field.field,
562
571
  description: field.description,
572
+ introducedIn: field.introducedIn,
563
573
  components: []
564
574
  }
565
575
  }
566
- byField[field.field].components.push({ type, name: compName })
576
+ byField[key].components.push({ type, name: compName })
567
577
  }
568
578
 
569
- let section = '[cols="1m,3,2a"]\n'
579
+ let section = showIntroducedIn ? '[cols="1m,3,2a,1m"]\n' : '[cols="1m,3,2a"]\n'
570
580
  section += '|===\n'
571
- section += '|Field |Description |Affected components\n\n'
581
+ section += showIntroducedIn ? '|Field |Description |Affected components |Introduced in\n\n' : '|Field |Description |Affected components\n\n'
572
582
 
573
- for (const [fieldName, info] of Object.entries(byField)) {
583
+ for (const info of Object.values(byField)) {
584
+ const fieldName = info.field
574
585
  const byType = {}
575
586
  for (const comp of info.components) {
576
587
  if (!byType[comp.type]) byType[comp.type] = []
@@ -595,7 +606,9 @@ function buildFieldsTable (fields, capFn) {
595
606
 
596
607
  section += `|${fieldName}\n`
597
608
  section += `|${desc}\n`
598
- section += `|${componentList}\n\n`
609
+ section += `|${componentList}\n`
610
+ if (showIntroducedIn) section += `|${info.introducedIn || '-'}\n`
611
+ section += '\n'
599
612
  }
600
613
 
601
614
  section += '|===\n\n'
@@ -1475,7 +1488,7 @@ async function handleRpcnConnectorDocs (options) {
1475
1488
  }
1476
1489
  }
1477
1490
 
1478
- printDeltaReport(oldIndexForDiff, newIndex)
1491
+ printDeltaReport(oldIndexForDiff, newIndex, { newVersion })
1479
1492
 
1480
1493
  const { generateConnectorDiffJson } = require('./report-delta.js')
1481
1494
  diffJson = generateConnectorDiffJson(