fexapi 0.1.1 → 0.1.3

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
@@ -22,25 +22,47 @@ fexapi [options]
22
22
  pnpm dlx fexapi init
23
23
  ```
24
24
 
25
+ `fexapi init` runs an interactive setup wizard and asks:
26
+
27
+ - What port? (default: 3000)
28
+ - Enable CORS? (Y/n)
29
+ - Generate sample schemas? (Y/n)
30
+
25
31
  Creates:
26
32
 
27
33
  - `fexapi/schema.fexapi`
28
34
  - `fexapi.config.json`
35
+ - `fexapi.config.js`
36
+ - `schemas/user.yaml` and `schemas/post.yaml` (if sample schemas are enabled)
29
37
 
30
38
  ### 2) Edit schema file
31
39
 
32
- `fexapi/schema.fexapi` uses a simple DSL with only `:` and `,` (no semicolons):
40
+ `fexapi/schema.fexapi` supports readable multi-line route fields (single-line still works):
33
41
 
34
42
  ```txt
35
43
  # Server
36
44
  port: 4100
37
45
 
38
46
  # Routes
39
- GET /users: id:uuid,name:name,email:email,age:number,phone:phone,pic:url,courseName:string
40
- GET /courses: id:uuid,courseName:string,mentor:name
47
+ GET /users:
48
+ id:uuid
49
+ name:name
50
+ email:email
51
+ age:number
52
+ phone:phone
53
+ pic:url
54
+ courseName:string
55
+
56
+ GET /courses:
57
+ id:uuid
58
+ courseName:string
59
+ mentor:name
60
+
61
+ # one-line format is also valid:
62
+ # GET /users: id:uuid,name:name,email:email
41
63
  ```
42
64
 
43
- ### 3) Generate artifacts (like migrations)
65
+ ### 3) Generate artifacts (updates migration)
44
66
 
45
67
  ```bash
46
68
  npx fexapi generate
@@ -49,7 +71,7 @@ npx fexapi generate
49
71
  Generates:
50
72
 
51
73
  - `fexapi/generated.api.json`
52
- - `fexapi/migrations/*.json`
74
+ - `fexapi/migrations/schema.json`
53
75
 
54
76
  ### 4) Start server
55
77
 
@@ -57,12 +79,31 @@ Generates:
57
79
  npx fexapi run
58
80
  # or
59
81
  npx fexapi serve
82
+ # request/response logging
83
+ npx fexapi serve --log
84
+ # or dev watch mode (nodemon-like)
85
+ npx fexapi dev --watch
86
+ # watch mode + request logs
87
+ npx fexapi dev --watch --log
60
88
  # or (inside local workspace package)
61
89
  npm run serve
