@v19i/openapi-enum-arrays
Version:
A @hey-api/openapi-ts plugin that generates typed enum arrays with intelligent conflict resolution
251 lines (250 loc) • 9.85 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnumParser = void 0;
const semantic_naming_1 = require("./semantic-naming");
/**
* Handles line processing, comments, and whitespace filtering
*/
class TypeScriptTokenizer {
shouldSkipLine(line) {
if (!line)
return true;
const commentPrefixes = ["//", "*", "/*", "/**"];
return commentPrefixes.some((prefix) => line.startsWith(prefix));
}
cleanLine(line) {
return line.trim();
}
countOccurrences(text, character) {
return (text.match(new RegExp(`\\${character}`, "g")) || []).length;
}
}
/**
* Tracks nesting depth and builds context paths
*/
class ScopeTracker {
contextPath = [];
nestingLevel = 0;
isInTypeDefinition = false;
startTypeDefinition(typeName) {
this.contextPath = [typeName];
this.nestingLevel = 0;
this.isInTypeDefinition = true;
}
enterNestedObject(propertyName, braceCount) {
this.contextPath.push(propertyName);
this.nestingLevel += braceCount;
}
exitScope(braceCount) {
this.nestingLevel -= braceCount;
for (let i = 0; i < braceCount; i++) {
if (this.contextPath.length > 1) {
this.contextPath.pop();
}
}
if (this.nestingLevel <= 0) {
this.isInTypeDefinition = false;
this.contextPath = [];
this.nestingLevel = 0;
}
}
adjustNestingLevel(braceCount) {
this.nestingLevel += braceCount;
}
getCurrentPath() {
return [...this.contextPath];
}
isParsingType() {
return this.isInTypeDefinition;
}
hasContext() {
return this.contextPath.length > 0;
}
}
/**
* Identifies different property patterns in TypeScript code
*/
class PropertyDetector {
extractTypeDefinitionName(line) {
const typeDefinitionMatch = line.match(/^export type\s+(\w+)\s*=\s*\{?/);
return typeDefinitionMatch ? typeDefinitionMatch[1] : null;
}
extractNestedObjectProperty(line) {
const nestedObjectMatch = line.match(/(\w+)\??\s*:\s*\{/);
return nestedObjectMatch ? nestedObjectMatch[1] : null;
}
extractUnionTypeProperty(line) {
const unionPropertyMatch = line.match(/(\w+)\??\s*:\s*(.+?)(?:[;,]|$)/);
if (unionPropertyMatch) {
return {
propertyName: unionPropertyMatch[1],
typeDefinition: unionPropertyMatch[2].trim(),
};
}
return null;
}
hasObjectOpeningBrace(line) {
const colonIndex = line.indexOf(":");
if (colonIndex === -1)
return false;
const afterColon = line.substring(colonIndex + 1);
const braceIndex = afterColon.indexOf("{");
const firstQuoteIndex = Math.min(afterColon.indexOf("'") === -1 ? Infinity : afterColon.indexOf("'"), afterColon.indexOf('"') === -1 ? Infinity : afterColon.indexOf('"'), afterColon.indexOf("`") === -1 ? Infinity : afterColon.indexOf("`"));
return braceIndex !== -1 && braceIndex < firstQuoteIndex;
}
}
/**
* Extracts union type values from type definitions
*/
class UnionExtractor {
extractEnumValues(typeDefinition) {
const values = new Set();
const arrayMatch = typeDefinition.match(/Array<([^>]+)>/);
if (arrayMatch) {
const innerType = arrayMatch[1];
const arrayValues = this.extractEnumValues(innerType);
arrayValues.forEach((v) => values.add(v));
return Array.from(values);
}
if (typeDefinition.includes("|")) {
const parts = typeDefinition.split("|").map((part) => part.trim());
for (const part of parts) {
const valueMatch = part.match(/^(['"`])(.+?)\1$/);
if (valueMatch) {
values.add(valueMatch[2]);
}
}
}
return Array.from(values);
}
}
/**
* Extracts enum information from TypeScript type definitions
*/
class EnumParser {
unionTypeRegex = /(['"`])[^'"`]*\1(\s*\|\s*(['"`])[^'"`]*\3)+/g;
typeDefRegex = /^export type\s+(\w+)\s*=\s*(.+);?$/gm;
propertyTypeRegex = /(\w+)\??\s*:\s*([^;,}]+)/g;
semanticNaming = new semantic_naming_1.SemanticNaming();
// Component instances
tokenizer = new TypeScriptTokenizer();
propertyDetector = new PropertyDetector();
unionExtractor = new UnionExtractor();
parseEnumsFromTypeFile(content) {
const enums = [];
// Parse standalone type definitions
enums.push(...this.parseStandaloneTypes(content));
// Parse inline property types within interfaces/types
enums.push(...this.extractEnumsFromNestedTypeProperties(content));
return enums;
}
parseStandaloneTypes(content) {
const enums = [];
let match = this.typeDefRegex.exec(content);
while (match !== null) {
const [, typeName, typeDefinition] = match;
// Clean up the type definition by removing trailing semicolon
const cleanTypeDefinition = typeDefinition.replace(/;$/, "").trim();
const enumValues = this.extractEnumValues(cleanTypeDefinition);
if (enumValues.length > 0) {
// Use semantic naming for standalone types too
const originalTypePath = `export type ${typeName}`;
const semanticName = this.semanticNaming.generateName(enumValues, originalTypePath);
enums.push({
name: semanticName,
values: enumValues,
originalTypePath,
});
}
match = this.typeDefRegex.exec(content);
}
return enums;
}
extractEnumsFromNestedTypeProperties(content) {
const foundEnums = [];
const sourceLines = content.split("\n");
const scopeTracker = new ScopeTracker();
for (const rawLine of sourceLines) {
const cleanLine = this.tokenizer.cleanLine(rawLine);
if (this.tokenizer.shouldSkipLine(cleanLine)) {
continue;
}
const typeDefinitionName = this.propertyDetector.extractTypeDefinitionName(cleanLine);
if (typeDefinitionName) {
scopeTracker.startTypeDefinition(typeDefinitionName);
if (cleanLine.includes("{")) {
scopeTracker.adjustNestingLevel(1);
}
continue;
}
if (!scopeTracker.isParsingType()) {
continue;
}
const openingBraceCount = this.tokenizer.countOccurrences(cleanLine, "{");
const closingBraceCount = this.tokenizer.countOccurrences(cleanLine, "}");
const nestedObjectPropertyName = this.propertyDetector.extractNestedObjectProperty(cleanLine);
if (nestedObjectPropertyName) {
scopeTracker.enterNestedObject(nestedObjectPropertyName, openingBraceCount);
continue;
}
const unionTypeProperty = this.propertyDetector.extractUnionTypeProperty(cleanLine);
if (unionTypeProperty && scopeTracker.hasContext()) {
const { propertyName, typeDefinition } = unionTypeProperty;
const hasObjectBrace = this.propertyDetector.hasObjectOpeningBrace(cleanLine);
if (hasObjectBrace) {
scopeTracker.enterNestedObject(propertyName, openingBraceCount);
continue;
}
const cleanTypeDefinition = typeDefinition.replace(/[;,]$/, "").trim();
const enumValues = this.unionExtractor.extractEnumValues(cleanTypeDefinition);
if (enumValues.length > 0) {
const currentPath = scopeTracker.getCurrentPath();
const fullPropertyPath = [...currentPath, propertyName].join(".");
const enumName = this.semanticNaming.generateName(enumValues, fullPropertyPath);
foundEnums.push({
name: enumName,
values: enumValues,
originalTypePath: fullPropertyPath,
});
}
}
if (closingBraceCount > 0) {
scopeTracker.exitScope(closingBraceCount);
}
if (openingBraceCount > 0 &&
!unionTypeProperty &&
!nestedObjectPropertyName &&
!typeDefinitionName) {
scopeTracker.adjustNestingLevel(openingBraceCount);
}
}
return foundEnums;
}
extractEnumValues(typeDefinition) {
const values = new Set();
// Handle Array<'value1' | 'value2'> patterns first
const arrayMatch = typeDefinition.match(/Array<([^>]+)>/);
if (arrayMatch) {
const innerType = arrayMatch[1];
const arrayValues = this.extractEnumValues(innerType);
arrayValues.forEach((v) => values.add(v));
return Array.from(values);
}
// Handle union types with string literals
if (typeDefinition.includes("|")) {
const parts = typeDefinition.split("|").map((part) => part.trim());
for (const part of parts) {
// Match single quotes, double quotes, or backticks
const valueMatch = part.match(/^(['"`])(.+?)\1$/);
if (valueMatch) {
values.add(valueMatch[2]);
}
}
}
return Array.from(values);
}
capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
exports.EnumParser = EnumParser;
;