|
| 1 | +#!/usr/bin/env node |
| 2 | +"use strict"; |
| 3 | +/* eslint-disable no-restricted-syntax */ |
| 4 | +var __importDefault = (this && this.__importDefault) || function (mod) { |
| 5 | + return (mod && mod.__esModule) ? mod : { "default": mod }; |
| 6 | +}; |
| 7 | +Object.defineProperty(exports, "__esModule", { value: true }); |
| 8 | +/** |
| 9 | + * Script to generate mixin properties from JSON schema |
| 10 | + * |
| 11 | + * This script generates mixin functions for property/holder, property/meta_holder, |
| 12 | + * and property/proto_holder schemas automatically. |
| 13 | + * |
| 14 | + * Usage: |
| 15 | + * node scripts/generate-mixin-properties.js |
| 16 | + */ |
| 17 | +const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface")); |
| 18 | +const child_process_1 = require("child_process"); |
| 19 | +const fs_1 = __importDefault(require("fs")); |
| 20 | +/** |
| 21 | + * Determines if a property should use requiredProp() or prop() |
| 22 | + * @param propertyName - Name of the property |
| 23 | + * @param requiredProperties - Array of required property names |
| 24 | + * @returns - True if property is required |
| 25 | + */ |
| 26 | +function isRequiredProperty(propertyName, requiredProperties) { |
| 27 | + return requiredProperties.includes(propertyName); |
| 28 | +} |
| 29 | +/** |
| 30 | + * Generates TypeScript type annotation for a property |
| 31 | + * @param propertyName - The property name |
| 32 | + * @param schemaName - Name of the schema (for type reference) |
| 33 | + * @returns - TypeScript type annotation |
| 34 | + */ |
| 35 | +function generateTypeAnnotation(propertyName, schemaName) { |
| 36 | + return `${schemaName}["${propertyName}"]`; |
| 37 | +} |
| 38 | +/** |
| 39 | + * Extracts properties from a schema, handling allOf if present |
| 40 | + * @param schema - The JSON schema |
| 41 | + * @returns - Object with properties and required fields |
| 42 | + */ |
| 43 | +function extractSchemaProperties(schema) { |
| 44 | + let properties = {}; |
| 45 | + let required = []; |
| 46 | + // Handle allOf by merging properties from all schemas |
| 47 | + if (schema.allOf && Array.isArray(schema.allOf)) { |
| 48 | + for (const subSchema of schema.allOf) { |
| 49 | + const extracted = extractSchemaProperties(subSchema); |
| 50 | + properties = { ...properties, ...extracted.properties }; |
| 51 | + required = [...required, ...extracted.required]; |
| 52 | + } |
| 53 | + } |
| 54 | + // Add properties from current schema |
| 55 | + if (schema.properties) { |
| 56 | + properties = { ...properties, ...schema.properties }; |
| 57 | + } |
| 58 | + if (schema.required) { |
| 59 | + required = [...required, ...schema.required]; |
| 60 | + } |
| 61 | + return { properties, required }; |
| 62 | +} |
| 63 | +/** |
| 64 | + * Generates the complete mixin function |
| 65 | + * @param schema - The JSON schema |
| 66 | + * @param schemaName - Name of the schema |
| 67 | + * @param mixinTypeName - Name of the mixin type |
| 68 | + * @param entityTypeName - Name of the entity type |
| 69 | + * @param skipFields - Array of field names to skip |
| 70 | + * @returns - Generated TypeScript code |
| 71 | + */ |
| 72 | +function generateMixinFunction(schema, schemaName, mixinTypeName, entityTypeName, skipFields = []) { |
| 73 | + // Convert mixin type name to camelCase for function name |
| 74 | + const functionName = mixinTypeName.charAt(0).toLowerCase() + mixinTypeName.slice(1); |
| 75 | + // Extract properties, handling allOf if present |
| 76 | + const { properties, required } = extractSchemaProperties(schema); |
| 77 | + if (Object.keys(properties).length === 0) { |
| 78 | + throw new Error("No properties found in schema"); |
| 79 | + } |
| 80 | + // Filter out skip fields |
| 81 | + const propertyEntries = Object.entries(properties).filter(([propertyName]) => !skipFields.includes(propertyName)); |
| 82 | + let code = `import type { InMemoryEntity } from "@mat3ra/code/dist/js/entity";\n`; |
| 83 | + code += `import type { ${schemaName} } from "@mat3ra/esse/dist/js/types";\n\n`; |
| 84 | + // Generate the mixin type using Omit utility |
| 85 | + const skipFieldNames = skipFields.map((field) => `"${field}"`).join(" | "); |
| 86 | + code += `export type ${mixinTypeName} = Omit<${schemaName}, ${skipFieldNames}>;\n\n`; |
| 87 | + // Generate the entity type |
| 88 | + code += `export type ${entityTypeName} = InMemoryEntity & ${mixinTypeName};\n\n`; |
| 89 | + code += `export function ${functionName}(item: InMemoryEntity) {\n`; |
| 90 | + code += ` // @ts-expect-error\n`; |
| 91 | + code += ` const properties: InMemoryEntity & ${mixinTypeName} = {\n`; |
| 92 | + for (let i = 0; i < propertyEntries.length; i++) { |
| 93 | + const [propertyName] = propertyEntries[i]; |
| 94 | + const isRequired = isRequiredProperty(propertyName, required); |
| 95 | + const methodName = isRequired ? "requiredProp" : "prop"; |
| 96 | + const typeAnnotation = generateTypeAnnotation(propertyName, schemaName); |
| 97 | + code += `get ${propertyName}() {\n`; |
| 98 | + code += `return this.${methodName}<${typeAnnotation}>("${propertyName}");\n`; |
| 99 | + code += `}`; |
| 100 | + // Add comma for all properties except the last one |
| 101 | + if (i < propertyEntries.length - 1) { |
| 102 | + code += `,\n`; |
| 103 | + } |
| 104 | + else { |
| 105 | + code += `,\n`; |
| 106 | + } |
| 107 | + } |
| 108 | + code += ` };\n\n`; |
| 109 | + code += ` Object.defineProperties(item, Object.getOwnPropertyDescriptors(properties));\n`; |
| 110 | + code += `}\n`; |
| 111 | + return code; |
| 112 | +} |
| 113 | +/** |
| 114 | + * Generates mixin function for a given schema ID |
| 115 | + * @param schemaId - The schema ID (e.g., "property/holder") |
| 116 | + * @param outputPath - The output file path |
| 117 | + * @param skipFields - Array of field names to skip |
| 118 | + * @returns - Generated TypeScript code |
| 119 | + */ |
| 120 | +function generateMixinFromSchemaId(schemaId, outputPath, skipFields = []) { |
| 121 | + var _a, _b; |
| 122 | + // Get the resolved schema by ID |
| 123 | + const schema = JSONSchemasInterface_1.default.getSchemaById(schemaId); |
| 124 | + if (!schema) { |
| 125 | + throw new Error(`Schema not found with ID: ${schemaId}`); |
| 126 | + } |
| 127 | + // Extract schema name from title for import |
| 128 | + let schemaName; |
| 129 | + if (schema.title) { |
| 130 | + // Convert title to proper schema name |
| 131 | + schemaName = schema.title |
| 132 | + .split(/\s+/) |
| 133 | + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) |
| 134 | + .join(""); |
| 135 | + } |
| 136 | + else { |
| 137 | + // Convert schema ID to proper schema name |
| 138 | + schemaName = |
| 139 | + schemaId |
| 140 | + .split(/[/-]/) |
| 141 | + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) |
| 142 | + .join("") + "Schema"; |
| 143 | + } |
| 144 | + // Extract type names from output file path |
| 145 | + const fileName = (_b = (_a = outputPath.split("/").pop()) === null || _a === void 0 ? void 0 : _a.replace(".ts", "")) !== null && _b !== void 0 ? _b : ""; |
| 146 | + if (!fileName) { |
| 147 | + throw new Error(`Invalid output path: ${outputPath}`); |
| 148 | + } |
| 149 | + const mixinTypeName = fileName; |
| 150 | + const entityTypeName = fileName.replace("SchemaMixin", "InMemoryEntity"); |
| 151 | + // Generate the complete mixin function |
| 152 | + return generateMixinFunction(schema, schemaName, mixinTypeName, entityTypeName, skipFields); |
| 153 | +} |
| 154 | +/** |
| 155 | + * Runs ESLint autofix on generated files |
| 156 | + * @param filePaths - Array of file paths to fix |
| 157 | + */ |
| 158 | +function runESLintAutofix(filePaths) { |
| 159 | + if (filePaths.length === 0) |
| 160 | + return; |
| 161 | + try { |
| 162 | + console.log("Running ESLint autofix on generated files..."); |
| 163 | + const filesToFix = filePaths.join(" "); |
| 164 | + (0, child_process_1.execSync)(`npx eslint --fix ${filesToFix}`, { stdio: "inherit" }); |
| 165 | + console.log("✓ ESLint autofix completed successfully"); |
| 166 | + } |
| 167 | + catch (error) { |
| 168 | + console.warn("⚠ ESLint autofix failed:", error instanceof Error ? error.message : String(error)); |
| 169 | + // Don't fail the entire process if ESLint autofix fails |
| 170 | + } |
| 171 | +} |
| 172 | +/** |
| 173 | + * Generates mixins for multiple schemas |
| 174 | + * @param schemas - Array of JSON schemas to use for generation |
| 175 | + * @param outputPaths - Object mapping schema IDs to output file paths |
| 176 | + * @param skipFields - Array of field names to skip during generation |
| 177 | + * @returns - Object with success and error counts |
| 178 | + */ |
| 179 | +function generateShemaMixin(schemas, outputPaths, skipFields = []) { |
| 180 | + // Setup schemas |
| 181 | + JSONSchemasInterface_1.default.setSchemas(schemas); |
| 182 | + console.log("Generating mixin properties for all schemas..."); |
| 183 | + const schemaIds = Object.keys(outputPaths); |
| 184 | + let successCount = 0; |
| 185 | + let errorCount = 0; |
| 186 | + const generatedFiles = []; |
| 187 | + for (const schemaId of schemaIds) { |
| 188 | + try { |
| 189 | + console.log(`\nProcessing schema: ${schemaId}`); |
| 190 | + const outputPath = outputPaths[schemaId]; |
| 191 | + if (!outputPath) { |
| 192 | + throw new Error(`No output path defined for schema: ${schemaId}`); |
| 193 | + } |
| 194 | + const generatedCode = generateMixinFromSchemaId(schemaId, outputPath, skipFields); |
| 195 | + // Ensure the directory exists |
| 196 | + const dir = outputPath.substring(0, outputPath.lastIndexOf("/")); |
| 197 | + if (!fs_1.default.existsSync(dir)) { |
| 198 | + fs_1.default.mkdirSync(dir, { recursive: true }); |
| 199 | + } |
| 200 | + fs_1.default.writeFileSync(outputPath, generatedCode); |
| 201 | + console.log(`✓ Generated mixin written to: ${outputPath}`); |
| 202 | + generatedFiles.push(outputPath); |
| 203 | + successCount += 1; |
| 204 | + } |
| 205 | + catch (error) { |
| 206 | + console.error(`✗ Error processing schema ${schemaId}: ${error instanceof Error ? error.message : String(error)}`); |
| 207 | + errorCount += 1; |
| 208 | + } |
| 209 | + } |
| 210 | + // Run ESLint autofix on generated files |
| 211 | + if (generatedFiles.length > 0) { |
| 212 | + runESLintAutofix(generatedFiles); |
| 213 | + } |
| 214 | + console.log(`\n=== Summary ===`); |
| 215 | + console.log(`Successfully generated: ${successCount} mixins`); |
| 216 | + if (errorCount > 0) { |
| 217 | + console.log(`Errors: ${errorCount} schemas failed`); |
| 218 | + } |
| 219 | + else { |
| 220 | + console.log("All mixins generated successfully!"); |
| 221 | + } |
| 222 | + return { successCount, errorCount }; |
| 223 | +} |
| 224 | +/** |
| 225 | + * @example |
| 226 | + * ```ts |
| 227 | + * import generateShemaMixin from "@mat3ra/code/dist/js/generateSchemaMixin"; |
| 228 | + * import allSchemas from "@mat3ra/esse/dist/js/schemas.json"; |
| 229 | + * |
| 230 | + * const result = generateShemaMixin(allSchemas, OUTPUT_PATHS, SKIP_FIELDS); |
| 231 | + * |
| 232 | + * if (result.errorCount > 0) { |
| 233 | + * process.exit(1); |
| 234 | + * } |
| 235 | + * ``` |
| 236 | + */ |
| 237 | +exports.default = generateShemaMixin; |
0 commit comments