docusaurus-plugin-generate-schema-docs 1.7.1 → 1.8.1

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 (29) hide show
  1. package/README.md +22 -0
  2. package/__tests__/ExampleDataLayer.test.js +149 -2
  3. package/__tests__/__fixtures__/schema-processing/components/dataLayer.json +9 -0
  4. package/__tests__/__fixtures__/schema-processing/event-reference.json +14 -0
  5. package/__tests__/__fixtures__/schema-processing/purchase-event.json +14 -0
  6. package/__tests__/components/PropertyRow.test.js +30 -0
  7. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
  8. package/__tests__/helpers/exampleModel.test.js +135 -0
  9. package/__tests__/helpers/schema-processing.test.js +56 -0
  10. package/__tests__/helpers/schemaToTableData.test.js +41 -0
  11. package/__tests__/helpers/snippetTargets.test.js +744 -0
  12. package/__tests__/helpers/trackingTargets.test.js +42 -0
  13. package/__tests__/runtimePayload.android.test.js +292 -0
  14. package/__tests__/runtimePayload.ios.test.js +282 -0
  15. package/__tests__/runtimePayload.web.test.js +32 -0
  16. package/__tests__/validateSchemas.test.js +53 -1
  17. package/components/ExampleDataLayer.js +191 -56
  18. package/components/PropertyRow.js +3 -2
  19. package/components/SchemaRows.css +10 -1
  20. package/helpers/exampleModel.js +70 -0
  21. package/helpers/schema-processing.js +41 -5
  22. package/helpers/schemaToExamples.js +52 -2
  23. package/helpers/schemaToTableData.js +40 -2
  24. package/helpers/snippetTargets.js +853 -0
  25. package/helpers/trackingTargets.js +73 -0
  26. package/helpers/validator.js +1 -0
  27. package/package.json +1 -1
  28. package/test-data/payloadContracts.js +155 -0
  29. package/validateSchemas.js +15 -0
@@ -10,13 +10,16 @@ import validateSchemas from '../validateSchemas';
10
10
  describe('validateSchemas', () => {
11
11
  let tmpDir;
12
12
  let consoleErrorSpy;
13
- let consoleLogSpy;
13
+ let consoleWarnSpy;
14
14
 
15
15
  beforeEach(() => {
16
16
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-test-'));
17
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
18
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
17
19
  });
18
20
 
19
21
  afterEach(() => {
22
+ jest.restoreAllMocks();
20
23
  fs.rmSync(tmpDir, { recursive: true, force: true });
21
24
  });
22
25
 
@@ -50,4 +53,53 @@ describe('validateSchemas', () => {
50
53
  const result = await validateSchemas(schemaDir);
51
54
  expect(result).toBe(false);
52
55
  });
56
+
57
+ it('should warn and fallback to default target when x-tracking-targets is missing', async () => {
58
+ const schemaDir = path.join(tmpDir, 'schemas');
59
+ fs.mkdirSync(schemaDir, { recursive: true });
60
+ fs.writeFileSync(
61
+ path.join(schemaDir, 'event.json'),
62
+ JSON.stringify({
63
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
64
+ type: 'object',
65
+ properties: {
66
+ event: {
67
+ type: 'string',
68
+ const: 'test_event',
69
+ },
70
+ },
71
+ required: ['event'],
72
+ }),
73
+ );
74
+
75
+ const result = await validateSchemas(schemaDir);
76
+ expect(result).toBe(true);
77
+ expect(consoleWarnSpy).toHaveBeenCalled();
78
+ expect(consoleWarnSpy.mock.calls[0][0]).toContain('web-datalayer-js');
79
+ });
80
+
81
+ it('should fail when x-tracking-targets has an unsupported target', async () => {
82
+ const schemaDir = path.join(tmpDir, 'schemas');
83
+ fs.mkdirSync(schemaDir, { recursive: true });
84
+ fs.writeFileSync(
85
+ path.join(schemaDir, 'event.json'),
86
+ JSON.stringify({
87
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
88
+ type: 'object',
89
+ 'x-tracking-targets': ['web-not-supported-js'],
90
+ properties: {
91
+ event: {
92
+ type: 'string',
93
+ const: 'test_event',
94
+ },
95
+ },
96
+ required: ['event'],
97
+ }),
98
+ );
99
+
100
+ const result = await validateSchemas(schemaDir);
101
+ expect(result).toBe(false);
102
+ expect(consoleErrorSpy).toHaveBeenCalled();
103
+ expect(consoleErrorSpy.mock.calls[0][0]).toContain('x-tracking-targets');
104
+ });
53
105
  });
