@devbie/apits 0.0.0-stage → 1.0.1

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
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # @devbie/apits
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Based on the OpenAPI specification, generate request and response type declarations and request method bodies for APIs
4
+
5
+ ## Installation
6
+
7
+ `npm install @devbie/apits`
8
+
9
+ ## Quick start (no config file needed)
10
+
11
+ Pass an OpenAPI document URL or a local JSON / YAML file:
12
+
13
+ ```bash
14
+ apits gen http://localhost:3041/api-json
15
+ apits gen ./openapi.yaml -o src/api
16
+ ```
17
+
18
+ The `gen` word is optional:
19
+
20
+ ```bash
21
+ apits ./openapi.json
22
+ ```
23
+
24
+ Each run generates two files in the output directory:
25
+
26
+ - `<name>.ts` — type declarations and request functions for every API
27
+ - `request.ts` — the default axios client (skipped with `--no-client`)
28
+
29
+ The file name is derived from the input when `-n/--name` is omitted:
30
+ `http://localhost:3041/api-json` → `localhost.ts`, `./user.openapi.yaml` →
31
+ `userOpenapi.ts`.
32
+
33
+ ### CLI options
34
+
35
+ | Option | Description | Default |
36
+ | --- | --- | --- |
37
+ | `input` (positional) | OpenAPI document URL or local `.json` / `.json5` / `.yaml` / `.yml` file | — |
38
+ | `-o, --output <path>` | Output directory | `src/api` |
39
+ | `-n, --name <name>` | Base name of the generated `<name>.ts`; when using a config file, filters sources by name | derived from the input host / file name |
40
+ | `--base-url <url>` | Runtime `baseURL` (supports the `[code]:` prefix) | — |
41
+ | `--no-client` | Do not scaffold the default `request.ts` axios client | client is generated |
42
+ | `-w, --watch` | Watch a local document and regenerate on change | off |
43
+
44
+ ## Optional configuration file
45
+
46
+ Create a config only when you need multiple sources, JSON/JSON5 config, or
47
+ function-based customization:
48
+
49
+ ```bash
50
+ apits init
51
+ ```
52
+
53
+ `apits init ./openapi.json` prefills the document address.
54
+
55
+ The config file is auto-discovered from any of:
56
+ `apits.config.local.ts`, `apits.config.ts`, `apits.config.local.json`,
57
+ `apits.config.json`, or the `apits` field in `package.json`.
58
+
59
+ ```ts
60
+ // apits.config.ts
61
+ import { defineConfig } from '@devbie/apits'
62
+
63
+ export default defineConfig([
64
+ {
65
+ input: 'http://localhost:3041/api-json',
66
+ output: 'src/api/user',
67
+ name: 'user', // optional, derived from `input` when omitted
68
+ baseURL: '[code]:process.env.BASE_API_URL',
69
+ clientImportTemplate: () => "import request from './request'",
70
+ client: true
71
+ }
72
+ ])
73
+ ```
74
+
75
+ CLI flags take precedence over config-file values. With a config file,
76
+ `-n/--name` selects which source(s) to run.
77
+
78
+ ## Generate from a config file
79
+
80
+ Once a config file exists, generate all sources with:
81
+
82
+ ```bash
83
+ apits gen
84
+ ```
85
+
86
+ Or generate a single source by name:
87
+
88
+ ```bash
89
+ apits gen -n user
90
+ ```
package/bin/apits ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const CLI = require("../dist/cjs/core/cli").default;
4
+ new CLI().run(process.argv.slice(2));
@@ -0,0 +1,29 @@
1
+ import yargs from "yargs";
2
+ /** Flags accepted by the `gen` command. */
3
+ interface GenFlags {
4
+ /** OpenAPI document URL or local file path. */
5
+ input?: string;
6
+ /** Output directory. */
7
+ output?: string;
8
+ /** File base name (CLI mode) or name filter (config-file mode). */
9
+ name?: string;
10
+ /** Runtime baseURL (string; `[code]:` prefix supported). */
11
+ baseUrl?: string;
12
+ /** Whether to scaffold the default request.ts client. */
13
+ client?: boolean;
14
+ /** Watch local input files and regenerate on change. */
15
+ watch?: boolean;
16
+ }
17
+ /** Scaffold an optional apits.config.ts template. */
18
+ export declare function genConfig(prefill?: {
19
+ input?: string;
20
+ }): Promise<void>;
21
+ export declare function start(flags?: GenFlags): Promise<void>;
22
+ export default class CLI {
23
+ run(args: string[]): {
24
+ [x: string]: any;
25
+ };
26
+ private genBuilder;
27
+ init(): yargs.Argv<any>;
28
+ }
29
+ export {};
@@ -0,0 +1,396 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/core/cli.ts
30
+ var cli_exports = {};
31
+ __export(cli_exports, {
32
+ default: () => CLI,
33
+ genConfig: () => genConfig,
34
+ start: () => start
35
+ });
36
+ module.exports = __toCommonJS(cli_exports);
37
+ var import_api = require("tsx/cjs/api");
38
+ var import_fs_extra = __toESM(require("fs-extra"));
39
+ var import_path = __toESM(require("path"));
40
+ var import_prompts = __toESM(require("prompts"));
41
+ var import_yargs = __toESM(require("yargs"));
42
+ var import_vtilsLite = require("../utils/vtilsLite.js");
43
+ var import_generator = require("./generator.js");
44
+ var import_chalk = __toESM(require("chalk"));
45
+ var conso = __toESM(require("../utils/console.js"));
46
+ var import_utils = require("../utils/utils.js");
47
+ var import_spinner = require("../utils/spinner.js");
48
+ var import_helpers = require("../utils/helpers.js");
49
+ (0, import_api.register)();
50
+ var CONFIG_FILE_CANDIDATES = [
51
+ "apits.config.local.ts",
52
+ "apits.config.ts",
53
+ "apits.config.local.json",
54
+ "apits.config.json"
55
+ ];
56
+ var NoInputError = class extends Error {
57
+ };
58
+ function isHttpInput(input) {
59
+ return /^https?:\/\//i.test(input);
60
+ }
61
+ function normalizeConfig(raw) {
62
+ return (0, import_helpers.defineConfig)(Array.isArray(raw) ? raw : [raw]);
63
+ }
64
+ async function discoverConfigFile(cwd = process.cwd()) {
65
+ for (const fileName of CONFIG_FILE_CANDIDATES) {
66
+ const filePath = import_path.default.join(cwd, fileName);
67
+ if (await import_fs_extra.default.pathExists(filePath)) {
68
+ return { filePath, kind: fileName.endsWith(".json") ? "json" : "ts" };
69
+ }
70
+ }
71
+ const packageJsonPath = import_path.default.join(cwd, "package.json");
72
+ if (await import_fs_extra.default.pathExists(packageJsonPath)) {
73
+ try {
74
+ const pkg = JSON.parse(await import_fs_extra.default.readFile(packageJsonPath, "utf-8"));
75
+ if (pkg.apits) {
76
+ return { filePath: packageJsonPath, kind: "package" };
77
+ }
78
+ } catch {
79
+ }
80
+ }
81
+ return void 0;
82
+ }
83
+ function loadConfigFile(discovered) {
84
+ const { filePath, kind } = discovered;
85
+ if (kind === "ts") {
86
+ const mod = require(filePath);
87
+ return normalizeConfig(mod.default ?? mod);
88
+ }
89
+ if (kind === "json") {
90
+ return normalizeConfig(JSON.parse(import_fs_extra.default.readFileSync(filePath, "utf-8")));
91
+ }
92
+ const pkg = JSON.parse(import_fs_extra.default.readFileSync(filePath, "utf-8"));
93
+ return normalizeConfig(pkg.apits);
94
+ }
95
+ function buildCliConfig(flags) {
96
+ const config = {
97
+ input: flags.input,
98
+ output: flags.output || "src/api"
99
+ };
100
+ if (flags.name) {
101
+ config.name = flags.name;
102
+ }
103
+ if (flags.baseUrl !== void 0) {
104
+ config.baseURL = flags.baseUrl;
105
+ }
106
+ config.client = flags.client ?? true;
107
+ return (0, import_helpers.defineConfig)(config)[0];
108
+ }
109
+ function assignUniqueNames(configs) {
110
+ const counts = /* @__PURE__ */ new Map();
111
+ configs.forEach((item) => {
112
+ const base = item.name || import_generator.Generator.deriveName(item.input);
113
+ const seen = counts.get(base) ?? 0;
114
+ counts.set(base, seen + 1);
115
+ if (!item.name) {
116
+ item.name = seen === 0 ? base : `${base}_${seen + 1}`;
117
+ }
118
+ });
119
+ }
120
+ async function resolveConfigs(flags) {
121
+ if (flags.input) {
122
+ const configs = [buildCliConfig(flags)];
123
+ assignUniqueNames(configs);
124
+ return { configs, source: "cli" };
125
+ }
126
+ const discovered = await discoverConfigFile();
127
+ if (discovered) {
128
+ let configs = loadConfigFile(discovered);
129
+ if (flags.name) {
130
+ configs = configs.filter((item) => item.name === flags.name);
131
+ if (configs.length === 0) {
132
+ throw new Error(`No config found with name: ${flags.name}`);
133
+ }
134
+ }
135
+ configs = configs.map((item) => ({
136
+ ...item,
137
+ ...flags.output ? { output: flags.output } : {},
138
+ ...flags.baseUrl !== void 0 ? { baseURL: flags.baseUrl } : {},
139
+ ...flags.client !== void 0 ? { client: flags.client } : {}
140
+ }));
141
+ assignUniqueNames(configs);
142
+ return { configs, source: "file", configFile: discovered.filePath };
143
+ }
144
+ throw new NoInputError();
145
+ }
146
+ function printNoInputHint() {
147
+ conso.error(
148
+ [
149
+ "No OpenAPI input found. Use one of:",
150
+ "",
151
+ " 1. Pass document URL or path directly (no config file needed):",
152
+ " apits gen http://localhost:3041/api-json -o src/api",
153
+ " apits gen ./openapi.yaml",
154
+ "",
155
+ " 2. Generate a config file (multi-source / function-based customization):",
156
+ " apits init"
157
+ ].join("\n")
158
+ );
159
+ }
160
+ async function genConfig(prefill) {
161
+ const cwd = process.cwd();
162
+ const configTSFile = import_path.default.join(cwd, "apits.config.ts");
163
+ if (await import_fs_extra.default.pathExists(configTSFile)) {
164
+ conso.tips(`Config file already exists: ${configTSFile}`);
165
+ const answers = await (0, import_prompts.default)({
166
+ message: "Overwrite existing config file?",
167
+ name: "override",
168
+ type: "confirm"
169
+ });
170
+ if (!answers.override)
171
+ return;
172
+ }
173
+ const configAnswers = await (0, import_prompts.default)([
174
+ {
175
+ message: "API document URL or local JSON/YAML file path",
176
+ name: "input",
177
+ type: "text",
178
+ initial: (prefill == null ? void 0 : prefill.input) || ""
179
+ },
180
+ {
181
+ message: "Generated file name (optional, derived from input if blank)",
182
+ name: "name",
183
+ type: "text",
184
+ initial: ""
185
+ }
186
+ ]);
187
+ if (!configAnswers || !configAnswers.input) {
188
+ return conso.tips("Cancelled, config file not written");
189
+ }
190
+ const nameLine = (configAnswers == null ? void 0 : configAnswers.name) ? ` name: '${configAnswers.name}',
191
+ ` : "";
192
+ await import_fs_extra.default.outputFile(
193
+ configTSFile,
194
+ await (0, import_utils.formatContent)(import_vtilsLite.dedent`
195
+ import { defineConfig } from '@devbie/apits'
196
+
197
+ export default defineConfig([{
198
+ input: '${(configAnswers == null ? void 0 : configAnswers.input) || ""}',
199
+ output: 'src/api',
200
+ ${nameLine} baseURL: '[code]:process.env.BASE_API_URL',
201
+ clientImportTemplate: () => "${`import request from './request'`}",
202
+ // Keep the scaffolded request.ts because the import above points at it.
203
+ client: true,
204
+ }])
205
+ `)
206
+ );
207
+ conso.success("Config file written");
208
+ }
209
+ async function startGenerate(config, index = 0) {
210
+ const { output: outputDir } = config;
211
+ const label = import_chalk.default.green(`${config.input} elapsed`);
212
+ console.time(label);
213
+ import_spinner.spinnerInstance.start();
214
+ const generator = new import_generator.Generator(config);
215
+ const output = await generator.generate();
216
+ await generator.write(output);
217
+ import_spinner.spinnerInstance.clear();
218
+ conso.log(import_chalk.default.yellowBright(`
219
+ ${index + 1}.-------------------------`));
220
+ conso.success(`Code generated, output: ${outputDir}`);
221
+ console.timeEnd(label);
222
+ conso.log(import_chalk.default.yellowBright("---------------------------\n"));
223
+ return true;
224
+ }
225
+ async function runAll(configs) {
226
+ await (0, import_helpers.asyncFnArrayOrderRun)(
227
+ configs.map((configItem, index) => {
228
+ return async () => {
229
+ await startGenerate(configItem, index);
230
+ };
231
+ })
232
+ );
233
+ }
234
+ function watchConfigs(configs) {
235
+ const localConfigs = configs.filter((item) => !isHttpInput(item.input));
236
+ if (localConfigs.length === 0) {
237
+ conso.tips("--watch only supports local files; remote URLs are ignored");
238
+ return;
239
+ }
240
+ const timers = /* @__PURE__ */ new Map();
241
+ localConfigs.forEach((configItem) => {
242
+ const filePath = import_path.default.resolve(configItem.input);
243
+ import_fs_extra.default.watch(filePath, async (eventType) => {
244
+ if (eventType === "rename") {
245
+ try {
246
+ import_fs_extra.default.watch(filePath, async (ev) => {
247
+ scheduleRebuild(filePath, ev);
248
+ });
249
+ } catch {
250
+ }
251
+ }
252
+ scheduleRebuild(filePath, eventType);
253
+ });
254
+ });
255
+ function scheduleRebuild(filePath, eventType) {
256
+ if (eventType !== "change" && eventType !== "rename")
257
+ return;
258
+ const oldTimer = timers.get(filePath);
259
+ if (oldTimer)
260
+ clearTimeout(oldTimer);
261
+ timers.set(
262
+ filePath,
263
+ setTimeout(async () => {
264
+ const configItem = localConfigs.find((c) => import_path.default.resolve(c.input) === filePath);
265
+ if (!configItem)
266
+ return;
267
+ const idx = localConfigs.indexOf(configItem);
268
+ conso.tips(`Detected change, regenerating: ${filePath}`);
269
+ try {
270
+ await startGenerate(configItem, idx);
271
+ } catch (err) {
272
+ conso.error(`Regeneration failed: ${err.message || err}`);
273
+ }
274
+ }, 200)
275
+ );
276
+ }
277
+ conso.tips(`Watching ${localConfigs.length} local document(s), Ctrl+C to exit`);
278
+ }
279
+ async function start(flags = {}) {
280
+ const timeLabel = import_chalk.default.green("Total elapsed");
281
+ console.time(timeLabel);
282
+ let resolved;
283
+ try {
284
+ resolved = await resolveConfigs(flags);
285
+ } catch (err) {
286
+ if (err instanceof NoInputError) {
287
+ printNoInputHint();
288
+ return;
289
+ }
290
+ import_spinner.spinnerInstance.stop();
291
+ conso.error(`Config resolution failed: ${err.message || err}`);
292
+ return;
293
+ }
294
+ conso.tips(
295
+ resolved.source === "cli" ? `Using CLI input: ${flags.input}` : `Found config file: ${resolved.configFile}`
296
+ );
297
+ try {
298
+ import_spinner.spinnerInstance.start("Generating code...\n");
299
+ await runAll(resolved.configs);
300
+ import_spinner.spinnerInstance.stop();
301
+ } catch (err) {
302
+ import_spinner.spinnerInstance.stop();
303
+ conso.error("Execution failed:");
304
+ conso.error(`Error: ${err.message || err}`);
305
+ if (err.stack) {
306
+ conso.error(`Stack: ${err.stack}`);
307
+ }
308
+ if (err.cause) {
309
+ conso.error(`Cause: ${err.cause}`);
310
+ }
311
+ return conso.error("Code generation failed, see error details above");
312
+ }
313
+ console.timeEnd(timeLabel);
314
+ if (flags.watch) {
315
+ watchConfigs(resolved.configs);
316
+ }
317
+ return null;
318
+ }
319
+ function toFlags(argv) {
320
+ return {
321
+ input: typeof argv.input === "string" && argv.input ? argv.input : void 0,
322
+ output: typeof argv.output === "string" ? argv.output : void 0,
323
+ name: typeof argv.name === "string" ? argv.name : void 0,
324
+ baseUrl: typeof argv.baseUrl === "string" ? argv.baseUrl : void 0,
325
+ client: typeof argv.client === "boolean" ? argv.client : void 0,
326
+ watch: Boolean(argv.watch)
327
+ };
328
+ }
329
+ var CLI = class {
330
+ run(args) {
331
+ const cli = this.init();
332
+ if (args.length === 0) {
333
+ cli.showHelp();
334
+ }
335
+ return cli.parse(args);
336
+ }
337
+ genBuilder(y) {
338
+ return y.positional("input", {
339
+ type: "string",
340
+ describe: "OpenAPI document URL or local JSON/YAML file path"
341
+ }).option("output", {
342
+ alias: "o",
343
+ type: "string",
344
+ describe: "Output directory (default: src/api)"
345
+ }).option("name", {
346
+ alias: "n",
347
+ type: "string",
348
+ describe: "Generated file name; filters by name in config-file mode"
349
+ }).option("base-url", {
350
+ type: "string",
351
+ describe: "Runtime baseURL (supports [code]: prefix for code emission)"
352
+ }).option("client", {
353
+ type: "boolean",
354
+ describe: "Generate default request.ts (use --no-client to disable, --client to force-enable)"
355
+ }).option("watch", {
356
+ alias: "w",
357
+ type: "boolean",
358
+ default: false,
359
+ describe: "Watch local document for changes and auto-regenerate"
360
+ });
361
+ }
362
+ init() {
363
+ return import_yargs.default.scriptName("apits").usage("Usage: $0 [gen] [input] [options]").command(
364
+ "init [input]",
365
+ "Scaffold an optional apits.config.ts file",
366
+ (y) => {
367
+ y.positional("input", {
368
+ type: "string",
369
+ describe: "Prefilled document URL or local file path"
370
+ });
371
+ },
372
+ async (argv) => {
373
+ await genConfig({ input: typeof argv.input === "string" ? argv.input : void 0 });
374
+ }
375
+ ).command(
376
+ "gen [input]",
377
+ "Generate TypeScript types and request functions from an OpenAPI document",
378
+ (y) => this.genBuilder(y),
379
+ (argv) => {
380
+ start(toFlags(argv));
381
+ }
382
+ ).command(
383
+ "$0 [input]",
384
+ false,
385
+ (y) => this.genBuilder(y),
386
+ (argv) => {
387
+ start(toFlags(argv));
388
+ }
389
+ ).help();
390
+ }
391
+ };
392
+ // Annotate the CommonJS export names for ESM import in node:
393
+ 0 && (module.exports = {
394
+ genConfig,
395
+ start
396
+ });