typescript-scaffolder
Version:
 ### Unit Test Coverage: 97.12%
166 lines (165 loc) • 7.61 kB
JavaScript
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;
}
}
;