UNPKG

@jsheaven/astro-client-generator

Version:

Generates TypeScript API client code for your Astro endpoints. No manual `fetch()` code writing anymore.

57 lines (46 loc) 7.35 kB
#!/usr/bin/env node import*as a from"kleur/colors";import Y from"yargs-parser";import q from"fast-glob";import{resolve as I,parse as x,sep as H}from"path";import{readFileSync as U,mkdirSync as k}from"fs";import{Project as C}from"ts-morph";import{watch as ue}from"chokidar";var A={apiDir:"./src/pages/api",baseUrl:"",outDir:"./src/pages/api-client",tsConfigPath:"./tsconfig.json",site:"http://localhost:3000"},D=["GET","POST","DELETE","PATCH","HEAD","PUT","OPTIONS","ALL"],b=["POST","DELETE","GET","PATCH","HEAD","PUT","OPTIONS"],E=e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}`,F=e=>`${e.charAt(0).toLowerCase()}${e.slice(1)}`,P=e=>e.join(` `).trim(),N=e=>{let t=[];return(e.indexOf("function GET")>-1||e.indexOf("export const GET")>-1)&&t.push("GET"),(e.indexOf("function POST")>-1||e.indexOf("export const POST")>-1)&&t.push("POST"),(e.indexOf("function DELETE")>-1||e.indexOf("export const DELETE")>-1)&&t.push("DELETE"),(e.indexOf("function PATCH")>-1||e.indexOf("export const PATCH")>-1)&&t.push("PATCH"),(e.indexOf("function HEAD")>-1||e.indexOf("export const HEAD")>-1)&&t.push("HEAD"),(e.indexOf("function PUT")>-1||e.indexOf("export const PUT")>-1)&&t.push("PUT"),(e.indexOf("function OPTIONS")>-1||e.indexOf("export const OPTIONS")>-1)&&t.push("OPTIONS"),e.indexOf("function ALL")>-1||e.indexOf("export const ALL")>-1?b:t},M=e=>({...A,...e});var j=(e,t)=>{let n=new C({tsConfigFilePath:t}),r=[];return e.forEach(c=>{let p=n.getSourceFile(c),s=p.getImportDeclarations().map(i=>i.getFullText()),u=[],l="",f="";p.getInterfaces().map(i=>{switch(i.getName()){case"ApiRequest":l=i.getText();break;case"ApiResponse":f=i.getText();break;default:u.push(i.getText())}});let d=p.getTypeAliases().map(i=>i.getText()),g=[];p.getExportSymbols().forEach(i=>{let m=D.indexOf(i.getName().toUpperCase());if(m>-1){let o=D[m];o==="DELETE"?g.push("DELETE"):o==="ALL"?g=b:g.push(o)}}),g.forEach(i=>{r.push({apiRoute:c,imports:s,method:i,requestInterface:l,responseInterface:f,genericInterfaces:u,genericTypes:d})})}),r},B=e=>{let t=[];return e.forEach(n=>{let r=U(n,{encoding:"utf-8"}),c=r.split(` `),p=[],s=[],u=[],l,f,d,g=0,i=[-1,-1],m=[-1,-1];c.forEach((o,h)=>{let R=/^import (.*) from (.*)/.test(o.trim()),y=o.indexOf("interface ")>-1,O=/^\s*(?:export\s+)?type\s+\w+\s*=\s*[\w<>,\s|]+\s*;?\s*$/.test(o.trim()),w=o.indexOf(" ApiRequest ")>-1,S=o.indexOf(" ApiResponse ")>-1,v=o.indexOf("{")>-1,L=o.indexOf("}")>-1;if(R){p.push(o);return}if(O){u.push(o);return}y&&w&&(f=h),y&&S&&(d=h),y&&f!==h&&d!==h&&(l=h),(f||d||l)&&v&&g++,(f||d||l)&&L&&(g--,g===0&&(f&&(i=[f,h],f=void 0),d&&(m=[d,h],d=void 0),l&&(s.push(P(c.slice(l,h+1))),l=void 0)))}),N(r).forEach(o=>{t.push({apiRoute:n,imports:p,method:o,requestInterface:P(c.slice(i[0],i[1]+1)),responseInterface:P(c.slice(m[0],m[1]+1)),genericInterfaces:s,genericTypes:u})})}),t},G=e=>e.reduce((t,n)=>{let{path:r}=n;return t[r]||(t[r]=[]),t[r].push(n),t},{}),$=e=>{console.log("\u2699\uFE0F Generating endpoint clients..."),e=M(e);let t=I(process.cwd(),e.apiDir),n=q.sync(`${t}/**/*.ts`),r;switch(e.parser){case"baseline":r=j(n,e.tsConfigPath);break;case"naive":default:r=B(n);break}r=r.filter(s=>s.responseInterface!==""),r.map(s=>(s.relativePath=`api${s.apiRoute.replace(t,"")}`.trim(),s.path=`${x(s.relativePath).dir}/${x(s.relativePath).name}`,s.camelCaseName=s.path.replace(/^api/i,"").split("/").map(u=>E(u)).join("").split(/[-_\ \.]/g).reduce((u,l)=>u+E(l),""),s));let c=G(r),p=[];Object.keys(c).forEach(s=>{let u=c[s],l=W(u,e),f=I(process.cwd(),e.outDir,u[0].relativePath.replace(/^api\//,"")),d=x(f),g=d.dir,i=`${g}${H}${d.name}-client.ts`.toLowerCase();p.push(i);let m=new C({tsConfigFilePath:e.tsConfigPath});k(g,{recursive:!0});let o=m.createSourceFile(i,l,{overwrite:!0}),h;do h=o.getFullWidth(),o.fixUnusedIdentifiers();while(h!==o.getFullWidth());o.fixMissingImports(),o.organizeImports(),o.formatText(),o.saveSync()}),console.log("\u{1F680} Finished building endpoint clients.")},V=(e,t,n,r,c)=>{let p=c?E(e.method.toLowerCase()):"";return` /** return (await fetch('${t.baseUrl}/${e.path}', { method: '${e.method}', ... })).json() */ export const ${F(e.camelCaseName)}${p} = async(${n}options: RequestOptions = {}): Promise<ApiResponse> => { let requestUrl = '${t.site}${t.baseUrl}/${e.path}' if (options && options.query) { requestUrl += '?' + Object.keys(options.query) .map((key) => key + '=' + options.query![key]) .join('&'); } delete options.query options.method = '${e.method}' ${r} return (await fetch(requestUrl, options)).json() }`},J=(e,t)=>`${e.imports.join(` `)} ${e.genericTypes.join(` `)} ${e.genericInterfaces.join(` `)} export interface QueryMap { [key: string]: string } export interface RequestOptions extends RequestInit { query?: QueryMap } ${t} ${e.responseInterface}`,W=(e,t)=>{let n=e[0].requestInterface?`${e[0].requestInterface}`:"",r=e[0].requestInterface?"payload: ApiRequest, ":"",c="";return e.forEach(p=>{let s=p.method!=="HEAD"&&p.method!=="GET"&&p.requestInterface?"options.body = JSON.stringify(payload)":"";c+=V(p,t,r,s,e.length>1)}),`${J(e[0],n)} ${c}`};import{resolve as z,parse as Q}from"path";import{fileURLToPath as _}from"url";import{readFile as K}from"fs/promises";var X=async e=>JSON.parse(await K(e,{encoding:"utf-8"})),T=async()=>await X(z(Q(_(import.meta.url)).dir,"../package.json"));var Z=(r=>(r.HELP="help",r.VERSION="version",r.GENERATE="generate",r))(Z||{}),ee=e=>{let t={apiDir:typeof e.apiDir=="string"?e.apiDir:A.apiDir,baseUrl:typeof e.baseUrl=="string"?e.baseUrl:A.baseUrl,outDir:typeof e.outDir=="string"?e.outDir:A.outDir};if(e.version)return{cmd:"version",options:t};if(e.help)return{cmd:"help",options:t};switch(e._[2]){case"help":return{cmd:"help",options:t};case"generate":return{cmd:"generate",options:t};default:return{cmd:"version",options:t}}},te=()=>{console.error(` ${a.bold("astro-client-generator")} - generates TypeScript clients for Astro endpoints ${a.bold("Commands:")} generate Generates the TypeScript clients for the endpoints. version Show the program version. help Show this help message. ${a.bold("Flags:")} --apiDir <string> Folder to the API directory on disk (source code), default: './src/pages/api' --baseUrl <string> API base URL for calling the API (only relevant if you host in a subdir, it's very unlikely), default: '' --outDir <string> Folder on disk to write the client code to, default: './src/pages/api-client' --version Show the version number and exit. --help Show this help message. ${a.bold("Example(s):")} npx @jsheaven/astro-client-generator generate `)},re=async()=>{console.log((await T()).version)},oe=async e=>{let t=Y(e),n=ee(t),r={...n.options};switch(console.log(a.dim(">"),`${a.bold(a.yellow("astro-client-generator"))} @ ${a.dim((await T()).version)}: ${a.magenta(a.bold(n.cmd))}`,a.gray("...")),n.cmd){case"help":te(),process.exit(0);case"version":await re(),process.exit(0);case"generate":{try{await $(r)}catch(c){se(c)}process.exit(0)}default:throw new Error(`Error running ${n.cmd}`)}},ne=e=>console.error(a.red(e.toString()||e)),se=e=>{ne(e),process.exit(1)};try{oe(process.argv)}catch(e){console.error(e),process.exit(1)}export{Z as Commands,oe as cli,ee as resolveArgs}; //# sourceMappingURL=cli.esm.js.map