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) 8.05 kB
#!/usr/bin/env node "use strict";var J=Object.create;var x=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var Q=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var K=(e,t)=>{for(var r in t)x(e,r,{get:t[r],enumerable:!0})},R=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of z(t))!_.call(e,n)&&n!==r&&x(e,n,{get:()=>t[n],enumerable:!(o=W(t,n))||o.enumerable});return e};var I=(e,t,r)=>(r=e!=null?J(Q(e)):{},R(t||!e||!e.__esModule?x(r,"default",{value:e,enumerable:!0}):r,e)),X=e=>R(x({},"__esModule",{value:!0}),e);var ge={};K(ge,{Commands:()=>U,cli:()=>F,resolveArgs:()=>k});module.exports=X(ge);var c=I(require("kleur/colors"),1),H=I(require("yargs-parser"),1);var w=I(require("fast-glob"),1),m=require("path"),P=require("fs"),b=require("ts-morph"),Y=require("chokidar"),y={apiDir:"./src/pages/api",baseUrl:"",outDir:"./src/pages/api-client",tsConfigPath:"./tsconfig.json",site:"http://localhost:3000"},O=["GET","POST","DELETE","PATCH","HEAD","PUT","OPTIONS","ALL"],S=["POST","DELETE","GET","PATCH","HEAD","PUT","OPTIONS"],C=e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}`,Z=e=>`${e.charAt(0).toLowerCase()}${e.slice(1)}`,D=e=>e.join(` `).trim(),ee=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?S:t},te=e=>({...y,...e});var re=(e,t)=>{let r=new b.Project({tsConfigFilePath:t}),o=[];return e.forEach(n=>{let p=r.getSourceFile(n),i=p.getImportDeclarations().map(a=>a.getFullText()),u=[],l="",f="";p.getInterfaces().map(a=>{switch(a.getName()){case"ApiRequest":l=a.getText();break;case"ApiResponse":f=a.getText();break;default:u.push(a.getText())}});let d=p.getTypeAliases().map(a=>a.getText()),g=[];p.getExportSymbols().forEach(a=>{let A=O.indexOf(a.getName().toUpperCase());if(A>-1){let s=O[A];s==="DELETE"?g.push("DELETE"):s==="ALL"?g=S:g.push(s)}}),g.forEach(a=>{o.push({apiRoute:n,imports:i,method:a,requestInterface:l,responseInterface:f,genericInterfaces:u,genericTypes:d})})}),o},oe=e=>{let t=[];return e.forEach(r=>{let o=(0,P.readFileSync)(r,{encoding:"utf-8"}),n=o.split(` `),p=[],i=[],u=[],l,f,d,g=0,a=[-1,-1],A=[-1,-1];n.forEach((s,h)=>{let N=/^import (.*) from (.*)/.test(s.trim()),T=s.indexOf("interface ")>-1,M=/^\s*(?:export\s+)?type\s+\w+\s*=\s*[\w<>,\s|]+\s*;?\s*$/.test(s.trim()),j=s.indexOf(" ApiRequest ")>-1,B=s.indexOf(" ApiResponse ")>-1,G=s.indexOf("{")>-1,V=s.indexOf("}")>-1;if(N){p.push(s);return}if(M){u.push(s);return}T&&j&&(f=h),T&&B&&(d=h),T&&f!==h&&d!==h&&(l=h),(f||d||l)&&G&&g++,(f||d||l)&&V&&(g--,g===0&&(f&&(a=[f,h],f=void 0),d&&(A=[d,h],d=void 0),l&&(i.push(D(n.slice(l,h+1))),l=void 0)))}),ee(o).forEach(s=>{t.push({apiRoute:r,imports:p,method:s,requestInterface:D(n.slice(a[0],a[1]+1)),responseInterface:D(n.slice(A[0],A[1]+1)),genericInterfaces:i,genericTypes:u})})}),t},ne=e=>e.reduce((t,r)=>{let{path:o}=r;return t[o]||(t[o]=[]),t[o].push(r),t},{}),v=e=>{console.log("\u2699\uFE0F Generating endpoint clients..."),e=te(e);let t=(0,m.resolve)(process.cwd(),e.apiDir),r=w.default.sync(`${t}/**/*.ts`),o;switch(e.parser){case"baseline":o=re(r,e.tsConfigPath);break;case"naive":default:o=oe(r);break}o=o.filter(i=>i.responseInterface!==""),o.map(i=>(i.relativePath=`api${i.apiRoute.replace(t,"")}`.trim(),i.path=`${(0,m.parse)(i.relativePath).dir}/${(0,m.parse)(i.relativePath).name}`,i.camelCaseName=i.path.replace(/^api/i,"").split("/").map(u=>C(u)).join("").split(/[-_\ \.]/g).reduce((u,l)=>u+C(l),""),i));let n=ne(o),p=[];Object.keys(n).forEach(i=>{let u=n[i],l=ae(u,e),f=(0,m.resolve)(process.cwd(),e.outDir,u[0].relativePath.replace(/^api\//,"")),d=(0,m.parse)(f),g=d.dir,a=`${g}${m.sep}${d.name}-client.ts`.toLowerCase();p.push(a);let A=new b.Project({tsConfigFilePath:e.tsConfigPath});(0,P.mkdirSync)(g,{recursive:!0});let s=A.createSourceFile(a,l,{overwrite:!0}),h;do h=s.getFullWidth(),s.fixUnusedIdentifiers();while(h!==s.getFullWidth());s.fixMissingImports(),s.organizeImports(),s.formatText(),s.saveSync()}),console.log("\u{1F680} Finished building endpoint clients.")},se=(e,t,r,o,n)=>{let p=n?C(e.method.toLowerCase()):"";return` /** return (await fetch('${t.baseUrl}/${e.path}', { method: '${e.method}', ... })).json() */ export const ${Z(e.camelCaseName)}${p} = async(${r}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}' ${o} return (await fetch(requestUrl, options)).json() }`},ie=(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}`,ae=(e,t)=>{let r=e[0].requestInterface?`${e[0].requestInterface}`:"",o=e[0].requestInterface?"payload: ApiRequest, ":"",n="";return e.forEach(p=>{let i=p.method!=="HEAD"&&p.method!=="GET"&&p.requestInterface?"options.body = JSON.stringify(payload)":"";n+=se(p,t,o,i,e.length>1)}),`${ie(e[0],r)} ${n}`};var E=require("path"),L=require("url"),q=require("fs/promises"),pe={},ce=async e=>JSON.parse(await(0,q.readFile)(e,{encoding:"utf-8"})),$=async()=>await ce((0,E.resolve)((0,E.parse)((0,L.fileURLToPath)(pe.url)).dir,"../package.json"));var U=(o=>(o.HELP="help",o.VERSION="version",o.GENERATE="generate",o))(U||{}),k=e=>{let t={apiDir:typeof e.apiDir=="string"?e.apiDir:y.apiDir,baseUrl:typeof e.baseUrl=="string"?e.baseUrl:y.baseUrl,outDir:typeof e.outDir=="string"?e.outDir:y.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}}},le=()=>{console.error(` ${c.bold("astro-client-generator")} - generates TypeScript clients for Astro endpoints ${c.bold("Commands:")} generate Generates the TypeScript clients for the endpoints. version Show the program version. help Show this help message. ${c.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. ${c.bold("Example(s):")} npx @jsheaven/astro-client-generator generate `)},ue=async()=>{console.log((await $()).version)},F=async e=>{let t=(0,H.default)(e),r=k(t),o={...r.options};switch(console.log(c.dim(">"),`${c.bold(c.yellow("astro-client-generator"))} @ ${c.dim((await $()).version)}: ${c.magenta(c.bold(r.cmd))}`,c.gray("...")),r.cmd){case"help":le(),process.exit(0);case"version":await ue(),process.exit(0);case"generate":{try{await v(o)}catch(n){de(n)}process.exit(0)}default:throw new Error(`Error running ${r.cmd}`)}},fe=e=>console.error(c.red(e.toString()||e)),de=e=>{fe(e),process.exit(1)};try{F(process.argv)}catch(e){console.error(e),process.exit(1)}0&&(module.exports={Commands,cli,resolveArgs}); //# sourceMappingURL=cli.cjs.js.map