@@ -1,80 +1,215 @@
1
- import React from 'react';
1
+ import React, { useEffect, useMemo } from 'react';
2
2
  import CodeBlock from '@theme/CodeBlock';
3
3
  import Tabs from '@theme/Tabs';
4
4
  import TabItem from '@theme/TabItem';
5
5
  import Heading from '@theme/Heading';
6
- import { schemaToExamples } from '../helpers/schemaToExamples';
6
+ import {
7
+ buildExampleModel,
8
+ findClearableProperties,
9
+ } from '../helpers/exampleModel';
7
10
 
8
- const generateCodeSnippet = (example, schema, dataLayerName = 'dataLayer') => {
9
- const clearableProperties = findClearableProperties(schema || {});
10
- let codeSnippet = '';
11
- const propertiesToClear = clearableProperties.filter(
12
- (prop) => prop in example,
13
- );
11
+ const TARGET_HASH_KEY = 'target';
12
+ const TARGET_HASH_PREFIX = `${TARGET_HASH_KEY}-`;
13
+ const TARGET_STORAGE_KEY = 'tracking-docs-selected-target';
14
14
 
15
- if (propertiesToClear.length > 0) {
16
- const resetObject = {};
17
- propertiesToClear.forEach((prop) => {
18
- resetObject[prop] = null;
19
- });
20
- codeSnippet += `window.${dataLayerName}.push(${JSON.stringify(
21
- resetObject,
22
- null,
23
- 2,
24
- )});\n`;
15
+ function parseHashTarget(rawHash = '') {
16
+ const raw = rawHash.startsWith('#') ? rawHash.substring(1) : rawHash;
17
+ if (!raw) return null;
18
+ if (raw.startsWith(TARGET_HASH_PREFIX)) {
19
+ return raw.substring(TARGET_HASH_PREFIX.length) || null;
25
20
  }
21
+ return null;
22
+ }
23
+
24
+ function readHashTarget() {
25
+ if (typeof window === 'undefined') return null;
26
+ return parseHashTarget(window.location.hash || '');
27
+ }
28
+
29
+ function readSearchTarget(search = '') {
30
+ const query = search.startsWith('?') ? search.substring(1) : search;
31
+ if (!query) return null;
32
+ const params = new URLSearchParams(query);
33
+ return params.get(TARGET_HASH_KEY);
34
+ }
26
35
 
27
- codeSnippet += `window.${dataLayerName}.push(${JSON.stringify(
28
- example,
36
+ function persistTarget(targetId) {
37
+ if (typeof window === 'undefined') return;
38
+ window.localStorage.setItem(TARGET_STORAGE_KEY, targetId);
39
+ window.history.replaceState(
29
40
  null,
30
- 2,
31
- )});`;
32
- return codeSnippet;
33
- };
41
+ '',
42
+ `${window.location.pathname}${window.location.search}#${TARGET_HASH_PREFIX}${targetId}`,
43
+ );
44
+ }
45
+
46
+ function resolveInitialTargetId(targets) {
47
+ const safeTargets = Array.isArray(targets)
48
+ ? targets.filter((t) => t && typeof t.id === 'string' && t.id.length > 0)
49
+ : [];
50
+ if (safeTargets.length === 0) return null;
51
+ const validTargetIds = new Set(safeTargets.map((t) => t.id));
52
+
53
+ const fromHash = readHashTarget();
54
+ if (fromHash && validTargetIds.has(fromHash)) {
55
+ return fromHash;
56
+ }
57
+
58
+ if (typeof window !== 'undefined') {
59
+ const fromStorage = window.localStorage.getItem(TARGET_STORAGE_KEY);
60
+ if (fromStorage && validTargetIds.has(fromStorage)) {
61
+ return fromStorage;
62
+ }
63
+ }
64
+
65
+ return safeTargets[0].id;
66
+ }
34
67
 
