161 lines
4.9 KiB
JavaScript
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)
|
|
}
|
|
}
|