UNPKG

@croct/content-model

Version:

A library for modeling, validating and interpolating structured content.

345 lines (344 loc) 13.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypescriptTypeGenerator = void 0; const object_hash_1 = __importDefault(require("object-hash")); const definition_1 = require("../../definition"); const codeWriter_1 = require("../codeWriter"); const version_1 = require("../../version"); var Dispatcher = definition_1.ContentDefinition.Dispatcher; var PartialVisitor = definition_1.ContentDefinition.PartialVisitor; /** * A visitor that canonicalizes the references of a content definition. * * This visitor finds shared definitions and replaces them with a reference * to a single definition to reduce the size of the generated code. */ class ReferenceCanonicalizer extends PartialVisitor { constructor(referenceMap, options) { super(); this.referenceMap = referenceMap; this.options = options; } static canonicalize(bundle, options) { const referenceMap = {}; for (const [id, definition] of Object.entries(bundle.definitions)) { referenceMap[id] = definition; } const visitor = new ReferenceCanonicalizer(referenceMap, options); const canonicalDefinitions = {}; for (const definition of Object.values(bundle.definitions)) { const key = ReferenceCanonicalizer.getReferenceId(definition, options.prefix); if (canonicalDefinitions[key] === undefined) { canonicalDefinitions[key] = visitor.visit(definition); } } return { root: visitor.visit(bundle.root), definitions: canonicalDefinitions, }; } visitStructure(definition, path) { return { ...definition, attributes: Object.fromEntries(Object.entries(definition.attributes).map(([name, attribute]) => [ name, { ...attribute, type: this.visit(attribute.type, path.joinedWith([name])), }, ])), }; } visitList(definition, path) { return { ...definition, items: this.visit(definition.items, path.joinedWith(['-'])), }; } visitUnion(definition, path) { return { ...definition, types: Object.fromEntries(Object.entries(definition.types).map(([discriminator, type]) => [ discriminator, this.visit(type, path), ])), }; } visitReference(definition) { return { ...definition, id: ReferenceCanonicalizer.getReferenceId(this.referenceMap[definition.id], this.options.prefix), }; } visitDefinition(definition) { return definition; } static getReferenceId(definition, prefix) { return prefix + (0, object_hash_1.default)(definition, { algorithm: 'md5' }).substring(0, 8); } } /** * TypeScript type generator. */ class TypescriptTypeGenerator extends Dispatcher { /** * Constructs a new instance. * * @param writer The code writer. * @param options The generator options. */ constructor(writer, options = {}) { super(); this.writer = writer ?? new codeWriter_1.CodeWriter(); this.options = { ...options, discriminatorProperty: options.discriminatorProperty ?? '_type', }; } visitBoolean() { this.writer.append('boolean'); } visitNumber() { this.writer.append('number'); } visitText(definition) { if (definition.choices !== undefined) { this.writer.append(`'${Object.keys(definition.choices).join("' | '")}'`); return; } this.writer.append('string'); } static formatIdentifier(identifier) { return identifier.replace(/[^a-z0-9_$]+/gi, '_') .replace(/(^[a-z])|_([a-z])/gi, (_, first, second) => (first ?? second).toUpperCase()); } visitList(definition, path) { this.writer.append('Array<'); this.visit(definition.items, path); this.writer.append('>'); } static wrapString(doc, maxLength) { return doc.replace(new RegExp(`(?![^\\n]{1,${maxLength}}$)([^\\n]{1,${maxLength}})\\s`, 'g'), '$1\n').split('\n'); } visitUnion(definition, path) { const subtypes = Object.entries(definition.types); for (let index = 0; index < subtypes.length; index++) { const [discriminator, subtype] = subtypes[index]; if (subtypes.length > 1 && index === 0) { this.writer .indent() .append(' '); } this.writer.append(`({'${this.options.discriminatorProperty}': '${discriminator}'} & `); this.visit(subtype, path); this.writer.append(')'); if (index < subtypes.length - 1) { this.writer.newLine(); } if (index !== subtypes.length - 1) { this.writer.append('| '); } } if (subtypes.length > 1) { this.writer.unindent(); } } generate(collection) { const referenceMap = new Map(); const canonicalizerOptions = { prefix: 'Def$', }; function getReferenceId(definition) { return ReferenceCanonicalizer.getReferenceId(definition, canonicalizerOptions.prefix); } const versionCache = new Map(); function parseVersion(version) { let parsedVersion = versionCache.get(version); if (parsedVersion === undefined) { parsedVersion = version_1.Version.parse(version); versionCache.set(version, parsedVersion); } return parsedVersion; } for (const module of Object.values(collection)) { for (const definitionVersions of Object.values(module.definitions)) { for (const bundle of Object.values(definitionVersions)) { const canonicalizedBundle = ReferenceCanonicalizer.canonicalize(bundle, canonicalizerOptions); const rootKey = getReferenceId(bundle.root); if (!referenceMap.has(rootKey)) { referenceMap.set(rootKey, canonicalizedBundle.root); } for (const [key, definition] of Object.entries(canonicalizedBundle.definitions)) { if (!referenceMap.has(key)) { referenceMap.set(key, definition); } } } } } let typeIndex = 0; for (const [key, definition] of referenceMap.entries()) { this.writeDocBlock(definition.title ?? '', definition.description ?? ''); this.writer.append(`type ${key} = `); this.visit(definition); this.writer .append(';') .newLine(); if (typeIndex++ < referenceMap.size - 1) { this.writer.newLine(); } } this.writer.newLine(); const modules = Object.entries(collection); for (let moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) { const [moduleName, module] = modules[moduleIndex]; this.writer .append(`declare module '${moduleName}' {`) .indent(); for (const [id, definitionVersions] of Object.entries(module.definitions)) { for (const [version, { root, discriminator }] of Object.entries(definitionVersions)) { this.writer .append('type ') .append(TypescriptTypeGenerator.formatIdentifier(id)) .append('V') .append(TypescriptTypeGenerator.formatIdentifier(version)) .append(' = ') .append(getReferenceId(root)); if (discriminator !== undefined && discriminator.values.length > 0) { this.writer .append(' & {') .append(`'${discriminator.property}': `); const values = [...new Set(discriminator.values)]; for (let index = 0; index < values.length; index++) { const value = values[index]; this.writer.append(typeof value === 'string' ? `'${value}'` : `${value}`); if (index < values.length - 1) { this.writer.append(' | '); } } this.writer.append('}'); } this.writer .append(';') .newLine(); } } this.writer .newLine() .append(`export interface ${module.mapName} {`) .indent(); const definitions = Object.entries(module.definitions); for (let definitionIndex = 0; definitionIndex < definitions.length; definitionIndex++) { const [id, definitionVersions] = definitions[definitionIndex]; const latestVersion = Object.keys(definitionVersions) .sort((left, right) => version_1.Version.compareDescending(parseVersion(left), parseVersion(right)))[0]; this.writer .append(`'${id}': {`) .indent(); if (latestVersion !== undefined) { this.writer .append('latest: ') .append(TypescriptTypeGenerator.formatIdentifier(id)) .append('V') .append(TypescriptTypeGenerator.formatIdentifier(latestVersion)) .append(',') .newLine(); } const versions = Object.keys(definitionVersions); for (let versionIndex = 0; versionIndex < versions.length; versionIndex++) { const version = versions[versionIndex]; this.writer .append(`'${version}': `) .append(TypescriptTypeGenerator.formatIdentifier(id)) .append('V') .append(TypescriptTypeGenerator.formatIdentifier(version)) .append(','); if (versionIndex < versions.length - 1) { this.writer.newLine(); } } this.writer .unindent() .newLine() .append('};'); if (definitionIndex < definitions.length - 1) { this.writer.newLine(2); } } this.writer .unindent() .newLine() .append('}') .unindent() .newLine() .append('}'); if (moduleIndex < modules.length - 1) { this.writer.newLine(2); } } return this.writer.toString(); } visitStructure(definition, path) { const attributes = Object.entries(definition.attributes); if (attributes.length === 0) { this.writer.append('Record<string, never>'); return; } this.writer .append('{') .indent(); for (let index = 0; index < attributes.length; index++) { const [name, attribute] = attributes[index]; if (attribute.private === true) { continue; } this.writeDocBlock(attribute.label ?? attribute.type.title ?? '', attribute.description ?? attribute.type.description ?? ''); this.writer .append(`'${name}'`) .append(attribute.optional === true ? '?' : '') .append(': '); this.visit(attribute.type, path.joinedWith([name])); this.writer.append(','); if (index < attributes.length - 1) { this.writer.newLine(); } } this.writer .unindent() .newLine() .append('}'); } visitReference({ id }) { this.writer.append(TypescriptTypeGenerator.formatIdentifier(id)); } writeDocBlock(title, description, maxLength = 80) { const doc = `${title}\n${description}`.trim(); if (doc === '') { return; } const lines = TypescriptTypeGenerator.wrapString(doc, maxLength); this.writer .append('/**') .newLine(); for (let index = 0; index < lines.length; index++) { this.writer .append(' * ') .append(lines[index]); if (index === 0 && lines.length > 1) { this.writer .newLine() .append(' * '); } this.writer.newLine(); } this.writer .append(' */') .newLine(); } } exports.TypescriptTypeGenerator = TypescriptTypeGenerator;