35
68
  export default function ExampleDataLayer({ schema, dataLayerName }) {
36
- const exampleGroups = schemaToExamples(schema);
69
+ const model = useMemo(
70
+ () => buildExampleModel(schema, { dataLayerName }),
71
+ [schema, dataLayerName],
72
+ );
73
+ const safeTargets = useMemo(
74
+ () =>
75
+ (Array.isArray(model.targets) ? model.targets : []).filter(
76
+ (target) =>
77
+ target && typeof target.id === 'string' && target.id.length > 0,
78
+ ),
79
+ [model.targets],
80
+ );
81
+ const exampleGroups = model.variantGroups;
82
+ const targetId = resolveInitialTargetId(safeTargets);
83
+ const showTargetTabs = safeTargets.length > 1;
84
+ const validTargetIds = useMemo(
85
+ () => new Set(safeTargets.map((target) => target.id)),
86
+ [safeTargets],
87
+ );
88
+
89
+ useEffect(() => {
90
+ if (typeof window === 'undefined') return undefined;
91
+ let isSyncing = false;
92
+ const originalReplaceState = window.history.replaceState;
93
+
94
+ const syncFromSearch = () => {
95
+ if (isSyncing) return;
96
+ const targetFromSearch = readSearchTarget(window.location.search);
97
+ if (!targetFromSearch || !validTargetIds.has(targetFromSearch)) return;
98
+
99
+ isSyncing = true;
100
+ try {
101
+ window.localStorage.setItem(TARGET_STORAGE_KEY, targetFromSearch);
102
+
103
+ const searchParams = new URLSearchParams(window.location.search);
104
+ searchParams.delete(TARGET_HASH_KEY);
105
+ const remainingSearch = searchParams.toString();
106
+ const searchPart = remainingSearch ? `?${remainingSearch}` : '';
107
+
108
+ originalReplaceState.call(
109
+ window.history,
110
+ null,
111
+ '',
112
+ `${window.location.pathname}${searchPart}#${TARGET_HASH_PREFIX}${targetFromSearch}`,
113
+ );
114
+ } finally {
115
+ isSyncing = false;
116
+ }
117
+ };
118
+
119
+ syncFromSearch();
120
+ window.history.replaceState = function patchedReplaceState(...args) {
121
+ const result = originalReplaceState.apply(window.history, args);
122
+ syncFromSearch();
123
+ return result;
124
+ };
125
+
126
+ return () => {
127
+ window.history.replaceState = originalReplaceState;
128
+ };
129
+ }, [validTargetIds]);
37
130
 
38
131
  if (!exampleGroups || exampleGroups.length === 0) {
39
132
  return null;
40
133
  }
41
134
 
42
- // Handle the simple case of a single default example with no choices
43
- if (exampleGroups.length === 1 && exampleGroups[0].property === 'default') {
44
- const codeSnippet = generateCodeSnippet(
45
- exampleGroups[0].options[0].example,
46
- schema,
47
- dataLayerName,
135
+ const getLanguageForTarget = (targetIdForSnippet) =>
136
+ safeTargets.find((target) => target.id === targetIdForSnippet)?.language ||
137
+ 'javascript';
138
+
139
+ const renderVariantGroups = (currentTargetId) => (
140
+ <>
141
+ {exampleGroups.map((group) => {
142
+ const showVariantTabs = group.options.length > 1;
143
+
144
+ if (!showVariantTabs) {
145
+ return (
146
+ <div key={group.property} style={{ marginTop: '20px' }}>
147
+ {!model.isSimpleDefault && (
148
+ <Heading as="h4">
149
+ <code>{group.property}</code> options:
150
+ </Heading>
151
+ )}
152
+ <CodeBlock language={getLanguageForTarget(currentTargetId)}>
153
+ {group.options[0].snippets[currentTargetId]}
154
+ </CodeBlock>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ return (
160
+ <div key={group.property} style={{ marginTop: '20px' }}>
161
+ <Heading as="h4">
162
+ <code>{group.property}</code> options:
163
+ </Heading>
164
+ <Tabs>
165
+ {group.options.map(({ id, title, snippets }) => (
166
+ <TabItem value={id} label={title} key={id}>
167
+ <CodeBlock language={getLanguageForTarget(currentTargetId)}>
168
+ {snippets[currentTargetId]}
169
+ </CodeBlock>
170
+ </TabItem>
171
+ ))}
172
+ </Tabs>
173
+ </div>
174
+ );
175
+ })}
176
+ </>
177
+ );
178
+
179
+ // Single target + single default variant => keep old layout
180
+ if (!showTargetTabs && model.isSimpleDefault) {
181
+ const snippets = exampleGroups[0].options[0].snippets || {};
182
+ const codeSnippet =
183
+ (targetId && snippets[targetId]) || Object.values(snippets)[0] || '';
184
+ return (
185
+ <CodeBlock language={getLanguageForTarget(targetId)}>
186
+ {codeSnippet}
187
+ </CodeBlock>
48
188
  );
49
- return <CodeBlock language="javascript">{codeSnippet}</CodeBlock>;
189
+ }
190
+
191
+ if (!showTargetTabs) {
192
+ return renderVariantGroups(targetId);
50
193
  }
51
194
 
52
195
  return (
53
- <>
54
- {exampleGroups.map((group) => (
55
- <div key={group.property} style={{ marginTop: '20px' }}>
56
- <Heading as="h4">
57
- <code>{group.property}</code> options:
58
- </Heading>
59
- <Tabs>
60
- {group.options.map(({ title, example }, index) => (
61
- <TabItem value={index} label={title} key={index}>
62
- <CodeBlock language="javascript">
63
- {generateCodeSnippet(example, schema, dataLayerName)}
64
- </CodeBlock>
65
- </TabItem>
66
- ))}
67
- </Tabs>
68
- </div>
69
- ))}
70
- </>
196
+ <div data-testid="target-tabs">
197
+ <Tabs defaultValue={targetId} queryString={TARGET_HASH_KEY}>
198
+ {safeTargets.map((target) => (
199
+ <TabItem value={target.id} label={target.label} key={target.id}>
200
+ <span id={`${TARGET_HASH_PREFIX}${target.id}`} />
201
+ {renderVariantGroups(target.id)}
202
+ </TabItem>
203
+ ))}
204
+ </Tabs>
205
+ </div>
71
206
  );
72
207
  }
73
208
 
74
- export const findClearableProperties = (schema) => {
75
- if (!schema || !schema.properties) return [];
76
-
77
- return Object.entries(schema.properties)
78
- .filter(([, definition]) => definition['x-gtm-clear'] === true)
79
- .map(([key]) => key);
209
+ export {
210
+ findClearableProperties,
211
+ resolveInitialTargetId,
212
+ readHashTarget,
213
+ readSearchTarget,
214
+ TARGET_STORAGE_KEY,
80
215
  };
@@ -125,6 +125,7 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
125
125
  rowSpan={rowSpan}
126
126
  style={{ ...indentStyle, ...continuingLinesStyle }}
127
127
  className={clsx(
128
+ 'property-cell',
128
129
  level > 0 && `level-${level}`,
129
130
  isLastInGroup && 'is-last',
130
131
  hasChildren && 'has-children',
@@ -143,7 +144,7 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
143
144
  </td>
144
145
 
145
146
  {/* The first constraint cell */}
146
- <td>
147
+ <td className="constraint-cell">
147
148
  {firstConstraint && (
148
149
  <code
149
150
  className={clsx(
@@ -181,7 +182,7 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
181
182
  {/* Render subsequent constraints in their own rows */}
182
183
  {remainingConstraints.map((constraint) => (
183
184
  <tr className={clsx(required && 'required-row')} key={constraint}>
184
- <td>
185
+ <td className="constraint-cell">
185
186
  <code
186
187
  className={clsx(
187
188
  'constraint-code',
@@ -63,10 +63,19 @@
63
63
  }
64
64
 
65
65
  .schema-table th:first-child,
66
- .schema-table td:first-child {
66
+ .schema-table td.property-cell {
67
67
  border-left: none;
68
68
  }
69
69
 
70
+ /*
71
+ * Constraint-only continuation rows render a single cell; in those rows that
72
+ * cell is also :first-child, but it still needs the separator before the
73
+ * "Constraints" column.
74
+ */
75
+ .schema-table td.constraint-cell {
76
+ border-left: 1px solid var(--ifm-table-border-color);
77
+ }
78
+
70
79
  .schema-table thead th {
71
80
  border-bottom: 2px solid var(--ifm-table-border-color);
72
81
  }
@@ -0,0 +1,70 @@
1
+ import { schemaToExamples } from './schemaToExamples';
2
+ import {
3
+ DEFAULT_SNIPPET_TARGET_ID,
4
+ findClearableProperties,
5
+ generateSnippetForTarget,
6
+ getSnippetTarget,
7
+ } from './snippetTargets';
8
+
9
+ export function resolveExampleTargets(schema) {
10
+ const configured = schema?.['x-tracking-targets'];
11
+ const targetIds =
12
+ Array.isArray(configured) && configured.length > 0
13
+ ? configured
14
+ : [DEFAULT_SNIPPET_TARGET_ID];
15
+
16
+ const targets = targetIds
17
+ .map((id) => {
18
+ try {
19
+ return getSnippetTarget(id);
20
+ } catch {
21
+ return null;
22
+ }
23
+ })
24
+ .filter(Boolean);
25
+
26
+ if (targets.length > 0) return targets;
27
+ return [getSnippetTarget(DEFAULT_SNIPPET_TARGET_ID)];
28
+ }
29
+
30
+ export function buildExampleModel(schema, { dataLayerName } = {}) {
31
+ const exampleGroups = schemaToExamples(schema);
32
+ const targets = resolveExampleTargets(schema);
33
+
34
+ if (!exampleGroups || exampleGroups.length === 0) {
35
+ return {
36
+ targets,
37
+ variantGroups: [],
38
+ isSimpleDefault: false,
39
+ };
40
+ }
41
+
42
+ const variantGroups = exampleGroups.map((group) => ({
43
+ property: group.property,
44
+ options: group.options.map((option, index) => ({
45
+ id: `${group.property}-${index}`,
46
+ title: option.title,
47
+ example: option.example,
48
+ snippets: Object.fromEntries(
49
+ targets.map((target) => [
50
+ target.id,
51
+ generateSnippetForTarget({
52
+ targetId: target.id,
53
+ example: option.example,
54
+ schema,
55
+ dataLayerName,
56
+ }),
57
+ ]),
58
+ ),
59
+ })),
60
+ }));
61
+
62
+ return {
63
+ targets,
64
+ variantGroups,
65
+ isSimpleDefault:
66
+ variantGroups.length === 1 && variantGroups[0].property === 'default',
67
+ };
68
+ }
69
+
70
+ export { findClearableProperties };
@@ -1,5 +1,33 @@
1
1
  import path from 'path';
2
+ import fs from 'fs';
2
3
  import processSchema from './processSchema.js';
4
+ import mergeJsonSchema from 'json-schema-merge-allof';
5
+
6
+ function mergePropertySchemas(baseProperties = {}, optionProperties = {}) {
7
+ const mergedProperties = {
8
+ ...baseProperties,
9
+ ...optionProperties,
10
+ };
11
+
12
+ for (const key of Object.keys(baseProperties)) {
13
+ if (!Object.prototype.hasOwnProperty.call(optionProperties, key)) {
14
+ continue;
15
+ }
16
+
17
+ mergedProperties[key] = mergeJsonSchema(
18
+ {
19
+ allOf: [baseProperties[key], optionProperties[key]],
20
+ },
21
+ {
22
+ resolvers: {
23
+ defaultResolver: mergeJsonSchema.options.resolvers.title,
24
+ },
25
+ },
26
+ );
27
+ }
28
+
29
+ return mergedProperties;
30
+ }
3
31
 
4
32
  export function slugify(text) {
5
33
  if (!text) {
@@ -20,7 +48,15 @@ export async function processOneOfSchema(schema, filePath) {
20
48
  const choiceType = schema.oneOf ? 'oneOf' : null;
21
49
 
22
50
  if (choiceType) {
23
- const parentWithoutChoice = { ...schema };
51
+ let parentSchema = schema;
52
+
53
+ // When the root schema is loaded from disk, use its processed form so
54
+ // parent-level allOf refs (e.g. shared dataLayer metadata) are preserved.
55
+ if (filePath && fs.existsSync(filePath)) {
56
+ parentSchema = await processSchema(filePath);
57
+ }
58
+
59
+ const parentWithoutChoice = { ...parentSchema };
24
60
  delete parentWithoutChoice[choiceType];
25
61
 
26
62
  for (const option of schema[choiceType]) {
@@ -34,10 +70,10 @@ export async function processOneOfSchema(schema, filePath) {
34
70
 
35
71
  // Merge the parent schema with the resolved option schema
36
72
  const newSchema = { ...parentWithoutChoice, ...resolvedOption };
37
- newSchema.properties = {
38
- ...parentWithoutChoice.properties,
39
- ...resolvedOption.properties,
40
- };
73
+ newSchema.properties = mergePropertySchemas(
74
+ parentWithoutChoice.properties,
75
+ resolvedOption.properties,
76
+ );
41
77
 
42
78
  let slug;
43
79
  const hadId = resolvedOption.$id && resolvedOption.$id.endsWith('.json');
@@ -63,18 +63,61 @@ const generateExampleForChoice = (rootSchema, path, option) => {
63
63
  }
64
64
  };
65
65
 
66
+ const pruneSiblingConditionalProperties = (
67
+ mergedSchema,
68
+ activeBranchSchema,
69
+ inactiveBranchSchema,
70
+ baseRequired = [],
71
+ ) => {
72
+ const branchesOnlyAdjustRequired =
73
+ !activeBranchSchema?.properties && !inactiveBranchSchema?.properties;
74
+
75
+ if (
76
+ !mergedSchema?.properties ||
77
+ !branchesOnlyAdjustRequired ||
78
+ !Array.isArray(inactiveBranchSchema?.required)
79
+ ) {
80
+ return mergedSchema;
81
+ }
82
+
83
+ const activeRequired = new Set(activeBranchSchema?.required || []);
84
+ const protectedRequired = new Set(baseRequired);
85
+
86
+ inactiveBranchSchema.required.forEach((name) => {
87
+ if (activeRequired.has(name) || protectedRequired.has(name)) {
88
+ return;
89
+ }
90
+
91
+ delete mergedSchema.properties[name];
92
+ if (Array.isArray(mergedSchema.required)) {
93
+ mergedSchema.required = mergedSchema.required.filter(
94
+ (requiredName) => requiredName !== name,
95
+ );
96
+ }
97
+ });
98
+
99
+ return mergedSchema;
100
+ };
101
+
66
102
  const generateConditionalExample = (rootSchema, path, branch) => {
67
103
  const schemaVariant = JSON.parse(JSON.stringify(rootSchema));
104
+ const siblingBranch = branch === 'then' ? 'else' : 'then';
68
105
 
69
106
  if (path.length === 0) {
70
107
  const branchSchema = schemaVariant[branch];
108
+ const siblingBranchSchema = schemaVariant[siblingBranch];
109
+ const baseRequired = schemaVariant.required || [];
71
110
  delete schemaVariant.if;
72
111
  delete schemaVariant.then;
73
112
  delete schemaVariant.else;
74
113
  if (branchSchema) {
75
- return buildExampleFromSchema(
114
+ const merged = pruneSiblingConditionalProperties(
76
115
  mergeJsonSchema({ allOf: [schemaVariant, branchSchema] }),
116
+ branchSchema,
117
+ siblingBranchSchema,
118
+ baseRequired,
77
119
  );
120
+ return buildExampleFromSchema(merged);
78
121
  }
79
122
  return buildExampleFromSchema(schemaVariant);
80
123
  }
@@ -84,11 +127,18 @@ const generateConditionalExample = (rootSchema, path, branch) => {
84
127
  target = target[segment];
85
128
  }
86
129
  const branchSchema = target[branch];
130
+ const siblingBranchSchema = target[siblingBranch];
131
+ const baseRequired = target.required || [];
87
132
  delete target.if;
88
133
  delete target.then;
89
134
  delete target.else;
90
135
  if (branchSchema) {
91
- const merged = mergeJsonSchema({ allOf: [target, branchSchema] });
136
+ const merged = pruneSiblingConditionalProperties(
137
+ mergeJsonSchema({ allOf: [target, branchSchema] }),
138
+ branchSchema,
139
+ siblingBranchSchema,
140
+ baseRequired,
141
+ );
92
142
  Object.keys(target).forEach((k) => delete target[k]);
93
143
  Object.assign(target, merged);
94
144
  }
@@ -11,6 +11,36 @@ function computeOwnBracket(level, parentGroupBrackets) {
11
11
  return { level, bracketIndex };
12
12
  }
13
13
 
14
+ function materializeConditionalBranchSchema(branchSchema, parentSchema) {
15
+ if (
16
+ !branchSchema ||
17
+ branchSchema.properties ||
18
+ branchSchema.oneOf ||
19
+ branchSchema.anyOf ||
20
+ branchSchema.if ||
21
+ !Array.isArray(branchSchema.required) ||
22
+ !parentSchema?.properties
23
+ ) {
24
+ return branchSchema;
25
+ }
26
+
27
+ const branchProperties = Object.fromEntries(
28
+ branchSchema.required
29
+ .filter((name) => parentSchema.properties[name])
30
+ .map((name) => [name, parentSchema.properties[name]]),
31
+ );
32
+
33
+ if (Object.keys(branchProperties).length === 0) {
34
+ return branchSchema;
35
+ }
36
+
37
+ return {
38
+ ...branchSchema,
39
+ type: 'object',
40
+ properties: branchProperties,
41
+ };
42
+ }
43
+
14
44
  function processOptions(
15
45
  choices,
16
46
  level,
@@ -144,11 +174,15 @@ export function schemaToTableData(
144
174
  // Then is NOT the last branch if Else exists — use innerContinuingLevels
145
175
  // to keep the parent line flowing. If Then IS the last branch, use original.
146
176
  const thenLevels = hasElse ? innerContinuingLevels : continuingLevels;
177
+ const thenSchema = materializeConditionalBranchSchema(
178
+ subSchema.then,
179
+ subSchema,
180
+ );
147
181
  branches.push({
148
182
  title: 'Then',
149
183
  description: subSchema.then.description,
150
184
  rows: schemaToTableData(
151
- subSchema.then,
185
+ thenSchema,
152
186
  currentLevel,
153
187
  currentPath,
154
188
  thenLevels,
@@ -160,11 +194,15 @@ export function schemaToTableData(
160
194
  }
161
195
  if (hasElse) {
162
196
  // Else is always the last branch — use original continuingLevels
197
+ const elseSchema = materializeConditionalBranchSchema(
198
+ subSchema.else,
199
+ subSchema,
200
+ );
163
201
  branches.push({
164
202
  title: 'Else',
165
203
  description: subSchema.else.description,
166
204
  rows: schemaToTableData(
167
- subSchema.else,
205
+ elseSchema,
168
206
  currentLevel,
169
207
  currentPath,
170
208
  continuingLevels,