@yunarch/config-web
Version:
Shared configurations for web projects.
144 lines (136 loc) • 7.59 kB
JavaScript
#!/usr/bin/env node
import{a as o,b as a,c as w}from"../chunk-LXZC4N54.js";import{existsSync as y}from"fs";import{mkdir as b,readFile as k,writeFile as O}from"fs/promises";import d from"path";import{styleText as n}from"util";import $ from"@inquirer/confirm";async function P(e,t){await a({name:"Generating models",command:async()=>{await o("npx",["openapi-typescript-codegen","--input",e,"--output",t,"--client","fetch"],{shell:!0})}})}import{writeFile as B}from"fs/promises";var x=`
import {
http as mswHttp,
type DefaultBodyType,
type HttpHandler,
type HttpResponseResolver,
type PathParams,
type RequestHandlerOptions,
} from 'msw';
import type { paths as ImportedPaths } from './schema';
// Type definitions
type Paths = ImportedPaths;
type HttpMethod =
| 'get'
| 'put'
| 'post'
| 'delete'
| 'options'
| 'head'
| 'patch'
| 'trace';
/**
* Type guard to get the http methods available for a given path.
*/
type Methods<Path extends keyof Paths> = {
[M in keyof Paths[Path]]: M extends HttpMethod
? Paths[Path][M] extends undefined
? never
: M
: never;
}[keyof Paths[Path]];
/**
* Type guard to get the content type 'application/json' or 'multipart/form-data' of a type.
*/
type ExtractContent<T> = T extends { content?: infer C }
? undefined extends C
? DefaultBodyType
: 'application/json' extends keyof C
? C['application/json']
: 'multipart/form-data' extends keyof C
? C['multipart/form-data']
: DefaultBodyType
: DefaultBodyType;
/**
* Type guard to get the parameters of a path.
*/
export type OpenapiPathParams<
P extends keyof Paths,
M extends keyof Paths[P],
> = 'parameters' extends keyof Paths[P][M]
? 'path' extends keyof Paths[P][M]['parameters']
? PathParams<keyof Paths[P][M]['parameters']['path']>
: PathParams
: PathParams;
/**
* Type guard to get the request body of a path.
*/
export type OpenapiPathRequestBody<
P extends keyof Paths,
M extends keyof Paths[P],
> = Paths[P][M] extends { requestBody?: infer RB }
? undefined extends RB
? DefaultBodyType
: ExtractContent<RB>
: DefaultBodyType;
/**
* Type guard to get the response body of a path.
*/
export type OpenapiPathResponseBody<
P extends keyof Paths,
M extends keyof Paths[P],
> = Paths[P][M] extends { responses?: infer R }
? undefined extends R
? DefaultBodyType
: 200 extends keyof R
? ExtractContent<R[200]>
: 201 extends keyof R
? ExtractContent<R[201]>
: DefaultBodyType
: DefaultBodyType;
/**
* Wrapper around MSW http function so we can have "typesafe" handlers against an openapi schema.
*
* @param path - The path to use from the openapi definition.
* @param method - The method to use on the handler.
* @param resolver - The MSW resolver function.
* @param options - The MSW http request handler options.
* @returns a typesafe wrapper for MSW http function.
*
* @throws Error if the method is not supported.
*/
export function http<P extends keyof Paths, M extends Methods<P>>(
path: P,
method: M,
resolver: HttpResponseResolver<
OpenapiPathParams<P, M>,
OpenapiPathRequestBody<P, M>,
OpenapiPathResponseBody<P, M>
>,
options?: RequestHandlerOptions
): HttpHandler {
const uri = \`*\${path.toString().replaceAll(/{(?<temp1>[^}]+)}/g, ':$1')}\`;
const handlers = {
head: mswHttp.head,
get: mswHttp.get,
post: mswHttp.post,
put: mswHttp.put,
delete: mswHttp.delete,
patch: mswHttp.patch,
options: mswHttp.options,
} as const;
if (typeof method !== 'string' || !Object.hasOwn(handlers, method)) {
throw new Error('Unsupported Http Method');
}
return handlers[method as keyof typeof handlers](uri, resolver, options);
}
`;async function T(e){await a({name:"Generating openapi MSW utils",command:async()=>{await B(`${e}/openapi-msw-http.ts`,x)}})}import{readFile as H,writeFile as E}from"fs/promises";async function M(e,t){await a({name:"Generating schema types",command:async()=>{await o("npx",["openapi-typescript",e,"-o",t],{shell:!0});let s=await H(t,"utf8");await E(t,`/* eslint-disable -- Autogenerated file */
${s}`)}})}async function C(e){if(d.extname(e)!=="")throw new Error("Output must be a directory.");let t=process.cwd(),s=d.resolve(e),r=s.startsWith(t)?s:d.resolve(t,d.relative(d.parse(e).root,e));return y(r)||await a({name:"Generating output directory",command:async()=>{await b(r,{recursive:!0})}}),r}async function S(e,t){let[s,i]=await Promise.all([a({name:"Reading input openapi schema",command:async()=>{if(!e.endsWith(".json"))throw new Error(`Input file must be a JSON file: ${e}`);if(e.startsWith("http"))try{let{stdout:r}=await o("curl",["-s",e,"--fail"],{encoding:"utf8"});return r}catch{throw new Error(`Failed to fetch remote OpenAPI file: ${e}`)}if(!y(e))throw new Error(`Input file does not exist: ${e}`);return await k(e,"utf8")}}),a({name:"Reading output openapi schema",command:async()=>{if(!t.endsWith(".json"))throw new Error(`Output file must be a JSON file: ${t}`);return y(t)?await k(t,"utf8"):!1}})]);return[JSON.stringify(JSON.parse(s)),i?JSON.stringify(JSON.parse(i)):!1]}w().name("openapi-sync").description("A CLI tool to convert OpenAPI 3.0/3.1 schemas to TypeScript types and create type-safe fetching based on a openapi file and keep them in sync.").requiredOption("-i, --input <path>","The input (local or remote) openapi schema (JSON).").requiredOption("-o, --output <folder>","The output folder to save the generated models and openapi schema and type definitions.").option("-y, --yes","Skip confirmation prompts and proceed with defaults.").option("-f, --force-gen","Force generation of typescript schemas and fetching code even if the input and output schemas are identical.").option("--include-msw-utils","Include MSW mocking utilities based on the generated typescript types.").option("--post-script <script>","A package.json script to run after the code generation.").option("--verify-openapi-sync","Verifies that the generated output is up to date with the input (e.g., in CI) to catch outdated or mismatched output without making changes.").addHelpText("after",`
Example usage:
${n("dim","$")} ${n("cyan","openapi-sync")} ${n("green","-i")} ${n("yellow","./openapi.json")} ${n("green","-o")} ${n("yellow","./src/api/gen")} ${n("green","--include-msw-utils")}
`).action(async({input:e,output:t,yes:s,forceGen:i,verifyOpenapiSync:r,includeMswUtils:R,postScript:m})=>{try{console.log(n("magenta",`
\u{1F680} openapi-sync
`));let p=await C(t),c=`${p}/openapi.json`,v=`${p}/schema.d.ts`,[u,g]=await S(e,c),h=u!==g;r&&(console.log(h?n("yellow",`
\u26A0\uFE0F Local and remote schemas does not match!
`):n("green",`
\u2705 Local and remote schemas match!
`)),process.exit(h?1:0)),g===!1&&await a({name:"Creating local schema",command:O(c,u)}),!h&&!i?(console.log(n("blue",`
No updates required.
`)),process.exit(0)):h&&(console.log(n("yellow",`
\u26A0\uFE0F Local and remote schemas does not match!
`)),s||await $({message:"Do you want to use the remote schema? (y/n)?"})?await a({name:"Replacing local schema with input schema",command:O(c,u)}):(console.log(n("yellow",`
\u26A0\uFE0F Sync remote schemas skipped.
`)),i||process.exit(0))),await Promise.all([M(c,v),P(c,p)]),R&&await T(p),m&&await a({name:"Running post script",command:async()=>{try{await(typeof Bun<"u"?o("bun",["run",m]):o("node",["--run",m]))}catch{await o("npm",["run",m],{shell:!0})}}}),console.log(n("green",`
\u2705 openapi-sync process completed!
`))}catch(p){console.error(p),process.exit(1)}}).parseAsync(process.argv);