fexapi 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/README.md +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +435 -0
- package/dist/schema.d.ts +19 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +121 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +128 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# FexAPI
|
|
2
|
+
|
|
3
|
+
Frontend Experience API - Mock API generation CLI tool for local development and testing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
```bash
|
|
7
|
+
npm install -g fexapi
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```bash
|
|
12
|
+
fexapi [options]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Schema-based mock API generation
|
|
18
|
+
- Local development server
|
|
19
|
+
- Faker.js integration
|
|
20
|
+
- Easy setup for testing workflows
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
24
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const schema_1 = require("./schema");
|
|
7
|
+
const server_1 = require("./server");
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const GENERATED_SPEC_RELATIVE_PATH = "fexapi/generated.api.json";
|
|
10
|
+
const printHelp = () => {
|
|
11
|
+
console.log("fexapi-cli");
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log("Usage:");
|
|
14
|
+
console.log(" fexapi init [--force]");
|
|
15
|
+
console.log(" fexapi generate");
|
|
16
|
+
console.log(" fexapi serve [--host <host>] [--port <number>]");
|
|
17
|
+
console.log(" fexapi [--host <host>] [--port <number>]");
|
|
18
|
+
console.log(" fexapi --help");
|
|
19
|
+
console.log("");
|
|
20
|
+
console.log("Examples:");
|
|
21
|
+
console.log(" fexapi init");
|
|
22
|
+
console.log(" fexapi init --force");
|
|
23
|
+
console.log(" fexapi generate");
|
|
24
|
+
console.log(" fexapi serve --host 127.0.0.1 --port 5000");
|
|
25
|
+
console.log(" fexapi --port 4000");
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log("Package manager usage:");
|
|
28
|
+
console.log(" npx fexapi init");
|
|
29
|
+
console.log(" pnpm dlx fexapi init");
|
|
30
|
+
console.log(" yarn dlx fexapi init");
|
|
31
|
+
console.log("");
|
|
32
|
+
console.log("`fexapi init` creates:");
|
|
33
|
+
console.log(" fexapi.config.json");
|
|
34
|
+
console.log(" fexapi/schema.fexapi");
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log("Then run:");
|
|
37
|
+
console.log(" fexapi generate");
|
|
38
|
+
console.log(" fexapi serve");
|
|
39
|
+
};
|
|
40
|
+
const findClosestPackageJson = (startDirectory) => {
|
|
41
|
+
let currentDirectory = startDirectory;
|
|
42
|
+
while (true) {
|
|
43
|
+
const candidate = (0, node_path_1.join)(currentDirectory, "package.json");
|
|
44
|
+
if ((0, node_fs_1.existsSync)(candidate)) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
const parentDirectory = (0, node_path_1.dirname)(currentDirectory);
|
|
48
|
+
if (parentDirectory === currentDirectory) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
currentDirectory = parentDirectory;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const readDependencyNames = (packageJsonPath) => {
|
|
55
|
+
const packageJsonText = (0, node_fs_1.readFileSync)(packageJsonPath, "utf-8");
|
|
56
|
+
const packageJson = JSON.parse(packageJsonText);
|
|
57
|
+
const dependencies = (packageJson.dependencies ?? {});
|
|
58
|
+
const devDependencies = (packageJson.devDependencies ?? {});
|
|
59
|
+
return new Set([
|
|
60
|
+
...Object.keys(dependencies),
|
|
61
|
+
...Object.keys(devDependencies),
|
|
62
|
+
]);
|
|
63
|
+
};
|
|
64
|
+
const readWorkspaceDependencyNames = (projectRoot) => {
|
|
65
|
+
const result = new Set();
|
|
66
|
+
const rootsToScan = [
|
|
67
|
+
(0, node_path_1.join)(projectRoot, "apps"),
|
|
68
|
+
(0, node_path_1.join)(projectRoot, "packages"),
|
|
69
|
+
];
|
|
70
|
+
for (const rootPath of rootsToScan) {
|
|
71
|
+
if (!(0, node_fs_1.existsSync)(rootPath)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of (0, node_fs_1.readdirSync)(rootPath)) {
|
|
75
|
+
const entryPath = (0, node_path_1.join)(rootPath, entry);
|
|
76
|
+
if (!(0, node_fs_1.statSync)(entryPath).isDirectory()) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const packageJsonPath = (0, node_path_1.join)(entryPath, "package.json");
|
|
80
|
+
if (!(0, node_fs_1.existsSync)(packageJsonPath)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const dependencyNames = readDependencyNames(packageJsonPath);
|
|
84
|
+
for (const dependencyName of dependencyNames) {
|
|
85
|
+
result.add(dependencyName);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
const detectProject = (packageJsonPath, projectRoot) => {
|
|
92
|
+
const dependencyNames = readDependencyNames(packageJsonPath);
|
|
93
|
+
const frameworks = new Set();
|
|
94
|
+
const tooling = new Set();
|
|
95
|
+
if (dependencyNames.has("turbo") ||
|
|
96
|
+
(0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, "turbo.json"))) {
|
|
97
|
+
tooling.add("turborepo");
|
|
98
|
+
}
|
|
99
|
+
if (dependencyNames.has("nx") || (0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, "nx.json"))) {
|
|
100
|
+
tooling.add("nx");
|
|
101
|
+
}
|
|
102
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, "pnpm-workspace.yaml"))) {
|
|
103
|
+
tooling.add("pnpm-workspace");
|
|
104
|
+
}
|
|
105
|
+
if (tooling.has("turborepo") || tooling.has("pnpm-workspace")) {
|
|
106
|
+
const workspaceDeps = readWorkspaceDependencyNames(projectRoot);
|
|
107
|
+
for (const dependencyName of workspaceDeps) {
|
|
108
|
+
dependencyNames.add(dependencyName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (dependencyNames.has("next")) {
|
|
112
|
+
frameworks.add("nextjs");
|
|
113
|
+
}
|
|
114
|
+
if (dependencyNames.has("react") || dependencyNames.has("react-dom")) {
|
|
115
|
+
frameworks.add("reactjs");
|
|
116
|
+
}
|
|
117
|
+
if (dependencyNames.has("vue")) {
|
|
118
|
+
frameworks.add("vue");
|
|
119
|
+
}
|
|
120
|
+
if (dependencyNames.has("nuxt")) {
|
|
121
|
+
frameworks.add("nuxt");
|
|
122
|
+
}
|
|
123
|
+
if (dependencyNames.has("svelte")) {
|
|
124
|
+
frameworks.add("svelte");
|
|
125
|
+
}
|
|
126
|
+
if (dependencyNames.has("@sveltejs/kit")) {
|
|
127
|
+
frameworks.add("sveltekit");
|
|
128
|
+
}
|
|
129
|
+
if (dependencyNames.has("@angular/core")) {
|
|
130
|
+
frameworks.add("angular");
|
|
131
|
+
}
|
|
132
|
+
if (dependencyNames.has("solid-js")) {
|
|
133
|
+
frameworks.add("solid");
|
|
134
|
+
}
|
|
135
|
+
if (dependencyNames.has("@remix-run/react") ||
|
|
136
|
+
dependencyNames.has("@remix-run/node")) {
|
|
137
|
+
frameworks.add("remix");
|
|
138
|
+
}
|
|
139
|
+
if (dependencyNames.has("astro")) {
|
|
140
|
+
frameworks.add("astro");
|
|
141
|
+
}
|
|
142
|
+
if (dependencyNames.has("vite")) {
|
|
143
|
+
tooling.add("vite");
|
|
144
|
+
}
|
|
145
|
+
const frameworkList = Array.from(frameworks);
|
|
146
|
+
const primaryFramework = frameworkList[0] ?? "unknown";
|
|
147
|
+
return {
|
|
148
|
+
primaryFramework,
|
|
149
|
+
frameworks: frameworkList.length > 0 ? frameworkList : ["unknown"],
|
|
150
|
+
tooling: Array.from(tooling),
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const getSchemaTemplate = (framework) => {
|
|
154
|
+
const frameworkHint = framework === "nextjs"
|
|
155
|
+
? "# Framework: Next.js"
|
|
156
|
+
: framework === "reactjs"
|
|
157
|
+
? "# Framework: React"
|
|
158
|
+
: "# Framework: unknown";
|
|
159
|
+
return [
|
|
160
|
+
frameworkHint,
|
|
161
|
+
"# Server",
|
|
162
|
+
"port: 4000",
|
|
163
|
+
"",
|
|
164
|
+
"# Routes",
|
|
165
|
+
"# Format: METHOD /endpoint: field:type,field:type",
|
|
166
|
+
"GET /users: id:uuid,fullName:name,username:string,email:email,avatarUrl:url",
|
|
167
|
+
"GET /posts: id:uuid,title:string,body:string,createdAt:date",
|
|
168
|
+
].join("\n");
|
|
169
|
+
};
|
|
170
|
+
const parseInitOptions = (initArgs) => {
|
|
171
|
+
const validFlags = new Set(["--force"]);
|
|
172
|
+
const invalidFlags = initArgs.filter((value) => value.startsWith("-") && !validFlags.has(value));
|
|
173
|
+
if (invalidFlags.length > 0) {
|
|
174
|
+
return { error: `Unknown option(s): ${invalidFlags.join(", ")}` };
|
|
175
|
+
}
|
|
176
|
+
return { force: initArgs.includes("--force") };
|
|
177
|
+
};
|
|
178
|
+
const parseGenerateOptions = (generateArgs) => {
|
|
179
|
+
const invalidFlags = generateArgs.filter((value) => value.startsWith("-"));
|
|
180
|
+
if (invalidFlags.length > 0) {
|
|
181
|
+
return { error: `Unknown option(s): ${invalidFlags.join(", ")}` };
|
|
182
|
+
}
|
|
183
|
+
return {};
|
|
184
|
+
};
|
|
185
|
+
const resolveProjectRoot = () => {
|
|
186
|
+
const packageJsonPath = findClosestPackageJson(process.cwd());
|
|
187
|
+
if (!packageJsonPath) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
return (0, node_path_1.dirname)(packageJsonPath);
|
|
191
|
+
};
|
|
192
|
+
const initializeProject = ({ force }) => {
|
|
193
|
+
const packageJsonPath = findClosestPackageJson(process.cwd());
|
|
194
|
+
if (!packageJsonPath) {
|
|
195
|
+
console.error("Could not find package.json in this directory or parent directories.");
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const projectRoot = (0, node_path_1.dirname)(packageJsonPath);
|
|
199
|
+
const detectedProject = detectProject(packageJsonPath, projectRoot);
|
|
200
|
+
const fexapiDirectoryPath = (0, node_path_1.join)(projectRoot, "fexapi");
|
|
201
|
+
const schemaPath = (0, node_path_1.join)(fexapiDirectoryPath, "schema.fexapi");
|
|
202
|
+
const configPath = (0, node_path_1.join)(projectRoot, "fexapi.config.json");
|
|
203
|
+
(0, node_fs_1.mkdirSync)(fexapiDirectoryPath, { recursive: true });
|
|
204
|
+
const configExists = (0, node_fs_1.existsSync)(configPath);
|
|
205
|
+
const schemaExists = (0, node_fs_1.existsSync)(schemaPath);
|
|
206
|
+
const config = {
|
|
207
|
+
framework: detectedProject.primaryFramework,
|
|
208
|
+
frameworks: detectedProject.frameworks,
|
|
209
|
+
tooling: detectedProject.tooling,
|
|
210
|
+
schemaPath: "fexapi/schema.fexapi",
|
|
211
|
+
generatedPath: GENERATED_SPEC_RELATIVE_PATH,
|
|
212
|
+
createdAt: new Date().toISOString(),
|
|
213
|
+
};
|
|
214
|
+
if (!configExists || force) {
|
|
215
|
+
(0, node_fs_1.writeFileSync)(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
216
|
+
}
|
|
217
|
+
if (!schemaExists || force) {
|
|
218
|
+
(0, node_fs_1.writeFileSync)(schemaPath, `${getSchemaTemplate(detectedProject.primaryFramework)}\n`, "utf-8");
|
|
219
|
+
}
|
|
220
|
+
console.log(`Initialized Fexapi in ${projectRoot}`);
|
|
221
|
+
console.log(`Detected framework: ${detectedProject.primaryFramework}`);
|
|
222
|
+
console.log(`Detected frameworks: ${detectedProject.frameworks.join(", ")}`);
|
|
223
|
+
if (detectedProject.tooling.length > 0) {
|
|
224
|
+
console.log(`Detected tooling: ${detectedProject.tooling.join(", ")}`);
|
|
225
|
+
}
|
|
226
|
+
if (configExists && !force) {
|
|
227
|
+
console.log(`Exists ${configPath}`);
|
|
228
|
+
}
|
|
229
|
+
else if (configExists && force) {
|
|
230
|
+
console.log(`Overwritten ${configPath}`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(`Created ${configPath}`);
|
|
234
|
+
}
|
|
235
|
+
if (schemaExists && !force) {
|
|
236
|
+
console.log(`Exists ${schemaPath}`);
|
|
237
|
+
}
|
|
238
|
+
else if (schemaExists && force) {
|
|
239
|
+
console.log(`Overwritten ${schemaPath}`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(`Created ${schemaPath}`);
|
|
243
|
+
}
|
|
244
|
+
if (detectedProject.primaryFramework === "unknown") {
|
|
245
|
+
console.log("No known framework dependency found. Update fexapi.config.json and schema.fexapi if needed.");
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
};
|
|
249
|
+
const generateFromSchema = () => {
|
|
250
|
+
const projectRoot = resolveProjectRoot();
|
|
251
|
+
if (!projectRoot) {
|
|
252
|
+
console.error("Could not find package.json in this directory or parent directories.");
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
const schemaPath = (0, node_path_1.join)(projectRoot, "fexapi", "schema.fexapi");
|
|
256
|
+
const generatedPath = (0, node_path_1.join)(projectRoot, "fexapi", "generated.api.json");
|
|
257
|
+
const configPath = (0, node_path_1.join)(projectRoot, "fexapi.config.json");
|
|
258
|
+
if (!(0, node_fs_1.existsSync)(schemaPath)) {
|
|
259
|
+
console.error(`Schema file not found: ${schemaPath}`);
|
|
260
|
+
console.error("Run `fexapi init` first.");
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const schemaText = (0, node_fs_1.readFileSync)(schemaPath, "utf-8");
|
|
264
|
+
const parsed = (0, schema_1.parseFexapiSchema)(schemaText);
|
|
265
|
+
if (parsed.errors.length > 0 || !parsed.schema) {
|
|
266
|
+
console.error("Failed to generate API from schema.fexapi");
|
|
267
|
+
for (const error of parsed.errors) {
|
|
268
|
+
console.error(`- ${error}`);
|
|
269
|
+
}
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
const generated = {
|
|
273
|
+
schemaVersion: 1,
|
|
274
|
+
generatedAt: new Date().toISOString(),
|
|
275
|
+
port: parsed.schema.port,
|
|
276
|
+
routes: parsed.schema.routes,
|
|
277
|
+
};
|
|
278
|
+
(0, node_fs_1.writeFileSync)(generatedPath, `${JSON.stringify(generated, null, 2)}\n`, "utf-8");
|
|
279
|
+
let existingConfig = {};
|
|
280
|
+
if ((0, node_fs_1.existsSync)(configPath)) {
|
|
281
|
+
try {
|
|
282
|
+
existingConfig = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
existingConfig = {};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const updatedConfig = {
|
|
289
|
+
...existingConfig,
|
|
290
|
+
schemaPath: "fexapi/schema.fexapi",
|
|
291
|
+
generatedPath: GENERATED_SPEC_RELATIVE_PATH,
|
|
292
|
+
lastGeneratedAt: new Date().toISOString(),
|
|
293
|
+
};
|
|
294
|
+
(0, node_fs_1.writeFileSync)(configPath, `${JSON.stringify(updatedConfig, null, 2)}\n`, "utf-8");
|
|
295
|
+
console.log(`Generated API spec at ${generatedPath}`);
|
|
296
|
+
console.log(`Routes generated: ${parsed.schema.routes.length}`);
|
|
297
|
+
console.log(`Configured server port: ${parsed.schema.port}`);
|
|
298
|
+
return 0;
|
|
299
|
+
};
|
|
300
|
+
const parseServeOptions = (serveArgs) => {
|
|
301
|
+
const getFlagValue = (flagName) => {
|
|
302
|
+
const index = serveArgs.indexOf(flagName);
|
|
303
|
+
if (index === -1) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const value = serveArgs[index + 1];
|
|
307
|
+
if (!value || value.startsWith("-")) {
|
|
308
|
+
return { error: `Missing value for ${flagName}` };
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
};
|
|
312
|
+
const unknownFlags = serveArgs.filter((value) => value.startsWith("-") && value !== "--host" && value !== "--port");
|
|
313
|
+
if (unknownFlags.length > 0) {
|
|
314
|
+
return { error: `Unknown option(s): ${unknownFlags.join(", ")}` };
|
|
315
|
+
}
|
|
316
|
+
const hostValue = getFlagValue("--host");
|
|
317
|
+
if (hostValue && typeof hostValue !== "string") {
|
|
318
|
+
return hostValue;
|
|
319
|
+
}
|
|
320
|
+
const portValue = getFlagValue("--port");
|
|
321
|
+
if (portValue && typeof portValue !== "string") {
|
|
322
|
+
return portValue;
|
|
323
|
+
}
|
|
324
|
+
const host = hostValue ?? "127.0.0.1";
|
|
325
|
+
const port = portValue ? Number(portValue) : undefined;
|
|
326
|
+
if (port !== undefined &&
|
|
327
|
+
(!Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
328
|
+
return { error: `Invalid port: ${portValue ?? ""}`.trim() };
|
|
329
|
+
}
|
|
330
|
+
return { host, port };
|
|
331
|
+
};
|
|
332
|
+
const loadGeneratedApiSpec = (projectRoot) => {
|
|
333
|
+
const generatedPath = (0, node_path_1.join)(projectRoot, GENERATED_SPEC_RELATIVE_PATH);
|
|
334
|
+
if (!(0, node_fs_1.existsSync)(generatedPath)) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(generatedPath, "utf-8"));
|
|
339
|
+
if (typeof parsed.port !== "number" || !Array.isArray(parsed.routes)) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
port: parsed.port,
|
|
344
|
+
routes: parsed.routes,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const [firstArg, ...restArgs] = args;
|
|
352
|
+
if (firstArg === "init") {
|
|
353
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
354
|
+
console.log("Usage: fexapi init [--force]");
|
|
355
|
+
console.log("Detects frameworks/tooling and creates fexapi.config.json + fexapi/schema.fexapi.");
|
|
356
|
+
console.log("Use --force to overwrite existing files.");
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
const initOptions = parseInitOptions(restArgs);
|
|
360
|
+
if ("error" in initOptions) {
|
|
361
|
+
console.error(initOptions.error);
|
|
362
|
+
console.log("");
|
|
363
|
+
console.log("Usage: fexapi init [--force]");
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
const exitCode = initializeProject({ force: initOptions.force });
|
|
367
|
+
process.exit(exitCode);
|
|
368
|
+
}
|
|
369
|
+
else if (firstArg === "generate") {
|
|
370
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
371
|
+
console.log("Usage: fexapi generate");
|
|
372
|
+
console.log("Reads fexapi/schema.fexapi and creates fexapi/generated.api.json.");
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
const generateOptions = parseGenerateOptions(restArgs);
|
|
376
|
+
if (generateOptions.error) {
|
|
377
|
+
console.error(generateOptions.error);
|
|
378
|
+
console.log("");
|
|
379
|
+
console.log("Usage: fexapi generate");
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const exitCode = generateFromSchema();
|
|
383
|
+
process.exit(exitCode);
|
|
384
|
+
}
|
|
385
|
+
else if (!firstArg || firstArg === "serve" || firstArg.startsWith("-")) {
|
|
386
|
+
const serveArgs = firstArg === "serve" ? restArgs : args;
|
|
387
|
+
if (serveArgs.includes("--help") || serveArgs.includes("-h")) {
|
|
388
|
+
printHelp();
|
|
389
|
+
process.exit(0);
|
|
390
|
+
}
|
|
391
|
+
const options = parseServeOptions(serveArgs);
|
|
392
|
+
if ("error" in options) {
|
|
393
|
+
console.error(options.error);
|
|
394
|
+
console.log("");
|
|
395
|
+
printHelp();
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
const projectRoot = resolveProjectRoot();
|
|
399
|
+
const generatedSpec = projectRoot
|
|
400
|
+
? loadGeneratedApiSpec(projectRoot)
|
|
401
|
+
: undefined;
|
|
402
|
+
const effectivePort = options.port ?? generatedSpec?.port ?? 4000;
|
|
403
|
+
if (generatedSpec) {
|
|
404
|
+
console.log(`Using generated schema routes (${generatedSpec.routes.length}) from ${GENERATED_SPEC_RELATIVE_PATH}`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log("No generated schema found. Run `fexapi generate` to serve schema-defined endpoints.");
|
|
408
|
+
}
|
|
409
|
+
const server = (0, server_1.startServer)({
|
|
410
|
+
host: options.host,
|
|
411
|
+
port: effectivePort,
|
|
412
|
+
apiSpec: generatedSpec,
|
|
413
|
+
});
|
|
414
|
+
const shutdown = () => {
|
|
415
|
+
server.close((error) => {
|
|
416
|
+
if (error) {
|
|
417
|
+
console.error("Error while shutting down server", error);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
process.exit(0);
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
process.on("SIGINT", shutdown);
|
|
424
|
+
process.on("SIGTERM", shutdown);
|
|
425
|
+
}
|
|
426
|
+
else if (firstArg === "help") {
|
|
427
|
+
printHelp();
|
|
428
|
+
process.exit(0);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.error(`Unknown command: ${firstArg}`);
|
|
432
|
+
console.log("");
|
|
433
|
+
printHelp();
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type FexapiFieldType = "number" | "string" | "boolean" | "date" | "uuid" | "email" | "url" | "name" | "phone";
|
|
2
|
+
export type FexapiField = {
|
|
3
|
+
name: string;
|
|
4
|
+
type: FexapiFieldType;
|
|
5
|
+
};
|
|
6
|
+
export type FexapiRoute = {
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
fields: FexapiField[];
|
|
10
|
+
};
|
|
11
|
+
export type FexapiSchema = {
|
|
12
|
+
port: number;
|
|
13
|
+
routes: FexapiRoute[];
|
|
14
|
+
};
|
|
15
|
+
export declare const parseFexapiSchema: (schemaText: string) => {
|
|
16
|
+
schema?: FexapiSchema;
|
|
17
|
+
errors: string[];
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAkGF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,MAAM,KACjB;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAoD3C,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseFexapiSchema = void 0;
|
|
4
|
+
const VALID_TYPES = [
|
|
5
|
+
"number",
|
|
6
|
+
"string",
|
|
7
|
+
"boolean",
|
|
8
|
+
"date",
|
|
9
|
+
"uuid",
|
|
10
|
+
"email",
|
|
11
|
+
"url",
|
|
12
|
+
"name",
|
|
13
|
+
"phone",
|
|
14
|
+
];
|
|
15
|
+
const DEFAULT_PORT = 4000;
|
|
16
|
+
const parseField = (rawField) => {
|
|
17
|
+
const [rawName, rawType] = rawField.split(":");
|
|
18
|
+
const name = rawName?.trim();
|
|
19
|
+
const type = rawType?.trim().toLowerCase();
|
|
20
|
+
if (!name) {
|
|
21
|
+
return { error: `Invalid field "${rawField}". Missing field name.` };
|
|
22
|
+
}
|
|
23
|
+
if (!type) {
|
|
24
|
+
return { error: `Invalid field "${rawField}". Missing field type.` };
|
|
25
|
+
}
|
|
26
|
+
if (!VALID_TYPES.includes(type)) {
|
|
27
|
+
return {
|
|
28
|
+
error: `Unknown type "${type}" in field "${name}". Valid types: ${VALID_TYPES.join(", ")}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
type: type,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const parseRoute = (line) => {
|
|
37
|
+
const separatorIndex = line.indexOf(":");
|
|
38
|
+
if (separatorIndex === -1) {
|
|
39
|
+
return {
|
|
40
|
+
error: "Invalid route definition. Expected format: METHOD /endpoint: field:type,field:type",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const rawLeft = line.slice(0, separatorIndex);
|
|
44
|
+
const rawFields = line.slice(separatorIndex + 1);
|
|
45
|
+
if (!rawLeft || !rawFields) {
|
|
46
|
+
return {
|
|
47
|
+
error: "Invalid route definition. Expected format: METHOD /endpoint: field:type,field:type",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const [rawMethod, rawPath] = rawLeft.trim().split(/\s+/, 2);
|
|
51
|
+
const method = rawMethod?.toUpperCase();
|
|
52
|
+
const path = rawPath?.trim();
|
|
53
|
+
if (!method || !path) {
|
|
54
|
+
return {
|
|
55
|
+
error: "Invalid route definition. Missing METHOD or /endpoint before ':'.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!path.startsWith("/")) {
|
|
59
|
+
return { error: `Route path must start with '/': ${path}` };
|
|
60
|
+
}
|
|
61
|
+
const fields = [];
|
|
62
|
+
for (const part of rawFields.split(",")) {
|
|
63
|
+
const trimmedPart = part.trim();
|
|
64
|
+
if (!trimmedPart) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const parsedField = parseField(trimmedPart);
|
|
68
|
+
if ("error" in parsedField) {
|
|
69
|
+
return { error: parsedField.error };
|
|
70
|
+
}
|
|
71
|
+
fields.push(parsedField);
|
|
72
|
+
}
|
|
73
|
+
if (fields.length === 0) {
|
|
74
|
+
return { error: `Route ${method} ${path} has no valid fields.` };
|
|
75
|
+
}
|
|
76
|
+
return { method, path, fields };
|
|
77
|
+
};
|
|
78
|
+
const parseFexapiSchema = (schemaText) => {
|
|
79
|
+
let port = DEFAULT_PORT;
|
|
80
|
+
const routes = [];
|
|
81
|
+
const errors = [];
|
|
82
|
+
const lines = schemaText
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.map((line) => line.trim())
|
|
85
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.toLowerCase().startsWith("port:")) {
|
|
88
|
+
const rawPort = line.slice(line.indexOf(":") + 1).trim();
|
|
89
|
+
const parsedPort = Number(rawPort);
|
|
90
|
+
if (!Number.isInteger(parsedPort) ||
|
|
91
|
+
parsedPort < 1 ||
|
|
92
|
+
parsedPort > 65535) {
|
|
93
|
+
errors.push(`Invalid port value: ${rawPort}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
port = parsedPort;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const parsedRoute = parseRoute(line);
|
|
101
|
+
if ("error" in parsedRoute) {
|
|
102
|
+
errors.push(parsedRoute.error);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
routes.push(parsedRoute);
|
|
106
|
+
}
|
|
107
|
+
if (routes.length === 0) {
|
|
108
|
+
errors.push("No routes defined in schema.fexapi.");
|
|
109
|
+
}
|
|
110
|
+
if (errors.length > 0) {
|
|
111
|
+
return { errors };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
schema: {
|
|
115
|
+
port,
|
|
116
|
+
routes,
|
|
117
|
+
},
|
|
118
|
+
errors: [],
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
exports.parseFexapiSchema = parseFexapiSchema;
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { FexapiRoute } from "./schema";
|
|
3
|
+
export type ServerOptions = {
|
|
4
|
+
host?: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
apiSpec?: GeneratedApiSpec;
|
|
7
|
+
};
|
|
8
|
+
export type GeneratedApiSpec = {
|
|
9
|
+
port: number;
|
|
10
|
+
routes: FexapiRoute[];
|
|
11
|
+
};
|
|
12
|
+
export declare const startServer: ({ host, port, apiSpec, }?: ServerOptions) => import("http").Server<typeof import("http").IncomingMessage, typeof ServerResponse>;
|
|
13
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AA2FF,eAAO,MAAM,WAAW,GAAI,2BAIzB,aAAkB,wFA8DpB,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startServer = void 0;
|
|
4
|
+
const faker_1 = require("@faker-js/faker");
|
|
5
|
+
const node_http_1 = require("node:http");
|
|
6
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
7
|
+
const DEFAULT_PORT = 4000;
|
|
8
|
+
const sendJson = (response, statusCode, payload) => {
|
|
9
|
+
response.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
10
|
+
response.end(JSON.stringify(payload));
|
|
11
|
+
};
|
|
12
|
+
const createValueFromField = (field) => {
|
|
13
|
+
switch (field.type) {
|
|
14
|
+
case "number":
|
|
15
|
+
return faker_1.faker.number.int({ min: 1, max: 10000 });
|
|
16
|
+
case "string":
|
|
17
|
+
return faker_1.faker.lorem.words({ min: 1, max: 4 });
|
|
18
|
+
case "boolean":
|
|
19
|
+
return faker_1.faker.datatype.boolean();
|
|
20
|
+
case "date":
|
|
21
|
+
return faker_1.faker.date.recent({ days: 30 }).toISOString();
|
|
22
|
+
case "uuid":
|
|
23
|
+
return faker_1.faker.string.uuid();
|
|
24
|
+
case "email":
|
|
25
|
+
return faker_1.faker.internet.email();
|
|
26
|
+
case "url":
|
|
27
|
+
return faker_1.faker.internet.url();
|
|
28
|
+
case "name":
|
|
29
|
+
return faker_1.faker.person.fullName();
|
|
30
|
+
case "phone":
|
|
31
|
+
return faker_1.faker.phone.number();
|
|
32
|
+
default:
|
|
33
|
+
return faker_1.faker.lorem.word();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const createRecordFromRoute = (route) => {
|
|
37
|
+
return route.fields.reduce((record, field) => {
|
|
38
|
+
record[field.name] = createValueFromField(field);
|
|
39
|
+
return record;
|
|
40
|
+
}, {});
|
|
41
|
+
};
|
|
42
|
+
const toCollectionKey = (routePath) => {
|
|
43
|
+
const segments = routePath.split("/").filter(Boolean);
|
|
44
|
+
const lastSegment = segments[segments.length - 1];
|
|
45
|
+
if (!lastSegment) {
|
|
46
|
+
return "data";
|
|
47
|
+
}
|
|
48
|
+
return lastSegment.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
49
|
+
};
|
|
50
|
+
const createMockUser = () => {
|
|
51
|
+
return {
|
|
52
|
+
id: faker_1.faker.string.uuid(),
|
|
53
|
+
fullName: faker_1.faker.person.fullName(),
|
|
54
|
+
username: faker_1.faker.internet.username(),
|
|
55
|
+
email: faker_1.faker.internet.email(),
|
|
56
|
+
avatarUrl: faker_1.faker.image.avatar(),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const createMockPost = () => {
|
|
60
|
+
return {
|
|
61
|
+
id: faker_1.faker.string.uuid(),
|
|
62
|
+
title: faker_1.faker.lorem.sentence(),
|
|
63
|
+
body: faker_1.faker.lorem.paragraphs({ min: 1, max: 3 }),
|
|
64
|
+
createdAt: faker_1.faker.date.recent({ days: 14 }).toISOString(),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const getCountFromUrl = (urlText, fallback = 5) => {
|
|
68
|
+
if (!urlText) {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
const url = new URL(urlText, "http://localhost");
|
|
72
|
+
const rawCount = Number(url.searchParams.get("count") ?? fallback);
|
|
73
|
+
if (!Number.isFinite(rawCount)) {
|
|
74
|
+
return fallback;
|
|
75
|
+
}
|
|
76
|
+
return Math.min(Math.max(Math.floor(rawCount), 1), 50);
|
|
77
|
+
};
|
|
78
|
+
const startServer = ({ host = DEFAULT_HOST, port = DEFAULT_PORT, apiSpec, } = {}) => {
|
|
79
|
+
const server = (0, node_http_1.createServer)((request, response) => {
|
|
80
|
+
const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
81
|
+
if (request.method === "GET" && pathname === "/health") {
|
|
82
|
+
sendJson(response, 200, {
|
|
83
|
+
ok: true,
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (apiSpec) {
|
|
89
|
+
const matchedRoute = apiSpec.routes.find((route) => route.method === request.method && route.path === pathname);
|
|
90
|
+
if (matchedRoute) {
|
|
91
|
+
const count = getCountFromUrl(request.url, 5);
|
|
92
|
+
const payloadKey = toCollectionKey(matchedRoute.path);
|
|
93
|
+
sendJson(response, 200, {
|
|
94
|
+
[payloadKey]: Array.from({ length: count }, () => createRecordFromRoute(matchedRoute)),
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (request.method === "GET" && pathname === "/users") {
|
|
100
|
+
const count = getCountFromUrl(request.url, 8);
|
|
101
|
+
sendJson(response, 200, {
|
|
102
|
+
users: Array.from({ length: count }, createMockUser),
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (request.method === "GET" && pathname === "/posts") {
|
|
107
|
+
const count = getCountFromUrl(request.url, 5);
|
|
108
|
+
sendJson(response, 200, {
|
|
109
|
+
posts: Array.from({ length: count }, createMockPost),
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
sendJson(response, 404, {
|
|
114
|
+
message: "Route not found",
|
|
115
|
+
availableRoutes: apiSpec
|
|
116
|
+
? [
|
|
117
|
+
"GET /health",
|
|
118
|
+
...apiSpec.routes.map((route) => `${route.method} ${route.path}`),
|
|
119
|
+
]
|
|
120
|
+
: ["GET /health", "GET /users?count=10", "GET /posts?count=5"],
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
server.listen(port, host, () => {
|
|
124
|
+
console.log(`Mock API running at http://${host}:${port}`);
|
|
125
|
+
});
|
|
126
|
+
return server;
|
|
127
|
+
};
|
|
128
|
+
exports.startServer = startServer;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fexapi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mock API generation CLI tool for local development and testing",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"fexapi": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"check-types": "tsc --noEmit",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "pnpm build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mock",
|
|
23
|
+
"api",
|
|
24
|
+
"cli",
|
|
25
|
+
"testing",
|
|
26
|
+
"development",
|
|
27
|
+
"faker",
|
|
28
|
+
"mock-server"
|
|
29
|
+
],
|
|
30
|
+
"author": "Shreeteja Mutukundu smutukundu2006@gmail.com",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/shreeteja172/fexapi.git",
|
|
35
|
+
"directory": "apps/cli"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/shreeteja172/fexapi/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/shreeteja172/fexapi#readme",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.15.3",
|
|
43
|
+
"typescript": "5.9.2"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@faker-js/faker": "^10.3.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|