orval
Version:
A swagger client generator for typescript
1 lines • 283 kB
Source Map (JSON)
{"version":3,"file":"config-18E5SVPo.mjs","names":["fs","yaml","fs","nodePath","path","pkg.name","pkg.version","version","validateSpec","nodePath","isNullish","fs","fs","fs","fs"],"sources":["../package.json","../src/client.ts","../src/api.ts","../src/import-open-api.ts","../src/utils/package-json.ts","../src/utils/tsconfig.ts","../src/utils/options.ts","../src/import-specs.ts","../src/formatters/prettier.ts","../src/utils/barrel.ts","../src/utils/execute-hook.ts","../src/utils/watcher.ts","../src/reusable-schemas.ts","../src/write-zod-specs.ts","../src/write-specs.ts","../src/generate-spec.ts","../src/utils/config.ts"],"sourcesContent":["","import angular from '@orval/angular';\nimport axios from '@orval/axios';\nimport type {\n AngularOptions,\n ClientFileBuilder,\n ClientGeneratorsBuilder,\n ClientMockGeneratorBuilder,\n ContextSpec,\n GeneratorClientFooter,\n GeneratorClientHeader,\n GeneratorClientImports,\n GeneratorClients,\n GeneratorClientTitle,\n GeneratorOperations,\n GeneratorOptions,\n GeneratorVerbOptions,\n GeneratorVerbsOptions,\n NormalizedOutputOptions,\n OutputClientFunc,\n} from '@orval/core';\nimport {\n asyncReduce,\n generateDependencyImports,\n getBaseUrlRuntimeImports,\n isFunction,\n logWarning,\n OutputClient,\n OutputMockType,\n pascal,\n} from '@orval/core';\nimport effect from '@orval/effect';\nimport fetchClient from '@orval/fetch';\nimport hono from '@orval/hono';\nimport mcp from '@orval/mcp';\nimport * as mock from '@orval/mock';\nimport query from '@orval/query';\nimport solidStart from '@orval/solid-start';\nimport swr from '@orval/swr';\nimport zod from '@orval/zod';\n\nconst DEFAULT_CLIENT = OutputClient.AXIOS;\n\nconst getGeneratorClient = (\n outputClient: OutputClient | OutputClientFunc,\n output: NormalizedOutputOptions,\n) => {\n const angularBuilder = angular() as (\n options?: AngularOptions,\n ) => ClientGeneratorsBuilder;\n const GENERATOR_CLIENT: GeneratorClients = {\n axios: axios({ type: 'axios' })(),\n 'axios-functions': axios({ type: 'axios-functions' })(),\n angular: angularBuilder(output.override.angular),\n 'angular-query': query({ output, type: 'angular-query' })(),\n 'react-query': query({ output, type: 'react-query' })(),\n 'solid-start': solidStart()(),\n 'solid-query': query({ output, type: 'solid-query' })(),\n 'svelte-query': query({ output, type: 'svelte-query' })(),\n 'vue-query': query({ output, type: 'vue-query' })(),\n swr: swr()(),\n zod: zod()(),\n effect: effect()(),\n hono: hono()(),\n fetch: fetchClient()(),\n mcp: mcp()(),\n };\n\n const generator = isFunction(outputClient)\n ? outputClient(GENERATOR_CLIENT)\n : GENERATOR_CLIENT[outputClient];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive guard for custom OutputClientFunc returning unexpected values\n if (!generator) {\n throw new Error(\n `Unknown output client provided to getGeneratorClient: ${String(outputClient)}`,\n );\n }\n\n return generator;\n};\n\nexport const generateClientImports: GeneratorClientImports = ({\n client,\n implementation,\n imports,\n projectName,\n hasSchemaDir,\n isAllowSyntheticDefaultImports,\n hasGlobalMutator,\n hasTagsMutator,\n hasParamsSerializerOptions,\n packageJson,\n output,\n}) => {\n const { dependencies } = getGeneratorClient(client, output);\n return generateDependencyImports(\n implementation,\n dependencies\n ? [\n ...dependencies(\n hasGlobalMutator,\n hasParamsSerializerOptions,\n packageJson,\n output.httpClient,\n hasTagsMutator,\n output.override,\n ),\n ...imports,\n ]\n : (imports as Parameters<typeof generateDependencyImports>[1]),\n projectName,\n hasSchemaDir,\n isAllowSyntheticDefaultImports,\n );\n};\n\nexport const generateClientHeader: GeneratorClientHeader = ({\n outputClient = DEFAULT_CLIENT,\n isRequestOptions,\n isGlobalMutator,\n isMutator,\n provideIn,\n hasAwaitedType,\n titles,\n output,\n verbOptions,\n tag,\n isDefaultTagBucket,\n clientImplementation,\n}) => {\n const { header } = getGeneratorClient(outputClient, output);\n\n const rawHeader = header\n ? header({\n title: titles.implementation,\n isRequestOptions,\n isGlobalMutator,\n isMutator,\n provideIn,\n hasAwaitedType,\n output,\n verbOptions,\n tag,\n isDefaultTagBucket,\n clientImplementation,\n })\n : '';\n\n const normalizedHeader =\n typeof rawHeader === 'string' ? { implementation: rawHeader } : rawHeader;\n\n return {\n implementation: normalizedHeader.implementation,\n implementationMock: `export const ${titles.implementationMock} = () => [\\n`,\n sharedTypes: normalizedHeader.sharedTypes,\n };\n};\n\nexport const generateClientFooter: GeneratorClientFooter = ({\n outputClient,\n operationNames,\n operations,\n hasMutator,\n hasAwaitedType,\n titles,\n output,\n}) => {\n const { footer } = getGeneratorClient(outputClient, output);\n\n if (!footer) {\n return {\n implementation: '',\n implementationMock: `\\n]\\n`,\n };\n }\n\n let implementation: string;\n try {\n if (isFunction(outputClient)) {\n implementation = (\n footer as unknown as (operationNames: string[]) => string\n )(operationNames);\n // being here means that the previous call worked\n logWarning(\n '⚠️ 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[] }.',\n );\n } else {\n implementation = footer({\n operationNames,\n operations,\n title: titles.implementation,\n hasMutator,\n hasAwaitedType,\n });\n }\n } catch {\n implementation = footer({\n operationNames,\n operations,\n title: titles.implementation,\n hasMutator,\n hasAwaitedType,\n });\n }\n\n return {\n implementation,\n implementationMock: `]\\n`,\n };\n};\n\nexport const generateClientTitle: GeneratorClientTitle = ({\n outputClient = DEFAULT_CLIENT,\n title,\n customTitleFunc,\n output,\n}) => {\n const { title: generatorTitle } = getGeneratorClient(outputClient, output);\n\n if (!generatorTitle) {\n return {\n implementation: '',\n implementationMock: `get${pascal(title)}Mock`,\n };\n }\n\n if (customTitleFunc) {\n const customTitle = customTitleFunc(title);\n return {\n implementation: generatorTitle(customTitle),\n implementationMock: `get${pascal(customTitle)}Mock`,\n };\n }\n return {\n implementation: generatorTitle(title),\n implementationMock: `get${pascal(title)}Mock`,\n };\n};\n\n/**\n * Invokes the underlying mock generator (msw, faker, or a user-provided\n * ClientMockBuilder) for a single generator entry. Returns the standard\n * `ClientMockGeneratorBuilder` shape (function/handler/handlerName +\n * imports) regardless of which generator handled it.\n */\nconst invokeMockGenerator = (\n verbOption: GeneratorVerbOptions,\n options: GeneratorOptions,\n entry: NonNullable<NormalizedOutputOptions['mock']['generators'][number]>,\n): ClientMockGeneratorBuilder => {\n if (isFunction(entry)) {\n return entry(verbOption, {\n ...options,\n mock: entry,\n });\n }\n return mock.generateMock(verbOption, {\n ...options,\n mock: entry,\n });\n};\n\nexport const generateOperations = (\n outputClient: OutputClient | OutputClientFunc = DEFAULT_CLIENT,\n verbsOptions: GeneratorVerbsOptions,\n options: GeneratorOptions,\n output: NormalizedOutputOptions,\n): Promise<GeneratorOperations> => {\n const baseUrlImports = getBaseUrlRuntimeImports(output.baseUrl);\n\n return asyncReduce(\n verbsOptions,\n async (acc, verbOption) => {\n const { client: generatorClient } = getGeneratorClient(\n outputClient,\n output,\n );\n const client = await generatorClient(\n verbOption,\n options,\n outputClient,\n output,\n );\n\n if (!client.implementation) {\n return acc;\n }\n\n // Run every configured mock generator for this operation. Each entry\n // contributes its own GeneratorMockOutputFull so writers can split the\n // results across per-type output files (e.g. `.msw.ts` + `.faker.ts`).\n // Function-form entries (ClientMockBuilder) inherit the historical\n // `msw` file extension and are treated as msw outputs for downstream\n // bookkeeping.\n const mockOutputs = output.mock.generators\n .filter((entry) => {\n // A faker entry with `operationResponses: false` opts out of the\n // per-operation `get<Op>ResponseMock` factories. The consolidated\n // schemas file (when `schemas: true`) is emitted separately and is\n // unaffected by this filter.\n if (\n !isFunction(entry) &&\n entry.type === OutputMockType.FAKER &&\n entry.operationResponses === false\n ) {\n return false;\n }\n return true;\n })\n .map((entry) => {\n const generated = invokeMockGenerator(verbOption, options, entry);\n return {\n type: isFunction(entry) ? OutputMockType.MSW : entry.type,\n implementation: generated.implementation,\n imports: generated.imports,\n strictMockSchemaTypeNames: generated.strictMockSchemaTypeNames,\n strictMockSchemaKinds: generated.strictMockSchemaKinds,\n };\n });\n\n const hasImplementation = client.implementation.trim().length > 0;\n const preferredOperationKey = verbOption.operationName;\n const baseOperationKey = verbOption.operationId\n ? `${verbOption.operationId}::${verbOption.operationName}`\n : verbOption.operationName;\n let operationKey = Object.hasOwn(acc, preferredOperationKey)\n ? baseOperationKey\n : preferredOperationKey;\n let collisionIndex = 1;\n\n while (Object.hasOwn(acc, operationKey)) {\n collisionIndex += 1;\n operationKey = `${baseOperationKey}::${collisionIndex}`;\n }\n\n acc[operationKey] = {\n implementation: hasImplementation\n ? (client.docComment ?? verbOption.doc) + client.implementation\n : client.implementation,\n imports: [...baseUrlImports, ...client.imports],\n mockOutputs,\n tags: verbOption.tags,\n mutator: verbOption.mutator,\n clientMutators: client.mutators,\n formData: verbOption.formData,\n formUrlEncoded: verbOption.formUrlEncoded,\n paramsSerializer: verbOption.paramsSerializer,\n paramsFilter: verbOption.paramsFilter,\n operationName: verbOption.operationName,\n fetchReviver: verbOption.fetchReviver,\n ...(client.returnType\n ? { types: { result: client.returnType } }\n : undefined),\n };\n\n return acc;\n },\n {} as GeneratorOperations,\n );\n};\n\nexport const generateExtraFiles = (\n outputClient: OutputClient | OutputClientFunc = DEFAULT_CLIENT,\n verbsOptions: Record<string, GeneratorVerbOptions>,\n output: NormalizedOutputOptions,\n context: ContextSpec,\n): Promise<ClientFileBuilder[]> => {\n const { extraFiles: generateExtraFiles } = getGeneratorClient(\n outputClient,\n output,\n );\n\n if (!generateExtraFiles) {\n return Promise.resolve([]);\n }\n\n return generateExtraFiles(verbsOptions, output, context);\n};\n","import {\n asyncReduce,\n type ContextSpec,\n generateVerbsOptions,\n type GeneratorApiBuilder,\n type GeneratorApiOperations,\n type GeneratorSchema,\n getFullRoute,\n getRoute,\n GetterPropType,\n isReference,\n type NormalizedInputOptions,\n type NormalizedOutputOptions,\n type OpenApiPathItemObject,\n resolveRef,\n} from '@orval/core';\nimport {\n dedupeStrictMockTypeDeclarations,\n generateMockImports,\n} from '@orval/mock';\n\nimport {\n generateClientFooter,\n generateClientHeader,\n generateClientImports,\n generateClientTitle,\n generateExtraFiles,\n generateOperations,\n} from './client';\n\nexport async function getApiBuilder({\n input,\n output,\n context,\n}: {\n input: NormalizedInputOptions;\n output: NormalizedOutputOptions;\n context: ContextSpec;\n}): Promise<GeneratorApiBuilder> {\n const api = await asyncReduce(\n Object.entries(context.spec.paths ?? {}),\n async (acc, [pathRoute, verbs]) => {\n if (!verbs) {\n return acc;\n }\n\n const route = getRoute(pathRoute);\n\n let resolvedVerbs: OpenApiPathItemObject = verbs;\n\n if (isReference(verbs)) {\n const { schema }: { schema: OpenApiPathItemObject } = resolveRef(\n verbs,\n context,\n );\n\n resolvedVerbs = schema;\n }\n\n let verbsOptions = await generateVerbsOptions({\n verbs: resolvedVerbs,\n input,\n output,\n route,\n pathRoute,\n context,\n });\n\n // GitHub #564 check if we want to exclude deprecated operations\n if (output.override.useDeprecatedOperations === false) {\n verbsOptions = verbsOptions.filter((verb) => {\n return !verb.deprecated;\n });\n }\n\n const schemas: GeneratorSchema[] = [];\n for (const {\n queryParams,\n headers,\n body,\n response,\n props,\n } of verbsOptions) {\n schemas.push(\n ...props.flatMap((param) =>\n param.type === GetterPropType.NAMED_PATH_PARAMS ? param.schema : [],\n ),\n );\n if (queryParams) {\n schemas.push(queryParams.schema, ...queryParams.deps);\n }\n if (headers) {\n schemas.push(headers.schema, ...headers.deps);\n }\n\n schemas.push(...body.schemas, ...response.schemas);\n }\n\n const fullRoute = getFullRoute(\n route,\n resolvedVerbs.servers ?? context.spec.servers,\n output.baseUrl,\n );\n if (!output.target) {\n throw new Error('Output does not have a target');\n }\n const pathOperations = await generateOperations(\n output.client,\n verbsOptions,\n {\n route: fullRoute,\n pathRoute,\n override: output.override,\n context,\n output: output.target,\n },\n output,\n );\n\n for (const verbOption of verbsOptions) {\n acc.verbOptions[verbOption.operationId] = verbOption;\n }\n acc.schemas.push(...schemas);\n for (const [key, value] of Object.entries(pathOperations)) {\n let operationKey = key;\n let counter = 1;\n while (Object.hasOwn(acc.operations, operationKey)) {\n operationKey = `${key}::${++counter}`;\n }\n acc.operations[operationKey] = value;\n }\n\n return acc;\n },\n {\n operations: {},\n verbOptions: {},\n schemas: [],\n } as GeneratorApiOperations,\n );\n\n const extraFiles = await generateExtraFiles(\n output.client,\n api.verbOptions,\n output,\n context,\n );\n\n return {\n operations: api.operations,\n schemas: api.schemas,\n verbOptions: api.verbOptions,\n title: generateClientTitle,\n header: generateClientHeader,\n footer: generateClientFooter,\n imports: generateClientImports,\n importsMock: generateMockImports,\n finalizeMockImplementation: dedupeStrictMockTypeDeclarations,\n extraFiles,\n };\n}\n","import {\n collectReferencedComponents,\n type ContextSpec,\n generateComponentDefinition,\n generateParameterDefinition,\n generateSchemasDefinition,\n type ImportOpenApi,\n type InputOptions,\n type NormalizedOutputOptions,\n type OpenApiComponentsObject,\n type OpenApiDocument,\n type WriteSpecBuilder,\n} from '@orval/core';\nimport { pick } from 'remeda';\n\nimport { getApiBuilder } from './api';\n\nfunction filterSpecComponents(\n spec: OpenApiDocument,\n input: InputOptions,\n): OpenApiDocument {\n const filters = input.filters;\n if (!filters?.tags || filters.schemas) return spec;\n\n const referenced = collectReferencedComponents(\n spec,\n filters.tags,\n filters.mode,\n );\n\n const allSchemas = spec.components?.schemas ?? {};\n\n return {\n ...spec,\n components: {\n ...spec.components,\n // `includeUnreferencedSchemas` keeps every `components.schemas` entry —\n // the section consumers are most likely to depend on — while the other\n // component sections stay pruned to what the matching operations use.\n schemas: filters.includeUnreferencedSchemas\n ? allSchemas\n : pick(allSchemas, referenced.schemas),\n responses: pick(spec.components?.responses ?? {}, referenced.responses),\n parameters: pick(\n spec.components?.parameters ?? {},\n referenced.parameters,\n ),\n requestBodies: pick(\n spec.components?.requestBodies ?? {},\n referenced.requestBodies,\n ),\n },\n };\n}\n\nexport async function importOpenApi({\n spec,\n input,\n output,\n target,\n workspace,\n projectName,\n}: ImportOpenApi): Promise<WriteSpecBuilder> {\n // The transformer has already been applied (pre-validation) in `resolveSpec`.\n const filteredSpec = filterSpecComponents(spec, input);\n\n const schemas = getApiSchemas({\n input,\n output,\n target,\n workspace,\n spec: filteredSpec,\n });\n\n const api = await getApiBuilder({\n input,\n output,\n context: {\n projectName,\n target,\n workspace,\n spec: filteredSpec,\n output,\n } satisfies ContextSpec,\n });\n\n return {\n ...api,\n schemas: [...schemas, ...api.schemas],\n target,\n // a valid spec will have info\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n info: filteredSpec.info!,\n spec: filteredSpec,\n };\n}\n\ninterface GetApiSchemasOptions {\n input: InputOptions;\n output: NormalizedOutputOptions;\n workspace: string;\n target: string;\n spec: OpenApiDocument;\n}\n\nfunction getApiSchemas({\n input,\n output,\n target,\n workspace,\n spec,\n}: GetApiSchemasOptions) {\n const context: ContextSpec = {\n target,\n workspace,\n spec,\n output,\n };\n\n const schemaDefinition = generateSchemasDefinition(\n spec.components?.schemas,\n context,\n output.override.components.schemas.suffix,\n input.filters,\n );\n\n const responseDefinition = generateComponentDefinition(\n spec.components?.responses,\n context,\n output.override.components.responses.suffix,\n );\n\n const swaggerResponseDefinition = generateComponentDefinition(\n 'responses' in spec\n ? (spec as { responses?: OpenApiComponentsObject['responses'] }).responses\n : undefined,\n context,\n '',\n );\n\n const bodyDefinition = generateComponentDefinition(\n spec.components?.requestBodies,\n context,\n output.override.components.requestBodies.suffix,\n );\n\n const parameters = generateParameterDefinition(\n spec.components?.parameters,\n context,\n output.override.components.parameters.suffix,\n );\n\n const schemas = [\n ...schemaDefinition,\n ...responseDefinition,\n ...swaggerResponseDefinition,\n ...bodyDefinition,\n ...parameters,\n ];\n\n return schemas;\n}\n","import { styleText } from 'node:util';\n\nimport {\n dynamicImport,\n isObject,\n isString,\n logVerbose,\n logWarning,\n type PackageJson,\n resolveInstalledVersions,\n} from '@orval/core';\nimport { findUp, findUpMultiple } from 'find-up';\nimport fs from 'fs-extra';\nimport yaml from 'js-yaml';\n\nimport { normalizePath } from './options';\n\ntype CatalogData = Pick<PackageJson, 'catalog' | 'catalogs'>;\n\nexport const loadPackageJson = async (\n packageJson?: string,\n workspace = process.cwd(),\n): Promise<PackageJson | undefined> => {\n if (!packageJson) {\n const pkgPath = await findUp(['package.json'], { cwd: workspace });\n if (pkgPath) {\n const pkg = await dynamicImport<unknown>(pkgPath, workspace);\n\n if (isPackageJson(pkg)) {\n return resolveAndAttachVersions(\n await maybeReplaceCatalog(pkg, workspace),\n workspace,\n pkgPath,\n );\n } else {\n throw new Error('Invalid package.json file');\n }\n }\n return;\n }\n\n const normalizedPath = normalizePath(packageJson, workspace);\n if (fs.existsSync(normalizedPath)) {\n const pkg = await dynamicImport<unknown>(normalizedPath);\n\n if (isPackageJson(pkg)) {\n return resolveAndAttachVersions(\n await maybeReplaceCatalog(pkg, workspace),\n workspace,\n normalizedPath,\n );\n } else {\n throw new Error(`Invalid package.json file: ${normalizedPath}`);\n }\n }\n return;\n};\n\nconst isPackageJson = (obj: unknown): obj is PackageJson => isObject(obj);\n\nconst resolvedCache = new Map<string, Record<string, string>>();\n\nconst resolveAndAttachVersions = (\n pkg: PackageJson,\n workspace: string,\n cacheKey: string,\n): PackageJson => {\n const cached = resolvedCache.get(cacheKey);\n if (cached) {\n pkg.resolvedVersions = cached;\n return pkg;\n }\n\n const resolved = resolveInstalledVersions(pkg, workspace);\n if (Object.keys(resolved).length > 0) {\n pkg.resolvedVersions = resolved;\n resolvedCache.set(cacheKey, resolved);\n for (const [name, version] of Object.entries(resolved)) {\n logVerbose(\n styleText(\n 'dim',\n `Detected ${styleText('white', name)} v${styleText('white', version)}`,\n ),\n );\n }\n }\n return pkg;\n};\n\nconst hasCatalogReferences = (pkg: PackageJson): boolean => {\n return [\n ...Object.entries(pkg.dependencies ?? {}),\n ...Object.entries(pkg.devDependencies ?? {}),\n ...Object.entries(pkg.peerDependencies ?? {}),\n ].some(([, value]) => isString(value) && value.startsWith('catalog:'));\n};\n\nconst loadPnpmWorkspaceCatalog = async (\n workspace: string,\n): Promise<CatalogData | undefined> => {\n const filePath = await findUp('pnpm-workspace.yaml', { cwd: workspace });\n if (!filePath) return undefined;\n try {\n const file = await fs.readFile(filePath, 'utf8');\n const data = yaml.load(file) as Record<string, unknown> | undefined;\n if (!data?.catalog && !data?.catalogs) return undefined;\n return {\n catalog: data.catalog as CatalogData['catalog'],\n catalogs: data.catalogs as CatalogData['catalogs'],\n };\n } catch {\n return undefined;\n }\n};\n\nconst loadPackageJsonCatalog = async (\n workspace: string,\n): Promise<CatalogData | undefined> => {\n const filePaths = await findUpMultiple('package.json', { cwd: workspace });\n\n for (const filePath of filePaths) {\n try {\n const pkg = (await fs.readJson(filePath)) as Record<string, unknown>;\n if (pkg.catalog || pkg.catalogs) {\n return {\n catalog: pkg.catalog as CatalogData['catalog'],\n catalogs: pkg.catalogs as CatalogData['catalogs'],\n };\n }\n } catch {\n // Continue to next file\n }\n }\n return undefined;\n};\n\nconst loadYarnrcCatalog = async (\n workspace: string,\n): Promise<CatalogData | undefined> => {\n const filePath = await findUp('.yarnrc.yml', { cwd: workspace });\n if (!filePath) return undefined;\n try {\n const file = await fs.readFile(filePath, 'utf8');\n const data = yaml.load(file) as Record<string, unknown> | undefined;\n if (!data?.catalog && !data?.catalogs) return undefined;\n return {\n catalog: data.catalog as CatalogData['catalog'],\n catalogs: data.catalogs as CatalogData['catalogs'],\n };\n } catch {\n return undefined;\n }\n};\n\nconst maybeReplaceCatalog = async (\n pkg: PackageJson,\n workspace: string,\n): Promise<PackageJson> => {\n if (!hasCatalogReferences(pkg)) {\n return pkg;\n }\n\n const catalogData =\n (await loadPnpmWorkspaceCatalog(workspace)) ??\n (await loadPackageJsonCatalog(workspace)) ??\n (await loadYarnrcCatalog(workspace));\n\n if (!catalogData) {\n logWarning(\n '⚠️ package.json contains catalog: references, but no catalog source was found (checked: pnpm-workspace.yaml, package.json, .yarnrc.yml).',\n );\n return pkg;\n }\n\n performSubstitution(pkg.dependencies, catalogData);\n performSubstitution(pkg.devDependencies, catalogData);\n performSubstitution(pkg.peerDependencies, catalogData);\n\n return pkg;\n};\n\nconst performSubstitution = (\n dependencies: Record<string, string> | undefined,\n catalogData: CatalogData,\n) => {\n if (!dependencies) return;\n for (const [packageName, version] of Object.entries(dependencies)) {\n if (version === 'catalog:' || version === 'catalog:default') {\n if (!catalogData.catalog) {\n logWarning(\n `⚠️ catalog: substitution for the package '${packageName}' failed as there is no default catalog.`,\n );\n continue;\n }\n const sub = catalogData.catalog[packageName];\n if (!sub) {\n logWarning(\n `⚠️ catalog: substitution for the package '${packageName}' failed as there is no matching package in the default catalog.`,\n );\n continue;\n }\n dependencies[packageName] = sub;\n } else if (version.startsWith('catalog:')) {\n const catalogName = version.slice('catalog:'.length);\n const catalog = catalogData.catalogs?.[catalogName];\n if (!catalog) {\n logWarning(\n `⚠️ '${version}' substitution for the package '${packageName}' failed as there is no matching catalog named '${catalogName}'. (available named catalogs are: ${Object.keys(catalogData.catalogs ?? {}).join(', ')})`,\n );\n continue;\n }\n const sub = catalog[packageName];\n if (!sub) {\n logWarning(\n `⚠️ '${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(', ')})`,\n );\n continue;\n }\n dependencies[packageName] = sub;\n }\n }\n};\n","import { isNullish, isObject, isString, type Tsconfig } from '@orval/core';\nimport { findUp } from 'find-up';\nimport fs from 'fs-extra';\nimport {\n parseTsconfig,\n type TsConfigJson,\n type TsConfigJsonResolved,\n} from 'get-tsconfig';\n\nimport { normalizePath } from './options';\n\ntype LowercaseString<T extends string> = T extends `${infer First}${infer Rest}`\n ? `${Lowercase<First>}${LowercaseString<Rest>}`\n : T;\n\nconst convertTarget = (config: TsConfigJsonResolved): Tsconfig => {\n if (!config.compilerOptions?.target) {\n return {\n baseUrl: config.compilerOptions?.baseUrl,\n ...config,\n } as Tsconfig;\n }\n const lowercaseTarget =\n config.compilerOptions.target.toLowerCase() as LowercaseString<TsConfigJson.CompilerOptions.Target>;\n return {\n baseUrl: config.compilerOptions.baseUrl,\n ...config,\n compilerOptions: { ...config.compilerOptions, target: lowercaseTarget },\n };\n};\n\nexport const loadTsconfig = async (\n tsconfig?: Tsconfig | string,\n workspace = process.cwd(),\n): Promise<Tsconfig | undefined> => {\n if (isNullish(tsconfig)) {\n const configPath = await findUp(['tsconfig.json', 'jsconfig.json'], {\n cwd: workspace,\n });\n if (configPath) {\n const config = parseTsconfig(configPath);\n return convertTarget(config);\n }\n return;\n }\n\n if (isString(tsconfig)) {\n const normalizedPath = normalizePath(tsconfig, workspace);\n if (fs.existsSync(normalizedPath)) {\n const config = parseTsconfig(normalizedPath);\n return convertTarget(config);\n }\n return;\n }\n\n if (isObject(tsconfig)) {\n return tsconfig;\n }\n return;\n};\n","import { existsSync } from 'node:fs';\nimport { access } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport nodePath from 'node:path';\nimport { styleText } from 'node:util';\n\nimport {\n type ConfigExternal,\n type EffectOptions,\n FormDataArrayHandling,\n type FakerMockOptions,\n type GlobalMockOptions,\n type GlobalOptions,\n type HonoOptions,\n type Hook,\n type HookFunction,\n type HookOption,\n type HooksOptions,\n type InputOptions,\n type InputTransformerFn,\n isBoolean,\n isFunction,\n isNullish,\n isObject,\n isString,\n isUrl,\n type JsDocOptions,\n logWarning,\n type McpOptions,\n type McpServerOptions,\n type Mutator,\n NamingConvention,\n type NormalizedEffectOptions,\n type NormalizedFactoryMethodsOptions,\n type NormalizedHonoOptions,\n type NormalizedHookOptions,\n type NormalizedJsDocOptions,\n type NormalizedMcpOptions,\n type NormalizedMcpServerOptions,\n type NormalizedMocksConfig,\n type NormalizedMutator,\n type NormalizedOperationOptions,\n type NormalizedOptions,\n type NormalizedOverrideOutput,\n type NormalizedQueryOptions,\n type NormalizedSchemaOptions,\n type OperationOptions,\n type OptionsExport,\n OutputClient,\n OutputHttpClient,\n OutputMockType,\n OutputMode,\n type OverrideOutput,\n PropertySortOrder,\n type QueryOptions,\n RefComponentSuffix,\n type SchemaOptions,\n} from '@orval/core';\nimport { getDefaultMockOptionsForType } from '@orval/mock';\n\nimport pkg from '../../package.json';\nimport { loadPackageJson } from './package-json';\nimport { loadTsconfig } from './tsconfig';\n\nconst INPUT_TARGET_FETCH_TIMEOUT_MS = 10_000;\n/**\n * Type helper to make it easier to use orval.config.ts\n * accepts a direct {@link ConfigExternal} object.\n */\nexport function defineConfig(options: ConfigExternal): ConfigExternal {\n return options;\n}\n\n/**\n * Type helper to make it easier to write input transformers.\n * accepts a direct {@link InputTransformerFn} function.\n */\nexport function defineTransformer(\n transformer: InputTransformerFn,\n): InputTransformerFn {\n return transformer;\n}\n\nfunction createFormData(\n workspace: string,\n formData: OverrideOutput['formData'],\n): NormalizedOverrideOutput['formData'] {\n const defaultArrayHandling = FormDataArrayHandling.SERIALIZE;\n if (formData === undefined)\n return { disabled: false, arrayHandling: defaultArrayHandling };\n if (isBoolean(formData))\n return { disabled: !formData, arrayHandling: defaultArrayHandling };\n if (isString(formData))\n return {\n disabled: false,\n mutator: normalizeMutator(workspace, formData),\n arrayHandling: defaultArrayHandling,\n };\n if ('mutator' in formData || 'arrayHandling' in formData)\n return {\n disabled: false,\n mutator: normalizeMutator(workspace, formData.mutator),\n arrayHandling: formData.arrayHandling ?? defaultArrayHandling,\n };\n return {\n disabled: false,\n mutator: normalizeMutator(workspace, formData),\n arrayHandling: defaultArrayHandling,\n };\n}\n\nfunction normalizeSchemasOption(\n schemas: string | SchemaOptions | false | undefined,\n workspace: string,\n): string | NormalizedSchemaOptions | undefined {\n if (!schemas) {\n return undefined;\n }\n\n if (isString(schemas)) {\n return normalizePath(schemas, workspace);\n }\n\n if (!isString(schemas.path) || schemas.path.trim() === '') {\n throw new Error(\n styleText(\n 'red',\n `\\`schemas.path\\` is required when \\`schemas\\` is an object (e.g. \\`schemas: { path: './model', type: 'zod' }\\`). To generate schemas alongside the target instead, omit \\`schemas\\` or set \\`schemas: false\\`.`,\n ),\n );\n }\n\n validatePackageSpecifier(schemas.importPath, 'schemas.importPath');\n\n return {\n path: normalizePath(schemas.path, workspace),\n type: schemas.type ?? 'typescript',\n importPath: schemas.importPath,\n splitByTags: schemas.splitByTags ?? false,\n };\n}\n\n/**\n * Validates that a config value is a valid package specifier (bare specifier\n * or sub-path import like `@acme/models` / `@acme/models/fakers`). Rejects\n * empty, whitespace-only, relative (`./`, `../`), and absolute paths with a\n * clear, actionable error message. No-op when the value is `undefined`.\n */\nfunction validatePackageSpecifier(\n value: string | undefined,\n fieldName: string,\n): void {\n if (value === undefined) {\n return;\n }\n\n if (!value) {\n throw new Error(\n `\\`${fieldName}\\` must be a non-empty package specifier (e.g. '@acme/models'). Received an empty string.`,\n );\n }\n\n if (value.trim() === '') {\n throw new Error(\n `\\`${fieldName}\\` must be a non-empty package specifier (e.g. '@acme/models'). Received a whitespace-only string.`,\n );\n }\n\n if (value.trim() !== value) {\n throw new Error(\n `\\`${fieldName}\\` must be a non-empty package specifier (e.g. '@acme/models'). Received a value with leading or trailing whitespace: \"${value}\"`,\n );\n }\n\n if (value.startsWith('.')) {\n throw new Error(\n `\\`${fieldName}\\` must be a package specifier (e.g. '@acme/models'), not a relative path. Received: \"${value}\"`,\n );\n }\n\n if (\n nodePath.isAbsolute(value) ||\n /^[A-Za-z]:[\\\\/]/.test(value) ||\n value.startsWith('\\\\\\\\')\n ) {\n throw new Error(\n `\\`${fieldName}\\` must be a package specifier (e.g. '@acme/models'), not an absolute path. Received: \"${value}\"`,\n );\n }\n}\n\nfunction looksLikePackageSpecifier(value: string): boolean {\n return (\n !!value &&\n value.trim() === value &&\n !value.startsWith('.') &&\n !nodePath.isAbsolute(value) &&\n !/^[A-Za-z]:[\\\\/]/.test(value) &&\n !value.startsWith('\\\\\\\\')\n );\n}\n\nfunction resolvePackageSpecifier(\n workspace: string,\n value: string,\n): string | undefined {\n try {\n return createRequire(nodePath.join(workspace, 'package.json')).resolve(\n value,\n );\n } catch {\n return;\n }\n}\n\nfunction isPackageSpecifierCandidate(\n workspace: string,\n value: string,\n): boolean {\n if (!looksLikePackageSpecifier(value)) {\n return false;\n }\n\n if (existsSync(nodePath.resolve(workspace, value))) {\n return false;\n }\n\n if (value.startsWith('@')) {\n return true;\n }\n\n const [packageName] = value.split('/');\n\n if (!value.includes('/')) {\n return true;\n }\n\n for (let dir = workspace; ; ) {\n if (existsSync(nodePath.join(dir, 'node_modules', packageName))) {\n return true;\n }\n\n const parent = nodePath.dirname(dir);\n if (parent === dir) {\n return false;\n }\n\n dir = parent;\n }\n}\n\nfunction normalizeEffectOptions(\n effect?: EffectOptions,\n): NormalizedEffectOptions {\n return {\n strict: {\n param: effect?.strict?.param ?? false,\n query: effect?.strict?.query ?? false,\n header: effect?.strict?.header ?? false,\n body: effect?.strict?.body ?? false,\n response: effect?.strict?.response ?? false,\n },\n generate: {\n param: effect?.generate?.param ?? true,\n query: effect?.generate?.query ?? true,\n header: effect?.generate?.header ?? true,\n body: effect?.generate?.body ?? true,\n response: effect?.generate?.response ?? true,\n },\n generateEachHttpStatus: effect?.generateEachHttpStatus ?? false,\n useBrandedTypes: effect?.useBrandedTypes ?? false,\n exactOptional: effect?.exactOptional ?? false,\n };\n}\n\nexport async function normalizeOptions(\n optionsExport: OptionsExport,\n workspace = process.cwd(),\n globalOptions: GlobalOptions = {},\n): Promise<NormalizedOptions> {\n const options = await (isFunction(optionsExport)\n ? optionsExport()\n : optionsExport);\n\n if (!options.input) {\n throw new Error(styleText('red', `Config requires an input.`));\n }\n\n if (!options.output) {\n throw new Error(styleText('red', `Config requires an output.`));\n }\n\n const inputOptions: InputOptions =\n isString(options.input) || Array.isArray(options.input)\n ? { target: options.input }\n : options.input;\n\n const outputOptions = isString(options.output)\n ? { target: options.output }\n : options.output;\n\n const outputWorkspace = normalizePath(\n outputOptions.workspace ?? '',\n workspace,\n );\n\n const { clean, client, httpClient, mode } = globalOptions;\n\n const tsconfig = await loadTsconfig(\n outputOptions.tsconfig ?? globalOptions.tsconfig,\n workspace,\n );\n\n const packageJson = await loadPackageJson(\n outputOptions.packageJson ?? globalOptions.packageJson,\n workspace,\n );\n\n // Normalize the `mock` option into a canonical `NormalizedMocksConfig`\n // so the rest of the pipeline can iterate `generators` uniformly without\n // branching on the input shape (boolean shorthand, function form, or\n // object form).\n const mocksOption = outputOptions.mock ?? globalOptions.mock;\n let mocks: NormalizedMocksConfig = {\n indexMockFiles: false,\n generators: [],\n };\n if (isBoolean(mocksOption) && mocksOption) {\n // `mock: true` shorthand emits both an MSW handler file and a faker\n // factory file using default options for each.\n mocks = {\n indexMockFiles: false,\n generators: [\n getDefaultMockOptionsForType(OutputMockType.MSW),\n getDefaultMockOptionsForType(OutputMockType.FAKER),\n ],\n };\n } else if (isFunction(mocksOption)) {\n // Function form treats the entire mocks option as a single\n // ClientMockBuilder. Wrap it in the array so writers can still iterate.\n mocks = { indexMockFiles: false, generators: [mocksOption] };\n } else if (mocksOption && typeof mocksOption === 'object') {\n if (!Array.isArray(mocksOption.generators)) {\n throw new TypeError(\n 'mock.generators must be an array of generator entries (e.g. [{ type: \"msw\" }]).',\n );\n }\n const sharedMockPath =\n mocksOption.path && isString(mocksOption.path)\n ? normalizePath(mocksOption.path, outputWorkspace)\n : undefined;\n mocks = {\n indexMockFiles: mocksOption.indexMockFiles ?? false,\n path: sharedMockPath,\n generators: mocksOption.generators.map((m) =>\n isFunction(m)\n ? m\n : ({\n ...getDefaultMockOptionsForType(m.type),\n ...m,\n path:\n m.path && isString(m.path)\n ? normalizePath(m.path, outputWorkspace)\n : sharedMockPath,\n } as GlobalMockOptions),\n ),\n };\n }\n\n const seenMockTypes = new Set<string>();\n for (const entry of mocks.generators) {\n if (isFunction(entry)) continue;\n if (seenMockTypes.has(entry.type)) {\n throw new Error(\n `Duplicate mock generator type \"${entry.type}\". Each type can only appear once in mock.generators.`,\n );\n }\n seenMockTypes.add(entry.type);\n\n if (entry.type === OutputMockType.FAKER) {\n validatePackageSpecifier(\n entry.schemasImportPath,\n 'mock.generators[faker].schemasImportPath',\n );\n }\n }\n\n const defaultFileExtension = '.ts';\n\n // Reusable Zod schemas land in `*.zod.ts` files by default so they sit\n // alongside any existing TypeScript types without a name collision. We\n // expose this as a separate `schemaFileExtension` field (not by flipping\n // the global `fileExtension`) so that non-schema writers (mode writers,\n // mock writers, the workspace barrel) keep their own extensions and don't\n // start emitting `*.zod.ts` for unrelated artifacts. A user-set\n // `output.fileExtension` overrides this default at the call site.\n const isZodSchemasOutput =\n !!outputOptions.schemas &&\n ((!isString(outputOptions.schemas) &&\n outputOptions.schemas.type === 'zod') ||\n (isString(outputOptions.schemas) &&\n (outputOptions.client ?? client) === 'zod' &&\n outputOptions.override?.zod?.generateReusableSchemas === true));\n const defaultSchemaFileExtension = isZodSchemasOutput\n ? '.zod.ts'\n : defaultFileExtension;\n\n const factoryMethodsConfig = outputOptions.factoryMethods;\n let factoryMethods: NormalizedFactoryMethodsOptions | undefined = undefined;\n\n if (factoryMethodsConfig) {\n factoryMethods = {\n functionNamePrefix: factoryMethodsConfig.functionNamePrefix ?? 'create',\n mode: factoryMethodsConfig.mode ?? 'split',\n outputDirectory: factoryMethodsConfig.outputDirectory\n ? normalizePath(factoryMethodsConfig.outputDirectory, outputWorkspace)\n : outputOptions.schemas\n ? normalizePath(\n isString(outputOptions.schemas)\n ? outputOptions.schemas\n : outputOptions.schemas.path,\n outputWorkspace,\n )\n : normalizePath(outputWorkspace, outputWorkspace),\n includeOptionalProperty:\n factoryMethodsConfig.includeOptionalProperty ?? true,\n };\n }\n\n // `useQuery` / `useMutation` defaults are applied per-verb in\n // `query-generator.ts` so we can tell \"unset\" from \"explicit true\" (#2376).\n const globalQueryOptions: NormalizedQueryOptions = {\n signal: true,\n shouldExportMutatorHooks: true,\n shouldExportHttpClient: true,\n shouldExportQueryKey: true,\n shouldFilterQueryKey: false,\n shouldSplitQueryKey: false,\n ...normalizeQueryOptions(outputOptions.override?.query, outputWorkspace),\n };\n\n const normalizedOptions: NormalizedOptions = {\n input: {\n target: globalOptions.input\n ? Array.isArray(globalOptions.input)\n ? await resolveFirstValidTarget(\n globalOptions.input,\n process.cwd(),\n inputOptions.parserOptions,\n )\n : normalizePathOrUrl(globalOptions.input, process.cwd())\n : Array.isArray(inputOptions.target)\n ? await resolveFirstValidTarget(\n inputOptions.target,\n workspace,\n inputOptions.parserOptions,\n )\n : normalizePathOrUrl(inputOptions.target, workspace),\n override: {\n transformer: normalizePath(\n inputOptions.override?.transformer,\n workspace,\n ),\n },\n unsafeDisableValidation: inputOptions.unsafeDisableValidation ?? false,\n filters: inputOptions.filters,\n parserOptions: inputOptions.parserOptions,\n },\n output: {\n target: globalOptions.output\n ? normalizePath(globalOptions.output, process.cwd())\n : normalizePath(outputOptions.target, outputWorkspace),\n schemas: normalizeSchemasOption(outputOptions.schemas, outputWorkspace),\n operationSchemas: outputOptions.operationSchemas\n ? normalizePath(outputOptions.operationSchemas, outputWorkspace)\n : undefined,\n namingConvention:\n outputOptions.namingConvention ?? NamingConvention.CAMEL_CASE,\n fileExtension: outputOptions.fileExtension ?? defaultFileExtension,\n schemaFileExtension:\n outputOptions.schemaFileExtension ??\n outputOptions.fileExtension ??\n defaultSchemaFileExtension,\n workspace: outputOptions.workspace ? outputWorkspace : undefined,\n client: outputOptions.client ?? client ?? OutputClient.AXIOS_FUNCTIONS,\n httpClient:\n outputOptions.httpClient ??\n httpClient ??\n // Auto-detect: use Angular HttpClient for angular-query by default\n ((outputOptions.client ?? client) === OutputClient.ANGULAR_QUERY\n ? OutputHttpClient.ANGULAR\n : OutputHttpClient.FETCH),\n mode: normalizeOutputMode(outputOptions.mode ?? mode),\n mock: mocks,\n clean: outputOptions.clean ?? clean ?? false,\n docs: outputOptions.docs ?? false,\n formatter: outputOptions.formatter ?? globalOptions.formatter,\n tsconfig,\n packageJson,\n headers: outputOptions.headers ?? false,\n indexFiles: outputOptions.indexFiles ?? true,\n baseUrl: outputOptions.baseUrl,\n unionAddMissingProperties:\n outputOptions.unionAddMissingProperties ?? false,\n factoryMethods,\n tagsSplitDeduplication: outputOptions.tagsSplitDeduplication ?? false,\n commonTypesFileName: outputOptions.commonTypesFileName ?? 'common-types',\n override: {\n ...outputOptions.override,\n mock: {\n arrayMin: outputOptions.override?.mock?.arrayMin ?? 1,\n arrayMax: outputOptions.override?.mock?.arrayMax ?? 10,\n stringMin: outputOptions.override?.mock?.stringMin ?? 10,\n stringMax: outputOptions.override?.mock?.stringMax ?? 20,\n fractionDigits: outputOptions.override?.mock?.fractionDigits ?? 2,\n ...outputOptions.override?.mock,\n },\n operations: normalizeOperationsAndTags(\n outputOptions.override?.operations ?? {},\n outputWorkspace,\n {\n query: globalQueryOptions,\n },\n 'operations',\n ),\n tags: normalizeOperationsAndTags(\n outputOptions.override?.tags ?? {},\n outputWorkspace,\n {\n query: globalQueryOptions,\n },\n 'tags',\n ),\n mutator: normalizeMutator(\n outputWorkspace,\n outputOptions.override?.mutator,\n ),\n formData: createFormData(\n outputWorkspace,\n outputOptions.override?.formData,\n ),\n formUrlEncoded:\n (isBoolean(outputOptions.override?.formUrlEncoded)\n ? outputOptions.override.formUrlEncoded\n : normalizeMutator(\n outputWorkspace,\n outputOptions.override?.formUrlEncoded,\n )) ?? true,\n paramsSerializer: normalizeMutator(\n outputWorkspace,\n outputOptions.override?.paramsSerializer,\n ),\n paramsFilter: normalizeMutator(\n outputWorkspace,\n outputOptions.override?.paramsFilter,\n ),\n header:\n outputOptions.override?.header === false\n ? false\n : isFunction(outputOptions.override?.header)\n ? outputOptions.override.header\n : getDefaultFilesHeader,\n requestOptions: outputOptions.override?.requestOptions ?? true,\n namingConvention: outputOptions.override?.namingConvention ?? {},\n components: {\n schemas: {\n suffix: RefComponentSuffix.schemas,\n itemSuffix:\n outputOptions.override?.components?.schemas?.itemSuffix ?? 'Item',\n ...outputOptions.override?.components?.schemas,\n },\n responses: {\n suffix: RefComponentSuffix.responses,\n ...outputOptions.override?.components?.responses,\n },\n parameters: {\n suffix: RefComponentSuffix.parameters,\n ...outputOptions.override?.components?.parameters,\n },\n requestBodies: {\n suffix: RefComponentSuffix.requestBodies,\n ...outputOptions.override?.components?.requestBodies,\n },\n },\n hono: normalizeHonoOptions(outputOptions.override?.hono, workspace),\n mcp: normalizeMcpOptions(outputOptions.override?.mcp, workspace),\n jsDoc: normalizeJSDocOptions(outputOptions.override?.jsDoc),\n query: globalQueryOptions,\n zod: {\n strict: {\n param: outputOptions.override?.zod?.strict?.param ?? false,\n query: outputOptions.override?.zod?.strict?.query ?? false,\n header: outputOptions.override?.zod?.strict?.header ?? false,\n body: outputOptions.override?.zod?.strict?.body ?? false,\n response: outputOptions.override?.zod?.strict?.response ?? false,\n },\n generate: {\n param: outputOptions.override?.zod?.generate?.param ?? true,\n query: outputOptions.override?.zod?.generate?.query ?? true,\n header: outputOptions.override?.zod?.generate?.header ?? true,\n body: outputOptions.override?.zod?.generate?.body ?? true,\n response: outputOptions.override?.zod?.generate?.response ?? true,\n },\n coerce: {\n param: outputOptions.override?.zod?.coerce?.param ?? false,\n query: outputOptions.override?.zod?.coerce?.query ?? false,\n header: outputOptions.override?.zod?.coerce?.header ?? false,\n body: outputOptions.override?.zod?.coerce?.body ?? false,\n response: outputOptions.override?.zod?.coerce?.response ?? false,\n },\n preprocess: {\n ...(outputOptions.override?.zod?.preprocess?.param\n ? {\n param: normalizeMutator(\n outputWorkspace,\n outputOptions.override.zod.preprocess.param,\n ),\n }\n : {}),\n ...(outputOptions.override?.zod?.preprocess?.query\n ? {\n query: normalizeMutator(\n outputWorkspace,\n outputOptions.override.zod.preprocess.query,\n ),\n }\n : {}),\n ...(outputOptions.override?.zod?.preprocess?.header\n ? {\n header: normalizeMutator(\n outputWorkspace,\n outputOptions.override.zod.preprocess.header,\n