fexapi 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,YAiErB,CAAC"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,YAgErB,CAAC"}
package/dist/cli/help.js CHANGED
@@ -16,6 +16,7 @@ const printHelp = () => {
16
16
  console.log(` ${(0, ui_1.formatCommand)("fexapi run [--host <host>] [--port <number>] [--log]")}`);
17
17
  console.log(` ${(0, ui_1.formatCommand)("fexapi [--host <host>] [--port <number>] [--log]")}`);
18
18
  console.log(` ${(0, ui_1.formatCommand)("fexapi --version")}`);
19
+ console.log(` ${(0, ui_1.formatCommand)("fexapi version")}`);
19
20
  console.log(` ${(0, ui_1.formatCommand)("fexapi --help")}`);
20
21
  (0, ui_1.printSpacer)();
21
22
  console.log(ui_1.ui.bold("Examples"));
@@ -37,12 +38,10 @@ const printHelp = () => {
37
38
  console.log(ui_1.ui.bold("fexapi init creates"));
38
39
  console.log(` ${ui_1.ui.dim("fexapi.config.js")}`);
39
40
  console.log(` ${ui_1.ui.dim("fexapi/schema.fexapi")}`);
40
- console.log(` ${ui_1.ui.dim("fexapi/schemas/*.yaml (optional, via wizard)")}`);
41
41
  (0, ui_1.printSpacer)();
42
42
  console.log(ui_1.ui.bold("Init wizard asks"));
43
43
  console.log(` ${ui_1.ui.dim("What port? (default: 4000)")}`);
44
44
  console.log(` ${ui_1.ui.dim("Enable CORS? (Y/n)")}`);
45
- console.log(` ${ui_1.ui.dim("Generate sample schemas? (Y/n)")}`);
46
45
  (0, ui_1.printSpacer)();
47
46
  console.log(ui_1.ui.bold("Then run"));
48
47
  console.log(` ${ui_1.ui.dim("# edit fexapi/schema.fexapi")}`);
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AA8BA,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,MAmHH,CAAC"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AA0EA,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,MAuNH,CAAC"}
@@ -5,21 +5,53 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const ui_1 = require("../cli/ui");
7
7
  const paths_1 = require("../project/paths");
8
+ const generate_1 = require("./generate");
8
9
  const serve_1 = require("./serve");
9
10
  const WATCH_DEBOUNCE_MS = 150;
11
+ const GENERATED_SPEC_PATH = "fexapi/generated.api.json";
12
+ const GENERATED_SPEC_SUPPRESS_MS = 1200;
10
13
  const normalizePath = (pathValue) => {
11
14
  return pathValue.replace(/\\/g, "/").toLowerCase();
12
15
  };
13
- const isWatchedPath = (projectRoot, changedPath) => {
16
+ const getWatchReaction = (projectRoot, changedPath) => {
14
17
  const relativePath = normalizePath((0, node_path_1.relative)(projectRoot, changedPath));
15
18
  if (relativePath === "fexapi.config.js") {
16
- return true;
19
+ return {
20
+ shouldRestart: true,
21
+ shouldRegenerate: false,
22
+ reason: "fexapi.config.js changed",
23
+ };
24
+ }
25
+ if (relativePath === "fexapi/schema.fexapi") {
26
+ return {
27
+ shouldRestart: true,
28
+ shouldRegenerate: true,
29
+ reason: "schema.fexapi changed",
30
+ };
31
+ }
32
+ if (relativePath.startsWith("fexapi/schemas/") &&
33
+ (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml"))) {
34
+ return {
35
+ shouldRestart: true,
36
+ shouldRegenerate: true,
37
+ reason: "custom schema definition changed",
38
+ };
39
+ }
40
+ if (relativePath === GENERATED_SPEC_PATH) {
41
+ return {
42
+ shouldRestart: true,
43
+ shouldRegenerate: false,
44
+ reason: "generated.api.json changed",
45
+ };
17
46
  }
18
47
  if (relativePath.startsWith("fexapi/")) {
19
- return true;
48
+ return {
49
+ shouldRestart: true,
50
+ shouldRegenerate: false,
51
+ reason: `file changed (${relativePath})`,
52
+ };
20
53
  }
21
- return (relativePath.startsWith("fexapi/schemas/") &&
22
- (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml")));
54
+ return undefined;
23
55
  };
24
56
  const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
25
57
  if (!watchEnabled) {
@@ -38,34 +70,87 @@ const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
38
70
  let restartTimer;
39
71
  let restartQueued = false;
40
72
  let restartInProgress = false;
41
- const restartServer = async (reason) => {
42
- if (!currentServer) {
43
- return;
73
+ let pendingReasons = new Set();
74
+ let pendingRegeneration = false;
75
+ let suppressGeneratedSpecUntilMs = 0;
76
+ const collectPendingWatchChanges = ({ reason, shouldRegenerate, }) => {
77
+ pendingReasons.add(reason);
78
+ if (shouldRegenerate) {
79
+ pendingRegeneration = true;
44
80
  }
81
+ };
82
+ const restartServer = async ({ reason, shouldRegenerate, }) => {
45
83
  if (restartInProgress) {
46
84
  restartQueued = true;
85
+ collectPendingWatchChanges({ reason, shouldRegenerate });
47
86
  return;
48
87
  }
49
88
  restartInProgress = true;
50
89
  (0, ui_1.logInfo)(`[watch] change detected (${reason})`);
51
- await new Promise((resolve) => {
52
- currentServer?.close(() => {
53
- resolve();
90
+ if (shouldRegenerate) {
91
+ (0, ui_1.printSpacer)();
92
+ (0, ui_1.logInfo)("[watch] regenerating generated.api.json from schema changes...");
93
+ const generationExitCode = (0, generate_1.generateFromSchema)();
94
+ if (generationExitCode !== 0) {
95
+ (0, ui_1.logError)("[watch] generation failed; keeping previous server state.");
96
+ (0, ui_1.logWarn)("Fix schema errors and save again. Watch mode will retry automatically.");
97
+ restartInProgress = false;
98
+ if (restartQueued) {
99
+ restartQueued = false;
100
+ const queuedReasons = Array.from(pendingReasons);
101
+ const queuedRegeneration = pendingRegeneration;
102
+ pendingReasons = new Set();
103
+ pendingRegeneration = false;
104
+ await restartServer({
105
+ reason: queuedReasons.join(", ") || "queued changes",
106
+ shouldRegenerate: queuedRegeneration,
107
+ });
108
+ }
109
+ return;
110
+ }
111
+ suppressGeneratedSpecUntilMs = Date.now() + GENERATED_SPEC_SUPPRESS_MS;
112
+ }
113
+ if (currentServer) {
114
+ await new Promise((resolve) => {
115
+ currentServer?.close(() => {
116
+ resolve();
117
+ });
54
118
  });
55
- });
119
+ }
56
120
  currentServer = (0, serve_1.createProjectServer)({ host, port, logEnabled });
121
+ if (currentServer) {
122
+ (0, ui_1.logSuccess)("[watch] server reloaded");
123
+ }
124
+ else {
125
+ (0, ui_1.logError)("[watch] server reload failed; waiting for next change to retry.");
126
+ }
57
127
  restartInProgress = false;
58
128
  if (restartQueued) {
59
129
  restartQueued = false;
60
- await restartServer("queued changes");
130
+ const queuedReasons = Array.from(pendingReasons);
131
+ const queuedRegeneration = pendingRegeneration;
132
+ pendingReasons = new Set();
133
+ pendingRegeneration = false;
134
+ await restartServer({
135
+ reason: queuedReasons.join(", ") || "queued changes",
136
+ shouldRegenerate: queuedRegeneration,
137
+ });
61
138
  }
62
139
  };
63
- const scheduleRestart = (reason) => {
140
+ const scheduleRestart = ({ reason, shouldRegenerate, }) => {
141
+ collectPendingWatchChanges({ reason, shouldRegenerate });
64
142
  if (restartTimer) {
65
143
  clearTimeout(restartTimer);
66
144
  }
67
145
  restartTimer = setTimeout(() => {
68
- void restartServer(reason);
146
+ const reasons = Array.from(pendingReasons);
147
+ const requiresRegeneration = pendingRegeneration;
148
+ pendingReasons = new Set();
149
+ pendingRegeneration = false;
150
+ void restartServer({
151
+ reason: reasons.join(", ") || reason,
152
+ shouldRegenerate: requiresRegeneration,
153
+ });
69
154
  }, WATCH_DEBOUNCE_MS);
70
155
  };
71
156
  const activeWatchers = [];
@@ -76,12 +161,21 @@ const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
76
161
  }
77
162
  const watcher = (0, node_fs_1.watch)(watchTarget, { recursive: true }, (_event, file) => {
78
163
  if (!file) {
79
- scheduleRestart("unknown file");
164
+ scheduleRestart({ reason: "unknown file", shouldRegenerate: false });
80
165
  return;
81
166
  }
82
167
  const changedPath = (0, node_path_1.join)(watchTarget, file.toString());
83
- if (isWatchedPath(projectRoot, changedPath)) {
84
- scheduleRestart((0, node_path_1.relative)(projectRoot, changedPath));
168
+ const normalizedRelativePath = normalizePath((0, node_path_1.relative)(projectRoot, changedPath));
169
+ if (normalizedRelativePath === GENERATED_SPEC_PATH &&
170
+ Date.now() < suppressGeneratedSpecUntilMs) {
171
+ return;
172
+ }
173
+ const watchReaction = getWatchReaction(projectRoot, changedPath);
174
+ if (watchReaction?.shouldRestart) {
175
+ scheduleRestart({
176
+ reason: watchReaction.reason,
177
+ shouldRegenerate: watchReaction.shouldRegenerate,
178
+ });
85
179
  }
86
180
  });
87
181
  activeWatchers.push(watcher);
@@ -94,9 +188,11 @@ const runDevCommand = ({ host, port, watchEnabled, logEnabled, }) => {
94
188
  for (const watcher of activeWatchers) {
95
189
  watcher.close();
96
190
  }
97
- await new Promise((resolve) => {
98
- currentServer?.close(() => resolve());
99
- });
191
+ if (currentServer) {
192
+ await new Promise((resolve) => {
193
+ currentServer?.close(() => resolve());
194
+ });
195
+ }
100
196
  process.exit(0);
101
197
  };
102
198
  process.on("SIGINT", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAgOA,eAAO,MAAM,iBAAiB,GAAU,YAErC;IACD,KAAK,EAAE,OAAO,CAAC;CAChB,KAAG,OAAO,CAAC,MAAM,CA8KjB,CAAC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA2JA,eAAO,MAAM,iBAAiB,GAAU,YAErC;IACD,KAAK,EAAE,OAAO,CAAC;CAChB,KAAG,OAAO,CAAC,MAAM,CAsHjB,CAAC"}
@@ -27,11 +27,10 @@ const askInitWizardQuestions = async () => {
27
27
  return {
28
28
  port: DEFAULT_INIT_PORT,
29
29
  cors: true,
30
- generateSampleSchemas: true,
31
30
  };
32
31
  }
33
32
  const questionInterface = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
34
- const totalSteps = 3;
33
+ const totalSteps = 2;
35
34
  const formatWizardPrompt = (step, question, hint, defaultValue) => {
36
35
  return `${ui_1.ui.cyan("?")} ${ui_1.ui.bold(`[${step}/${totalSteps}]`)} ${ui_1.ui.white(question)} ${ui_1.ui.gray(`(${hint})`)} ${ui_1.ui.dim(`default: ${defaultValue}`)} ${ui_1.ui.gray("› ")}`;
37
36
  };
@@ -69,23 +68,10 @@ const askInitWizardQuestions = async () => {
69
68
  (0, ui_1.printSpacer)();
70
69
  }
71
70
  console.log(`${ui_1.ui.green("✓")} ${ui_1.ui.dim("CORS")}: ${cors ? ui_1.ui.green("enabled") : ui_1.ui.gray("disabled")}`);
72
- let generateSampleSchemas = true;
73
- while (true) {
74
- const answer = await questionInterface.question(formatWizardPrompt(3, "Generate sample schemas?", "Y/n", "Y"));
75
- const parsed = parseYesNo(answer, true);
76
- if (parsed !== undefined) {
77
- generateSampleSchemas = parsed;
78
- break;
79
- }
80
- console.log(`${ui_1.ui.red("✕")} ${ui_1.ui.white("Please answer with Y/Yes or N/No.")}`);
81
- (0, ui_1.printSpacer)();
82
- }
83
- console.log(`${ui_1.ui.green("✓")} ${ui_1.ui.dim("Sample schemas")}: ${generateSampleSchemas ? ui_1.ui.green("enabled") : ui_1.ui.gray("disabled")}`);
84
71
  (0, ui_1.printSpacer)();
85
72
  return {
86
73
  port,
87
74
  cors,
88
- generateSampleSchemas,
89
75
  };
90
76
  }
91
77
  finally {
@@ -101,49 +87,6 @@ const getRuntimeConfigTemplate = ({ port, cors, }) => {
101
87
  "};",
102
88
  ].join("\n");
103
89
  };
104
- const SAMPLE_USER_SCHEMA = [
105
- "id:",
106
- " type: uuid",
107
- "fullName:",
108
- " type: name",
109
- " faker: person.fullName",
110
- "username:",
111
- " type: string",
112
- " faker: internet.username",
113
- "email:",
114
- " type: email",
115
- " faker: internet.email",
116
- "avatarUrl:",
117
- " type: url",
118
- " faker: image.avatar",
119
- "bio:",
120
- " type: string",
121
- " faker: lorem.sentence",
122
- "isActive:",
123
- " type: boolean",
124
- "joinedAt:",
125
- " type: date",
126
- ].join("\n");
127
- const SAMPLE_POST_SCHEMA = [
128
- "id:",
129
- " type: uuid",
130
- "title:",
131
- " type: string",
132
- " faker: lorem.sentence",
133
- "body:",
134
- " type: string",
135
- " faker: lorem.paragraphs",
136
- "authorId:",
137
- " type: uuid",
138
- "published:",
139
- " type: boolean",
140
- "likes:",
141
- " type: number",
142
- " min: 0",
143
- " max: 500",
144
- "createdAt:",
145
- " type: date",
146
- ].join("\n");
147
90
  const initializeProject = async ({ force, }) => {
148
91
  const initStartedAtMs = (0, ui_1.nowMs)();
149
92
  const packageJsonPath = (0, paths_1.findClosestPackageJson)(process.cwd());
@@ -156,9 +99,6 @@ const initializeProject = async ({ force, }) => {
156
99
  const fexapiDirectoryPath = (0, node_path_1.join)(projectRoot, "fexapi");
157
100
  const schemaPath = (0, node_path_1.join)(fexapiDirectoryPath, "schema.fexapi");
158
101
  const runtimeConfigPath = (0, node_path_1.join)(projectRoot, "fexapi.config.js");
159
- const schemasDirectoryPath = (0, node_path_1.join)(projectRoot, "fexapi", "schemas");
160
- const userSchemaPath = (0, node_path_1.join)(schemasDirectoryPath, "user.yaml");
161
- const postSchemaPath = (0, node_path_1.join)(schemasDirectoryPath, "post.yaml");
162
102
  const wizardAnswers = await askInitWizardQuestions();
163
103
  (0, ui_1.printGroupHeader)("Init");
164
104
  const initSpinner = (0, ui_1.startSpinner)("Scaffolding fexapi project files");
@@ -176,29 +116,6 @@ const initializeProject = async ({ force, }) => {
176
116
  cors: wizardAnswers.cors,
177
117
  })}\n`, "utf-8");
178
118
  }
179
- let userSchemaStatus = "skipped";
180
- let postSchemaStatus = "skipped";
181
- if (wizardAnswers.generateSampleSchemas) {
182
- (0, node_fs_1.mkdirSync)(schemasDirectoryPath, { recursive: true });
183
- const userSchemaExists = (0, node_fs_1.existsSync)(userSchemaPath);
184
- if (!userSchemaExists || force) {
185
- initSpinner.update("Writing sample user schema");
186
- (0, node_fs_1.writeFileSync)(userSchemaPath, `${SAMPLE_USER_SCHEMA}\n`, "utf-8");
187
- userSchemaStatus = userSchemaExists ? "overwritten" : "created";
188
- }
189
- else {
190
- userSchemaStatus = "exists";
191
- }
192
- const postSchemaExists = (0, node_fs_1.existsSync)(postSchemaPath);
193
- if (!postSchemaExists || force) {
194
- initSpinner.update("Writing sample post schema");
195
- (0, node_fs_1.writeFileSync)(postSchemaPath, `${SAMPLE_POST_SCHEMA}\n`, "utf-8");
196
- postSchemaStatus = postSchemaExists ? "overwritten" : "created";
197
- }
198
- else {
199
- postSchemaStatus = "exists";
200
- }
201
- }
202
119
  initSpinner.succeed("Project scaffolding complete");
203
120
  (0, ui_1.logSuccess)(`Initialized fexapi in ${projectRoot}`);
204
121
  (0, ui_1.logInfo)(`Detected framework: ${detectedProject.primaryFramework}`);
@@ -226,29 +143,6 @@ const initializeProject = async ({ force, }) => {
226
143
  else {
227
144
  (0, ui_1.logSuccess)(`Created ${runtimeConfigPath}`);
228
145
  }
229
- if (wizardAnswers.generateSampleSchemas) {
230
- if (userSchemaStatus === "exists") {
231
- (0, ui_1.logWarn)(`Exists ${userSchemaPath}`);
232
- }
233
- else if (userSchemaStatus === "overwritten") {
234
- (0, ui_1.logSuccess)(`Overwritten ${userSchemaPath}`);
235
- }
236
- else if (userSchemaStatus === "created") {
237
- (0, ui_1.logSuccess)(`Created ${userSchemaPath}`);
238
- }
239
- if (postSchemaStatus === "exists") {
240
- (0, ui_1.logWarn)(`Exists ${postSchemaPath}`);
241
- }
242
- else if (postSchemaStatus === "overwritten") {
243
- (0, ui_1.logSuccess)(`Overwritten ${postSchemaPath}`);
244
- }
245
- else if (postSchemaStatus === "created") {
246
- (0, ui_1.logSuccess)(`Created ${postSchemaPath}`);
247
- }
248
- }
249
- else {
250
- (0, ui_1.logWarn)("Sample schemas were skipped.");
251
- }
252
146
  if (detectedProject.primaryFramework === "unknown") {
253
147
  (0, ui_1.logWarn)("No known framework dependency found. Update fexapi.config.js and schema.fexapi if needed.");
254
148
  }
@@ -257,8 +151,6 @@ const initializeProject = async ({ force, }) => {
257
151
  const createdFiles = [
258
152
  !schemaExists || force,
259
153
  !runtimeConfigExists || force,
260
- userSchemaStatus === "created" || userSchemaStatus === "overwritten",
261
- postSchemaStatus === "created" || postSchemaStatus === "overwritten",
262
154
  ].filter(Boolean).length;
263
155
  (0, ui_1.printSummaryCard)("Init Summary", [
264
156
  {
@@ -273,10 +165,6 @@ const initializeProject = async ({ force, }) => {
273
165
  label: "cors",
274
166
  value: wizardAnswers.cors ? "enabled" : "disabled",
275
167
  },
276
- {
277
- label: "sample schemas",
278
- value: wizardAnswers.generateSampleSchemas ? "enabled" : "disabled",
279
- },
280
168
  {
281
169
  label: "files changed",
282
170
  value: String(createdFiles),
package/dist/index.js CHANGED
@@ -13,8 +13,6 @@ const args = process.argv.slice(2);
13
13
  const [firstArg, ...restArgs] = args;
14
14
  const main = async () => {
15
15
  if (firstArg === "--version" || firstArg === "-v" || firstArg === "version") {
16
- (0, ui_1.printBanner)();
17
- (0, ui_1.printSpacer)();
18
16
  console.log((0, ui_1.getCliVersion)());
19
17
  process.exit(0);
20
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fexapi",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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",