62
90
  ```
63
91
 
64
92
  Server port is read from `schema.fexapi` unless overridden by CLI `--port`.
65
93
 
94
+ `fexapi dev --watch` auto-reloads when these files change:
95
+
96
+ - `fexapi/schema.fexapi`
97
+ - `fexapi/generated.api.json`
98
+ - `fexapi.config.js`
99
+ - `fexapi.config.json`
100
+ - `schemas/*.yaml`
101
+
102
+ `fexapi serve --log` prints request logs like:
103
+
104
+ - `[GET] /users/1 → 200 (45ms)`
105
+ - `[POST] /posts → 201 (12ms)`
106
+
66
107
  ## Configuration File Support
67
108
 
68
109
  Create a `fexapi.config.js` in your project root:
@@ -9,6 +9,15 @@ export declare const parseGenerateOptions: (generateArgs: string[]) => {
9
9
  export declare const parseServeOptions: (serveArgs: string[]) => {
10
10
  host: string;
11
11
  port?: number;
12
+ logEnabled: boolean;
13
+ } | {
14
+ error: string;
15
+ };
16
+ export declare const parseDevOptions: (devArgs: string[]) => {
17
+ host: string;
18
+ port?: number;
19
+ watchEnabled: boolean;
20
+ logEnabled: boolean;
12
21
  } | {
13
22
  error: string;
14
23
  };
@@ -1 +1 @@
1
- {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,EAAE,KACjB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAWtC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,cAAc,MAAM,EAAE,KACrB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAQlB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,MAAM,EAAE,KAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CA+CnD,CAAC"}
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,EAAE,KACjB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAWtC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,cAAc,MAAM,EAAE,KACrB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAQlB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,MAAM,EAAE,KAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAkDxE,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,EAAE,KAEf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAC3E;IAAE,KAAK,EAAE,MAAM,CAAA;CAwDlB,CAAC"}
package/dist/cli/args.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseServeOptions = exports.parseGenerateOptions = exports.parseInitOptions = void 0;
3
+ exports.parseDevOptions = exports.parseServeOptions = exports.parseGenerateOptions = exports.parseInitOptions = void 0;
4
4
  const parseInitOptions = (initArgs) => {
5
5
  const validFlags = new Set(["--force"]);
6
6
  const invalidFlags = initArgs.filter((value) => value.startsWith("-") && !validFlags.has(value));
@@ -30,7 +30,10 @@ const parseServeOptions = (serveArgs) => {
30
30
  }
31
31
  return value;
32
32
  };
33
- const unknownFlags = serveArgs.filter((value) => value.startsWith("-") && value !== "--host" && value !== "--port");
33
+ const unknownFlags = serveArgs.filter((value) => value.startsWith("-") &&
34
+ value !== "--host" &&
35
+ value !== "--port" &&
36
+ value !== "--log");
34
37
  if (unknownFlags.length > 0) {
35
38
  return { error: `Unknown option(s): ${unknownFlags.join(", ")}` };
36
39
  }
@@ -48,6 +51,48 @@ const parseServeOptions = (serveArgs) => {
48
51
  (!Number.isInteger(port) || port < 1 || port > 65535)) {
49
52
  return { error: `Invalid port: ${portValue ?? ""}`.trim() };
50
53
  }
51
- return { host, port };
54
+ return { host, port, logEnabled: serveArgs.includes("--log") };
52
55
  };
53
56
  exports.parseServeOptions = parseServeOptions;
57
+ const parseDevOptions = (devArgs) => {
58
+ const getFlagValue = (flagName) => {
59
+ const index = devArgs.indexOf(flagName);
60
+ if (index === -1) {
61
+ return undefined;
62
+ }
63
+ const value = devArgs[index + 1];
64
+ if (!value || value.startsWith("-")) {
65
+ return { error: `Missing value for ${flagName}` };
66
+ }
67
+ return value;
68
+ };
69
+ const unknownFlags = devArgs.filter((value) => value.startsWith("-") &&
70
+ value !== "--host" &&
71
+ value !== "--port" &&
72
+ value !== "--watch" &&
73
+ value !== "--log");
74
+ if (unknownFlags.length > 0) {
75
+ return { error: `Unknown option(s): ${unknownFlags.join(", ")}` };
76
+ }
77
+ const hostValue = getFlagValue("--host");
78
+ if (hostValue && typeof hostValue !== "string") {
79
+ return hostValue;
80
+ }
81
+ const portValue = getFlagValue("--port");
82
+ if (portValue && typeof portValue !== "string") {
83
+ return portValue;
84
+ }
85
+ const host = hostValue ?? "127.0.0.1";
86
+ const port = portValue ? Number(portValue) : undefined;
87
+ if (port !== undefined &&
88
+ (!Number.isInteger(port) || port < 1 || port > 65535)) {
89
+ return { error: `Invalid port: ${portValue ?? ""}`.trim() };
90
+ }
91
+ return {
92
+ host,
93
+ port,
94
+ watchEnabled: devArgs.includes("--watch"),
95
+ logEnabled: devArgs.includes("--log"),
96
+ };
97
+ };
98
+ exports.parseDevOptions = parseDevOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,YAmCrB,CAAC"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,YAgDrB,CAAC"}
package/dist/cli/help.js CHANGED
@@ -7,15 +7,19 @@ const printHelp = () => {
7
7
  console.log("Usage:");
8
8
  console.log(" fexapi init [--force]");
9
9
  console.log(" fexapi generate");
10
- console.log(" fexapi serve [--host <host>] [--port <number>]");
11
- console.log(" fexapi run [--host <host>] [--port <number>]");
12
- console.log(" fexapi [--host <host>] [--port <number>]");
10
+ console.log(" fexapi dev [--watch] [--host <host>] [--port <number>] [--log]");
11
+ console.log(" fexapi serve [--host <host>] [--port <number>] [--log]");
12
+ console.log(" fexapi run [--host <host>] [--port <number>] [--log]");
13
+ console.log(" fexapi [--host <host>] [--port <number>] [--log]");
13
14
  console.log(" fexapi --help");
14
15
  console.log("");
15
16
  console.log("Examples:");
16
17
  console.log(" fexapi init");
17
18
  console.log(" fexapi init --force");
18
19
  console.log(" fexapi generate");
20
+ console.log(" fexapi dev --watch");
21
+ console.log(" fexapi dev --watch --log");
22
+ console.log(" fexapi serve --log");
19
23
  console.log(" fexapi serve --host 127.0.0.1 --port 5000");
20
24
  console.log(" fexapi --port 4000");
21
25
  console.log("");
@@ -26,7 +30,14 @@ const printHelp = () => {
26
30
  console.log("");
27
31
  console.log("`fexapi init` creates:");
28
32
  console.log(" fexapi.config.json");
33
+ console.log(" fexapi.config.js");
29
34
  console.log(" fexapi/schema.fexapi");
35
+ console.log(" schemas/*.yaml (optional, via wizard)");
36
+ console.log("");
37
+ console.log("Init wizard asks:");
38
+ console.log(" What port? (default: 3000)");
39
+ console.log(" Enable CORS? (Y/n)");
40
+ console.log(" Generate sample schemas? (Y/n)");
30
41
  console.log("");
31
42
  console.log("Then run:");
32
43
  console.log(" # edit fexapi/schema.fexapi");
@@ -35,6 +46,6 @@ const printHelp = () => {
35
46
  console.log("");
36
47
  console.log("Generate output:");
37
48
  console.log(" fexapi/generated.api.json");
38
- console.log(" fexapi/migrations/*.json");
49
+ console.log(" fexapi/migrations/schema.json");
39
50
  };
40
51
  exports.printHelp = printHelp;
@@ -0,0 +1,7 @@
1
+ export declare const runDevCommand: ({ host, port, watchEnabled, logEnabled, }: {
2
+ host: string;
3
+ port?: number;
4
+ watchEnabled: boolean;
5
+ logEnabled: boolean;
6
+ }) => number;
7
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,aAAa,GAAI,2CAK3B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;CACrB,KAAG,MAuHH,CAAC"}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDevCommand = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const paths_1 = require("../project/paths");
7
+ const serve_1 = require("./serve");
8
+ const WATCH_DEBOUNCE_MS = 150;
9
+ const normalizePath = (pathValue) => {
10
+ return pathValue.replace(/\\/g, "/").toLowerCase();
11
+ };
12
+ const isWatchedPath = (projectRoot, changedPath) => {
13
+ const relativePath = normalizePath((0, node_path_1.relative)(projectRoot, changedPath));
14
+ if (relativePath === "fexapi.config.js" ||
15
+ relativePath === "fexapi.config.json") {
16
+ return true;
17
+ }
18
+ if (relativePath.startsWith("fexapi/")) {
19
+ return true;
20
+ }
21
+ return (relativePath.startsWith("schemas/") &&
22
+ (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml")));
23
+ };
24
+ const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
25
+ if (!watchEnabled) {
26
+ return (0, serve_1.serveProject)({ host, port, logEnabled });
27
+ }
28
+ const projectRoot = (0, paths_1.resolveProjectRoot)();
29
+ if (!projectRoot) {
30
+ console.error("Could not find package.json in this directory or parent directories.");
31
+ return 1;
32
+ }
33
+ let currentServer = (0, serve_1.createProjectServer)({ host, port, logEnabled });
34
+ if (!currentServer) {
35
+ return 1;
36
+ }
37
+ console.log("Watch mode enabled. Restarting on config/schema changes...");
38
+ let restartTimer;
39
+ let restartQueued = false;
40
+ let restartInProgress = false;
41
+ const restartServer = async (reason) => {
42
+ if (!currentServer) {
43
+ return;
44
+ }
45
+ if (restartInProgress) {
46
+ restartQueued = true;
47
+ return;
48
+ }
49
+ restartInProgress = true;
50
+ console.log(`\n[watch] change detected (${reason})`);
51
+ await new Promise((resolve) => {
52
+ currentServer?.close(() => {
53
+ resolve();
54
+ });
55
+ });
56
+ currentServer = (0, serve_1.createProjectServer)({ host, port, logEnabled });
57
+ restartInProgress = false;
58
+ if (restartQueued) {
59
+ restartQueued = false;
60
+ await restartServer("queued changes");
61
+ }
62
+ };
63
+ const scheduleRestart = (reason) => {
64
+ if (restartTimer) {
65
+ clearTimeout(restartTimer);
66
+ }
67
+ restartTimer = setTimeout(() => {
68
+ void restartServer(reason);
69
+ }, WATCH_DEBOUNCE_MS);
70
+ };
71
+ const activeWatchers = [];
72
+ const watchTargets = [
73
+ (0, node_path_1.join)(projectRoot, "fexapi"),
74
+ (0, node_path_1.join)(projectRoot, "schemas"),
75
+ projectRoot,
76
+ ];
77
+ for (const watchTarget of watchTargets) {
78
+ if (!(0, node_fs_1.existsSync)(watchTarget)) {
79
+ continue;
80
+ }
81
+ const watcher = (0, node_fs_1.watch)(watchTarget, { recursive: true }, (_event, file) => {
82
+ if (!file) {
83
+ scheduleRestart("unknown file");
84
+ return;
85
+ }
86
+ const changedPath = (0, node_path_1.join)(watchTarget, file.toString());
87
+ if (isWatchedPath(projectRoot, changedPath)) {
88
+ scheduleRestart((0, node_path_1.relative)(projectRoot, changedPath));
89
+ }
90
+ });
91
+ activeWatchers.push(watcher);
92
+ }
93
+ const cleanupAndExit = async () => {
94
+ if (restartTimer) {
95
+ clearTimeout(restartTimer);
96
+ restartTimer = undefined;
97
+ }
98
+ for (const watcher of activeWatchers) {
99
+ watcher.close();
100
+ }
101
+ await new Promise((resolve) => {
102
+ currentServer?.close(() => resolve());
103
+ });
104
+ process.exit(0);
105
+ };
106
+ process.on("SIGINT", () => {
107
+ void cleanupAndExit();
108
+ });
109
+ process.on("SIGTERM", () => {
110
+ void cleanupAndExit();
111
+ });
112
+ return 0;
113
+ };
114
+ exports.runDevCommand = runDevCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,QAAO,MAkGrC,CAAC"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,kBAAkB,QAAO,MAyGrC,CAAC"}
@@ -37,8 +37,16 @@ const generateFromSchema = () => {
37
37
  routes: parsed.schema.routes,
38
38
  };
39
39
  (0, node_fs_1.mkdirSync)(migrationsDirectoryPath, { recursive: true });
40
+ const existingMigrationFiles = (0, node_fs_1.readdirSync)(migrationsDirectoryPath, {
41
+ withFileTypes: true,
42
+ })
43
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
44
+ .map((entry) => (0, node_path_1.join)(migrationsDirectoryPath, entry.name));
45
+ for (const migrationFilePath of existingMigrationFiles) {
46
+ (0, node_fs_1.unlinkSync)(migrationFilePath);
47
+ }
40
48
  const migrationId = new Date().toISOString().replace(/[.:]/g, "-");
41
- const migrationPath = (0, node_path_1.join)(migrationsDirectoryPath, `${migrationId}_schema.json`);
49
+ const migrationPath = (0, node_path_1.join)(migrationsDirectoryPath, "schema.json");
42
50
  const migration = {
43
51
  migrationId,
44
52
  sourceSchema: "fexapi/schema.fexapi",
@@ -65,7 +73,7 @@ const generateFromSchema = () => {
65
73
  };
66
74
  (0, node_fs_1.writeFileSync)(configPath, `${JSON.stringify(updatedConfig, null, 2)}\n`, "utf-8");
67
75
  console.log(`Generated API spec at ${generatedPath}`);
68
- console.log(`Migration created at ${migrationPath}`);
76
+ console.log(`Migration updated at ${migrationPath}`);
69
77
  console.log(`Routes generated: ${parsed.schema.routes.length}`);
70
78
  console.log(`Configured server port: ${parsed.schema.port}`);
71
79
  return 0;
@@ -1,4 +1,4 @@
1
- export declare const initializeProject: ({ force }: {
1
+ export declare const initializeProject: ({ force, }: {
2
2
  force: boolean;
3
- }) => number;
3
+ }) => Promise<number>;
4
4
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,iBAAiB,GAAI,WAAW;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,KAAG,MAwEjE,CAAC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAwKA,eAAO,MAAM,iBAAiB,GAAU,YAErC;IACD,KAAK,EAAE,OAAO,CAAC;CAChB,KAAG,OAAO,CAAC,MAAM,CAmJjB,CAAC"}
@@ -3,10 +3,125 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.initializeProject = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
+ const promises_1 = require("node:readline/promises");
7
+ const node_process_1 = require("node:process");
6
8
  const constants_1 = require("../constants");
7
9
  const detect_1 = require("../project/detect");
8
10
  const paths_1 = require("../project/paths");
9
- const initializeProject = ({ force }) => {
11
+ const DEFAULT_INIT_PORT = 3000;
12
+ const parseYesNo = (value, defaultValue) => {
13
+ const normalized = value.trim().toLowerCase();
14
+ if (!normalized) {
15
+ return defaultValue;
16
+ }
17
+ if (normalized === "y" || normalized === "yes") {
18
+ return true;
19
+ }
20
+ if (normalized === "n" || normalized === "no") {
21
+ return false;
22
+ }
23
+ return undefined;
24
+ };
25
+ const askInitWizardQuestions = async () => {
26
+ if (!node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY) {
27
+ return {
28
+ port: DEFAULT_INIT_PORT,
29
+ cors: true,
30
+ generateSampleSchemas: true,
31
+ };
32
+ }
33
+ const questionInterface = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
34
+ try {
35
+ let port = DEFAULT_INIT_PORT;
36
+ while (true) {
37
+ const answer = await questionInterface.question(`What port? (default: ${DEFAULT_INIT_PORT}) `);
38
+ if (!answer.trim()) {
39
+ break;
40
+ }
41
+ const parsedPort = Number(answer.trim());
42
+ if (Number.isInteger(parsedPort) &&
43
+ parsedPort >= 1 &&
44
+ parsedPort <= 65535) {
45
+ port = parsedPort;
46
+ break;
47
+ }
48
+ console.log("Please enter a valid port (1-65535).\n");
49
+ }
50
+ let cors = true;
51
+ while (true) {
52
+ const answer = await questionInterface.question("Enable CORS? (Y/n) ");
53
+ const parsed = parseYesNo(answer, true);
54
+ if (parsed !== undefined) {
55
+ cors = parsed;
56
+ break;
57
+ }
58
+ console.log("Please answer with Y/Yes or N/No.\n");
59
+ }
60
+ let generateSampleSchemas = true;
61
+ while (true) {
62
+ const answer = await questionInterface.question("Generate sample schemas? (Y/n) ");
63
+ const parsed = parseYesNo(answer, true);
64
+ if (parsed !== undefined) {
65
+ generateSampleSchemas = parsed;
66
+ break;
67
+ }
68
+ console.log("Please answer with Y/Yes or N/No.\n");
69
+ }
70
+ console.log("");
71
+ return {
72
+ port,
73
+ cors,
74
+ generateSampleSchemas,
75
+ };
76
+ }
77
+ finally {
78
+ questionInterface.close();
79
+ }
80
+ };
81
+ const getRuntimeConfigTemplate = ({ port, cors, includeSampleRoutes, }) => {
82
+ const routeSection = includeSampleRoutes
83
+ ? [
84
+ " routes: {",
85
+ ' "/users": { count: 25, schema: "user" },',
86
+ ' "/posts": { count: 40, schema: "post" },',
87
+ " },",
88
+ ]
89
+ : [];
90
+ return [
91
+ "module.exports = {",
92
+ ` port: ${port},`,
93
+ ` cors: ${cors},`,
94
+ " delay: 0,",
95
+ ...routeSection,
96
+ "};",
97
+ ].join("\n");
98
+ };
99
+ const SAMPLE_USER_SCHEMA = [
100
+ "id:",
101
+ " type: uuid",
102
+ "fullName:",
103
+ " type: name",
104
+ " faker: person.fullName",
105
+ "email:",
106
+ " type: email",
107
+ " faker: internet.email",
108
+ "avatarUrl:",
109
+ " type: url",
110
+ " faker: image.avatar",
111
+ ].join("\n");
112
+ const SAMPLE_POST_SCHEMA = [
113
+ "id:",
114
+ " type: uuid",
115
+ "title:",
116
+ " type: string",
117
+ " faker: lorem.sentence",
118
+ "body:",
119
+ " type: string",
120
+ " faker: lorem.paragraph",
121
+ "createdAt:",
122
+ " type: date",
123
+ ].join("\n");
124
+ const initializeProject = async ({ force, }) => {
10
125
  const packageJsonPath = (0, paths_1.findClosestPackageJson)(process.cwd());
11
126
  if (!packageJsonPath) {
12
127
  console.error("Could not find package.json in this directory or parent directories.");
@@ -17,6 +132,11 @@ const initializeProject = ({ force }) => {
17
132
  const fexapiDirectoryPath = (0, node_path_1.join)(projectRoot, "fexapi");
18
133
  const schemaPath = (0, node_path_1.join)(fexapiDirectoryPath, "schema.fexapi");
19
134
  const configPath = (0, node_path_1.join)(projectRoot, "fexapi.config.json");
135
+ const runtimeConfigPath = (0, node_path_1.join)(projectRoot, "fexapi.config.js");
136
+ const schemasDirectoryPath = (0, node_path_1.join)(projectRoot, "schemas");
137
+ const userSchemaPath = (0, node_path_1.join)(schemasDirectoryPath, "user.yaml");
138
+ const postSchemaPath = (0, node_path_1.join)(schemasDirectoryPath, "post.yaml");
139
+ const wizardAnswers = await askInitWizardQuestions();
20
140
  (0, node_fs_1.mkdirSync)(fexapiDirectoryPath, { recursive: true });
21
141
  const configExists = (0, node_fs_1.existsSync)(configPath);
22
142
  const schemaExists = (0, node_fs_1.existsSync)(schemaPath);
@@ -26,13 +146,45 @@ const initializeProject = ({ force }) => {
26
146
  tooling: detectedProject.tooling,
27
147
  schemaPath: "fexapi/schema.fexapi",
28
148
  generatedPath: constants_1.GENERATED_SPEC_RELATIVE_PATH,
149
+ defaultPort: wizardAnswers.port,
150
+ corsEnabled: wizardAnswers.cors,
151
+ sampleSchemasGenerated: wizardAnswers.generateSampleSchemas,
29
152
  createdAt: new Date().toISOString(),
30
153
  };
31
154
  if (!configExists || force) {
32
155
  (0, node_fs_1.writeFileSync)(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
33
156
  }
34
157
  if (!schemaExists || force) {
35
- (0, node_fs_1.writeFileSync)(schemaPath, `${(0, detect_1.getSchemaTemplate)(detectedProject.primaryFramework)}\n`, "utf-8");
158
+ (0, node_fs_1.writeFileSync)(schemaPath, `${(0, detect_1.getSchemaTemplate)(detectedProject.primaryFramework, wizardAnswers.port)}\n`, "utf-8");
159
+ }
160
+ const runtimeConfigExists = (0, node_fs_1.existsSync)(runtimeConfigPath);
161
+ if (!runtimeConfigExists || force) {
162
+ (0, node_fs_1.writeFileSync)(runtimeConfigPath, `${getRuntimeConfigTemplate({
163
+ port: wizardAnswers.port,
164
+ cors: wizardAnswers.cors,
165
+ includeSampleRoutes: wizardAnswers.generateSampleSchemas,
166
+ })}\n`, "utf-8");
167
+ }
168
+ let userSchemaStatus = "skipped";
169
+ let postSchemaStatus = "skipped";
170
+ if (wizardAnswers.generateSampleSchemas) {
171
+ (0, node_fs_1.mkdirSync)(schemasDirectoryPath, { recursive: true });
172
+ const userSchemaExists = (0, node_fs_1.existsSync)(userSchemaPath);
173
+ if (!userSchemaExists || force) {
174
+ (0, node_fs_1.writeFileSync)(userSchemaPath, `${SAMPLE_USER_SCHEMA}\n`, "utf-8");
175
+ userSchemaStatus = userSchemaExists ? "overwritten" : "created";
176
+ }
177
+ else {
178
+ userSchemaStatus = "exists";
179
+ }
180
+ const postSchemaExists = (0, node_fs_1.existsSync)(postSchemaPath);
181
+ if (!postSchemaExists || force) {
182
+ (0, node_fs_1.writeFileSync)(postSchemaPath, `${SAMPLE_POST_SCHEMA}\n`, "utf-8");
183
+ postSchemaStatus = postSchemaExists ? "overwritten" : "created";
184
+ }
185
+ else {
186
+ postSchemaStatus = "exists";
187
+ }
36
188
  }
37
189
  console.log(`Initialized Fexapi in ${projectRoot}`);
38
190
  console.log(`Detected framework: ${detectedProject.primaryFramework}`);
@@ -58,6 +210,38 @@ const initializeProject = ({ force }) => {
58
210
  else {
59
211
  console.log(`Created ${schemaPath}`);
60
212
  }
213
+ if (runtimeConfigExists && !force) {
214
+ console.log(`Exists ${runtimeConfigPath}`);
215
+ }
216
+ else if (runtimeConfigExists && force) {
217
+ console.log(`Overwritten ${runtimeConfigPath}`);
218
+ }
219
+ else {
220
+ console.log(`Created ${runtimeConfigPath}`);
221
+ }
222
+ if (wizardAnswers.generateSampleSchemas) {
223
+ if (userSchemaStatus === "exists") {
224
+ console.log(`Exists ${userSchemaPath}`);
225
+ }
226
+ else if (userSchemaStatus === "overwritten") {
227
+ console.log(`Overwritten ${userSchemaPath}`);
228
+ }
229
+ else if (userSchemaStatus === "created") {
230
+ console.log(`Created ${userSchemaPath}`);
231
+ }
232
+ if (postSchemaStatus === "exists") {
233
+ console.log(`Exists ${postSchemaPath}`);
234
+ }
235
+ else if (postSchemaStatus === "overwritten") {
236
+ console.log(`Overwritten ${postSchemaPath}`);
237
+ }
238
+ else if (postSchemaStatus === "created") {
239
+ console.log(`Created ${postSchemaPath}`);
240
+ }
241
+ }
242
+ else {
243
+ console.log("Sample schemas were skipped.");
244
+ }
61
245
  if (detectedProject.primaryFramework === "unknown") {
62
246
  console.log("No known framework dependency found. Update fexapi.config.json and schema.fexapi if needed.");
63
247
  }
@@ -1,5 +1,12 @@
1
- export declare const serveProject: ({ host, port, }: {
1
+ import type { Server } from "node:http";
2
+ export declare const createProjectServer: ({ host, port, logEnabled, }: {
2
3
  host: string;
3
4
  port?: number;
5
+ logEnabled?: boolean;
6
+ }) => Server | undefined;
7
+ export declare const serveProject: ({ host, port, logEnabled, }: {
8
+ host: string;
9
+ port?: number;
10
+ logEnabled?: boolean;
4
11
  }) => number;
5
12
  //# sourceMappingURL=serve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,GAAI,iBAG1B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,KAAG,MAkEH,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAQxC,eAAO,MAAM,mBAAmB,GAAI,6BAIjC;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,KAAG,MAAM,GAAG,SA2DZ,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,6BAI1B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,KAAG,MAsBH,CAAC"}
@@ -1,14 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.serveProject = void 0;
3
+ exports.serveProject = exports.createProjectServer = void 0;
4
4
  const constants_1 = require("../constants");
5
5
  const generated_spec_1 = require("../config/generated-spec");
6
6
  const runtime_config_1 = require("../config/runtime-config");
7
7
  const schema_definitions_1 = require("../config/schema-definitions");
8
8
  const paths_1 = require("../project/paths");
9
9
  const server_1 = require("../server");
10
- const serveProject = ({ host, port, }) => {
10
+ const createProjectServer = ({ host, port, logEnabled = false, }) => {
11
11
  const projectRoot = (0, paths_1.resolveProjectRoot)();
12
+ if (!projectRoot) {
13
+ console.error("Could not find package.json in this directory or parent directories.");
14
+ return undefined;
15
+ }
12
16
  const runtimeConfig = projectRoot
13
17
  ? (0, runtime_config_1.loadFexapiRuntimeConfig)(projectRoot)
14
18
  : undefined;
@@ -33,13 +37,21 @@ const serveProject = ({ host, port, }) => {
33
37
  Object.keys(runtimeConfig.routes).length === 0) {
34
38
  console.log("No generated schema found. Run `fexapi generate` to serve schema-defined endpoints.");
35
39
  }
36
- const server = (0, server_1.startServer)({
40
+ return (0, server_1.startServer)({
37
41
  host,
38
42
  port: effectivePort,
39
43
  apiSpec: generatedSpec,
40
44
  runtimeConfig,
41
45
  schemaDefinitions,
46
+ logRequests: logEnabled,
42
47
  });
48
+ };
49
+ exports.createProjectServer = createProjectServer;
50
+ const serveProject = ({ host, port, logEnabled, }) => {
51
+ const server = (0, exports.createProjectServer)({ host, port, logEnabled });
52
+ if (!server) {
53
+ return 1;
54
+ }
43
55
  const shutdown = () => {
44
56
  server.close((error) => {
45
57
  if (error) {
package/dist/index.js CHANGED
@@ -2,71 +2,97 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const args_1 = require("./cli/args");
5
+ const dev_1 = require("./commands/dev");
5
6
  const help_1 = require("./cli/help");
6
7
  const generate_1 = require("./commands/generate");
7
8
  const init_1 = require("./commands/init");
8
9
  const serve_1 = require("./commands/serve");
9
10
  const args = process.argv.slice(2);
10
11
  const [firstArg, ...restArgs] = args;
11
- if (firstArg === "init") {
12
- if (restArgs.includes("--help") || restArgs.includes("-h")) {
13
- console.log("Usage: fexapi init [--force]");
14
- console.log("Detects frameworks/tooling and creates fexapi.config.json + fexapi/schema.fexapi.");
15
- console.log("Use --force to overwrite existing files.");
16
- process.exit(0);
12
+ const main = async () => {
13
+ if (firstArg === "init") {
14
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
15
+ console.log("Usage: fexapi init [--force]");
16
+ console.log("Runs an interactive setup wizard and creates fexapi config/schema files.");
17
+ console.log("Use --force to overwrite existing files.");
18
+ process.exit(0);
19
+ }
20
+ const initOptions = (0, args_1.parseInitOptions)(restArgs);
21
+ if ("error" in initOptions) {
22
+ console.error(initOptions.error);
23
+ console.log("");
24
+ console.log("Usage: fexapi init [--force]");
25
+ process.exit(1);
26
+ }
27
+ process.exit(await (0, init_1.initializeProject)({ force: initOptions.force }));
17
28
  }
18
- const initOptions = (0, args_1.parseInitOptions)(restArgs);
19
- if ("error" in initOptions) {
20
- console.error(initOptions.error);
21
- console.log("");
22
- console.log("Usage: fexapi init [--force]");
23
- process.exit(1);
29
+ else if (firstArg === "generate") {
30
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
31
+ console.log("Usage: fexapi generate");
32
+ console.log("Reads fexapi/schema.fexapi and updates generated API artifacts + migration.");
33
+ process.exit(0);
34
+ }
35
+ const generateOptions = (0, args_1.parseGenerateOptions)(restArgs);
36
+ if (generateOptions.error) {
37
+ console.error(generateOptions.error);
38
+ console.log("");
39
+ console.log("Usage: fexapi generate");
40
+ process.exit(1);
41
+ }
42
+ process.exit((0, generate_1.generateFromSchema)());
24
43
  }
25
- process.exit((0, init_1.initializeProject)({ force: initOptions.force }));
26
- }
27
- else if (firstArg === "generate") {
28
- if (restArgs.includes("--help") || restArgs.includes("-h")) {
29
- console.log("Usage: fexapi generate");
30
- console.log("Reads fexapi/schema.fexapi and creates generated API artifacts + migrations.");
31
- process.exit(0);
44
+ else if (firstArg === "dev") {
45
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
46
+ console.log("Usage: fexapi dev [--watch] [--host <host>] [--port <number>] [--log]");
47
+ console.log("Starts development server and optionally auto-reloads when config/schema files change.");
48
+ process.exit(0);
49
+ }
50
+ const devOptions = (0, args_1.parseDevOptions)(restArgs);
51
+ if ("error" in devOptions) {
52
+ console.error(devOptions.error);
53
+ console.log("");
54
+ console.log("Usage: fexapi dev [--watch] [--host <host>] [--port <number>] [--log]");
55
+ process.exit(1);
56
+ }
57
+ const exitCode = (0, dev_1.runDevCommand)(devOptions);
58
+ if (exitCode !== 0) {
59
+ process.exit(exitCode);
60
+ }
32
61
  }
33
- const generateOptions = (0, args_1.parseGenerateOptions)(restArgs);
34
- if (generateOptions.error) {
35
- console.error(generateOptions.error);
36
- console.log("");
37
- console.log("Usage: fexapi generate");
38
- process.exit(1);
62
+ else if (!firstArg ||
63
+ firstArg === "serve" ||
64
+ firstArg === "run" ||
65
+ firstArg.startsWith("-")) {
66
+ const serveArgs = firstArg === "serve" || firstArg === "run" ? restArgs : args;
67
+ if (serveArgs.includes("--help") || serveArgs.includes("-h")) {
68
+ (0, help_1.printHelp)();
69
+ process.exit(0);
70
+ }
71
+ const options = (0, args_1.parseServeOptions)(serveArgs);
72
+ if ("error" in options) {
73
+ console.error(options.error);
74
+ console.log("");
75
+ (0, help_1.printHelp)();
76
+ process.exit(1);
77
+ }
78
+ const exitCode = (0, serve_1.serveProject)(options);
79
+ if (exitCode !== 0) {
80
+ process.exit(exitCode);
81
+ }
39
82
  }
40
- process.exit((0, generate_1.generateFromSchema)());
41
- }
42
- else if (!firstArg ||
43
- firstArg === "serve" ||
44
- firstArg === "run" ||
45
- firstArg.startsWith("-")) {
46
- const serveArgs = firstArg === "serve" || firstArg === "run" ? restArgs : args;
47
- if (serveArgs.includes("--help") || serveArgs.includes("-h")) {
83
+ else if (firstArg === "help") {
48
84
  (0, help_1.printHelp)();
49
85
  process.exit(0);
50
86
  }
51
- const options = (0, args_1.parseServeOptions)(serveArgs);
52
- if ("error" in options) {
53
- console.error(options.error);
87
+ else {
88
+ console.error(`Unknown command: ${firstArg}`);
54
89
  console.log("");
55
90
  (0, help_1.printHelp)();
56
91
  process.exit(1);
57
92
  }
58
- const exitCode = (0, serve_1.serveProject)(options);
59
- if (exitCode !== 0) {
60
- process.exit(exitCode);
61
- }
62
- }
63
- else if (firstArg === "help") {
64
- (0, help_1.printHelp)();
65
- process.exit(0);
66
- }
67
- else {
68
- console.error(`Unknown command: ${firstArg}`);
69
- console.log("");
70
- (0, help_1.printHelp)();
93
+ };
94
+ void main().catch((error) => {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ console.error(`Unexpected error: ${message}`);
71
97
  process.exit(1);
72
- }
98
+ });
@@ -1,4 +1,4 @@
1
1
  import type { DetectedProject, SupportedFramework } from "../types/project";
2
2
  export declare const detectProject: (packageJsonPath: string, projectRoot: string) => DetectedProject;
3
- export declare const getSchemaTemplate: (framework: SupportedFramework) => string;
3
+ export declare const getSchemaTemplate: (framework: SupportedFramework, port?: number) => string;
4
4
  //# sourceMappingURL=detect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/project/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAyD5E,eAAO,MAAM,aAAa,GACxB,iBAAiB,MAAM,EACvB,aAAa,MAAM,KAClB,eAwDF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,WAAW,kBAAkB,KAAG,MAkBjE,CAAC"}
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/project/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAyD5E,eAAO,MAAM,aAAa,GACxB,iBAAiB,MAAM,EACvB,aAAa,MAAM,KAClB,eAwDF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,kBAAkB,EAC7B,aAAW,KACV,MA+BF,CAAC"}
@@ -93,7 +93,7 @@ const detectProject = (packageJsonPath, projectRoot) => {
93
93
  };
94
94
  };
95
95
  exports.detectProject = detectProject;
96
- const getSchemaTemplate = (framework) => {
96
+ const getSchemaTemplate = (framework, port = 4000) => {
97
97
  const frameworkHint = framework === "nextjs"
98
98
  ? "# Framework: Next.js"
99
99
  : framework === "reactjs"
@@ -102,12 +102,25 @@ const getSchemaTemplate = (framework) => {
102
102
  return [
103
103
  frameworkHint,
104
104
  "# Server",
105
- "port: 4000",
105
+ `port: ${port}`,
106
106
  "",
107
107
  "# Routes",
108
- "# Format: METHOD /endpoint: field:type,field:type",
109
- "GET /users: id:uuid,fullName:name,username:string,email:email,avatarUrl:url",
110
- "GET /posts: id:uuid,title:string,body:string,createdAt:date",
108
+ "# Format (single-line): METHOD /endpoint: field:type,field:type",
109
+ "# Format (multi-line):",
110
+ "# METHOD /endpoint:",
111
+ "# field:type",
112
+ "# field:type",
113
+ "GET /users:",
114
+ " id:uuid",
115
+ " fullName:name",
116
+ " username:string",
117
+ " email:email",
118
+ " avatarUrl:url",
119
+ "GET /posts:",
120
+ " id:uuid",
121
+ " title:string",
122
+ " body:string",
123
+ " createdAt:date",
111
124
  ].join("\n");
112
125
  };
113
126
  exports.getSchemaTemplate = getSchemaTemplate;
@@ -1 +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"}
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;AAqHF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,MAAM,KACjB;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAmG3C,CAAC"}
package/dist/schema.js CHANGED
@@ -13,6 +13,28 @@ const VALID_TYPES = [
13
13
  "phone",
14
14
  ];
15
15
  const DEFAULT_PORT = 4000;
16
+ const ROUTE_FORMAT_ERROR_MESSAGE = "Invalid route definition. Expected format: " +
17
+ "METHOD /endpoint: field:type,field:type (or multiline fields under METHOD /endpoint:)";
18
+ const isPortLine = (line) => /^port\s*:/i.test(line.trim());
19
+ const parseRouteHeader = (line) => {
20
+ const separatorIndex = line.indexOf(":");
21
+ if (separatorIndex === -1) {
22
+ return undefined;
23
+ }
24
+ const rawLeft = line.slice(0, separatorIndex);
25
+ const rawFields = line.slice(separatorIndex + 1);
26
+ const [rawMethod, rawPath] = rawLeft.trim().split(/\s+/, 2);
27
+ const method = rawMethod?.toUpperCase();
28
+ const path = rawPath?.trim();
29
+ if (!method || !path || !path.startsWith("/")) {
30
+ return undefined;
31
+ }
32
+ return {
33
+ method,
34
+ path,
35
+ inlineFields: rawFields,
36
+ };
37
+ };
16
38
  const parseField = (rawField) => {
17
39
  const [rawName, rawType] = rawField.split(":");
18
40
  const name = rawName?.trim();
@@ -33,42 +55,23 @@ const parseField = (rawField) => {
33
55
  type: type,
34
56
  };
35
57
  };
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();
58
+ const parseRoute = ({ method, path, rawFields, }) => {
53
59
  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
+ return { error: ROUTE_FORMAT_ERROR_MESSAGE };
60
61
  }
61
62
  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 };
63
+ for (const rawFieldLine of rawFields) {
64
+ for (const part of rawFieldLine.split(",")) {
65
+ const trimmedPart = part.trim();
66
+ if (!trimmedPart) {
67
+ continue;
68
+ }
69
+ const parsedField = parseField(trimmedPart);
70
+ if ("error" in parsedField) {
71
+ return { error: parsedField.error };
72
+ }
73
+ fields.push(parsedField);
70
74
  }
71
- fields.push(parsedField);
72
75
  }
73
76
  if (fields.length === 0) {
74
77
  return { error: `Route ${method} ${path} has no valid fields.` };
@@ -79,13 +82,15 @@ const parseFexapiSchema = (schemaText) => {
79
82
  let port = DEFAULT_PORT;
80
83
  const routes = [];
81
84
  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();
85
+ const lines = schemaText.split(/\r?\n/);
86
+ for (let index = 0; index < lines.length; index += 1) {
87
+ const rawLine = lines[index] ?? "";
88
+ const trimmedLine = rawLine.trim();
89
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
90
+ continue;
91
+ }
92
+ if (isPortLine(trimmedLine)) {
93
+ const rawPort = trimmedLine.slice(trimmedLine.indexOf(":") + 1).trim();
89
94
  const parsedPort = Number(rawPort);
90
95
  if (!Number.isInteger(parsedPort) ||
91
96
  parsedPort < 1 ||
@@ -97,12 +102,45 @@ const parseFexapiSchema = (schemaText) => {
97
102
  }
98
103
  continue;
99
104
  }
100
- const parsedRoute = parseRoute(line);
105
+ const header = parseRouteHeader(trimmedLine);
106
+ if (!header) {
107
+ errors.push(ROUTE_FORMAT_ERROR_MESSAGE);
108
+ continue;
109
+ }
110
+ const rawFields = [];
111
+ if (header.inlineFields.trim()) {
112
+ rawFields.push(header.inlineFields);
113
+ }
114
+ let lookaheadIndex = index + 1;
115
+ while (lookaheadIndex < lines.length) {
116
+ const lookaheadRawLine = lines[lookaheadIndex] ?? "";
117
+ const lookaheadTrimmedLine = lookaheadRawLine.trim();
118
+ if (!lookaheadTrimmedLine || lookaheadTrimmedLine.startsWith("#")) {
119
+ lookaheadIndex += 1;
120
+ continue;
121
+ }
122
+ if (isPortLine(lookaheadTrimmedLine) ||
123
+ parseRouteHeader(lookaheadTrimmedLine)) {
124
+ break;
125
+ }
126
+ if (/^\s+/.test(lookaheadRawLine)) {
127
+ rawFields.push(lookaheadTrimmedLine.replace(/^-+\s*/, ""));
128
+ lookaheadIndex += 1;
129
+ continue;
130
+ }
131
+ break;
132
+ }
133
+ const parsedRoute = parseRoute({
134
+ method: header.method,
135
+ path: header.path,
136
+ rawFields,
137
+ });
101
138
  if ("error" in parsedRoute) {
102
139
  errors.push(parsedRoute.error);
103
140
  continue;
104
141
  }
