diff --git a/.changeset/blue-maps-attack.md b/.changeset/blue-maps-attack.md new file mode 100644 index 0000000..262c403 --- /dev/null +++ b/.changeset/blue-maps-attack.md @@ -0,0 +1,6 @@ +--- +"alova-vscode-extension": patch +"@alova/wormhole": patch +--- + +fix apifox plugin issue diff --git a/packages/wormhole/src/helper/config/ConfigHelper.ts b/packages/wormhole/src/helper/config/ConfigHelper.ts index 85599e7..b89037c 100644 --- a/packages/wormhole/src/helper/config/ConfigHelper.ts +++ b/packages/wormhole/src/helper/config/ConfigHelper.ts @@ -89,7 +89,7 @@ export class ConfigHelper { const allAlovaJSon = this.configManager .getConfig() .generator - .map(async item => TemplateHelper.readData(this.projectPath, item.output)) + .map(async item => TemplateHelper.readData(this.projectPath, item.output!)) return Promise.all(allAlovaJSon) } } diff --git a/packages/wormhole/src/helper/config/ConfigManager.ts b/packages/wormhole/src/helper/config/ConfigManager.ts index 8df9306..b4f6d0a 100644 --- a/packages/wormhole/src/helper/config/ConfigManager.ts +++ b/packages/wormhole/src/helper/config/ConfigManager.ts @@ -1,6 +1,7 @@ import type { Config, GeneratorConfig } from './type' import { isArray, isObject, mergeWith, omit } from 'lodash' import { fromError } from 'zod-validation-error' +import prepareConfig from '@/functions/prepareConfig' import { logger } from '@/helper' import { generatorHelper } from '@/helper/config/GeneratorHelper' import { zConfig } from './zType' @@ -32,10 +33,10 @@ export class ConfigManager { * 加载并验证配置 */ public async load(config: Partial): Promise { - // 合并配置 - const mergedConfig = this.mergeConfig(this.defaultConfig, config) + // 处理配置 + const userConfig = await this.handleConfig(config) // 验证配置 - const validatedConfig = this.validateConfig(mergedConfig) + const validatedConfig = this.validateConfig(userConfig) // 更新配置 this.config = validatedConfig this.readConfig = Object.freeze(this.config) @@ -56,6 +57,13 @@ export class ConfigManager { await this.load({ ...this.config, ...partialConfig }) } + private async handleConfig(config: Partial) { + // 合并配置 + const userConfig = this.mergeConfig(this.defaultConfig, config) + // 处理插件的config配置 + userConfig.generator = await Promise.all(userConfig.generator.map(item => prepareConfig(item))) + return this.mergeConfig(this.defaultConfig, userConfig) + } /** * 验证配置 */ diff --git a/packages/wormhole/src/helper/config/GeneratorHelper.ts b/packages/wormhole/src/helper/config/GeneratorHelper.ts index 2528347..c78d72a 100644 --- a/packages/wormhole/src/helper/config/GeneratorHelper.ts +++ b/packages/wormhole/src/helper/config/GeneratorHelper.ts @@ -1,12 +1,11 @@ import type { OutputFileOptions } from '@/helper' import type { AlovaVersion, GeneratorConfig, TemplateType } from '@/type' import path from 'node:path' -import { isEqual, pick } from 'lodash' +import { isEqual } from 'lodash' import { fromError } from 'zod-validation-error' import { openApiParser, TemplateParser } from '@/core/parser' import getAlovaVersion from '@/functions/getAlovaVersion' import getAutoTemplateType from '@/functions/getAutoTemplateType' -import prepareConfig from '@/functions/prepareConfig' import { logger, PluginDriver, TemplateHelper } from '@/helper' import { existsPromise, generateFile, toCase as transformFileName } from '@/utils' import { zGeneratorConfig } from './zType' @@ -118,7 +117,7 @@ export class GeneratorHelper { } static openApiData(config: GeneratorConfig, projectPath: string) { - return openApiParser.parse(config.input, { + return openApiParser.parse(config.input!, { projectPath, platformType: config.platform, fetchOptions: config.fetchOptions, @@ -132,20 +131,13 @@ export class GeneratorHelper { force?: boolean }, ) { - // plugin: handle extends - config = await prepareConfig(config) - const pluginDriver = new PluginDriver(config.plugins) // plugin: handle before parse openapi - const configBeforeParse = await pluginDriver.hookSeq( + await pluginDriver.hookParallel( 'beforeOpenapiParse', - [pick(config, ['input', 'plugins', 'platform'])], - (result, args) => { - return result ? [result] : args - }, + [Object.freeze(config)], ) - config = { ...config, ...configBeforeParse } let document = await this.openApiData(config, options.projectPath) if (!document) { @@ -157,7 +149,7 @@ export class GeneratorHelper { return result ? [result] : args }) ?? document - const output = path.resolve(options.projectPath, config.output) + const output = path.resolve(options.projectPath, config.output!) const version = GeneratorHelper.getAlovaVersion(config, options.projectPath) const templateHelper = TemplateHelper.load({ type: this.getTemplateType(config, options.projectPath), @@ -167,7 +159,7 @@ export class GeneratorHelper { projectPath: options.projectPath, generatorConfig: config, }) - const oldTemplateData = TemplateHelper.getData(options.projectPath, config.output) + const oldTemplateData = TemplateHelper.getData(options.projectPath, config.output!) // Transform output filename by config.fileNameCase without changing template filename const toCase = (name: string) => transformFileName(name, config.fileNameCase) // Inject computed filenames into template render data for templates to reference @@ -197,7 +189,7 @@ export class GeneratorHelper { ], { output }) } - await TemplateHelper.setData(templateData, options.projectPath, config.output) + await TemplateHelper.setData(templateData, options.projectPath, config.output!) const generateFiles: OutputFileOptions[] = [ { diff --git a/packages/wormhole/src/helper/config/type.ts b/packages/wormhole/src/helper/config/type.ts index df4b49b..b2dc4ec 100644 --- a/packages/wormhole/src/helper/config/type.ts +++ b/packages/wormhole/src/helper/config/type.ts @@ -26,12 +26,11 @@ export interface ApiPlugin { */ config?: (config: GeneratorConfig) => MaybePromise /** - * Manipulate the input config before parsing the openapi file. - * Returning null does NOT replacing anything. + * Called before parsing the OpenAPI file. */ beforeOpenapiParse?: ( - inputConfig: Pick - ) => MaybePromise | undefined | null | void> + config: GeneratorConfig + ) => void /** * Manipulate the openapi document after parsing. * Returning null does NOT replacing anything. @@ -66,7 +65,7 @@ export interface GeneratorConfig { * input: 'http://192.168.5.123:8080' -> When it does not point to the openapi file, it must be used with the `platform` parameter */ - input: string + input?: string // Fetch options used by remote OpenAPI retrieval (headers, timeout, insecure). See FetchOptions in '@/utils/base'. fetchOptions?: FetchOptions /** @@ -92,7 +91,7 @@ export interface GeneratorConfig { * The output path of the interface file and type file, multiple generators cannot have repeated addresses, otherwise the generated codes will cover each other, which is meaningless. * @requires true */ - output: string + output?: string /** * Specify the media type of the generated response data. After specifying, use this data type to generate the response ts format of the 2xx status code. diff --git a/packages/wormhole/src/helper/config/zType.ts b/packages/wormhole/src/helper/config/zType.ts index 4fa07bb..5d95722 100644 --- a/packages/wormhole/src/helper/config/zType.ts +++ b/packages/wormhole/src/helper/config/zType.ts @@ -34,26 +34,23 @@ function zPluginReturn(schema: T) { return zMaybePromise(z.union([schema, z.undefined(), z.null(), z.void()])) } -// 定义 beforeOpenapiParse 的输入配置类型 -const zInputConfig = z.lazy(() => z.object({ - input: z.string(), - platform: zPlatformType.optional(), - plugins: z.array(zApiPlugin).optional(), - fetchOptions: zFetchOptions.optional(), -})) as z.ZodSchema> - // 定义 OpenAPIDocument 类型(简化版本,因为完整的 OpenAPI 规范非常复杂) const zOpenAPIDocument = z.any() as z.ZodSchema export const zApiPlugin = z.object({ name: z.string().optional(), config: z.lazy( - () => z.function().args(_zGeneratorConfig).returns(zPluginReturn(_zGeneratorConfig)).optional(), + () => z.function() + .args(_zGeneratorConfig) + .returns(zPluginReturn(_zGeneratorConfig)) + .optional(), + ), + beforeOpenapiParse: z.lazy( + () => z.function() + .args(_zGeneratorConfig) + .returns(z.void()) + .optional(), ), - beforeOpenapiParse: z.function() - .args(zInputConfig) - .returns(zPluginReturn(zInputConfig)) - .optional(), afterOpenapiParse: z.function() .args(zOpenAPIDocument) .returns(zPluginReturn(zOpenAPIDocument)) @@ -234,7 +231,7 @@ export const zConfig = z.object({ const globalKeySet = new Set() const outputSet = new Set() data.forEach((item) => { - if (outputSet.has(path.join(item.output))) { + if (outputSet.has(path.join(item.output ?? ''))) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['generator', 'output'], @@ -242,7 +239,7 @@ export const zConfig = z.object({ }) return } - outputSet.add(path.join(item.output)) + outputSet.add(path.join(item.output ?? '')) if (!item.global) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/packages/wormhole/src/plugins/presets/apifox.ts b/packages/wormhole/src/plugins/presets/apifox.ts index cf7b92e..8d8e039 100644 --- a/packages/wormhole/src/plugins/presets/apifox.ts +++ b/packages/wormhole/src/plugins/presets/apifox.ts @@ -65,13 +65,12 @@ export function apifox({ } return { name: 'apifox', - async beforeOpenapiParse(inputConfig) { - const next = { ...inputConfig } + config(config) { const base = 'https://api.apifox.com/v1/projects' if (projectId && apifoxToken) { - next.input = `${base}/${encodeURIComponent(projectId)}/export-openapi?locale=${encodeURIComponent(locale)}` - next.fetchOptions = { - ...next.fetchOptions, + config.input = `${base}/${encodeURIComponent(projectId)}/export-openapi?locale=${encodeURIComponent(locale)}` + config.fetchOptions = { + ...config.fetchOptions, headers: { 'X-Apifox-Api-Version': apifoxVersion, 'Authorization': `Bearer ${apifoxToken}`, @@ -80,7 +79,7 @@ export function apifox({ data: body, } } - return next + return config }, } } diff --git a/packages/wormhole/src/plugins/presets/tagModifier.ts b/packages/wormhole/src/plugins/presets/tagModifier.ts index 5f6dd7d..78a12df 100644 --- a/packages/wormhole/src/plugins/presets/tagModifier.ts +++ b/packages/wormhole/src/plugins/presets/tagModifier.ts @@ -52,7 +52,7 @@ export function processApiTags(apiDescriptor: ApiDescriptor, handler: ModifierHa const modifiedTag = handler(tag) // If handler returns null/undefined/void, remove this tag - if (modifiedTag == null) { + if (!modifiedTag) { return null } diff --git a/packages/wormhole/src/readConfig.ts b/packages/wormhole/src/readConfig.ts index 6a9c9fc..9b7daf8 100644 --- a/packages/wormhole/src/readConfig.ts +++ b/packages/wormhole/src/readConfig.ts @@ -51,7 +51,7 @@ export async function getApiDocs(config: Config, projectPath = process.cwd()) { } await configHelper.load(config, projectPath) return configHelper.getOutput().map((output) => { - const templateData = TemplateHelper.getData(projectPath, output) + const templateData = TemplateHelper.getData(projectPath, output!) return templateData?.pathApis ?? [] }) } diff --git a/packages/wormhole/test/__snapshots__/generate.spec.ts.snap b/packages/wormhole/test/__snapshots__/generate.spec.ts.snap index 0b16b31..5a7d430 100644 --- a/packages/wormhole/test/__snapshots__/generate.spec.ts.snap +++ b/packages/wormhole/test/__snapshots__/generate.spec.ts.snap @@ -19200,3 +19200,1205 @@ declare global { } " `; + +exports[`generate API > shouldn't replace \`index\` file if it is generated 1`] = ` +"/// +/* tslint:disable */ +/* eslint-disable */ +/** + * Swagger Petstore - version 1.0.7 + * + * This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. + * + * OpenAPI version: 3.0.0 + * + * Contact: + * + * NOTE: This file is auto generated by the alova's vscode plugin. + * + * https://alova.js.org/devtools/vscode + * + * **Do not edit the file manually.** + */ +export default { + 'pet.uploadFile': ['POST', '/pet/{petId}/uploadImage'], + 'pet.addPet': ['POST', '/pet'], + 'pet.updatePet': ['PUT', '/pet'], + 'pet.findPetsByStatus': ['GET', '/pet/findByStatus'], + 'pet.findPetsByTags': ['GET', '/pet/findByTags'], + 'pet.getPetById': ['GET', '/pet/{petId}'], + 'pet.updatePetWithForm': ['POST', '/pet/{petId}'], + 'pet.deletePet': ['DELETE', '/pet/{petId}'], + 'store.getInventory': ['GET', '/store/inventory'], + 'store.placeOrder': ['POST', '/store/order'], + 'store.getOrderById': ['GET', '/store/order/{orderId}'], + 'store.deleteOrder': ['DELETE', '/store/order/{orderId}'], + 'user.createUsersWithListInput': ['POST', '/user/createWithList'], + 'user.getUserByName': ['GET', '/user/{username}'], + 'user.updateUser': ['PUT', '/user/{username}'], + 'user.deleteUser': ['DELETE', '/user/{username}'], + 'user.loginUser': ['GET', '/user/login'], + 'user.logoutUser': ['GET', '/user/logout'], + 'user.createUsersWithArrayInput': ['POST', '/user/createWithArray'], + 'user.createUser': ['POST', '/user'] +}; +" +`; + +exports[`generate API > shouldn't replace \`index\` file if it is generated 2`] = ` +"import { createAlova } from 'alova'; +import fetchAdapter from 'alova/fetch'; +import { createApis, withConfigType, mountApis } from './createApis'; + +export const alovaInstance = createAlova({ + baseURL: 'https://petstore.swagger.io/v2', + requestAdapter: fetchAdapter(), + beforeRequest: method => {}, + responded: res => { + return res.json(); + } +}); + +export const $$userConfigMap = withConfigType({}); + +const Apis = createApis(alovaInstance, $$userConfigMap); + +mountApis(Apis); + +export default Apis; +" +`; + +exports[`generate API > shouldn't replace \`index\` file if it is generated 3`] = ` +"/* tslint:disable */ +/* eslint-disable */ +/** + * Swagger Petstore - version 1.0.7 + * + * This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. + * + * OpenAPI version: 3.0.0 + * + * Contact: + * + * NOTE: This file is auto generated by the alova's vscode plugin. + * + * https://alova.js.org/devtools/vscode + * + * **Do not edit the file manually.** + */ +import type { Alova, MethodType, AlovaGenerics, AlovaMethodCreateConfig } from 'alova'; +import { Method } from 'alova'; +import apiDefinitions from './apiDefinitions'; + +const cache = Object.create(null); +const createFunctionalProxy = (array: (string | symbol)[], alovaInstance: Alova, configMap: any) => { + const apiPathKey = array.join('.') as keyof typeof apiDefinitions; + if (cache[apiPathKey]) { + return cache[apiPathKey]; + } + // create a new proxy instance + const proxy = new Proxy(function () {}, { + get(_, property) { + // record the target property, so that it can get the completed accessing paths + const newArray = [...array, property]; + // always return a new proxy to continue recording accessing paths. + return createFunctionalProxy(newArray, alovaInstance, configMap); + }, + apply(_, __, [config]) { + const apiItem = apiDefinitions[apiPathKey]; + if (!apiItem) { + throw new Error(\`the api path of \\\`\${apiPathKey}\\\` is not found\`); + } + const mergedConfig = { + ...configMap[apiPathKey], + ...config + }; + const [method, url] = apiItem; + const pathParams = mergedConfig.pathParams; + const urlReplaced = url!.replace(/\\{([^}]+)\\}/g, (_, key) => { + const pathParam = pathParams[key]; + return pathParam; + }); + delete mergedConfig.pathParams; + let data = mergedConfig.data; + if (Object.prototype.toString.call(data) === '[object Object]' && typeof FormData !== 'undefined') { + let hasBlobData = false; + const formData = new FormData(); + for (const key in data) { + if (Array.isArray(data[key])) { + for (const ele of data[key]) { + formData.append(key, ele); + if (ele instanceof Blob) { + hasBlobData = true; + } + } + } else { + formData.append(key, data[key]); + if (data[key] instanceof Blob) { + hasBlobData = true; + } + } + } + data = hasBlobData ? formData : data; + } + return new Method(method!.toUpperCase() as MethodType, alovaInstance, urlReplaced, mergedConfig, data); + } + }); + cache[apiPathKey] = proxy; + return proxy; +}; + +export const createApis = (alovaInstance: Alova, configMap: any) => { + const Apis = new Proxy({} as Apis, { + get(_, property) { + return createFunctionalProxy([property], alovaInstance, configMap); + } + }); + return Apis; +}; + +export const mountApis = (Apis: Apis) => { + // define global variable \`Apis\` + (globalThis as any).Apis = Apis; +}; + +type MethodConfig = AlovaMethodCreateConfig< + (typeof import('./index'))['alovaInstance'] extends Alova ? AG : any, + any, + T +>; +type APISofParameters = Tag extends keyof Apis + ? Url extends keyof Apis[Tag] + ? Apis[Tag][Url] extends (...args: any) => any + ? Parameters + : any + : any + : any; +type MethodsConfigMap = { + [P in keyof typeof import('./apiDefinitions').default]?: MethodConfig< + P extends \`\${infer Tag}.\${infer Url}\` ? Parameters[0]>['transform']>[0] : any + >; +}; +export const withConfigType = (config: Config) => config; +" +`; + +exports[`generate API > shouldn't replace \`index\` file if it is generated 4`] = ` +"/* tslint:disable */ +/* eslint-disable */ +/** + * Swagger Petstore - version 1.0.7 + * + * This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. + * + * OpenAPI version: 3.0.0 + * + * Contact: + * + * NOTE: This file is auto generated by the alova's vscode plugin. + * + * https://alova.js.org/devtools/vscode + * + * **Do not edit the file manually.** + */ +import type { Alova, AlovaMethodCreateConfig, AlovaGenerics, Method } from 'alova'; +import type { $$userConfigMap, alovaInstance } from './index'; +import type apiDefinitions from './apiDefinitions'; + +type CollapsedAlova = typeof alovaInstance; +type UserMethodConfigMap = typeof $$userConfigMap; + +type Alova2MethodConfig = + CollapsedAlova extends Alova< + AlovaGenerics< + any, + any, + infer RequestConfig, + infer Response, + infer ResponseHeader, + infer L1Cache, + infer L2Cache, + infer SE + > + > + ? Omit< + AlovaMethodCreateConfig< + AlovaGenerics, + any, + Responded + >, + 'params' + > + : never; + +// Extract the return type of transform function that define in $$userConfigMap, if it not exists, use the default type. +type ExtractUserDefinedTransformed< + DefinitionKey extends keyof typeof apiDefinitions, + Default +> = DefinitionKey extends keyof UserMethodConfigMap + ? UserMethodConfigMap[DefinitionKey]['transform'] extends (...args: any[]) => any + ? Awaited> + : Default + : Default; +type Alova2Method< + Responded, + DefinitionKey extends keyof typeof apiDefinitions, + CurrentConfig extends Alova2MethodConfig +> = + CollapsedAlova extends Alova< + AlovaGenerics< + any, + any, + infer RequestConfig, + infer Response, + infer ResponseHeader, + infer L1Cache, + infer L2Cache, + infer SE + > + > + ? Method< + AlovaGenerics< + CurrentConfig extends undefined + ? ExtractUserDefinedTransformed + : CurrentConfig['transform'] extends (...args: any[]) => any + ? Awaited> + : ExtractUserDefinedTransformed, + any, + RequestConfig, + Response, + ResponseHeader, + L1Cache, + L2Cache, + SE + > + > + : never; + +export interface Category { + id?: number; + name?: string; +} +export interface Tag { + id?: number; + name?: string; +} +export interface Pet { + id?: number; + category?: Category; + name: string; + photoUrls: string[]; + tags?: Tag[]; + /** + * pet status in the store + */ + status?: 'available' | 'pending' | 'sold'; +} +export interface Order { + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + /** + * Order Status + */ + status?: 'placed' | 'approved' | 'delivered'; + complete?: boolean; +} +export interface User { + id?: number; + username?: string; + firstName?: string; + lastName?: string; + email?: string; + password?: string; + phone?: string; + /** + * User Status + */ + userStatus?: number; +} +export interface ApiResponse { + code?: number; + type?: string; + message?: string; +} +declare global { + interface Apis { + pet: { + /** + * --- + * + * [POST] uploads an image + * + * **path:** /pet/{petId}/uploadImage + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // ID of pet to update + * petId: number + * } + * \`\`\` + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * // Additional data to pass to server + * additionalMetadata?: string + * // file to upload + * file?: Blob + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = { + * code?: number + * type?: string + * message?: string + * } + * \`\`\` + */ + uploadFile< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * ID of pet to update + */ + petId: number; + }; + data: { + /** + * Additional data to pass to server + */ + additionalMetadata?: string; + /** + * file to upload + */ + file?: Blob; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [POST] Add a new pet to the store + * + * **path:** /pet + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * id?: number + * category?: { + * id?: number + * name?: string + * } + * name: string + * // [items] start + * // [items] end + * photoUrls: string[] + * // [items] start + * // [items] end + * tags?: Array<{ + * id?: number + * name?: string + * }> + * // pet status in the store + * status?: 'available' | 'pending' | 'sold' + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + addPet< + Config extends Alova2MethodConfig & { + data: Pet; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [PUT] Update an existing pet + * + * **path:** /pet + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * id?: number + * category?: { + * id?: number + * name?: string + * } + * name: string + * // [items] start + * // [items] end + * photoUrls: string[] + * // [items] start + * // [items] end + * tags?: Array<{ + * id?: number + * name?: string + * }> + * // pet status in the store + * status?: 'available' | 'pending' | 'sold' + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + updatePet< + Config extends Alova2MethodConfig & { + data: Pet; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Finds Pets by status + * + * **path:** /pet/findByStatus + * + * --- + * + * **Query Parameters** + * \`\`\`ts + * type QueryParameters = { + * // Status values that need to be considered for filter + * // [items] start + * // [items] end + * status: ('available' | 'pending' | 'sold')[] + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = Array<{ + * id?: number + * category?: { + * id?: number + * name?: string + * } + * name: string + * // [items] start + * // [items] end + * photoUrls: string[] + * // [items] start + * // [items] end + * tags?: Array<{ + * id?: number + * name?: string + * }> + * // pet status in the store + * status?: 'available' | 'pending' | 'sold' + * }> + * \`\`\` + */ + findPetsByStatus< + Config extends Alova2MethodConfig & { + params: { + /** + * Status values that need to be considered for filter + */ + status: ('available' | 'pending' | 'sold')[]; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Finds Pets by tags + * + * **path:** /pet/findByTags + * + * --- + * + * **Query Parameters** + * \`\`\`ts + * type QueryParameters = { + * // Tags to filter by + * // [items] start + * // [items] end + * tags: string[] + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = Array<{ + * id?: number + * category?: { + * id?: number + * name?: string + * } + * name: string + * // [items] start + * // [items] end + * photoUrls: string[] + * // [items] start + * // [items] end + * tags?: Array<{ + * id?: number + * name?: string + * }> + * // pet status in the store + * status?: 'available' | 'pending' | 'sold' + * }> + * \`\`\` + */ + findPetsByTags< + Config extends Alova2MethodConfig & { + params: { + /** + * Tags to filter by + */ + tags: string[]; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Find pet by ID + * + * **path:** /pet/{petId} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // ID of pet to return + * petId: number + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = { + * id?: number + * category?: { + * id?: number + * name?: string + * } + * name: string + * // [items] start + * // [items] end + * photoUrls: string[] + * // [items] start + * // [items] end + * tags?: Array<{ + * id?: number + * name?: string + * }> + * // pet status in the store + * status?: 'available' | 'pending' | 'sold' + * } + * \`\`\` + */ + getPetById< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * ID of pet to return + */ + petId: number; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [POST] Updates a pet in the store with form data + * + * **path:** /pet/{petId} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // ID of pet that needs to be updated + * petId: number + * } + * \`\`\` + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * // Updated name of the pet + * name?: string + * // Updated status of the pet + * status?: string + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + updatePetWithForm< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * ID of pet that needs to be updated + */ + petId: number; + }; + data: { + /** + * Updated name of the pet + */ + name?: string; + /** + * Updated status of the pet + */ + status?: string; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [DELETE] Deletes a pet + * + * **path:** /pet/{petId} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // Pet id to delete + * petId: number + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + deletePet< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * Pet id to delete + */ + petId: number; + }; + } + >( + config: Config + ): Alova2Method; + }; + store: { + /** + * --- + * + * [GET] Returns pet inventories by status + * + * **path:** /store/inventory + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = Record + * \`\`\` + */ + getInventory>>( + config?: Config + ): Alova2Method, 'store.getInventory', Config>; + /** + * --- + * + * [POST] Place an order for a pet + * + * **path:** /store/order + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * id?: number + * petId?: number + * quantity?: number + * shipDate?: string + * // Order Status + * status?: 'placed' | 'approved' | 'delivered' + * complete?: boolean + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = { + * id?: number + * petId?: number + * quantity?: number + * shipDate?: string + * // Order Status + * status?: 'placed' | 'approved' | 'delivered' + * complete?: boolean + * } + * \`\`\` + */ + placeOrder< + Config extends Alova2MethodConfig & { + data: Order; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Find purchase order by ID + * + * **path:** /store/order/{orderId} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // ID of pet that needs to be fetched + * orderId: number + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = { + * id?: number + * petId?: number + * quantity?: number + * shipDate?: string + * // Order Status + * status?: 'placed' | 'approved' | 'delivered' + * complete?: boolean + * } + * \`\`\` + */ + getOrderById< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * ID of pet that needs to be fetched + */ + orderId: number; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [DELETE] Delete purchase order by ID + * + * **path:** /store/order/{orderId} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // ID of the order that needs to be deleted + * orderId: number + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + deleteOrder< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * ID of the order that needs to be deleted + */ + orderId: number; + }; + } + >( + config: Config + ): Alova2Method; + }; + user: { + /** + * --- + * + * [POST] Creates list of users with given input array + * + * **path:** /user/createWithList + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = Array<{ + * id?: number + * username?: string + * firstName?: string + * lastName?: string + * email?: string + * password?: string + * phone?: string + * // User Status + * userStatus?: number + * }> + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = null + * \`\`\` + */ + createUsersWithListInput< + Config extends Alova2MethodConfig & { + data: User[]; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Get user by user name + * + * **path:** /user/{username} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // The name that needs to be fetched. Use user1 for testing. + * username: string + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = { + * id?: number + * username?: string + * firstName?: string + * lastName?: string + * email?: string + * password?: string + * phone?: string + * // User Status + * userStatus?: number + * } + * \`\`\` + */ + getUserByName< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * The name that needs to be fetched. Use user1 for testing. + */ + username: string; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [PUT] Updated user + * + * **path:** /user/{username} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // name that need to be updated + * username: string + * } + * \`\`\` + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * id?: number + * username?: string + * firstName?: string + * lastName?: string + * email?: string + * password?: string + * phone?: string + * // User Status + * userStatus?: number + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + updateUser< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * name that need to be updated + */ + username: string; + }; + data: User; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [DELETE] Delete user + * + * **path:** /user/{username} + * + * --- + * + * **Path Parameters** + * \`\`\`ts + * type PathParameters = { + * // The name that needs to be deleted + * username: string + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = unknown + * \`\`\` + */ + deleteUser< + Config extends Alova2MethodConfig & { + pathParams: { + /** + * The name that needs to be deleted + */ + username: string; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Logs user into the system + * + * **path:** /user/login + * + * --- + * + * **Query Parameters** + * \`\`\`ts + * type QueryParameters = { + * // The user name for login + * username: string + * // The password for login in clear text + * password: string + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = string + * \`\`\` + */ + loginUser< + Config extends Alova2MethodConfig & { + params: { + /** + * The user name for login + */ + username: string; + /** + * The password for login in clear text + */ + password: string; + }; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [GET] Logs out current logged in user session + * + * **path:** /user/logout + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = null + * \`\`\` + */ + logoutUser>( + config?: Config + ): Alova2Method; + /** + * --- + * + * [POST] Creates list of users with given input array + * + * **path:** /user/createWithArray + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = Array<{ + * id?: number + * username?: string + * firstName?: string + * lastName?: string + * email?: string + * password?: string + * phone?: string + * // User Status + * userStatus?: number + * }> + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = null + * \`\`\` + */ + createUsersWithArrayInput< + Config extends Alova2MethodConfig & { + data: User[]; + } + >( + config: Config + ): Alova2Method; + /** + * --- + * + * [POST] Create user + * + * **path:** /user + * + * --- + * + * **RequestBody** + * \`\`\`ts + * type RequestBody = { + * id?: number + * username?: string + * firstName?: string + * lastName?: string + * email?: string + * password?: string + * phone?: string + * // User Status + * userStatus?: number + * } + * \`\`\` + * + * --- + * + * **Response** + * \`\`\`ts + * type Response = null + * \`\`\` + */ + createUser< + Config extends Alova2MethodConfig & { + data: User; + } + >( + config: Config + ): Alova2Method; + }; + } + + var Apis: Apis; +} +" +`; diff --git a/packages/wormhole/test/plugins/__snapshots__/apifox.spec.ts.snap b/packages/wormhole/test/plugins/__snapshots__/apifox.spec.ts.snap index 533581b..76e0b3e 100644 --- a/packages/wormhole/test/plugins/__snapshots__/apifox.spec.ts.snap +++ b/packages/wormhole/test/plugins/__snapshots__/apifox.spec.ts.snap @@ -51,3 +51,55 @@ export default { }; " `; + +exports[`apifox preset plugin - config > should generate using Apifox export endpoint via MSW 1`] = ` +"/// +/* tslint:disable */ +/* eslint-disable */ +/** + * Swagger Generator - version 3.0.57 + * + * This is an online swagger codegen server. You can find out more at https://github.com/swagger-api/swagger-codegen or on [irc.freenode.net, #swagger](http://swagger.io/irc/). + * + * OpenAPI version: 3.0.1 + * + * + * NOTE: This file is auto generated by the alova's vscode plugin. + * + * https://alova.js.org/devtools/vscode + * + * **Do not edit the file manually.** + */ +export default { + 'clients.generateFromURL': ['GET', '/generate'], + 'servers.generateFromURL': ['GET', '/generate'], + 'documentation.generateFromURL': ['GET', '/generate'], + 'config.generateFromURL': ['GET', '/generate'], + 'clients.generate': ['POST', '/generate'], + 'servers.generate': ['POST', '/generate'], + 'documentation.generate': ['POST', '/generate'], + 'config.generate': ['POST', '/generate'], + 'clients.clientLanguages': ['GET', '/clients'], + 'documentation.clientLanguages': ['GET', '/clients'], + 'servers.serverLanguages': ['GET', '/servers'], + 'documentation.documentationLanguages': ['GET', '/documentation'], + 'clients.languages': ['GET', '/{type}/{version}'], + 'servers.languages': ['GET', '/{type}/{version}'], + 'documentation.languages': ['GET', '/{type}/{version}'], + 'config.languages': ['GET', '/{type}/{version}'], + 'clients.languagesMulti': ['GET', '/types'], + 'servers.languagesMulti': ['GET', '/types'], + 'documentation.languagesMulti': ['GET', '/types'], + 'config.languagesMulti': ['GET', '/types'], + 'clients.listOptions': ['GET', '/options'], + 'servers.listOptions': ['GET', '/options'], + 'documentation.listOptions': ['GET', '/options'], + 'config.listOptions': ['GET', '/options'], + 'clients.generateBundle': ['POST', '/model'], + 'servers.generateBundle': ['POST', '/model'], + 'documentation.generateBundle': ['POST', '/model'], + 'config.generateBundle': ['POST', '/model'], + 'documentation.renderTemplate': ['POST', '/render'] +}; +" +`; diff --git a/packages/wormhole/test/plugins/apifox.spec.ts b/packages/wormhole/test/plugins/apifox.spec.ts index 4439a4e..e64a7e1 100644 --- a/packages/wormhole/test/plugins/apifox.spec.ts +++ b/packages/wormhole/test/plugins/apifox.spec.ts @@ -5,9 +5,10 @@ import { generateWithPlugin } from '../util' vi.mock('node:fs') vi.mock('node:fs/promises') -describe('apifox preset plugin - beforeOpenapiParse', () => { - const baseInputConfig: Pick = { +describe('apifox preset plugin - config', () => { + const baseInputConfig: GeneratorConfig = { input: 'dummy', + output: 'xxx', platform: 'swagger', plugins: [], } @@ -18,7 +19,7 @@ describe('apifox preset plugin - beforeOpenapiParse', () => { apifoxToken: 'token-abc', }) - const next = (await plugin.beforeOpenapiParse!(baseInputConfig)) ?? baseInputConfig + const next = (await plugin.config!(baseInputConfig)) ?? baseInputConfig // input URL expect(next.input).toBe( @@ -53,7 +54,7 @@ describe('apifox preset plugin - beforeOpenapiParse', () => { selectedTags: ['user', 'order'], }) - const next = (await plugin.beforeOpenapiParse!(baseInputConfig)) ?? baseInputConfig + const next = (await plugin.config!(baseInputConfig)) ?? baseInputConfig const data = next.fetchOptions?.data as Record expect(data.scope?.type).toBe('SELECTED_TAGS') expect(data.scope?.selectedTags).toEqual(['user', 'order']) @@ -72,7 +73,7 @@ describe('apifox preset plugin - beforeOpenapiParse', () => { excludedByTags: ['internal'], }) - const next = (await plugin.beforeOpenapiParse!(baseInputConfig)) ?? baseInputConfig + const next = (await plugin.config!(baseInputConfig)) ?? baseInputConfig expect(next.input).toBe( 'https://api.apifox.com/v1/projects/%E4%B8%AD%E6%96%87%20%E9%A1%B9%E7%9B%AE/export-openapi?locale=en-US', @@ -99,8 +100,8 @@ describe('apifox preset plugin - beforeOpenapiParse', () => { const pluginMissingProject = apifox({ apifoxToken: 't', projectId: '' }) const pluginMissingToken = apifox({ projectId: 'p', apifoxToken: '' }) - const next1 = (await pluginMissingProject.beforeOpenapiParse!(baseInputConfig)) ?? baseInputConfig - const next2 = (await pluginMissingToken.beforeOpenapiParse!(baseInputConfig)) ?? baseInputConfig + const next1 = (await pluginMissingProject.config!(baseInputConfig)) ?? baseInputConfig + const next2 = (await pluginMissingToken.config!(baseInputConfig)) ?? baseInputConfig expect(next1).toEqual(baseInputConfig) expect(next2).toEqual(baseInputConfig) @@ -116,7 +117,7 @@ describe('apifox preset plugin - beforeOpenapiParse', () => { // Integration: actually use apifox via generateWithPlugin and MSW it('should generate using Apifox export endpoint via MSW', async () => { const { apiDefinitionsFile } = await generateWithPlugin( - 'test-apifox', + '', [ apifox({ projectId: 'proj-123', diff --git a/packages/wormhole/test/plugins/plugin.spec.ts b/packages/wormhole/test/plugins/plugin.spec.ts index a0b557d..573065f 100644 --- a/packages/wormhole/test/plugins/plugin.spec.ts +++ b/packages/wormhole/test/plugins/plugin.spec.ts @@ -55,8 +55,7 @@ describe('plugin test', () => { it('should execute beforeOpenapiParse hook correctly', async () => { const beforeParseHookFn = vi.fn() - const inputPath = resolve(__dirname, '../openapis/openapi_301.json') - const inputTest = 'test' + const inputTest = resolve(__dirname, '../openapis/openapi_301.json') const beforeParsePlugin = createPlugin(() => ({ name: 'beforeParsePlugin', beforeOpenapiParse: (inputConfig) => { @@ -66,12 +65,7 @@ describe('plugin test', () => { expect(inputConfig.input).toBe(inputTest) expect(inputConfig).toHaveProperty('platform') expect(inputConfig).toHaveProperty('plugins') - - // Return modified config to test the hook actually works - return { - ...inputConfig, - input: inputPath, // Modify input for testing - } + // Do not modify `config`; side effects only }, })) diff --git a/packages/wormhole/typings/index.d.ts b/packages/wormhole/typings/index.d.ts index 099cf02..f1ad43c 100644 --- a/packages/wormhole/typings/index.d.ts +++ b/packages/wormhole/typings/index.d.ts @@ -89,7 +89,7 @@ export interface GeneratorConfig { * input: 'openapi/api.json' -> Take the current project as the local address of the relative directory * input: 'http://192.168.5.123:8080' -> When it does not point to the openapi file, it must be used with the `platform` parameter */ - input: string; + input?: string; fetchOptions?: FetchOptions; /** * A list of type identifiers to exclude from generation. @@ -113,7 +113,7 @@ export interface GeneratorConfig { * The output path of the interface file and type file, multiple generators cannot have repeated addresses, otherwise the generated codes will cover each other, which is meaningless. * @requires true */ - output: string; + output?: string; /** * Specify the media type of the generated response data. After specifying, use this data type to generate the response ts format of the 2xx status code. * @defualt 'application/json' diff --git a/packages/wormhole/typings/plugins.d.ts b/packages/wormhole/typings/plugins.d.ts index 4c1955c..ff43edf 100644 --- a/packages/wormhole/typings/plugins.d.ts +++ b/packages/wormhole/typings/plugins.d.ts @@ -76,7 +76,7 @@ export interface GeneratorConfig { * input: 'openapi/api.json' -> Take the current project as the local address of the relative directory * input: 'http://192.168.5.123:8080' -> When it does not point to the openapi file, it must be used with the `platform` parameter */ - input: string; + input?: string; fetchOptions?: FetchOptions; /** * A list of type identifiers to exclude from generation. @@ -100,7 +100,7 @@ export interface GeneratorConfig { * The output path of the interface file and type file, multiple generators cannot have repeated addresses, otherwise the generated codes will cover each other, which is meaningless. * @requires true */ - output: string; + output?: string; /** * Specify the media type of the generated response data. After specifying, use this data type to generate the response ts format of the 2xx status code. * @defualt 'application/json'