flowdoc-gen 0.1.2 → 0.1.4
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/dist/index.cjs +1412 -0
- package/dist/index.d.cts +148 -0
- package/package.json +2 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
defineConfig: () => defineConfig,
|
|
34
|
+
flowdoc: () => flowdoc,
|
|
35
|
+
generate: () => generate,
|
|
36
|
+
init: () => init,
|
|
37
|
+
serve: () => serve
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(src_exports);
|
|
40
|
+
|
|
41
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.15_tsx@4.22.4_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
42
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
43
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
44
|
+
|
|
45
|
+
// src/generate.ts
|
|
46
|
+
var import_fs3 = require("fs");
|
|
47
|
+
var import_path3 = require("path");
|
|
48
|
+
var import_url = require("url");
|
|
49
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
50
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
51
|
+
|
|
52
|
+
// ../core/dist/index.js
|
|
53
|
+
var import_fs = require("fs");
|
|
54
|
+
var import_path = require("path");
|
|
55
|
+
var CONFIG_FILES = [
|
|
56
|
+
"flowdoc.config.ts",
|
|
57
|
+
"flowdoc.config.js",
|
|
58
|
+
"flowdoc.config.mjs"
|
|
59
|
+
];
|
|
60
|
+
var findConfigFile = (cwd) => {
|
|
61
|
+
for (const file of CONFIG_FILES) {
|
|
62
|
+
const full = (0, import_path.join)(cwd, file);
|
|
63
|
+
if ((0, import_fs.existsSync)(full)) return full;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
var loadConfig = async (configPath) => {
|
|
68
|
+
const resolved = (0, import_path.resolve)(configPath);
|
|
69
|
+
const mod = await import(resolved);
|
|
70
|
+
const config = "default" in mod ? mod.default : mod;
|
|
71
|
+
if (!config) throw new Error(`No config exported from ${configPath}`);
|
|
72
|
+
return config;
|
|
73
|
+
};
|
|
74
|
+
var resolveConfig = (config, cwd) => ({
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
baseUrl: "http://localhost:3000",
|
|
77
|
+
...config,
|
|
78
|
+
entry: (0, import_path.resolve)(cwd, config.entry),
|
|
79
|
+
output: (0, import_path.resolve)(cwd, config.output ?? "./docs-output")
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ../parser/dist/index.js
|
|
83
|
+
var import_ts_morph = require("ts-morph");
|
|
84
|
+
var import_glob = require("glob");
|
|
85
|
+
var import_path2 = require("path");
|
|
86
|
+
var import_fs2 = require("fs");
|
|
87
|
+
var import_ts_morph2 = require("ts-morph");
|
|
88
|
+
var import_ts_morph3 = require("ts-morph");
|
|
89
|
+
var import_ts_morph4 = require("ts-morph");
|
|
90
|
+
var import_ts_morph5 = require("ts-morph");
|
|
91
|
+
var zodNodeToJsonSchema = (node) => {
|
|
92
|
+
if (!import_ts_morph2.Node.isCallExpression(node)) return {};
|
|
93
|
+
const expr = node.getExpression();
|
|
94
|
+
const callText = expr.getText();
|
|
95
|
+
if (callText === "z.string") return buildStringSchema(node);
|
|
96
|
+
if (callText === "z.number") return buildNumberSchema(node);
|
|
97
|
+
if (callText === "z.boolean") return { type: "boolean" };
|
|
98
|
+
if (callText === "z.null") return { type: "null" };
|
|
99
|
+
if (callText === "z.literal") return buildLiteralSchema(node);
|
|
100
|
+
if (callText === "z.enum") return buildEnumSchema(node);
|
|
101
|
+
if (callText === "z.nativeEnum") return { type: "string" };
|
|
102
|
+
if (callText === "z.object") return buildObjectSchema(node);
|
|
103
|
+
if (callText === "z.array") return buildArraySchema(node);
|
|
104
|
+
if (callText === "z.union") return buildUnionSchema(node);
|
|
105
|
+
if (callText === "z.optional") return buildOptionalSchema(node);
|
|
106
|
+
if (callText === "z.date") return { type: "string", format: "date-time" };
|
|
107
|
+
if (callText === "z.any") return {};
|
|
108
|
+
if (callText === "z.unknown") return {};
|
|
109
|
+
if (import_ts_morph2.Node.isPropertyAccessExpression(expr)) {
|
|
110
|
+
return buildChainedSchema(node);
|
|
111
|
+
}
|
|
112
|
+
return {};
|
|
113
|
+
};
|
|
114
|
+
var buildStringSchema = (node) => {
|
|
115
|
+
const schema = { type: "string" };
|
|
116
|
+
applyChainedValidations(node, schema);
|
|
117
|
+
return schema;
|
|
118
|
+
};
|
|
119
|
+
var buildNumberSchema = (node) => {
|
|
120
|
+
const schema = { type: "number" };
|
|
121
|
+
applyChainedValidations(node, schema);
|
|
122
|
+
return schema;
|
|
123
|
+
};
|
|
124
|
+
var buildLiteralSchema = (node) => {
|
|
125
|
+
const arg = node.getArguments()[0];
|
|
126
|
+
if (!arg) return {};
|
|
127
|
+
const text = arg.getText().replace(/['"]/g, "");
|
|
128
|
+
return { type: "string", enum: [text] };
|
|
129
|
+
};
|
|
130
|
+
var buildEnumSchema = (node) => {
|
|
131
|
+
const arg = node.getArguments()[0];
|
|
132
|
+
if (!arg || !import_ts_morph2.Node.isArrayLiteralExpression(arg)) return { type: "string" };
|
|
133
|
+
const values = arg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
134
|
+
return { type: "string", enum: values };
|
|
135
|
+
};
|
|
136
|
+
var buildObjectSchema = (node) => {
|
|
137
|
+
const arg = node.getArguments()[0];
|
|
138
|
+
if (!arg || !import_ts_morph2.Node.isObjectLiteralExpression(arg)) return { type: "object" };
|
|
139
|
+
const properties = {};
|
|
140
|
+
const required = [];
|
|
141
|
+
for (const prop of arg.getProperties()) {
|
|
142
|
+
if (!import_ts_morph2.Node.isPropertyAssignment(prop)) continue;
|
|
143
|
+
const name = prop.getName();
|
|
144
|
+
const init2 = prop.getInitializer();
|
|
145
|
+
if (!init2) continue;
|
|
146
|
+
const childSchema = zodNodeToJsonSchema(init2);
|
|
147
|
+
properties[name] = childSchema;
|
|
148
|
+
if (!isOptionalZodExpr(init2)) {
|
|
149
|
+
required.push(name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties,
|
|
155
|
+
...required.length > 0 ? { required } : {}
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
var buildArraySchema = (node) => {
|
|
159
|
+
const arg = node.getArguments()[0];
|
|
160
|
+
if (!arg) return { type: "array" };
|
|
161
|
+
return { type: "array", items: zodNodeToJsonSchema(arg) };
|
|
162
|
+
};
|
|
163
|
+
var buildUnionSchema = (node) => {
|
|
164
|
+
const arg = node.getArguments()[0];
|
|
165
|
+
if (!arg || !import_ts_morph2.Node.isArrayLiteralExpression(arg)) return {};
|
|
166
|
+
const schemas = arg.getElements().map((el) => zodNodeToJsonSchema(el));
|
|
167
|
+
return { anyOf: schemas };
|
|
168
|
+
};
|
|
169
|
+
var buildOptionalSchema = (node) => {
|
|
170
|
+
const arg = node.getArguments()[0];
|
|
171
|
+
if (!arg) return {};
|
|
172
|
+
return zodNodeToJsonSchema(arg);
|
|
173
|
+
};
|
|
174
|
+
var buildChainedSchema = (node) => {
|
|
175
|
+
const chain = unwrapChain(node);
|
|
176
|
+
if (!chain.root) return {};
|
|
177
|
+
const base = zodNodeToJsonSchema(chain.root);
|
|
178
|
+
applyChainedCallsToSchema(chain.methods, base);
|
|
179
|
+
return base;
|
|
180
|
+
};
|
|
181
|
+
var unwrapChain = (node) => {
|
|
182
|
+
const methods = [];
|
|
183
|
+
let current = node;
|
|
184
|
+
while (import_ts_morph2.Node.isCallExpression(current)) {
|
|
185
|
+
const expr = current.getExpression();
|
|
186
|
+
if (import_ts_morph2.Node.isPropertyAccessExpression(expr)) {
|
|
187
|
+
const obj = expr.getExpression();
|
|
188
|
+
if (import_ts_morph2.Node.isIdentifier(obj)) {
|
|
189
|
+
return { root: current, methods };
|
|
190
|
+
}
|
|
191
|
+
const methodName = expr.getName();
|
|
192
|
+
const args = current.getArguments();
|
|
193
|
+
methods.unshift({ name: methodName, args });
|
|
194
|
+
current = obj;
|
|
195
|
+
} else {
|
|
196
|
+
return { root: current, methods };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { root: null, methods };
|
|
200
|
+
};
|
|
201
|
+
var applyChainedValidations = (node, schema) => {
|
|
202
|
+
const chain = unwrapChain(node);
|
|
203
|
+
applyChainedCallsToSchema(chain.methods, schema);
|
|
204
|
+
};
|
|
205
|
+
var applyChainedCallsToSchema = (methods, schema) => {
|
|
206
|
+
for (const { name, args } of methods) {
|
|
207
|
+
switch (name) {
|
|
208
|
+
case "email":
|
|
209
|
+
schema.format = "email";
|
|
210
|
+
break;
|
|
211
|
+
case "url":
|
|
212
|
+
schema.format = "uri";
|
|
213
|
+
break;
|
|
214
|
+
case "uuid":
|
|
215
|
+
schema.format = "uuid";
|
|
216
|
+
break;
|
|
217
|
+
case "datetime":
|
|
218
|
+
schema.format = "date-time";
|
|
219
|
+
break;
|
|
220
|
+
case "min": {
|
|
221
|
+
const val = getNumericArg(args[0]);
|
|
222
|
+
if (val !== null) {
|
|
223
|
+
if (schema.type === "string") schema.minLength = val;
|
|
224
|
+
else schema.minimum = val;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "max": {
|
|
229
|
+
const val = getNumericArg(args[0]);
|
|
230
|
+
if (val !== null) {
|
|
231
|
+
if (schema.type === "string") schema.maxLength = val;
|
|
232
|
+
else schema.maximum = val;
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "describe": {
|
|
237
|
+
const desc = getStringArg(args[0]);
|
|
238
|
+
if (desc) schema.description = desc;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "default": {
|
|
242
|
+
const arg = args[0];
|
|
243
|
+
if (arg) schema.default = tryParseValue(arg.getText());
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "optional":
|
|
247
|
+
case "nullish":
|
|
248
|
+
schema.nullable = true;
|
|
249
|
+
break;
|
|
250
|
+
case "positive":
|
|
251
|
+
schema.minimum = 0;
|
|
252
|
+
break;
|
|
253
|
+
case "negative":
|
|
254
|
+
schema.maximum = 0;
|
|
255
|
+
break;
|
|
256
|
+
case "int":
|
|
257
|
+
schema.type = "integer";
|
|
258
|
+
break;
|
|
259
|
+
case "regex": {
|
|
260
|
+
const pattern = args[0]?.getText();
|
|
261
|
+
if (pattern) schema.pattern = pattern.replace(/^\/|\/[gimsuy]*$/g, "");
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
var isOptionalZodExpr = (node) => {
|
|
268
|
+
const text = node.getText();
|
|
269
|
+
return text.includes(".optional()") || text.includes(".nullish()") || text.startsWith("z.optional(");
|
|
270
|
+
};
|
|
271
|
+
var getNumericArg = (node) => {
|
|
272
|
+
if (!node) return null;
|
|
273
|
+
const val = Number(node.getText());
|
|
274
|
+
return isNaN(val) ? null : val;
|
|
275
|
+
};
|
|
276
|
+
var getStringArg = (node) => {
|
|
277
|
+
if (!node) return null;
|
|
278
|
+
const text = node.getText();
|
|
279
|
+
const match = text.match(/^['"`](.*?)['"`]$/s);
|
|
280
|
+
return match?.[1] ?? null;
|
|
281
|
+
};
|
|
282
|
+
var tryParseValue = (text) => {
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(text);
|
|
285
|
+
} catch {
|
|
286
|
+
return text.replace(/['"]/g, "");
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var extractZodSchemas = (sourceFile) => {
|
|
290
|
+
const schemas = {};
|
|
291
|
+
const varDeclarations = sourceFile.getVariableDeclarations();
|
|
292
|
+
for (const decl of varDeclarations) {
|
|
293
|
+
const init2 = decl.getInitializer();
|
|
294
|
+
if (!init2) continue;
|
|
295
|
+
const text = init2.getText();
|
|
296
|
+
if (!text.startsWith("z.")) continue;
|
|
297
|
+
const name = decl.getName();
|
|
298
|
+
try {
|
|
299
|
+
schemas[name] = zodNodeToJsonSchema(init2);
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return schemas;
|
|
304
|
+
};
|
|
305
|
+
var YUP_ROOT_METHODS = /* @__PURE__ */ new Set([
|
|
306
|
+
"string",
|
|
307
|
+
"number",
|
|
308
|
+
"boolean",
|
|
309
|
+
"object",
|
|
310
|
+
"array",
|
|
311
|
+
"date",
|
|
312
|
+
"mixed",
|
|
313
|
+
"ref",
|
|
314
|
+
"lazy"
|
|
315
|
+
]);
|
|
316
|
+
var yupNodeToJsonSchema = (node) => {
|
|
317
|
+
if (!import_ts_morph3.Node.isCallExpression(node)) return {};
|
|
318
|
+
const chain = unwrapYupChain(node);
|
|
319
|
+
if (!chain.root) return {};
|
|
320
|
+
const base = buildYupBase(chain.rootMethod);
|
|
321
|
+
applyYupChain(chain.methods, base);
|
|
322
|
+
return base;
|
|
323
|
+
};
|
|
324
|
+
var unwrapYupChain = (node) => {
|
|
325
|
+
const methods = [];
|
|
326
|
+
let current = node;
|
|
327
|
+
while (import_ts_morph3.Node.isCallExpression(current)) {
|
|
328
|
+
const expr = current.getExpression();
|
|
329
|
+
if (!import_ts_morph3.Node.isPropertyAccessExpression(expr)) {
|
|
330
|
+
if (import_ts_morph3.Node.isIdentifier(expr) && YUP_ROOT_METHODS.has(expr.getText())) {
|
|
331
|
+
return { root: current, rootMethod: expr.getText(), methods };
|
|
332
|
+
}
|
|
333
|
+
return { root: null, rootMethod: "", methods };
|
|
334
|
+
}
|
|
335
|
+
const obj = expr.getExpression();
|
|
336
|
+
const methodName = expr.getName();
|
|
337
|
+
if (import_ts_morph3.Node.isIdentifier(obj) && obj.getText() === "yup" && YUP_ROOT_METHODS.has(methodName)) {
|
|
338
|
+
return { root: current, rootMethod: methodName, methods };
|
|
339
|
+
}
|
|
340
|
+
methods.unshift({ name: methodName, args: current.getArguments() });
|
|
341
|
+
current = obj;
|
|
342
|
+
}
|
|
343
|
+
return { root: null, rootMethod: "", methods };
|
|
344
|
+
};
|
|
345
|
+
var buildYupBase = (method) => {
|
|
346
|
+
switch (method) {
|
|
347
|
+
case "string":
|
|
348
|
+
return { type: "string" };
|
|
349
|
+
case "number":
|
|
350
|
+
return { type: "number" };
|
|
351
|
+
case "boolean":
|
|
352
|
+
return { type: "boolean" };
|
|
353
|
+
case "date":
|
|
354
|
+
return { type: "string", format: "date-time" };
|
|
355
|
+
case "array":
|
|
356
|
+
return { type: "array" };
|
|
357
|
+
case "object":
|
|
358
|
+
return { type: "object" };
|
|
359
|
+
default:
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var applyYupChain = (methods, schema) => {
|
|
364
|
+
let required = false;
|
|
365
|
+
for (const { name, args } of methods) {
|
|
366
|
+
switch (name) {
|
|
367
|
+
case "required":
|
|
368
|
+
required = true;
|
|
369
|
+
break;
|
|
370
|
+
case "optional":
|
|
371
|
+
case "nullable":
|
|
372
|
+
schema.nullable = true;
|
|
373
|
+
break;
|
|
374
|
+
case "email":
|
|
375
|
+
schema.format = "email";
|
|
376
|
+
break;
|
|
377
|
+
case "url":
|
|
378
|
+
schema.format = "uri";
|
|
379
|
+
break;
|
|
380
|
+
case "uuid":
|
|
381
|
+
schema.format = "uuid";
|
|
382
|
+
break;
|
|
383
|
+
case "min": {
|
|
384
|
+
const v = getNum(args[0]);
|
|
385
|
+
if (v !== null) {
|
|
386
|
+
if (schema.type === "string") schema.minLength = v;
|
|
387
|
+
else if (schema.type === "array") schema.minItems = v;
|
|
388
|
+
else schema.minimum = v;
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "max": {
|
|
393
|
+
const v = getNum(args[0]);
|
|
394
|
+
if (v !== null) {
|
|
395
|
+
if (schema.type === "string") schema.maxLength = v;
|
|
396
|
+
else if (schema.type === "array") schema.maxItems = v;
|
|
397
|
+
else schema.maximum = v;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "length": {
|
|
402
|
+
const v = getNum(args[0]);
|
|
403
|
+
if (v !== null && schema.type === "string") {
|
|
404
|
+
schema.minLength = v;
|
|
405
|
+
schema.maxLength = v;
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "integer":
|
|
410
|
+
schema.type = "integer";
|
|
411
|
+
break;
|
|
412
|
+
case "positive":
|
|
413
|
+
schema.minimum = 0;
|
|
414
|
+
break;
|
|
415
|
+
case "negative":
|
|
416
|
+
schema.maximum = 0;
|
|
417
|
+
break;
|
|
418
|
+
case "oneOf": {
|
|
419
|
+
const arg = args[0];
|
|
420
|
+
if (arg && import_ts_morph3.Node.isArrayLiteralExpression(arg)) {
|
|
421
|
+
schema.enum = arg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case "shape": {
|
|
426
|
+
const arg = args[0];
|
|
427
|
+
if (arg && import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
428
|
+
const { properties, required: req } = parseYupShape(arg);
|
|
429
|
+
schema.type = "object";
|
|
430
|
+
schema.properties = properties;
|
|
431
|
+
if (req.length > 0) schema.required = req;
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case "of": {
|
|
436
|
+
const arg = args[0];
|
|
437
|
+
if (arg) schema.items = yupNodeToJsonSchema(arg);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case "label": {
|
|
441
|
+
const s = getStr(args[0]);
|
|
442
|
+
if (s) schema.title = s;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case "meta": {
|
|
446
|
+
const arg = args[0];
|
|
447
|
+
if (arg && import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
448
|
+
for (const prop of arg.getProperties()) {
|
|
449
|
+
if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "description") {
|
|
450
|
+
const s = getStr(prop.getInitializer());
|
|
451
|
+
if (s) schema.description = s;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "default": {
|
|
458
|
+
const arg = args[0];
|
|
459
|
+
if (arg) schema.default = tryParse(arg.getText());
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (required) schema.__required = true;
|
|
465
|
+
};
|
|
466
|
+
var parseYupShape = (arg) => {
|
|
467
|
+
const properties = {};
|
|
468
|
+
const required = [];
|
|
469
|
+
if (!arg || !import_ts_morph3.Node.isObjectLiteralExpression(arg)) return { properties, required };
|
|
470
|
+
for (const prop of arg.getProperties()) {
|
|
471
|
+
if (!import_ts_morph3.Node.isPropertyAssignment(prop)) continue;
|
|
472
|
+
const name = prop.getName();
|
|
473
|
+
const init2 = prop.getInitializer();
|
|
474
|
+
if (!init2) continue;
|
|
475
|
+
const childSchema = yupNodeToJsonSchema(init2);
|
|
476
|
+
const isReq = childSchema.__required === true;
|
|
477
|
+
delete childSchema.__required;
|
|
478
|
+
properties[name] = childSchema;
|
|
479
|
+
if (isReq) required.push(name);
|
|
480
|
+
}
|
|
481
|
+
return { properties, required };
|
|
482
|
+
};
|
|
483
|
+
var getNum = (node) => {
|
|
484
|
+
if (!node) return null;
|
|
485
|
+
const v = Number(node.getText());
|
|
486
|
+
return isNaN(v) ? null : v;
|
|
487
|
+
};
|
|
488
|
+
var getStr = (node) => {
|
|
489
|
+
if (!node) return null;
|
|
490
|
+
const m = node.getText().match(/^['"`](.*?)['"`]$/s);
|
|
491
|
+
return m?.[1] ?? null;
|
|
492
|
+
};
|
|
493
|
+
var tryParse = (text) => {
|
|
494
|
+
try {
|
|
495
|
+
return JSON.parse(text);
|
|
496
|
+
} catch {
|
|
497
|
+
return text.replace(/['"]/g, "");
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var isYupExpr = (text) => /^yup\.(?:string|number|boolean|object|array|date|mixed)\s*\(/.test(text) || /^(?:string|number|boolean|object|array|date|mixed)\s*\(\s*\)/.test(text);
|
|
501
|
+
var extractYupSchemas = (sourceFile) => {
|
|
502
|
+
const schemas = {};
|
|
503
|
+
for (const decl of sourceFile.getVariableDeclarations()) {
|
|
504
|
+
const init2 = decl.getInitializer();
|
|
505
|
+
if (!init2) continue;
|
|
506
|
+
const text = init2.getText();
|
|
507
|
+
if (!isYupExpr(text)) continue;
|
|
508
|
+
try {
|
|
509
|
+
const schema = yupNodeToJsonSchema(init2);
|
|
510
|
+
if (Object.keys(schema).length > 0) schemas[decl.getName()] = schema;
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return schemas;
|
|
515
|
+
};
|
|
516
|
+
var JOI_PRIMITIVES = /* @__PURE__ */ new Set([
|
|
517
|
+
"string",
|
|
518
|
+
"number",
|
|
519
|
+
"boolean",
|
|
520
|
+
"object",
|
|
521
|
+
"array",
|
|
522
|
+
"date",
|
|
523
|
+
"binary",
|
|
524
|
+
"any",
|
|
525
|
+
"alternatives",
|
|
526
|
+
"link"
|
|
527
|
+
]);
|
|
528
|
+
var joiNodeToJsonSchema = (node, joiId = "Joi") => {
|
|
529
|
+
if (!import_ts_morph4.Node.isCallExpression(node)) return {};
|
|
530
|
+
const chain = unwrapJoiChain(node, joiId);
|
|
531
|
+
if (!chain.root) return {};
|
|
532
|
+
const base = buildJoiBase(chain.rootMethod);
|
|
533
|
+
applyJoiChain(chain.methods, base, joiId);
|
|
534
|
+
return base;
|
|
535
|
+
};
|
|
536
|
+
var unwrapJoiChain = (node, joiId) => {
|
|
537
|
+
const methods = [];
|
|
538
|
+
let current = node;
|
|
539
|
+
while (import_ts_morph4.Node.isCallExpression(current)) {
|
|
540
|
+
const expr = current.getExpression();
|
|
541
|
+
if (!import_ts_morph4.Node.isPropertyAccessExpression(expr)) {
|
|
542
|
+
return { root: null, rootMethod: "", methods };
|
|
543
|
+
}
|
|
544
|
+
const obj = expr.getExpression();
|
|
545
|
+
const methodName = expr.getName();
|
|
546
|
+
if (import_ts_morph4.Node.isIdentifier(obj) && obj.getText() === joiId && JOI_PRIMITIVES.has(methodName)) {
|
|
547
|
+
return { root: current, rootMethod: methodName, methods };
|
|
548
|
+
}
|
|
549
|
+
methods.unshift({ name: methodName, args: current.getArguments() });
|
|
550
|
+
current = obj;
|
|
551
|
+
}
|
|
552
|
+
return { root: null, rootMethod: "", methods };
|
|
553
|
+
};
|
|
554
|
+
var buildJoiBase = (method) => {
|
|
555
|
+
switch (method) {
|
|
556
|
+
case "string":
|
|
557
|
+
return { type: "string" };
|
|
558
|
+
case "number":
|
|
559
|
+
return { type: "number" };
|
|
560
|
+
case "boolean":
|
|
561
|
+
return { type: "boolean" };
|
|
562
|
+
case "date":
|
|
563
|
+
return { type: "string", format: "date-time" };
|
|
564
|
+
case "binary":
|
|
565
|
+
return { type: "string", format: "binary" };
|
|
566
|
+
case "array":
|
|
567
|
+
return { type: "array" };
|
|
568
|
+
case "object":
|
|
569
|
+
return { type: "object" };
|
|
570
|
+
default:
|
|
571
|
+
return {};
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
var applyJoiChain = (methods, schema, joiId) => {
|
|
575
|
+
for (const { name, args } of methods) {
|
|
576
|
+
switch (name) {
|
|
577
|
+
case "required":
|
|
578
|
+
schema.__required = true;
|
|
579
|
+
break;
|
|
580
|
+
case "optional":
|
|
581
|
+
case "allow":
|
|
582
|
+
schema.nullable = true;
|
|
583
|
+
break;
|
|
584
|
+
case "email":
|
|
585
|
+
schema.format = "email";
|
|
586
|
+
break;
|
|
587
|
+
case "uri":
|
|
588
|
+
schema.format = "uri";
|
|
589
|
+
break;
|
|
590
|
+
case "guid":
|
|
591
|
+
case "uuid":
|
|
592
|
+
schema.format = "uuid";
|
|
593
|
+
break;
|
|
594
|
+
case "isoDate":
|
|
595
|
+
schema.format = "date-time";
|
|
596
|
+
break;
|
|
597
|
+
case "min": {
|
|
598
|
+
const v = getNum2(args[0]);
|
|
599
|
+
if (v !== null) {
|
|
600
|
+
if (schema.type === "string") schema.minLength = v;
|
|
601
|
+
else if (schema.type === "array") schema.minItems = v;
|
|
602
|
+
else schema.minimum = v;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case "max": {
|
|
607
|
+
const v = getNum2(args[0]);
|
|
608
|
+
if (v !== null) {
|
|
609
|
+
if (schema.type === "string") schema.maxLength = v;
|
|
610
|
+
else if (schema.type === "array") schema.maxItems = v;
|
|
611
|
+
else schema.maximum = v;
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case "length": {
|
|
616
|
+
const v = getNum2(args[0]);
|
|
617
|
+
if (v !== null && schema.type === "string") {
|
|
618
|
+
schema.minLength = v;
|
|
619
|
+
schema.maxLength = v;
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case "integer":
|
|
624
|
+
schema.type = "integer";
|
|
625
|
+
break;
|
|
626
|
+
case "positive":
|
|
627
|
+
schema.minimum = 0;
|
|
628
|
+
break;
|
|
629
|
+
case "negative":
|
|
630
|
+
schema.maximum = 0;
|
|
631
|
+
break;
|
|
632
|
+
case "valid": {
|
|
633
|
+
const vals = args.map((a) => a.getText().replace(/['"]/g, "")).filter((v) => !v.includes(".override"));
|
|
634
|
+
if (vals.length > 0) schema.enum = vals;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case "items": {
|
|
638
|
+
const arg = args[0];
|
|
639
|
+
if (arg) schema.items = joiNodeToJsonSchema(arg, joiId);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "keys": {
|
|
643
|
+
const arg = args[0];
|
|
644
|
+
if (arg && import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
|
|
645
|
+
const { properties, required } = parseJoiKeys(arg, joiId);
|
|
646
|
+
schema.type = "object";
|
|
647
|
+
schema.properties = properties;
|
|
648
|
+
if (required.length > 0) schema.required = required;
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case "description": {
|
|
653
|
+
const s = getStr2(args[0]);
|
|
654
|
+
if (s) schema.description = s;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case "label": {
|
|
658
|
+
const s = getStr2(args[0]);
|
|
659
|
+
if (s) schema.title = s;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case "default": {
|
|
663
|
+
const arg = args[0];
|
|
664
|
+
if (arg) schema.default = tryParse2(arg.getText());
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "pattern": {
|
|
668
|
+
const arg = args[0];
|
|
669
|
+
if (arg) schema.pattern = arg.getText().replace(/^\/|\/[gimsuy]*$/g, "");
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
var parseJoiKeys = (arg, joiId) => {
|
|
676
|
+
const properties = {};
|
|
677
|
+
const required = [];
|
|
678
|
+
if (!arg || !import_ts_morph4.Node.isObjectLiteralExpression(arg)) return { properties, required };
|
|
679
|
+
for (const prop of arg.getProperties()) {
|
|
680
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(prop)) continue;
|
|
681
|
+
const name = prop.getName();
|
|
682
|
+
const init2 = prop.getInitializer();
|
|
683
|
+
if (!init2) continue;
|
|
684
|
+
const childSchema = joiNodeToJsonSchema(init2, joiId);
|
|
685
|
+
const isReq = childSchema.__required === true;
|
|
686
|
+
delete childSchema.__required;
|
|
687
|
+
properties[name] = childSchema;
|
|
688
|
+
if (isReq) required.push(name);
|
|
689
|
+
}
|
|
690
|
+
return { properties, required };
|
|
691
|
+
};
|
|
692
|
+
var getNum2 = (node) => {
|
|
693
|
+
if (!node) return null;
|
|
694
|
+
const v = Number(node.getText());
|
|
695
|
+
return isNaN(v) ? null : v;
|
|
696
|
+
};
|
|
697
|
+
var getStr2 = (node) => {
|
|
698
|
+
if (!node) return null;
|
|
699
|
+
const m = node.getText().match(/^['"`](.*?)['"`]$/s);
|
|
700
|
+
return m?.[1] ?? null;
|
|
701
|
+
};
|
|
702
|
+
var tryParse2 = (text) => {
|
|
703
|
+
try {
|
|
704
|
+
return JSON.parse(text);
|
|
705
|
+
} catch {
|
|
706
|
+
return text.replace(/['"]/g, "");
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
var detectJoiId = (sourceFile) => {
|
|
710
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
711
|
+
const mod = decl.getModuleSpecifierValue();
|
|
712
|
+
if (mod !== "joi" && mod !== "@hapi/joi") continue;
|
|
713
|
+
const def = decl.getDefaultImport() ?? decl.getNamespaceImport();
|
|
714
|
+
if (def) return def.getText();
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
};
|
|
718
|
+
var isJoiExpr = (text, joiId) => new RegExp(`^${joiId}\\.(?:${[...JOI_PRIMITIVES].join("|")})\\s*\\(`).test(text);
|
|
719
|
+
var extractJoiSchemas = (sourceFile) => {
|
|
720
|
+
const schemas = {};
|
|
721
|
+
const joiId = detectJoiId(sourceFile);
|
|
722
|
+
if (!joiId) return schemas;
|
|
723
|
+
for (const decl of sourceFile.getVariableDeclarations()) {
|
|
724
|
+
const init2 = decl.getInitializer();
|
|
725
|
+
if (!init2) continue;
|
|
726
|
+
if (!isJoiExpr(init2.getText(), joiId)) continue;
|
|
727
|
+
try {
|
|
728
|
+
const schema = joiNodeToJsonSchema(init2, joiId);
|
|
729
|
+
if (Object.keys(schema).length > 0) schemas[decl.getName()] = schema;
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return schemas;
|
|
734
|
+
};
|
|
735
|
+
var DECORATOR_MAP = {
|
|
736
|
+
IsString: { type: "string" },
|
|
737
|
+
IsNumber: { type: "number" },
|
|
738
|
+
IsInt: { type: "integer" },
|
|
739
|
+
IsBoolean: { type: "boolean" },
|
|
740
|
+
IsDate: { type: "string", format: "date-time" },
|
|
741
|
+
IsDateString: { type: "string", format: "date-time" },
|
|
742
|
+
IsEmail: { type: "string", format: "email" },
|
|
743
|
+
IsUrl: { type: "string", format: "uri" },
|
|
744
|
+
IsUUID: { type: "string", format: "uuid" },
|
|
745
|
+
IsArray: { type: "array" },
|
|
746
|
+
IsObject: { type: "object" },
|
|
747
|
+
IsHexColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
|
|
748
|
+
IsIP: { type: "string", format: "ipv4" },
|
|
749
|
+
IsJWT: { type: "string", format: "jwt" },
|
|
750
|
+
IsPhoneNumber: { type: "string" },
|
|
751
|
+
IsPostalCode: { type: "string" }
|
|
752
|
+
};
|
|
753
|
+
var CONSTRAINED = {
|
|
754
|
+
MinLength: (s, v) => {
|
|
755
|
+
s.type = s.type ?? "string";
|
|
756
|
+
s.minLength = Number(v);
|
|
757
|
+
},
|
|
758
|
+
MaxLength: (s, v) => {
|
|
759
|
+
s.type = s.type ?? "string";
|
|
760
|
+
s.maxLength = Number(v);
|
|
761
|
+
},
|
|
762
|
+
Min: (s, v) => {
|
|
763
|
+
s.minimum = Number(v);
|
|
764
|
+
},
|
|
765
|
+
Max: (s, v) => {
|
|
766
|
+
s.maximum = Number(v);
|
|
767
|
+
},
|
|
768
|
+
ArrayMinSize: (s, v) => {
|
|
769
|
+
s.minItems = Number(v);
|
|
770
|
+
},
|
|
771
|
+
ArrayMaxSize: (s, v) => {
|
|
772
|
+
s.maxItems = Number(v);
|
|
773
|
+
},
|
|
774
|
+
Length: (s, v) => {
|
|
775
|
+
s.minLength = Number(v);
|
|
776
|
+
s.maxLength = Number(v);
|
|
777
|
+
},
|
|
778
|
+
Matches: (s, v) => {
|
|
779
|
+
const pat = String(v).replace(/^\/|\/[gimsuy]*$/g, "");
|
|
780
|
+
s.pattern = pat;
|
|
781
|
+
},
|
|
782
|
+
IsIn: (s, v) => {
|
|
783
|
+
if (Array.isArray(v)) s.enum = v.map(String);
|
|
784
|
+
},
|
|
785
|
+
IsEnum: (s, _v) => {
|
|
786
|
+
s.type = s.type ?? "string";
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var OPTIONAL_DECORATORS = /* @__PURE__ */ new Set(["IsOptional"]);
|
|
790
|
+
var extractClassValidatorSchemas = (sourceFile) => {
|
|
791
|
+
const schemas = {};
|
|
792
|
+
const hasClassValidator = sourceFile.getImportDeclarations().some((d) => {
|
|
793
|
+
const mod = d.getModuleSpecifierValue();
|
|
794
|
+
return mod === "class-validator" || mod.includes("class-validator");
|
|
795
|
+
});
|
|
796
|
+
if (!hasClassValidator) return schemas;
|
|
797
|
+
for (const cls of sourceFile.getClasses()) {
|
|
798
|
+
const className = cls.getName();
|
|
799
|
+
if (!className) continue;
|
|
800
|
+
const properties = {};
|
|
801
|
+
const required = [];
|
|
802
|
+
for (const prop of cls.getProperties()) {
|
|
803
|
+
const decorators = prop.getDecorators();
|
|
804
|
+
if (decorators.length === 0) continue;
|
|
805
|
+
const propSchema = {};
|
|
806
|
+
let isOptional = false;
|
|
807
|
+
for (const decorator of decorators) {
|
|
808
|
+
const name = decorator.getName();
|
|
809
|
+
if (DECORATOR_MAP[name]) {
|
|
810
|
+
Object.assign(propSchema, DECORATOR_MAP[name]);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (CONSTRAINED[name]) {
|
|
814
|
+
const call = decorator.getCallExpression();
|
|
815
|
+
const firstArg = call?.getArguments()[0];
|
|
816
|
+
if (firstArg) {
|
|
817
|
+
const raw = firstArg.getText();
|
|
818
|
+
if (import_ts_morph5.Node.isArrayLiteralExpression(firstArg)) {
|
|
819
|
+
const values = firstArg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
820
|
+
CONSTRAINED[name](propSchema, values);
|
|
821
|
+
} else {
|
|
822
|
+
const num = Number(raw);
|
|
823
|
+
CONSTRAINED[name](propSchema, isNaN(num) ? raw.replace(/['"]/g, "") : num);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (OPTIONAL_DECORATORS.has(name)) {
|
|
829
|
+
isOptional = true;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (name === "ValidateNested") {
|
|
833
|
+
propSchema.type = "object";
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (!propSchema.type) {
|
|
838
|
+
const typeNode = prop.getTypeNode();
|
|
839
|
+
if (typeNode) {
|
|
840
|
+
const typeText = typeNode.getText();
|
|
841
|
+
if (typeText === "string") propSchema.type = "string";
|
|
842
|
+
else if (typeText === "number") propSchema.type = "number";
|
|
843
|
+
else if (typeText === "boolean") propSchema.type = "boolean";
|
|
844
|
+
else if (typeText.endsWith("[]")) propSchema.type = "array";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (Object.keys(propSchema).length === 0) continue;
|
|
848
|
+
const propName = prop.getName();
|
|
849
|
+
properties[propName] = propSchema;
|
|
850
|
+
if (!isOptional) required.push(propName);
|
|
851
|
+
}
|
|
852
|
+
if (Object.keys(properties).length === 0) continue;
|
|
853
|
+
schemas[className] = {
|
|
854
|
+
type: "object",
|
|
855
|
+
properties,
|
|
856
|
+
...required.length > 0 ? { required } : {}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return schemas;
|
|
860
|
+
};
|
|
861
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
862
|
+
var findTsConfig = (startDir) => {
|
|
863
|
+
let dir = startDir;
|
|
864
|
+
for (let i = 0; i < 5; i++) {
|
|
865
|
+
const candidate = (0, import_path2.join)(dir, "tsconfig.json");
|
|
866
|
+
if ((0, import_fs2.existsSync)(candidate)) return candidate;
|
|
867
|
+
const parent = (0, import_path2.dirname)(dir);
|
|
868
|
+
if (parent === dir) break;
|
|
869
|
+
dir = parent;
|
|
870
|
+
}
|
|
871
|
+
return void 0;
|
|
872
|
+
};
|
|
873
|
+
var extractExpressRoutes = async (config) => {
|
|
874
|
+
const cwd = (0, import_fs2.existsSync)(config.entry) && !config.entry.endsWith(".ts") && !config.entry.endsWith(".js") ? config.entry : (0, import_path2.dirname)(config.entry);
|
|
875
|
+
const tsConfigPath = findTsConfig(cwd);
|
|
876
|
+
const project = tsConfigPath ? new import_ts_morph.Project({ tsConfigFilePath: tsConfigPath, skipAddingFilesFromTsConfig: true }) : new import_ts_morph.Project({ compilerOptions: { allowJs: true, strict: false } });
|
|
877
|
+
const patterns = ["**/*.ts", "**/*.js"].map((p) => (0, import_path2.resolve)(cwd, "**", p));
|
|
878
|
+
const files = await (0, import_glob.glob)(`${cwd}/**/*.{ts,js}`, {
|
|
879
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/*.d.ts", "**/*.spec.*", "**/*.test.*"]
|
|
880
|
+
});
|
|
881
|
+
for (const file of files) {
|
|
882
|
+
project.addSourceFileAtPath(file);
|
|
883
|
+
}
|
|
884
|
+
const globalSchemas = {};
|
|
885
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
886
|
+
Object.assign(globalSchemas, extractZodSchemas(sourceFile));
|
|
887
|
+
Object.assign(globalSchemas, extractYupSchemas(sourceFile));
|
|
888
|
+
Object.assign(globalSchemas, extractJoiSchemas(sourceFile));
|
|
889
|
+
Object.assign(globalSchemas, extractClassValidatorSchemas(sourceFile));
|
|
890
|
+
}
|
|
891
|
+
const allRoutes = [];
|
|
892
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
893
|
+
const ctx = { zodSchemas: globalSchemas, routerPrefix: "" };
|
|
894
|
+
const routes = extractRoutesFromFile(sourceFile, ctx);
|
|
895
|
+
allRoutes.push(...routes);
|
|
896
|
+
}
|
|
897
|
+
return deduplicateRoutes(allRoutes);
|
|
898
|
+
};
|
|
899
|
+
var extractRoutesFromFile = (sourceFile, ctx) => {
|
|
900
|
+
const routes = [];
|
|
901
|
+
const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
|
|
902
|
+
for (const call of callExpressions) {
|
|
903
|
+
const route = tryExtractRoute(call, ctx);
|
|
904
|
+
if (route) routes.push(route);
|
|
905
|
+
}
|
|
906
|
+
return routes;
|
|
907
|
+
};
|
|
908
|
+
var tryExtractRoute = (call, ctx) => {
|
|
909
|
+
const expr = call.getExpression();
|
|
910
|
+
if (!import_ts_morph.Node.isPropertyAccessExpression(expr)) return null;
|
|
911
|
+
const methodName = expr.getName().toUpperCase();
|
|
912
|
+
if (!HTTP_METHODS.includes(methodName)) return null;
|
|
913
|
+
const args = call.getArguments();
|
|
914
|
+
if (args.length < 2) return null;
|
|
915
|
+
const firstArg = args[0];
|
|
916
|
+
if (!firstArg) return null;
|
|
917
|
+
if (!import_ts_morph.Node.isStringLiteral(firstArg) && !import_ts_morph.Node.isTemplateExpression(firstArg) && !import_ts_morph.Node.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const rawPath = firstArg.getText().replace(/['"'`]/g, "");
|
|
921
|
+
const path = normalizePath(ctx.routerPrefix, rawPath);
|
|
922
|
+
const method = methodName;
|
|
923
|
+
const middlewareArgs = args.slice(1, -1);
|
|
924
|
+
const handlerArg = args[args.length - 1];
|
|
925
|
+
const { requestBody, parameters } = extractFromMiddleware(middlewareArgs, ctx);
|
|
926
|
+
const pathParams = extractPathParameters(path);
|
|
927
|
+
const handlerInfo = extractHandlerInfo(handlerArg);
|
|
928
|
+
const allParameters = mergeParameters(parameters, pathParams);
|
|
929
|
+
const tags = inferTags(path);
|
|
930
|
+
const route = {
|
|
931
|
+
method,
|
|
932
|
+
path,
|
|
933
|
+
tags,
|
|
934
|
+
parameters: allParameters,
|
|
935
|
+
responses: buildDefaultResponses(method),
|
|
936
|
+
middleware: middlewareArgs.map((m) => m.getText())
|
|
937
|
+
};
|
|
938
|
+
if (handlerInfo.summary !== void 0) route.summary = handlerInfo.summary;
|
|
939
|
+
if (handlerInfo.description !== void 0) route.description = handlerInfo.description;
|
|
940
|
+
if (requestBody !== null) route.requestBody = requestBody;
|
|
941
|
+
return route;
|
|
942
|
+
};
|
|
943
|
+
var extractFromMiddleware = (middleware, ctx) => {
|
|
944
|
+
let requestBody = null;
|
|
945
|
+
const parameters = [];
|
|
946
|
+
for (const mw of middleware) {
|
|
947
|
+
const text = mw.getText();
|
|
948
|
+
const bodyMatch = text.match(/(?:validateBody|validate|zodValidate|joiValidate|validateDto|bodyValidator|checkBody|schemaValidator)\((\w+)\)/) ?? text.match(/celebrate\s*\(\s*\{\s*body\s*:\s*(\w+)/);
|
|
949
|
+
if (bodyMatch) {
|
|
950
|
+
const schemaName = bodyMatch[1];
|
|
951
|
+
const schema = schemaName ? ctx.zodSchemas[schemaName] : null;
|
|
952
|
+
if (schema) {
|
|
953
|
+
requestBody = {
|
|
954
|
+
required: true,
|
|
955
|
+
content: { "application/json": { schema } }
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const queryMatch = text.match(/(?:validateQuery|queryValidator|checkQuery)\((\w+)\)/);
|
|
960
|
+
if (queryMatch) {
|
|
961
|
+
const schemaName = queryMatch[1];
|
|
962
|
+
const schema = schemaName ? ctx.zodSchemas[schemaName] : null;
|
|
963
|
+
if (schema?.type === "object" && schema.properties) {
|
|
964
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
965
|
+
parameters.push({
|
|
966
|
+
name,
|
|
967
|
+
in: "query",
|
|
968
|
+
required: schema.required?.includes(name) ?? false,
|
|
969
|
+
schema: propSchema
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return { requestBody, parameters };
|
|
976
|
+
};
|
|
977
|
+
var extractPathParameters = (path) => {
|
|
978
|
+
const paramRegex = /:(\w+)/g;
|
|
979
|
+
const params = [];
|
|
980
|
+
let match;
|
|
981
|
+
while ((match = paramRegex.exec(path)) !== null) {
|
|
982
|
+
const name = match[1];
|
|
983
|
+
if (name) {
|
|
984
|
+
params.push({
|
|
985
|
+
name,
|
|
986
|
+
in: "path",
|
|
987
|
+
required: true,
|
|
988
|
+
schema: { type: "string" }
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return params;
|
|
993
|
+
};
|
|
994
|
+
var extractHandlerInfo = (handler) => {
|
|
995
|
+
const leadingComments = handler.getLeadingCommentRanges();
|
|
996
|
+
if (leadingComments.length === 0) return {};
|
|
997
|
+
const comment = leadingComments[leadingComments.length - 1];
|
|
998
|
+
if (!comment) return {};
|
|
999
|
+
const text = comment.getText();
|
|
1000
|
+
const summary = extractJsDocTag(text, null);
|
|
1001
|
+
const description = extractJsDocTag(text, "description");
|
|
1002
|
+
const result = {};
|
|
1003
|
+
if (summary !== null) result.summary = summary;
|
|
1004
|
+
if (description !== null) result.description = description;
|
|
1005
|
+
return result;
|
|
1006
|
+
};
|
|
1007
|
+
var extractJsDocTag = (comment, tag) => {
|
|
1008
|
+
if (tag === null) {
|
|
1009
|
+
const match2 = comment.match(/\/\*\*\s*\n?\s*\*\s*(.+)/);
|
|
1010
|
+
return match2?.[1]?.trim() ?? null;
|
|
1011
|
+
}
|
|
1012
|
+
const match = comment.match(new RegExp(`@${tag}\\s+(.+)`));
|
|
1013
|
+
return match?.[1]?.trim() ?? null;
|
|
1014
|
+
};
|
|
1015
|
+
var mergeParameters = (fromMiddleware, fromPath) => {
|
|
1016
|
+
const existing = new Set(fromMiddleware.map((p) => `${p.in}:${p.name}`));
|
|
1017
|
+
const merged = [...fromMiddleware];
|
|
1018
|
+
for (const param of fromPath) {
|
|
1019
|
+
if (!existing.has(`${param.in}:${param.name}`)) {
|
|
1020
|
+
merged.push(param);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return merged;
|
|
1024
|
+
};
|
|
1025
|
+
var normalizePath = (prefix, path) => {
|
|
1026
|
+
const combined = `${prefix}${path}`.replace(/\/+/g, "/");
|
|
1027
|
+
return combined.startsWith("/") ? combined : `/${combined}`;
|
|
1028
|
+
};
|
|
1029
|
+
var inferTags = (path) => {
|
|
1030
|
+
const segments = path.split("/").filter(Boolean);
|
|
1031
|
+
const tag = segments.find((s) => !s.startsWith(":"));
|
|
1032
|
+
return tag ? [tag] : ["default"];
|
|
1033
|
+
};
|
|
1034
|
+
var buildDefaultResponses = (method) => {
|
|
1035
|
+
const responses = {
|
|
1036
|
+
"200": { description: method === "DELETE" ? "Success" : "Successful response" },
|
|
1037
|
+
"400": { description: "Bad request" },
|
|
1038
|
+
"401": { description: "Unauthorized" },
|
|
1039
|
+
"500": { description: "Internal server error" }
|
|
1040
|
+
};
|
|
1041
|
+
if (method === "POST") {
|
|
1042
|
+
responses["201"] = { description: "Created" };
|
|
1043
|
+
}
|
|
1044
|
+
if (method === "DELETE") {
|
|
1045
|
+
responses["204"] = { description: "No content" };
|
|
1046
|
+
}
|
|
1047
|
+
return responses;
|
|
1048
|
+
};
|
|
1049
|
+
var deduplicateRoutes = (routes) => {
|
|
1050
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1051
|
+
return routes.filter((r) => {
|
|
1052
|
+
const key = `${r.method}:${r.path}`;
|
|
1053
|
+
if (seen.has(key)) return false;
|
|
1054
|
+
seen.add(key);
|
|
1055
|
+
return true;
|
|
1056
|
+
});
|
|
1057
|
+
};
|
|
1058
|
+
var buildSpec = (routes, config) => {
|
|
1059
|
+
const groups = groupRoutes(routes, config.groups);
|
|
1060
|
+
return {
|
|
1061
|
+
info: {
|
|
1062
|
+
title: config.name,
|
|
1063
|
+
version: config.version ?? "1.0.0",
|
|
1064
|
+
baseUrl: config.baseUrl ?? "http://localhost:3000",
|
|
1065
|
+
...config.description !== void 0 ? { description: config.description } : {}
|
|
1066
|
+
},
|
|
1067
|
+
...config.auth !== void 0 ? { auth: config.auth } : {},
|
|
1068
|
+
groups,
|
|
1069
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1070
|
+
sourceFramework: config.framework
|
|
1071
|
+
};
|
|
1072
|
+
};
|
|
1073
|
+
var groupRoutes = (routes, groupConfig) => {
|
|
1074
|
+
if (!groupConfig) {
|
|
1075
|
+
return groupByTag(routes);
|
|
1076
|
+
}
|
|
1077
|
+
const groups = [];
|
|
1078
|
+
const assignedRoutes = /* @__PURE__ */ new Set();
|
|
1079
|
+
for (const [groupName, patterns] of Object.entries(groupConfig)) {
|
|
1080
|
+
const matched = routes.filter((r) => {
|
|
1081
|
+
const key = `${r.method}:${r.path}`;
|
|
1082
|
+
if (assignedRoutes.has(key)) return false;
|
|
1083
|
+
const matches = patterns.some((pattern) => matchesGlob(r.path, pattern));
|
|
1084
|
+
if (matches) assignedRoutes.add(key);
|
|
1085
|
+
return matches;
|
|
1086
|
+
});
|
|
1087
|
+
if (matched.length > 0) {
|
|
1088
|
+
groups.push({ name: groupName, routes: matched });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const unassigned = routes.filter(
|
|
1092
|
+
(r) => !assignedRoutes.has(`${r.method}:${r.path}`)
|
|
1093
|
+
);
|
|
1094
|
+
if (unassigned.length > 0) {
|
|
1095
|
+
groups.push({ name: "Other", routes: unassigned });
|
|
1096
|
+
}
|
|
1097
|
+
return groups;
|
|
1098
|
+
};
|
|
1099
|
+
var groupByTag = (routes) => {
|
|
1100
|
+
const tagMap = /* @__PURE__ */ new Map();
|
|
1101
|
+
for (const route of routes) {
|
|
1102
|
+
const tag = route.tags[0] ?? "default";
|
|
1103
|
+
if (!tagMap.has(tag)) tagMap.set(tag, []);
|
|
1104
|
+
tagMap.get(tag).push(route);
|
|
1105
|
+
}
|
|
1106
|
+
return Array.from(tagMap.entries()).map(([name, groupRoutes2]) => ({
|
|
1107
|
+
name: capitalize(name),
|
|
1108
|
+
routes: groupRoutes2
|
|
1109
|
+
}));
|
|
1110
|
+
};
|
|
1111
|
+
var matchesGlob = (path, pattern) => {
|
|
1112
|
+
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*$/, "(/.*)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
1113
|
+
return new RegExp(`^${regexStr}$`).test(path);
|
|
1114
|
+
};
|
|
1115
|
+
var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
1116
|
+
|
|
1117
|
+
// src/generate.ts
|
|
1118
|
+
var generate = async (opts = {}) => {
|
|
1119
|
+
const cwd = process.cwd();
|
|
1120
|
+
const spinner = opts.quiet ? null : (0, import_ora.default)();
|
|
1121
|
+
spinner?.start("Loading flowdoc config...");
|
|
1122
|
+
const configPath = opts.config ? (0, import_path3.resolve)(cwd, opts.config) : findConfigFile(cwd);
|
|
1123
|
+
if (!configPath) {
|
|
1124
|
+
spinner?.fail(import_chalk.default.red("No flowdoc.config.ts found. Run `flowdoc init` first."));
|
|
1125
|
+
process.exit(1);
|
|
1126
|
+
}
|
|
1127
|
+
let rawConfig;
|
|
1128
|
+
try {
|
|
1129
|
+
rawConfig = await loadConfig(configPath);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
spinner?.fail(import_chalk.default.red(`Failed to load config: ${String(err)}`));
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
const config = resolveConfig(rawConfig, cwd);
|
|
1135
|
+
spinner?.succeed(`Config loaded \u2014 ${import_chalk.default.cyan(config.name)}`);
|
|
1136
|
+
spinner?.start(`Scanning ${import_chalk.default.cyan(config.entry)} for routes...`);
|
|
1137
|
+
let routes;
|
|
1138
|
+
try {
|
|
1139
|
+
routes = await extractExpressRoutes(config);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
spinner?.fail(import_chalk.default.red(`Parse failed: ${String(err)}`));
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
spinner?.succeed(
|
|
1145
|
+
`Found ${import_chalk.default.green(String(routes.length))} routes across ${import_chalk.default.cyan(config.framework)} app`
|
|
1146
|
+
);
|
|
1147
|
+
const spec = buildSpec(routes, config);
|
|
1148
|
+
const outputDir = opts.output ? (0, import_path3.resolve)(cwd, opts.output) : config.output ?? (0, import_path3.resolve)(cwd, "docs-output");
|
|
1149
|
+
(0, import_fs3.mkdirSync)(outputDir, { recursive: true });
|
|
1150
|
+
const specPath = (0, import_path3.join)(outputDir, "flowdoc.json");
|
|
1151
|
+
(0, import_fs3.writeFileSync)(specPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
1152
|
+
await writeUiHtml(outputDir, config);
|
|
1153
|
+
if (!opts.quiet) {
|
|
1154
|
+
console.log();
|
|
1155
|
+
console.log(import_chalk.default.bold(" flowdoc generated successfully"));
|
|
1156
|
+
console.log();
|
|
1157
|
+
console.log(` ${import_chalk.default.gray("Spec:")} ${import_chalk.default.cyan(specPath)}`);
|
|
1158
|
+
console.log(` ${import_chalk.default.gray("UI:")} ${import_chalk.default.cyan((0, import_path3.join)(outputDir, "index.html"))}`);
|
|
1159
|
+
console.log();
|
|
1160
|
+
console.log(` ${import_chalk.default.gray("Routes:")} ${import_chalk.default.green(String(routes.length))}`);
|
|
1161
|
+
console.log(` ${import_chalk.default.gray("Groups:")} ${import_chalk.default.green(String(spec.groups.length))}`);
|
|
1162
|
+
console.log();
|
|
1163
|
+
}
|
|
1164
|
+
return spec;
|
|
1165
|
+
};
|
|
1166
|
+
var writeUiHtml = async (outputDir, config) => {
|
|
1167
|
+
const brand = config.theme?.brand ?? "#6366f1";
|
|
1168
|
+
const title = config.name;
|
|
1169
|
+
const darkMode = config.theme?.darkMode !== false;
|
|
1170
|
+
const cliRoot = (0, import_path3.dirname)((0, import_path3.dirname)((0, import_url.fileURLToPath)(importMetaUrl)));
|
|
1171
|
+
const uiAssetsSource = (0, import_path3.join)(cliRoot, "ui-assets");
|
|
1172
|
+
const uiAssetsDest = (0, import_path3.join)(outputDir, "assets");
|
|
1173
|
+
if ((0, import_fs3.existsSync)(uiAssetsSource)) {
|
|
1174
|
+
(0, import_fs3.mkdirSync)(uiAssetsDest, { recursive: true });
|
|
1175
|
+
(0, import_fs3.cpSync)(uiAssetsSource, uiAssetsDest, { recursive: true });
|
|
1176
|
+
}
|
|
1177
|
+
const html = generateHtmlShell({ title, brand, darkMode });
|
|
1178
|
+
(0, import_fs3.writeFileSync)((0, import_path3.join)(outputDir, "index.html"), html, "utf-8");
|
|
1179
|
+
};
|
|
1180
|
+
var generateHtmlShell = ({ title, brand, darkMode }) => `<!DOCTYPE html>
|
|
1181
|
+
<html lang="en" class="${darkMode ? "dark" : ""}">
|
|
1182
|
+
<head>
|
|
1183
|
+
<meta charset="UTF-8" />
|
|
1184
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1185
|
+
<title>${title} \u2014 API Docs</title>
|
|
1186
|
+
<meta name="description" content="API documentation generated by flowdoc" />
|
|
1187
|
+
<script>
|
|
1188
|
+
window.__FLOWDOC_BRAND__ = "${brand}";
|
|
1189
|
+
window.__FLOWDOC_DARK__ = ${String(darkMode)};
|
|
1190
|
+
</script>
|
|
1191
|
+
<script type="module" crossorigin src="./assets/ui.js"></script>
|
|
1192
|
+
<link rel="stylesheet" href="./assets/index.css" />
|
|
1193
|
+
</head>
|
|
1194
|
+
<body>
|
|
1195
|
+
<div id="root"></div>
|
|
1196
|
+
</body>
|
|
1197
|
+
</html>`;
|
|
1198
|
+
|
|
1199
|
+
// src/serve.ts
|
|
1200
|
+
var import_http = require("http");
|
|
1201
|
+
var import_fs4 = require("fs");
|
|
1202
|
+
var import_path4 = require("path");
|
|
1203
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
1204
|
+
var import_open = __toESM(require("open"), 1);
|
|
1205
|
+
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
1206
|
+
var MIME_TYPES = {
|
|
1207
|
+
".html": "text/html",
|
|
1208
|
+
".js": "application/javascript",
|
|
1209
|
+
".css": "text/css",
|
|
1210
|
+
".json": "application/json",
|
|
1211
|
+
".svg": "image/svg+xml",
|
|
1212
|
+
".png": "image/png",
|
|
1213
|
+
".ico": "image/x-icon"
|
|
1214
|
+
};
|
|
1215
|
+
var serve = async (opts = {}) => {
|
|
1216
|
+
const cwd = process.cwd();
|
|
1217
|
+
const outputDir = opts.output ?? (0, import_path4.join)(cwd, "docs-output");
|
|
1218
|
+
const port = opts.port ?? 4e3;
|
|
1219
|
+
await generate({ ...opts, quiet: false });
|
|
1220
|
+
const server = (0, import_http.createServer)((req, res) => {
|
|
1221
|
+
const url = req.url === "/" || req.url === "" ? "/index.html" : req.url ?? "/index.html";
|
|
1222
|
+
const filePath = (0, import_path4.join)(outputDir, url);
|
|
1223
|
+
if (!(0, import_fs4.existsSync)(filePath)) {
|
|
1224
|
+
res.writeHead(404);
|
|
1225
|
+
res.end("Not found");
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const ext = (0, import_path4.extname)(filePath);
|
|
1229
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1230
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1231
|
+
(0, import_fs4.createReadStream)(filePath).pipe(res);
|
|
1232
|
+
});
|
|
1233
|
+
server.listen(port, () => {
|
|
1234
|
+
const url = `http://localhost:${port}`;
|
|
1235
|
+
console.log();
|
|
1236
|
+
console.log(` ${import_chalk2.default.bold("flowdoc")} is running at ${import_chalk2.default.cyan(url)}`);
|
|
1237
|
+
if (opts.watch) {
|
|
1238
|
+
console.log(` ${import_chalk2.default.dim("watching for changes\u2026")}`);
|
|
1239
|
+
}
|
|
1240
|
+
console.log();
|
|
1241
|
+
if (!opts.noOpen) {
|
|
1242
|
+
void (0, import_open.default)(url);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
if (!opts.watch) return;
|
|
1246
|
+
const configPath = opts.config ?? findConfigFile(cwd) ?? "";
|
|
1247
|
+
let watchGlob = "src/**/*.ts";
|
|
1248
|
+
if (configPath) {
|
|
1249
|
+
try {
|
|
1250
|
+
const raw = await loadConfig(configPath);
|
|
1251
|
+
const config = resolveConfig(raw, cwd);
|
|
1252
|
+
const entryDir = (0, import_path4.resolve)(cwd, config.entry).replace(/\/[^/]+$/, "");
|
|
1253
|
+
watchGlob = `${entryDir}/**/*.ts`;
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
let rebuilding = false;
|
|
1258
|
+
const watcher = import_chokidar.default.watch(watchGlob, {
|
|
1259
|
+
ignoreInitial: true,
|
|
1260
|
+
ignored: ["**/node_modules/**", "**/dist/**", "**/docs-output/**"]
|
|
1261
|
+
});
|
|
1262
|
+
const rebuild = async (filePath) => {
|
|
1263
|
+
if (rebuilding) return;
|
|
1264
|
+
rebuilding = true;
|
|
1265
|
+
console.log(` ${import_chalk2.default.dim("\u2192")} ${import_chalk2.default.yellow(filePath.replace(cwd + "/", ""))} changed \u2014 regenerating\u2026`);
|
|
1266
|
+
try {
|
|
1267
|
+
await generate({ ...opts, quiet: true });
|
|
1268
|
+
console.log(` ${import_chalk2.default.green("\u2713")} docs updated`);
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
console.error(` ${import_chalk2.default.red("\u2717")} regeneration failed:`, err instanceof Error ? err.message : err);
|
|
1271
|
+
} finally {
|
|
1272
|
+
rebuilding = false;
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
watcher.on("add", rebuild).on("change", rebuild).on("unlink", rebuild);
|
|
1276
|
+
process.on("SIGINT", () => {
|
|
1277
|
+
void watcher.close();
|
|
1278
|
+
server.close();
|
|
1279
|
+
process.exit(0);
|
|
1280
|
+
});
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/init.ts
|
|
1284
|
+
var import_fs5 = require("fs");
|
|
1285
|
+
var import_path5 = require("path");
|
|
1286
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
1287
|
+
var CONFIG_TEMPLATE = `import { defineConfig } from "flowdoc-gen";
|
|
1288
|
+
|
|
1289
|
+
export default defineConfig({
|
|
1290
|
+
name: "My API",
|
|
1291
|
+
version: "1.0.0",
|
|
1292
|
+
description: "API documentation generated by flowdoc",
|
|
1293
|
+
framework: "express",
|
|
1294
|
+
entry: "./src",
|
|
1295
|
+
output: "./docs-output",
|
|
1296
|
+
theme: {
|
|
1297
|
+
brand: "#6366f1",
|
|
1298
|
+
darkMode: true,
|
|
1299
|
+
},
|
|
1300
|
+
// groups: [
|
|
1301
|
+
// { name: "Auth", match: "/auth/**" },
|
|
1302
|
+
// { name: "Users", match: "/users/**" },
|
|
1303
|
+
// ],
|
|
1304
|
+
});
|
|
1305
|
+
`;
|
|
1306
|
+
var init = (cwd = process.cwd()) => {
|
|
1307
|
+
const configPath = (0, import_path5.resolve)(cwd, "flowdoc.config.ts");
|
|
1308
|
+
if ((0, import_fs5.existsSync)(configPath)) {
|
|
1309
|
+
console.log(import_chalk3.default.yellow(" flowdoc.config.ts already exists \u2014 skipping."));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
(0, import_fs5.writeFileSync)(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
1313
|
+
console.log();
|
|
1314
|
+
console.log(` ${import_chalk3.default.bold("flowdoc")} initialized`);
|
|
1315
|
+
console.log();
|
|
1316
|
+
console.log(` Created ${import_chalk3.default.cyan("flowdoc.config.ts")}`);
|
|
1317
|
+
console.log();
|
|
1318
|
+
console.log(" Next steps:");
|
|
1319
|
+
console.log(` 1. Edit ${import_chalk3.default.cyan("flowdoc.config.ts")} \u2014 set your entry path`);
|
|
1320
|
+
console.log(` 2. Run ${import_chalk3.default.cyan("flowdoc generate")} to build your docs`);
|
|
1321
|
+
console.log(` 3. Run ${import_chalk3.default.cyan("flowdoc serve")} to preview locally`);
|
|
1322
|
+
console.log();
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// src/middleware.ts
|
|
1326
|
+
var import_fs6 = require("fs");
|
|
1327
|
+
var import_path6 = require("path");
|
|
1328
|
+
var import_url2 = require("url");
|
|
1329
|
+
var MIME = {
|
|
1330
|
+
".html": "text/html",
|
|
1331
|
+
".js": "application/javascript",
|
|
1332
|
+
".css": "text/css",
|
|
1333
|
+
".json": "application/json"
|
|
1334
|
+
};
|
|
1335
|
+
var flowdoc = (opts = {}) => {
|
|
1336
|
+
const cwd = process.cwd();
|
|
1337
|
+
let outputDir = null;
|
|
1338
|
+
let ready = false;
|
|
1339
|
+
let initError = null;
|
|
1340
|
+
const init2 = (async () => {
|
|
1341
|
+
try {
|
|
1342
|
+
const configPath = opts.config ?? findConfigFile(cwd);
|
|
1343
|
+
if (!configPath) throw new Error("No flowdoc.config.ts found. Run `flowdoc init` first.");
|
|
1344
|
+
const rawConfig = await loadConfig(configPath);
|
|
1345
|
+
const config = resolveConfig(rawConfig, cwd);
|
|
1346
|
+
outputDir = config.output ?? (0, import_path6.join)(cwd, "docs-output");
|
|
1347
|
+
const routes = await extractExpressRoutes(config);
|
|
1348
|
+
const spec = buildSpec(routes, config);
|
|
1349
|
+
(0, import_fs6.mkdirSync)(outputDir, { recursive: true });
|
|
1350
|
+
(0, import_fs6.writeFileSync)((0, import_path6.join)(outputDir, "flowdoc.json"), JSON.stringify(spec, null, 2));
|
|
1351
|
+
const cliRoot = (0, import_path6.dirname)((0, import_path6.dirname)((0, import_url2.fileURLToPath)(importMetaUrl)));
|
|
1352
|
+
const uiAssetsSource = (0, import_path6.join)(cliRoot, "ui-assets");
|
|
1353
|
+
if ((0, import_fs6.existsSync)(uiAssetsSource)) {
|
|
1354
|
+
const dest = (0, import_path6.join)(outputDir, "assets");
|
|
1355
|
+
(0, import_fs6.mkdirSync)(dest, { recursive: true });
|
|
1356
|
+
(0, import_fs6.cpSync)(uiAssetsSource, dest, { recursive: true });
|
|
1357
|
+
}
|
|
1358
|
+
ready = true;
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
initError = err instanceof Error ? err.message : String(err);
|
|
1361
|
+
}
|
|
1362
|
+
})();
|
|
1363
|
+
return async (req, res, next) => {
|
|
1364
|
+
await init2;
|
|
1365
|
+
if (initError || !outputDir) {
|
|
1366
|
+
res.status(500).send(`flowdoc init failed: ${initError}`);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const urlPath = req.path === "/" || req.path === "" ? "/index.html" : req.path;
|
|
1370
|
+
if (urlPath === "/index.html") {
|
|
1371
|
+
const brand = "#6366f1";
|
|
1372
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
1373
|
+
res.setHeader("Content-Type", "text/html");
|
|
1374
|
+
res.send(buildHtml({ baseUrl, brand }));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const filePath = (0, import_path6.join)(outputDir, urlPath);
|
|
1378
|
+
if (!(0, import_fs6.existsSync)(filePath)) {
|
|
1379
|
+
next();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const mime = MIME[(0, import_path6.extname)(filePath)] ?? "application/octet-stream";
|
|
1383
|
+
res.setHeader("Content-Type", mime);
|
|
1384
|
+
(0, import_fs6.createReadStream)(filePath).pipe(res);
|
|
1385
|
+
};
|
|
1386
|
+
};
|
|
1387
|
+
var buildHtml = ({ baseUrl, brand }) => `<!DOCTYPE html>
|
|
1388
|
+
<html lang="en" class="dark">
|
|
1389
|
+
<head>
|
|
1390
|
+
<meta charset="UTF-8" />
|
|
1391
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1392
|
+
<title>API Docs</title>
|
|
1393
|
+
<script>
|
|
1394
|
+
window.__FLOWDOC_BRAND__ = "${brand}";
|
|
1395
|
+
window.__FLOWDOC_BASE_URL__ = "${baseUrl}";
|
|
1396
|
+
</script>
|
|
1397
|
+
<script type="module" crossorigin src="./assets/ui.js"></script>
|
|
1398
|
+
<link rel="stylesheet" href="./assets/index.css" />
|
|
1399
|
+
</head>
|
|
1400
|
+
<body><div id="root"></div></body>
|
|
1401
|
+
</html>`;
|
|
1402
|
+
|
|
1403
|
+
// src/index.ts
|
|
1404
|
+
var defineConfig = (config) => config;
|
|
1405
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1406
|
+
0 && (module.exports = {
|
|
1407
|
+
defineConfig,
|
|
1408
|
+
flowdoc,
|
|
1409
|
+
generate,
|
|
1410
|
+
init,
|
|
1411
|
+
serve
|
|
1412
|
+
});
|