api-farmer 0.1.3 → 0.1.5

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 CHANGED
@@ -83,6 +83,7 @@ export default defineConfig({
83
83
  verb() {},
84
84
  url() {},
85
85
  entity() {},
86
+ name() {},
86
87
  fn() {},
87
88
  comment() {},
88
89
  type() {},
@@ -97,6 +98,25 @@ export default defineConfig({
97
98
  })
98
99
  ```
99
100
 
101
+ #### `name` Transformer
102
+
103
+ The `name` transformer is a convenience option that derives both `fn` and `type` from a single function. When set, `fn` is the return value, and `type` is automatically capitalized from `fn`. This avoids writing duplicate logic for `fn` and `type`.
104
+
105
+ ```ts
106
+ export default defineConfig({
107
+ transformer: {
108
+ name({ verb, entity }) {
109
+ return `api${verb}${entity}`
110
+ // fn -> apiGetUsers
111
+ // type -> ApiGetUsers
112
+ },
113
+ },
114
+ })
115
+ ```
116
+
117
+ > [!NOTE]
118
+ > If `name` is set, `fn` and `type` transformers will be ignored.
119
+
100
120
  ### Configuration Options
101
121
 
102
122
  ```ts
@@ -155,7 +175,7 @@ export interface Config {
155
175
  uncountableNouns?: string[]
156
176
  /**
157
177
  * Whether to clean the output directory before generating.
158
- * @default false
178
+ * @default true
159
179
  */
160
180
  clean?: boolean
161
181
  /**
@@ -163,6 +183,11 @@ export interface Config {
163
183
  * @see https://openapi-ts.dev/node
164
184
  */
165
185
  openapiTsOptions?: OpenAPITSOptions
186
+ /**
187
+ * Whether to exclude deprecated API endpoints.
188
+ * @default false
189
+ */
190
+ excludeDeprecated?: boolean
166
191
  }
167
192
  ```
168
193
 
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { r as getCliVersion } from "./utils-DachPo4u.mjs";
3
+ import { Command } from "commander";
4
+ //#region src/cli.ts
5
+ const program = new Command();
6
+ program.version(getCliVersion());
7
+ program.action(async () => {
8
+ const { generate } = await import("./generate-SAL57AwU.mjs").then((n) => n.i);
9
+ return generate();
10
+ });
11
+ program.parse();
12
+ //#endregion
13
+ export {};
@@ -0,0 +1,336 @@
1
+ import { a as getResponseMetadataItems, c as isRequiredRequestBody, d as readTemplateFile, f as CWD, i as getRequestBodyContentType, l as readSchema, p as SUPPORTED_HTTP_METHODS } from "./utils-DachPo4u.mjs";
2
+ import pluralize from "pluralize";
3
+ import { camelize, groupBy, isArray, merge, pascalCase, upperFirst } from "rattail";
4
+ import { resolve } from "path";
5
+ import ejs from "ejs";
6
+ import fse from "fs-extra";
7
+ import openapiTS, { astToString } from "openapi-typescript";
8
+ import prettier from "prettier";
9
+ import { logger } from "rslog";
10
+ import ts from "typescript";
11
+ import { loadConfig } from "unconfig";
12
+ //#region \0rolldown/runtime.js
13
+ var __defProp = Object.defineProperty;
14
+ var __exportAll = (all, no_symbols) => {
15
+ let target = {};
16
+ for (var name in all) __defProp(target, name, {
17
+ get: all[name],
18
+ enumerable: true
19
+ });
20
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
21
+ return target;
22
+ };
23
+ //#endregion
24
+ //#region src/transformer.ts
25
+ function transformModuleName({ name }) {
26
+ return camelize(name);
27
+ }
28
+ function transformUrl({ path }) {
29
+ return path.replace(/{/g, ":").replace(/}/g, "");
30
+ }
31
+ function transformComment({ summary, description, path, method }) {
32
+ return `
33
+ /**${summary ? `\n* ${summary}` : ""}${description && summary !== description ? `\n* @description ${description}\n*` : ""}
34
+ * @url ${path}
35
+ * @method ${method.toLocaleUpperCase()}
36
+ */
37
+ `.trim();
38
+ }
39
+ function transformVerb({ method }) {
40
+ switch (method) {
41
+ case "post": return "Create";
42
+ case "put": return "Update";
43
+ default: return pascalCase(method);
44
+ }
45
+ }
46
+ function transformEntity({ path, method, uncountableNouns }) {
47
+ const words = path.split("/").filter(Boolean);
48
+ return words.reduce((entity, word, index) => {
49
+ if (word.includes("{")) return entity;
50
+ word = word.replace(/\.([a-z])/g, (_, p) => p.toUpperCase());
51
+ const isUncountableNoun = uncountableNouns.includes(word);
52
+ word = pascalCase(word);
53
+ word = isUncountableNoun ? word : pluralize.singular(word);
54
+ if (method === "get" && index === words.length - 1) word = isUncountableNoun ? `${word}List` : pluralize.plural(word);
55
+ return `${entity}${word}`;
56
+ }, "");
57
+ }
58
+ function transformFn({ verb, entity }) {
59
+ return `api${verb}${entity}`;
60
+ }
61
+ function transformType({ verb, entity }) {
62
+ return `Api${verb}${entity}`;
63
+ }
64
+ function transformTypeValue({ fullPath, method }) {
65
+ return `paths['${fullPath}']['${method}']`;
66
+ }
67
+ function transformTypeQuery({ type }) {
68
+ return `${type}Query`;
69
+ }
70
+ function transformTypeQueryValue({ type }) {
71
+ return `${type}['parameters']['query']`;
72
+ }
73
+ function transformTypeRequestBody({ type }) {
74
+ return `${type}RequestBody`;
75
+ }
76
+ function transformTypeRequestBodyValue({ type, required, requestContentType }) {
77
+ return required ? `${type}['requestBody']['content']['${requestContentType}']` : `NonNullable<${type}['requestBody']>['content']['${requestContentType}'] | undefined`;
78
+ }
79
+ function transformTypeResponseBody({ type }) {
80
+ return `${type}ResponseBody`;
81
+ }
82
+ function transformTypeResponseBodyValue({ type, responseMetadataItems }) {
83
+ return responseMetadataItems.map(({ status, responseContentType }) => `${type}['responses']['${status}']['content']['${responseContentType}']`).join(" | ");
84
+ }
85
+ function createTransformer() {
86
+ return {
87
+ moduleName: transformModuleName,
88
+ verb: transformVerb,
89
+ url: transformUrl,
90
+ comment: transformComment,
91
+ entity: transformEntity,
92
+ fn: transformFn,
93
+ type: transformType,
94
+ typeValue: transformTypeValue,
95
+ typeQuery: transformTypeQuery,
96
+ typeQueryValue: transformTypeQueryValue,
97
+ typeRequestBody: transformTypeRequestBody,
98
+ typeRequestBodyValue: transformTypeRequestBodyValue,
99
+ typeResponseBody: transformTypeResponseBody,
100
+ typeResponseBodyValue: transformTypeResponseBodyValue
101
+ };
102
+ }
103
+ //#endregion
104
+ //#region src/config.ts
105
+ function defineConfig(config) {
106
+ return config;
107
+ }
108
+ async function getConfig() {
109
+ const { config } = await loadConfig({ sources: [{ files: "api-farmer.config" }] });
110
+ return config ?? {};
111
+ }
112
+ //#endregion
113
+ //#region src/generate.ts
114
+ var generate_exports = /* @__PURE__ */ __exportAll({
115
+ detectCollisions: () => detectCollisions,
116
+ generate: () => generate,
117
+ generateTypes: () => generateTypes,
118
+ partitionApiModules: () => partitionApiModules,
119
+ renderApiModules: () => renderApiModules,
120
+ transformPayloads: () => transformPayloads
121
+ });
122
+ function transformPayloads(pathItems, options) {
123
+ const { transformer, path, fullPath, base, uncountableNouns, validateStatus, excludeDeprecated } = options;
124
+ return Object.entries(pathItems).filter(([key]) => SUPPORTED_HTTP_METHODS.includes(key)).filter(([, operation]) => !(excludeDeprecated && operation.deprecated)).reduce((payloads, [method, operation]) => {
125
+ const url = transformer.url({
126
+ path,
127
+ base,
128
+ fullPath
129
+ });
130
+ const args = {
131
+ path,
132
+ base,
133
+ fullPath,
134
+ url,
135
+ method,
136
+ uncountableNouns,
137
+ operation
138
+ };
139
+ const entity = transformer.entity(args);
140
+ const verb = transformer.verb(args);
141
+ const comment = transformer.comment({
142
+ ...args,
143
+ ...operation
144
+ });
145
+ const requestContentType = operation.requestBody ? getRequestBodyContentType(operation.requestBody) : void 0;
146
+ const responseMetadataItems = getResponseMetadataItems(operation, validateStatus);
147
+ const nameArgs = {
148
+ ...args,
149
+ verb,
150
+ entity
151
+ };
152
+ const fn = transformer.name ? transformer.name(nameArgs) : transformer.fn(nameArgs);
153
+ const type = transformer.name ? upperFirst(fn) : transformer.type(nameArgs);
154
+ const typeValue = transformer.typeValue({
155
+ ...args,
156
+ verb,
157
+ entity
158
+ });
159
+ const typeQuery = transformer.typeQuery({
160
+ ...args,
161
+ type,
162
+ verb,
163
+ entity
164
+ });
165
+ const typeQueryValue = transformer.typeQueryValue({
166
+ ...args,
167
+ type,
168
+ verb,
169
+ entity
170
+ });
171
+ const typeRequestBody = transformer.typeRequestBody({
172
+ ...args,
173
+ type,
174
+ verb,
175
+ entity
176
+ });
177
+ const typeRequestBodyValue = operation.requestBody && requestContentType ? transformer.typeRequestBodyValue({
178
+ ...args,
179
+ type,
180
+ verb,
181
+ entity,
182
+ required: isRequiredRequestBody(operation.requestBody),
183
+ requestContentType
184
+ }) : "undefined";
185
+ const typeResponseBody = transformer.typeResponseBody({
186
+ ...args,
187
+ type,
188
+ verb,
189
+ entity
190
+ });
191
+ const typeResponseBodyValue = responseMetadataItems.length > 0 ? transformer.typeResponseBodyValue({
192
+ ...args,
193
+ type,
194
+ verb,
195
+ entity,
196
+ responseMetadataItems
197
+ }) : "undefined";
198
+ payloads.push({
199
+ comment,
200
+ fn,
201
+ url,
202
+ method,
203
+ verb,
204
+ entity,
205
+ requestContentType,
206
+ type,
207
+ typeValue,
208
+ typeQuery,
209
+ typeQueryValue,
210
+ typeRequestBody,
211
+ typeRequestBodyValue,
212
+ typeResponseBody,
213
+ typeResponseBodyValue
214
+ });
215
+ return payloads;
216
+ }, []);
217
+ }
218
+ function partitionApiModules(schema, options) {
219
+ const { base, transformer, uncountableNouns, validateStatus, excludeDeprecated } = options;
220
+ const schemaPaths = schema.paths ?? {};
221
+ const keyToPaths = groupBy(base ? Object.keys(schemaPaths).map((key) => key.replace(base, "")) : Object.keys(schemaPaths), (key) => key.split("/")[1]);
222
+ return Object.entries(keyToPaths).reduce((apiModules, [name, paths]) => {
223
+ const payloads = paths.reduce((payloads, path) => {
224
+ const fullPath = base ? base + path : path;
225
+ const pathItems = schemaPaths[fullPath];
226
+ payloads.push(...transformPayloads(pathItems, {
227
+ ...options,
228
+ path,
229
+ fullPath,
230
+ transformer,
231
+ uncountableNouns,
232
+ validateStatus,
233
+ excludeDeprecated
234
+ }));
235
+ return payloads;
236
+ }, []);
237
+ apiModules.push({
238
+ name: transformer.moduleName({ name }),
239
+ payloads
240
+ });
241
+ return apiModules;
242
+ }, []);
243
+ }
244
+ function renderApiModules(apiModules, options) {
245
+ const { output, ts, typesOnly, overrides, preset } = options;
246
+ const templateFile = readTemplateFile(preset);
247
+ const typesFilename = options.typesFilename.replace(".ts", "");
248
+ return Promise.all(apiModules.map((apiModule) => new Promise((promiseResolve) => {
249
+ const data = {
250
+ apiModule,
251
+ typesFilename,
252
+ ts,
253
+ typesOnly
254
+ };
255
+ prettier.format(ejs.render(templateFile, data), {
256
+ parser: "typescript",
257
+ semi: false,
258
+ singleQuote: true,
259
+ printWidth: 120
260
+ }).then((content) => {
261
+ const path = resolve(output, `${apiModule.name}.${ts ? "ts" : "js"}`);
262
+ if ((!overrides || isArray(overrides) && !overrides.includes(apiModule.name)) && fse.existsSync(path)) {
263
+ logger.warn(`File already exists, skip: ${path}`);
264
+ promiseResolve(content);
265
+ return;
266
+ }
267
+ fse.outputFileSync(path, content);
268
+ logger.success(`Generated ${path}`);
269
+ promiseResolve(content);
270
+ });
271
+ })));
272
+ }
273
+ async function generateTypes(schema, output, typesFilename, openapiTsOptions) {
274
+ const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Blob"));
275
+ const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
276
+ const contents = astToString(await openapiTS(schema, {
277
+ defaultNonNullable: false,
278
+ transform(schemaObject) {
279
+ if (schemaObject.format === "binary") return schemaObject.nullable ? ts.factory.createUnionTypeNode([BLOB, NULL]) : BLOB;
280
+ },
281
+ ...openapiTsOptions
282
+ }));
283
+ const typesFilepath = resolve(CWD, output, typesFilename);
284
+ fse.outputFileSync(typesFilepath, contents);
285
+ logger.success(`Generated ${typesFilepath}`);
286
+ }
287
+ function detectCollisions(apiModules) {
288
+ const fnMap = /* @__PURE__ */ new Map();
289
+ for (const apiModule of apiModules) for (const payload of apiModule.payloads) {
290
+ const entries = fnMap.get(payload.fn) ?? [];
291
+ entries.push({
292
+ module: apiModule.name,
293
+ url: payload.url,
294
+ method: payload.method
295
+ });
296
+ fnMap.set(payload.fn, entries);
297
+ }
298
+ for (const [fn, entries] of fnMap) if (entries.length > 1) {
299
+ const endpoints = entries.map((e) => `[${e.method.toUpperCase()}] ${e.url}`).join(", ");
300
+ logger.warn(`API name collision: "${fn}" is generated by multiple endpoints: ${endpoints}. Consider using the "transformer" option to customize API naming.`);
301
+ }
302
+ }
303
+ async function generate(userOptions = {}) {
304
+ const { base, ts = true, typesOnly = false, overrides = true, preset = "axle", input = "./schema.json", output = "./src/apis/generated", typesFilename = "_types.ts", clean = true, validateStatus = (status) => status >= 200 && status < 300, transformer = {}, uncountableNouns = [], openapiTsOptions = {}, excludeDeprecated = false } = merge(await getConfig(), userOptions);
305
+ const mergedTransformer = {
306
+ ...createTransformer(),
307
+ ...transformer
308
+ };
309
+ const schema = await readSchema(input);
310
+ if (clean) {
311
+ fse.removeSync(resolve(CWD, output));
312
+ logger.info(`Cleaned output directory: ${resolve(CWD, output)}`);
313
+ }
314
+ logger.info("Generating API modules...");
315
+ if (openapiTsOptions.excludeDeprecated === void 0) openapiTsOptions.excludeDeprecated = excludeDeprecated;
316
+ if (ts) await generateTypes(schema, output, typesFilename, openapiTsOptions);
317
+ const apiModules = partitionApiModules(schema, {
318
+ base,
319
+ uncountableNouns,
320
+ transformer: mergedTransformer,
321
+ validateStatus,
322
+ excludeDeprecated
323
+ });
324
+ detectCollisions(apiModules);
325
+ await renderApiModules(apiModules, {
326
+ output,
327
+ typesFilename,
328
+ ts,
329
+ typesOnly,
330
+ overrides,
331
+ preset
332
+ });
333
+ logger.success("Done");
334
+ }
335
+ //#endregion
336
+ export { transformUrl as C, transformTypeValue as S, transformTypeQueryValue as _, partitionApiModules as a, transformTypeResponseBody as b, defineConfig as c, transformComment as d, transformEntity as f, transformTypeQuery as g, transformType as h, generate_exports as i, getConfig as l, transformModuleName as m, generate as n, renderApiModules as o, transformFn as p, generateTypes as r, transformPayloads as s, detectCollisions as t, createTransformer as u, transformTypeRequestBody as v, transformVerb as w, transformTypeResponseBodyValue as x, transformTypeRequestBodyValue as y };