@openmirai/typeforge 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ <p align="center">
2
+ <img src="./assets/typeforge-logo.png" alt="Typeforge logo" width="280" />
3
+ </p>
4
+
5
+ # @openmirai/typeforge
6
+
7
+ Headless **OpenAPI / Swagger → TypeScript** codegen. The CLI is `typeforge`. It reads a spec, writes typed route enums, request types, and HTTP caller functions, and never talks to a network.
8
+
9
+ - **npm:** [`@openmirai/typeforge`](https://www.npmjs.com/package/@openmirai/typeforge)
10
+ - **GitHub:** [openmirai/mirai-openapi-codegen](https://github.com/openmirai/mirai-openapi-codegen)
11
+
12
+ You own `http.ts` (the `HTTPFetch` adapter). Generated files import that adapter — they do not invent axios/fetch calls inline.
13
+
14
+ ## Migrating from `@openmirai/openapi-codegen`
15
+
16
+ Install `@openmirai/typeforge` and update package imports and scripts to use the canonical `typeforge` name. During migration, the package also exposes the legacy `openapi-codegen` binary and reads `openapi-codegen.json`, `openapi-codegen.local.json`, and the `openapiCodegen` package.json key. New projects created by `typeforge init` use the Typeforge names.
17
+
18
+ ## What it generates
19
+
20
+ For each **source** (a named API, e.g. `atlas`), under `<apiRoot>/<source>/generated/`:
21
+
22
+ | Output | Role |
23
+ | --- | --- |
24
+ | `types/**/*.d.ts` | Params, body, and response types per operation |
25
+ | `functions/**/*.ts` | Typed callers (`getWidgets`, …) |
26
+ | `routes.ts` | `Routes` string map + `RouteTargets` enum (name configurable) |
27
+ | `runtime.ts` | Re-exports `HTTPFetch`, `httpFetch` (if singleton), routes |
28
+ | `base.ts` | `BaseResponse<T>` when the spec uses a response envelope (or `base.d.ts` for split declaration output) |
29
+
30
+ Optional:
31
+
32
+ - **TanStack Query** — set `tanstackQuery: true` in `source.ts` **and** add `<apiRoot>/query-scope.ts`.
33
+ - **Zod** — wrap a schema with `createZodValidator` from `@openmirai/typeforge/validation/zod` and pass it as `config.validateResponse`.
34
+
35
+ ## Install
36
+
37
+ Requires **Node.js 20.11+** (LTS). Use any package manager.
38
+
39
+ | Package manager | Install |
40
+ | --- | --- |
41
+ | npm | `npm install --save-dev @openmirai/typeforge` |
42
+ | pnpm | `pnpm add -D @openmirai/typeforge` |
43
+ | yarn | `yarn add -D @openmirai/typeforge` |
44
+ | bun | `bun add -d @openmirai/typeforge` |
45
+
46
+ Axios is an **optional peer**. Install `axios` only if you use `--client axios`.
47
+
48
+ Add a script so every package manager resolves the CLI from `node_modules/.bin`:
49
+
50
+ ```json
51
+ {
52
+ "scripts": {
53
+ "generate:types": "typeforge generate --all"
54
+ }
55
+ }
56
+ ```
57
+
58
+ Then run `npm run generate:types`, `pnpm run generate:types`, `yarn generate:types`, or `bun run generate:types`.
59
+
60
+ ## CLI usage
61
+
62
+ Prefer the `package.json` script above. To invoke the binary directly:
63
+
64
+ | Command | npm | pnpm | yarn | bun |
65
+ | --- | --- | --- | --- | --- |
66
+ | Init a source | `npx typeforge init --source atlas --client axios` | `pnpm exec typeforge init --source atlas --client axios` | `yarn typeforge init --source atlas --client axios` | `bunx typeforge init --source atlas --client axios` |
67
+ | Generate one source | `npx typeforge generate --source atlas` | `pnpm exec typeforge generate --source atlas` | `yarn typeforge generate --source atlas` | `bunx typeforge generate --source atlas` |
68
+ | Generate all sources | `npx typeforge generate --all` | `pnpm exec typeforge generate --all` | `yarn typeforge generate --all` | `bunx typeforge generate --all` |
69
+ | Drift check (CI) | `npx typeforge generate --all --check` | `pnpm exec typeforge generate --all --check` | `yarn typeforge generate --all --check` | `bunx typeforge generate --all --check` |
70
+
71
+ | Subcommand | Purpose |
72
+ | --- | --- |
73
+ | `init` | Scaffold `http.ts`, `source.ts`, `known-types.ts` |
74
+ | `generate` | Write generated files |
75
+ | `check` | Same as `generate --check` — exit 1 if output would change |
76
+ | `accept-base` | Update generated `base.ts` (`base.d.ts` for split declaration output) and patch `models.ts` `BaseResponse` |
77
+
78
+ `--check` and `--accept-base` cannot be combined. See [docs/cli.md](docs/cli.md) for the full command reference.
79
+
80
+ ## How the flow works
81
+
82
+ ```text
83
+ init → source.ts + http.ts → resolve spec → generate → typed callers
84
+ ```
85
+
86
+ ### 1. Init a source
87
+
88
+ ```bash
89
+ typeforge init --source atlas --client axios
90
+ typeforge init --source orbit --client fetch --layout packages
91
+ ```
92
+
93
+ `--client` is `axios` | `fetch` | `custom`. `--layout` is `monolith` (default, `apiRoot` = `src/api`) or `packages` (`apiRoot` = `packages/utils/src/api`).
94
+
95
+ Init creates (if missing):
96
+
97
+ - `typeforge.json` with `apiRoot`
98
+ - `<apiRoot>/http.ts` — your `HTTPFetch` implementation
99
+ - `<apiRoot>/known-types.ts` — optional schema → local type mapping
100
+ - `<apiRoot>/<source>/source.ts` — per-API config (type-safe template)
101
+ - `<apiRoot>/<source>/generated/` directory
102
+
103
+ Existing files are skipped.
104
+
105
+ ### 2. Configure `source.ts`
106
+
107
+ Use `defineSourceConfig` for autocomplete and compile-time checks:
108
+
109
+ ```ts
110
+ import { defineSourceConfig } from "@openmirai/typeforge";
111
+
112
+ export default defineSourceConfig({
113
+ spec: "./specs/acme.json",
114
+ functionsDir: "packages/utils/src/api/routes/atlas",
115
+ typesDir: "packages/types/src/api/atlas",
116
+ pathPrefix: "/api/acme/v3",
117
+ stripApiPrefix: true,
118
+ routeEnumName: "RouteTargets",
119
+ generationMode: "authoritative",
120
+ naming: "path",
121
+ ignorePaths: [],
122
+ maxRenderDepth: 50,
123
+ resolveMapKeyRefs: true,
124
+ tanstackQuery: false,
125
+ queryExtends: {
126
+ page: "page",
127
+ limit: "limit",
128
+ sortBy: "sortBy",
129
+ sortOrder: "sortOrder",
130
+ paginationTypeName: "OffsetLimitQuery",
131
+ paginationImportPath: "./pagination",
132
+ sortTypeName: "SortParams",
133
+ sortImportPath: "./pagination",
134
+ },
135
+ });
136
+ ```
137
+
138
+ Plain `export default { ... }` still works; the CLI reads config fields from the file at generate time.
139
+
140
+ Re-exported types from the package root:
141
+
142
+ - `SourceConfig`, `QueryExtendsConfig`, `GenerationMode`, `NamingStrategy`
143
+ - `defineSourceConfig(config)` — identity helper for typed `source.ts`
144
+
145
+ | Field | Meaning |
146
+ | --- | --- |
147
+ | `spec` | Project-relative spec path (used when no `--spec` / env override) |
148
+ | `functionsDir` | Project-relative function output directory (defaults to the source's `generated/functions`) |
149
+ | `typesDir` | Project-relative type output directory (defaults to the source's `generated/types`; a generated `base.d.ts` is placed beside this directory when customized) |
150
+ | `pathPrefix` | Only generate operations under this prefix (e.g. `/api/acme/v3`) |
151
+ | `ignorePaths` | Extra paths to skip |
152
+ | `stripApiPrefix` | Strip a leading `/api` segment from route enum member names |
153
+ | `routeEnumName` | Enum name (default `RouteTargets`) |
154
+ | `generationMode` | `authoritative` (overwrite routes) or `merge` (keep extra enum members) |
155
+ | `naming` | `path` or `operationId` for function names |
156
+ | `queryExtends` | Fold page/limit/sort query params into shared pagination types |
157
+ | `tanstackQuery` | Emit Query helpers when `query-scope.ts` exists |
158
+ | `importBase` | Force import prefix for generated function files (overrides tsconfig aliases) |
159
+ | `maxRenderDepth` / `resolveMapKeyRefs` | Schema renderer limits |
160
+ | `unwrapResponseData` | Emit an envelope's `data` schema as the operation response type when the project's `HTTPFetch` already unwraps envelopes |
161
+
162
+ ### 3. Spec resolution (first match wins)
163
+
164
+ 1. `--spec <path>`
165
+ 2. Env `OPENAPI_SPEC_<KEY>` — source key uppercased, hyphens → underscores
166
+ 3. `spec` in that source’s `source.ts`
167
+ 4. `typeforge.local.json` (gitignored) map of `{ "<source>": "<path>" }`
168
+ 5. Committed snapshot `<apiRoot>/<source>/spec.json`
169
+
170
+ ### 4. Envelope modes
171
+
172
+ Inferred from success response schemas. Details: [docs/envelope.md](docs/envelope.md).
173
+
174
+ | Mode | When | Types |
175
+ | --- | --- | --- |
176
+ | **shared** | One envelope shape (`data` / `success` / `message`) | `BaseResponse<Unwrapped>` |
177
+ | **raw** | No shared envelope | Spec schema as-is |
178
+ | **mixed** | Some ops have `data`, others do not | Unwrap **per operation** when `data` exists |
179
+
180
+ Set `unwrapResponseData: true` when the project's injected `HTTPFetch`
181
+ normalizes successful envelope bodies before returning `{ data }`. Every
182
+ operation whose success schema is recognized as an API envelope then receives
183
+ its `data` payload type. Data-only objects and business payloads that also
184
+ contain `success` remain raw. Metadata-only envelopes without a `data` field
185
+ receive the `null` type,
186
+ matching clients that normalize an omitted payload to `null`.
187
+ The default remains envelope-preserving and is compatible with the bundled
188
+ Axios and Fetch adapters.
189
+
190
+ ### 5. HTTPFetch (`http.ts`)
191
+
192
+ Adapters implement `HTTPFetch` from `@openmirai/typeforge/http` (or the axios/fetch adapter packages). Methods return `Promise<{ data: TResponse }>`.
193
+
194
+ - If `http.ts` **exports `httpFetch`**, generated functions call that singleton.
195
+ - Otherwise they take `props.http: HTTPFetch` (injected).
196
+
197
+ ### 6. Path-alias aware imports
198
+
199
+ Generated function files import types and `runtime`, and generated response
200
+ types import the generated base declaration (`base.ts` in monolith output, or
201
+ `base.d.ts` for split declaration output), using:
202
+
203
+ 1. `importBase` in `source.ts`, if set
204
+ 2. Else `compilerOptions.paths` from the nearest ancestor `tsconfig.json` with path aliases, starting at the corresponding `functionsDir` or `typesDir`
205
+ 3. Else relative paths (`../../runtime`)
206
+
207
+ ## Where files go
208
+
209
+ `typeforge.json`:
210
+
211
+ ```json
212
+ { "apiRoot": "packages/utils/src/api" }
213
+ ```
214
+
215
+ You can also set `"typeforge": { "apiRoot": "..." }` in `package.json`. The JSON file wins.
216
+
217
+ **Monolith** (`--layout monolith`, default):
218
+
219
+ ```text
220
+ src/api/http.ts
221
+ src/api/known-types.ts
222
+ src/api/models.ts # optional BaseResponse drift check
223
+ src/api/query-scope.ts # optional TanStack
224
+ src/api/atlas/source.ts
225
+ src/api/atlas/spec.json # optional snapshot
226
+ src/api/atlas/generated/…
227
+ ```
228
+
229
+ **Packages layout** (`--layout packages`): typical placement is `packages/utils/src/api/<source>/`.
230
+
231
+ Set `functionsDir` and `typesDir` when callers and declarations belong in
232
+ different packages. Relative imports continue to work without aliases; when a
233
+ nearby `tsconfig.json` maps both output roots, deep generated imports use those
234
+ aliases automatically.
235
+
236
+ ## Zod (optional)
237
+
238
+ ```ts
239
+ import { createZodValidator } from "@openmirai/typeforge/validation/zod";
240
+ import { widgetListSchema } from "./widget-list";
241
+
242
+ await getWidgets({
243
+ params: { page: 1, limit: 20 },
244
+ config: { validateResponse: createZodValidator(widgetListSchema) },
245
+ });
246
+ ```
247
+
248
+ ## Releasing
249
+
250
+ Publishes go through [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) (GitHub Actions OIDC). Do not `npm publish` from a laptop.
251
+
252
+ | | Value |
253
+ | --- | --- |
254
+ | npm package | `@openmirai/typeforge` |
255
+ | GitHub repo | `openmirai/mirai-openapi-codegen` |
256
+ | Workflow | `.github/workflows/publish.yml` |
257
+ | Tag | `v*` (e.g. `v0.1.3`) |
258
+
259
+ ## Develop this repo
260
+
261
+ This repository uses **pnpm** for its own CI. Consumers are not required to use pnpm.
262
+
263
+ ```bash
264
+ pnpm install
265
+ pnpm verify # format, lint, typecheck, build, coverage
266
+ ```
267
+
268
+ Test layout: [test/README.md](test/README.md).
Binary file
@@ -0,0 +1,38 @@
1
+ import { AxiosInstance } from "axios";
2
+ //#region src/json/types.d.ts
3
+ type QueryParamValue = string | number | boolean | null | undefined;
4
+ type QueryParams = Record<string, QueryParamValue>;
5
+ //#endregion
6
+ //#region src/http/validate.d.ts
7
+ type ResponseValidator<T> = (value: unknown) => T;
8
+ //#endregion
9
+ //#region src/http/types.d.ts
10
+ interface HTTPFetchConfig<TParams extends object = QueryParams, TResponse = unknown> {
11
+ signal?: AbortSignal;
12
+ params?: TParams;
13
+ headers?: Record<string, string>;
14
+ validateResponse?: ResponseValidator<TResponse>;
15
+ }
16
+ interface HTTPFetch {
17
+ get<TResponse, TParams extends object = QueryParams>(route: string, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
18
+ data: TResponse;
19
+ }>;
20
+ post<TResponse, TBody = unknown, TParams extends object = QueryParams>(route: string, body: TBody, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
21
+ data: TResponse;
22
+ }>;
23
+ put<TResponse, TBody = unknown, TParams extends object = QueryParams>(route: string, body: TBody, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
24
+ data: TResponse;
25
+ }>;
26
+ patch<TResponse, TBody = unknown, TParams extends object = QueryParams>(route: string, body: TBody, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
27
+ data: TResponse;
28
+ }>;
29
+ delete<TResponse, TParams extends object = QueryParams>(route: string, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
30
+ data: TResponse;
31
+ }>;
32
+ }
33
+ //#endregion
34
+ //#region src/adapters/axios/index.d.ts
35
+ declare function createAxiosAdapter(instance: AxiosInstance): HTTPFetch;
36
+ //#endregion
37
+ export { type HTTPFetch, type HTTPFetchConfig, createAxiosAdapter };
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/json/types.ts","../../../src/http/validate.ts","../../../src/http/types.ts","../../../src/adapters/axios/index.ts"],"mappings":";;KAUY;KAEA,cAAc,eAAe;;;KCZ7B,kBAAkB,MAAM,mBAAmB;;;UCOtC,gBACf,yBAAyB,aACzB;EAEA,SAAS;EACT,SAAS;EACT,UAAU;EACV,mBAAmB,kBAAkB;;UAGtB;EACf,IAAI,WAAW,yBAAyB,aACtC,eACA,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,KAAK,WAAW,iBAAiB,yBAAyB,aACxD,eACA,MAAM,OACN,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,IAAI,WAAW,iBAAiB,yBAAyB,aACvD,eACA,MAAM,OACN,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,MAAM,WAAW,iBAAiB,yBAAyB,aACzD,eACA,MAAM,OACN,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,OAAO,WAAW,yBAAyB,aACzC,eACA,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;;;;iBCLL,mBAAmB,UAAU,gBAAgB"}
@@ -0,0 +1,43 @@
1
+ //#region src/http/validate.ts
2
+ var ResponseValidationError = class extends Error {
3
+ cause;
4
+ constructor(message, cause) {
5
+ super(message);
6
+ this.name = "ResponseValidationError";
7
+ this.cause = cause;
8
+ }
9
+ };
10
+ function coerceResponseData(value, validator) {
11
+ if (validator === void 0) return value;
12
+ try {
13
+ return validator(value);
14
+ } catch (error) {
15
+ throw new ResponseValidationError("Response validation failed", error);
16
+ }
17
+ }
18
+ //#endregion
19
+ //#region src/adapters/axios/index.ts
20
+ function toAxiosConfig(config) {
21
+ if (config === void 0) return;
22
+ const axiosConfig = {};
23
+ if (config.signal !== void 0) axiosConfig.signal = config.signal;
24
+ if (config.params !== void 0) axiosConfig.params = config.params;
25
+ if (config.headers !== void 0) axiosConfig.headers = config.headers;
26
+ return axiosConfig;
27
+ }
28
+ function mapResponse(data, config) {
29
+ return { data: coerceResponseData(data, config?.validateResponse) };
30
+ }
31
+ function createAxiosAdapter(instance) {
32
+ return {
33
+ delete: (route, config) => instance.delete(route, toAxiosConfig(config)).then((response) => mapResponse(response.data, config)),
34
+ get: (route, config) => instance.get(route, toAxiosConfig(config)).then((response) => mapResponse(response.data, config)),
35
+ patch: (route, body, config) => instance.patch(route, body, toAxiosConfig(config)).then((response) => mapResponse(response.data, config)),
36
+ post: (route, body, config) => instance.post(route, body, toAxiosConfig(config)).then((response) => mapResponse(response.data, config)),
37
+ put: (route, body, config) => instance.put(route, body, toAxiosConfig(config)).then((response) => mapResponse(response.data, config))
38
+ };
39
+ }
40
+ //#endregion
41
+ export { createAxiosAdapter };
42
+
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/http/validate.ts","../../../src/adapters/axios/index.ts"],"sourcesContent":["export type ResponseValidator<T> = (value: unknown) => T;\n\nexport class ResponseValidationError extends Error {\n override readonly cause: unknown;\n\n constructor(message: string, cause: unknown) {\n super(message);\n this.name = \"ResponseValidationError\";\n this.cause = cause;\n }\n}\n\nexport function coerceResponseData<T>(\n value: unknown,\n validator?: ResponseValidator<T>\n): T {\n if (validator === undefined) {\n return value as T;\n }\n\n try {\n return validator(value);\n } catch (error) {\n throw new ResponseValidationError(\"Response validation failed\", error);\n }\n}\n","import type { AxiosInstance, AxiosRequestConfig } from \"axios\";\n\nimport { coerceResponseData } from \"../../http/validate\";\nimport type { HTTPFetch, HTTPFetchConfig } from \"../../http/types\";\nimport type { QueryParams } from \"../../json/types\";\n\nfunction toAxiosConfig<TParams extends object>(\n config?: HTTPFetchConfig<TParams, unknown>\n): AxiosRequestConfig | undefined {\n if (config === undefined) {\n return undefined;\n }\n\n const axiosConfig: AxiosRequestConfig = {};\n if (config.signal !== undefined) {\n axiosConfig.signal = config.signal;\n }\n if (config.params !== undefined) {\n axiosConfig.params = config.params;\n }\n if (config.headers !== undefined) {\n axiosConfig.headers = config.headers;\n }\n return axiosConfig;\n}\n\nfunction mapResponse<TResponse, TParams extends object>(\n data: unknown,\n config?: HTTPFetchConfig<TParams, TResponse>\n): { data: TResponse } {\n return {\n data: coerceResponseData<TResponse>(data, config?.validateResponse),\n };\n}\n\nexport function createAxiosAdapter(instance: AxiosInstance): HTTPFetch {\n return {\n delete: <TResponse, TParams extends object = QueryParams>(\n route: string,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) =>\n instance\n .delete<TResponse>(route, toAxiosConfig(config))\n .then((response) => mapResponse(response.data, config)),\n get: <TResponse, TParams extends object = QueryParams>(\n route: string,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) =>\n instance\n .get<TResponse>(route, toAxiosConfig(config))\n .then((response) => mapResponse(response.data, config)),\n patch: <TResponse, TBody = unknown, TParams extends object = QueryParams>(\n route: string,\n body: TBody,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) =>\n instance\n .patch<TResponse>(route, body, toAxiosConfig(config))\n .then((response) => mapResponse(response.data, config)),\n post: <TResponse, TBody = unknown, TParams extends object = QueryParams>(\n route: string,\n body: TBody,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) =>\n instance\n .post<TResponse>(route, body, toAxiosConfig(config))\n .then((response) => mapResponse(response.data, config)),\n put: <TResponse, TBody = unknown, TParams extends object = QueryParams>(\n route: string,\n body: TBody,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) =>\n instance\n .put<TResponse>(route, body, toAxiosConfig(config))\n .then((response) => mapResponse(response.data, config)),\n };\n}\n\nexport type { HTTPFetch, HTTPFetchConfig } from \"../../http/types\";\n"],"mappings":";AAEA,IAAa,0BAAb,cAA6C,MAAM;CACjD;CAEA,YAAY,SAAiB,OAAgB;EAC3C,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,QAAQ;CACf;AACF;AAEA,SAAgB,mBACd,OACA,WACG;CACH,IAAI,cAAc,KAAA,GAChB,OAAO;CAGT,IAAI;EACF,OAAO,UAAU,KAAK;CACxB,SAAS,OAAO;EACd,MAAM,IAAI,wBAAwB,8BAA8B,KAAK;CACvE;AACF;;;ACnBA,SAAS,cACP,QACgC;CAChC,IAAI,WAAW,KAAA,GACb;CAGF,MAAM,cAAkC,CAAC;CACzC,IAAI,OAAO,WAAW,KAAA,GACpB,YAAY,SAAS,OAAO;CAE9B,IAAI,OAAO,WAAW,KAAA,GACpB,YAAY,SAAS,OAAO;CAE9B,IAAI,OAAO,YAAY,KAAA,GACrB,YAAY,UAAU,OAAO;CAE/B,OAAO;AACT;AAEA,SAAS,YACP,MACA,QACqB;CACrB,OAAO,EACL,MAAM,mBAA8B,MAAM,QAAQ,gBAAgB,EACpE;AACF;AAEA,SAAgB,mBAAmB,UAAoC;CACrE,OAAO;EACL,SACE,OACA,WAEA,SACG,OAAkB,OAAO,cAAc,MAAM,CAAC,CAAC,CAC/C,MAAM,aAAa,YAAY,SAAS,MAAM,MAAM,CAAC;EAC1D,MACE,OACA,WAEA,SACG,IAAe,OAAO,cAAc,MAAM,CAAC,CAAC,CAC5C,MAAM,aAAa,YAAY,SAAS,MAAM,MAAM,CAAC;EAC1D,QACE,OACA,MACA,WAEA,SACG,MAAiB,OAAO,MAAM,cAAc,MAAM,CAAC,CAAC,CACpD,MAAM,aAAa,YAAY,SAAS,MAAM,MAAM,CAAC;EAC1D,OACE,OACA,MACA,WAEA,SACG,KAAgB,OAAO,MAAM,cAAc,MAAM,CAAC,CAAC,CACnD,MAAM,aAAa,YAAY,SAAS,MAAM,MAAM,CAAC;EAC1D,MACE,OACA,MACA,WAEA,SACG,IAAe,OAAO,MAAM,cAAc,MAAM,CAAC,CAAC,CAClD,MAAM,aAAa,YAAY,SAAS,MAAM,MAAM,CAAC;CAC5D;AACF"}
@@ -0,0 +1,42 @@
1
+ //#region src/json/types.d.ts
2
+ type QueryParamValue = string | number | boolean | null | undefined;
3
+ type QueryParams = Record<string, QueryParamValue>;
4
+ //#endregion
5
+ //#region src/http/validate.d.ts
6
+ type ResponseValidator<T> = (value: unknown) => T;
7
+ //#endregion
8
+ //#region src/http/types.d.ts
9
+ interface HTTPFetchConfig<TParams extends object = QueryParams, TResponse = unknown> {
10
+ signal?: AbortSignal;
11
+ params?: TParams;
12
+ headers?: Record<string, string>;
13
+ validateResponse?: ResponseValidator<TResponse>;
14
+ }
15
+ interface HTTPFetch {
16
+ get<TResponse, TParams extends object = QueryParams>(route: string, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
17
+ data: TResponse;
18
+ }>;
19
+ post<TResponse, TBody = unknown, TParams extends object = QueryParams>(route: string, body: TBody, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
20
+ data: TResponse;
21
+ }>;
22
+ put<TResponse, TBody = unknown, TParams extends object = QueryParams>(route: string, body: TBody, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
23
+ data: TResponse;
24
+ }>;
25
+ patch<TResponse, TBody = unknown, TParams extends object = QueryParams>(route: string, body: TBody, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
26
+ data: TResponse;
27
+ }>;
28
+ delete<TResponse, TParams extends object = QueryParams>(route: string, config?: HTTPFetchConfig<TParams, TResponse>): Promise<{
29
+ data: TResponse;
30
+ }>;
31
+ }
32
+ //#endregion
33
+ //#region src/adapters/fetch/index.d.ts
34
+ interface FetchAdapterOptions {
35
+ baseURL?: string;
36
+ headers?: Record<string, string>;
37
+ fetch?: typeof fetch;
38
+ }
39
+ declare function createFetchAdapter(options?: FetchAdapterOptions): HTTPFetch;
40
+ //#endregion
41
+ export { FetchAdapterOptions, type HTTPFetch, type HTTPFetchConfig, createFetchAdapter };
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/json/types.ts","../../../src/http/validate.ts","../../../src/http/types.ts","../../../src/adapters/fetch/index.ts"],"mappings":";KAUY;KAEA,cAAc,eAAe;;;KCZ7B,kBAAkB,MAAM,mBAAmB;;;UCOtC,gBACf,yBAAyB,aACzB;EAEA,SAAS;EACT,SAAS;EACT,UAAU;EACV,mBAAmB,kBAAkB;;UAGtB;EACf,IAAI,WAAW,yBAAyB,aACtC,eACA,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,KAAK,WAAW,iBAAiB,yBAAyB,aACxD,eACA,MAAM,OACN,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,IAAI,WAAW,iBAAiB,yBAAyB,aACvD,eACA,MAAM,OACN,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,MAAM,WAAW,iBAAiB,yBAAyB,aACzD,eACA,MAAM,OACN,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;EACnB,OAAO,WAAW,yBAAyB,aACzC,eACA,SAAS,gBAAgB,SAAS,aACjC;IAAU,MAAM;;;;;UCnCJ;EACf;EACA,UAAU;EACV,eAAe;;iBAqED,mBACd,UAAS,sBACR"}
@@ -0,0 +1,77 @@
1
+ //#region src/json/types.ts
2
+ function isJsonValue(value) {
3
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
4
+ if (Array.isArray(value)) return value.every(isJsonValue);
5
+ if (typeof value === "object" && value !== null) return Object.values(value).every(isJsonValue);
6
+ return false;
7
+ }
8
+ function parseJson(text) {
9
+ const parsed = JSON.parse(text);
10
+ if (!isJsonValue(parsed)) throw new TypeError("JSON text did not parse to a valid JSON value");
11
+ return parsed;
12
+ }
13
+ //#endregion
14
+ //#region src/http/validate.ts
15
+ var ResponseValidationError = class extends Error {
16
+ cause;
17
+ constructor(message, cause) {
18
+ super(message);
19
+ this.name = "ResponseValidationError";
20
+ this.cause = cause;
21
+ }
22
+ };
23
+ function coerceResponseData(value, validator) {
24
+ if (validator === void 0) return value;
25
+ try {
26
+ return validator(value);
27
+ } catch (error) {
28
+ throw new ResponseValidationError("Response validation failed", error);
29
+ }
30
+ }
31
+ //#endregion
32
+ //#region src/adapters/fetch/index.ts
33
+ function appendQuery(url, params) {
34
+ if (params === void 0 || Object.keys(params).length === 0) return url;
35
+ const search = new URLSearchParams();
36
+ for (const [key, value] of Object.entries(params)) search.append(key, String(value));
37
+ const query = search.toString();
38
+ if (query.length === 0) return url;
39
+ return `${url}${url.includes("?") ? "&" : "?"}${query}`;
40
+ }
41
+ function readResponseBody(text) {
42
+ if (text.length === 0) return;
43
+ return parseJson(text);
44
+ }
45
+ async function request(method, route, options, body, config) {
46
+ const fetchImpl = options.fetch ?? globalThis.fetch;
47
+ const url = appendQuery(`${options.baseURL ?? ""}${route}`, config?.params);
48
+ const headers = {
49
+ ...options.headers,
50
+ ...config?.headers
51
+ };
52
+ const init = {
53
+ headers,
54
+ method
55
+ };
56
+ if (config?.signal !== void 0) init.signal = config.signal;
57
+ if (body !== void 0) {
58
+ headers["Content-Type"] ??= "application/json";
59
+ init.body = JSON.stringify(body);
60
+ }
61
+ const response = await fetchImpl(url, init);
62
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
63
+ return { data: coerceResponseData(readResponseBody(await response.text()), config?.validateResponse) };
64
+ }
65
+ function createFetchAdapter(options = {}) {
66
+ return {
67
+ delete: (route, config) => request("DELETE", route, options, void 0, config),
68
+ get: (route, config) => request("GET", route, options, void 0, config),
69
+ patch: (route, body, config) => request("PATCH", route, options, body, config),
70
+ post: (route, body, config) => request("POST", route, options, body, config),
71
+ put: (route, body, config) => request("PUT", route, options, body, config)
72
+ };
73
+ }
74
+ //#endregion
75
+ export { createFetchAdapter };
76
+
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/json/types.ts","../../../src/http/validate.ts","../../../src/adapters/fetch/index.ts"],"sourcesContent":["export type JsonPrimitive = string | number | boolean | null;\n\nexport interface JsonObject {\n [key: string]: JsonValue;\n}\n\nexport type JsonArray = Array<JsonValue>;\n\nexport type JsonValue = JsonPrimitive | JsonObject | JsonArray;\n\nexport type QueryParamValue = string | number | boolean | null | undefined;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function isJsonPrimitive(value: JsonValue): value is JsonPrimitive {\n return (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n );\n}\n\nexport function isJsonValue(value: unknown): value is JsonValue {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return true;\n }\n\n if (Array.isArray(value)) {\n return value.every(isJsonValue);\n }\n\n if (typeof value === \"object\" && value !== null) {\n return Object.values(value).every(isJsonValue);\n }\n\n return false;\n}\n\nexport function isJsonObject(value: unknown): value is JsonObject {\n return (\n isJsonValue(value) &&\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value)\n );\n}\n\nexport function isJsonArray(value: unknown): value is JsonArray {\n return Array.isArray(value) && value.every(isJsonValue);\n}\n\nexport function parseJson(text: string): JsonValue {\n const parsed: unknown = JSON.parse(text);\n if (!isJsonValue(parsed)) {\n throw new TypeError(\"JSON text did not parse to a valid JSON value\");\n }\n return parsed;\n}\n\nexport function readJsonObject(text: string): JsonObject {\n const parsed = parseJson(text);\n if (!isJsonObject(parsed)) {\n throw new TypeError(\"JSON text did not parse to a JSON object\");\n }\n return parsed;\n}\n\nexport function readJsonPrimitives(\n values: JsonValue | undefined\n): Array<JsonPrimitive> {\n if (!isJsonArray(values)) {\n return [];\n }\n\n return values.filter(isJsonPrimitive);\n}\n","export type ResponseValidator<T> = (value: unknown) => T;\n\nexport class ResponseValidationError extends Error {\n override readonly cause: unknown;\n\n constructor(message: string, cause: unknown) {\n super(message);\n this.name = \"ResponseValidationError\";\n this.cause = cause;\n }\n}\n\nexport function coerceResponseData<T>(\n value: unknown,\n validator?: ResponseValidator<T>\n): T {\n if (validator === undefined) {\n return value as T;\n }\n\n try {\n return validator(value);\n } catch (error) {\n throw new ResponseValidationError(\"Response validation failed\", error);\n }\n}\n","import type { JsonValue, QueryParams } from \"../../json/types\";\nimport { parseJson } from \"../../json/types\";\nimport { coerceResponseData } from \"../../http/validate\";\nimport type { HTTPFetch, HTTPFetchConfig } from \"../../http/types\";\n\nexport interface FetchAdapterOptions {\n baseURL?: string;\n headers?: Record<string, string>;\n fetch?: typeof fetch;\n}\n\nfunction appendQuery(url: string, params?: object): string {\n if (params === undefined || Object.keys(params).length === 0) {\n return url;\n }\n\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n search.append(key, String(value));\n }\n\n const query = search.toString();\n if (query.length === 0) {\n return url;\n }\n\n return `${url}${url.includes(\"?\") ? \"&\" : \"?\"}${query}`;\n}\n\nfunction readResponseBody(text: string): JsonValue | undefined {\n if (text.length === 0) {\n return undefined;\n }\n return parseJson(text);\n}\n\nasync function request<TResponse, TParams extends object>(\n method: string,\n route: string,\n options: FetchAdapterOptions,\n body?: unknown,\n config?: HTTPFetchConfig<TParams, TResponse>\n): Promise<{ data: TResponse }> {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const baseURL = options.baseURL ?? \"\";\n const url = appendQuery(`${baseURL}${route}`, config?.params);\n\n const headers: Record<string, string> = {\n ...options.headers,\n ...config?.headers,\n };\n\n const init: RequestInit = {\n headers,\n method,\n };\n if (config?.signal !== undefined) {\n init.signal = config.signal;\n }\n\n if (body !== undefined) {\n headers[\"Content-Type\"] ??= \"application/json\";\n init.body = JSON.stringify(body);\n }\n\n const response = await fetchImpl(url, init);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status} ${response.statusText}`);\n }\n\n const text = await response.text();\n const parsed = readResponseBody(text);\n return {\n data: coerceResponseData<TResponse>(parsed, config?.validateResponse),\n };\n}\n\nexport function createFetchAdapter(\n options: FetchAdapterOptions = {}\n): HTTPFetch {\n return {\n delete: <TResponse, TParams extends object = QueryParams>(\n route: string,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) =>\n request<TResponse, TParams>(\"DELETE\", route, options, undefined, config),\n get: <TResponse, TParams extends object = QueryParams>(\n route: string,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) => request<TResponse, TParams>(\"GET\", route, options, undefined, config),\n patch: <TResponse, TBody = unknown, TParams extends object = QueryParams>(\n route: string,\n body: TBody,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) => request<TResponse, TParams>(\"PATCH\", route, options, body, config),\n post: <TResponse, TBody = unknown, TParams extends object = QueryParams>(\n route: string,\n body: TBody,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) => request<TResponse, TParams>(\"POST\", route, options, body, config),\n put: <TResponse, TBody = unknown, TParams extends object = QueryParams>(\n route: string,\n body: TBody,\n config?: HTTPFetchConfig<TParams, TResponse>\n ) => request<TResponse, TParams>(\"PUT\", route, options, body, config),\n };\n}\n\nexport type { HTTPFetch, HTTPFetchConfig } from \"../../http/types\";\n"],"mappings":";AAuBA,SAAgB,YAAY,OAAoC;CAC9D,IACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU,WAEjB,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,WAAW;CAGhC,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,WAAW;CAG/C,OAAO;AACT;AAeA,SAAgB,UAAU,MAAyB;CACjD,MAAM,SAAkB,KAAK,MAAM,IAAI;CACvC,IAAI,CAAC,YAAY,MAAM,GACrB,MAAM,IAAI,UAAU,+CAA+C;CAErE,OAAO;AACT;;;AC7DA,IAAa,0BAAb,cAA6C,MAAM;CACjD;CAEA,YAAY,SAAiB,OAAgB;EAC3C,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,QAAQ;CACf;AACF;AAEA,SAAgB,mBACd,OACA,WACG;CACH,IAAI,cAAc,KAAA,GAChB,OAAO;CAGT,IAAI;EACF,OAAO,UAAU,KAAK;CACxB,SAAS,OAAO;EACd,MAAM,IAAI,wBAAwB,8BAA8B,KAAK;CACvE;AACF;;;ACdA,SAAS,YAAY,KAAa,QAAyB;CACzD,IAAI,WAAW,KAAA,KAAa,OAAO,KAAK,MAAM,CAAC,CAAC,WAAW,GACzD,OAAO;CAGT,MAAM,SAAS,IAAI,gBAAgB;CACnC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,OAAO,OAAO,KAAK,OAAO,KAAK,CAAC;CAGlC,MAAM,QAAQ,OAAO,SAAS;CAC9B,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,OAAO,GAAG,MAAM,IAAI,SAAS,GAAG,IAAI,MAAM,MAAM;AAClD;AAEA,SAAS,iBAAiB,MAAqC;CAC7D,IAAI,KAAK,WAAW,GAClB;CAEF,OAAO,UAAU,IAAI;AACvB;AAEA,eAAe,QACb,QACA,OACA,SACA,MACA,QAC8B;CAC9B,MAAM,YAAY,QAAQ,SAAS,WAAW;CAE9C,MAAM,MAAM,YAAY,GADR,QAAQ,WAAW,KACE,SAAS,QAAQ,MAAM;CAE5D,MAAM,UAAkC;EACtC,GAAG,QAAQ;EACX,GAAG,QAAQ;CACb;CAEA,MAAM,OAAoB;EACxB;EACA;CACF;CACA,IAAI,QAAQ,WAAW,KAAA,GACrB,KAAK,SAAS,OAAO;CAGvB,IAAI,SAAS,KAAA,GAAW;EACtB,QAAQ,oBAAoB;EAC5B,KAAK,OAAO,KAAK,UAAU,IAAI;CACjC;CAEA,MAAM,WAAW,MAAM,UAAU,KAAK,IAAI;CAC1C,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,YAAY;CAKlE,OAAO,EACL,MAAM,mBAFO,iBAAiB,MADb,SAAS,KAAK,CAGK,GAAQ,QAAQ,gBAAgB,EACtE;AACF;AAEA,SAAgB,mBACd,UAA+B,CAAC,GACrB;CACX,OAAO;EACL,SACE,OACA,WAEA,QAA4B,UAAU,OAAO,SAAS,KAAA,GAAW,MAAM;EACzE,MACE,OACA,WACG,QAA4B,OAAO,OAAO,SAAS,KAAA,GAAW,MAAM;EACzE,QACE,OACA,MACA,WACG,QAA4B,SAAS,OAAO,SAAS,MAAM,MAAM;EACtE,OACE,OACA,MACA,WACG,QAA4B,QAAQ,OAAO,SAAS,MAAM,MAAM;EACrE,MACE,OACA,MACA,WACG,QAA4B,OAAO,OAAO,SAAS,MAAM,MAAM;CACtE;AACF"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ //#region src/cli.d.ts
2
+ interface ParsedArgs {
3
+ command?: string;
4
+ /** Populated by the first --source value; kept for init (single-source) compat. */
5
+ source?: string;
6
+ /** All --source values collected for multi-source generate. */
7
+ sources: Array<string>;
8
+ spec?: string;
9
+ client?: "axios" | "fetch" | "custom";
10
+ layout?: "monolith" | "packages";
11
+ check?: boolean;
12
+ acceptBase?: boolean;
13
+ all?: boolean;
14
+ }
15
+ declare function parseArgs(argv: Array<string>): ParsedArgs;
16
+ //#endregion
17
+ export { parseArgs };
18
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";UASU;EACR;;EAEA;;EAEA,SAAS;EACT;EACA;EACA;EACA;EACA;EACA;;iBAGc,UAAU,MAAM,gBAAgB"}