docusaurus-plugin-generate-schema-docs 1.7.0 → 1.8.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.
package/README.md CHANGED
@@ -73,6 +73,45 @@ npm run update-schema-ids
73
73
 
74
74
  This command will update the `$id` of all schemas in the versioned directories.
75
75
 
76
+ ### Sync GTM Variables (Optional)
77
+
78
+ If you use Google Tag Manager, you can sync Data Layer Variables from your schemas:
79
+
80
+ ```bash
81
+ npm install --save-optional @owntag/gtm-cli
82
+ npm run sync:gtm
83
+ ```
84
+
85
+ The Docusaurus CLI command is:
86
+
87
+ ```bash
88
+ docusaurus sync-gtm
89
+ ```
90
+
91
+ By default, it resolves schemas from the project root. Use `--path=<siteDir>` to target a different site directory.
92
+
93
+ ### Firebase Snippet Targets
94
+
95
+ `ExampleDataLayer` supports Firebase snippet targets for:
96
+
97
+ - `android-firebase-kotlin-sdk`
98
+ - `android-firebase-java-sdk`
99
+ - `ios-firebase-swift-sdk`
100
+ - `ios-firebase-objc-sdk`
101
+
102
+ Mapping rules for generated parameters:
103
+
104
+ - `string` -> string parameter
105
+ - `integer`/`boolean` -> integer/long parameter (`true` = `1`, `false` = `0`)
106
+ - `number` -> double parameter
107
+ - `items` -> non-empty array of flat item objects
108
+ - unsupported nested values cause a generation error (no automatic flattening or JSON-string fallback)
109
+
110
+ Reference docs used for syntax and kept as source of truth:
111
+
112
+ - https://firebase.google.com/docs/analytics/android/events
113
+ - https://firebase.google.com/docs/analytics/ios/events
114
+
76
115
  ## How it Works
77
116
 
78
117
  The plugin reads your JSON schemas, dereferences any `$ref` properties, and merges `allOf` properties. It then generates an MDX file for each schema, which uses custom React components to render the schema details.
@@ -3,6 +3,10 @@ import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import ExampleDataLayer, {
5
5
  findClearableProperties,
6
+ readHashTarget,
7
+ readSearchTarget,
8
+ resolveInitialTargetId,
9
+ TARGET_STORAGE_KEY,
6
10
  } from '../components/ExampleDataLayer';
7
11
  import choiceEventSchema from './__fixtures__/static/schemas/choice-event.json';
8
12
 
@@ -14,8 +18,20 @@ jest.mock('@theme/CodeBlock', () => {
14
18
  });
15
19
 
