UNPKG

orval

Version:

A swagger client generator for typescript

1,170 lines (1,159 loc) 70.2 kB
import path from "node:path"; import { FormDataArrayHandling, GetterPropType, NamingConvention, OutputClient, OutputHttpClient, OutputMode, PropertySortOrder, RefComponentSuffix, asyncReduce, conventionName, createLogger, createSuccessMessage, dynamicImport, fixCrossDirectoryImports, fixRegularSchemaImports, generateComponentDefinition, generateDependencyImports, generateParameterDefinition, generateSchemasDefinition, generateVerbsOptions, getFileInfo, getFullRoute, getMockFileExtensionByTypeName, getRoute, isBoolean, isFunction, isNullish, isObject, isReference, isString, isUrl, jsDoc, log, logError, logVerbose, pascal, removeFilesAndEmptyFolders, resolveInstalledVersions, resolveRef, splitSchemasByType, upath, writeSchemas, writeSingleMode, writeSplitMode, writeSplitTagsMode, writeTagsMode } from "@orval/core"; import { bundle } from "@scalar/json-magic/bundle"; import { fetchUrls, parseJson, parseYaml, readFiles } from "@scalar/json-magic/bundle/plugins/node"; import { upgrade, validate } from "@scalar/openapi-parser"; import { isNullish as isNullish$1, unique } from "remeda"; import * as mock from "@orval/mock"; import { DEFAULT_MOCK_OPTIONS, generateMockImports } from "@orval/mock"; import angular from "@orval/angular"; import axios from "@orval/axios"; import fetchClient from "@orval/fetch"; import hono from "@orval/hono"; import mcp from "@orval/mcp"; import query from "@orval/query"; import solidStart from "@orval/solid-start"; import swr from "@orval/swr"; import zod, { dereference, generateZodValidationSchemaDefinition, isZodVersionV4, parseZodValidationSchemaDefinition } from "@orval/zod"; import { styleText } from "node:util"; import { ExecaError, execa } from "execa"; import fs from "fs-extra"; import fs$1, { access } from "node:fs/promises"; import { parseArgsStringToArgv } from "string-argv"; import { findUp, findUpMultiple } from "find-up"; import yaml from "js-yaml"; import { parse } from "tsconfck"; import fs$2 from "node:fs"; import { createJiti } from "jiti"; //#region package.json var name = "orval"; var description = "A swagger client generator for typescript"; var version = "8.6.2"; //#endregion //#region src/client.ts const DEFAULT_CLIENT = OutputClient.AXIOS; const getGeneratorClient = (outputClient, output) => { const angularBuilder = angular(); const GENERATOR_CLIENT = { axios: axios({ type: "axios" })(), "axios-functions": axios({ type: "axios-functions" })(), angular: angularBuilder(output.override.angular), "angular-query": query({ output, type: "angular-query" })(), "react-query": query({ output, type: "react-query" })(), "solid-start": solidStart()(), "solid-query": query({ output, type: "solid-query" })(), "svelte-query": query({ output, type: "svelte-query" })(), "vue-query": query({ output, type: "vue-query" })(), swr: swr()(), zod: zod()(), hono: hono()(), fetch: fetchClient()(), mcp: mcp()() }; const generator = isFunction(outputClient) ? outputClient(GENERATOR_CLIENT) : GENERATOR_CLIENT[outputClient]; if (!generator) throw new Error(`Unknown output client provided to getGeneratorClient: ${String(outputClient)}`); return generator; }; const generateClientImports = ({ client, implementation, imports, projectName, hasSchemaDir, isAllowSyntheticDefaultImports, hasGlobalMutator, hasTagsMutator, hasParamsSerializerOptions, packageJson, output }) => { const { dependencies } = getGeneratorClient(client, output); return generateDependencyImports(implementation, dependencies ? [...dependencies(hasGlobalMutator, hasParamsSerializerOptions, packageJson, output.httpClient, hasTagsMutator, output.override), ...imports] : imports, projectName, hasSchemaDir, isAllowSyntheticDefaultImports); }; const generateClientHeader = ({ outputClient = DEFAULT_CLIENT, isRequestOptions, isGlobalMutator, isMutator, provideIn, hasAwaitedType, titles, output, verbOptions, tag, clientImplementation }) => { const { header } = getGeneratorClient(outputClient, output); return { implementation: header ? header({ title: titles.implementation, isRequestOptions, isGlobalMutator, isMutator, provideIn, hasAwaitedType, output, verbOptions, tag, clientImplementation }) : "", implementationMock: `export const ${titles.implementationMock} = () => [\n` }; }; const generateClientFooter = ({ outputClient, operationNames, hasMutator, hasAwaitedType, titles, output }) => { const { footer } = getGeneratorClient(outputClient, output); if (!footer) return { implementation: "", implementationMock: `\n]\n` }; let implementation; try { if (isFunction(outputClient)) { implementation = footer(operationNames); console.warn("[WARN] Passing an array of strings for operations names to the footer function is deprecated and will be removed in a future major release. Please pass them in an object instead: { operationNames: string[] }."); } else implementation = footer({ operationNames, title: titles.implementation, hasMutator, hasAwaitedType }); } catch { implementation = footer({ operationNames, title: titles.implementation, hasMutator, hasAwaitedType }); } return { implementation, implementationMock: `]\n` }; }; const generateClientTitle = ({ outputClient = DEFAULT_CLIENT, title, customTitleFunc, output }) => { const { title: generatorTitle } = getGeneratorClient(outputClient, output); if (!generatorTitle) return { implementation: "", implementationMock: `get${pascal(title)}Mock` }; if (customTitleFunc) { const customTitle = customTitleFunc(title); return { implementation: generatorTitle(customTitle), implementationMock: `get${pascal(customTitle)}Mock` }; } return { implementation: generatorTitle(title), implementationMock: `get${pascal(title)}Mock` }; }; const generateMock = (verbOption, options) => { if (!options.mock) return { implementation: { function: "", handler: "", handlerName: "" }, imports: [] }; if (isFunction(options.mock)) return options.mock(verbOption, options); return mock.generateMock(verbOption, options); }; const generateOperations = (outputClient = DEFAULT_CLIENT, verbsOptions, options, output) => { return asyncReduce(verbsOptions, async (acc, verbOption) => { const { client: generatorClient } = getGeneratorClient(outputClient, output); const client = await generatorClient(verbOption, options, outputClient, output); if (!client.implementation) return acc; const generatedMock = generateMock(verbOption, options); const hasImplementation = client.implementation.trim().length > 0; const preferredOperationKey = verbOption.operationName; const baseOperationKey = verbOption.operationId ? `${verbOption.operationId}::${verbOption.operationName}` : verbOption.operationName; let operationKey = Object.hasOwn(acc, preferredOperationKey) ? baseOperationKey : preferredOperationKey; let collisionIndex = 1; while (Object.hasOwn(acc, operationKey)) { collisionIndex += 1; operationKey = `${baseOperationKey}::${collisionIndex}`; } acc[operationKey] = { implementation: hasImplementation ? verbOption.doc + client.implementation : client.implementation, imports: client.imports, implementationMock: generatedMock.implementation, importsMock: generatedMock.imports, tags: verbOption.tags, mutator: verbOption.mutator, clientMutators: client.mutators, formData: verbOption.formData, formUrlEncoded: verbOption.formUrlEncoded, paramsSerializer: verbOption.paramsSerializer, operationName: verbOption.operationName, fetchReviver: verbOption.fetchReviver }; return acc; }, {}); }; const generateExtraFiles = (outputClient = DEFAULT_CLIENT, verbsOptions, output, context) => { const { extraFiles: generateExtraFiles } = getGeneratorClient(outputClient, output); if (!generateExtraFiles) return Promise.resolve([]); return generateExtraFiles(verbsOptions, output, context); }; //#endregion //#region src/api.ts async function getApiBuilder({ input, output, context }) { const api = await asyncReduce(Object.entries(context.spec.paths ?? {}), async (acc, [pathRoute, verbs]) => { if (!verbs) return acc; const route = getRoute(pathRoute); let resolvedVerbs = verbs; if (isReference(verbs)) { const { schema } = resolveRef(verbs, context); resolvedVerbs = schema; } let verbsOptions = await generateVerbsOptions({ verbs: resolvedVerbs, input, output, route, pathRoute, context }); if (output.override.useDeprecatedOperations === false) verbsOptions = verbsOptions.filter((verb) => { return !verb.deprecated; }); const schemas = []; for (const { queryParams, headers, body, response, props } of verbsOptions) { schemas.push(...props.flatMap((param) => param.type === GetterPropType.NAMED_PATH_PARAMS ? param.schema : [])); if (queryParams) schemas.push(queryParams.schema, ...queryParams.deps); if (headers) schemas.push(headers.schema, ...headers.deps); schemas.push(...body.schemas, ...response.schemas); } const fullRoute = getFullRoute(route, resolvedVerbs.servers ?? context.spec.servers, output.baseUrl); if (!output.target) throw new Error("Output does not have a target"); const pathOperations = await generateOperations(output.client, verbsOptions, { route: fullRoute, pathRoute, override: output.override, context, mock: output.mock, output: output.target }, output); for (const verbOption of verbsOptions) acc.verbOptions[verbOption.operationId] = verbOption; acc.schemas.push(...schemas); acc.operations = { ...acc.operations, ...pathOperations }; return acc; }, { operations: {}, verbOptions: {}, schemas: [] }); const extraFiles = await generateExtraFiles(output.client, api.verbOptions, output, context); return { operations: api.operations, schemas: api.schemas, verbOptions: api.verbOptions, title: generateClientTitle, header: generateClientHeader, footer: generateClientFooter, imports: generateClientImports, importsMock: generateMockImports, extraFiles }; } //#endregion //#region src/import-open-api.ts async function importOpenApi({ spec, input, output, target, workspace, projectName }) { const transformedOpenApi = await applyTransformer(spec, input.override.transformer, workspace); const schemas = getApiSchemas({ input, output, target, workspace, spec: transformedOpenApi }); const api = await getApiBuilder({ input, output, context: { projectName, target, workspace, spec: transformedOpenApi, output } }); return { ...api, schemas: [...schemas, ...api.schemas], target, info: transformedOpenApi.info, spec: transformedOpenApi }; } async function applyTransformer(openApi, transformer, workspace) { const transformerFn = transformer ? await dynamicImport(transformer, workspace) : void 0; if (!transformerFn) return openApi; const transformedOpenApi = transformerFn(openApi); const { valid, errors } = await validate(transformedOpenApi); if (!valid) throw new Error(`Validation failed`, { cause: errors }); return transformedOpenApi; } function getApiSchemas({ input, output, target, workspace, spec }) { const context = { target, workspace, spec, output }; const schemaDefinition = generateSchemasDefinition(spec.components?.schemas, context, output.override.components.schemas.suffix, input.filters); const responseDefinition = generateComponentDefinition(spec.components?.responses, context, output.override.components.responses.suffix); const swaggerResponseDefinition = generateComponentDefinition("responses" in spec ? spec.responses : void 0, context, ""); const bodyDefinition = generateComponentDefinition(spec.components?.requestBodies, context, output.override.components.requestBodies.suffix); const parameters = generateParameterDefinition(spec.components?.parameters, context, output.override.components.parameters.suffix); return [ ...schemaDefinition, ...responseDefinition, ...swaggerResponseDefinition, ...bodyDefinition, ...parameters ]; } //#endregion //#region src/import-specs.ts async function resolveSpec(input, parserOptions) { const dereferencedData = dereferenceExternalRef(await bundle(input, { plugins: [ readFiles(), fetchUrls({ headers: parserOptions?.headers }), parseJson(), parseYaml() ], treeShake: false })); validateComponentKeys(dereferencedData); const { valid, errors } = await validate(dereferencedData); if (!valid) throw new Error("Validation failed", { cause: errors }); const { specification } = upgrade(dereferencedData); return specification; } async function importSpecs(workspace, options, projectName) { const { input, output } = options; return importOpenApi({ spec: await resolveSpec(input.target, input.parserOptions), input, output, target: isString(input.target) ? input.target : workspace, workspace, projectName }); } const COMPONENT_KEY_PATTERN = /^[a-zA-Z0-9.\-_]+$/; const COMPONENT_SECTIONS = [ "schemas", "responses", "parameters", "examples", "requestBodies", "headers", "securitySchemes", "links", "callbacks", "pathItems" ]; /** * Validate that all component keys conform to the OAS regex: ^[a-zA-Z0-9.\-_]+$ * @see https://spec.openapis.org/oas/v3.0.3.html#fixed-fields-5 * @see https://spec.openapis.org/oas/v3.1.0#fixed-fields-5 */ function validateComponentKeys(data) { const components = data.components; if (!isObject(components)) return; const invalidKeys = []; for (const section of COMPONENT_SECTIONS) { const sectionObj = components[section]; if (!isObject(sectionObj)) continue; for (const key of Object.keys(sectionObj)) if (!COMPONENT_KEY_PATTERN.test(key)) invalidKeys.push(`components.${section}.${key}`); } if (invalidKeys.length > 0) throw new Error(`Invalid component key${invalidKeys.length > 1 ? "s" : ""} found. OpenAPI component keys must match the pattern ${COMPONENT_KEY_PATTERN} (non-ASCII characters are not allowed per the spec).\n See: https://spec.openapis.org/oas/v3.0.3.html#components-object\n Invalid keys:\n` + invalidKeys.map((k) => ` - ${k}`).join("\n")); } /** * The plugins from `@scalar/json-magic` does not dereference $ref. * Instead it fetches them and puts them under x-ext, and changes the $ref to point to #x-ext/<name>. * This function: * 1. Merges external schemas into main spec's components.schemas (with collision handling) * 2. Replaces x-ext refs with standard component refs or inlined content */ function dereferenceExternalRef(data) { const extensions = data["x-ext"] ?? {}; const schemaNameMappings = mergeExternalSchemas(data, extensions); const result = {}; for (const [key, value] of Object.entries(data)) if (key !== "x-ext") result[key] = replaceXExtRefs(value, extensions, schemaNameMappings); return result; } /** * Merge external document schemas into main spec's components.schemas * Returns mapping of original schema names to final names (with suffixes for collisions) */ function mergeExternalSchemas(data, extensions) { const schemaNameMappings = {}; if (Object.keys(extensions).length === 0) return schemaNameMappings; data.components ??= {}; const mainComponents = data.components; mainComponents.schemas ??= {}; const mainSchemas = mainComponents.schemas; for (const [extKey, extDoc] of Object.entries(extensions)) { schemaNameMappings[extKey] = {}; if (isObject(extDoc) && "components" in extDoc) { const extComponents = extDoc.components; if (isObject(extComponents) && "schemas" in extComponents) { const extSchemas = extComponents.schemas; for (const [schemaName, schema] of Object.entries(extSchemas)) { const existingSchema = mainSchemas[schemaName]; const isXExtRef = isObject(existingSchema) && "$ref" in existingSchema && isString(existingSchema.$ref) && existingSchema.$ref.startsWith("#/x-ext/"); let finalSchemaName = schemaName; if (schemaName in mainSchemas && !isXExtRef) { finalSchemaName = `${schemaName}_${extKey.replaceAll(/[^a-zA-Z0-9]/g, "_")}`; schemaNameMappings[extKey][schemaName] = finalSchemaName; } else schemaNameMappings[extKey][schemaName] = schemaName; mainSchemas[finalSchemaName] = scrubUnwantedKeys(schema); } } } } for (const [extKey, mapping] of Object.entries(schemaNameMappings)) for (const [, finalName] of Object.entries(mapping)) { const schema = mainSchemas[finalName]; if (schema) mainSchemas[finalName] = updateInternalRefs(schema, extKey, schemaNameMappings); } return schemaNameMappings; } /** * Remove unwanted keys like $schema and $id from objects */ function scrubUnwantedKeys(obj) { const UNWANTED_KEYS = new Set(["$schema", "$id"]); if (obj === null || obj === void 0) return obj; if (Array.isArray(obj)) return obj.map((x) => scrubUnwantedKeys(x)); if (isObject(obj)) { const rec = obj; const out = {}; for (const [k, v] of Object.entries(rec)) { if (UNWANTED_KEYS.has(k)) continue; out[k] = scrubUnwantedKeys(v); } return out; } return obj; } /** * Update internal refs within an external schema to use suffixed names */ function updateInternalRefs(obj, extKey, schemaNameMappings) { if (obj === null || obj === void 0) return obj; if (Array.isArray(obj)) return obj.map((element) => updateInternalRefs(element, extKey, schemaNameMappings)); if (isObject(obj)) { const record = obj; if ("$ref" in record && isString(record.$ref)) { const refValue = record.$ref; if (refValue.startsWith("#/components/schemas/")) { const schemaName = refValue.replace("#/components/schemas/", ""); const mappedName = schemaNameMappings[extKey][schemaName]; if (mappedName) return { $ref: `#/components/schemas/${mappedName}` }; } } const result = {}; for (const [key, value] of Object.entries(record)) result[key] = updateInternalRefs(value, extKey, schemaNameMappings); return result; } return obj; } /** * Replace x-ext refs with either standard component refs or inlined content */ function replaceXExtRefs(obj, extensions, schemaNameMappings) { if (isNullish$1(obj)) return obj; if (Array.isArray(obj)) return obj.map((element) => replaceXExtRefs(element, extensions, schemaNameMappings)); if (isObject(obj)) { const record = obj; if ("$ref" in record && isString(record.$ref)) { const refValue = record.$ref; if (refValue.startsWith("#/x-ext/")) { const parts = refValue.replace("#/x-ext/", "").split("/"); const extKey = parts.shift(); if (extKey) { if (parts.length >= 3 && parts[0] === "components" && parts[1] === "schemas") { const schemaName = parts.slice(2).join("/"); return { $ref: `#/components/schemas/${schemaNameMappings[extKey][schemaName] || schemaName}` }; } let refObj = extensions[extKey]; for (const p of parts) if (refObj && (isObject(refObj) || Array.isArray(refObj)) && p in refObj) refObj = refObj[p]; else { refObj = void 0; break; } if (refObj) return replaceXExtRefs(scrubUnwantedKeys(refObj), extensions, schemaNameMappings); } } } const result = {}; for (const [key, value] of Object.entries(record)) result[key] = replaceXExtRefs(value, extensions, schemaNameMappings); return result; } return obj; } //#endregion //#region src/formatters/prettier.ts /** * Format files with prettier. * Tries the programmatic API first (project dependency), * then falls back to the globally installed CLI. */ async function formatWithPrettier(paths, projectTitle) { const prettier = await tryImportPrettier(); if (prettier) { const filePaths = [...new Set(await collectFilePaths(paths))]; if (filePaths.length === 0) return; const config = await prettier.resolveConfig(filePaths[0]) ?? {}; await Promise.all(filePaths.map(async (filePath) => { try { const content = await fs$1.readFile(filePath, "utf8"); const formatted = await prettier.format(content, { ...config, filepath: filePath }); await fs$1.writeFile(filePath, formatted); } catch (error) { if (isMissingFileError(error)) return; if (error instanceof Error) if (error.name === "UndefinedParserError") {} else log(styleText("yellow", `⚠️ ${projectTitle ? `${projectTitle} - ` : ""}Failed to format file ${filePath}: ${error.toString()}`)); else log(styleText("yellow", `⚠️ ${projectTitle ? `${projectTitle} - ` : ""}Failed to format file ${filePath}: unknown error}`)); } })); return; } try { await execa("prettier", ["--write", ...paths]); } catch { log(styleText("yellow", `⚠️ ${projectTitle ? `${projectTitle} - ` : ""}prettier not found. Install it as a project dependency or globally.`)); } } function isMissingFileError(error) { return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; } /** * Try to import prettier from the project's dependencies. * Returns undefined if prettier is not installed. */ async function tryImportPrettier() { try { return await import("prettier"); } catch { return; } } /** * Recursively collect absolute file paths from a mix of files and directories. */ async function collectFilePaths(paths) { const results = []; for (const p of paths) { const absolute = path.resolve(p); try { const stat = await fs$1.stat(absolute); if (stat.isFile()) results.push(absolute); else if (stat.isDirectory()) { const subFiles = await collectFilePaths((await fs$1.readdir(absolute)).map((entry) => path.join(absolute, entry))); results.push(...subFiles); } } catch {} } return results; } //#endregion //#region src/utils/execute-hook.ts const executeHook = async (name, commands = [], args = []) => { log(styleText("white", `Running ${name} hook...`)); for (const command of commands) try { if (isString(command)) await executeCommand(command, args); else if (isFunction(command)) await command(args); else if (isObject(command)) await executeObjectCommand(command, args); } catch (error) { logError(error, `Failed to run ${name} hook`); } }; async function executeCommand(command, args) { const [cmd, ..._args] = [...parseArgsStringToArgv(command), ...args]; await execa(cmd, _args); } async function executeObjectCommand(command, args) { if (command.injectGeneratedDirsAndFiles === false) args = []; if (isString(command.command)) await executeCommand(command.command, args); else if (isFunction(command.command)) await command.command(); } //#endregion //#region src/utils/package-json.ts const loadPackageJson = async (packageJson, workspace = process.cwd()) => { if (!packageJson) { const pkgPath = await findUp(["package.json"], { cwd: workspace }); if (pkgPath) { const pkg = await dynamicImport(pkgPath, workspace); if (isPackageJson(pkg)) return resolveAndAttachVersions(await maybeReplaceCatalog(pkg, workspace), workspace, pkgPath); else throw new Error("Invalid package.json file"); } return; } const normalizedPath = normalizePath(packageJson, workspace); if (fs.existsSync(normalizedPath)) { const pkg = await dynamicImport(normalizedPath); if (isPackageJson(pkg)) return resolveAndAttachVersions(await maybeReplaceCatalog(pkg, workspace), workspace, normalizedPath); else throw new Error(`Invalid package.json file: ${normalizedPath}`); } }; const isPackageJson = (obj) => isObject(obj); const resolvedCache = /* @__PURE__ */ new Map(); const resolveAndAttachVersions = (pkg, workspace, cacheKey) => { const cached = resolvedCache.get(cacheKey); if (cached) { pkg.resolvedVersions = cached; return pkg; } const resolved = resolveInstalledVersions(pkg, workspace); if (Object.keys(resolved).length > 0) { pkg.resolvedVersions = resolved; resolvedCache.set(cacheKey, resolved); for (const [name, version] of Object.entries(resolved)) logVerbose(styleText("dim", `Detected ${styleText("white", name)} v${styleText("white", version)}`)); } return pkg; }; const hasCatalogReferences = (pkg) => { return [ ...Object.entries(pkg.dependencies ?? {}), ...Object.entries(pkg.devDependencies ?? {}), ...Object.entries(pkg.peerDependencies ?? {}) ].some(([, value]) => isString(value) && value.startsWith("catalog:")); }; const loadPnpmWorkspaceCatalog = async (workspace) => { const filePath = await findUp("pnpm-workspace.yaml", { cwd: workspace }); if (!filePath) return void 0; try { const file = await fs.readFile(filePath, "utf8"); const data = yaml.load(file); if (!data?.catalog && !data?.catalogs) return void 0; return { catalog: data.catalog, catalogs: data.catalogs }; } catch { return; } }; const loadPackageJsonCatalog = async (workspace) => { const filePaths = await findUpMultiple("package.json", { cwd: workspace }); for (const filePath of filePaths) try { const pkg = await fs.readJson(filePath); if (pkg.catalog || pkg.catalogs) return { catalog: pkg.catalog, catalogs: pkg.catalogs }; } catch {} }; const loadYarnrcCatalog = async (workspace) => { const filePath = await findUp(".yarnrc.yml", { cwd: workspace }); if (!filePath) return void 0; try { const file = await fs.readFile(filePath, "utf8"); const data = yaml.load(file); if (!data?.catalog && !data?.catalogs) return void 0; return { catalog: data.catalog, catalogs: data.catalogs }; } catch { return; } }; const maybeReplaceCatalog = async (pkg, workspace) => { if (!hasCatalogReferences(pkg)) return pkg; const catalogData = await loadPnpmWorkspaceCatalog(workspace) ?? await loadPackageJsonCatalog(workspace) ?? await loadYarnrcCatalog(workspace); if (!catalogData) { log(`⚠️ ${styleText("yellow", "package.json contains catalog: references, but no catalog source was found (checked: pnpm-workspace.yaml, package.json, .yarnrc.yml).")}`); return pkg; } performSubstitution(pkg.dependencies, catalogData); performSubstitution(pkg.devDependencies, catalogData); performSubstitution(pkg.peerDependencies, catalogData); return pkg; }; const performSubstitution = (dependencies, catalogData) => { if (!dependencies) return; for (const [packageName, version] of Object.entries(dependencies)) if (version === "catalog:" || version === "catalog:default") { if (!catalogData.catalog) { log(`⚠️ ${styleText("yellow", `catalog: substitution for the package '${packageName}' failed as there is no default catalog.`)}`); continue; } const sub = catalogData.catalog[packageName]; if (!sub) { log(`⚠️ ${styleText("yellow", `catalog: substitution for the package '${packageName}' failed as there is no matching package in the default catalog.`)}`); continue; } dependencies[packageName] = sub; } else if (version.startsWith("catalog:")) { const catalogName = version.slice(8); const catalog = catalogData.catalogs?.[catalogName]; if (!catalog) { log(`⚠️ ${styleText("yellow", `'${version}' substitution for the package '${packageName}' failed as there is no matching catalog named '${catalogName}'. (available named catalogs are: ${Object.keys(catalogData.catalogs ?? {}).join(", ")})`)}`); continue; } const sub = catalog[packageName]; if (!sub) { log(`⚠️ ${styleText("yellow", `'${version}' substitution for the package '${packageName}' failed as there is no package in the catalog named '${catalogName}'. (packages in the catalog are: ${Object.keys(catalog).join(", ")})`)}`); continue; } dependencies[packageName] = sub; } }; //#endregion //#region src/utils/tsconfig.ts const loadTsconfig = async (tsconfig, workspace = process.cwd()) => { if (isNullish(tsconfig)) { const configPath = await findUp(["tsconfig.json", "jsconfig.json"], { cwd: workspace }); if (configPath) return (await parse(configPath)).tsconfig; return; } if (isString(tsconfig)) { const normalizedPath = normalizePath(tsconfig, workspace); if (fs.existsSync(normalizedPath)) { const config = await parse(normalizedPath); return config.referenced?.find(({ tsconfigFile }) => tsconfigFile === normalizedPath)?.tsconfig ?? config.tsconfig; } return; } if (isObject(tsconfig)) return tsconfig; }; //#endregion //#region src/utils/options.ts const INPUT_TARGET_FETCH_TIMEOUT_MS = 1e4; /** * Type helper to make it easier to use orval.config.ts * accepts a direct {@link ConfigExternal} object. */ function defineConfig(options) { return options; } /** * Type helper to make it easier to write input transformers. * accepts a direct {@link InputTransformerFn} function. */ function defineTransformer(transformer) { return transformer; } function createFormData(workspace, formData) { const defaultArrayHandling = FormDataArrayHandling.SERIALIZE; if (formData === void 0) return { disabled: false, arrayHandling: defaultArrayHandling }; if (isBoolean(formData)) return { disabled: !formData, arrayHandling: defaultArrayHandling }; if (isString(formData)) return { disabled: false, mutator: normalizeMutator(workspace, formData), arrayHandling: defaultArrayHandling }; if ("mutator" in formData || "arrayHandling" in formData) return { disabled: false, mutator: normalizeMutator(workspace, formData.mutator), arrayHandling: formData.arrayHandling ?? defaultArrayHandling }; return { disabled: false, mutator: normalizeMutator(workspace, formData), arrayHandling: defaultArrayHandling }; } function normalizeSchemasOption(schemas, workspace) { if (!schemas) return; if (isString(schemas)) return normalizePath(schemas, workspace); return { path: normalizePath(schemas.path, workspace), type: schemas.type }; } async function normalizeOptions(optionsExport, workspace = process.cwd(), globalOptions = {}) { const options = await (isFunction(optionsExport) ? optionsExport() : optionsExport); if (!options.input) throw new Error(styleText("red", `Config requires an input.`)); if (!options.output) throw new Error(styleText("red", `Config requires an output.`)); const inputOptions = isString(options.input) || Array.isArray(options.input) ? { target: options.input } : options.input; const outputOptions = isString(options.output) ? { target: options.output } : options.output; const outputWorkspace = normalizePath(outputOptions.workspace ?? "", workspace); const { clean, prettier, client, httpClient, mode, biome } = globalOptions; const tsconfig = await loadTsconfig(outputOptions.tsconfig ?? globalOptions.tsconfig, workspace); const packageJson = await loadPackageJson(outputOptions.packageJson ?? globalOptions.packageJson, workspace); const mockOption = outputOptions.mock ?? globalOptions.mock; let mock; if (isBoolean(mockOption) && mockOption) mock = DEFAULT_MOCK_OPTIONS; else if (isFunction(mockOption)) mock = mockOption; else if (mockOption) mock = { ...DEFAULT_MOCK_OPTIONS, ...mockOption }; else mock = void 0; const defaultFileExtension = ".ts"; const globalQueryOptions = { useQuery: true, useMutation: true, signal: true, shouldExportMutatorHooks: true, shouldExportHttpClient: true, shouldExportQueryKey: true, shouldSplitQueryKey: false, ...normalizeQueryOptions(outputOptions.override?.query, workspace) }; const normalizedOptions = { input: { target: globalOptions.input ? Array.isArray(globalOptions.input) ? await resolveFirstValidTarget(globalOptions.input, process.cwd(), inputOptions.parserOptions) : normalizePathOrUrl(globalOptions.input, process.cwd()) : Array.isArray(inputOptions.target) ? await resolveFirstValidTarget(inputOptions.target, workspace, inputOptions.parserOptions) : normalizePathOrUrl(inputOptions.target, workspace), override: { transformer: normalizePath(inputOptions.override?.transformer, workspace) }, filters: inputOptions.filters, parserOptions: inputOptions.parserOptions }, output: { target: globalOptions.output ? normalizePath(globalOptions.output, process.cwd()) : normalizePath(outputOptions.target, outputWorkspace), schemas: normalizeSchemasOption(outputOptions.schemas, outputWorkspace), operationSchemas: outputOptions.operationSchemas ? normalizePath(outputOptions.operationSchemas, outputWorkspace) : void 0, namingConvention: outputOptions.namingConvention ?? NamingConvention.CAMEL_CASE, fileExtension: outputOptions.fileExtension ?? defaultFileExtension, workspace: outputOptions.workspace ? outputWorkspace : void 0, client: outputOptions.client ?? client ?? OutputClient.AXIOS_FUNCTIONS, httpClient: outputOptions.httpClient ?? httpClient ?? ((outputOptions.client ?? client) === OutputClient.ANGULAR_QUERY ? OutputHttpClient.ANGULAR : OutputHttpClient.FETCH), mode: normalizeOutputMode(outputOptions.mode ?? mode), mock, clean: outputOptions.clean ?? clean ?? false, docs: outputOptions.docs ?? false, prettier: outputOptions.prettier ?? prettier ?? false, biome: outputOptions.biome ?? biome ?? false, tsconfig, packageJson, headers: outputOptions.headers ?? false, indexFiles: outputOptions.indexFiles ?? true, baseUrl: outputOptions.baseUrl, unionAddMissingProperties: outputOptions.unionAddMissingProperties ?? false, override: { ...outputOptions.override, mock: { arrayMin: outputOptions.override?.mock?.arrayMin ?? 1, arrayMax: outputOptions.override?.mock?.arrayMax ?? 10, stringMin: outputOptions.override?.mock?.stringMin ?? 10, stringMax: outputOptions.override?.mock?.stringMax ?? 20, fractionDigits: outputOptions.override?.mock?.fractionDigits ?? 2, ...outputOptions.override?.mock }, operations: normalizeOperationsAndTags(outputOptions.override?.operations ?? {}, outputWorkspace, { query: globalQueryOptions }), tags: normalizeOperationsAndTags(outputOptions.override?.tags ?? {}, outputWorkspace, { query: globalQueryOptions }), mutator: normalizeMutator(outputWorkspace, outputOptions.override?.mutator), formData: createFormData(outputWorkspace, outputOptions.override?.formData), formUrlEncoded: (isBoolean(outputOptions.override?.formUrlEncoded) ? outputOptions.override.formUrlEncoded : normalizeMutator(outputWorkspace, outputOptions.override?.formUrlEncoded)) ?? true, paramsSerializer: normalizeMutator(outputWorkspace, outputOptions.override?.paramsSerializer), header: outputOptions.override?.header === false ? false : isFunction(outputOptions.override?.header) ? outputOptions.override.header : getDefaultFilesHeader, requestOptions: outputOptions.override?.requestOptions ?? true, namingConvention: outputOptions.override?.namingConvention ?? {}, components: { schemas: { suffix: RefComponentSuffix.schemas, itemSuffix: outputOptions.override?.components?.schemas?.itemSuffix ?? "Item", ...outputOptions.override?.components?.schemas }, responses: { suffix: RefComponentSuffix.responses, ...outputOptions.override?.components?.responses }, parameters: { suffix: RefComponentSuffix.parameters, ...outputOptions.override?.components?.parameters }, requestBodies: { suffix: RefComponentSuffix.requestBodies, ...outputOptions.override?.components?.requestBodies } }, hono: normalizeHonoOptions(outputOptions.override?.hono, workspace), jsDoc: normalizeJSDocOptions(outputOptions.override?.jsDoc), query: globalQueryOptions, zod: { strict: { param: outputOptions.override?.zod?.strict?.param ?? false, query: outputOptions.override?.zod?.strict?.query ?? false, header: outputOptions.override?.zod?.strict?.header ?? false, body: outputOptions.override?.zod?.strict?.body ?? false, response: outputOptions.override?.zod?.strict?.response ?? false }, generate: { param: outputOptions.override?.zod?.generate?.param ?? true, query: outputOptions.override?.zod?.generate?.query ?? true, header: outputOptions.override?.zod?.generate?.header ?? true, body: outputOptions.override?.zod?.generate?.body ?? true, response: outputOptions.override?.zod?.generate?.response ?? true }, coerce: { param: outputOptions.override?.zod?.coerce?.param ?? false, query: outputOptions.override?.zod?.coerce?.query ?? false, header: outputOptions.override?.zod?.coerce?.header ?? false, body: outputOptions.override?.zod?.coerce?.body ?? false, response: outputOptions.override?.zod?.coerce?.response ?? false }, preprocess: { ...outputOptions.override?.zod?.preprocess?.param ? { param: normalizeMutator(workspace, outputOptions.override.zod.preprocess.param) } : {}, ...outputOptions.override?.zod?.preprocess?.query ? { query: normalizeMutator(workspace, outputOptions.override.zod.preprocess.query) } : {}, ...outputOptions.override?.zod?.preprocess?.header ? { header: normalizeMutator(workspace, outputOptions.override.zod.preprocess.header) } : {}, ...outputOptions.override?.zod?.preprocess?.body ? { body: normalizeMutator(workspace, outputOptions.override.zod.preprocess.body) } : {}, ...outputOptions.override?.zod?.preprocess?.response ? { response: normalizeMutator(workspace, outputOptions.override.zod.preprocess.response) } : {} }, generateEachHttpStatus: outputOptions.override?.zod?.generateEachHttpStatus ?? false, dateTimeOptions: outputOptions.override?.zod?.dateTimeOptions ?? {}, timeOptions: outputOptions.override?.zod?.timeOptions ?? {} }, swr: { generateErrorTypes: false, ...outputOptions.override?.swr }, angular: { provideIn: outputOptions.override?.angular?.provideIn ?? "root", client: outputOptions.override?.angular?.retrievalClient ?? outputOptions.override?.angular?.client ?? "httpClient", runtimeValidation: outputOptions.override?.angular?.runtimeValidation ?? false, ...outputOptions.override?.angular?.httpResource ? { httpResource: outputOptions.override.angular.httpResource } : {} }, fetch: { includeHttpResponseReturnType: outputOptions.override?.fetch?.includeHttpResponseReturnType ?? true, forceSuccessResponse: outputOptions.override?.fetch?.forceSuccessResponse ?? false, runtimeValidation: outputOptions.override?.fetch?.runtimeValidation ?? false, ...outputOptions.override?.fetch, ...outputOptions.override?.fetch?.jsonReviver ? { jsonReviver: normalizeMutator(outputWorkspace, outputOptions.override.fetch.jsonReviver) } : {} }, useDates: outputOptions.override?.useDates ?? false, useDeprecatedOperations: outputOptions.override?.useDeprecatedOperations ?? true, enumGenerationType: outputOptions.override?.enumGenerationType ?? "const", suppressReadonlyModifier: outputOptions.override?.suppressReadonlyModifier ?? false, preserveReadonlyRequestBodies: outputOptions.override?.preserveReadonlyRequestBodies ?? "strip", aliasCombinedTypes: outputOptions.override?.aliasCombinedTypes ?? false }, allParamsOptional: outputOptions.allParamsOptional ?? false, urlEncodeParameters: outputOptions.urlEncodeParameters ?? false, optionsParamRequired: outputOptions.optionsParamRequired ?? false, propertySortOrder: outputOptions.propertySortOrder ?? PropertySortOrder.SPECIFICATION }, hooks: options.hooks ? normalizeHooks(options.hooks) : {} }; if (!normalizedOptions.input.target) throw new Error(styleText("red", `Config requires an input target.`)); if (!normalizedOptions.output.target && !normalizedOptions.output.schemas) throw new Error(styleText("red", `Config requires an output target or schemas.`)); return normalizedOptions; } function normalizeMutator(workspace, mutator) { if (isObject(mutator)) { const m = mutator; if (!m.path) throw new Error(styleText("red", `Mutator requires a path.`)); return { path: path.resolve(workspace, m.path), name: m.name, default: m.default ?? !m.name, alias: m.alias, external: m.external, extension: m.extension }; } if (isString(mutator)) return { path: path.resolve(workspace, mutator), default: true }; } async function resolveFirstValidTarget(targets, workspace, parserOptions) { for (const target of targets) { if (isUrl(target)) { try { const headers = getHeadersForUrl(target, parserOptions?.headers); const headResponse = await fetchWithTimeout(target, { method: "HEAD", headers }); if (headResponse.ok) return target; if (headResponse.status === 405 || headResponse.status === 501) { if ((await fetchWithTimeout(target, { method: "GET", headers })).ok) return target; } } catch { continue; } continue; } const resolvedTarget = normalizePath(target, workspace); try { await access(resolvedTarget); return resolvedTarget; } catch { continue; } } throw new Error(styleText("red", `None of the input targets could be resolved:\n${targets.map((target) => ` - ${target}`).join("\n")}`)); } function getHeadersForUrl(url, headersConfig) { if (!headersConfig) return {}; const { hostname } = new URL(url); const matchedHeaders = {}; for (const headerEntry of headersConfig) if (headerEntry.domains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`))) Object.assign(matchedHeaders, headerEntry.headers); return matchedHeaders; } async function fetchWithTimeout(target, init) { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, INPUT_TARGET_FETCH_TIMEOUT_MS); try { return await fetch(target, { ...init, signal: controller.signal }); } finally { clearTimeout(timeoutId); } } function normalizePathOrUrl(path, workspace) { if (isString(path) && !isUrl(path)) return normalizePath(path, workspace); return path; } function normalizePath(path$1, workspace) { if (!isString(path$1)) return path$1; return path.resolve(workspace, path$1); } function normalizeOperationsAndTags(operationsOrTags, workspace, global) { return Object.fromEntries(Object.entries(operationsOrTags).map(([key, { transformer, mutator, formData, formUrlEncoded, paramsSerializer, query, angular, zod, ...rest }]) => { return [key, { ...rest, ...angular ? { angular: { provideIn: angular.provideIn ?? "root", client: angular.retrievalClient ?? angular.client ?? "httpClient", runtimeValidation: angular.runtimeValidation ?? false, ...angular.httpResource ? { httpResource: angular.httpResource } : {} } } : {}, ...query ? { query: normalizeQueryOptions(query, workspace, global.query) } : {}, ...zod ? { zod: { strict: { param: zod.strict?.param ?? false, query: zod.strict?.query ?? false, header: zod.strict?.header ?? false, body: zod.strict?.body ?? false, response: zod.strict?.response ?? false }, generate: { param: zod.generate?.param ?? true, query: zod.generate?.query ?? true, header: zod.generate?.header ?? true, body: zod.generate?.body ?? true, response: zod.generate?.response ?? true }, coerce: { param: zod.coerce?.param ?? false, query: zod.coerce?.query ?? false, header: zod.coerce?.header ?? false, body: zod.coerce?.body ?? false, response: zod.coerce?.response ?? false }, preprocess: { ...zod.preprocess?.param ? { param: normalizeMutator(workspace, zod.preprocess.param) } : {}, ...zod.preprocess?.query ? { query: normalizeMutator(workspace, zod.preprocess.query) } : {}, ...zod.preprocess?.header ? { header: normalizeMutator(workspace, zod.preprocess.header) } : {}, ...zod.preprocess?.body ? { body: normalizeMutator(workspace, zod.preprocess.body) } : {}, ...zod.preprocess?.response ? { response: normalizeMutator(workspace, zod.preprocess.response) } : {} }, generateEachHttpStatus: zod.generateEachHttpStatus ?? false, dateTimeOptions: zod.dateTimeOptions ?? {}, timeOptions: zod.timeOptions ?? {} } } : {}, ...transformer ? { transformer: normalizePath(transformer, workspace) } : {}, ...mutator ? { mutator: normalizeMutator(workspace, mutator) } : {}, ...formData === void 0 ? {} : { formData: createFormData(workspace, formData) }, ...formUrlEncoded ? { formUrlEncoded: isBoolean(formUrlEncoded) ? formUrlEncoded : normalizeMutator(workspace, formUrlEncoded) } : {}, ...paramsSerializer ? { paramsSerializer: normalizeMutator(workspace, paramsSerializer) } : {} }]; })); } function normalizeOutputMode(mode) { if (!mode) return OutputMode.SINGLE; if (!Object.values(OutputMode).includes(mode)) { createLogger().warn(styleText("yellow", `Unknown provided mode => ${mode}`)); return OutputMode.SINGLE; } return mode; } function normalizeHooks(hooks) { const keys = Object.keys(hooks); const result = {}; for (const key of keys) if (isString(hooks[key])) result[key] = [hooks[key]]; else if (Array.isArray(hooks[key])) result[key] = hooks[key]; else if (isFunction(hooks[key])) result[key] = [hooks[key]]; else if (isObject(hooks[key])) result[key] = [hooks[key]]; return result; } function normalizeHonoOptions(hono = {}, workspace) { return { ...hono.handlers ? { handlers: path.resolve(workspace, hono.handlers) } : {}, compositeRoute: hono.compositeRoute ? path.resolve(workspace, hono.compositeRoute) : "", validator: hono.validator ?? true, validatorOutputPath: hono.validatorOutputPath ? path.resolve(workspace, hono.validatorOutputPath) : "" }; } function normalizeJSDocOptions(jsdoc = {}) { return { ...jsdoc }; } function normalizeQueryOptions(queryOptions = {}, outputWorkspace, globalOptions = {}) { if (queryOptions.options) console.warn("[WARN] Using query options is deprecated and will be removed in a future major release. Please use queryOptions or mutationOptions instead."); return { ...isNullish(queryOptions.usePrefetch) ? {} : { usePrefetch: queryOptions.usePrefetch }, ...isNullish(queryOptions.useInvalidate) ? {} : { useInvalidate: queryOptions.useInvalidate }, ...isNullish(queryOptions.useQuery) ? {} : { useQuery: queryOptions.useQuery }, ...isNullish(queryOptions.useSuspenseQuery) ? {} : { useSuspenseQuery: queryOptions.useSuspenseQuery }, ...isNullish(queryOptions.useMutation) ? {} : { useMutation: queryOptions.useMutation }, ...isNullish(queryOptions.useInfinite) ? {} : { useInfinite: queryOptions.useInfinite }, ...isNullish(queryOptions.useSuspenseInfiniteQuery) ? {} : { useSuspenseInfiniteQuery: queryOptions.useSuspenseInfiniteQuery }, ...queryOptions.useInfiniteQueryParam ? { useInfiniteQueryParam: queryOptions.useInfiniteQueryParam } : {}, ...queryOptions.options ? { options: queryOptions.options } : {}, ...globalOptions.queryKey ? { queryKey: globalOptions.queryKey } : {}, ...queryOptions.queryKey ? { queryKey: normalizeMutator(outputWorkspace, queryOptions.queryKey) } : {}, ...globalOptions.queryOptions ? { queryOptions: globalOptions.queryOptions } : {}, ...queryOptions.queryOptions ? { queryOptions: normalizeMutator(outputWorkspace, queryOptions.queryOptions) } : {}, ...globalOptions.mutationOptions ? { mutationOptions: globalOptions.mutationOptions } : {}, ...queryOptions.mutationOptions ? { mutationOptions: normalizeMutator(outputWorkspace, queryOptions.mutationOptions) } : {}, ...isNullish(globalOptions.shouldExportQueryKey) ? {} : { shouldExportQueryKey: globalOptions.shouldExportQueryKey }, ...isNullish(queryOptions.shouldExportQueryKey) ? {} : { shouldExportQueryKey: queryOptions.shouldExportQueryKey }, ...isNullish(globalOptions.shouldExportHttpClient) ? {} : { shouldExportHttpClient: globalOptions.shouldExportHttpClient }, ...isNullish(queryOptions.shouldExportHttpClient) ? {} : { shouldExportHttpClient: queryOptions.shouldExportHttpClient }, ...isNullish(globalOptions.shouldExportMutatorHooks) ? {} : { shouldExportMutatorHooks: globalOptions.shouldExportMutatorHooks }, ...isNullish(queryOptions.shouldExportMutatorHooks) ? {} : { shouldExportMutatorHooks: queryOptions.shouldExportMutatorHooks }, ...isNullish(globalOptions.shouldSplitQueryKey) ? {} : { shouldSplitQueryKey: globalOptions.shouldSplitQueryKey }, ...isNullish(queryOptions.shouldSplitQueryKey) ? {} : { shouldSplitQueryKey: queryOptions.shouldSplitQueryKey }, ...isNullish(globalOptions.signal) ? {} : { signal: globalOptions.signal }, ...isNullish(globalOptions.useOperationIdAsQueryKey) ? {} : { useOperationIdAsQueryKey: globalOptions.useOperationIdAsQueryKey }, ...isNullish(queryOptions.useOperationIdAsQueryKey) ? {} : { useOperationIdAsQueryKey: queryOptions.useOperationIdAsQueryKey }, ...isNullish(globalOptions.signal) ? {} : { signal: globalOptions.signal }, ...isNullish(queryOptions.signal) ? {} : { signal: queryOptions.signal }, ...isNullish(globalOptions.version) ? {} : { version: globalOptions.version }, ...isNullish(queryOptions.version) ? {} : { version: queryOptions.version }, ...queryOptions.mutationInvalidates ? { mutationInvalidates: queryOptions.mutationInvalidates } : {}, ...isNullish(globalOptions.runtimeValidation) ? {} : { runtimeValidation: globalOptions.runtimeValidation }, ...isNullish(queryOptions.runtimeValidation) ? {} : { runtimeValidation: queryOptions.runtimeValidation } }; } function getDefaultFilesHeader({ title, description, version: version$1 } = {}) { return [ `Generated by ${name} v${version} 🍺`, `Do not edit manually.`, ...title ? [title] : [], ...description ? [description] : [], ...version$1 ? [`OpenAPI spec version: ${version$1}`] : [] ]; } //#endregion //#region src/utils/watcher.ts /** * Start a file watcher and invoke an async callback on file changes. * * If `watchOptions` is falsy the watcher is not started. Supported shapes: * - boolean: when true the `defaultTarget` is watched * - string: a single path to watch * - string[]: an array of paths to watch * * @param watchOptions - false to disable watching, or a path/paths to watch * @param watchFn - async callback executed on change events * @param defaultTarget - path(s) to watch when `watchOptions` is `true` (default: '.') * @returns Resolves once the watcher has been started (or imme