docusaurus-plugin-generate-schema-docs 1.8.3 → 1.8.5

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 (62) hide show
  1. package/README.md +12 -0
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
  3. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof-multi.json +12 -0
  4. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof.json +30 -0
  5. package/__tests__/__fixtures__/validateSchemas/schema-with-not-edge-cases.json +24 -0
  6. package/__tests__/__fixtures__/validateSchemas/schema-with-not-non-object.json +15 -0
  7. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +6 -0
  8. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +6 -0
  9. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +15 -0
  10. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +6 -0
  11. package/__tests__/components/PropertiesTable.test.js +66 -0
  12. package/__tests__/components/PropertyRow.test.js +85 -4
  13. package/__tests__/components/SchemaJsonViewer.test.js +118 -0
  14. package/__tests__/generateEventDocs.anchor.test.js +1 -1
  15. package/__tests__/generateEventDocs.nested.test.js +1 -1
  16. package/__tests__/generateEventDocs.partials.test.js +1 -1
  17. package/__tests__/generateEventDocs.test.js +506 -1
  18. package/__tests__/generateEventDocs.versioned.test.js +1 -1
  19. package/__tests__/helpers/buildExampleFromSchema.test.js +240 -0
  20. package/__tests__/helpers/constraintSchemaPaths.test.js +208 -0
  21. package/__tests__/helpers/continuingLinesStyle.test.js +492 -0
  22. package/__tests__/helpers/example-helper.test.js +12 -0
  23. package/__tests__/helpers/exampleModel.test.js +209 -0
  24. package/__tests__/helpers/file-system.test.js +73 -1
  25. package/__tests__/helpers/getConstraints.test.js +43 -0
  26. package/__tests__/helpers/mergeSchema.test.js +94 -0
  27. package/__tests__/helpers/processSchema.test.js +309 -1
  28. package/__tests__/helpers/schema-doc-template.test.js +54 -0
  29. package/__tests__/helpers/schema-processing.test.js +122 -2
  30. package/__tests__/helpers/schemaToExamples.test.js +1007 -0
  31. package/__tests__/helpers/schemaToTableData.mutations.test.js +970 -0
  32. package/__tests__/helpers/schemaToTableData.test.js +157 -0
  33. package/__tests__/helpers/schemaTraversal.test.js +110 -0
  34. package/__tests__/helpers/snippetTargets.test.js +432 -0
  35. package/__tests__/helpers/trackingTargets.test.js +319 -0
  36. package/__tests__/helpers/validator.test.js +385 -1
  37. package/__tests__/index.test.js +436 -0
  38. package/__tests__/syncGtm.test.js +366 -6
  39. package/__tests__/update-schema-ids.test.js +70 -1
  40. package/__tests__/validateSchemas-integration.test.js +2 -2
  41. package/__tests__/validateSchemas.test.js +192 -1
  42. package/components/PropertiesTable.js +32 -2
  43. package/components/PropertyRow.js +29 -2
  44. package/components/SchemaJsonViewer.js +234 -131
  45. package/components/SchemaRows.css +40 -0
  46. package/components/SchemaViewer.js +11 -2
  47. package/generateEventDocs.js +21 -1
  48. package/helpers/constraintSchemaPaths.js +10 -14
  49. package/helpers/example-helper.js +2 -2
  50. package/helpers/getConstraints.js +20 -0
  51. package/helpers/processSchema.js +32 -1
  52. package/helpers/schema-doc-template.js +4 -0
  53. package/helpers/schemaToExamples.js +29 -35
  54. package/helpers/schemaToTableData.js +538 -492
  55. package/helpers/schemaTraversal.cjs +148 -0
  56. package/helpers/trackingTargets.js +26 -3
  57. package/helpers/validator.js +18 -4
  58. package/index.js +1 -2
  59. package/package.json +1 -1
  60. package/scripts/sync-gtm.js +65 -34
  61. package/test-data/payloadContracts.js +35 -0
  62. package/validateSchemas.js +1 -1