16
20
  jest.mock('@theme/Tabs', () => {
17
- return function Tabs({ children }) {
18
- return <div data-testid="tabs">{children}</div>;
21
+ return function Tabs({ children, queryString, defaultValue }) {
22
+ const attrs = {};
23
+ if (queryString !== undefined) {
24
+ attrs['data-query-string'] = queryString;
25
+ }
26
+ if (defaultValue !== undefined) {
27
+ attrs['data-default-value'] = defaultValue;
28
+ }
29
+
30
+ return (
31
+ <div data-testid="tabs" {...attrs}>
32
+ {children}
33
+ </div>
34
+ );
19
35
  };
20
36
  });
21
37
 
@@ -30,6 +46,11 @@ jest.mock('@theme/TabItem', () => {
30
46
  });
31
47
 
32
48
  describe('ExampleDataLayer', () => {
49
+ afterEach(() => {
50
+ window.location.hash = '';
51
+ window.localStorage.clear();
52
+ });
53
+
33
54
  it('should render a single example for a simple schema', () => {
34
55
  const schema = {
35
56
  type: 'object',
@@ -81,6 +102,59 @@ describe('ExampleDataLayer', () => {
81
102
  );
82
103
  expect(getByText(/window.customDataLayer.push/)).toBeInTheDocument();
83
104
  });
105
+
106
+ it('should render target tabs when multiple targets are configured', () => {
107
+ const schema = {
108
+ type: 'object',
109
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
110
+ properties: {
111
+ event: { type: 'string', examples: ['test_event'] },
112
+ },
113
+ };
114
+
115
+ const { getByTestId, getAllByTestId } = render(
116
+ <ExampleDataLayer schema={schema} />,
117
+ );
118
+ expect(getByTestId('target-tabs')).toBeInTheDocument();
119
+ const tabLabels = getAllByTestId('tab-item').map((item) =>
120
+ item.getAttribute('data-label'),
121
+ );
122
+ expect(tabLabels).toEqual(
123
+ expect.arrayContaining([
124
+ 'Web Data Layer (JS)',
125
+ 'Android Firebase (Kotlin)',
126
+ ]),
127
+ );
128
+ expect(getByTestId('tabs')).toHaveAttribute('data-query-string', 'target');
129
+ });
130
+
131
+ it('uses per-target syntax highlighting language', () => {
132
+ const schema = {
133
+ type: 'object',
134
+ 'x-tracking-targets': ['android-firebase-kotlin-sdk'],
135
+ properties: {
136
+ event: { type: 'string', examples: ['purchase'] },
137
+ value: { type: 'number', examples: [14.98] },
138
+ },
139
+ };
140
+
141
+ const { container } = render(<ExampleDataLayer schema={schema} />);
142
+ const codeBlocks = container.querySelectorAll('pre[data-language]');
143
+ expect(codeBlocks.length).toBeGreaterThan(0);
144
+ expect(codeBlocks[0]).toHaveAttribute('data-language', 'kotlin');
145
+ });
146
+
147
+ it('should not render target tabs for single-target schemas', () => {
148
+ const schema = {
149
+ type: 'object',
150
+ properties: {
151
+ event: { type: 'string', examples: ['test_event'] },
152
+ },
153
+ };
154
+
155
+ const { queryByTestId } = render(<ExampleDataLayer schema={schema} />);
156
+ expect(queryByTestId('target-tabs')).toBeNull();
157
+ });
84
158
  });
85
159
 
86
160
  describe('findClearableProperties', () => {
@@ -103,3 +177,76 @@ describe('findClearableProperties', () => {
103
177
  expect(findClearableProperties(schema)).toEqual(['prop2', 'prop4']);
104
178
  });
105
179
  });
180
+
181
+ describe('target selection helpers', () => {
182
+ afterEach(() => {
183
+ window.location.hash = '';
184
+ window.localStorage.clear();
185
+ });
186
+
187
+ it('reads target from hash', () => {
188
+ window.location.hash = '#target-android-firebase-java-sdk';
189
+ expect(readHashTarget()).toBe('android-firebase-java-sdk');
190
+ });
191
+
192
+ it('reads target from search', () => {
193
+ expect(readSearchTarget('?target=android-firebase-java-sdk')).toBe(
194
+ 'android-firebase-java-sdk',
195
+ );
196
+ });
197
+
198
+ it('persistTarget writes hash without duplicate query target', () => {
199
+ const originalReplaceState = window.history.replaceState;
200
+ const replaceSpy = jest.fn();
201
+ window.history.replaceState = replaceSpy;
202
+ window.history.pushState(
203
+ {},
204
+ '',
205
+ '/next/event-reference/purchase-event?target=android-firebase-kotlin-sdk',
206
+ );
207
+
208
+ const schema = {
209
+ type: 'object',
210
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
211
+ properties: {
212
+ event: { type: 'string', examples: ['purchase'] },
213
+ },
214
+ };
215
+
216
+ render(<ExampleDataLayer schema={schema} />);
217
+ expect(replaceSpy).toHaveBeenCalled();
218
+ const lastCallUrl =
219
+ replaceSpy.mock.calls[replaceSpy.mock.calls.length - 1][2];
220
+ expect(lastCallUrl).toContain('#target-android-firebase-kotlin-sdk');
221
+ expect(lastCallUrl).not.toContain('?target=android-firebase-kotlin-sdk');
222
+
223
+ window.history.replaceState = originalReplaceState;
224
+ });
225
+
226
+ it('prefers hash over localStorage for initial target resolution', () => {
227
+ const targets = [
228
+ { id: 'web-datalayer-js' },
229
+ { id: 'android-firebase-java-sdk' },
230
+ ];
231
+ window.localStorage.setItem(TARGET_STORAGE_KEY, 'web-datalayer-js');
232
+ window.location.hash = '#target-android-firebase-java-sdk';
233
+
234
+ expect(resolveInitialTargetId(targets)).toBe('android-firebase-java-sdk');
235
+ });
236
+
237
+ it('falls back to first target when hash is unknown', () => {
238
+ const targets = [
239
+ { id: 'web-datalayer-js' },
240
+ { id: 'android-firebase-java-sdk' },
241
+ ];
242
+ window.location.hash = '#target-unknown';
243
+
244
+ expect(resolveInitialTargetId(targets)).toBe('web-datalayer-js');
245
+ });
246
+
247
+ it('ignores malformed targets and still resolves a valid initial target', () => {
248
+ const targets = [undefined, {}, { id: 'android-firebase-kotlin-sdk' }];
249
+ window.location.hash = '#target-android-firebase-kotlin-sdk';
250
+ expect(resolveInitialTargetId(targets)).toBe('android-firebase-kotlin-sdk');
251
+ });
252
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "ecommerce": {
5
+ "type": "object",
6
+ "x-gtm-clear": true
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "title": "Event Reference",
3
+ "type": "object",
4
+ "allOf": [
5
+ {
6
+ "$ref": "./components/dataLayer.json"
7
+ }
8
+ ],
9
+ "oneOf": [
10
+ {
11
+ "$ref": "./purchase-event.json"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "title": "Purchase",
3
+ "type": "object",
4
+ "properties": {
5
+ "ecommerce": {
6
+ "type": "object",
7
+ "properties": {
8
+ "transaction_id": {
9
+ "type": "string"
10
+ }
11
+ }
12
+ }
13
+ }
14
+ }
@@ -79,6 +79,36 @@ describe('PropertyRow', () => {
79
79
  expect(getByText('maxLength: 10')).toBeInTheDocument();
80
80
  });
81
81
 
82
+ it('marks only the property column cell with property-cell class', () => {
83
+ const row = {
84
+ name: 'name',
85
+ level: 0,
86
+ required: true,
87
+ propertyType: 'string',
88
+ description: '',
89
+ example: '',
90
+ constraints: ['required', 'minLength: 1'],
91
+ path: ['name'],
92
+ };
93
+
94
+ const { container } = render(
95
+ <table>
96
+ <tbody>
97
+ <PropertyRow row={row} />
98
+ </tbody>
99
+ </table>,
100
+ );
101
+
102
+ const propertyCell = container.querySelector('td.property-cell');
103
+ expect(propertyCell).toBeInTheDocument();
104
+
105
+ const subsequentConstraintRows = container.querySelectorAll('tbody tr');
106
+ expect(subsequentConstraintRows.length).toBeGreaterThan(1);
107
+ expect(
108
+ subsequentConstraintRows[1].querySelector('td.property-cell'),
109
+ ).not.toBeInTheDocument();
110
+ });
111
+
82
112
  it('renders an example', () => {
83
113
  const row = {
84
114
  name: 'name',
@@ -1,7 +1,7 @@
1
1
  // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
- exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
3
+ exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
4
4
 
5
- exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
5
+ exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
6
6
 
7
- exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
7
+ exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
@@ -0,0 +1,88 @@
1
+ import {
2
+ buildExampleModel,
3
+ resolveExampleTargets,
4
+ } from '../../helpers/exampleModel';
5
+ import { DEFAULT_SNIPPET_TARGET_ID } from '../../helpers/snippetTargets';
6
+ import choiceEventSchema from '../__fixtures__/static/schemas/choice-event.json';
7
+ import conditionalEventSchema from '../__fixtures__/static/schemas/conditional-event.json';
8
+
9
+ describe('buildExampleModel', () => {
10
+ it('builds a default model for simple schema', () => {
11
+ const schema = {
12
+ type: 'object',
13
+ properties: {
14
+ event: { type: 'string', examples: ['test_event'] },
15
+ },
16
+ };
17
+
18
+ const model = buildExampleModel(schema);
19
+ expect(model.targets.map((t) => t.id)).toEqual([DEFAULT_SNIPPET_TARGET_ID]);
20
+ expect(model.isSimpleDefault).toBe(true);
21
+ expect(model.variantGroups).toHaveLength(1);
22
+ expect(model.variantGroups[0].property).toBe('default');
23
+ expect(
24
+ model.variantGroups[0].options[0].snippets[DEFAULT_SNIPPET_TARGET_ID],
25
+ ).toContain('window.dataLayer.push');
26
+ });
27
+
28
+ it('builds grouped variants for choice schemas', () => {
29
+ const model = buildExampleModel(choiceEventSchema);
30
+ const groupProperties = model.variantGroups.map((group) => group.property);
31
+
32
+ expect(model.isSimpleDefault).toBe(false);
33
+ expect(groupProperties).toEqual(
34
+ expect.arrayContaining(['user_id', 'payment_method']),
35
+ );
36
+ expect(
37
+ model.variantGroups[0].options[0].snippets[DEFAULT_SNIPPET_TARGET_ID],
38
+ ).toContain('window.dataLayer.push');
39
+ });
40
+
41
+ it('builds conditional variants for if-then-else schemas', () => {
42
+ const model = buildExampleModel(conditionalEventSchema);
43
+ const conditionalGroup = model.variantGroups.find(
44
+ (group) => group.property === 'conditional',
45
+ );
46
+
47
+ expect(conditionalGroup).toBeDefined();
48
+ expect(conditionalGroup.options).toHaveLength(2);
49
+ expect(conditionalGroup.options.map((o) => o.title)).toEqual(
50
+ expect.arrayContaining([
51
+ 'When condition is met',
52
+ 'When condition is not met',
53
+ ]),
54
+ );
55
+ });
56
+
57
+ it('uses configured dataLayerName in snippets', () => {
58
+ const schema = {
59
+ type: 'object',
60
+ properties: {
61
+ event: { type: 'string', examples: ['test_event'] },
62
+ },
63
+ };
64
+
65
+ const model = buildExampleModel(schema, {
66
+ dataLayerName: 'customDataLayer',
67
+ });
68
+ expect(
69
+ model.variantGroups[0].options[0].snippets[DEFAULT_SNIPPET_TARGET_ID],
70
+ ).toContain('window.customDataLayer.push');
71
+ });
72
+
73
+ it('resolves multiple targets when x-tracking-targets is provided', () => {
74
+ const schema = {
75
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
76
+ type: 'object',
77
+ properties: {
78
+ event: { type: 'string', examples: ['test_event'] },
79
+ },
80
+ };
81
+
82
+ const targets = resolveExampleTargets(schema);
83
+ expect(targets.map((target) => target.id)).toEqual([
84
+ 'web-datalayer-js',
85
+ 'android-firebase-kotlin-sdk',
86
+ ]);
87
+ });
88
+ });
@@ -1,5 +1,10 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
1
5
  import { processOneOfSchema } from '../../helpers/schema-processing';
2
6
  import path from 'path';
7
+ import fs from 'fs';
3
8
 
4
9
  describe('schema-processing', () => {
5
10
  describe('processOneOfSchema', () => {
@@ -78,5 +83,56 @@ describe('schema-processing', () => {
78
83
  const result = await processOneOfSchema(rootSchema, filePath);
79
84
  expect(result[0].schema.$id).toBe('root.json#option-1');
80
85
  });
86
+
87
+ it('preserves parent property metadata when an option refines the same property', async () => {
88
+ const rootSchema = {
89
+ $id: 'root.json',
90
+ title: 'Root',
91
+ oneOf: [
92
+ {
93
+ title: 'Purchase',
94
+ properties: {
95
+ ecommerce: {
96
+ type: 'object',
97
+ properties: {
98
+ transaction_id: { type: 'string' },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ ],
104
+ properties: {
105
+ ecommerce: {
106
+ type: 'object',
107
+ 'x-gtm-clear': true,
108
+ },
109
+ },
110
+ };
111
+
112
+ const result = await processOneOfSchema(
113
+ rootSchema,
114
+ '/path/to/schema.json',
115
+ );
116
+ const mergedEcommerce = result[0].schema.properties.ecommerce;
117
+
118
+ expect(mergedEcommerce['x-gtm-clear']).toBe(true);
119
+ expect(mergedEcommerce.properties.transaction_id.type).toBe('string');
120
+ });
121
+
122
+ it('preserves parent allOf metadata when root oneOf options are file refs', async () => {
123
+ const rootFile = path.join(
124
+ __dirname,
125
+ '..',
126
+ '__fixtures__',
127
+ 'schema-processing',
128
+ 'event-reference.json',
129
+ );
130
+ const rootSchema = JSON.parse(fs.readFileSync(rootFile, 'utf8'));
131
+ const result = await processOneOfSchema(rootSchema, rootFile);
132
+ const mergedEcommerce = result[0].schema.properties.ecommerce;
133
+
134
+ expect(mergedEcommerce['x-gtm-clear']).toBe(true);
135
+ expect(mergedEcommerce.properties.transaction_id.type).toBe('string');
136
+ });
81
137
  });
82
138
  });