docusaurus-plugin-generate-schema-docs 1.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,38 @@
1
+ import React from 'react';
2
+ import CodeBlock from '@theme/CodeBlock';
3
+ import buildExampleFromSchema from '../helpers/buildExampleFromSchema';
4
+
5
+ export default function ExampleDataLayer({ schema }) {
6
+ // 1. Identify properties that need to be reset (cleared) first
7
+ const propertiesToReset = findComplexPropertiesToReset(schema || {});
8
+
9
+ // 2. Build the main example data
10
+ const example = buildExampleFromSchema(schema || {});
11
+
12
+ // 3. Construct the code snippet
13
+ let codeSnippet = '';
14
+
15
+ // If there are properties to reset, push them as null first
16
+ if (propertiesToReset.length > 0)
17
+ {
18
+ const resetObject = {};
19
+ propertiesToReset.forEach(prop => {
20
+ resetObject[prop] = null;
21
+ });
22
+ codeSnippet += `window.dataLayer.push(${JSON.stringify(resetObject, null, 2)});\n`;
23
+ }
24
+
25
+ // Append the main data payload
26
+ codeSnippet += `window.dataLayer.push(${JSON.stringify(example, null, 2)});`;
27
+
28
+ return <CodeBlock language="javascript">{codeSnippet}</CodeBlock>
29
+ };
30
+
31
+ const findComplexPropertiesToReset = (schema) => {
32
+ if (!schema || !schema.properties) return [];
33
+
34
+ return Object.entries(schema.properties)
35
+ .filter(([key, definition]) => definition["x-gtm-clear"] === true)
36
+ .map(([key]) => key);
37
+ }
38
+
@@ -0,0 +1,19 @@
1
+ import SchemaRows from './SchemaRows';
2
+
3
+ export default function PropertiesTable({ schema }) {
4
+
5
+ return <table>
6
+ <thead>
7
+ <tr>
8
+ <th width="20%">Property</th>
9
+ <th width="15%">Type</th>
10
+ <th width="10%">Req</th>
11
+ <th with="15%">Examples</th>
12
+ <th>Description</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <SchemaRows properties={schema.properties} requiredList={schema.required} />
17
+ </tbody>
18
+ </table>
19
+ }
@@ -0,0 +1,10 @@
1
+ import CodeBlock from '@theme/CodeBlock';
2
+
3
+ export default function SchemaJsonViewer({ schema }) {
4
+
5
+ return (
6
+ <details>
7
+ <summary>View Raw JSON Schema</summary>
8
+ <CodeBlock language="json">{JSON.stringify(schema, null, 2)}</CodeBlock>
9
+ </details>);
10
+ }
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+
3
+ const SchemaRows = ({ properties, requiredList = [], level = 0 }) => {
4
+ return (
5
+ <>
6
+ {Object.entries(properties).map(([key, prop]) => {
7
+ const isReq = requiredList.includes(key);
8
+
9
+ // 1. Calculate if it has children (Same as before)
10
+ const hasChildren =
11
+ (prop.type === 'object' &&
12
+ prop.properties &&
13
+ Object.keys(prop.properties).length > 0) ||
14
+ (prop.type === 'array' &&
15
+ prop.items &&
16
+ prop.items.properties &&
17
+ Object.keys(prop.items.properties).length > 0);
18
+
19
+ const isObject = prop.type === 'object';
20
+ const isArrayOfObjects = prop.type === 'array' && prop.items && prop.items.type === 'object';
21
+
22
+ if ((isObject || isArrayOfObjects) && !hasChildren)
23
+ {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ <React.Fragment key={key}>
29
+ {/* Main property row */}
30
+ <tr style={{ backgroundColor: isReq ? 'rgba(255,0,0,0.05)' : 'transparent' }}>
31
+ <td>
32
+ <strong>{key}</strong>
33
+ {hasChildren && <span style={{ fontSize: '0.8em', marginLeft: '5px' }}>⤵</span>}
34
+ </td>
35
+ <td><code>{Array.isArray(prop.type) ? prop.type.join('|') : prop.type}</code></td>
36
+ <td style={{ textAlign: 'center' }}>{isReq ? '✅' : ''}</td>
37
+ <td>{prop.examples ? prop.examples.join(', ') : ''}</td>
38
+ <td>{prop.description || ''}</td>
39
+ </tr>
40
+
41
+ {/* Nested children rendered immediately after parent */}
42
+ {hasChildren && (
43
+ <tr>
44
+ <td colSpan="5" style={{ paddingLeft: '20px', borderLeft: '4px solid #eee' }}>
45
+ <strong>{prop.type === 'array' ? `${key} [ ]` : `${key} { }`}</strong>
46
+ <table style={{ width: '100%', marginTop: '5px' }}>
47
+ <thead>
48
+ <tr>
49
+ <th width="20%">Property</th>
50
+ <th width="15%">Type</th>
51
+ <th width="10%">Req</th>
52
+ <th width="15%">Examples</th>
53
+ <th>Description</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ <SchemaRows
58
+ properties={prop.type === 'object' ? prop.properties : prop.items.properties}
59
+ requiredList={prop.type === 'object' ? prop.required || [] : prop.items.required || []}
60
+ level={level + 1}
61
+ />
62
+ </tbody>
63
+ </table>
64
+ </td>
65
+ </tr>
66
+ )
67
+ }
68
+ </React.Fragment >
69
+ );
70
+ })}
71
+ </>
72
+ );
73
+ };
74
+
75
+ export default SchemaRows;
@@ -0,0 +1,19 @@
1
+ import ExampleDataLayer from './ExampleDataLayer';
2
+ import PropertiesTable from './PropertiesTable';
3
+
4
+ // --- Main Exported Component ---
5
+ // Helper: Build an example payload from per-property `examples` or other hints
6
+
7
+
8
+ export default function SchemaViewer({ schema }) {
9
+
10
+ return (
11
+ <div>
12
+ <h2>DataLayer Example</h2>
13
+ <ExampleDataLayer schema={schema} />
14
+
15
+ <h2>Event Properties</h2>
16
+ <PropertiesTable schema={schema} />
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,68 @@
1
+ import $RefParser from "@apidevtools/json-schema-ref-parser";
2
+ import mergeJsonSchema from "json-schema-merge-allof";
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ export default async function generateEventDocs() {
7
+ // CONFIGURATION
8
+ const SCHEMA_DIR = 'schemas'; // Where your JSON files are
9
+ const OUTPUT_DIR = 'docs/events'; // Where MDX goes
10
+
11
+ // Ensure output dir exists
12
+ if (!fs.existsSync(OUTPUT_DIR))
13
+ {
14
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
15
+ }
16
+
17
+ // Read all JSON files
18
+ const files = fs.readdirSync(SCHEMA_DIR).filter(file => file.endsWith('.json'));
19
+
20
+ console.log(`🚀 Generating documentation for ${files.length} schemas...`);
21
+
22
+ for (const file of files)
23
+ {
24
+ const filePath = path.join(SCHEMA_DIR, file);
25
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
26
+ const schema = JSON.parse(rawContent);
27
+
28
+ // First, dereference all $ref properties
29
+ const clonedSchema = await $RefParser.dereference(filePath, {
30
+ mutateInputSchema: false, dereference: {
31
+ circular: 'ignore'
32
+ }
33
+ });
34
+
35
+ // Then merge allOf properties
36
+ const mergedSchema = mergeJsonSchema(clonedSchema, {
37
+ resolvers: {
38
+ defaultResolver: mergeJsonSchema.options.resolvers.title
39
+ }
40
+ });
41
+
42
+ // Define the MDX Content
43
+ // We embed the JSON directly into the file to avoid Webpack import issues
44
+ const mdxContent = `---
45
+ title: ${schema.title}
46
+ description: ${schema.description}
47
+ sidebar_label: ${schema.title}
48
+ ---
49
+
50
+ import SchemaViewer from '@theme/SchemaViewer';
51
+ import SchemaJsonViewer from '@theme/SchemaJsonViewer';
52
+
53
+ # ${schema.title}
54
+
55
+ ${schema.description}
56
+
57
+ <SchemaViewer schema={${JSON.stringify(mergedSchema)}} />
58
+ <SchemaJsonViewer schema={${JSON.stringify(schema)}} />
59
+ `;
60
+
61
+ // Write the .mdx file
62
+ const outputFilename = file.replace('.json', '.mdx');
63
+ fs.writeFileSync(path.join(OUTPUT_DIR, outputFilename), mdxContent);
64
+ console.log(`✅ Generated docs/events/${outputFilename}`);
65
+ }
66
+
67
+ console.log('🎉 Documentation generation complete!');
68
+ }
@@ -0,0 +1,83 @@
1
+ const buildExampleFromSchema = (schema) => {
2
+ const buildValue = (prop) => {
3
+ if (!prop) return undefined;
4
+
5
+ // 1. Prefer explicit examples or constants if available
6
+ if (prop.examples && prop.examples.length) return prop.examples[0];
7
+ if (prop.const !== undefined) return prop.const;
8
+ if (prop.default !== undefined) return prop.default;
9
+
10
+ const type = Array.isArray(prop.type) ? prop.type[0] : prop.type;
11
+
12
+ if (type === 'object')
13
+ {
14
+ if (prop.properties)
15
+ {
16
+ const obj = {};
17
+ Object.entries(prop.properties).forEach(([k, v]) => {
18
+ const val = buildValue(v);
19
+ // Only add the property if it has a valid value (not undefined)
20
+ if (val !== undefined)
21
+ {
22
+ obj[k] = val;
23
+ }
24
+ });
25
+
26
+ // If the object ends up having no keys, consider it "empty" and return undefined
27
+ if (Object.keys(obj).length === 0) return undefined;
28
+ return obj;
29
+ }
30
+ // Object with no properties defined
31
+ return {};
32
+ }
33
+
34
+ if (type === 'array')
35
+ {
36
+ if (prop.items)
37
+ {
38
+ const itemVal = buildValue(prop.items);
39
+ // If the inner item is valid, return it as an array
40
+ if (itemVal !== undefined)
41
+ {
42
+ return [itemVal];
43
+ }
44
+ }
45
+ // Empty array or array of undefined items
46
+ return [];
47
+ }
48
+
49
+ switch (type)
50
+ {
51
+ case 'string': return '';
52
+ case 'integer':
53
+ case 'number': return 0;
54
+ case 'boolean': return false;
55
+ default: return undefined;
56
+ }
57
+ };
58
+
59
+ // If the schema provides a top-level examples array with an object, prefer it
60
+ if (schema && schema.examples && schema.examples.length)
61
+ {
62
+ const first = schema.examples[0];
63
+ if (typeof first === 'object' && first !== null) return first;
64
+ }
65
+
66
+ // Otherwise, build from properties
67
+ if (schema && schema.properties)
68
+ {
69
+ const out = {};
70
+ Object.entries(schema.properties).forEach(([k, p]) => {
71
+ const val = buildValue(p);
72
+ // Only add top-level keys if they are not undefined
73
+ if (val !== undefined)
74
+ {
75
+ out[k] = val;
76
+ }
77
+ });
78
+ return out;
79
+ }
80
+
81
+ return {};
82
+ }
83
+ export default buildExampleFromSchema;
package/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import validateSchemas from './validateSchemas.js';
2
+ import generateEventDocs from './generateEventDocs.js';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export default function (context, options) {
10
+ return {
11
+ name: 'docusaurus-plugin-generate-schema-docs',
12
+
13
+ getThemePath() {
14
+ return path.resolve(__dirname, './components');
15
+ },
16
+
17
+ extendCli(cli) {
18
+ cli
19
+ .command('validate-schemas')
20
+ .description('Validate JSON Schemas with the examples inside the schemas')
21
+ .action(async () => {
22
+ console.log('Validating GTM Schemas...');
23
+ // You might get the path from 'options' or assume a default
24
+ const schemaPath = options?.path || path.join(context.siteDir, 'schemas');
25
+
26
+ const success = await validateSchemas(schemaPath);
27
+
28
+ if (!success)
29
+ {
30
+ console.error('Validation failed.');
31
+ process.exit(1); // Important for CI to fail!
32
+ }
33
+ console.log('✅ All schemas and examples are valid!');
34
+ });
35
+
36
+ cli
37
+ .command('generate schema-docs')
38
+ .description('Generate schema documentation from JSON schemas')
39
+ .action(async () => {
40
+ // You can pass options here if generateEventDocs needs the path too
41
+ // e.g., await generateEventDocs(options.path || './static/schemas');
42
+ await generateEventDocs();
43
+ });
44
+ },
45
+
46
+ async loadContent() {
47
+ await generateEventDocs();
48
+ },
49
+ };
50
+ }
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "docusaurus-plugin-generate-schema-docs",
3
+ "version": "1.1.0",
4
+ "description": "Docusaurus plugin to generate documentation from JSON schemas.",
5
+ "main": "index.js",
6
+ "license": "MIT",
7
+ "dependencies": {
8
+ "@apidevtools/json-schema-ref-parser": "^15.1.3",
9
+ "fs-extra": "^11.2.0",
10
+ "json-schema-merge-allof": "^0.8.1"
11
+ }
12
+ }
@@ -0,0 +1,86 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import buildExampleFromSchema from './helpers/buildExampleFromSchema';
4
+ import Ajv2020 from 'ajv/dist/2020.js';
5
+ import $RefParser from "@apidevtools/json-schema-ref-parser";
6
+ import mergeJsonSchema from "json-schema-merge-allof";
7
+
8
+ const validateSchemas = async (schemaPath) => {
9
+ const ajv = new Ajv2020();
10
+ ajv.addKeyword('x-gtm-clear');
11
+
12
+ const getAllFiles = (dir, allFiles = []) => {
13
+ const files = fs.readdirSync(dir);
14
+
15
+ files.forEach(file => {
16
+ const filePath = path.join(dir, file);
17
+ if (fs.statSync(filePath).isDirectory())
18
+ {
19
+ getAllFiles(filePath, allFiles);
20
+ } else
21
+ {
22
+ if (file.endsWith('.json'))
23
+ {
24
+ allFiles.push(filePath);
25
+ }
26
+ }
27
+ });
28
+ return allFiles;
29
+ };
30
+
31
+ const allSchemaFiles = getAllFiles('schemas');
32
+ for (const file of allSchemaFiles)
33
+ {
34
+ const schemaContent = fs.readFileSync(file, 'utf-8');
35
+ const schema = JSON.parse(schemaContent);
36
+ ajv.addSchema(schema);
37
+ }
38
+
39
+ const schemaFiles = fs.readdirSync(schemaPath).filter(file => file.endsWith('.json'));
40
+ let allValid = true;
41
+ for (const file of schemaFiles)
42
+ {
43
+ const filePath = path.join(schemaPath, file);
44
+
45
+ // Dereference and merge, same as in generateEventDocs.js
46
+ const clonedSchema = await $RefParser.dereference(filePath, {
47
+ mutateInputSchema: false, dereference: {
48
+ circular: 'ignore'
49
+ }
50
+ });
51
+ const mergedSchema = mergeJsonSchema(clonedSchema, {
52
+ resolvers: {
53
+ defaultResolver: mergeJsonSchema.options.resolvers.title
54
+ }
55
+ });
56
+
57
+ const example_data = buildExampleFromSchema(mergedSchema);
58
+
59
+ if (!example_data)
60
+ {
61
+ console.error(`❌ Schema ${file} does not produce a valid example.`);
62
+ allValid = false;
63
+ } else
64
+ {
65
+ const originalSchema = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
66
+ const validate = ajv.getSchema(originalSchema.$id);
67
+ if (!validate) {
68
+ console.error(`❌ Could not find compiled schema for ${originalSchema.$id}`);
69
+ allValid = false;
70
+ continue;
71
+ }
72
+ if (validate(example_data))
73
+ {
74
+ console.log(`✅ Schema ${file} produced a valid example.`);
75
+ } else
76
+ {
77
+ console.error(`❌ Schema ${file} example data failed validation:`);
78
+ console.error(validate.errors);
79
+ allValid = false;
80
+ }
81
+ }
82
+ }
83
+ return allValid;
84
+ };
85
+
86
+ export default validateSchemas;