const { compileFromFile } = require('json-schema-to-typescript'); const fs = require('fs'); const recast = require('recast'); const traverse = require('@babel/traverse').default; const {parse} = require('@babel/parser'); const t = require('@babel/types'); const skip = { FillRule: true, ImportRule: true, FontFaceRule: true, FontPaletteValuesRule: true, NamespaceRule: true, CustomMediaRule: true, LayerStatementRule: true, PropertyRule: true, UnknownAtRule: true, DefaultAtRule: true } compileFromFile('node/ast.json', { additionalProperties: false }).then(ts => { ts = ts.replaceAll('For_DefaultAtRule', ''); // Use recast/babel to make some types generic so we can replace them in index.d.ts. let ast = recast.parse(ts, { parser: { parse() { return parse(ts, { sourceType: 'module', plugins: ['typescript'], tokens: true }); } } }); traverse(ast, { Program(path) { process(path.scope.getBinding('Declaration')); process(path.scope.getBinding('MediaQuery')); }, TSInterfaceDeclaration(path) { // Dedupe. if (path.node.id.name.startsWith('GenericBorderFor_LineStyleAnd_')) { if (path.node.id.name.endsWith('_0')) { path.node.id.name = 'GenericBorderFor_LineStyle'; } else { path.remove(); } } }, ReferencedIdentifier(path) { if (path.node.name.startsWith('GenericBorderFor_LineStyleAnd_')) { path.node.name = 'GenericBorderFor_LineStyle'; } }, TSTypeAliasDeclaration(path) { // Workaround for schemars not supporting untagged variants. // https://github.com/GREsau/schemars/issues/222 if ( (path.node.id.name === 'Translate' || path.node.id.name === 'Scale') && path.node.typeAnnotation.type === 'TSUnionType' && path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' && path.node.typeAnnotation.types[1].members[0].key.name === 'xyz' ) { path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); } else if (path.node.id.name === 'AnimationAttachmentRange' && path.node.typeAnnotation.type === 'TSUnionType') { let types = path.node.typeAnnotation.types; if (types[1].type === 'TSTypeLiteral' && types[1].members[0].key.name === 'lengthpercentage') { path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); } if (types[2].type === 'TSTypeLiteral' && types[2].members[0].key.name === 'timelinerange') { path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); } } } }); ts = recast.print(ast, {objectCurlySpacing: false}).code; fs.writeFileSync('node/ast.d.ts', ts) }); function process(binding) { // Follow the references upward from the binding to add generics. for (let reference of binding.referencePaths) { if (reference.node !== binding.identifier) { genericize(reference, binding.identifier.name); } } } function genericize(path, name, seen = new Set()) { if (seen.has(path.node)) return; seen.add(path.node); // Find the parent declaration of the reference, and add a generic if needed. let parent = path.findParent(p => p.isDeclaration()); if (!parent.node.typeParameters) { parent.node.typeParameters = t.tsTypeParameterDeclaration([]); } let params = parent.get('typeParameters'); let param = params.node.params.find(p => p.default.typeName.name === name); if (!param) { params.pushContainer('params', t.tsTypeParameter(null, t.tsTypeReference(t.identifier(name)), name[0])); } // Replace the reference with the generic, or add a type parameter. if (path.node.name === name) { path.replaceWith(t.identifier(name[0])); } else { if (!path.parent.typeParameters) { path.parent.typeParameters = t.tsTypeParameterInstantiation([]); } let param = path.parent.typeParameters.params.find(p => p.typeName.name === name[0]); if (!param) { path.parentPath.get('typeParameters').pushContainer('params', t.tsTypeReference(t.identifier(name[0]))); } } // Keep going to all references of this reference. let binding = path.scope.getBinding(parent.node.id.name); for (let reference of binding.referencePaths) { if (reference.node !== binding.identifier) { genericize(reference, name, seen); } } }