import fs from 'node:fs/promises' import path from 'node:path' import process from 'node:process' import { pathToFileURL } from 'node:url' export const CONFIG_FILE_NAMES = [ 'yx-generate-api.config.mjs', 'yx-generate-api.config.js', 'yx-generate-api.config.cjs', ] export const PARAM_STYLE = { OBJECT: 'object', POSITIONAL: 'positional', } export const SUPPORTED_PARAM_STYLES = Object.values(PARAM_STYLE) export const DEFAULT_SWAGGER_TIMEOUT_MS = 20_000 export const DEFAULT_SYNC_OPTIONS = { blockStart: '// AUTO-GENERATED API EXPORTS START', blockEnd: '// AUTO-GENERATED API EXPORTS END', includeGeneratedIndexSnapshot: true, snapshotTitle: '// generated/index.js content:', } export const loadProjectConfig = async ({ configPath, cwd = process.cwd() } = {}) => { const resolvedConfigPath = await resolveConfigPath({ configPath, cwd }) const importedConfig = await import( `${pathToFileURL(resolvedConfigPath).href}?cacheBust=${Date.now()}` ) const rawConfig = importedConfig.default ?? importedConfig if (!rawConfig || typeof rawConfig !== 'object') { throw new Error(`Config file must export an object: ${resolvedConfigPath}`) } const rootDir = path.dirname(resolvedConfigPath) const syncConfig = rawConfig.sync || {} const externalIndexFile = rawConfig.externalIndexFile ? resolveProjectPath(rootDir, rawConfig.externalIndexFile) : null return { configPath: resolvedConfigPath, rootDir, swaggerUrl: rawConfig.swaggerUrl ? resolveSwaggerSource(rootDir, rawConfig.swaggerUrl) : '', swaggerTimeoutMs: normalizePositiveInteger( rawConfig.swaggerTimeoutMs, 'swaggerTimeoutMs', DEFAULT_SWAGGER_TIMEOUT_MS, ), outputDir: rawConfig.outputDir ? resolveProjectPath(rootDir, rawConfig.outputDir) : resolveProjectPath(rootDir, 'src/api/generated'), generateIndexFile: rawConfig.generateIndexFile ?? true, externalIndexFile, requestImport: normalizeString(rawConfig.requestImport) || '../request', paramStyle: normalizeParamStyle(rawConfig.paramStyle || PARAM_STYLE.OBJECT), cleanOutput: rawConfig.cleanOutput ?? true, sync: { enabled: syncConfig.enabled ?? Boolean(externalIndexFile && (rawConfig.generateIndexFile ?? true)), blockStart: normalizeString(syncConfig.blockStart) || DEFAULT_SYNC_OPTIONS.blockStart, blockEnd: normalizeString(syncConfig.blockEnd) || DEFAULT_SYNC_OPTIONS.blockEnd, includeGeneratedIndexSnapshot: syncConfig.includeGeneratedIndexSnapshot ?? DEFAULT_SYNC_OPTIONS.includeGeneratedIndexSnapshot, snapshotTitle: normalizeString(syncConfig.snapshotTitle) || DEFAULT_SYNC_OPTIONS.snapshotTitle, exportFrom: normalizeString(syncConfig.exportFrom) || null, }, } } export const resolveConfigPath = async ({ configPath, cwd = process.cwd() } = {}) => { if (configPath) { const explicitPath = path.resolve(cwd, configPath) await assertFileExists(explicitPath, `Config file not found: ${explicitPath}`) return explicitPath } for (const fileName of CONFIG_FILE_NAMES) { const candidatePath = path.resolve(cwd, fileName) if (await fileExists(candidatePath)) { return candidatePath } } throw new Error( `Config file not found in ${cwd}. Expected one of: ${CONFIG_FILE_NAMES.join(', ')}`, ) } export const resolveCwdPath = (cwd, targetPath) => { return path.resolve(cwd, targetPath) } export const resolveProjectPath = (projectRoot, targetPath) => { return path.isAbsolute(targetPath) ? targetPath : path.resolve(projectRoot, targetPath) } export const resolveSwaggerSource = (baseDir, source) => { const normalizedSource = normalizeString(source) if (!normalizedSource) { return '' } if (/^https?:\/\//i.test(normalizedSource) || /^file:\/\//i.test(normalizedSource)) { return normalizedSource } return resolveProjectPath(baseDir, normalizedSource) } export const normalizeParamStyle = (value) => { const normalizedValue = normalizeString(value).toLowerCase() if (SUPPORTED_PARAM_STYLES.includes(normalizedValue)) { return normalizedValue } throw new Error( `Unsupported paramStyle: ${value}. Expected one of: ${SUPPORTED_PARAM_STYLES.join(' / ')}`, ) } const normalizeString = (value) => { return String(value || '').trim() } const normalizePositiveInteger = (value, label, fallbackValue) => { if (value === undefined || value === null || String(value).trim() === '') { return fallbackValue } const parsedValue = Number.parseInt(String(value), 10) if (!Number.isInteger(parsedValue) || parsedValue <= 0) { throw new Error(`${label} must be a positive integer. Received: ${value}`) } return parsedValue } const fileExists = async (targetPath) => { try { await fs.access(targetPath) return true } catch { return false } } const assertFileExists = async (targetPath, errorMessage) => { if (!(await fileExists(targetPath))) { throw new Error(errorMessage) } }