@clean-js/api-gen
Version:
[docs](https://lulusir.github.io/clean-js/api-gen/usage) [中文文档](https://github.com/lulusir/clean-js-api-gen/blob/main/README-zh.md)
3 lines (2 loc) • 17.9 kB
JavaScript
;var e=require("jiti"),t=require("path"),s=require("node:os"),a=require("fs-extra"),r=require("ts-morph"),i=require("change-case"),n=require("json-schema-to-typescript"),o=require("mkdirp"),c=require("json-schema-to-zod"),h=require("node-fetch"),l=require("@apidevtools/json-schema-ref-parser");const u=new class{url="";type="axios";outDir="clean-js";diff=!0;zod=!1;mock=!1;loadRuntime(){const t=process.cwd(),s=e(t,{interopDefault:!0,esmResolve:!0})("./clean.config");Object.assign(this,s)}loadConfig(e){Object.assign(this,e)}getOutPath(){return t.join(process.cwd(),this.outDir)}getServicePath(e="http"){return t.join(this.getOutPath(),`./${e}.service.ts`)}getMockPath(e="http"){return t.join(this.getOutPath(),`./${e}.mock.ts`)}getAstCachePath(){return t.join(this.getOutPath(),".ast.cache.json")}getLogPath(){return t.join(this.getOutPath(),"./log")}};class p{sf;constructor(){this.sf=this.getSourceFile(),this.addZod(this.sf)}getSourceFile(){const e=a.readFileSync(t.join(__dirname,"../template/",{axios:"axios.tpl.ts",umi3:"umi3.tpl.ts"}[u.type||"axios"]),"utf-8");return new r.Project({}).createSourceFile(u.getServicePath(),e,{overwrite:!0})}insertCode(e){this.sf.insertText(this.sf.getEnd(),e)}async save(){await this.sf.save()}async addZod(e){u.zod&&(e.addImportDeclaration({namedImports:["Schema","z"],moduleSpecifier:"zod"}),e.insertText(e.getEnd(),"function verifyZod(schema: Schema, value:any, url: string) {\n if (schema) {\n try {\n const res = schema?.safeParse?.(value);\n if (res) {\n if (!res.success) {\n console.warn('zod verify error on url: ' + url, res.error);\n Req._zodErrorHandler(res.error, value, url, schema)\n }\n }\n } catch (error) {\n // ignore error\n }\n }\n}"))}}function m(e){const t=e.replace(/[«»<>:\\|?*".]/g,"");return i.pascalCase(t)}function d(e){return Boolean(e.$ref)}const f={integer:"number",number:"number",string:"string",boolean:"boolean",object:"object",null:"null",array:"array",undefined:"undefined",file:"File"};function y(e,t="camel"){return("camel"===t?i.camelCase:i.pascalCase)(e.replaceAll(/{}/g,"").replaceAll("/","_"))}class w{static async schemaToRenameInterface(e,t){const s=t,a={...e,title:s};return await n.compile(a,s,{bannerComment:"",additionalProperties:!1})}static getSourceFile(){const e=a.readFileSync(t.join(__dirname,"../template/",{axios:"axios.tpl.ts",umi3:"umi3.tpl.ts"}[u.type||"axios"]),"utf-8");return new r.Project({}).createSourceFile(u.getServicePath(),e,{overwrite:!0})}static cleanOut(){return a.rm(u.getOutPath(),{recursive:!0}).catch((e=>{console.log(e)}))}static async writeOutFolder(){a.existsSync(u.getOutPath())&&await o(u.getOutPath(),{})}static async writeFile(e,s){const r=t.basename(e),i=t.dirname(e);try{await o(i,{}),a.writeFileSync(t.join(i,r),s)}catch(e){console.log(e)}}}class g{ast;constructor(e){this.ast=e}getSourceFile(){return new r.Project({}).createSourceFile("")}async paint(){return await this.paintRequestsOneFile(this.ast.requests)}async paintRequestsOneFile(e){const t=this.getSourceFile();return e.length&&await Promise.all(e.map((async e=>{const{pathAlias:s,queryAlias:a,bodyAlias:r,response200Alias:i}=await this.processPrams(t,e);this.generateFunc(e,t,r,a,s,i)}))),t.getText()}async processPrams(e,t){let s=null;if(t.responses?.length){const a=t.responses.filter((e=>200===e.status))[0];s=await this.writeSchema("N"+t.id+".Res",e,a?.schema,"Response")}const a=[];await Promise.all(Object.entries(t?.pathParams||{})?.map((async([s,r])=>{if(r){const i=await this.writeSchema("N"+t.id+".Path",e,r,m(s),s);a.push({name:s,alias:i})}})));const r=[];await Promise.all(Object.entries(t?.queryParams||{})?.map((async([s,a])=>{if(a){const i=await this.writeSchema("N"+t.id+".Query",e,a,m(s),s);r.push({name:s,alias:i})}})));let i=null;if(t.bodyParams){if("json"===t.bodyParams.type){i={...await this.writeSchema("N"+t.id+".Body",e,t.bodyParams.schema,"Body"),type:"json"}}if("formData"===t.bodyParams.type){i={...await this.writeSchema("N"+t.id+".Body",e,t.bodyParams.schema,"BodyFile"),type:"formData"}}}return{bodyAlias:i,queryAlias:r,pathAlias:a,response200Alias:s}}async generateFunc(e,t,s,a,r,i){let n=[];if((s||a.length||r.length)&&(n=[{name:"parameter",type:e=>{e.block((()=>{s&&e.write(`body: ${s.alias},`).endsWith(","),a.length&&e.write("params: ").block((()=>{a.forEach((t=>{e.write(`'${t.name}'${t.alias.required?":":"?:"} ${t.alias.alias},`)}))})).endsWith(","),r.length&&e.write("path: ").block((()=>{r.forEach((t=>{e.write(`'${t.name}': ${t.alias.alias},`)}))})).endsWith(",")}))}}]),"axios"===u.type){n.push({name:"config",hasQuestionToken:!0,type:"AxiosRequestConfig"});const o=t?.addFunction({isExported:!0,name:e.id,parameters:n,statements:t=>{u.zod&&t.write(`const s = ${c.parseSchema(i?.schema?.schema)};`),t.write("return Req.request"),i&&t.write(`<${i.alias}>`),t.write("("),t.block((()=>{r.length?t.writeLine(`url: replaceUrlPath('${e.url}', parameter?.path),`):t.writeLine(`url: '${e.url}',`),t.writeLine(`method: '${e.method}',`),a.length&&t.writeLine("params: parameter.params,"),s&&("json"===s.type&&t.writeLine("data: parameter.body,"),"formData"===s.type&&(t.writeLine("data: handleFormData(parameter.body),"),t.writeLine("headers: {\n 'Content-Type': 'multipart/form-data'\n },"))),t.writeLine("...config")})).write(")"),u.zod&&t.write(`.then(res => {\n if (verifyZod && s) {\n verifyZod(s, res.data, '${e.url}')\n }\n return res\n })`),t.write(";")}});e.description&&o.addJsDoc({description:e.description})}else if("umi3"===u.type){n.push({name:"config",hasQuestionToken:!0,type:"RequestUmiOptions"});const o=t?.addFunction({isExported:!0,name:e.id,parameters:n,statements:t=>{u.zod&&t.write(`const s = ${c.parseSchema(i?.schema?.schema)};`),t.write("return Req.request"),i&&t.write(`<${i.alias}>`),r.length?t.write(`( replaceUrlPath('${e.url}', parameter?.path),`):t.writeLine(`('${e.url}',`),t.block((()=>{t.writeLine(`method: '${e.method}',`),a.length&&t.writeLine("params: parameter.params,"),s&&("json"===s.type&&t.writeLine("data: parameter.body,"),"formData"===s.type&&(t.writeLine("data: handleFormData(parameter.body),"),t.writeLine("requestType: 'form',"))),t.writeLine("...config")})).write(")"),u.zod&&t.write(`.then(res => {\n if (verifyZod && s) {\n verifyZod(s, res, '${e.url}')\n }\n return res\n})`),t.write(";")}});e.description&&o.addJsDoc({description:e.description})}}async writeSchema(e,t,s,a,i){let n="any",o=!0,c=e,h="";if(c.includes(".")&&([c,h]=e.split(".")),s){n=a;const e=function(e){return void 0===e?f.undefined:f[e]}(s.schema?.type);if(["number","string","boolean","null"].includes(e))n=e,i&&(s.schema.required?.includes(i)||(o=!1));else{const e=await w.schemaToRenameInterface(s.schema,n);let a=t.getModule(c)||t.addModule({name:c,isExported:!0});if(a.setDeclarationKind(r.ModuleDeclarationKind.Namespace),h){let t=a.getModule(h)||a.addModule({name:h,isExported:!0});t.insertText(t.getEnd()-1,e)}else a.insertText(a.getEnd()-1,e)}}if(n===a){let e=c;h&&(e+="."+h),n=e+"."+n}return{alias:n,required:o,schema:s}}}class b{ast;constructor(e){this.ast=e}singleMax=100;async visit(){if(this.ast.requests.length<this.singleMax){const e=await new g(this.ast).paint(),t=new p;t.insertCode(e),await t.save()}else await this.processGen()}processGen(){return new Promise((e=>{const t=s.cpus().length,a=this.singleMax,r=t*a;let i=[];if(this.ast.requests.length>r)i=Array(t).fill(0).map((()=>[])),this.ast.requests.forEach(((e,s)=>{i[s%t].push(e)}));else{let e=0;for(;e<this.ast.requests.length;){const t=[];for(;t.length<a;){const s=this.ast.requests[e];if(e+=1,t.push(s),e>=this.ast.requests.length)break}i.push(t)}}const n=new p;let o=0;i.forEach((t=>{const{fork:s}=require("child_process"),a=s(__dirname+"/process/requestGen.js");a.send(JSON.stringify({config:u,ast:{requests:t}})),a.on("message",(async t=>{o+=1,n.insertCode(t),a.send("done"),o===i.length&&(await n.save(),e(null))}))}))}))}}var v,j,q,_;!function(e){e.Delete="DELETE",e.Get="GET",e.Post="POST",e.Put="PUT",e.Patch="PATCH"}(v||(v={})),function(e){e.Form="form",e.JSON="json",e.file="file",e.raw="raw"}(j||(j={})),function(e){e.Done="done",e.Undone="undone"}(q||(q={})),function(e){e.Static="static",e.Var="var"}(_||(_={}));class P{url;constructor(e){this.url=new URL(e)}async convertToSwaggerV2Model(e){return{swagger:"2.0",info:{title:"",version:"last",description:""},basePath:"/",tags:(()=>{let t=[];return e.forEach((e=>{t.push({name:e?.name||"emptyName",description:e.desc})})),t})(),schemes:["http"],paths:(()=>{let t={};for(let s of e)for(let e of s.list)null==t[e.path]&&(t[e.path]={}),t[e.path][e.method.toLowerCase()]=(()=>{let t={responses:{}};switch(t.summary=e.title,t.description=this.getApiLink(e.project_id,e._id),e.req_body_type){case"form":case"file":t.consumes=["multipart/form-data"];break;case"json":t.consumes=["application/json"];break;case"raw":t.consumes=["text/plain"]}return t.parameters=(()=>{let t=[];for(let s of e.req_headers)"Content-Type"!==s.name&&t.push({name:s.name,in:"header",description:`${s.name} (Only:${s.value})`,required:1===Number(s.required),type:"string",default:s.value});for(let s of e.req_params)t.push({name:s.name,in:"path",description:s.desc,required:!0,type:"string"});for(let s of e.req_query)t.push({name:s.name,in:"query",required:1===Number(s.required),description:s.desc,type:"string"});if("get"!==e.method.toLowerCase())switch(e.req_body_type){case"form":for(let s of e.req_body_form)t.push({name:s.name,in:"formData",required:1===Number(s.required),description:s.desc,type:"text"===s.type?"string":"file"});break;case"json":if(e.req_body_other){let s=JSON.parse(e.req_body_other);s&&t.push({name:"root",in:"body",description:s.description,schema:s})}break;case"file":t.push({name:"upfile",in:"formData",description:e.req_body_other,type:"file"});break;case"raw":t.push({name:"raw",in:"body",description:"raw paramter",schema:{type:"string",format:"binary",default:e.req_body_other}})}return t})(),t.responses={200:{description:"successful operation",schema:(()=>{let t={};if("raw"===e.res_body_type)t.type="string",t.format="binary",t.default=e.res_body;else if("json"===e.res_body_type&&e.res_body){let s=JSON.parse(e.res_body);null!==s&&(t=s)}return t})()}},t})();return t})()}}getApiLink(e,t){return"Yapi link: "+(this.url.origin+`/project/${e}/interface/api/${t}`)}}async function O(e,t){if(s=e,Array.isArray(s)){const s=new P(t);return await s.convertToSwaggerV2Model(e)}var s;return e}const S=console.log;class k{doc;constructor(e){this.doc=e}root={requests:[]};async visit(){return await l.dereference(this.doc).then((e=>{this.doc=e})),await this.visit_doc(),this.root}async visit_doc(){this.root.requests=await this.visit_paths(this.doc.paths)}async visit_SchemaObject(e){return{schema:e,version:"OpenAPIV2"}}async visit_paths(e){const t=[];Object.entries(e).forEach((([e,s])=>{s&&Object.entries(s).forEach((([s,a])=>{!function(e){return["get","put","post","delete","options","head","patch","trace"].includes(e)}(s)?console.log("等待处理其他方法",s):t.push(this.visit_operationObject(a,e,s))}))}));return await Promise.all(t)}async visit_operationObject(e,t,s){const a={id:`${y(s+"/"+t)}`,url:t,method:s,responses:[],description:e.description},r=e.parameters?.map((async e=>{if(e){const t=await this.visit_ParameterObjectAST(e);"path"===e.in&&t.schema&&(a.pathParams={...a.pathParams,[e.name]:t}),"query"===e.in&&t&&(a.queryParams={...a.queryParams,[e.name]:t}),"body"===e.in&&(a.bodyParams=await this.visit_RequestBodyObject(e)),"header"===e.in&&t&&(a.headers={...a.headers,[e.name]:t}),"cookie"===e.in&&console.error("skip cookie"),"formData"===e.in&&(a.bodyParams?a.bodyParams.schema?.schema.properties&&(a.bodyParams.schema.schema.properties[e.name]={type:"file"===e.type?"any":"string"}):a.bodyParams={type:"formData",schema:{version:"OpenAPIV2",schema:{type:"object",properties:{[e.name]:{type:"file"===e.type?"any":"string"}}}}})}}))||[];await Promise.all(r);e.consumes?.length&&a?.headers&&(a.headers["Content-Type"]={schema:{type:"string",required:["Content-Type"]},version:"OpenAPIV2"});const{responses:i}=e;if(i){const e=await this.visit_ResponseObject(i[200]);e&&a.responses?.push(e)}return a}async visit_ParameterObjectAST(e){if(e?.type){return{schema:{type:e.type,required:e.required?[e.name]:[]},version:"OpenAPIV2"}}return await this.visit_SchemaObject(e.schema)}async visit_refOrSchema(e){if(e){return await this.visit_SchemaObject(e)}}async visit_RequestBodyObject(e){if(!e)return;const t={type:"json"};return t.schema=await this.visit_SchemaObject(e.schema),t}async visit_ResponseObject(e){if(!e)return;const t={status:200,type:"json"};return t.schema=await this.visit_refOrSchema(e.schema),t}}class x{doc;constructor(e){this.doc=e}root={requests:[]};async visit(){return await l.dereference(this.doc).then((e=>{this.doc=e})),await this.visit_doc(),this.root}async visit_doc(){this.root.requests=await this.visit_paths(this.doc.paths)}async parseRef(){return await l.dereference({components:JSON.parse(JSON.stringify(this.doc))})}async visit_SchemaObject(e){return{schema:e,version:"OpenAPIV3"}}async visit_paths(e){const t=[];Object.entries(e).forEach((([e,s])=>{s&&Object.entries(s).forEach((([s,a])=>{!function(e){return["get","put","post","delete","options","head","patch","trace"].includes(e)}(s)?console.log("等待处理其他方法",s):t.push(this.visit_operationObject(a,e,s))}))}));return await Promise.all(t)}async visit_operationObject(e,t,s){const a={id:`${y(s+"/"+t)}`,url:t,method:s,responses:[],description:e.description},r=e.parameters?.map((async e=>{if(d(e))throw Error("hand");if(e){const t=await this.visit_ParameterObjectAST(e);"path"===e.in&&t.schema&&(a.pathParams={...a.pathParams,[e.name]:t}),"query"===e.in&&t&&(a.queryParams={...a.queryParams,[e.name]:t}),"header"===e.in&&t&&(a.headers={...a.headers,[e.name]:t}),"cookie"===e.in&&console.error("skip cookie")}}))||[];await Promise.all(r),a.bodyParams=await this.visit_RequestBodyObject(e?.requestBody);const{responses:i}=e;if(i){const e=await this.visit_ResponseObject(i[200]||i.default);e&&a.responses?.push(e)}return a}async visit_ParameterObjectAST(e){return await this.visit_SchemaObject(e.schema)}async visit_refOrSchema(e){if(e){if(d(e))throw Error("Ref,解构失败");return await this.visit_SchemaObject(e)}}async visit_RequestBodyObject(e){if(!e)return;if(d(e))return void console.log("skip visit_RequestBodyObject isReferenceObject");const t={type:"json"};if(e.content["application/json"]){const s=e.content["application/json"];t.type="json",t.schema=await this.visit_SchemaObject(s.schema)}else if(e.content["multipart/form-data"]){const s=e.content["multipart/form-data"];t.type="formData",t.schema=await this.visit_SchemaObject(s.schema)}return t}async visit_ResponseObject(e){if(!e)return;if(d(e))return void console.log("skip visit_RequestBodyObject isReferenceObject");const t={status:200,type:"json"},s=e?.content;if(s)if(s["application/json"]||s["*/*"]){const e=s["application/json"]||s["*/*"];t.type="json",t.schema=await this.visit_refOrSchema(e.schema)}else if(s["text/javascript"]){const e=s["text/javascript"];t.type="javascript",t.schema=await this.visit_refOrSchema(e.schema)}else if(s["application/javascript"]){const e=s["application/javascript"];t.type="javascript",t.schema=await this.visit_refOrSchema(e.schema)}return t}}class A{async parse(e){const t=this.getParser(e);S("Cleaning ..."),await w.writeOutFolder(),S("Parsing ...");return await t.visit()}getParser(e){return e?.swagger?.startsWith("2.0")?new k(e):new x(e)}}class ${ast;constructor(e){this.ast=e,this.realAst=this.filterAst()}realAst;async visit(){await this.paint()}getSourceFile(){const e=a.readFileSync(t.join(__dirname,"../template/","apimock.tpl.ts"),"utf-8");return new r.Project({}).createSourceFile(u.getMockPath(),e,{overwrite:!0})}async paint(){const e=this.getSourceFile();if(this.realAst.requests.length){const t=e.getVariableDeclaration("apiRoutes");if(t){const e=t.getInitializerIfKind(r.SyntaxKind.ArrayLiteralExpression);e&&this.realAst.requests.forEach((t=>{const s=t.url,a=t.method,r=t.responses?.filter((e=>200===e.status))[0];if(r){const t="json"===r.type,i=`\n {\n path: '${s}',\n method: '${a}',\n handler: async (ctx) => {\n${`const s = ${c.parseSchema(r?.schema?.schema)};`}\n\nconst mockData = generateMock(s);\n\n\n${t?'ctx.res.headers.set("content-type", "application/json");':""}\n\n${t?"ctx.res.body = JSON.stringify(mockData);":"ctx.res.body = mockData;"}\n },\n }\n `;e.addElement(i)}}))}e.formatText()}await e.save()}filterAst(){if("boolean"!=typeof u.mock){const e=u.mock?.includePath||[],t=u.mock?.excludePath||[];if(!e?.length&&!t?.length)return this.ast;const s=[];this.ast.requests.forEach((a=>{const r=a.url;let i=!1,n=!1;e?.includes(r)&&(i=!0);for(const t of e)if(t.endsWith("*")){const e=t.slice(0,-1);if(r.startsWith(e)){i=!0;break}}t?.includes(r)&&(n=!0);for(const e of t)if(e.endsWith("*")){const t=e.slice(0,-1);if(r.startsWith(t)){n=!0;break}}!n&&i&&s.push(a)}));return{requests:s}}return{requests:[]}}}!async function(){try{console.time("Time"),u.loadRuntime();const{url:e}=u;let s=await async function(e){if(e.startsWith("http"))return await h(e).then((e=>e.json()));{let s=t.isAbsolute(e)?e:t.join(process.cwd(),e);const r=a.readFileSync(s,"utf-8");return JSON.parse(r)}}(e);s=await O(s,e);const r=new A,i=await r.parse(s);if(!(i.requests.length>0))throw Error("Has not ast request ");{if(u.diff){const{fork:e}=require("child_process");e(__dirname+"/process/diffProcess.js").send(JSON.stringify({config:u,ast:i}))}S("Generating ...");const e=new b(i),t=new $(i),s=e.visit(),a=t.visit();await s,await a,S("done ..."),console.timeEnd("Time")}}catch(e){console.error(e),process.exit()}}(),exports.defineConfig=function(e){return e};