diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c4962599a..82edb9272 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -33,6 +33,15 @@ jobs: run: yarn - name: build run: yarn build + - name: lint + # not all packages pass linting yet - ongoing work + run: | + yarn workspace @orval/angular lint + yarn workspace @orval/axios lint + yarn workspace @orval/fetch lint + yarn workspace @orval/hono lint + yarn workspace @orval/mcp lint + yarn workspace @orval/swr lint - name: samples up to date uses: nickcharlton/diff-check@v1.0.0 with: diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a50a5ed5..e20278b24 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,14 @@ { "cSpell.words": [ + "axios", "Emptyish", + "hono", "listitem", "openapi", "Orval", "Petstore", - "tanstack" + "tanstack", + "zods" ], "files.associations": { "turbo.json": "jsonc" diff --git a/eslint.config.mjs b/eslint.config.mjs index 4f24fb04e..d2d5bbbdd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -37,9 +37,28 @@ export default defineConfig( 'no-case-declarations': 'warn', 'no-prototype-builtins': 'warn', 'unicorn/prevent-abbreviations': 'off', + 'unicorn/consistent-function-scoping': [ + 'error', + { checkArrowFunctions: false }, + ], + '@typescript-eslint/restrict-template-expressions': [ + 'error', + // default (not strict) settings + // consider tightening these in the future + { + allow: [{ name: ['Error', 'URL', 'URLSearchParams'], from: 'lib' }], + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowNumber: true, + allowRegExp: true, + }, + ], // enable these in the future + 'unicorn/no-null': 'warn', 'unicorn/prefer-at': 'off', + 'unicorn/no-array-reduce': 'warn', '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/no-explicit-any': 'warn', diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 9f151ff91..141973379 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -118,6 +118,9 @@ export const generateAngularFooter: ClientFooterBuilder = ({ for (const operationName of operationNames) { if (returnTypesToWrite.has(operationName)) { + // Map.has ensures Map.get will not return undefined, but TS still complains + // bug https://github.com/microsoft/TypeScript/issues/13086 + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands footer += returnTypesToWrite.get(operationName) + '\n'; } } @@ -142,9 +145,9 @@ const generateImplementation = ( }: GeneratorVerbOptions, { route, context }: GeneratorOptions, ) => { - const isRequestOptions = override?.requestOptions !== false; - const isFormData = !override?.formData.disabled; - const isFormUrlEncoded = override?.formUrlEncoded !== false; + const isRequestOptions = override.requestOptions !== false; + const isFormData = !override.formData.disabled; + const isFormUrlEncoded = override.formUrlEncoded !== false; const isExactOptionalPropertyTypes = !!context.output.tsconfig?.compilerOptions?.exactOptionalPropertyTypes; const bodyForm = generateFormDataAndUrlEncodedFunction({ @@ -180,7 +183,7 @@ const generateImplementation = ( const requestOptions = isRequestOptions ? generateMutatorRequestOptions( - override?.requestOptions, + override.requestOptions, mutator.hasThirdArg, ) : ''; @@ -213,11 +216,11 @@ const generateImplementation = ( queryParams, response, verb, - requestOptions: override?.requestOptions, + requestOptions: override.requestOptions, isFormData, isFormUrlEncoded, paramsSerializer, - paramsSerializerOptions: override?.paramsSerializerOptions, + paramsSerializerOptions: override.paramsSerializerOptions, isAngular: true, isExactOptionalPropertyTypes, hasSignal: false, diff --git a/packages/axios/src/index.ts b/packages/axios/src/index.ts index 36f063cb8..47e9f0103 100644 --- a/packages/axios/src/index.ts +++ b/packages/axios/src/index.ts @@ -76,9 +76,9 @@ const generateAxiosImplementation = ( }: GeneratorVerbOptions, { route, context }: GeneratorOptions, ) => { - const isRequestOptions = override?.requestOptions !== false; - const isFormData = !override?.formData.disabled; - const isFormUrlEncoded = override?.formUrlEncoded !== false; + const isRequestOptions = override.requestOptions !== false; + const isFormData = !override.formData.disabled; + const isFormUrlEncoded = override.formUrlEncoded !== false; const isExactOptionalPropertyTypes = !!context.output.tsconfig?.compilerOptions?.exactOptionalPropertyTypes; @@ -110,7 +110,7 @@ const generateAxiosImplementation = ( const requestOptions = isRequestOptions ? generateMutatorRequestOptions( - override?.requestOptions, + override.requestOptions, mutator.hasSecondArg, ) : ''; @@ -154,11 +154,11 @@ const generateAxiosImplementation = ( queryParams, response, verb, - requestOptions: override?.requestOptions, + requestOptions: override.requestOptions, isFormData, isFormUrlEncoded, paramsSerializer, - paramsSerializerOptions: override?.paramsSerializerOptions, + paramsSerializerOptions: override.paramsSerializerOptions, isExactOptionalPropertyTypes, hasSignal: false, }); @@ -222,6 +222,9 @@ export const generateAxiosFooter: ClientFooterBuilder = ({ for (const operationName of operationNames) { if (returnTypesToWrite.has(operationName)) { + // Map.has ensures Map.get will not return undefined, but TS still complains + // bug https://github.com/microsoft/TypeScript/issues/13086 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const func = returnTypesToWrite.get(operationName)!; footer += func(noFunction ? undefined : title) + '\n'; } @@ -240,10 +243,7 @@ export const generateAxios = ( return { implementation, imports }; }; -export const generateAxiosFunctions: ClientBuilder = async ( - verbOptions, - options, -) => { +export const generateAxiosFunctions: ClientBuilder = (verbOptions, options) => { const { implementation, imports } = generateAxios(verbOptions, options); return { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 120940288..9d34dd116 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -38,7 +38,7 @@ export interface NormalizedOptions { export type NormalizedOutputOptions = { workspace?: string; - target?: string; + target: string; schemas?: string; namingConvention: NamingConvention; fileExtension: string; @@ -71,8 +71,8 @@ export type NormalizedOverrideOutput = { title?: (title: string) => string; transformer?: OutputTransformer; mutator?: NormalizedMutator; - operations: Record; - tags: Record; + operations: Record; + tags: Record; mock?: OverrideMockOptions; contentType?: OverrideOutputContentType; header: false | ((info: InfoObject) => string[] | string); @@ -208,7 +208,7 @@ export type EnumGeneration = export type OutputOptions = { workspace?: string; - target?: string; + target: string; schemas?: string; namingConvention?: NamingConvention; fileExtension?: string; @@ -546,7 +546,7 @@ export type NormalizedZodOptions = { body: boolean | ZodCoerceType[]; response: boolean | ZodCoerceType[]; }; - preprocess: { + preprocess?: { param?: NormalizedMutator; query?: NormalizedMutator; header?: NormalizedMutator; @@ -616,9 +616,9 @@ export type AngularOptions = { export type SwrOptions = { useInfinite?: boolean; useSWRMutationForGet?: boolean; - swrOptions?: any; - swrMutationOptions?: any; - swrInfiniteOptions?: any; + swrOptions?: unknown; + swrMutationOptions?: unknown; + swrInfiniteOptions?: unknown; }; export type NormalizedFetchOptions = { diff --git a/packages/core/src/utils/get-property-safe.ts b/packages/core/src/utils/get-property-safe.ts new file mode 100644 index 000000000..a03dc66c5 --- /dev/null +++ b/packages/core/src/utils/get-property-safe.ts @@ -0,0 +1,22 @@ +/** + * Type safe way to get arbitrary property from an object. + * + * @param obj - The object from which to retrieve the property. + * @param propertyName - The name of the property to retrieve. + * @returns Object with `hasProperty: true` and `value` of the property if it exists; otherwise `hasProperty: false` and undefined. + * + * @remarks Until TypeScript adds type-narrowing for Object.hasOwn we have to use this workaround + */ +export function getPropertySafe( + obj: T, + propertyName: K | string, +): + | { hasProperty: true; value: T[K] } + | { hasProperty: false; value: undefined } { + if (Object.hasOwn(obj, propertyName)) { + // safe to cast here because of the above check + return { hasProperty: true, value: obj[propertyName as K] }; + } + + return { hasProperty: false, value: undefined }; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 8c1d25c51..a2afb4b97 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -9,6 +9,7 @@ export * from './dynamic-import'; export * from './extension'; export * from './file'; export * from './file-extensions'; +export * from './get-property-safe'; export * from './is-body-verb'; export * from './logger'; export * from './merge-deep'; diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index 1384ae728..d147e0433 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -39,9 +39,9 @@ export const generateRequestFunction = ( }: GeneratorVerbOptions, { route, context, pathRoute }: GeneratorOptions, ) => { - const isRequestOptions = override?.requestOptions !== false; - const isFormData = !override?.formData.disabled; - const isFormUrlEncoded = override?.formUrlEncoded !== false; + const isRequestOptions = override.requestOptions !== false; + const isFormData = !override.formData.disabled; + const isFormUrlEncoded = override.formUrlEncoded !== false; const getUrlFnName = camel(`get-${operationName}-url`); const getUrlFnProps = toObjectString( @@ -58,7 +58,7 @@ export const generateRequestFunction = ( | PathItemObject | undefined; const parameters = - spec?.[verb]?.parameters || ([] as (ParameterObject | ReferenceObject)[]); + spec?.[verb]?.parameters ?? ([] as (ParameterObject | ReferenceObject)[]); const explodeParameters = parameters.filter((parameter) => { const { schema } = resolveRef(parameter, context); @@ -129,9 +129,11 @@ ${ contentType === 'application/nd-json' || contentType === 'application/x-ndjson'; - const isNdJson = response.contentTypes.some(isContentTypeNdJson); + const isNdJson = response.contentTypes.some((contentType) => + isContentTypeNdJson(contentType), + ); const responseTypeName = fetchResponseTypeName( - override.fetch?.includeHttpResponseReturnType, + override.fetch.includeHttpResponseReturnType, isNdJson ? 'Response' : response.definition.success, operationName, ); @@ -230,8 +232,8 @@ ${override.fetch.forceSuccessResponse && hasSuccess ? '' : `export type ${respon ? `Promise<${successName}>` : `Promise<${responseTypeName}>`; - const globalFetchOptions = isObject(override?.requestOptions) - ? `${stringify(override?.requestOptions)?.slice(1, -1)?.trim()}` + const globalFetchOptions = isObject(override.requestOptions) + ? stringify(override.requestOptions)?.slice(1, -1).trim() : ''; const fetchMethodOption = `method: '${verb.toUpperCase()}'`; const ignoreContentTypes = ['multipart/form-data']; diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts index 53c9f71e7..6ceaf4fb2 100644 --- a/packages/hono/src/index.ts +++ b/packages/hono/src/index.ts @@ -9,7 +9,6 @@ import { generateMutatorImports, type GeneratorDependency, type GeneratorImport, - type GeneratorMutator, type GeneratorVerbOptions, getFileInfo, getOrvalGeneratedTypes, @@ -176,48 +175,49 @@ const getHonoHandlers = ( handlerCode: string, /** Whether any of the handler code snippets requires importing zValidator. */ hasZValidator: boolean, -] => - opts.reduce<[string, boolean]>( - ([code, hasZValidator], opts) => { - const { handlerName, contextTypeName, verbOption, validator } = opts; +] => { + let code = ''; + let hasZValidator = false; - let currentValidator = ''; + for (const { handlerName, contextTypeName, verbOption, validator } of opts) { + let currentValidator = ''; - if (validator) { - if (verbOption.headers) { - currentValidator += `zValidator('header', ${verbOption.operationName}Header),\n`; - } - if (verbOption.params.length > 0) { - currentValidator += `zValidator('param', ${verbOption.operationName}Params),\n`; - } - if (verbOption.queryParams) { - currentValidator += `zValidator('query', ${verbOption.operationName}QueryParams),\n`; - } - if (verbOption.body.definition) { - currentValidator += `zValidator('json', ${verbOption.operationName}Body),\n`; - } - if ( - validator !== 'hono' && - verbOption.response.originalSchema?.['200']?.content?.[ - 'application/json' - ] - ) { - currentValidator += `zValidator('response', ${verbOption.operationName}Response),\n`; - } + if (validator) { + if (verbOption.headers) { + currentValidator += `zValidator('header', ${verbOption.operationName}Header),\n`; + } + if (verbOption.params.length > 0) { + currentValidator += `zValidator('param', ${verbOption.operationName}Params),\n`; } + if (verbOption.queryParams) { + currentValidator += `zValidator('query', ${verbOption.operationName}QueryParams),\n`; + } + if (verbOption.body.definition) { + currentValidator += `zValidator('json', ${verbOption.operationName}Body),\n`; + } + if ( + validator !== 'hono' && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + verbOption.response.originalSchema?.['200']?.content?.[ + 'application/json' + ] + ) { + currentValidator += `zValidator('response', ${verbOption.operationName}Response),\n`; + } + } - code += ` + code += ` export const ${handlerName} = factory.createHandlers( ${currentValidator}async (c: ${contextTypeName}) => { }, );`; - hasZValidator ||= currentValidator !== ''; - return [code, hasZValidator]; - }, - ['', false], - ); + hasZValidator ||= currentValidator !== ''; + } + + return [code, hasZValidator]; +}; const getZvalidatorImports = ( verbOptions: GeneratorVerbOptions[], @@ -252,6 +252,7 @@ const getZvalidatorImports = ( if ( !isHonoValidator && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access response.originalSchema?.['200']?.content?.['application/json'] != undefined ) { @@ -267,16 +268,20 @@ const getZvalidatorImports = ( const getVerbOptionGroupByTag = ( verbOptions: Record, ) => { - return Object.values(verbOptions).reduce< - Record - >((acc, value) => { + const grouped: Record = {}; + + for (const value of Object.values(verbOptions)) { const tag = value.tags[0]; - if (!acc[tag]) { - acc[tag] = []; + // this is not always false + // TODO look into types + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!grouped[tag]) { + grouped[tag] = []; } - acc[tag].push(value); - return acc; - }, {}); + grouped[tag].push(value); + } + + return grouped; }; const generateHandlerFile = async ({ @@ -303,21 +308,19 @@ const generateHandlerFile = async ({ const rawFile = await fs.readFile(path, 'utf8'); let content = rawFile; - content += Object.values(verbs).reduce((acc, verbOption) => { + for (const verbOption of Object.values(verbs)) { const handlerName = `${verbOption.operationName}Handlers`; const contextTypeName = `${pascal(verbOption.operationName)}Context`; if (!rawFile.includes(handlerName)) { - acc += getHonoHandlers({ + content += getHonoHandlers({ handlerName, contextTypeName, verbOption, validator, })[0]; } - - return acc; - }, ''); + } return content; } @@ -469,7 +472,7 @@ const getContext = (verbOption: GeneratorVerbOptions) => { } const queryType = verbOption.queryParams - ? `query: ${verbOption.queryParams?.schema.name},` + ? `query: ${verbOption.queryParams.schema.name},` : ''; const bodyType = verbOption.body.definition ? `json: ${verbOption.body.definition},` @@ -617,7 +620,7 @@ const generateZodFiles = async ( const builderContexts = await Promise.all( Object.entries(groupByTags).map(async ([tag, verbs]) => { const zods = await Promise.all( - verbs.map((verbOption) => + verbs.map(async (verbOption) => generateZod( verbOption, { @@ -626,7 +629,7 @@ const generateZodFiles = async ( override: output.override, context, mock: output.mock, - output: output.target!, + output: output.target, }, output.client, ), @@ -640,18 +643,14 @@ const generateZodFiles = async ( }; } - const allMutators = zods.reduce>( - (acc, z) => { - for (const mutator of z.mutators ?? []) { - acc[mutator.name] = mutator; - } - return acc; - }, - {}, - ); + const allMutators = new Map( + zods.flatMap((z) => z.mutators ?? []).map((m) => [m.name, m]), + ) + .values() + .toArray(); const mutatorsImports = generateMutatorImports({ - mutators: Object.values(allMutators), + mutators: allMutators, }); let content = `${header}import { z as zod } from 'zod';\n${mutatorsImports}\n`; @@ -670,13 +669,11 @@ const generateZodFiles = async ( }), ); - return Promise.all( - builderContexts.filter((context) => context.content !== ''), - ); + return builderContexts.filter((context) => context.content !== ''); } const zods = await Promise.all( - Object.values(verbOptions).map((verbOption) => + Object.values(verbOptions).map(async (verbOption) => generateZod( verbOption, { @@ -685,25 +682,21 @@ const generateZodFiles = async ( override: output.override, context, mock: output.mock, - output: output.target!, + output: output.target, }, output.client, ), ), ); - const allMutators = zods.reduce>( - (acc, z) => { - for (const mutator of z.mutators ?? []) { - acc[mutator.name] = mutator; - } - return acc; - }, - {}, - ); + const allMutators = new Map( + zods.flatMap((z) => z.mutators ?? []).map((m) => [m.name, m]), + ) + .values() + .toArray(); const mutatorsImports = generateMutatorImports({ - mutators: Object.values(allMutators), + mutators: allMutators, }); let content = `${header}import { z as zod } from 'zod';\n${mutatorsImports}\n`; @@ -742,7 +735,7 @@ const generateZvalidator = ( }; }; -const generateCompositeRoutes = async ( +const generateCompositeRoutes = ( verbOptions: Record, output: NormalizedOutputOptions, context: ContextSpecs, @@ -776,7 +769,7 @@ const generateCompositeRoutes = async ( const handlersPath = generateModuleSpecifier( compositeRouteInfo.path, - upath.join(handlerFileInfo.dirname ?? '', `./${operationName}`), + upath.join(handlerFileInfo.dirname, `./${operationName}`), ); return `import { ${importHandlerName} } from '${handlersPath}';`; @@ -797,7 +790,7 @@ const generateCompositeRoutes = async ( const handlersPath = generateModuleSpecifier( compositeRouteInfo.path, - upath.join(targetInfo.dirname ?? '', tag), + upath.join(targetInfo.dirname, tag), ); return `import {\n${importHandlerNames}\n} from '${handlersPath}/${tag}.handlers';`; @@ -841,13 +834,18 @@ export const generateExtraFiles: ClientExtraFilesBuilder = async ( schemaModule = `${pathWithoutExtension}.schemas`; } - const [handlers, contexts, zods, compositeRoutes] = await Promise.all([ + const contexts = generateContextFiles( + verbOptions, + output, + context, + schemaModule, + ); + const compositeRoutes = output.override.hono.compositeRoute + ? generateCompositeRoutes(verbOptions, output, context) + : []; + const [handlers, zods] = await Promise.all([ generateHandlerFiles(verbOptions, output, validator.path), - generateContextFiles(verbOptions, output, context, schemaModule), generateZodFiles(verbOptions, output, context), - output.override.hono.compositeRoute - ? generateCompositeRoutes(verbOptions, output, context) - : [], ]); return [ diff --git a/packages/hono/src/route.ts b/packages/hono/src/route.ts index bf80e4df9..1c5599761 100644 --- a/packages/hono/src/route.ts +++ b/packages/hono/src/route.ts @@ -15,21 +15,18 @@ const getRoutePath = (path: string): string => { }); const next = hasParam(matches[3]) ? getRoutePath(matches[3]) : matches[3]; - return hasParam(path) ? `${prev}\:${param}${next}` : `${prev}${param}${next}`; + return hasParam(path) ? `${prev}:${param}${next}` : `${prev}${param}${next}`; }; export const getRoute = (route: string) => { const splittedRoute = route.split('/'); - return splittedRoute.reduce((acc, path, i) => { - if (!path && !i) { - return acc; - } + let acc = ''; + for (const [i, path] of splittedRoute.entries()) { + if (!path && i === 0) continue; - if (!path.includes('{')) { - return `${acc}/${path}`; - } + acc += path.includes('{') ? `/${getRoutePath(path)}` : `/${path}`; + } - return `${acc}/${getRoutePath(path)}`; - }, ''); + return acc; }; diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 8ff0b71ce..27d8cb403 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -6,7 +6,6 @@ import { type ClientHeaderBuilder, type ContextSpecs, generateMutatorImports, - type GeneratorMutator, type GeneratorVerbOptions, getFileInfo, getFullRoute, @@ -32,11 +31,7 @@ const getHeader = ( return Array.isArray(header) ? jsDoc({ description: header }) : header; }; -export const getMcpHeader: ClientHeaderBuilder = ({ - verbOptions, - output, - clientImplementation, -}) => { +export const getMcpHeader: ClientHeaderBuilder = ({ verbOptions, output }) => { const targetInfo = getFileInfo(output.target); const schemaInfo = getFileInfo(output.schemas); @@ -44,8 +39,8 @@ export const getMcpHeader: ClientHeaderBuilder = ({ ? upath.relativeSafe(targetInfo.dirname, schemaInfo.dirname) : './' + targetInfo.filename + '.schemas'; - const importSchemaNames = Object.values(verbOptions) - .flatMap((verbOption) => { + const importSchemaNames = new Set( + Object.values(verbOptions).flatMap((verbOption) => { const imports = []; const pascalOperationName = pascal(verbOption.operationName); @@ -58,13 +53,10 @@ export const getMcpHeader: ClientHeaderBuilder = ({ } return imports; - }) - .reduce((acc, name) => { - if (!acc.find((i) => i === name)) { - acc.push(name); - } - return acc; - }, []); + }), + ) + .values() + .toArray(); const importSchemasImplementation = `import {\n ${importSchemaNames.join( ',\n ', @@ -72,15 +64,13 @@ export const getMcpHeader: ClientHeaderBuilder = ({ `; const relativeFetchClientPath = './http-client'; - const importFetchClientNames = Object.values(verbOptions) - .flatMap((verbOption) => verbOption.operationName) - .reduce((acc, name) => { - if (!acc.find((i) => i === name)) { - acc.push(name); - } - - return acc; - }, []); + const importFetchClientNames = new Set( + Object.values(verbOptions).flatMap( + (verbOption) => verbOption.operationName, + ), + ) + .values() + .toArray(); const importFetchClientImplementation = `import {\n ${importFetchClientNames.join( ',\n ', @@ -95,7 +85,7 @@ export const getMcpHeader: ClientHeaderBuilder = ({ return content + '\n'; }; -export const generateMcp: ClientBuilder = async (verbOptions, options) => { +export const generateMcp: ClientBuilder = (verbOptions) => { const handlerArgsTypes = []; const pathParamsType = verbOptions.params .map((param) => { @@ -167,7 +157,7 @@ export const ${handlerName} = async (${handlerArgsTypes.length > 0 ? `args: ${ha }; }; -export const generateServer = async ( +export const generateServer = ( verbOptions: Record, output: NormalizedOutputOptions, context: ContextSpecs, @@ -179,29 +169,29 @@ export const generateServer = async ( const toolImplementations = Object.values(verbOptions) .map((verbOption) => { - const imputSchemaTypes = []; + const inputSchemaTypes = []; if (verbOption.params.length > 0) - imputSchemaTypes.push( + inputSchemaTypes.push( ` pathParams: ${verbOption.operationName}Params`, ); if (verbOption.queryParams) - imputSchemaTypes.push( + inputSchemaTypes.push( ` queryParams: ${verbOption.operationName}QueryParams`, ); if (verbOption.body.definition) - imputSchemaTypes.push(` bodyParams: ${verbOption.operationName}Body`); + inputSchemaTypes.push(` bodyParams: ${verbOption.operationName}Body`); - const imputSchemaImplementation = - imputSchemaTypes.length > 0 + const inputSchemaImplementation = + inputSchemaTypes.length > 0 ? ` { - ${imputSchemaTypes.join(',\n ')} + ${inputSchemaTypes.join(',\n ')} },` : ''; const toolImplementation = ` server.tool( '${verbOption.operationName}', - '${verbOption.summary}',${imputSchemaImplementation ? `\n${imputSchemaImplementation}` : ''} + '${verbOption.summary}',${inputSchemaImplementation ? `\n${inputSchemaImplementation}` : ''} ${verbOption.operationName}Handler );`; @@ -280,7 +270,7 @@ const generateZodFiles = async ( output: NormalizedOutputOptions, context: ContextSpecs, ) => { - const { extension, dirname, filename } = getFileInfo(output.target); + const { extension, dirname } = getFileInfo(output.target); const header = getHeader( output.override.header, @@ -288,7 +278,7 @@ const generateZodFiles = async ( ); const zods = await Promise.all( - Object.values(verbOptions).map((verbOption) => + Object.values(verbOptions).map(async (verbOption) => generateZod( verbOption, { @@ -297,25 +287,21 @@ const generateZodFiles = async ( override: output.override, context, mock: output.mock, - output: output.target!, + output: output.target, }, output.client, ), ), ); - const allMutators = zods.reduce>( - (acc, z) => { - for (const mutator of z.mutators ?? []) { - acc[mutator.name] = mutator; - } - return acc; - }, - {}, - ); + const allMutators = new Map( + zods.flatMap((z) => z.mutators ?? []).map((m) => [m.name, m]), + ) + .values() + .toArray(); const mutatorsImports = generateMutatorImports({ - mutators: Object.values(allMutators), + mutators: allMutators, }); let content = `${header}import { z as zod } from 'zod';\n${mutatorsImports}\n`; @@ -332,7 +318,7 @@ const generateZodFiles = async ( ]; }; -const generateHttpClinetFiles = async ( +const generateHttpClientFiles = async ( verbOptions: Record, output: NormalizedOutputOptions, context: ContextSpecs, @@ -345,7 +331,7 @@ const generateHttpClinetFiles = async ( ); const clients = await Promise.all( - Object.values(verbOptions).map((verbOption) => { + Object.values(verbOptions).map(async (verbOption) => { const fullRoute = getFullRoute( verbOption.route, context.specs[context.specKey].servers, @@ -358,7 +344,7 @@ const generateHttpClinetFiles = async ( override: output.override, context, mock: output.mock, - output: output.target!, + output: output.target, }; return generateClient(verbOption, options, output.client, output); @@ -372,16 +358,13 @@ const generateHttpClinetFiles = async ( const relativeSchemasPath = output.schemas ? upath.relativeSafe(dirname, getFileInfo(output.schemas).dirname) : './' + filename + '.schemas'; + const importNames = clients .flatMap((client) => client.imports) - .reduce((acc, imp) => { - if (!acc.find((i) => i === imp.name)) { - acc.push(imp.name); - } + .map((imp) => imp.name); + const uniqueImportNames = new Set(importNames).values().toArray(); - return acc; - }, []); - const importImplementation = `import { ${importNames.join( + const importImplementation = `import { ${uniqueImportNames.join( ',\n', )} } from '${relativeSchemasPath}';`; @@ -419,10 +402,10 @@ export const generateExtraFiles: ClientExtraFilesBuilder = async ( output, context, ) => { - const [server, zods, httpClients] = await Promise.all([ - generateServer(verbOptions, output, context), + const server = generateServer(verbOptions, output, context); + const [zods, httpClients] = await Promise.all([ generateZodFiles(verbOptions, output, context), - generateHttpClinetFiles(verbOptions, output, context), + generateHttpClientFiles(verbOptions, output, context), ]); return [...server, ...zods, ...httpClients]; diff --git a/packages/mock/src/delay.ts b/packages/mock/src/delay.ts index a6e2e08ad..a2b6f44e1 100644 --- a/packages/mock/src/delay.ts +++ b/packages/mock/src/delay.ts @@ -4,10 +4,7 @@ export const getDelay = ( override?: NormalizedOverrideOutput, options?: GlobalMockOptions, ): GlobalMockOptions['delay'] => { - const overrideDelay = - override?.mock?.delay === undefined - ? options?.delay - : override?.mock?.delay; + const overrideDelay = override?.mock?.delay ?? options?.delay; const delayFunctionLazyExecute = override?.mock?.delayFunctionLazyExecute ?? options?.delayFunctionLazyExecute; diff --git a/packages/mock/src/faker/compatibleV9.test.ts b/packages/mock/src/faker/compatible-v9.test.ts similarity index 95% rename from packages/mock/src/faker/compatibleV9.test.ts rename to packages/mock/src/faker/compatible-v9.test.ts index 525336c53..808f0029d 100644 --- a/packages/mock/src/faker/compatibleV9.test.ts +++ b/packages/mock/src/faker/compatible-v9.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { isFakerVersionV9 } from './compatibleV9'; +import { isFakerVersionV9 } from './compatible-v9'; describe('isFakerVersionV9', () => { it('should return false when faker is not in package.json', () => { diff --git a/packages/mock/src/faker/compatibleV9.ts b/packages/mock/src/faker/compatible-v9.ts similarity index 100% rename from packages/mock/src/faker/compatibleV9.ts rename to packages/mock/src/faker/compatible-v9.ts diff --git a/packages/mock/src/faker/getters/combine.ts b/packages/mock/src/faker/getters/combine.ts index 6067050fc..8041b5281 100644 --- a/packages/mock/src/faker/getters/combine.ts +++ b/packages/mock/src/faker/getters/combine.ts @@ -73,7 +73,7 @@ export const combineSchemasMock = ({ if ( '$ref' in val && existingReferencedProperties.includes( - pascal(val.$ref.split('/').pop()!), + pascal(val.$ref.split('/').pop() ?? ''), ) ) { if (arr.length === 1) { @@ -96,7 +96,7 @@ export const combineSchemasMock = ({ schema: { ...val, name: item.name, - path: item.path ? item.path : '#', + path: item.path ?? '#', }, combine: { separator, @@ -139,7 +139,9 @@ export const combineSchemasMock = ({ let finalValue = value === 'undefined' ? value - : `${separator === 'allOf' && !containsOnlyPrimitiveValues ? '{' : ''}${value}${separator === 'allOf' ? (containsOnlyPrimitiveValues ? '' : '}') : '])'}`; + : // containsOnlyPrimitiveValues isn't just true, it's being set to false inside the above reduce and the type system doesn't detect it + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${separator === 'allOf' && !containsOnlyPrimitiveValues ? '{' : ''}${value}${separator === 'allOf' ? (containsOnlyPrimitiveValues ? '' : '}') : '])'}`; if (itemResolvedValue) { finalValue = finalValue.startsWith('...') ? `...{${finalValue}, ${itemResolvedValue.value}}` diff --git a/packages/mock/src/faker/getters/object.ts b/packages/mock/src/faker/getters/object.ts index a7a8c32e7..c246e575c 100644 --- a/packages/mock/src/faker/getters/object.ts +++ b/packages/mock/src/faker/getters/object.ts @@ -99,9 +99,7 @@ export const getMockObject = ({ if (item.properties) { let value = - !combine || - combine?.separator === 'oneOf' || - combine?.separator === 'anyOf' + !combine || combine.separator === 'oneOf' || combine.separator === 'anyOf' ? '{' : ''; const imports: GeneratorImport[] = []; @@ -120,7 +118,7 @@ export const getMockObject = ({ } const isRequired = - mockOptions?.required || + mockOptions?.required ?? (Array.isArray(item.required) ? item.required : []).includes(key); // Check to see if the property is a reference to an existing property @@ -128,7 +126,7 @@ export const getMockObject = ({ if ( '$ref' in prop && existingReferencedProperties.includes( - pascal(prop.$ref.split('/').pop()!), + pascal(prop.$ref.split('/').pop() ?? ''), ) ) { return; @@ -167,9 +165,7 @@ export const getMockObject = ({ value += properyScalars.join(', '); value += - !combine || - combine?.separator === 'oneOf' || - combine?.separator === 'anyOf' + !combine || combine.separator === 'oneOf' || combine.separator === 'anyOf' ? '}' : ''; @@ -188,7 +184,7 @@ export const getMockObject = ({ if ( isReference(item.additionalProperties) && existingReferencedProperties.includes( - item.additionalProperties.$ref.split('/').pop()!, + item.additionalProperties.$ref.split('/').pop() ?? '', ) ) { return { value: `{}`, imports: [], name: item.name }; diff --git a/packages/mock/src/faker/getters/route.ts b/packages/mock/src/faker/getters/route.ts index 6e07272ce..6a89ece7f 100644 --- a/packages/mock/src/faker/getters/route.ts +++ b/packages/mock/src/faker/getters/route.ts @@ -19,7 +19,7 @@ const getRoutePath = (path: string): string => { }; export const getRouteMSW = (route: string, baseUrl = '*') => { - route = route.replaceAll(':', '\\\:'); + route = route.replaceAll(':', String.raw`\:`); const splittedRoute = route.split('/'); return splittedRoute.reduce((acc, path, i) => { diff --git a/packages/mock/src/faker/getters/scalar.test.ts b/packages/mock/src/faker/getters/scalar.test.ts index 53c185700..286e848cc 100644 --- a/packages/mock/src/faker/getters/scalar.test.ts +++ b/packages/mock/src/faker/getters/scalar.test.ts @@ -119,7 +119,7 @@ describe('getMockScalar (example handling with falsy values)', () => { existingReferencedProperties: [], splitMockImplementations: [], mockOptions: { useExamples: true }, - context: { output: {} } as ContextSpecs, + context: { output: { override: {} } } as ContextSpecs, // TODO this should be: satisfies ContextSpecs }; it('should return the example value when it is a false value', () => { diff --git a/packages/mock/src/faker/getters/scalar.ts b/packages/mock/src/faker/getters/scalar.ts index 024db4721..b9129a209 100644 --- a/packages/mock/src/faker/getters/scalar.ts +++ b/packages/mock/src/faker/getters/scalar.ts @@ -11,7 +11,7 @@ import { import type { SchemaObject as SchemaObject31 } from 'openapi3-ts/oas31'; import type { MockDefinition, MockSchemaObject } from '../../types'; -import { isFakerVersionV9 } from '../compatibleV9'; +import { isFakerVersionV9 } from '../compatible-v9'; import { DEFAULT_FORMAT_MOCK } from '../constants'; import { getNullable, @@ -65,7 +65,7 @@ export const getMockScalar = ({ } const overrideTag = Object.entries(mockOptions?.tags ?? {}) - .sort((a, b) => { + .toSorted((a, b) => { return a[0].localeCompare(b[0]); }) .reduce( @@ -74,7 +74,7 @@ export const getMockScalar = ({ {} as { properties: Record }, ); - const tagProperty = resolveMockOverride(overrideTag?.properties, item); + const tagProperty = resolveMockOverride(overrideTag.properties, item); if (tagProperty) { return tagProperty; @@ -87,7 +87,7 @@ export const getMockScalar = ({ } if ( - (context.output.override?.mock?.useExamples || mockOptions?.useExamples) && + (context.output.override.mock?.useExamples || mockOptions?.useExamples) && item.example !== undefined ) { return { @@ -152,8 +152,8 @@ export const getMockScalar = ({ existingReferencedProperties, 'number', ); - } else if ('const' in item) { - value = '' + (item as SchemaObject31).const; + } else if ('const' in item && typeof item.const === 'string') { + value = item.const; } return { @@ -166,8 +166,8 @@ export const getMockScalar = ({ case 'boolean': { let value = 'faker.datatype.boolean()'; - if ('const' in item) { - value = '' + (item as SchemaObject31).const; + if ('const' in item && typeof item.const === 'string') { + value = item.const; } return { value, @@ -184,7 +184,7 @@ export const getMockScalar = ({ if ( '$ref' in item.items && existingReferencedProperties.includes( - pascal(item.items.$ref.split('/').pop()!), + pascal(item.items.$ref.split('/').pop() ?? ''), ) ) { return { value: '[]', imports: [], name: item.name }; @@ -334,8 +334,8 @@ const getEnum = ( type?: 'string' | 'number', ) => { if (!item.enum) return ''; - const joindEnumValues = item.enum - .filter((e) => e !== null) + const joinedEnumValues = item.enum + .filter((e) => e !== null) // TODO fix type, e can absolutely be null .map((e) => type === 'string' || (type === undefined && typeof e === 'string') ? `'${escape(e)}'` @@ -343,7 +343,7 @@ const getEnum = ( ) .join(','); - let enumValue = `[${joindEnumValues}]`; + let enumValue = `[${joinedEnumValues}]`; if (context.output.override.enumGenerationType === EnumGeneration.ENUM) { if (item.isRef || existingReferencedProperties.length === 0) { enumValue += ` as ${item.name}${item.name.endsWith('[]') ? '' : '[]'}`; diff --git a/packages/mock/src/faker/resolvers/value.ts b/packages/mock/src/faker/resolvers/value.ts index 13ba1855d..30ffb88ed 100644 --- a/packages/mock/src/faker/resolvers/value.ts +++ b/packages/mock/src/faker/resolvers/value.ts @@ -8,6 +8,7 @@ import { pascal, } from '@orval/core'; import type { SchemaObject } from 'openapi3-ts/oas30'; +import { prop } from 'remeda'; import type { MockDefinition, MockSchemaObject } from '../../types'; import { overrideVarName } from '../getters'; @@ -19,7 +20,7 @@ export const resolveMockOverride = ( properties: Record | undefined = {}, item: SchemaObject & { name: string; path?: string }, ) => { - const path = item.path ? item.path : `#.${item.name}`; + const path = item.path ?? `#.${item.name}`; const property = Object.entries(properties).find(([key]) => { if (isRegex(key)) { const regex = new RegExp(key.slice(1, -1)); @@ -86,12 +87,10 @@ export const resolveMockValue = ({ } = getRefInfo(schema.$ref, context); const schemaRef = Array.isArray(refPaths) - ? (refPaths.reduce( - (obj, key) => - obj && typeof obj === 'object' - ? (obj as Record)[key] - : undefined, + ? (prop( context.specs[specKey], + // @ts-expect-error: [ts2556] refPaths are not guaranteed to be valid keys of the spec + ...refPaths, ) as Partial) : undefined; @@ -100,7 +99,7 @@ export const resolveMockValue = ({ name: pascal(originalName), path: schema.path, isRef: true, - required: [...(schemaRef?.required ?? []), ...(schema?.required ?? [])], + required: [...(schemaRef?.required ?? []), ...(schema.required ?? [])], }; const newSeparator = newSchema.allOf @@ -138,7 +137,7 @@ export const resolveMockValue = ({ ) { const funcName = `get${pascal(operationId)}Response${pascal(newSchema.name)}Mock`; if ( - !splitMockImplementations?.some((f) => + !splitMockImplementations.some((f) => f.includes(`export const ${funcName}`), ) ) { @@ -151,7 +150,7 @@ export const resolveMockValue = ({ const args = `${overrideVarName}: ${type} = {}`; const func = `export const ${funcName} = (${args}): ${newSchema.name} => ({${scalar.value.startsWith('...') ? '' : '...'}${scalar.value}, ...${overrideVarName}});`; - splitMockImplementations?.push(func); + splitMockImplementations.push(func); } scalar.value = newSchema.nullable diff --git a/packages/mock/src/msw/index.ts b/packages/mock/src/msw/index.ts index 9e71a8d48..5b19917e1 100644 --- a/packages/mock/src/msw/index.ts +++ b/packages/mock/src/msw/index.ts @@ -134,9 +134,7 @@ const generateDefinition = ( const handlerImplementation = ` export const ${handlerName} = (overrideResponse?: ${returnType} | ((${infoParam}: Parameters[1]>[0]) => Promise<${returnType}> | ${returnType}), options?: RequestHandlerOptions) => { return http.${verb}('${route}', async (${infoParam}) => {${ - delay === false - ? '' - : `await delay(${isFunction(delay) ? `(${delay})()` : delay});` + typeof delay === 'number' ? `await delay(${delay});` : '' } ${isReturnHttpResponse ? '' : `if (typeof overrideResponse === 'function') {await overrideResponse(info); }`} return new HttpResponse(${ @@ -188,7 +186,7 @@ export const generateMSW = ( const route = getRouteMSW( pathRoute, - override?.mock?.baseUrl ?? (isFunction(mock) ? undefined : mock?.baseUrl), + override.mock?.baseUrl ?? (isFunction(mock) ? undefined : mock?.baseUrl), ); const handlerName = `get${pascal(operationId)}MockHandler`; diff --git a/packages/mock/src/msw/mocks.ts b/packages/mock/src/msw/mocks.ts index c3c20e7bb..1971cad5f 100644 --- a/packages/mock/src/msw/mocks.ts +++ b/packages/mock/src/msw/mocks.ts @@ -22,7 +22,7 @@ const getMockPropertiesWithoutFunc = (properties: any, spec: OpenAPIObject) => ? `(${value})()` : stringify(value as string)!; - acc[key] = implementation?.replaceAll( + acc[key] = implementation.replaceAll( /import_faker\.defaults|import_faker\.faker|_faker\.faker/g, 'faker', ); @@ -59,7 +59,7 @@ const getMockWithoutFunc = ( operations: Object.entries(override.operations).reduce< Exclude >((acc, [key, value]) => { - if (value.mock?.properties) { + if (value?.mock?.properties) { acc[key] = { properties: getMockPropertiesWithoutFunc( value.mock.properties, @@ -77,7 +77,7 @@ const getMockWithoutFunc = ( tags: Object.entries(override.tags).reduce< Exclude >((acc, [key, value]) => { - if (value.mock?.properties) { + if (value?.mock?.properties) { acc[key] = { properties: getMockPropertiesWithoutFunc( value.mock.properties, @@ -151,13 +151,13 @@ export const getResponsesMockDefinition = ({ { value: definition, originalSchema, example, examples, imports, isRef }, ) => { if ( - context.output.override?.mock?.useExamples || + context.output.override.mock?.useExamples || mockOptions?.useExamples ) { let exampleValue = - example || - originalSchema?.example || - Object.values(examples || {})[0] || + example ?? + originalSchema?.example ?? + Object.values(examples ?? {})[0] ?? originalSchema?.examples?.[0]; exampleValue = exampleValue?.value ?? exampleValue; if (exampleValue) { @@ -207,9 +207,7 @@ export const getResponsesMockDefinition = ({ acc.imports.push(...scalar.imports); acc.definitions.push( - transformer - ? transformer(scalar.value, returnType) - : scalar.value.toString(), + transformer ? transformer(scalar.value, returnType) : scalar.value, ); return acc; @@ -275,9 +273,9 @@ export const getMockOptionsDataOverride = ( override: NormalizedOverrideOutput, ) => { const responseOverride = - override?.operations?.[operationId]?.mock?.data || + override.operations[operationId]?.mock?.data ?? operationTags - .map((operationTag) => override?.tags?.[operationTag]?.mock?.data) + .map((operationTag) => override.tags[operationTag]?.mock?.data) .find((e) => e !== undefined); const implementation = isFunction(responseOverride) ? `(${responseOverride})()` diff --git a/packages/mock/src/types.ts b/packages/mock/src/types.ts index d35d6cf35..02cabe192 100644 --- a/packages/mock/src/types.ts +++ b/packages/mock/src/types.ts @@ -10,8 +10,9 @@ export interface MockDefinition { includedProperties?: string[]; } -export type MockSchemaObject = SchemaObject & { +export type MockSchemaObject = Omit & { name: string; path?: string; isRef?: boolean; + enum?: string[]; }; diff --git a/packages/swr/src/client.ts b/packages/swr/src/client.ts index 4773b6d72..d14564567 100644 --- a/packages/swr/src/client.ts +++ b/packages/swr/src/client.ts @@ -62,9 +62,9 @@ const generateAxiosRequestFunction = ( }: GeneratorVerbOptions, { route, context }: GeneratorOptions, ) => { - const isRequestOptions = override?.requestOptions !== false; - const isFormData = !override?.formData.disabled; - const isFormUrlEncoded = override?.formUrlEncoded !== false; + const isRequestOptions = override.requestOptions !== false; + const isFormData = !override.formData.disabled; + const isFormUrlEncoded = override.formUrlEncoded !== false; const isExactOptionalPropertyTypes = !!context.output.tsconfig?.compilerOptions?.exactOptionalPropertyTypes; const isSyntheticDefaultImportsAllowed = isSyntheticDefaultImportsAllow( @@ -94,7 +94,7 @@ const generateAxiosRequestFunction = ( }); const propsImplementation = - mutator?.bodyTypeName && body.definition + mutator.bodyTypeName && body.definition ? toObjectString(props, 'implementation').replace( new RegExp(`(\\w*):\\s?${body.definition}`), `$1: ${mutator.bodyTypeName}<${body.definition}>`, @@ -103,7 +103,7 @@ const generateAxiosRequestFunction = ( const requestOptions = isRequestOptions ? generateMutatorRequestOptions( - override?.requestOptions, + override.requestOptions, mutator.hasSecondArg, ) : ''; @@ -129,11 +129,11 @@ const generateAxiosRequestFunction = ( queryParams, response, verb, - requestOptions: override?.requestOptions, + requestOptions: override.requestOptions, isFormData, isFormUrlEncoded, paramsSerializer, - paramsSerializerOptions: override?.paramsSerializerOptions, + paramsSerializerOptions: override.paramsSerializerOptions, isExactOptionalPropertyTypes, hasSignal: false, }); @@ -161,7 +161,7 @@ export const getSwrRequestOptions = ( return httpClient === OutputHttpClient.AXIOS ? 'axios?: AxiosRequestConfig' : 'fetch?: RequestInit'; - } else if (mutator?.hasSecondArg) { + } else if (mutator.hasSecondArg) { return `request?: SecondParameter`; } else { return ''; @@ -193,7 +193,7 @@ export const getSwrRequestSecondArg = ( return httpClient === OutputHttpClient.AXIOS ? 'axios: axiosOptions' : 'fetch: fetchOptions'; - } else if (mutator?.hasSecondArg) { + } else if (mutator.hasSecondArg) { return 'request: requestOptions'; } else { return ''; @@ -208,7 +208,7 @@ export const getHttpRequestSecondArg = ( return httpClient === OutputHttpClient.AXIOS ? `axiosOptions` : `fetchOptions`; - } else if (mutator?.hasSecondArg) { + } else if (mutator.hasSecondArg) { return 'requestOptions'; } else { return ''; diff --git a/packages/swr/src/index.ts b/packages/swr/src/index.ts index b46c8e70e..3c9e6e596 100644 --- a/packages/swr/src/index.ts +++ b/packages/swr/src/index.ts @@ -387,7 +387,7 @@ const generateSwrHook = ( }: GeneratorVerbOptions, { route, context }: GeneratorOptions, ) => { - const isRequestOptions = override?.requestOptions !== false; + const isRequestOptions = override.requestOptions !== false; const httpClient = context.output.httpClient; const doc = jsDoc({ summary, deprecated }); @@ -523,7 +523,7 @@ export const ${swrKeyFnName} = (${queryKeyProps}) => [\`${route}\`${ const swrMutationFetcherType = getSwrMutationFetcherType( response, httpClient, - override.fetch?.includeHttpResponseReturnType, + override.fetch.includeHttpResponseReturnType, operationName, mutator, ); @@ -594,13 +594,6 @@ export const ${swrMutationFetcherName} = (${queryKeyProps} ${swrMutationFetcherO }] as const; `; - const swrMutationFetcherType = getSwrMutationFetcherType( - response, - httpClient, - override.fetch?.includeHttpResponseReturnType, - operationName, - mutator, - ); const swrMutationFetcherOptionType = getSwrMutationFetcherOptionType( httpClient, mutator, diff --git a/packages/tsdown.base.ts b/packages/tsdown.base.ts index 5d6b37965..498881d94 100644 --- a/packages/tsdown.base.ts +++ b/packages/tsdown.base.ts @@ -1,6 +1,6 @@ -import type { UserConfig } from 'tsdown'; +import type { Options } from 'tsdown'; -export const baseOptions: UserConfig = { +export const baseOptions: Options = { entry: ['src/index.ts'], target: 'node22.18', format: 'esm', diff --git a/packages/zod/src/compatibleV4.test.ts b/packages/zod/src/compatible-v4.test.ts similarity index 99% rename from packages/zod/src/compatibleV4.test.ts rename to packages/zod/src/compatible-v4.test.ts index 050a7f887..8cd00754f 100644 --- a/packages/zod/src/compatibleV4.test.ts +++ b/packages/zod/src/compatible-v4.test.ts @@ -7,7 +7,7 @@ import { getZodDateTimeFormat, getZodTimeFormat, isZodVersionV4, -} from './compatibleV4'; +} from './compatible-v4'; describe('isZodVersionV4', () => { it('should return false when zod is not in package.json', () => { diff --git a/packages/zod/src/compatibleV4.ts b/packages/zod/src/compatible-v4.ts similarity index 100% rename from packages/zod/src/compatibleV4.ts rename to packages/zod/src/compatible-v4.ts diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 3396bd505..c94dd3f82 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -10,6 +10,7 @@ import { type GeneratorOptions, type GeneratorVerbOptions, getNumberWord, + getPropertySafe, getRefInfo, isBoolean, isObject, @@ -20,15 +21,19 @@ import { stringify, type ZodCoerceType, } from '@orval/core'; -import type { - ParameterObject, - PathItemObject, - ReferenceObject, - RequestBodyObject, - ResponseObject, - SchemaObject, +import { + isSchemaObject, + type ParameterObject, + type PathItemObject, + type ReferenceObject, + type RequestBodyObject, + type ResponseObject, + type SchemaObject, } from 'openapi3-ts/oas30'; -import type { SchemaObject as SchemaObject31 } from 'openapi3-ts/oas31'; +import { + isSchemaObject as isSchemaObject31, + type SchemaObject as SchemaObject31, +} from 'openapi3-ts/oas31'; import { unique } from 'remeda'; import { @@ -38,7 +43,7 @@ import { getZodDateTimeFormat, getZodTimeFormat, isZodVersionV4, -} from './compatibleV4'; +} from './compatible-v4'; const ZOD_DEPENDENCIES: GeneratorDependency[] = [ { @@ -195,6 +200,8 @@ export const generateZodValidationSchemaDefinition = ( const type = resolveZodType(schema); const required = rules?.required ?? false; const nullable = + // changing to ?? here changes behavior - so don't + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ('nullable' in schema && schema.nullable) || (Array.isArray(schema.type) && schema.type.includes('null')); const min = schema.minimum ?? schema.minLength ?? schema.minItems; @@ -284,6 +291,8 @@ export const generateZodValidationSchemaDefinition = ( context.output.override.useDates; if (isDateType) { + // openapi3-ts's SchemaObject defines default as 'any' + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument defaultValue = `new Date("${escape(schema.default)}")`; } else if (isObject(schema.default)) { const entries = Object.entries(schema.default) @@ -299,11 +308,19 @@ export const generateZodValidationSchemaDefinition = ( return `${key}: [${arrayItems.join(', ')}]`; } - return `${key}: ${value}`; + if ( + value === null || + value === undefined || + typeof value === 'number' || + typeof value === 'boolean' + ) + return `${key}: ${value}`; }) .join(', '); defaultValue = `{ ${entries} }`; } else { + // openapi3-ts's SchemaObject defines default as 'any' + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const rawStringified = stringify(schema.default); defaultValue = rawStringified === undefined @@ -326,12 +343,12 @@ export const generateZodValidationSchemaDefinition = ( // Handle multi-type schemas (OpenAPI 3.1+ type arrays) if (typeof type === 'object' && 'multiType' in type) { - const types = (type as { multiType: string[] }).multiType; + const types = type.multiType; functions.push([ 'oneOf', types.map((t) => generateZodValidationSchemaDefinition( - { ...schema, type: t as any }, + { ...schema, type: t }, context, name, strict, @@ -392,7 +409,7 @@ export const generateZodValidationSchemaDefinition = ( if ( schema.items && - (max || Number.POSITIVE_INFINITY) > schema31.prefixItems.length + (max ?? Number.POSITIVE_INFINITY) > schema31.prefixItems.length ) { // only add zod.rest() if number of tuple elements can exceed provided prefixItems: functions.push([ @@ -430,7 +447,7 @@ export const generateZodValidationSchemaDefinition = ( break; } case 'string': { - if (schema.enum && type === 'string') { + if (schema.enum) { break; } @@ -457,7 +474,7 @@ export const generateZodValidationSchemaDefinition = ( 'uri', 'hostname', 'uuid', - ].includes(schema.format || '') + ].includes(schema.format ?? '') ) { if ('const' in schema) { functions.push(['literal', `"${schema.const}"`]); @@ -482,24 +499,18 @@ export const generateZodValidationSchemaDefinition = ( } if (schema.format === 'time') { - const options = context.output.override.zod?.timeOptions; + const options = context.output.override.zod.timeOptions; const formatAPI = getZodTimeFormat(isZodV4); - functions.push([ - formatAPI, - options ? JSON.stringify(options) : undefined, - ]); + functions.push([formatAPI, JSON.stringify(options)]); break; } if (schema.format === 'date-time') { - const options = context.output.override.zod?.dateTimeOptions; + const options = context.output.override.zod.dateTimeOptions; const formatAPI = getZodDateTimeFormat(isZodV4); - functions.push([ - formatAPI, - options ? JSON.stringify(options) : undefined, - ]); + functions.push([formatAPI, JSON.stringify(options)]); break; } @@ -520,7 +531,6 @@ export const generateZodValidationSchemaDefinition = ( break; } - case 'object': default: { if (schema.properties) { const objectType = getObjectFunctionName(isZodV4, strict); @@ -707,7 +717,7 @@ export const parseZodValidationSchemaDefinition = ( consts: argConsts, }: { functions: [string, any][]; consts: string[] }, ) => { - const value = functions.map(parseProperty).join(''); + const value = functions.map((prop) => parseProperty(prop)).join(''); const valueWithZod = `${value.startsWith('.') ? 'zod' : ''}${value}`; if (argConsts.length > 0) { @@ -729,7 +739,7 @@ export const parseZodValidationSchemaDefinition = ( if (fn === 'oneOf' || fn === 'anyOf') { // Can't use zod.union() with a single item if (args.length === 1) { - return args[0].functions.map(parseProperty).join(''); + return args[0].functions.map((prop) => parseProperty(prop)).join(''); } const union = args.map( @@ -740,10 +750,10 @@ export const parseZodValidationSchemaDefinition = ( functions: [string, any][]; consts: string[]; }) => { - const value = functions.map(parseProperty).join(''); + const value = functions.map((prop) => parseProperty(prop)).join(''); const valueWithZod = `${value.startsWith('.') ? 'zod' : ''}${value}`; // consts are missing here - consts += argConsts?.join('\n'); + consts += argConsts.join('\n'); return valueWithZod; }, ); @@ -752,7 +762,7 @@ export const parseZodValidationSchemaDefinition = ( } if (fn === 'additionalProperties') { - const value = args.functions.map(parseProperty).join(''); + const value = args.functions.map((prop) => parseProperty(prop)).join(''); const valueWithZod = `${value.startsWith('.') ? 'zod' : ''}${value}`; consts += args.consts; return `zod.record(zod.string(), ${valueWithZod})`; @@ -765,7 +775,7 @@ export const parseZodValidationSchemaDefinition = ( ${Object.entries(args) .map(([key, schema]) => { const value = (schema as ZodValidationSchemaDefinition).functions - .map(parseProperty) + .map((prop) => parseProperty(prop)) .join(''); consts += (schema as ZodValidationSchemaDefinition).consts.join('\n'); return ` "${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`; @@ -774,7 +784,7 @@ ${Object.entries(args) })`; } if (fn === 'array') { - const value = args.functions.map(parseProperty).join(''); + const value = args.functions.map((prop) => parseProperty(prop)).join(''); if (typeof args.consts === 'string') { consts += args.consts; } else if (Array.isArray(args.consts)) { @@ -790,13 +800,13 @@ ${Object.entries(args) if (fn === 'tuple') { return `zod.tuple([${(args as ZodValidationSchemaDefinition[]) .map((x) => { - const value = x.functions.map(parseProperty).join(''); + const value = x.functions.map((prop) => parseProperty(prop)).join(''); return `${value.startsWith('.') ? 'zod' : ''}${value}`; }) .join(',\n')}])`; } if (fn === 'rest') { - return `.rest(zod${(args as ZodValidationSchemaDefinition).functions.map(parseProperty)})`; + return `.rest(zod${(args as ZodValidationSchemaDefinition).functions.map((prop) => parseProperty(prop))})`; } const shouldCoerceType = coerceTypes && @@ -816,7 +826,7 @@ ${Object.entries(args) consts += input.consts.join('\n'); - const schema = input.functions.map(parseProperty).join(''); + const schema = input.functions.map((prop) => parseProperty(prop)).join(''); const value = preprocess ? `.preprocess(${preprocess.name}, ${ schema.startsWith('.') ? 'zod' : '' @@ -825,7 +835,7 @@ ${Object.entries(args) const zod = `${value.startsWith('.') ? 'zod' : ''}${value}`; // Some export consts includes `,` as prefix, adding replace to remove those - if (consts?.includes(',export')) { + if (consts.includes(',export')) { consts = consts.replaceAll(',export', '\nexport'); } return { zod, consts }; @@ -853,7 +863,7 @@ const deference = ( const childContext: ContextSpecs = { ...context, ...(refName - ? { parents: [...(context.parents || []), refName] } + ? { parents: [...(context.parents ?? []), refName] } : undefined), }; @@ -930,7 +940,7 @@ const parseBodyAndResponse = ({ ).schema; const schema = - resolvedRef.content?.['application/json']?.schema || + resolvedRef.content?.['application/json']?.schema ?? resolvedRef.content?.['multipart/form-data']?.schema; if (!schema) { @@ -1071,7 +1081,7 @@ const parseParameters = ({ schema, context, camel(`${operationName}-${parameter.in}-${parameter.name}`), - mapStrict[parameter.in as 'path' | 'query' | 'header'] ?? false, + getPropertySafe(mapStrict, parameter.in).value ?? false, isZodV4, { required: parameter.required, @@ -1346,9 +1356,9 @@ const generateZodRoute = async ( parsedBody.isArray ? `export const ${operationName}BodyItem = ${inputBody.zod} export const ${operationName}Body = zod.array(${operationName}BodyItem)${ - parsedBody.rules?.min ? `.min(${parsedBody.rules?.min})` : '' + parsedBody.rules?.min ? `.min(${parsedBody.rules.min})` : '' }${ - parsedBody.rules?.max ? `.max(${parsedBody.rules?.max})` : '' + parsedBody.rules?.max ? `.max(${parsedBody.rules.max})` : '' }` : `export const ${operationName}Body = ${inputBody.zod}`, ] @@ -1367,11 +1377,11 @@ export const ${operationName}Body = zod.array(${operationName}BodyItem)${ } export const ${operationResponse} = zod.array(${operationResponse}Item)${ parsedResponses[index].rules?.min - ? `.min(${parsedResponses[index].rules?.min})` + ? `.min(${parsedResponses[index].rules.min})` : '' }${ parsedResponses[index].rules?.max - ? `.max(${parsedResponses[index].rules?.max})` + ? `.max(${parsedResponses[index].rules.max})` : '' }` : `export const ${operationResponse} = ${inputResponse.zod}`, diff --git a/packages/zod/src/zod.test.ts b/packages/zod/src/zod.test.ts index 402584c9c..ad3d71997 100644 --- a/packages/zod/src/zod.test.ts +++ b/packages/zod/src/zod.test.ts @@ -2119,7 +2119,7 @@ describe('generatePartOfSchemaGenerateZod', () => { }); describe('parsePrefixItemsArrayAsTupleZod', () => { - it('generates correctly', async () => { + it('generates correctly', () => { const arrayWithPrefixItemsSchema: SchemaObject31 = { type: 'array', prefixItems: [{ type: 'string' }, {}], @@ -2171,7 +2171,7 @@ describe('parsePrefixItemsArrayAsTupleZod', () => { }); describe('parsePrefixItemsArrayAsTupleZod', () => { - it('correctly omits rest', async () => { + it('correctly omits rest', () => { const arrayWithPrefixItemsSchema: SchemaObject31 = { type: 'array', prefixItems: [{ type: 'string' }, {}], diff --git a/samples/hono/composite-routes-with-tags-split/package.json b/samples/hono/composite-routes-with-tags-split/package.json index 8bf4a119f..48b570288 100644 --- a/samples/hono/composite-routes-with-tags-split/package.json +++ b/samples/hono/composite-routes-with-tags-split/package.json @@ -2,6 +2,7 @@ "name": "@hono/composite-routes-with-tags-split", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "dev": "wrangler dev src/app.ts --ip 0.0.0.0", "deploy": "wrangler deploy --minify src/index.ts", diff --git a/samples/hono/hono-with-fetch-client/package.json b/samples/hono/hono-with-fetch-client/package.json index 25fc2f963..8bc9d6655 100644 --- a/samples/hono/hono-with-fetch-client/package.json +++ b/samples/hono/hono-with-fetch-client/package.json @@ -2,6 +2,7 @@ "name": "hono-with-fetch-client", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "generate-api": "orval" }, diff --git a/samples/hono/hono-with-zod/package.json b/samples/hono/hono-with-zod/package.json index b858bb0f7..558cb6ed0 100644 --- a/samples/hono/hono-with-zod/package.json +++ b/samples/hono/hono-with-zod/package.json @@ -1,6 +1,7 @@ { "name": "hono-with-zod", "private": true, + "type": "module", "scripts": { "dev": "wrangler dev src/index.ts", "deploy": "wrangler deploy --minify src/index.ts", diff --git a/samples/mcp/petstore/package.json b/samples/mcp/petstore/package.json index fcdd45f2e..9b7efd66c 100644 --- a/samples/mcp/petstore/package.json +++ b/samples/mcp/petstore/package.json @@ -2,6 +2,7 @@ "name": "mcp-petstore", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "build": "tsc", "dev": "ts-node src/server.ts", diff --git a/samples/next-app-with-fetch/package.json b/samples/next-app-with-fetch/package.json index dd4a60780..885cd16cd 100644 --- a/samples/next-app-with-fetch/package.json +++ b/samples/next-app-with-fetch/package.json @@ -2,6 +2,7 @@ "name": "next-app-with-fetch", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "dev": "next dev", "build": "next build", diff --git a/samples/react-app-with-swr/basic/package.json b/samples/react-app-with-swr/basic/package.json index bdb131adb..8c0ac02ca 100644 --- a/samples/react-app-with-swr/basic/package.json +++ b/samples/react-app-with-swr/basic/package.json @@ -2,6 +2,7 @@ "name": "swr-basic", "version": "8.0.0-rc.1", "private": true, + "type": "module", "dependencies": { "@faker-js/faker": "^10.1.0", "@types/node": "^22.18.12", diff --git a/samples/react-app/package.json b/samples/react-app/package.json index 073308aa3..37caefb58 100644 --- a/samples/react-app/package.json +++ b/samples/react-app/package.json @@ -2,6 +2,7 @@ "name": "react-app", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "start": "react-scripts start", "build": "react-scripts build", diff --git a/samples/react-query/form-data-mutator/package.json b/samples/react-query/form-data-mutator/package.json index fd7c070fd..38ec0e5ad 100644 --- a/samples/react-query/form-data-mutator/package.json +++ b/samples/react-query/form-data-mutator/package.json @@ -2,6 +2,7 @@ "name": "react-query-form-data-mutator", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "generate-api": "orval", "test": "tsc --noEmit endpoints.ts" diff --git a/samples/react-query/form-data/package.json b/samples/react-query/form-data/package.json index 2ecea0ce1..b3b8347da 100644 --- a/samples/react-query/form-data/package.json +++ b/samples/react-query/form-data/package.json @@ -2,6 +2,7 @@ "name": "react-query-form-data", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "generate-api": "orval", "test": "tsc --noEmit endpoints.ts" diff --git a/samples/react-query/form-url-encoded-mutator/package.json b/samples/react-query/form-url-encoded-mutator/package.json index 772758f89..38d3eb3f3 100644 --- a/samples/react-query/form-url-encoded-mutator/package.json +++ b/samples/react-query/form-url-encoded-mutator/package.json @@ -2,6 +2,7 @@ "name": "react-query-form-url-encoded-mutator", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "generate-api": "orval", "test": "tsc --noEmit endpoints.ts" diff --git a/samples/react-query/form-url-encoded/package.json b/samples/react-query/form-url-encoded/package.json index 45e44d82c..929e856ce 100644 --- a/samples/react-query/form-url-encoded/package.json +++ b/samples/react-query/form-url-encoded/package.json @@ -2,6 +2,7 @@ "name": "react-query-form-url-encoded", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "generate-api": "orval", "test": "tsc --noEmit endpoints.ts" diff --git a/samples/react-query/hook-mutator/package.json b/samples/react-query/hook-mutator/package.json index f9a1c4e72..4362fce8d 100644 --- a/samples/react-query/hook-mutator/package.json +++ b/samples/react-query/hook-mutator/package.json @@ -2,6 +2,7 @@ "name": "react-query-hook-mutator", "version": "8.0.0-rc.1", "private": true, + "type": "module", "scripts": { "generate-api": "orval", "test": "tsc --noEmit endpoints.ts"