import fs from 'node:fs';
import path from 'node:path';
import chalk from 'chalk';
import { resolve } from 'import-meta-resolve';
import { shouldQuote } from '@nangohq/nango-yaml';
import { TYPES_FILE_NAME } from '../constants.js';
import { printDebug } from '../utils.js';
import { NANGO_VERSION } from '../version.js';
import { parse } from './config.service.js';
import type { FunctionConfig } from '../zeroYaml/definitions.js';
import type { NangoModel, NangoModelField, NangoYamlParsed } from '@nangohq/types';
export type ModelsMap = Map<string, Record<string, any>>;
* Load nango.yaml and generate model.ts
*/
export function loadYamlAndGenerate({ fullPath, debug = false }: { fullPath: string; debug?: boolean }): NangoYamlParsed | null {
if (debug) {
printDebug(`Generating ${TYPES_FILE_NAME} file`);
}
const fp = path.resolve(fullPath, TYPES_FILE_NAME);
if (!fs.existsSync(fp)) {
if (debug) {
printDebug('First compilation');
}
} else {
if (debug) {
printDebug(`File already exists, replacing`);
}
}
const parsing = parse(fullPath, debug);
if (parsing.isErr()) {
console.log(chalk.red(parsing.error.message));
return null;
}
const modelTs = buildModelsTS({ parsed: parsing.value.parsed! });
fs.writeFileSync(fp, modelTs);
if (debug) {
printDebug(`${TYPES_FILE_NAME} generated`);
}
generateNangoJson({ parsed: parsing.value.parsed!, fullPath, debug });
return parsing.value.parsed!;
}
* Build models.ts
*/
export function buildModelsTS({ parsed }: { parsed: NangoYamlParsed }): string {
return `// ---------------------------
// This file was generated by Nango (v${NANGO_VERSION})
// It's recommended to version this file
// https://nango.dev
// ---------------------------
// ------ Models
${generateInterfaces({ parsed }).join('\n\n')}
// ------ /Models
// ------ SDK
${generateSDKTypes()}
// ------ /SDK
// ------ Flows
export const NangoFlows = ${JSON.stringify(parsed.integrations, null, 2)} as const;
// ------ /Flows
`;
}
export function generateInterfaces({ parsed }: { parsed: NangoYamlParsed }): string[] {
const interfaces: string[] = [];
for (const [, model] of parsed.models) {
interfaces.push(modelToTypescript({ model }));
}
return interfaces;
}
* Transform a JSON model to its Typescript equivalent
*/
export function modelToTypescript({ model }: { model: NangoModel }) {
const output: string[] = [];
if (model.isAnon) {
output.push(`export type ${model.name} = ${fieldToTypescript({ field: model.fields[0]! })}`);
} else {
if (model.description) {
output.push([`/**`, ` * ${model.description}`, ` */`].join('\n'));
}
output.push(`export interface ${model.name} {`);
output.push(...fieldsToTypescript({ fields: model.fields }));
output.push(`};`);
}
return output.join('\n');
}
export function fieldsToTypescript({ fields }: { fields: NangoModelField[] }) {
const output: string[] = [];
const dynamic = fields.find((field) => field.dynamic);
if (dynamic) {
if (dynamic.description) {
output.push([` /**`, ` * ${dynamic.description}`, ` */`].join('\n'));
}
output.push(` [key: string]: ${fieldToTypescript({ field: dynamic })};`);
}
for (const field of fields) {
if (field.dynamic) {
continue;
}
if (field.description) {
output.push([` /**`, ` * ${field.description}`, ` */`].join('\n'));
}
output.push(` ${shouldQuote(field.name) ? `"${field.name}"` : field.name}${field.optional ? '?' : ''}: ${fieldToTypescript({ field: field })};`);
}
return output;
}
* Transform a field definition to its typescript equivalent
*/
export function fieldToTypescript({ field }: { field: NangoModelField }): string | boolean | null | undefined | number {
let output = '';
if (Array.isArray(field.value)) {
if (field.union) {
output = field.value.map((f) => fieldToTypescript({ field: f })).join(' | ');
} else {
if (!field.tsType && field.array) {
output = field.value.map((f) => fieldToTypescript({ field: f })).join(' | ');
} else {
output = `{${fieldsToTypescript({ fields: field.value }).join('\n')}}`;
}
if (field.array) {
output = `(${output})[]`;
}
}
} else if (field.model || field.tsType) {
output = `${field.value}${field.array ? '[]' : ''}`;
} else if (field.value === null) {
output = 'null';
} else if (typeof field.value === 'string') {
output = `'${field.value}${field.array ? '[]' : ''}'`;
} else {
output = `${field.value}${field.array ? '[]' : ''}`;
}
if (field.optional && !output.includes('undefined')) {
output = `${output} | undefined`;
}
return output;
}
* Generate SDK types
*/
export function generateSDKTypes() {
const filePath = resolve('@nangohq/runner-sdk/models.d.ts', import.meta.url);
const typesContent = fs.readFileSync(new URL(filePath), 'utf8');
return `
${typesContent}
`;
}
export function generateNangoJson({ fullPath, parsed, debug }: { fullPath: string; parsed: NangoYamlParsed; debug: boolean }): void {
const exportPath = path.resolve(fullPath, '.nango');
if (debug) {
printDebug(`Generating .nango/nango.json in ${exportPath}`);
}
if (!fs.existsSync(exportPath)) {
fs.mkdirSync(exportPath, { recursive: true });
}
const nangoJsonPath = path.join(exportPath, 'nango.json');
fs.writeFileSync(nangoJsonPath, JSON.stringify(parsed.integrations, null, 2));
if (debug) {
printDebug(`Generated export ${nangoJsonPath}`);
}
}
export function generateFunctionsJson({ fullPath, functions, debug }: { fullPath: string; functions: FunctionConfig[]; debug: boolean }): void {
const exportPath = path.resolve(fullPath, '.nango');
const functionsJsonPath = path.join(exportPath, 'functions.json');
if (functions.length === 0) {
if (fs.existsSync(functionsJsonPath)) {
if (debug) {
printDebug(`Removing stale .nango/functions.json in ${exportPath}`);
}
fs.rmSync(functionsJsonPath);
}
return;
}
if (debug) {
printDebug(`Generating .nango/functions.json in ${exportPath}`);
}
if (!fs.existsSync(exportPath)) {
fs.mkdirSync(exportPath, { recursive: true });
}
fs.writeFileSync(functionsJsonPath, JSON.stringify(functions, null, 2));
if (debug) {
printDebug(`Generated export ${functionsJsonPath}`);
}
}