105
142
  routes.push(parsedRoute);
143
+ index = lookaheadIndex - 1;
106
144
  }
107
145
  if (routes.length === 0) {
108
146
  errors.push("No routes defined in schema.fexapi.");
package/dist/server.d.ts CHANGED
@@ -7,10 +7,11 @@ export type ServerOptions = {
7
7
  apiSpec?: GeneratedApiSpec;
8
8
  runtimeConfig?: FexapiRuntimeConfig;
9
9
  schemaDefinitions?: FexapiSchemaDefinitions;
10
+ logRequests?: boolean;
10
11
  };
11
12
  export type GeneratedApiSpec = {
12
13
  port: number;
13
14
  routes: FexapiRoute[];
14
15
  };
15
- export declare const startServer: ({ host, port, apiSpec, runtimeConfig, schemaDefinitions, }?: ServerOptions) => import("http").Server<typeof import("http").IncomingMessage, typeof ServerResponse>;
16
+ export declare const startServer: ({ host, port, apiSpec, runtimeConfig, schemaDefinitions, logRequests, }?: ServerOptions) => import("http").Server<typeof import("http").IncomingMessage, typeof ServerResponse>;
16
17
  //# sourceMappingURL=server.d.ts.map
@@ -1 +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;AACzD,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EAExB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAyLF,eAAO,MAAM,WAAW,GAAI,6DAMzB,aAAkB,wFAqGpB,CAAC"}
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;AACzD,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EAExB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAyLF,eAAO,MAAM,WAAW,GAAI,0EAOzB,aAAkB,wFAmGpB,CAAC"}
package/dist/server.js CHANGED
@@ -143,7 +143,7 @@ const getCountFromUrl = (urlText, fallback = 5) => {
143
143
  }
144
144
  return Math.min(Math.max(Math.floor(rawCount), 1), 50);
145
145
  };