@@ -0,0 +1,148 @@
1
+ function visitSchemaNodes(schema, visitor, path = []) {
2
+ if (!schema || typeof schema !== 'object') {
3
+ return;
4
+ }
5
+
6
+ visitor(schema, { path });
7
+
8
+ if (schema.properties && typeof schema.properties === 'object') {
9
+ Object.entries(schema.properties).forEach(([key, propertySchema]) => {
10
+ visitSchemaNodes(propertySchema, visitor, [...path, 'properties', key]);
11
+ });
12
+ }
13
+
14
+ if (schema.items && typeof schema.items === 'object') {
15
+ visitSchemaNodes(schema.items, visitor, [...path, 'items']);
16
+ }
17
+
18
+ if (Array.isArray(schema.oneOf)) {
19
+ schema.oneOf.forEach((optionSchema, index) => {
20
+ visitSchemaNodes(optionSchema, visitor, [...path, 'oneOf', index]);
21
+ });
22
+ }
23
+
24
+ if (Array.isArray(schema.anyOf)) {
25
+ schema.anyOf.forEach((optionSchema, index) => {
26
+ visitSchemaNodes(optionSchema, visitor, [...path, 'anyOf', index]);
27
+ });
28
+ }
29
+
30
+ if (schema.if && typeof schema.if === 'object') {
31
+ visitSchemaNodes(schema.if, visitor, [...path, 'if']);
32
+ }
33
+
34
+ if (schema.then && typeof schema.then === 'object') {
35
+ visitSchemaNodes(schema.then, visitor, [...path, 'then']);
36
+ }
37
+
38
+ if (schema.else && typeof schema.else === 'object') {
39
+ visitSchemaNodes(schema.else, visitor, [...path, 'else']);
40
+ }
41
+ }
42
+
43
+ function visitPropertyEntryBranches(
44
+ schema,
45
+ visitor,
46
+ { prefix = '', path = [], skipArraySubProperties = false } = {},
47
+ ) {
48
+ if (!schema || typeof schema !== 'object') {
49
+ return;
50
+ }
51
+
52
+ if (Array.isArray(schema.oneOf)) {
53
+ schema.oneOf.forEach((optionSchema, index) => {
54
+ visitSchemaPropertyEntries(optionSchema, visitor, {
55
+ prefix,
56
+ path: [...path, 'oneOf', index],
57
+ skipArraySubProperties,
58
+ });
59
+ });
60
+ }
61
+
62
+ if (Array.isArray(schema.anyOf)) {
63
+ schema.anyOf.forEach((optionSchema, index) => {
64
+ visitSchemaPropertyEntries(optionSchema, visitor, {
65
+ prefix,
66
+ path: [...path, 'anyOf', index],
67
+ skipArraySubProperties,
68
+ });
69
+ });
70
+ }
71
+
72
+ if (schema.then && typeof schema.then === 'object') {
73
+ visitSchemaPropertyEntries(schema.then, visitor, {
74
+ prefix,
75
+ path: [...path, 'then'],
76
+ skipArraySubProperties,
77
+ });
78
+ }
79
+
80
+ if (schema.else && typeof schema.else === 'object') {
81
+ visitSchemaPropertyEntries(schema.else, visitor, {
82
+ prefix,
83
+ path: [...path, 'else'],
84
+ skipArraySubProperties,
85
+ });
86
+ }
87
+ }
88
+
89
+ function visitSchemaPropertyEntries(
90
+ schema,
91
+ visitor,
92
+ { prefix = '', path = [], skipArraySubProperties = false } = {},
93
+ ) {
94
+ if (!schema || typeof schema !== 'object') {
95
+ return;
96
+ }
97
+
98
+ if (schema.properties && typeof schema.properties === 'object') {
99
+ Object.entries(schema.properties).forEach(([key, propertySchema]) => {
100
+ const currentName = prefix ? `${prefix}.${key}` : key;
101
+ const currentPath = [...path, 'properties', key];
102
+
103
+ visitor(propertySchema, {
104
+ key,
105
+ name: currentName,
106
+ path: currentPath,
107
+ parentSchema: schema,
108
+ });
109
+
110
+ if (propertySchema?.type === 'object' && propertySchema.properties) {
111
+ visitSchemaPropertyEntries(propertySchema, visitor, {
112
+ prefix: currentName,
113
+ path: currentPath,
114
+ skipArraySubProperties,
115
+ });
116
+ }
117
+
118
+ if (
119
+ propertySchema?.type === 'array' &&
120
+ propertySchema.items?.properties &&
121
+ !skipArraySubProperties
122
+ ) {
123
+ visitSchemaPropertyEntries(propertySchema.items, visitor, {
124
+ prefix: `${currentName}.0`,
125
+ path: [...currentPath, 'items'],
126
+ skipArraySubProperties,
127
+ });
128
+ }
129
+
130
+ visitPropertyEntryBranches(propertySchema, visitor, {
131
+ prefix: currentName,
132
+ path: currentPath,
133
+ skipArraySubProperties,
134
+ });
135
+ });
136
+ }
137
+
138
+ visitPropertyEntryBranches(schema, visitor, {
139
+ prefix,
140
+ path,
141
+ skipArraySubProperties,
142
+ });
143
+ }
144
+
145
+ module.exports = {
146
+ visitSchemaNodes,
147
+ visitSchemaPropertyEntries,
148
+ };
@@ -10,6 +10,28 @@ export const SUPPORTED_TRACKING_TARGETS = [
10
10
 
11
11
  const TARGET_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+){2,}$/;
