dto2ts 0.1.0
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/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +68 -0
- package/dist/generate.d.ts +10 -0
- package/dist/generate.js +87 -0
- package/dist/generators/http-client.d.ts +5 -0
- package/dist/generators/http-client.js +133 -0
- package/dist/generators/models.d.ts +10 -0
- package/dist/generators/models.js +77 -0
- package/dist/generators/services.d.ts +6 -0
- package/dist/generators/services.js +77 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +15 -0
- package/dist/ir.d.ts +48 -0
- package/dist/ir.js +8 -0
- package/dist/parsers/csharp-types.d.ts +9 -0
- package/dist/parsers/csharp-types.js +111 -0
- package/dist/parsers/csharp.d.ts +3 -0
- package/dist/parsers/csharp.js +224 -0
- package/dist/parsers/openapi.d.ts +3 -0
- package/dist/parsers/openapi.js +218 -0
- package/dist/parsers/resources.d.ts +10 -0
- package/dist/parsers/resources.js +41 -0
- package/dist/utils/fs.d.ts +4 -0
- package/dist/utils/fs.js +69 -0
- package/dist/utils/naming.d.ts +10 -0
- package/dist/utils/naming.js +42 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serkan Çakmak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# dto2ts
|
|
2
|
+
|
|
3
|
+
Generate **TypeScript models** and **fetch-based CRUD services** from your C# API.
|
|
4
|
+
|
|
5
|
+
Point it at your ASP.NET Core backend and get a fully typed, ready-to-use client for your
|
|
6
|
+
frontend — no manual DTO copying, no drift between backend and frontend.
|
|
7
|
+
|
|
8
|
+
Two input sources are supported:
|
|
9
|
+
|
|
10
|
+
- **OpenAPI / Swagger** — a `swagger.json` file or a live URL (e.g. the spec ASP.NET Core
|
|
11
|
+
generates automatically). The most robust option.
|
|
12
|
+
- **C# source files** (`.cs`) — parses your DTO/record/enum definitions directly, no need to
|
|
13
|
+
run the API first.
|
|
14
|
+
|
|
15
|
+
The generated code has **zero runtime dependencies** — it uses the built-in `fetch`, so it
|
|
16
|
+
runs in the browser, Node 18+, Angular, React, Vue, or anywhere else.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install --save-dev dto2ts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or run it once without installing:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx dto2ts --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
### From OpenAPI / Swagger
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# From a live API
|
|
38
|
+
npx dto2ts -s openapi -i https://localhost:5001/openapi/v1.json -o ./src/api
|
|
39
|
+
|
|
40
|
+
# From a local file
|
|
41
|
+
npx dto2ts -s openapi -i ./swagger.json -o ./src/api
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### From C# source files
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Point at a folder — all .cs files are scanned recursively (bin/ and obj/ are skipped)
|
|
48
|
+
npx dto2ts -s csharp -i ../Backend/Dtos -o ./src/api
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What gets generated
|
|
52
|
+
|
|
53
|
+
One file per type, following the Angular style guide (`.model.ts` / `.enum.ts` / `.service.ts`),
|
|
54
|
+
with barrel `index.ts` files so you can import from a single path:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
src/api/
|
|
58
|
+
├── models/
|
|
59
|
+
│ ├── product-dto.model.ts # export interface ProductDto
|
|
60
|
+
│ ├── category-dto.model.ts # export interface CategoryDto
|
|
61
|
+
│ ├── product-status.enum.ts # export enum ProductStatus
|
|
62
|
+
│ └── index.ts # barrel
|
|
63
|
+
├── services/
|
|
64
|
+
│ ├── product.service.ts # export class ProductService
|
|
65
|
+
│ ├── category.service.ts
|
|
66
|
+
│ └── index.ts # barrel
|
|
67
|
+
├── http-client.ts # fetch-based HttpClient + ApiError
|
|
68
|
+
└── index.ts # top-level barrel re-export
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Model files reference each other with `import type` (no runtime coupling), and services import
|
|
72
|
+
their DTOs from the `../models` barrel — so everything compiles cleanly even under strict
|
|
73
|
+
`verbatimModuleSyntax`.
|
|
74
|
+
|
|
75
|
+
Example generated model:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
export enum ProductStatus {
|
|
79
|
+
Draft = 0,
|
|
80
|
+
Active = 1,
|
|
81
|
+
Archived = 2,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ProductDto {
|
|
85
|
+
id: number;
|
|
86
|
+
name: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
price: number;
|
|
89
|
+
stock?: number;
|
|
90
|
+
status: ProductStatus;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
tags: string[];
|
|
93
|
+
category?: CategoryDto;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
And a generated service:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
export class ProductService {
|
|
101
|
+
constructor(
|
|
102
|
+
private readonly http: HttpClient,
|
|
103
|
+
private readonly basePath: string = "/api/products"
|
|
104
|
+
) {}
|
|
105
|
+
|
|
106
|
+
list(options?: RequestOptions): Promise<ProductDto[]>;
|
|
107
|
+
getById(id: number, options?: RequestOptions): Promise<ProductDto>;
|
|
108
|
+
create(payload: ProductDto, options?: RequestOptions): Promise<ProductDto>;
|
|
109
|
+
update(id: number, payload: ProductDto, options?: RequestOptions): Promise<ProductDto>;
|
|
110
|
+
patch(id: number, payload: Partial<ProductDto>, options?: RequestOptions): Promise<ProductDto>;
|
|
111
|
+
remove(id: number, options?: RequestOptions): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Using the generated client
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { HttpClient, ProductService, ApiError } from "./api";
|
|
119
|
+
|
|
120
|
+
const http = new HttpClient({
|
|
121
|
+
baseUrl: "https://api.example.com",
|
|
122
|
+
// Optional: attach headers to every request (sync or async — great for auth tokens)
|
|
123
|
+
getHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const products = new ProductService(http);
|
|
127
|
+
|
|
128
|
+
// CRUD
|
|
129
|
+
const all = await products.list({ query: { page: 1, pageSize: 20 } });
|
|
130
|
+
const one = await products.getById(42);
|
|
131
|
+
const created = await products.create({ id: 0, name: "Pen", price: 9.9, status: 1, tags: [], createdAt: "" });
|
|
132
|
+
const updated = await products.update(42, created);
|
|
133
|
+
const patched = await products.patch(42, { price: 12.5 });
|
|
134
|
+
await products.remove(42);
|
|
135
|
+
|
|
136
|
+
// Error handling
|
|
137
|
+
try {
|
|
138
|
+
await products.getById(999);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (e instanceof ApiError) {
|
|
141
|
+
console.error(e.status, e.body); // e.g. 404 and the server response body
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Angular
|
|
147
|
+
|
|
148
|
+
The generated `HttpClient` is framework-agnostic. In Angular you can register a service via DI:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// app.config.ts
|
|
152
|
+
import { HttpClient, ProductService } from "./api";
|
|
153
|
+
|
|
154
|
+
export const appConfig = {
|
|
155
|
+
providers: [
|
|
156
|
+
{ provide: HttpClient, useFactory: () => new HttpClient({ baseUrl: "http://localhost:5099" }) },
|
|
157
|
+
ProductService,
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// a component
|
|
164
|
+
private readonly products = inject(ProductService);
|
|
165
|
+
this.products.list().then((items) => (this.items = items));
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Configuration file
|
|
169
|
+
|
|
170
|
+
Instead of CLI flags, drop a `dto2ts.config.json` in your project root:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"source": "openapi",
|
|
175
|
+
"input": "./swagger.json",
|
|
176
|
+
"output": "./src/api",
|
|
177
|
+
"camelCase": true,
|
|
178
|
+
"dateAsString": true,
|
|
179
|
+
"generateServices": true,
|
|
180
|
+
"idType": "number",
|
|
181
|
+
"entities": ["ProductDto", "CategoryDto"],
|
|
182
|
+
"basePaths": { "ProductDto": "/api/v1/products" }
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Then just run `npx dto2ts`. CLI flags override the config file.
|
|
187
|
+
|
|
188
|
+
## Options
|
|
189
|
+
|
|
190
|
+
| CLI flag | Config key | Description | Default |
|
|
191
|
+
| ------------------ | ---------------------- | ---------------------------------------------------- | ---------------- |
|
|
192
|
+
| `-s, --source` | `source` | `openapi` \| `csharp` | — (required) |
|
|
193
|
+
| `-i, --input` | `input` | `swagger.json` path/URL, or a `.cs` file/folder | — (required) |
|
|
194
|
+
| `-o, --output` | `output` | Output directory | — (required) |
|
|
195
|
+
| `--no-camel-case` | `camelCase: false` | Keep original PascalCase field names | camelCase |
|
|
196
|
+
| `--date-as-date` | `dateAsString: false` | Map `DateTime` → `Date` instead of `string` | `string` |
|
|
197
|
+
| `--no-services` | `generateServices: false` | Generate models only, no services | services on |
|
|
198
|
+
| `--entities a,b` | `entities: [...]` | Only generate services for these models | all with an `id` |
|
|
199
|
+
| `--id-type` | `idType` | Default TS type for the `id` parameter | `number` |
|
|
200
|
+
| — | `basePaths` | Map of model name → base path override | convention |
|
|
201
|
+
|
|
202
|
+
## Type mapping (C# → TypeScript)
|
|
203
|
+
|
|
204
|
+
| C# | TypeScript |
|
|
205
|
+
| ----------------------------------------------- | --------------------------------- |
|
|
206
|
+
| `string`, `char`, `Guid` | `string` |
|
|
207
|
+
| `int`, `long`, `decimal`, `double`, `byte`, … | `number` |
|
|
208
|
+
| `bool` | `boolean` |
|
|
209
|
+
| `DateTime`, `DateTimeOffset`, `DateOnly` | `string` (or `Date`) |
|
|
210
|
+
| `T?`, `Nullable<T>` | optional field (`prop?`) |
|
|
211
|
+
| `List<T>`, `IEnumerable<T>`, `T[]`, `HashSet<T>`| `T[]` |
|
|
212
|
+
| `Dictionary<K,V>` | `Record<string \| number, V>` |
|
|
213
|
+
| `Task<T>` | `T` (unwrapped) |
|
|
214
|
+
| `enum` | `enum` |
|
|
215
|
+
| custom class / record | reference to the matching interface |
|
|
216
|
+
|
|
217
|
+
## How services are generated
|
|
218
|
+
|
|
219
|
+
- **OpenAPI**: real routes are detected from the `paths` section — the base path and id type
|
|
220
|
+
come straight from the spec.
|
|
221
|
+
- **C#**: any interface with an `id` field is treated as a resource; the base path follows the
|
|
222
|
+
`/api/<pluralized-name>` convention.
|
|
223
|
+
- In both cases you can override with the `entities` and `basePaths` config options.
|
|
224
|
+
|
|
225
|
+
## Programmatic API
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { generate } from "dto2ts";
|
|
229
|
+
|
|
230
|
+
const { api, files } = await generate({
|
|
231
|
+
source: "openapi",
|
|
232
|
+
input: "./swagger.json",
|
|
233
|
+
output: "./src/api",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log(`Generated ${api.models.length} models`, files);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Notes & gotchas
|
|
240
|
+
|
|
241
|
+
- Generated files are **overwritten on every run** — don't edit them by hand. Re-run dto2ts as
|
|
242
|
+
part of your build instead.
|
|
243
|
+
- ASP.NET Core serializes JSON as camelCase by default, so keep `camelCase: true` (the default).
|
|
244
|
+
- **Enums via .NET's default OpenAPI**: `System.Text.Json` serializes enums as integers, and the
|
|
245
|
+
generated spec exposes them as a bare `{ "type": "integer" }` with no member names. If you use
|
|
246
|
+
the `openapi` source, such enums become an empty type. Two fixes:
|
|
247
|
+
1. Prefer the **`csharp` source**, which reads the real enum definition, or
|
|
248
|
+
2. Register a `JsonStringEnumConverter` in your API so the spec carries the enum values.
|
|
249
|
+
- The C# parser is regex-based and covers standard DTO/record/enum shapes. For deeply nested or
|
|
250
|
+
heavily generic types, prefer the OpenAPI source.
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const generate_1 = require("./generate");
|
|
7
|
+
const program = new commander_1.Command();
|
|
8
|
+
program
|
|
9
|
+
.name("dto2ts")
|
|
10
|
+
.description("C# API (OpenAPI/Swagger veya .cs DTO) kaynaklarından TS model + fetch CRUD service üretir.")
|
|
11
|
+
.version("0.1.0");
|
|
12
|
+
program
|
|
13
|
+
.command("generate", { isDefault: true })
|
|
14
|
+
.description("Model ve service dosyalarını üret")
|
|
15
|
+
.option("-s, --source <source>", "kaynak türü: openapi | csharp")
|
|
16
|
+
.option("-i, --input <path>", "swagger.json yolu/URL'si veya .cs dosyası/klasörü")
|
|
17
|
+
.option("-o, --output <dir>", "çıktı klasörü")
|
|
18
|
+
.option("--no-camel-case", "alan adlarını camelCase'e çevirme (orijinal PascalCase kalsın)")
|
|
19
|
+
.option("--date-as-date", "DateTime alanlarını Date olarak üret (varsayılan: string)")
|
|
20
|
+
.option("--no-services", "yalnızca model üret, service üretme")
|
|
21
|
+
.option("--entities <list>", "yalnızca bu modeller için service üret (virgülle ayır)")
|
|
22
|
+
.option("--id-type <type>", "id alanının varsayılan tipi (number | string)")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
try {
|
|
25
|
+
const fileConfig = (0, config_1.loadConfigFile)() ?? {};
|
|
26
|
+
const cli = {};
|
|
27
|
+
if (opts.source)
|
|
28
|
+
cli.source = opts.source;
|
|
29
|
+
if (opts.input)
|
|
30
|
+
cli.input = opts.input;
|
|
31
|
+
if (opts.output)
|
|
32
|
+
cli.output = opts.output;
|
|
33
|
+
if (opts.camelCase === false)
|
|
34
|
+
cli.camelCase = false;
|
|
35
|
+
if (opts.dateAsDate)
|
|
36
|
+
cli.dateAsString = false;
|
|
37
|
+
if (opts.services === false)
|
|
38
|
+
cli.generateServices = false;
|
|
39
|
+
if (opts.entities)
|
|
40
|
+
cli.entities = String(opts.entities).split(",").map((s) => s.trim()).filter(Boolean);
|
|
41
|
+
if (opts.idType)
|
|
42
|
+
cli.idType = opts.idType;
|
|
43
|
+
const merged = { ...fileConfig, ...cli };
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const { api, files } = await (0, generate_1.generate)(merged);
|
|
46
|
+
const ms = Date.now() - start;
|
|
47
|
+
const modelCount = api.models.length;
|
|
48
|
+
const serviceCount = api.resources.length;
|
|
49
|
+
console.log(`✔ dto2ts tamamlandı (${ms}ms)`);
|
|
50
|
+
console.log(` ${modelCount} model, ${serviceCount} service üretildi:`);
|
|
51
|
+
for (const f of files)
|
|
52
|
+
console.log(` → ${f}`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(`✖ Hata: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
program.parseAsync(process.argv);
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface GenConfig {
|
|
2
|
+
/** Girdi kaynağı. */
|
|
3
|
+
source: "openapi" | "csharp";
|
|
4
|
+
/** openapi için swagger.json yolu/URL'si; csharp için dosya ya da klasör yolu. */
|
|
5
|
+
input: string;
|
|
6
|
+
/** Üretilen dosyaların yazılacağı klasör. */
|
|
7
|
+
output: string;
|
|
8
|
+
/** Alan adlarını camelCase'e çevir (ASP.NET Core varsayılan JSON davranışı). Varsayılan: true. */
|
|
9
|
+
camelCase?: boolean;
|
|
10
|
+
/** DateTime alanlarını "string" olarak üret (false ise "Date"). Varsayılan: true. */
|
|
11
|
+
dateAsString?: boolean;
|
|
12
|
+
/** CRUD service üretilsin mi. Varsayılan: true. */
|
|
13
|
+
generateServices?: boolean;
|
|
14
|
+
/** Service üretimini yalnızca bu modellerle sınırla (boşsa hepsi/uygun olanlar). */
|
|
15
|
+
entities?: string[];
|
|
16
|
+
/** Model adı -> base path override. Örn. { "ProductDto": "/api/v1/products" }. */
|
|
17
|
+
basePaths?: Record<string, string>;
|
|
18
|
+
/** Id alanının varsayılan tipi (base path'e /{id} eklenirken). Varsayılan: "number". */
|
|
19
|
+
idType?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_CONFIG: Partial<GenConfig>;
|
|
22
|
+
/** Çalışma dizininde config dosyası ara ve yükle. */
|
|
23
|
+
export declare function loadConfigFile(cwd?: string): Partial<GenConfig> | null;
|
|
24
|
+
export declare function resolveConfig(partial: Partial<GenConfig>): GenConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
37
|
+
exports.loadConfigFile = loadConfigFile;
|
|
38
|
+
exports.resolveConfig = resolveConfig;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
exports.DEFAULT_CONFIG = {
|
|
42
|
+
camelCase: true,
|
|
43
|
+
dateAsString: true,
|
|
44
|
+
generateServices: true,
|
|
45
|
+
idType: "number",
|
|
46
|
+
};
|
|
47
|
+
const CONFIG_FILE_NAMES = ["dto2ts.config.json", ".dto2tsrc.json"];
|
|
48
|
+
/** Çalışma dizininde config dosyası ara ve yükle. */
|
|
49
|
+
function loadConfigFile(cwd = process.cwd()) {
|
|
50
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
51
|
+
const p = path.join(cwd, name);
|
|
52
|
+
if (fs.existsSync(p)) {
|
|
53
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function resolveConfig(partial) {
|
|
60
|
+
const merged = { ...exports.DEFAULT_CONFIG, ...partial };
|
|
61
|
+
if (!merged.source)
|
|
62
|
+
throw new Error('config: "source" zorunlu ("openapi" | "csharp").');
|
|
63
|
+
if (!merged.input)
|
|
64
|
+
throw new Error('config: "input" zorunlu.');
|
|
65
|
+
if (!merged.output)
|
|
66
|
+
throw new Error('config: "output" zorunlu.');
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GenConfig } from "./config";
|
|
2
|
+
import { ApiModel } from "./ir";
|
|
3
|
+
export interface GenerateResult {
|
|
4
|
+
api: ApiModel;
|
|
5
|
+
files: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Kaynağı parse edip ApiModel (IR) üret. */
|
|
8
|
+
export declare function parseSource(config: GenConfig): Promise<ApiModel>;
|
|
9
|
+
/** Tam üretim akışı: parse -> generate -> diske yaz. */
|
|
10
|
+
export declare function generate(partial: Partial<GenConfig>): Promise<GenerateResult>;
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseSource = parseSource;
|
|
37
|
+
exports.generate = generate;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const config_1 = require("./config");
|
|
40
|
+
const openapi_1 = require("./parsers/openapi");
|
|
41
|
+
const csharp_1 = require("./parsers/csharp");
|
|
42
|
+
const models_1 = require("./generators/models");
|
|
43
|
+
const services_1 = require("./generators/services");
|
|
44
|
+
const http_client_1 = require("./generators/http-client");
|
|
45
|
+
const fs_1 = require("./utils/fs");
|
|
46
|
+
/** Kaynağı parse edip ApiModel (IR) üret. */
|
|
47
|
+
async function parseSource(config) {
|
|
48
|
+
if (config.source === "openapi")
|
|
49
|
+
return (0, openapi_1.parseOpenApi)(config);
|
|
50
|
+
if (config.source === "csharp")
|
|
51
|
+
return (0, csharp_1.parseCsharp)(config);
|
|
52
|
+
throw new Error(`Bilinmeyen source: ${config.source}`);
|
|
53
|
+
}
|
|
54
|
+
function generateBarrel(hasServices) {
|
|
55
|
+
const lines = [
|
|
56
|
+
"// Bu dosya dto2ts tarafından otomatik üretildi.",
|
|
57
|
+
'export * from "./models";',
|
|
58
|
+
'export * from "./http-client";',
|
|
59
|
+
];
|
|
60
|
+
if (hasServices)
|
|
61
|
+
lines.push('export * from "./services";');
|
|
62
|
+
return lines.join("\n") + "\n";
|
|
63
|
+
}
|
|
64
|
+
/** Tam üretim akışı: parse -> generate -> diske yaz. */
|
|
65
|
+
async function generate(partial) {
|
|
66
|
+
const config = (0, config_1.resolveConfig)(partial);
|
|
67
|
+
const api = await parseSource(config);
|
|
68
|
+
const outDir = path.resolve(config.output);
|
|
69
|
+
const written = [];
|
|
70
|
+
const emit = (relPath, content) => {
|
|
71
|
+
const full = path.join(outDir, relPath);
|
|
72
|
+
(0, fs_1.writeFile)(full, content);
|
|
73
|
+
written.push(full);
|
|
74
|
+
};
|
|
75
|
+
// models/*.model.ts | *.enum.ts + models/index.ts
|
|
76
|
+
for (const file of (0, models_1.generateModelFiles)(api))
|
|
77
|
+
emit(file.path, file.content);
|
|
78
|
+
// http-client.ts
|
|
79
|
+
emit("http-client.ts", (0, http_client_1.generateHttpClient)());
|
|
80
|
+
// services/*.service.ts + services/index.ts
|
|
81
|
+
const serviceFiles = config.generateServices === false ? [] : (0, services_1.generateServiceFiles)(api);
|
|
82
|
+
for (const file of serviceFiles)
|
|
83
|
+
emit(file.path, file.content);
|
|
84
|
+
// kök index.ts
|
|
85
|
+
emit("index.ts", generateBarrel(serviceFiles.length > 0));
|
|
86
|
+
return { api, files: written };
|
|
87
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateHttpClient = generateHttpClient;
|
|
4
|
+
/**
|
|
5
|
+
* Statik http-client.ts içeriği. Üretilen service'lerin dayandığı,
|
|
6
|
+
* fetch tabanlı, sıfır bağımlılıklı istemci.
|
|
7
|
+
*/
|
|
8
|
+
function generateHttpClient() {
|
|
9
|
+
return `// Bu dosya dto2ts tarafından otomatik üretildi. Elle düzenlenebilir kabul edilir ama
|
|
10
|
+
// yeniden üretimde üzerine yazılır.
|
|
11
|
+
|
|
12
|
+
export type QueryValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
|
|
13
|
+
|
|
14
|
+
export interface RequestOptions {
|
|
15
|
+
/** İstek başlıkları (base başlıklarla birleşir). */
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
/** Query string parametreleri. */
|
|
18
|
+
query?: Record<string, QueryValue>;
|
|
19
|
+
/** İptal için AbortSignal. */
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HttpClientOptions {
|
|
24
|
+
/** API kök adresi. Örn. "https://api.example.com". */
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
/** Her isteğe eklenecek başlıklar (senkron ya da async, örn. token). */
|
|
27
|
+
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
28
|
+
/** Özel fetch implementasyonu (test/SSR için). Varsayılan: global fetch. */
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** HTTP başarısız olduğunda fırlatılan hata. */
|
|
33
|
+
export class ApiError extends Error {
|
|
34
|
+
constructor(
|
|
35
|
+
public readonly status: number,
|
|
36
|
+
public readonly statusText: string,
|
|
37
|
+
public readonly body: unknown,
|
|
38
|
+
public readonly url: string
|
|
39
|
+
) {
|
|
40
|
+
super(\`HTTP \${status} \${statusText} — \${url}\`);
|
|
41
|
+
this.name = "ApiError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildQuery(query?: Record<string, QueryValue>): string {
|
|
46
|
+
if (!query) return "";
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
for (const [key, value] of Object.entries(query)) {
|
|
49
|
+
if (value === null || value === undefined) continue;
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
for (const v of value) params.append(key, String(v));
|
|
52
|
+
} else {
|
|
53
|
+
params.append(key, String(value));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const s = params.toString();
|
|
57
|
+
return s ? \`?\${s}\` : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class HttpClient {
|
|
61
|
+
private readonly baseUrl: string;
|
|
62
|
+
private readonly fetchImpl: typeof fetch;
|
|
63
|
+
|
|
64
|
+
constructor(private readonly options: HttpClientOptions) {
|
|
65
|
+
this.baseUrl = options.baseUrl.replace(/\\/+$/, "");
|
|
66
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
67
|
+
if (!this.fetchImpl) {
|
|
68
|
+
throw new Error("Global fetch bulunamadı. Node 18+ kullanın ya da options.fetch verin.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async request<T>(
|
|
73
|
+
method: string,
|
|
74
|
+
path: string,
|
|
75
|
+
body?: unknown,
|
|
76
|
+
options: RequestOptions = {}
|
|
77
|
+
): Promise<T> {
|
|
78
|
+
const url = \`\${this.baseUrl}\${path.startsWith("/") ? path : "/" + path}\${buildQuery(options.query)}\`;
|
|
79
|
+
|
|
80
|
+
const headers: Record<string, string> = {
|
|
81
|
+
Accept: "application/json",
|
|
82
|
+
...(this.options.getHeaders ? await this.options.getHeaders() : {}),
|
|
83
|
+
...options.headers,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const hasBody = body !== undefined && body !== null;
|
|
87
|
+
if (hasBody) headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
|
|
88
|
+
|
|
89
|
+
const res = await this.fetchImpl(url, {
|
|
90
|
+
method,
|
|
91
|
+
headers,
|
|
92
|
+
body: hasBody ? JSON.stringify(body) : undefined,
|
|
93
|
+
signal: options.signal,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
let errBody: unknown = null;
|
|
98
|
+
try {
|
|
99
|
+
errBody = await res.json();
|
|
100
|
+
} catch {
|
|
101
|
+
try {
|
|
102
|
+
errBody = await res.text();
|
|
103
|
+
} catch {
|
|
104
|
+
/* yoksay */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new ApiError(res.status, res.statusText, errBody, url);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (res.status === 204) return undefined as T;
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
if (!text) return undefined as T;
|
|
113
|
+
return JSON.parse(text) as T;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get<T>(path: string, options?: RequestOptions): Promise<T> {
|
|
117
|
+
return this.request<T>("GET", path, undefined, options);
|
|
118
|
+
}
|
|
119
|
+
post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {
|
|
120
|
+
return this.request<T>("POST", path, body, options);
|
|
121
|
+
}
|
|
122
|
+
put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {
|
|
123
|
+
return this.request<T>("PUT", path, body, options);
|
|
124
|
+
}
|
|
125
|
+
patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {
|
|
126
|
+
return this.request<T>("PATCH", path, body, options);
|
|
127
|
+
}
|
|
128
|
+
delete<T>(path: string, options?: RequestOptions): Promise<T> {
|
|
129
|
+
return this.request<T>("DELETE", path, undefined, options);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ApiModel, ModelDef } from "../ir";
|
|
2
|
+
export interface GeneratedFile {
|
|
3
|
+
/** Çıktı klasörüne göre göreli yol. Örn. "models/product-dto.model.ts". */
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
/** Bir modelin uzantısız dosya adı tabanı. */
|
|
8
|
+
export declare function modelFileBase(m: ModelDef): string;
|
|
9
|
+
/** Her modeli kendi dosyasına derle (models/<ad>.model.ts | <ad>.enum.ts) + barrel. */
|
|
10
|
+
export declare function generateModelFiles(api: ApiModel): GeneratedFile[];
|