UNPKG

typescript-scaffolder

Version:

![npm version](https://img.shields.io/npm/v/typescript-scaffolder) ### Unit Test Coverage: 97.12%

166 lines (165 loc) 7.61 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.inferJsonSchema = inferJsonSchema; exports.inferJsonSchemaFromPath = inferJsonSchemaFromPath; const quicktype_core_1 = require("quicktype-core"); const logger_1 = require("./logger"); const object_helpers_1 = require("./object-helpers"); const structure_validators_1 = require("./structure-validators"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); /** * Used to override quicktypes naming coercion by restoring underscores * @param schema * @param newName */ function renameFirstInterface(schema, newName) { return schema.replace(/export interface (\w+)/, () => `export interface ${newName}`); } /** * Infers a schema based on JSON string * NOTE: Use JSON.stringify(obj) on the JSON value * before passing to this function * @param json * @param interfaceName */ async function inferJsonSchema(json, interfaceName) { const funcName = 'inferJsonSchema'; logger_1.Logger.debug(funcName, 'Inferring schema...'); let parsed; try { parsed = JSON.parse(json); } catch (err) { const message = err?.message ?? String(err); const preview = json.slice(0, 120) + (json.length > 120 ? '…' : ''); const fullMessage = `Invalid JSON input: ${message}. Preview: ${preview}`; logger_1.Logger.warn(funcName, fullMessage); throw new Error(fullMessage); } try { // Step 2: Detect globally duplicated keys const duplicateKeys = (0, object_helpers_1.findGloballyDuplicatedKeys)(parsed); logger_1.Logger.debug(funcName, `Found duplicate keys: ${[...duplicateKeys].join(', ')}`); // Step 3: Prefix duplicate keys with parent field names const prefixedKeys = new Set(); const cleanedObject = (0, object_helpers_1.prefixDuplicateKeys)(parsed, duplicateKeys, prefixedKeys); // Step 4: Re-serialize cleaned JSON const cleanedJson = JSON.stringify(cleanedObject); // Step 5: Prepare Quicktype input const jsonInput = (0, quicktype_core_1.jsonInputForTargetLanguage)('typescript'); await jsonInput.addSource({ name: interfaceName, samples: [cleanedJson] }); const inputData = new quicktype_core_1.InputData(); inputData.addInput(jsonInput); logger_1.Logger.debug(funcName, 'Awaiting quicktype result...'); const result = await (0, quicktype_core_1.quicktype)({ inputData, lang: 'typescript', rendererOptions: { 'infer-enums': 'false', 'prefer-unions': 'false', 'just-types': 'true', } }); logger_1.Logger.debug(funcName, 'Schema successfully inferred'); // Step 6: Clean up nullable fields let cleanedLines = result.lines.map((line) => line.replace(/(\s*)(?:(['"`].+?['"`])|(\w+))\s*:\s*null\b/, (_, spacing, quoted, bare) => `${spacing}${quoted ?? bare}?: any`)); // Step 7: Strip prefixes from duplicated keys in field names (only those we actually prefixed) if (prefixedKeys.size > 0) { const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const prefixedList = Array.from(prefixedKeys); cleanedLines = cleanedLines.map(line => { let out = line; for (const pk of prefixedList) { const unprefixed = pk.split(object_helpers_1.prefixDelimiter).pop(); const pattern = new RegExp(escapeRegExp(pk), 'g'); out = out.replace(pattern, unprefixed); } return out; }); } // Step 7.a: Remove delimiter core left inside identifier names by Quicktype (e.g., DataPREFIXObject) { const core = object_helpers_1.prefixDelimiter.replace(/_/g, ''); // e.g., "PREFIX" const coreCapitalized = core.charAt(0) + core.slice(1).toLowerCase(); // e.g., "Prefix" const idToken = new RegExp(`([A-Za-z0-9_])${core}(?=[A-Za-z0-9_])`, 'g'); const idTokenCap = new RegExp(`([A-Za-z0-9_])${coreCapitalized}(?=[A-Za-z0-9_])`, 'g'); cleanedLines = cleanedLines.map((line) => line.replace(idToken, '$1').replace(idTokenCap, '$1')); } // Step 7.5: Validate no accidental duplicate keys in final TypeScript output { const interfaceStart = /^\s*export interface\s+(\w+)\s*\{/; const propertyLine = /^\s*(?:(["'`])([^"'`]+)\1|([A-Za-z_$][\w$]*))\??\s*:/; let currentInterface = null; let seen = new Set(); const dupes = {}; for (const line of cleanedLines) { const startMatch = line.match(interfaceStart); if (startMatch) { currentInterface = startMatch[1]; seen = new Set(); continue; } if (currentInterface && line.trim().startsWith('}')) { currentInterface = null; continue; } if (!currentInterface) continue; const propMatch = line.match(propertyLine); if (propMatch) { const name = (propMatch[2] ?? propMatch[3] ?? '').trim(); const norm = name; // already unprefixed and normalized by prior steps if (seen.has(norm)) { if (!dupes[currentInterface]) dupes[currentInterface] = new Set(); dupes[currentInterface].add(norm); } else { seen.add(norm); } } } const entries = Object.entries(dupes).filter(([, set]) => set.size > 0); if (entries.length > 0) { const msg = entries .map(([iface, set]) => `${iface}: ${Array.from(set).join(', ')}`) .join('; '); throw new Error(`Duplicate properties found in interface(s) ${msg}`); } } // Step 8: Ensure interface name is preserved return renameFirstInterface(cleanedLines.join('\n'), interfaceName); } catch (error) { logger_1.Logger.warn(funcName, `Failed to infer JSON schema: ${error}`); return null; } } /** * Infers a schema from a JSON file based on the full file path * * @param filePath */ async function inferJsonSchemaFromPath(filePath) { const funcName = 'inferJsonSchemaFromPath'; logger_1.Logger.debug(funcName, 'Inferring schema from file...'); try { const rawJson = fs_1.default.readFileSync(filePath, 'utf-8'); logger_1.Logger.debug(funcName, 'Successfully read json file'); // 🔍 Check for duplicate keys in the raw JSON (0, structure_validators_1.assertNoDuplicateKeys)(rawJson, path_1.default.relative(process.cwd(), filePath)); const interfaceName = (0, object_helpers_1.deriveObjectName)(filePath); logger_1.Logger.debug(funcName, 'Inferring interface...'); return await inferJsonSchema(rawJson, interfaceName); } catch (error) { logger_1.Logger.warn(funcName, `Failed to process schema: ${error.message}`); return null; } }