146
- const startServer = ({ host = DEFAULT_HOST, port = DEFAULT_PORT, apiSpec, runtimeConfig, schemaDefinitions = {}, } = {}) => {
146
+ const startServer = ({ host = DEFAULT_HOST, port = DEFAULT_PORT, apiSpec, runtimeConfig, schemaDefinitions = {}, logRequests = false, } = {}) => {
147
147
  const corsEnabled = runtimeConfig?.cors ?? false;
148
148
  const responseDelay = runtimeConfig?.delay ?? 0;
149
149
  const configuredRoutes = runtimeConfig?.routes ?? {};
@@ -152,11 +152,19 @@ const startServer = ({ host = DEFAULT_HOST, port = DEFAULT_PORT, apiSpec, runtim
152
152
  ? apiSpec.routes.map((route) => `${route.method} ${route.path}`)
153
153
  : [];
154
154
  const availableRoutes = [
155
- "GET /health",
156
155
  ...new Set([...availableConfiguredRoutes, ...availableSchemaRoutes]),
157
156
  ];
158
157
  const server = (0, node_http_1.createServer)((request, response) => {
158
+ const requestStartedAt = Date.now();
159
159
  const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
160
+ if (logRequests) {
161
+ response.on("finish", () => {
162
+ const method = request.method ?? "UNKNOWN";
163
+ const durationMs = Date.now() - requestStartedAt;
164
+ const statusCode = response.statusCode;
165
+ console.log(`[${method}] ${pathname} → ${statusCode} (${durationMs}ms)`);
166
+ });
167
+ }
160
168
  if (corsEnabled && request.method === "OPTIONS") {
161
169
  response.writeHead(204, {
162
170
  "Access-Control-Allow-Origin": "*",
@@ -166,13 +174,6 @@ const startServer = ({ host = DEFAULT_HOST, port = DEFAULT_PORT, apiSpec, runtim
166
174
  response.end();
167
175
  return;
168
176
  }
169
- if (request.method === "GET" && pathname === "/health") {
170
- sendJson(response, 200, {
171
- ok: true,
172
- timestamp: new Date().toISOString(),
173
- }, { cors: corsEnabled, delay: responseDelay });
174
- return;
175
- }
176
177
  if (request.method === "GET") {
177
178
  const configuredRoute = configuredRoutes[pathname];
178
179
  if (configuredRoute) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fexapi",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Mock API generation CLI tool for local development and testing",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,6 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc -p tsconfig.json",
16
+ "lint": "pnpm exec eslint . --max-warnings 0",
16
17
  "check-types": "tsc --noEmit",
17
18
  "start": "node dist/index.js",
18
19
  "serve": "node dist/index.js serve",
@@ -40,7 +41,9 @@
40
41
  },
41
42
  "homepage": "https://github.com/shreeteja172/fexapi#readme",
42
43
  "devDependencies": {
44
+ "@repo/eslint-config": "workspace:*",
43
45
  "@types/node": "^22.15.3",
46
+ "eslint": "^9.39.1",
44
47
  "typescript": "5.9.2"
45
48
  },
46
49
  "dependencies": {