12
12
 
13
+ function isReferenceAggregatorSchema(schema) {
14
+ if (!schema || typeof schema !== 'object') {
15
+ return false;
16
+ }
17
+
18
+ const hasChoiceAggregation =
19
+ Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf);
20
+ const hasPropertiesKeyword = Object.hasOwn(schema, 'properties');
21
+ const hasValidPropertiesMap =
22
+ hasPropertiesKeyword &&
23
+ schema.properties &&
24
+ typeof schema.properties === 'object' &&
25
+ !Array.isArray(schema.properties);
26
+ const hasOwnProperties =
27
+ hasValidPropertiesMap && Object.keys(schema.properties).length > 0;
28
+
29
+ return (
30
+ hasChoiceAggregation &&
31
+ (!hasPropertiesKeyword || (hasValidPropertiesMap && !hasOwnProperties))
32
+ );
33
+ }
34
+
13
35
  export function resolveTrackingTargets(
14
36
  schema,
15
37
  { schemaFile = 'schema', isQuiet = false } = {},
@@ -17,9 +39,10 @@ export function resolveTrackingTargets(
17
39
  const configuredTargets = schema?.['x-tracking-targets'];
18
40
 
19
41
  if (configuredTargets == null) {
20
- const warning = isQuiet
21
- ? null
22
- : `Schema ${schemaFile} is missing x-tracking-targets. Falling back to "${DEFAULT_TRACKING_TARGET}".`;
42
+ const warning =
43
+ isQuiet || isReferenceAggregatorSchema(schema)
44
+ ? null
45
+ : `Schema ${schemaFile} is missing x-tracking-targets. Falling back to "${DEFAULT_TRACKING_TARGET}".`;
23
46
  return {
24
47
  targets: [DEFAULT_TRACKING_TARGET],
25
48
  warning,
@@ -9,14 +9,29 @@ import { promises as fs } from 'fs';
9
9
  import { URL } from 'url';
10
10
  import { resolveConstraintSchemaPath } from './constraintSchemaPaths.js';
11
11
 
12
+ const schemaFileCache = new Map();
13
+
14
+ export function clearSchemaFileCache() {
15
+ schemaFileCache.clear();
16
+ }
17
+
18
+ function readSchemaFile(filePath) {
19
+ if (!schemaFileCache.has(filePath)) {
20
+ schemaFileCache.set(
21
+ filePath,
22
+ fs.readFile(filePath, 'utf-8').then(JSON.parse),
23
+ );
24
+ }
25
+ return schemaFileCache.get(filePath);
26
+ }
27
+
12
28
  function createAjvInstance(schemas, mainSchema, schemaPath) {
13
29
  const schemaVersion = mainSchema?.$schema;
14
30
 
15
31
  const loadSchema = async (uri) => {
16
32
  const constraintPath = resolveConstraintSchemaPath(uri);
17
33
  if (constraintPath) {
18
- const schemaContent = await fs.readFile(constraintPath, 'utf-8');
19
- return JSON.parse(schemaContent);
34
+ return readSchemaFile(constraintPath);
20
35
  }
21
36
 
22
37
  let localPath;
@@ -29,8 +44,7 @@ function createAjvInstance(schemas, mainSchema, schemaPath) {
29
44
  } else {
30
45
  localPath = path.resolve(schemaPath, uri);
31
46
  }
32
- const schemaContent = await fs.readFile(localPath, 'utf-8');
33
- return JSON.parse(schemaContent);
47
+ return readSchemaFile(localPath);
34
48
  };
35
49
 
36
50
  const options = {
package/index.js CHANGED
@@ -7,8 +7,7 @@ import path from 'path';
7
7
  import { execSync } from 'child_process';
8
8
  import { fileURLToPath } from 'url';
9
9
 
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
11
 
13
12
  export default async function (context, options) {
14
13
  const { siteDir } = context;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-generate-schema-docs",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "Docusaurus plugin to generate documentation from JSON schemas.",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -3,6 +3,9 @@ const path = require('path');
3
3
  const { execSync } = require('child_process');
4
4
  const RefParser = require('@apidevtools/json-schema-ref-parser');
5
5
  const mergeAllOf = require('json-schema-merge-allof');
6
+ const {
7
+ visitSchemaPropertyEntries,
8
+ } = require('../helpers/schemaTraversal.cjs');
6
9
 
7
10
  const logger = {
8
11
  _isQuiet: false,
@@ -86,48 +89,57 @@ function findJsonFiles(dir) {
86
89
  return results;
87
90
  }
88
91
 
92
+ function isRootTrackingSchema(schema) {
93
+ return Boolean(schema?.properties?.event || schema?.['x-tracking-targets']);
94
+ }
95
+
89
96
  function parseSchema(schema, options, prefix = '') {
90
- if (!schema || !schema.properties) {
91
- return [];
92
- }
97
+ const variables = [];
98
+
99
+ visitSchemaPropertyEntries(
100
+ schema,
101
+ (property, context) => {
102
+ variables.push({
103
+ name: context.name,
104
+ description: property.description,
105
+ type: property.type,
106
+ });
107
+ },
108
+ {
109
+ prefix,
110
+ skipArraySubProperties: options.skipArraySubProperties,
111
+ },
112
+ );
93
113
 
94
- let variables = [];
95
- for (const key in schema.properties) {
96
- const property = schema.properties[key];
97
- const currentPath = prefix ? `${prefix}.${key}` : key;
98
- variables.push({
99
- name: currentPath,
100
- description: property.description,
101
- type: property.type,
102
- });
103
- if (
104
- property.type === 'array' &&
105
- property.items &&
106
- !options.skipArraySubProperties
107
- ) {
108
- if (property.items.properties) {
109
- variables.push(
110
- ...parseSchema(property.items, options, `${currentPath}.0`),
111
- );
112
- }
113
- } else if (property.type === 'object' && property.properties) {
114
- variables.push(...parseSchema(property, options, currentPath));
115
- }
116
- }
117
114
  return variables;
118
115
  }
119
116
 
117
+ function shouldIncludeSchemaForGtm(schema) {
118
+ if (!isRootTrackingSchema(schema)) {
119
+ return false;
120
+ }
121
+
122
+ const trackingTargets = schema?.['x-tracking-targets'];
123
+
124
+ return (
125
+ Array.isArray(trackingTargets) &&
126
+ trackingTargets.includes('web-datalayer-js')
127
+ );
128
+ }
129
+
120
130
  async function getVariablesFromSchemas(
121
131
  schemaPath,
122
132
  { skipArraySubProperties = false },
123
133
  ) {
124
134
  const allVariables = new Map();
125
135
  const jsonFiles = findJsonFiles(schemaPath);
126
- const eventFiles = jsonFiles.filter((f) => !f.includes('components'));
127
136
 
128
- for (const file of eventFiles) {
137
+ for (const file of jsonFiles) {
129
138
  try {
130
139
  let schema = await RefParser.bundle(file);
140
+ if (!shouldIncludeSchemaForGtm(schema)) {
141
+ continue;
142
+ }
131
143
  schema = mergeAllOf(schema);
132
144
  const fileVariables = parseSchema(schema, { skipArraySubProperties });
133
145
  for (const variable of fileVariables) {
@@ -205,19 +217,27 @@ function createGtmVariables(variablesToCreate) {
205
217
 
206
218
  function deleteGtmVariables(variablesToDelete) {
207
219
  logger.log(`Found ${variablesToDelete.length} variables to delete.`);
220
+ const deleted = [];
221
+ const failedDeletes = [];
208
222
  for (const v of variablesToDelete) {
209
223
  const command = `gtm variables delete --variable-id ${v.variableId} --force --quiet`;
210
224
  logger.log(`Executing: ${command}`);
211
- execSync(command, { stdio: 'inherit' });
225
+ try {
226
+ execSync(command, { stdio: 'inherit' });
227
+ deleted.push(v.parameter.find((p) => p.key === 'name').value);
228
+ } catch {
229
+ failedDeletes.push({
230
+ name: v.parameter.find((p) => p.key === 'name').value,
231
+ variableId: v.variableId,
232
+ });
233
+ }
212
234
  }
213
- return variablesToDelete.map(
214
- (v) => v.parameter.find((p) => p.key === 'name').value,
215
- );
235
+ return { deleted, failedDeletes };
216
236
  }
217
237
 
218
238
  async function syncGtmVariables(
219
239
  schemaVariables,
220
- { skipArraySubProperties = false },
240
+ { skipArraySubProperties = false } = {},
221
241
  ) {
222
242
  const gtmVariables = getGtmVariables();
223
243
 
@@ -235,12 +255,13 @@ async function syncGtmVariables(
235
255
  );
236
256
 
237
257
  const created = createGtmVariables(toCreate);
238
- const deleted = deleteGtmVariables(toDelete);
258
+ const { deleted, failedDeletes } = deleteGtmVariables(toDelete);
239
259
 
240
260
  logger.log('GTM variable synchronization complete.');
241
261
  return {
242
262
  created,
243
263
  deleted,
264
+ failedDeletes,
244
265
  inSync: inSync.map((v) => v.name),
245
266
  };
246
267
  }
@@ -348,6 +369,14 @@ async function main(argv, deps) {
348
369
  ),
349
370
  );
350
371
  } else {
372
+ if (summary.failedDeletes?.length > 0) {
373
+ log.log(
374
+ `Skipped deleting ${summary.failedDeletes.length} GTM variables (GTM rejected the delete, they may still be referenced):`,
375
+ );
376
+ for (const failed of summary.failedDeletes) {
377
+ log.log(`- ${failed.name} (ID: ${failed.variableId})`);
378
+ }
379
+ }
351
380
  log.log('Synchronization successful!');
352
381
  log.log(
353
382
  `All changes applied in GTM workspace: "${workspaceName}" (ID: ${workspaceId})`,
@@ -385,9 +414,11 @@ module.exports = {
385
414
  main,
386
415
  getVariablesToCreate,
387
416
  getVariablesToDelete,
417
+ getGtmVariables,
388
418
  createGtmVariables,
389
419
  deleteGtmVariables,
390
420
  parseSchema,
421
+ shouldIncludeSchemaForGtm,
391
422
  findJsonFiles,
392
423
  safeJsonParse,
393
424
  logger,
@@ -118,6 +118,41 @@ const PAYLOAD_CONTRACTS = [
118
118
  },
119
119
  },
120
120
  },
121
+ {
122
+ id: 'login-with-user-properties',
123
+ class: 'user_properties',
124
+ example: {
125
+ event: 'login',
126
+ method: 'email',
127
+ user_properties: {
128
+ sign_up_method: 'email',
129
+ allow_ad_personalization_signals: 'false',
130
+ },
131
+ },
132
+ expected: {
133
+ web: {
134
+ eventName: 'login',
135
+ payload: {
136
+ event: 'login',
137
+ method: 'email',
138
+ user_properties: {
139
+ sign_up_method: 'email',
140
+ allow_ad_personalization_signals: 'false',
141
+ },
142
+ },
143
+ },
144
+ firebase: {
145
+ eventName: 'login',
146
+ parameters: {
147
+ method: 'email',
148
+ },
149
+ userProperties: {
150
+ sign_up_method: 'email',
151
+ allow_personalized_ads: 'false',
152
+ },
153
+ },
154
+ },
155
+ },
121
156
  {
122
157
  id: 'web-fallback-json-serialization',
123
158
  class: 'fallback_json_serialization',
@@ -43,7 +43,7 @@ const validateSingleSchema = async (filePath, schemaPath) => {
43
43
  fileHasAnyExample = true;
44
44
  const { example, title } = option;
45
45
 
46
- if (!example) {
46
+ if (typeof example === 'undefined') {
47
47
  errors.push(
48
48
  `x Schema ${file} (option: ${title}) does not produce a valid example.`,
49
49
  );