@syncify/codegen
Version:
Shopify GraphQL codegen handling for the sane developer.
831 lines (722 loc) • 25.9 kB
text/typescript
import type { CodegenPlugin } from '@graphql-codegen/plugin-helpers';
import { plugin } from '@graphql-codegen/typescript';
import { format } from 'prettier';
import ts from 'typescript';
import { Create, gray } from '@syncify/ansi';
import { TypesToPick } from './types';
/**
* Configuration options for the custom GraphQL codegen plugin.
* Defines which types to include in the output and how dependencies are handled.
*
* @example
* ```json
* {
* "pickTypes": ["Market", "Translation"],
* "depthInclusion": 2,
* "skipTypeName": true
* }
* ```
*/
export interface SyncifyPluginConfig {
/**
* An array of type names to explicitly include in the generated TypeScript output.
* These types, along with their dependencies (up to the specified depth), will be extracted
* from the schema. Default types (`DisplayableError`, `PageInfo`, `UserError`, `Node`)
* are always included regardless of this list, but can be overridden here if needed.
*
* @remarks
* - Duplicate entries are automatically removed.
* - If a type is not found in the schema, a warning is logged, but processing continues.
*
* @example
* ```json
* {
* "pickTypes": ["Market", "Translation"]
* }
* ```
* This includes `Market`, `Translation`, and the default types in the output.
*/
pickTypes: TypesToPick;
/**
* The maximum depth to which type dependencies are included in the output.
* Controls how many levels of referenced types are extracted beyond the explicitly picked types.
* If unspecified, all dependencies are included (infinite depth).
*
* @default 1
*
* @remarks
* - Depth 0 includes only the picked types.
* - Depth 1 includes picked types and their direct dependencies.
* - Negative values are treated as 0.
*
* @example
* ```json
* {
* "pickTypes": ["Translation"],
* "depthInclusion": 1
* }
* ```
* Includes `Translation` and its direct dependencies (e.g., `Market`), but not `Market`’s dependencies.
*/
depthInclusion?: number;
/**
* Whether to exclude `__typename` fields from the generated TypeScript types.
* This option is passed through to the `@graphql-codegen/typescript` plugin to control
* whether `__typename` properties are included in the output.
*
* @default true
* @remarks
* - Useful for reducing clutter in the output when `__typename` isn’t needed.
* - Only affects types generated by the underlying TypeScript plugin.
*
* @example
* ```json
* {
* "pickTypes": ["Market"],
* "skipTypeName": true
* }
* ```
* Excludes `__typename` from `Market` and related types.
*/
skipTypeName?: boolean;
}
// Built-in types that shouldn't be transformed
const BUILT_IN_TYPES = new Set([
'string',
'number',
'boolean',
'null',
'undefined',
'any',
'never',
'unknown',
'Array'
]);
// Default types to always include
const DEFAULT_TYPES = [
'DisplayableError',
'PageInfo',
'UserError',
'Node',
'Maybe'
];
// Scalar type mappings
const SCALAR_MAPPING: Record<string, string> = {
ID: 'string',
String: 'string',
URL: 'string',
Date: 'string',
DateTime: 'string',
Color: 'string',
UnsignedInt64: 'string',
UtcOffset: 'string',
Decimal: 'string',
Money: 'string',
FormattedString: 'string',
HTML: 'string',
Int: 'number',
Float: 'number',
BigInt: 'number',
Boolean: 'boolean',
ARN: 'any',
StorefrontID: 'any',
JSON: 'Record<string, any>'
};
/**
* Ensures unique type names in the array
*/
function uniqueTypes (types: TypesToPick) {
return Array.from(new Set([ ...DEFAULT_TYPES, ...types ]));
}
/**
* The main codegen plugin implementation
*/
export default <CodegenPlugin<SyncifyPluginConfig>>{
plugin: async (schema, documents, config, info) => {
const log = Create()
.Break()
.Top('Generating Types')
.Newline()
.toLog({ clear: true });
// Ensure pickTypes are unique
config.pickTypes = uniqueTypes(config.pickTypes) as TypesToPick;
try {
// Generate TypeScript output
const typescriptOutput = await plugin(schema, documents, {
skipTypename: config?.skipTypeName ?? true
}, info);
// Transform the types
const { transformedContent, typeLiterals, includedTypes } = await transformTypes(
typescriptOutput.content,
config,
log
);
// Generate model types if needed
if (typeLiterals.length > 0 && info?.outputFile) {
logOptionalTypeLiterals(typeLiterals, info.outputFile, log);
}
logEnd(includedTypes.size, log);
return { content: transformedContent };
} catch (error) {
log.Error(`Failed to generate types: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
};
/**
* Main transformation function
*/
async function transformTypes (content: string, config: SyncifyPluginConfig, log: any) {
const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
log.Line(`Base TypeScript output generated (${content.length} chars)`);
// Collect types from source
const { typeMap, enumNames, typeLiterals } = collectTypes(sourceFile, log);
// Find types to include based on config
const includedTypes = collectIncludedTypes(typeMap, config, log);
// Transform nodes
const transformedNodes = transformNodes(typeMap, includedTypes, enumNames, sourceFile, log);
// Generate output
let fileContent = printNodes(transformedNodes, sourceFile, log);
fileContent = replaceScalars(fileContent, log);
fileContent = cleanUpContent(fileContent, log);
// Format the output
const transformedContent = await format(
'/* eslint-disable no-use-before-define */\n\n' + fileContent,
{
parser: 'typescript',
tabWidth: 2,
printWidth: 120,
singleQuote: true,
useTabs: false
}
);
return { transformedContent, typeLiterals, includedTypes };
}
/**
* Collects all types from the source file
*/
function collectTypes (sourceFile: ts.SourceFile, log: any) {
const typeMap = new Map<string, ts.TypeAliasDeclaration | ts.InterfaceDeclaration>();
const enumNames = new Set<string>();
const typeLiterals: string[] = [];
// Use a visitor pattern for more efficient traversal
function collectTypesVisitor (node: ts.Node) {
if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
const name = node.name?.text;
if (name) {
typeMap.set(name, node);
typeLiterals.push(name);
log.Line(`Found type: ${name} (${ts.isInterfaceDeclaration(node) ? 'interface' : 'type alias'})`);
}
} else if (ts.isEnumDeclaration(node) && node.name) {
enumNames.add(node.name.text);
log.Line(`Found enum: ${node.name.text} - will be replaced with 'string'`);
}
ts.forEachChild(node, collectTypesVisitor);
}
collectTypesVisitor(sourceFile);
return { typeMap, enumNames, typeLiterals };
}
/**
* Determines which types should be included based on dependencies
*/
function collectIncludedTypes (
typeMap: Map<string, ts.TypeAliasDeclaration | ts.InterfaceDeclaration>,
config: SyncifyPluginConfig,
log: any
) {
const includedTypes = new Map<string, number>();
const maxDepth = config.depthInclusion || Infinity;
// Initialize with selected types
config.pickTypes.forEach(type => includedTypes.set(type, 0));
// Recursively collect type references
function collectReferencedTypes (node: ts.Node, depth: number) {
// Early termination if we're beyond max depth
if (depth > maxDepth) return;
// Helper to process type references
function processTypeReference (typeName: string, currentDepth: number) {
if (!typeMap.has(typeName) || BUILT_IN_TYPES.has(typeName)) return;
const existingDepth = includedTypes.get(typeName);
// Only process if this is a shorter path to the type
if (existingDepth === undefined || existingDepth > currentDepth) {
includedTypes.set(typeName, currentDepth);
log.Line(`Including ${typeName} at depth ${currentDepth}`);
// Process the referenced type's dependencies if it exists
const referencedType = typeMap.get(typeName);
if (referencedType && (currentDepth < maxDepth || config.pickTypes.includes(typeName))) {
collectReferencedTypes(referencedType, currentDepth);
}
}
}
// Process specific node types
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
processTypeReference(node.typeName.text, depth + 1);
} else if (ts.isArrayTypeNode(node)) {
collectReferencedTypes(node.elementType, depth);
} else if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
node.types.forEach(type => collectReferencedTypes(type, depth));
} else if (ts.isParenthesizedTypeNode(node)) {
collectReferencedTypes(node.type, depth);
}
// Continue traversal
ts.forEachChild(node, child => collectReferencedTypes(child, depth));
}
// Start collection from picked types
config.pickTypes.forEach(type => {
if (typeMap.has(type)) {
collectReferencedTypes(typeMap.get(type)!, 0);
} else {
log.Warn(`Type ${type} not found in source`);
}
});
return includedTypes;
}
/**
* Transform nodes for output
*/
function transformNodes (
typeMap: Map<string, ts.TypeAliasDeclaration | ts.InterfaceDeclaration>,
includedTypes: Map<string, number>,
enumNames: Set<string>,
sourceFile: ts.SourceFile,
log: any
) {
const transformedNodes: ts.Node[] = [];
// Create type visitor function
const visitType = createTypeVisitor(enumNames, sourceFile, log);
// Transform each included type
includedTypes.forEach((_, type) => {
const node = typeMap.get(type);
if (!node) {
log.Warn(`Type ${type} not found in typeMap`);
return;
}
transformedNodes.push(transformNode(node, type, visitType, sourceFile, log));
});
// Generate Query types
augmentQueryRoot(typeMap.get('QueryRoot'), transformedNodes, visitType, sourceFile, log);
// Final node transformations
return finalizeNodes(transformedNodes, includedTypes, sourceFile, log);
}
/**
* Creates a reusable type visitor function
*/
function createTypeVisitor (enumNames: Set<string>, sourceFile: ts.SourceFile, log: any) {
return function visitType (node: ts.TypeNode): ts.TypeNode {
log.Line(`Visiting type node: ${node.getText(sourceFile)}`);
// Handle Maybe type unwrapping
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
const refType = node.typeName.text;
if (refType === 'Maybe' && node.typeArguments?.length) {
log.Line(`Unwrapping Maybe<${node.typeArguments[0].getText(sourceFile)}>`);
return visitType(node.typeArguments[0]);
} else if (enumNames.has(refType)) {
log.Line(`Replacing enum ${refType} with 'string'`);
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
}
}
// Handle array types
if (ts.isArrayTypeNode(node)) {
const elementType = visitType(node.elementType);
if (elementType !== node.elementType) {
return ts.factory.createArrayTypeNode(elementType);
}
} else if (ts.isUnionTypeNode(node)) { // Handle union types
const types = node.types.map(visitType);
const allAny = types.every(t => t.kind === ts.SyntaxKind.AnyKeyword);
const hasNonAny = types.some(t => t.kind !== ts.SyntaxKind.AnyKeyword);
if (allAny) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
if (hasNonAny) {
const cleanedTypes = types.filter(t => t.kind !== ts.SyntaxKind.AnyKeyword);
if (cleanedTypes.length === 0) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
if (cleanedTypes.length !== types.length) {
return cleanedTypes.length === 1
? cleanedTypes[0]
: ts.factory.createUnionTypeNode([
...cleanedTypes,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)
] as ts.TypeNode[]);
}
}
}
return node;
};
}
/**
* Transform a single node (type alias or interface)
*/
function transformNode (
node: ts.TypeAliasDeclaration | ts.InterfaceDeclaration,
type: string,
visitType: (node: ts.TypeNode) => ts.TypeNode,
sourceFile: ts.SourceFile,
log: any
) {
let transformedNode: ts.TypeAliasDeclaration | ts.InterfaceDeclaration;
// Special handling for Payload types
if (type.endsWith('Payload')) {
log.Line(`Transforming Payload type: ${type}`);
const prefix = type.replace(/Payload$/, '');
const lowerPrefix = prefix.charAt(0).toLowerCase() + prefix.slice(1);
const newTypeName = `Mutation${prefix}`;
// Create the transformed type
const originalType = ts.isTypeAliasDeclaration(node)
? visitType(node.type)
: ts.factory.createTypeLiteralNode(
node.members.map(m => {
if (ts.isPropertySignature(m) && m.type) {
return ts.factory.createPropertySignature(
m.modifiers,
m.name,
m.questionToken,
visitType(m.type)
);
}
return m;
})
);
const wrappedType = ts.factory.createTypeLiteralNode([
ts.factory.createPropertySignature(
undefined,
lowerPrefix,
undefined,
originalType
)
]);
transformedNode = ts.factory.createTypeAliasDeclaration(
[ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) ],
ts.factory.createIdentifier(newTypeName),
undefined,
wrappedType
);
} else { // Handle regular types (non-payload)
if (ts.isTypeAliasDeclaration(node)) {
transformedNode = ts.factory.createTypeAliasDeclaration(
[ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) ],
node.name,
node.typeParameters,
visitType(node.type)
);
} else {
// Process heritage clauses (extends/implements)
const heritageClauses = processHeritageClauses(node, log);
// Process members
const members = node.members.map(member => {
if (ts.isPropertySignature(member) && member.type) {
return ts.factory.createPropertySignature(
member.modifiers,
member.name,
member.questionToken,
visitType(member.type)
);
}
return member;
});
transformedNode = ts.factory.createInterfaceDeclaration(
[ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) ],
node.name,
node.typeParameters,
heritageClauses?.length > 0 ? heritageClauses : undefined,
members
);
}
}
// Apply JSDoc comments
return addJsDocComment(transformedNode, node, sourceFile, log);
}
/**
* Process heritage clauses, removing any references to 'any'
*/
function processHeritageClauses (node: ts.InterfaceDeclaration, log: any) {
if (!node.heritageClauses) return [];
return node.heritageClauses
.map(clause => {
const types = clause.types
.filter(type => {
if (ts.isIdentifier(type.expression) && type.expression.text === 'any') {
log.Line(`Removing 'any' from heritage clause for ${node.name.text}`);
return false;
}
return true;
});
return types.length > 0
? ts.factory.createHeritageClause(clause.token, types)
: null;
})
.filter(Boolean) as ts.HeritageClause[];
}
/**
* Add JSDoc comment to a node
*/
function addJsDocComment (
node: ts.Node,
originalNode: ts.Node,
sourceFile: ts.SourceFile,
log: any
) {
// Clear any existing comments
let result = ts.setSyntheticLeadingComments(node, undefined);
// Extract JSDoc from original node
const jsDoc = getJsDoc(originalNode, sourceFile);
if (jsDoc) {
const nodeName = ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)
? node.name.text
: 'node';
log.Line(`Adding JSDoc to ${nodeName}: "${jsDoc}"`);
result = ts.addSyntheticLeadingComment(
result,
ts.SyntaxKind.MultiLineCommentTrivia,
`* ${jsDoc}\n `,
true
);
}
return result;
}
/**
* Generate Query types from QueryRoot properties
*/
function augmentQueryRoot (
queryRootNode: ts.TypeAliasDeclaration | ts.InterfaceDeclaration | undefined,
transformedNodes: ts.Node[],
visitType: (node: ts.TypeNode) => ts.TypeNode,
sourceFile: ts.SourceFile,
log: any
) {
if (!queryRootNode) {
log.Warn('QueryRoot not found in typeMap');
return;
}
// Process each property in QueryRoot
const processProperty = (prop: ts.PropertySignature) => {
if (!prop.name || !ts.isIdentifier(prop.name) || !prop.type) {
log.Line(`Skipping property in QueryRoot: ${prop.name ? prop.name.getText(sourceFile) : 'unnamed'} - missing required fields`, gray);
return;
}
const propName = prop.name.getText(sourceFile);
const typeName = `Query${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
const jsDoc = getJsDoc(prop, sourceFile);
// Process the inner type
const innerType = visitType(prop.type);
// Handle Maybe types
const isMaybeType = (
innerType.kind === ts.SyntaxKind.TypeReference &&
ts.isIdentifier((innerType as ts.TypeReferenceNode).typeName) &&
(innerType as ts.TypeReferenceNode).typeName.getText(sourceFile) === 'Maybe' &&
(innerType as ts.TypeReferenceNode).typeArguments?.length
);
// Create the actual type
const unwrappedType = isMaybeType
? (innerType as ts.TypeReferenceNode).typeArguments![0]
: innerType;
const objectType = ts.factory.createTypeLiteralNode([
ts.factory.createPropertySignature(
undefined,
propName,
undefined,
unwrappedType
)
]);
const finalType = isMaybeType
? ts.factory.createTypeReferenceNode('Maybe', [ objectType ])
: objectType;
// Create the new type alias
let newTypeAlias = ts.factory.createTypeAliasDeclaration(
[ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) ],
ts.factory.createIdentifier(typeName),
undefined,
finalType
);
// Add JSDoc if available
if (jsDoc) {
log.Line(`Adding JSDoc to ${typeName}: "${jsDoc}"`);
newTypeAlias = ts.addSyntheticLeadingComment(
newTypeAlias,
ts.SyntaxKind.MultiLineCommentTrivia,
`* ${jsDoc}\n `,
true
);
}
transformedNodes.push(newTypeAlias);
log.Line(`Generated ${typeName} for QueryRoot property ${propName}`);
};
// Process QueryRoot based on its type
if (ts.isInterfaceDeclaration(queryRootNode)) {
log.Line('Processing QueryRoot (interface) for augmentation');
queryRootNode.members.filter(ts.isPropertySignature).forEach(processProperty);
} else if (ts.isTypeAliasDeclaration(queryRootNode) && ts.isTypeLiteralNode(queryRootNode.type)) {
log.Line('Processing QueryRoot (type alias) for augmentation');
queryRootNode.type.members.filter(ts.isPropertySignature).forEach(processProperty);
} else {
log.Line('QueryRoot type is not a type literal or interface, skipping augmentation');
}
}
/**
* Convert nodes to text
*/
function printNodes (nodes: ts.Node[], sourceFile: ts.SourceFile, log: any) {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
return nodes.map(node => {
const text = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
log.Line(`Processed node: ${ts.SyntaxKind[node.kind]}`);
return text;
}).join('\n\n');
}
/**
* Final processing of nodes
*/
function finalizeNodes (
transformedNodes: ts.Node[],
includedTypes: Map<string, number>,
sourceFile: ts.SourceFile,
log: any
) {
const definedTypes = new Set(includedTypes.keys());
// Create transformer for post-processing
const transformer = (context: ts.TransformationContext) => (root: ts.Node): ts.Node => {
const visit = (node: ts.Node): ts.Node => {
// Replace undefined type references with 'any'
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
const refType = node.typeName.text;
if (!definedTypes.has(refType) && !BUILT_IN_TYPES.has(refType)) {
log.Line(`Post-processing: Replacing undefined ${refType} with 'any'`);
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
} else if (
ts.isArrayTypeNode(node) &&
ts.isTypeReferenceNode(node.elementType) &&
ts.isIdentifier(node.elementType.typeName)) { // Replace undefined array element types
const elemTypeName = node.elementType.typeName.text;
if (!definedTypes.has(elemTypeName) && !BUILT_IN_TYPES.has(elemTypeName)) {
log.Line(`Post-processing: Replacing Array<${elemTypeName}> with Array<any>`);
return ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
}
} else if (ts.isIntersectionTypeNode(node)) { // Simplify intersection types
const types = node.types.map(visit);
// Simplify intersections with 'any'
const allAny = types.every(t => t.kind === ts.SyntaxKind.AnyKeyword);
if (allAny) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
// Remove redundant 'any' from intersections
const cleanedTypes = types.filter(t => t.kind !== ts.SyntaxKind.AnyKeyword);
if (cleanedTypes.length === 0) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
if (cleanedTypes.length < types.length) {
return cleanedTypes.length === 1
? cleanedTypes[0]
: ts.factory.createIntersectionTypeNode(cleanedTypes as ts.TypeNode[]);
}
return ts.factory.createIntersectionTypeNode(types as ts.TypeNode[]);
} else if (ts.isUnionTypeNode(node)) { // Simplify union types
const types = node.types.map(visit);
// Simplify unions with 'any'
const allAny = types.every(t => t.kind === ts.SyntaxKind.AnyKeyword);
if (allAny) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
// Remove redundant types when 'any' is present
const hasAny = types.some(t => t.kind === ts.SyntaxKind.AnyKeyword);
if (hasAny) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
} else if (ts.isInterfaceDeclaration(node) && node.heritageClauses) { // Clean up interface heritage clauses
const cleanedHeritage = processHeritageClauses(node, log);
if (cleanedHeritage.length !== node.heritageClauses.length) {
return ts.factory.createInterfaceDeclaration(
node.modifiers,
node.name,
node.typeParameters,
cleanedHeritage.length > 0 ? cleanedHeritage : undefined,
node.members
);
}
}
return ts.visitEachChild(node, visit, context);
};
return ts.visitNode(root, visit) as typeof root;
};
// Apply transformations
const final = transformedNodes.map(node => {
const result = ts.transform(node, [ transformer ]).transformed[0];
return result;
});
// Filter out unnecessary types
return final.filter(node => {
if (ts.isTypeAliasDeclaration(node)) {
const name = node.name.text;
if (name === 'InputMaybe' || name === 'Scalars') {
log.Line(`Removing ${name} type definition`);
return false;
}
}
return true;
});
}
/**
* Replace scalar type references
*/
function replaceScalars (content: string, log: any) {
return content.replace(/Scalars\['([^']+)'\]\['(input|output)'\]/g, (match, scalarName) => {
const replacement = SCALAR_MAPPING[scalarName] || 'any';
log.Line(`Replacing ${match} with ${replacement}`);
return replacement;
});
}
/**
* Clean up the generated content
*/
function cleanUpContent (content: string, log: any) {
return content
// Remove redundant comment markers
.replace(/\* \*/g, '*')
// Remove duplicate JSDoc comments
.replace(/(\/\*\*.*?\*\/)\s*\1/g, '$1')
// Unwrap InputMaybe types
.replace(/InputMaybe<([^]+?)>(?=;)/g, '$1')
// Remove Maybe type definition
.replace(/export type Maybe<T> = null \| any;/, '')
// Unwrap Maybe types
.replace(/(?<=(?:<|: ))Maybe<([^]+?)>(?=(?:;|>))/g, '$1');
}
/**
* Extract JSDoc comment from a node
*/
function getJsDoc (node: ts.Node, sourceFile: ts.SourceFile) {
const leadingComments = ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart());
if (!leadingComments || leadingComments.length === 0) return undefined;
// Find the first JSDoc-style comment
const jsDocComment = leadingComments
.filter(comment => comment.kind === ts.SyntaxKind.MultiLineCommentTrivia)
.map(comment => sourceFile.text.slice(comment.pos + 2, comment.end - 2).trim())
.find(text => text.startsWith('*'));
return jsDocComment?.replace(/^\*/, '').trim();
}
/**
* Generate optional type literals
*/
function logOptionalTypeLiterals (typeLiterals: string[], outputFile: string, log: any) {
const uniqueLiterals = Array.from(new Set(typeLiterals))
.map(name => `'${name}'`)
.join(' | ');
// eslint-disable-next-line
const dtsContent = [
'import type { LiteralUnion } from \'type-fest\';',
'',
`export type Models = LiteralUnion<${uniqueLiterals}, string>;`
].join('\n');
log.Line(`Models type generated with ${Array.from(new Set(typeLiterals)).length} unique type literals`);
}
/**
* Log completion message
*/
function logEnd (typeCount: number, log: any) {
log.Line(`Extracted ${typeCount} types (including dependencies)`)
.Newline()
.End('Generated Types')
.Break();
}