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.
- package/components/ExampleDataLayer.js +38 -0
- package/components/PropertiesTable.js +19 -0
- package/components/SchemaJsonViewer.js +10 -0
- package/components/SchemaRows.js +75 -0
- package/components/SchemaViewer.js +19 -0
- package/generateEventDocs.js +68 -0
- package/helpers/buildExampleFromSchema.js +83 -0
- package/index.js +50 -0
- package/package.json +12 -0
- package/validateSchemas.js +86 -0
|
@@ -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;
|