yx_generate_api_js/src/core/config.js

161 lines
4.9 KiB
JavaScript

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)
}
}