import { FormatHelpers, type TypeScriptPreset } from '@asyncapi/modelina'; import _endent from 'endent'; import { ensureNullable, getArrayItemNonNullTypes, getModelPathPrefix, indexOfNullArrayProperty, indexOfNullObjectProperty, replaceAnyType, } from './utils.js'; // endent is a TypeScript-compiled CJS package that emits // `module.exports = { __esModule: true, default: value }`. // Node.js native ESM does not honour the __esModule sentinel — the default // import receives the whole module.exports wrapper. Pattern 9 in // ESM_MIGRATION_GUIDE.md. The `?? _endent` fallback keeps the CJS build safe. const endent = ((_endent as any).default ?? _endent) as typeof _endent; /** * Exports types. */ export const EXPORT_TYPES_PRESET: TypeScriptPreset = { interface: { self({ content }) { return `export ${content}`; }, }, enum: { self({ content }) { return `export ${content}`; }, }, type: { self({ content }) { return `export ${content}`; }, }, }; /** * Preset for TypeScriptGenerator, additionalProperties added to models as "[k: string]: unknown;" */ export const ADDITIONAL_PROPERTIES_PRESET: TypeScriptPreset = { interface: { self({ content }) { return content; }, property({ property, content }) { if (property.unconstrainedPropertyName === 'additionalProperties') { return `\n[k: string]: unknown;`; } return content; }, }, }; /** * Creates a preset for TypeScriptGenerator that adds imports to resulting models. * Pass `importSpecifierExtension` (e.g. `".js"`) to emit explicit file extensions * on relative specifiers, required for ESM-compatible output. * All cross-model dependencies are TypeScript interfaces, so imports are always * emitted as `import type` to satisfy `verbatimModuleSyntax`. */ export const createImportsPreset = ( importSpecifierExtension = '', ): TypeScriptPreset => ({ interface: { self({ content, model }) { const interfacePrefix = getModelPathPrefix(model.name); const dependencies = model.getNearestDependencies(); const imports = dependencies.map((dep) => ({ class: FormatHelpers.toPascalCase(dep.name), location: interfacePrefix !== getModelPathPrefix(dep.name) ? `../${getModelPathPrefix(dep.name)}/${FormatHelpers.toParamCase( dep.name, )}${importSpecifierExtension}` : `./${FormatHelpers.toParamCase( dep.name, )}${importSpecifierExtension}`, })); return endent` ${imports .sort((a, b) => a.location.localeCompare(b.location)) .map((i) => `import type { ${i.class} } from '${i.location}';`) .join('\n')} ${content}`; }, property({ content }) { return content; }, }, }); /** * Preset for TypeScriptGenerator, sets nullable properties as unions in resulted models(compatibility with zapatos generated models). */ export const NULLABLE_PROPERTY_TO_UNION_PRESET: TypeScriptPreset = { interface: { self({ content }) { return content; }, property({ renderer, property, content }) { let index = indexOfNullObjectProperty(property); // Regular object property, not array. if (index > -1) { property.property.type = ensureNullable(property.property.type, index); return renderer.renderProperty(property); } // Array property. index = indexOfNullArrayProperty(property); if (index > -1) { // Workaround: modelina drops non-null types from nullable array items // e.g. items: { type: ["string", "null"] } renders as null[] instead of (string | null)[]. // Reconstruct from the original schema instead. const nonNullTypes = getArrayItemNonNullTypes(property); if (nonNullTypes) { property.property.type = `(${[...nonNullTypes, 'null'].join( ' | ', )})[]`; return renderer.renderProperty(property); } // This pattern is not perfect but it's good enough for this use case. const regex = /((?\w+)|\((?[\w\s|]+)\))\[\]/; const match = regex.exec(property.property.type); if (match && (match?.groups?.single || match?.groups?.multiple)) { const newType = ensureNullable( match.groups.single ?? match.groups.multiple, index, ); // At this point we assume that the type will be a union of at least two types - null and something else. property.property.type = `(${newType})[]`; return renderer.renderProperty(property); } } return content; }, }, }; /** * Converts all occurrences of 'any' to 'unknown'. */ export const ANY_TO_UNKNOWN_PRESET: TypeScriptPreset = { interface: { property({ renderer, property }) { property.property.type = replaceAnyType( property.property.type, 'unknown', ); return renderer.renderProperty(property); }, }, type: { self({ content }) { // TODO: This is inconsistent with interface but not sure how to use type renderer. return replaceAnyType(content, 'unknown'); }, }, };