Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
build(types): add script to generate Config types
Add script `packages/mermaid/scripts/create-types-from-json-schema.mjs` to automatically generate the TypeScript definition for `MermaidConfig` from the `MermaidConfig` JSON Schema at `packages/mermaid/src/schemas/config.schema.yaml`. To do this, we are using this library [`json-schema-to-typescript`][1], which is also used by Webpack to generate their types from their JSON Schema. In order to make sure that this isn't a breaking change, the script makes all fields **optional**, as that is what the original typescript file has. Additionally, I've put in some custom logic into the script, so that the exact same order is used for the TypeScript file, to make the `git diff` easier to review. In the future, we can remove this custom logic, once we no longer need to worry about `git merge` conflicts. [1]: https://github.com/bcherny/json-schema-to-typescript
- Loading branch information
1 parent
f9d3d53
commit a156446
Showing
6 changed files
with
619 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
234 changes: 234 additions & 0 deletions
234
packages/mermaid/scripts/create-types-from-json-schema.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
/** | ||
* Script to load Mermaid Config JSON Schema from YAML and to: | ||
* | ||
* - Validate JSON Schema | ||
* | ||
* Then to generate: | ||
* | ||
* - config.types.ts TypeScript file | ||
*/ | ||
|
||
/* eslint-disable no-console */ | ||
|
||
import { readFile, writeFile } from 'node:fs/promises'; | ||
import { join } from 'node:path'; | ||
import assert from 'node:assert'; | ||
|
||
import { load, JSON_SCHEMA } from 'js-yaml'; | ||
import { compile } from 'json-schema-to-typescript'; | ||
|
||
import Ajv2019 from 'ajv/dist/2019.js'; | ||
|
||
// options for running the main command | ||
const verifyOnly = process.argv.includes('--verify'); | ||
|
||
/** | ||
* @typedef {import("ajv/dist/2019.js").JSONSchemaType} JSONSchemaType | ||
* @typedef {import("ajv/dist/2019.js").AnySchema} AnySchema | ||
* @typedef {import("../src/config.type").MermaidConfig} MermaidConfig | ||
* This type might be slightly wrong, since we're creating it in this script. | ||
* @typedef {import("../src/config.type").BaseDiagramConfig} BaseDiagramConfig | ||
* | ||
*/ | ||
|
||
/** | ||
* All of the keys in the mermaid config that have a mermaid diagram config. | ||
*/ | ||
const MERMAID_CONFIG_DIAGRAM_KEYS = [ | ||
'flowchart', | ||
'sequence', | ||
'gantt', | ||
'journey', | ||
'class', | ||
'state', | ||
'er', | ||
'pie', | ||
'requirement', | ||
'mindmap', | ||
'timeline', | ||
'gitGraph', | ||
'c4', | ||
]; | ||
|
||
/** | ||
* Loads the MermaidConfig JSON schema YAML file. | ||
* | ||
* @returns {Promise<unknown>} - The loaded JSON Schema, use {@link validateSchema} to confirm | ||
* it is a valid JSON Schema. | ||
*/ | ||
async function loadJsonSchemaFromYaml() { | ||
const configSchemaFile = join('src', 'schemas', 'config.schema.yaml'); | ||
const contentsYaml = await readFile(configSchemaFile, { encoding: 'utf8' }); | ||
const jsonSchema = load(contentsYaml, { | ||
filename: configSchemaFile, | ||
// only allow JSON types in our YAML doc (will probably be default in YAML 1.3) | ||
// e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`. | ||
schema: JSON_SCHEMA, | ||
}); | ||
return jsonSchema; | ||
} | ||
|
||
/** | ||
* Asserts that the given object is a valid JSON Schema object. | ||
* | ||
* @param {unknown} jsonSchema - The object to validate as JSON Schema 2019-09 | ||
* @throws {Error} if the given object is invalid. | ||
* @returns {asserts jsonSchema is JSONSchemaType<MermaidConfig>} | ||
*/ | ||
function validateSchema(jsonSchema) { | ||
const ajv = new Ajv2019({ | ||
allErrors: true, | ||
allowUnionTypes: true, | ||
strict: true, | ||
}); | ||
|
||
ajv.addKeyword({ | ||
keyword: 'meta:enum', // used by jsonschema2md (in docs.mts script) | ||
errors: false, | ||
}); | ||
ajv.addKeyword({ | ||
keyword: 'tsType', // used by json-schema-to-typescript | ||
errors: false, | ||
}); | ||
|
||
ajv.compile(jsonSchema); | ||
} | ||
|
||
/** | ||
* Generate a typescript definition from a JSON Schema using json-schema-to-typescript. | ||
* | ||
* @param {JSONSchemaType<MermaidConfig>} mermaidConfigSchema - The input JSON Schema. | ||
*/ | ||
async function generateTypescript(mermaidConfigSchema) { | ||
/** | ||
* Replace all usages of `allOf` with `extends`. | ||
* | ||
* `extends` is only valid JSON Schema in very old versions of JSON Schema. | ||
* However, json-schema-to-typescript creates much nicer types when using | ||
* `extends`, so we should use them instead when possible. | ||
* | ||
* @param {JSONSchemaType} schema - The input schema. | ||
* @returns {JSONSchemaType} The schema with `allOf` replaced with `extends`. | ||
*/ | ||
function replaceAllOfWithExtends(schema) { | ||
if (schema['allOf']) { | ||
const { allOf, ...schemaWithoutAllOf } = schema; | ||
return { | ||
...schemaWithoutAllOf, | ||
extends: allOf, | ||
}; | ||
} | ||
return schema; | ||
} | ||
|
||
/** | ||
* For backwards compatibility with older Mermaid Typescript defs, | ||
* we need to make all value optional instead of required. | ||
* | ||
* This is because the `MermaidConfig` type is used as an input, and everything is optional, | ||
* since all the required values have default values.s | ||
* | ||
* In the future, we should make make the input to Mermaid `Partial<MermaidConfig>`. | ||
* | ||
* @todo TODO: Remove this function when Mermaid releases a new breaking change. | ||
* @param {JSONSchemaType} schema - The input schema. | ||
* @returns {JSONSchemaType} The schema with all required values removed. | ||
*/ | ||
function removeRequired(schema) { | ||
return { ...schema, required: [] }; | ||
} | ||
|
||
/** | ||
* This is a temporary hack to control the order the types are generated in. | ||
* | ||
* By default, json-schema-to-typescript outputs the $defs in the order they | ||
* are used, then any unused schemas at the end. | ||
* | ||
* **The only purpose of this function is to make the `git diff` simpler** | ||
* **We should remove this later to simplify the code** | ||
* | ||
* @todo TODO: Remove this function in a future PR. | ||
* @param {JSONSchemaType} schema - The input schema. | ||
* @returns {JSONSchemaType} The schema with all `$ref`s removed. | ||
*/ | ||
function unrefSubschemas(schema) { | ||
return { | ||
...schema, | ||
properties: Object.fromEntries( | ||
Object.entries(schema.properties).map(([key, propertySchema]) => { | ||
if (MERMAID_CONFIG_DIAGRAM_KEYS.includes(key)) { | ||
const { $ref, ...propertySchemaWithoutRef } = propertySchema; | ||
const [ | ||
_root, // eslint-disable-line @typescript-eslint/no-unused-vars | ||
_defs, // eslint-disable-line @typescript-eslint/no-unused-vars | ||
defName, | ||
] = $ref.split('/'); | ||
return [ | ||
key, | ||
{ | ||
...propertySchemaWithoutRef, | ||
tsType: defName, | ||
}, | ||
]; | ||
} | ||
return [key, propertySchema]; | ||
}) | ||
), | ||
}; | ||
} | ||
|
||
assert.ok(mermaidConfigSchema.$defs); | ||
const modifiedSchema = { | ||
...unrefSubschemas(removeRequired(mermaidConfigSchema)), | ||
|
||
$defs: Object.fromEntries( | ||
Object.entries(mermaidConfigSchema.$defs).map(([key, subSchema]) => { | ||
return [key, removeRequired(replaceAllOfWithExtends(subSchema))]; | ||
}) | ||
), | ||
}; | ||
|
||
const typescriptFile = await compile( | ||
modifiedSchema, // json-schema-to-typescript only allows JSON Schema 4 as input type | ||
'MermaidConfig', | ||
{ | ||
additionalProperties: false, // in JSON Schema 2019-09, these are called `unevaluatedProperties` | ||
unreachableDefinitions: true, // definition for FontConfig is unreachable | ||
} | ||
); | ||
|
||
// TODO, should we somehow use the functions from `docs.mts` instead? | ||
if (verifyOnly) { | ||
const originalFile = await readFile('./src/config.type.ts', { encoding: 'utf-8' }); | ||
if (typescriptFile !== originalFile) { | ||
console.error('❌ Error: ./src/config.type.ts will be changed.'); | ||
console.error("Please run 'pnpm run --filter mermaid types:build-config' to update this"); | ||
process.exitCode = 1; | ||
} else { | ||
console.log('✅ ./src/config.type.ts will be unchanged'); | ||
} | ||
} else { | ||
console.log('Writing typescript file to ./src/config.type.ts'); | ||
await writeFile('./src/config.type.ts', typescriptFile, { encoding: 'utf8' }); | ||
} | ||
} | ||
|
||
/** Main function */ | ||
async function main() { | ||
if (verifyOnly) { | ||
console.log( | ||
'Verifying that ./src/config.type.ts is in sync with src/schemas/config.schema.yaml' | ||
); | ||
} | ||
|
||
const configJsonSchema = await loadJsonSchemaFromYaml(); | ||
validateSchema(configJsonSchema); | ||
|
||
// Generate types from JSON Schema | ||
await generateTypescript(configJsonSchema); | ||
} | ||
|
||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
Oops, something went wrong.