flowdoc-gen 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/dist/bin.js +1 -1
- package/dist/{chunk-3GGK6LWE.js → index.cjs} +358 -114
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +114 -2
- package/dist/index.js +12 -15
- package/dist/{init-3GG2S3V3.js → init-6XHCTCLE.js} +8 -15
- package/package.json +2 -1
- package/dist/bin.d.ts +0 -1
- package/dist/chunk-P6Z6T3W4.js +0 -51
- package/dist/chunk-SAMPAR3A.js +0 -93
- package/dist/chunk-VG2YJHSH.js +0 -52
- package/dist/chunk-XXW6UJOX.js +0 -604
- package/dist/generate-E4V2RQYB.js +0 -6
- package/dist/generate-J6FGBLQ4.js +0 -7
- package/dist/generate-MNQL7RGI.js +0 -7
- package/dist/init-27XS6ADW.js +0 -53
- package/dist/init-HVJTHT4U.js +0 -6
- package/dist/serve-NNDUXHXZ.js +0 -94
- package/dist/serve-VKTQ5E5O.js +0 -7
- package/dist/serve-Y4E3DTAJ.js +0 -94
|
@@ -1,15 +1,53 @@
|
|
|
1
|
-
|
|
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);
|
|
2
40
|
|
|
3
41
|
// src/generate.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
42
|
+
var import_fs3 = require("fs");
|
|
43
|
+
var import_path3 = require("path");
|
|
44
|
+
var import_url = require("url");
|
|
45
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
46
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
9
47
|
|
|
10
48
|
// ../core/dist/index.js
|
|
11
|
-
|
|
12
|
-
|
|
49
|
+
var import_fs = require("fs");
|
|
50
|
+
var import_path = require("path");
|
|
13
51
|
var CONFIG_FILES = [
|
|
14
52
|
"flowdoc.config.ts",
|
|
15
53
|
"flowdoc.config.js",
|
|
@@ -17,13 +55,13 @@ var CONFIG_FILES = [
|
|
|
17
55
|
];
|
|
18
56
|
var findConfigFile = (cwd) => {
|
|
19
57
|
for (const file of CONFIG_FILES) {
|
|
20
|
-
const full = join(cwd, file);
|
|
21
|
-
if (existsSync(full)) return full;
|
|
58
|
+
const full = (0, import_path.join)(cwd, file);
|
|
59
|
+
if ((0, import_fs.existsSync)(full)) return full;
|
|
22
60
|
}
|
|
23
61
|
return null;
|
|
24
62
|
};
|
|
25
63
|
var loadConfig = async (configPath) => {
|
|
26
|
-
const resolved = resolve(configPath);
|
|
64
|
+
const resolved = (0, import_path.resolve)(configPath);
|
|
27
65
|
const mod = await import(resolved);
|
|
28
66
|
const config = "default" in mod ? mod.default : mod;
|
|
29
67
|
if (!config) throw new Error(`No config exported from ${configPath}`);
|
|
@@ -33,25 +71,21 @@ var resolveConfig = (config, cwd) => ({
|
|
|
33
71
|
version: "1.0.0",
|
|
34
72
|
baseUrl: "http://localhost:3000",
|
|
35
73
|
...config,
|
|
36
|
-
entry: resolve(cwd, config.entry),
|
|
37
|
-
output: resolve(cwd, config.output ?? "./docs-output")
|
|
74
|
+
entry: (0, import_path.resolve)(cwd, config.entry),
|
|
75
|
+
output: (0, import_path.resolve)(cwd, config.output ?? "./docs-output")
|
|
38
76
|
});
|
|
39
77
|
|
|
40
78
|
// ../parser/dist/index.js
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
import { Node } from "ts-morph";
|
|
50
|
-
import { Node as Node2 } from "ts-morph";
|
|
51
|
-
import { Node as Node3 } from "ts-morph";
|
|
52
|
-
import { Node as Node4 } from "ts-morph";
|
|
79
|
+
var import_ts_morph = require("ts-morph");
|
|
80
|
+
var import_glob = require("glob");
|
|
81
|
+
var import_path2 = require("path");
|
|
82
|
+
var import_fs2 = require("fs");
|
|
83
|
+
var import_ts_morph2 = require("ts-morph");
|
|
84
|
+
var import_ts_morph3 = require("ts-morph");
|
|
85
|
+
var import_ts_morph4 = require("ts-morph");
|
|
86
|
+
var import_ts_morph5 = require("ts-morph");
|
|
53
87
|
var zodNodeToJsonSchema = (node) => {
|
|
54
|
-
if (!Node.isCallExpression(node)) return {};
|
|
88
|
+
if (!import_ts_morph2.Node.isCallExpression(node)) return {};
|
|
55
89
|
const expr = node.getExpression();
|
|
56
90
|
const callText = expr.getText();
|
|
57
91
|
if (callText === "z.string") return buildStringSchema(node);
|
|
@@ -68,7 +102,7 @@ var zodNodeToJsonSchema = (node) => {
|
|
|
68
102
|
if (callText === "z.date") return { type: "string", format: "date-time" };
|
|
69
103
|
if (callText === "z.any") return {};
|
|
70
104
|
if (callText === "z.unknown") return {};
|
|
71
|
-
if (Node.isPropertyAccessExpression(expr)) {
|
|
105
|
+
if (import_ts_morph2.Node.isPropertyAccessExpression(expr)) {
|
|
72
106
|
return buildChainedSchema(node);
|
|
73
107
|
}
|
|
74
108
|
return {};
|
|
@@ -91,23 +125,23 @@ var buildLiteralSchema = (node) => {
|
|
|
91
125
|
};
|
|
92
126
|
var buildEnumSchema = (node) => {
|
|
93
127
|
const arg = node.getArguments()[0];
|
|
94
|
-
if (!arg || !Node.isArrayLiteralExpression(arg)) return { type: "string" };
|
|
128
|
+
if (!arg || !import_ts_morph2.Node.isArrayLiteralExpression(arg)) return { type: "string" };
|
|
95
129
|
const values = arg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
96
130
|
return { type: "string", enum: values };
|
|
97
131
|
};
|
|
98
132
|
var buildObjectSchema = (node) => {
|
|
99
133
|
const arg = node.getArguments()[0];
|
|
100
|
-
if (!arg || !Node.isObjectLiteralExpression(arg)) return { type: "object" };
|
|
134
|
+
if (!arg || !import_ts_morph2.Node.isObjectLiteralExpression(arg)) return { type: "object" };
|
|
101
135
|
const properties = {};
|
|
102
136
|
const required = [];
|
|
103
137
|
for (const prop of arg.getProperties()) {
|
|
104
|
-
if (!Node.isPropertyAssignment(prop)) continue;
|
|
138
|
+
if (!import_ts_morph2.Node.isPropertyAssignment(prop)) continue;
|
|
105
139
|
const name = prop.getName();
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
108
|
-
const childSchema = zodNodeToJsonSchema(
|
|
140
|
+
const init2 = prop.getInitializer();
|
|
141
|
+
if (!init2) continue;
|
|
142
|
+
const childSchema = zodNodeToJsonSchema(init2);
|
|
109
143
|
properties[name] = childSchema;
|
|
110
|
-
if (!isOptionalZodExpr(
|
|
144
|
+
if (!isOptionalZodExpr(init2)) {
|
|
111
145
|
required.push(name);
|
|
112
146
|
}
|
|
113
147
|
}
|
|
@@ -124,7 +158,7 @@ var buildArraySchema = (node) => {
|
|
|
124
158
|
};
|
|
125
159
|
var buildUnionSchema = (node) => {
|
|
126
160
|
const arg = node.getArguments()[0];
|
|
127
|
-
if (!arg || !Node.isArrayLiteralExpression(arg)) return {};
|
|
161
|
+
if (!arg || !import_ts_morph2.Node.isArrayLiteralExpression(arg)) return {};
|
|
128
162
|
const schemas = arg.getElements().map((el) => zodNodeToJsonSchema(el));
|
|
129
163
|
return { anyOf: schemas };
|
|
130
164
|
};
|
|
@@ -143,11 +177,11 @@ var buildChainedSchema = (node) => {
|
|
|
143
177
|
var unwrapChain = (node) => {
|
|
144
178
|
const methods = [];
|
|
145
179
|
let current = node;
|
|
146
|
-
while (Node.isCallExpression(current)) {
|
|
180
|
+
while (import_ts_morph2.Node.isCallExpression(current)) {
|
|
147
181
|
const expr = current.getExpression();
|
|
148
|
-
if (Node.isPropertyAccessExpression(expr)) {
|
|
182
|
+
if (import_ts_morph2.Node.isPropertyAccessExpression(expr)) {
|
|
149
183
|
const obj = expr.getExpression();
|
|
150
|
-
if (Node.isIdentifier(obj)) {
|
|
184
|
+
if (import_ts_morph2.Node.isIdentifier(obj)) {
|
|
151
185
|
return { root: current, methods };
|
|
152
186
|
}
|
|
153
187
|
const methodName = expr.getName();
|
|
@@ -252,13 +286,13 @@ var extractZodSchemas = (sourceFile) => {
|
|
|
252
286
|
const schemas = {};
|
|
253
287
|
const varDeclarations = sourceFile.getVariableDeclarations();
|
|
254
288
|
for (const decl of varDeclarations) {
|
|
255
|
-
const
|
|
256
|
-
if (!
|
|
257
|
-
const text =
|
|
289
|
+
const init2 = decl.getInitializer();
|
|
290
|
+
if (!init2) continue;
|
|
291
|
+
const text = init2.getText();
|
|
258
292
|
if (!text.startsWith("z.")) continue;
|
|
259
293
|
const name = decl.getName();
|
|
260
294
|
try {
|
|
261
|
-
schemas[name] = zodNodeToJsonSchema(
|
|
295
|
+
schemas[name] = zodNodeToJsonSchema(init2);
|
|
262
296
|
} catch {
|
|
263
297
|
}
|
|
264
298
|
}
|
|
@@ -276,7 +310,7 @@ var YUP_ROOT_METHODS = /* @__PURE__ */ new Set([
|
|
|
276
310
|
"lazy"
|
|
277
311
|
]);
|
|
278
312
|
var yupNodeToJsonSchema = (node) => {
|
|
279
|
-
if (!
|
|
313
|
+
if (!import_ts_morph3.Node.isCallExpression(node)) return {};
|
|
280
314
|
const chain = unwrapYupChain(node);
|
|
281
315
|
if (!chain.root) return {};
|
|
282
316
|
const base = buildYupBase(chain.rootMethod);
|
|
@@ -286,17 +320,17 @@ var yupNodeToJsonSchema = (node) => {
|
|
|
286
320
|
var unwrapYupChain = (node) => {
|
|
287
321
|
const methods = [];
|
|
288
322
|
let current = node;
|
|
289
|
-
while (
|
|
323
|
+
while (import_ts_morph3.Node.isCallExpression(current)) {
|
|
290
324
|
const expr = current.getExpression();
|
|
291
|
-
if (!
|
|
292
|
-
if (
|
|
325
|
+
if (!import_ts_morph3.Node.isPropertyAccessExpression(expr)) {
|
|
326
|
+
if (import_ts_morph3.Node.isIdentifier(expr) && YUP_ROOT_METHODS.has(expr.getText())) {
|
|
293
327
|
return { root: current, rootMethod: expr.getText(), methods };
|
|
294
328
|
}
|
|
295
329
|
return { root: null, rootMethod: "", methods };
|
|
296
330
|
}
|
|
297
331
|
const obj = expr.getExpression();
|
|
298
332
|
const methodName = expr.getName();
|
|
299
|
-
if (
|
|
333
|
+
if (import_ts_morph3.Node.isIdentifier(obj) && obj.getText() === "yup" && YUP_ROOT_METHODS.has(methodName)) {
|
|
300
334
|
return { root: current, rootMethod: methodName, methods };
|
|
301
335
|
}
|
|
302
336
|
methods.unshift({ name: methodName, args: current.getArguments() });
|
|
@@ -379,14 +413,14 @@ var applyYupChain = (methods, schema) => {
|
|
|
379
413
|
break;
|
|
380
414
|
case "oneOf": {
|
|
381
415
|
const arg = args[0];
|
|
382
|
-
if (arg &&
|
|
416
|
+
if (arg && import_ts_morph3.Node.isArrayLiteralExpression(arg)) {
|
|
383
417
|
schema.enum = arg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
384
418
|
}
|
|
385
419
|
break;
|
|
386
420
|
}
|
|
387
421
|
case "shape": {
|
|
388
422
|
const arg = args[0];
|
|
389
|
-
if (arg &&
|
|
423
|
+
if (arg && import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
390
424
|
const { properties, required: req } = parseYupShape(arg);
|
|
391
425
|
schema.type = "object";
|
|
392
426
|
schema.properties = properties;
|
|
@@ -406,9 +440,9 @@ var applyYupChain = (methods, schema) => {
|
|
|
406
440
|
}
|
|
407
441
|
case "meta": {
|
|
408
442
|
const arg = args[0];
|
|
409
|
-
if (arg &&
|
|
443
|
+
if (arg && import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
410
444
|
for (const prop of arg.getProperties()) {
|
|
411
|
-
if (
|
|
445
|
+
if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "description") {
|
|
412
446
|
const s = getStr(prop.getInitializer());
|
|
413
447
|
if (s) schema.description = s;
|
|
414
448
|
}
|
|
@@ -428,13 +462,13 @@ var applyYupChain = (methods, schema) => {
|
|
|
428
462
|
var parseYupShape = (arg) => {
|
|
429
463
|
const properties = {};
|
|
430
464
|
const required = [];
|
|
431
|
-
if (!arg || !
|
|
465
|
+
if (!arg || !import_ts_morph3.Node.isObjectLiteralExpression(arg)) return { properties, required };
|
|
432
466
|
for (const prop of arg.getProperties()) {
|
|
433
|
-
if (!
|
|
467
|
+
if (!import_ts_morph3.Node.isPropertyAssignment(prop)) continue;
|
|
434
468
|
const name = prop.getName();
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
437
|
-
const childSchema = yupNodeToJsonSchema(
|
|
469
|
+
const init2 = prop.getInitializer();
|
|
470
|
+
if (!init2) continue;
|
|
471
|
+
const childSchema = yupNodeToJsonSchema(init2);
|
|
438
472
|
const isReq = childSchema.__required === true;
|
|
439
473
|
delete childSchema.__required;
|
|
440
474
|
properties[name] = childSchema;
|
|
@@ -463,12 +497,12 @@ var isYupExpr = (text) => /^yup\.(?:string|number|boolean|object|array|date|mixe
|
|
|
463
497
|
var extractYupSchemas = (sourceFile) => {
|
|
464
498
|
const schemas = {};
|
|
465
499
|
for (const decl of sourceFile.getVariableDeclarations()) {
|
|
466
|
-
const
|
|
467
|
-
if (!
|
|
468
|
-
const text =
|
|
500
|
+
const init2 = decl.getInitializer();
|
|
501
|
+
if (!init2) continue;
|
|
502
|
+
const text = init2.getText();
|
|
469
503
|
if (!isYupExpr(text)) continue;
|
|
470
504
|
try {
|
|
471
|
-
const schema = yupNodeToJsonSchema(
|
|
505
|
+
const schema = yupNodeToJsonSchema(init2);
|
|
472
506
|
if (Object.keys(schema).length > 0) schemas[decl.getName()] = schema;
|
|
473
507
|
} catch {
|
|
474
508
|
}
|
|
@@ -488,7 +522,7 @@ var JOI_PRIMITIVES = /* @__PURE__ */ new Set([
|
|
|
488
522
|
"link"
|
|
489
523
|
]);
|
|
490
524
|
var joiNodeToJsonSchema = (node, joiId = "Joi") => {
|
|
491
|
-
if (!
|
|
525
|
+
if (!import_ts_morph4.Node.isCallExpression(node)) return {};
|
|
492
526
|
const chain = unwrapJoiChain(node, joiId);
|
|
493
527
|
if (!chain.root) return {};
|
|
494
528
|
const base = buildJoiBase(chain.rootMethod);
|
|
@@ -498,14 +532,14 @@ var joiNodeToJsonSchema = (node, joiId = "Joi") => {
|
|
|
498
532
|
var unwrapJoiChain = (node, joiId) => {
|
|
499
533
|
const methods = [];
|
|
500
534
|
let current = node;
|
|
501
|
-
while (
|
|
535
|
+
while (import_ts_morph4.Node.isCallExpression(current)) {
|
|
502
536
|
const expr = current.getExpression();
|
|
503
|
-
if (!
|
|
537
|
+
if (!import_ts_morph4.Node.isPropertyAccessExpression(expr)) {
|
|
504
538
|
return { root: null, rootMethod: "", methods };
|
|
505
539
|
}
|
|
506
540
|
const obj = expr.getExpression();
|
|
507
541
|
const methodName = expr.getName();
|
|
508
|
-
if (
|
|
542
|
+
if (import_ts_morph4.Node.isIdentifier(obj) && obj.getText() === joiId && JOI_PRIMITIVES.has(methodName)) {
|
|
509
543
|
return { root: current, rootMethod: methodName, methods };
|
|
510
544
|
}
|
|
511
545
|
methods.unshift({ name: methodName, args: current.getArguments() });
|
|
@@ -603,7 +637,7 @@ var applyJoiChain = (methods, schema, joiId) => {
|
|
|
603
637
|
}
|
|
604
638
|
case "keys": {
|
|
605
639
|
const arg = args[0];
|
|
606
|
-
if (arg &&
|
|
640
|
+
if (arg && import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
|
|
607
641
|
const { properties, required } = parseJoiKeys(arg, joiId);
|
|
608
642
|
schema.type = "object";
|
|
609
643
|
schema.properties = properties;
|
|
@@ -637,13 +671,13 @@ var applyJoiChain = (methods, schema, joiId) => {
|
|
|
637
671
|
var parseJoiKeys = (arg, joiId) => {
|
|
638
672
|
const properties = {};
|
|
639
673
|
const required = [];
|
|
640
|
-
if (!arg || !
|
|
674
|
+
if (!arg || !import_ts_morph4.Node.isObjectLiteralExpression(arg)) return { properties, required };
|
|
641
675
|
for (const prop of arg.getProperties()) {
|
|
642
|
-
if (!
|
|
676
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(prop)) continue;
|
|
643
677
|
const name = prop.getName();
|
|
644
|
-
const
|
|
645
|
-
if (!
|
|
646
|
-
const childSchema = joiNodeToJsonSchema(
|
|
678
|
+
const init2 = prop.getInitializer();
|
|
679
|
+
if (!init2) continue;
|
|
680
|
+
const childSchema = joiNodeToJsonSchema(init2, joiId);
|
|
647
681
|
const isReq = childSchema.__required === true;
|
|
648
682
|
delete childSchema.__required;
|
|
649
683
|
properties[name] = childSchema;
|
|
@@ -683,11 +717,11 @@ var extractJoiSchemas = (sourceFile) => {
|
|
|
683
717
|
const joiId = detectJoiId(sourceFile);
|
|
684
718
|
if (!joiId) return schemas;
|
|
685
719
|
for (const decl of sourceFile.getVariableDeclarations()) {
|
|
686
|
-
const
|
|
687
|
-
if (!
|
|
688
|
-
if (!isJoiExpr(
|
|
720
|
+
const init2 = decl.getInitializer();
|
|
721
|
+
if (!init2) continue;
|
|
722
|
+
if (!isJoiExpr(init2.getText(), joiId)) continue;
|
|
689
723
|
try {
|
|
690
|
-
const schema = joiNodeToJsonSchema(
|
|
724
|
+
const schema = joiNodeToJsonSchema(init2, joiId);
|
|
691
725
|
if (Object.keys(schema).length > 0) schemas[decl.getName()] = schema;
|
|
692
726
|
} catch {
|
|
693
727
|
}
|
|
@@ -777,7 +811,7 @@ var extractClassValidatorSchemas = (sourceFile) => {
|
|
|
777
811
|
const firstArg = call?.getArguments()[0];
|
|
778
812
|
if (firstArg) {
|
|
779
813
|
const raw = firstArg.getText();
|
|
780
|
-
if (
|
|
814
|
+
if (import_ts_morph5.Node.isArrayLiteralExpression(firstArg)) {
|
|
781
815
|
const values = firstArg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
782
816
|
CONSTRAINED[name](propSchema, values);
|
|
783
817
|
} else {
|
|
@@ -824,20 +858,20 @@ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
|
824
858
|
var findTsConfig = (startDir) => {
|
|
825
859
|
let dir = startDir;
|
|
826
860
|
for (let i = 0; i < 5; i++) {
|
|
827
|
-
const candidate =
|
|
828
|
-
if (
|
|
829
|
-
const parent = dirname(dir);
|
|
861
|
+
const candidate = (0, import_path2.join)(dir, "tsconfig.json");
|
|
862
|
+
if ((0, import_fs2.existsSync)(candidate)) return candidate;
|
|
863
|
+
const parent = (0, import_path2.dirname)(dir);
|
|
830
864
|
if (parent === dir) break;
|
|
831
865
|
dir = parent;
|
|
832
866
|
}
|
|
833
867
|
return void 0;
|
|
834
868
|
};
|
|
835
869
|
var extractExpressRoutes = async (config) => {
|
|
836
|
-
const cwd =
|
|
870
|
+
const cwd = (0, import_fs2.existsSync)(config.entry) && !config.entry.endsWith(".ts") && !config.entry.endsWith(".js") ? config.entry : (0, import_path2.dirname)(config.entry);
|
|
837
871
|
const tsConfigPath = findTsConfig(cwd);
|
|
838
|
-
const project = tsConfigPath ? new Project({ tsConfigFilePath: tsConfigPath, skipAddingFilesFromTsConfig: true }) : new Project({ compilerOptions: { allowJs: true, strict: false } });
|
|
839
|
-
const patterns = ["**/*.ts", "**/*.js"].map((p) =>
|
|
840
|
-
const files = await glob(`${cwd}/**/*.{ts,js}`, {
|
|
872
|
+
const project = tsConfigPath ? new import_ts_morph.Project({ tsConfigFilePath: tsConfigPath, skipAddingFilesFromTsConfig: true }) : new import_ts_morph.Project({ compilerOptions: { allowJs: true, strict: false } });
|
|
873
|
+
const patterns = ["**/*.ts", "**/*.js"].map((p) => (0, import_path2.resolve)(cwd, "**", p));
|
|
874
|
+
const files = await (0, import_glob.glob)(`${cwd}/**/*.{ts,js}`, {
|
|
841
875
|
ignore: ["**/node_modules/**", "**/dist/**", "**/*.d.ts", "**/*.spec.*", "**/*.test.*"]
|
|
842
876
|
});
|
|
843
877
|
for (const file of files) {
|
|
@@ -860,7 +894,7 @@ var extractExpressRoutes = async (config) => {
|
|
|
860
894
|
};
|
|
861
895
|
var extractRoutesFromFile = (sourceFile, ctx) => {
|
|
862
896
|
const routes = [];
|
|
863
|
-
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
897
|
+
const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
|
|
864
898
|
for (const call of callExpressions) {
|
|
865
899
|
const route = tryExtractRoute(call, ctx);
|
|
866
900
|
if (route) routes.push(route);
|
|
@@ -869,14 +903,14 @@ var extractRoutesFromFile = (sourceFile, ctx) => {
|
|
|
869
903
|
};
|
|
870
904
|
var tryExtractRoute = (call, ctx) => {
|
|
871
905
|
const expr = call.getExpression();
|
|
872
|
-
if (!
|
|
906
|
+
if (!import_ts_morph.Node.isPropertyAccessExpression(expr)) return null;
|
|
873
907
|
const methodName = expr.getName().toUpperCase();
|
|
874
908
|
if (!HTTP_METHODS.includes(methodName)) return null;
|
|
875
909
|
const args = call.getArguments();
|
|
876
910
|
if (args.length < 2) return null;
|
|
877
911
|
const firstArg = args[0];
|
|
878
912
|
if (!firstArg) return null;
|
|
879
|
-
if (!
|
|
913
|
+
if (!import_ts_morph.Node.isStringLiteral(firstArg) && !import_ts_morph.Node.isTemplateExpression(firstArg) && !import_ts_morph.Node.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
880
914
|
return null;
|
|
881
915
|
}
|
|
882
916
|
const rawPath = firstArg.getText().replace(/['"'`]/g, "");
|
|
@@ -1077,50 +1111,51 @@ var matchesGlob = (path, pattern) => {
|
|
|
1077
1111
|
var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
1078
1112
|
|
|
1079
1113
|
// src/generate.ts
|
|
1114
|
+
var import_meta = {};
|
|
1080
1115
|
var generate = async (opts = {}) => {
|
|
1081
1116
|
const cwd = process.cwd();
|
|
1082
|
-
const spinner = opts.quiet ? null :
|
|
1117
|
+
const spinner = opts.quiet ? null : (0, import_ora.default)();
|
|
1083
1118
|
spinner?.start("Loading flowdoc config...");
|
|
1084
|
-
const configPath = opts.config ?
|
|
1119
|
+
const configPath = opts.config ? (0, import_path3.resolve)(cwd, opts.config) : findConfigFile(cwd);
|
|
1085
1120
|
if (!configPath) {
|
|
1086
|
-
spinner?.fail(
|
|
1121
|
+
spinner?.fail(import_chalk.default.red("No flowdoc.config.ts found. Run `flowdoc init` first."));
|
|
1087
1122
|
process.exit(1);
|
|
1088
1123
|
}
|
|
1089
1124
|
let rawConfig;
|
|
1090
1125
|
try {
|
|
1091
1126
|
rawConfig = await loadConfig(configPath);
|
|
1092
1127
|
} catch (err) {
|
|
1093
|
-
spinner?.fail(
|
|
1128
|
+
spinner?.fail(import_chalk.default.red(`Failed to load config: ${String(err)}`));
|
|
1094
1129
|
process.exit(1);
|
|
1095
1130
|
}
|
|
1096
1131
|
const config = resolveConfig(rawConfig, cwd);
|
|
1097
|
-
spinner?.succeed(`Config loaded \u2014 ${
|
|
1098
|
-
spinner?.start(`Scanning ${
|
|
1132
|
+
spinner?.succeed(`Config loaded \u2014 ${import_chalk.default.cyan(config.name)}`);
|
|
1133
|
+
spinner?.start(`Scanning ${import_chalk.default.cyan(config.entry)} for routes...`);
|
|
1099
1134
|
let routes;
|
|
1100
1135
|
try {
|
|
1101
1136
|
routes = await extractExpressRoutes(config);
|
|
1102
1137
|
} catch (err) {
|
|
1103
|
-
spinner?.fail(
|
|
1138
|
+
spinner?.fail(import_chalk.default.red(`Parse failed: ${String(err)}`));
|
|
1104
1139
|
process.exit(1);
|
|
1105
1140
|
}
|
|
1106
1141
|
spinner?.succeed(
|
|
1107
|
-
`Found ${
|
|
1142
|
+
`Found ${import_chalk.default.green(String(routes.length))} routes across ${import_chalk.default.cyan(config.framework)} app`
|
|
1108
1143
|
);
|
|
1109
1144
|
const spec = buildSpec(routes, config);
|
|
1110
|
-
const outputDir = opts.output ?
|
|
1111
|
-
mkdirSync(outputDir, { recursive: true });
|
|
1112
|
-
const specPath =
|
|
1113
|
-
writeFileSync(specPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
1145
|
+
const outputDir = opts.output ? (0, import_path3.resolve)(cwd, opts.output) : config.output ?? (0, import_path3.resolve)(cwd, "docs-output");
|
|
1146
|
+
(0, import_fs3.mkdirSync)(outputDir, { recursive: true });
|
|
1147
|
+
const specPath = (0, import_path3.join)(outputDir, "flowdoc.json");
|
|
1148
|
+
(0, import_fs3.writeFileSync)(specPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
1114
1149
|
await writeUiHtml(outputDir, config);
|
|
1115
1150
|
if (!opts.quiet) {
|
|
1116
1151
|
console.log();
|
|
1117
|
-
console.log(
|
|
1152
|
+
console.log(import_chalk.default.bold(" flowdoc generated successfully"));
|
|
1118
1153
|
console.log();
|
|
1119
|
-
console.log(` ${
|
|
1120
|
-
console.log(` ${
|
|
1154
|
+
console.log(` ${import_chalk.default.gray("Spec:")} ${import_chalk.default.cyan(specPath)}`);
|
|
1155
|
+
console.log(` ${import_chalk.default.gray("UI:")} ${import_chalk.default.cyan((0, import_path3.join)(outputDir, "index.html"))}`);
|
|
1121
1156
|
console.log();
|
|
1122
|
-
console.log(` ${
|
|
1123
|
-
console.log(` ${
|
|
1157
|
+
console.log(` ${import_chalk.default.gray("Routes:")} ${import_chalk.default.green(String(routes.length))}`);
|
|
1158
|
+
console.log(` ${import_chalk.default.gray("Groups:")} ${import_chalk.default.green(String(spec.groups.length))}`);
|
|
1124
1159
|
console.log();
|
|
1125
1160
|
}
|
|
1126
1161
|
return spec;
|
|
@@ -1129,15 +1164,15 @@ var writeUiHtml = async (outputDir, config) => {
|
|
|
1129
1164
|
const brand = config.theme?.brand ?? "#6366f1";
|
|
1130
1165
|
const title = config.name;
|
|
1131
1166
|
const darkMode = config.theme?.darkMode !== false;
|
|
1132
|
-
const cliRoot =
|
|
1133
|
-
const uiAssetsSource =
|
|
1134
|
-
const uiAssetsDest =
|
|
1135
|
-
if (
|
|
1136
|
-
mkdirSync(uiAssetsDest, { recursive: true });
|
|
1137
|
-
cpSync(uiAssetsSource, uiAssetsDest, { recursive: true });
|
|
1167
|
+
const cliRoot = (0, import_path3.dirname)((0, import_path3.dirname)((0, import_url.fileURLToPath)(import_meta.url)));
|
|
1168
|
+
const uiAssetsSource = (0, import_path3.join)(cliRoot, "ui-assets");
|
|
1169
|
+
const uiAssetsDest = (0, import_path3.join)(outputDir, "assets");
|
|
1170
|
+
if ((0, import_fs3.existsSync)(uiAssetsSource)) {
|
|
1171
|
+
(0, import_fs3.mkdirSync)(uiAssetsDest, { recursive: true });
|
|
1172
|
+
(0, import_fs3.cpSync)(uiAssetsSource, uiAssetsDest, { recursive: true });
|
|
1138
1173
|
}
|
|
1139
1174
|
const html = generateHtmlShell({ title, brand, darkMode });
|
|
1140
|
-
writeFileSync(
|
|
1175
|
+
(0, import_fs3.writeFileSync)((0, import_path3.join)(outputDir, "index.html"), html, "utf-8");
|
|
1141
1176
|
};
|
|
1142
1177
|
var generateHtmlShell = ({ title, brand, darkMode }) => `<!DOCTYPE html>
|
|
1143
1178
|
<html lang="en" class="${darkMode ? "dark" : ""}">
|
|
@@ -1158,9 +1193,218 @@ var generateHtmlShell = ({ title, brand, darkMode }) => `<!DOCTYPE html>
|
|
|
1158
1193
|
</body>
|
|
1159
1194
|
</html>`;
|
|
1160
1195
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1196
|
+
// src/serve.ts
|
|
1197
|
+
var import_http = require("http");
|
|
1198
|
+
var import_fs4 = require("fs");
|
|
1199
|
+
var import_path4 = require("path");
|
|
1200
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
1201
|
+
var import_open = __toESM(require("open"), 1);
|
|
1202
|
+
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
1203
|
+
var MIME_TYPES = {
|
|
1204
|
+
".html": "text/html",
|
|
1205
|
+
".js": "application/javascript",
|
|
1206
|
+
".css": "text/css",
|
|
1207
|
+
".json": "application/json",
|
|
1208
|
+
".svg": "image/svg+xml",
|
|
1209
|
+
".png": "image/png",
|
|
1210
|
+
".ico": "image/x-icon"
|
|
1166
1211
|
};
|
|
1212
|
+
var serve = async (opts = {}) => {
|
|
1213
|
+
const cwd = process.cwd();
|
|
1214
|
+
const outputDir = opts.output ?? (0, import_path4.join)(cwd, "docs-output");
|
|
1215
|
+
const port = opts.port ?? 4e3;
|
|
1216
|
+
await generate({ ...opts, quiet: false });
|
|
1217
|
+
const server = (0, import_http.createServer)((req, res) => {
|
|
1218
|
+
const url = req.url === "/" || req.url === "" ? "/index.html" : req.url ?? "/index.html";
|
|
1219
|
+
const filePath = (0, import_path4.join)(outputDir, url);
|
|
1220
|
+
if (!(0, import_fs4.existsSync)(filePath)) {
|
|
1221
|
+
res.writeHead(404);
|
|
1222
|
+
res.end("Not found");
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const ext = (0, import_path4.extname)(filePath);
|
|
1226
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1227
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1228
|
+
(0, import_fs4.createReadStream)(filePath).pipe(res);
|
|
1229
|
+
});
|
|
1230
|
+
server.listen(port, () => {
|
|
1231
|
+
const url = `http://localhost:${port}`;
|
|
1232
|
+
console.log();
|
|
1233
|
+
console.log(` ${import_chalk2.default.bold("flowdoc")} is running at ${import_chalk2.default.cyan(url)}`);
|
|
1234
|
+
if (opts.watch) {
|
|
1235
|
+
console.log(` ${import_chalk2.default.dim("watching for changes\u2026")}`);
|
|
1236
|
+
}
|
|
1237
|
+
console.log();
|
|
1238
|
+
if (!opts.noOpen) {
|
|
1239
|
+
void (0, import_open.default)(url);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
if (!opts.watch) return;
|
|
1243
|
+
const configPath = opts.config ?? findConfigFile(cwd) ?? "";
|
|
1244
|
+
let watchGlob = "src/**/*.ts";
|
|
1245
|
+
if (configPath) {
|
|
1246
|
+
try {
|
|
1247
|
+
const raw = await loadConfig(configPath);
|
|
1248
|
+
const config = resolveConfig(raw, cwd);
|
|
1249
|
+
const entryDir = (0, import_path4.resolve)(cwd, config.entry).replace(/\/[^/]+$/, "");
|
|
1250
|
+
watchGlob = `${entryDir}/**/*.ts`;
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
let rebuilding = false;
|
|
1255
|
+
const watcher = import_chokidar.default.watch(watchGlob, {
|
|
1256
|
+
ignoreInitial: true,
|
|
1257
|
+
ignored: ["**/node_modules/**", "**/dist/**", "**/docs-output/**"]
|
|
1258
|
+
});
|
|
1259
|
+
const rebuild = async (filePath) => {
|
|
1260
|
+
if (rebuilding) return;
|
|
1261
|
+
rebuilding = true;
|
|
1262
|
+
console.log(` ${import_chalk2.default.dim("\u2192")} ${import_chalk2.default.yellow(filePath.replace(cwd + "/", ""))} changed \u2014 regenerating\u2026`);
|
|
1263
|
+
try {
|
|
1264
|
+
await generate({ ...opts, quiet: true });
|
|
1265
|
+
console.log(` ${import_chalk2.default.green("\u2713")} docs updated`);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
console.error(` ${import_chalk2.default.red("\u2717")} regeneration failed:`, err instanceof Error ? err.message : err);
|
|
1268
|
+
} finally {
|
|
1269
|
+
rebuilding = false;
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
watcher.on("add", rebuild).on("change", rebuild).on("unlink", rebuild);
|
|
1273
|
+
process.on("SIGINT", () => {
|
|
1274
|
+
void watcher.close();
|
|
1275
|
+
server.close();
|
|
1276
|
+
process.exit(0);
|
|
1277
|
+
});
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// src/init.ts
|
|
1281
|
+
var import_fs5 = require("fs");
|
|
1282
|
+
var import_path5 = require("path");
|
|
1283
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
1284
|
+
var CONFIG_TEMPLATE = `import { defineConfig } from "flowdoc-gen";
|
|
1285
|
+
|
|
1286
|
+
export default defineConfig({
|
|
1287
|
+
name: "My API",
|
|
1288
|
+
version: "1.0.0",
|
|
1289
|
+
description: "API documentation generated by flowdoc",
|
|
1290
|
+
framework: "express",
|
|
1291
|
+
entry: "./src",
|
|
1292
|
+
output: "./docs-output",
|
|
1293
|
+
theme: {
|
|
1294
|
+
brand: "#6366f1",
|
|
1295
|
+
darkMode: true,
|
|
1296
|
+
},
|
|
1297
|
+
// groups: [
|
|
1298
|
+
// { name: "Auth", match: "/auth/**" },
|
|
1299
|
+
// { name: "Users", match: "/users/**" },
|
|
1300
|
+
// ],
|
|
1301
|
+
});
|
|
1302
|
+
`;
|
|
1303
|
+
var init = (cwd = process.cwd()) => {
|
|
1304
|
+
const configPath = (0, import_path5.resolve)(cwd, "flowdoc.config.ts");
|
|
1305
|
+
if ((0, import_fs5.existsSync)(configPath)) {
|
|
1306
|
+
console.log(import_chalk3.default.yellow(" flowdoc.config.ts already exists \u2014 skipping."));
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
(0, import_fs5.writeFileSync)(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
1310
|
+
console.log();
|
|
1311
|
+
console.log(` ${import_chalk3.default.bold("flowdoc")} initialized`);
|
|
1312
|
+
console.log();
|
|
1313
|
+
console.log(` Created ${import_chalk3.default.cyan("flowdoc.config.ts")}`);
|
|
1314
|
+
console.log();
|
|
1315
|
+
console.log(" Next steps:");
|
|
1316
|
+
console.log(` 1. Edit ${import_chalk3.default.cyan("flowdoc.config.ts")} \u2014 set your entry path`);
|
|
1317
|
+
console.log(` 2. Run ${import_chalk3.default.cyan("flowdoc generate")} to build your docs`);
|
|
1318
|
+
console.log(` 3. Run ${import_chalk3.default.cyan("flowdoc serve")} to preview locally`);
|
|
1319
|
+
console.log();
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// src/middleware.ts
|
|
1323
|
+
var import_fs6 = require("fs");
|
|
1324
|
+
var import_path6 = require("path");
|
|
1325
|
+
var import_url2 = require("url");
|
|
1326
|
+
var import_meta2 = {};
|
|
1327
|
+
var MIME = {
|
|
1328
|
+
".html": "text/html",
|
|
1329
|
+
".js": "application/javascript",
|
|
1330
|
+
".css": "text/css",
|
|
1331
|
+
".json": "application/json"
|
|
1332
|
+
};
|
|
1333
|
+
var flowdoc = (opts = {}) => {
|
|
1334
|
+
const cwd = process.cwd();
|
|
1335
|
+
let outputDir = null;
|
|
1336
|
+
let ready = false;
|
|
1337
|
+
let initError = null;
|
|
1338
|
+
const init2 = (async () => {
|
|
1339
|
+
try {
|
|
1340
|
+
const configPath = opts.config ?? findConfigFile(cwd);
|
|
1341
|
+
if (!configPath) throw new Error("No flowdoc.config.ts found. Run `flowdoc init` first.");
|
|
1342
|
+
const rawConfig = await loadConfig(configPath);
|
|
1343
|
+
const config = resolveConfig(rawConfig, cwd);
|
|
1344
|
+
outputDir = config.output ?? (0, import_path6.join)(cwd, "docs-output");
|
|
1345
|
+
const routes = await extractExpressRoutes(config);
|
|
1346
|
+
const spec = buildSpec(routes, config);
|
|
1347
|
+
(0, import_fs6.mkdirSync)(outputDir, { recursive: true });
|
|
1348
|
+
(0, import_fs6.writeFileSync)((0, import_path6.join)(outputDir, "flowdoc.json"), JSON.stringify(spec, null, 2));
|
|
1349
|
+
const cliRoot = (0, import_path6.dirname)((0, import_path6.dirname)((0, import_url2.fileURLToPath)(import_meta2.url)));
|
|
1350
|
+
const uiAssetsSource = (0, import_path6.join)(cliRoot, "ui-assets");
|
|
1351
|
+
if ((0, import_fs6.existsSync)(uiAssetsSource)) {
|
|
1352
|
+
const dest = (0, import_path6.join)(outputDir, "assets");
|
|
1353
|
+
(0, import_fs6.mkdirSync)(dest, { recursive: true });
|
|
1354
|
+
(0, import_fs6.cpSync)(uiAssetsSource, dest, { recursive: true });
|
|
1355
|
+
}
|
|
1356
|
+
ready = true;
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
initError = err instanceof Error ? err.message : String(err);
|
|
1359
|
+
}
|
|
1360
|
+
})();
|
|
1361
|
+
return async (req, res, next) => {
|
|
1362
|
+
await init2;
|
|
1363
|
+
if (initError || !outputDir) {
|
|
1364
|
+
res.status(500).send(`flowdoc init failed: ${initError}`);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const urlPath = req.path === "/" || req.path === "" ? "/index.html" : req.path;
|
|
1368
|
+
if (urlPath === "/index.html") {
|
|
1369
|
+
const brand = "#6366f1";
|
|
1370
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
1371
|
+
res.setHeader("Content-Type", "text/html");
|
|
1372
|
+
res.send(buildHtml({ baseUrl, brand }));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const filePath = (0, import_path6.join)(outputDir, urlPath);
|
|
1376
|
+
if (!(0, import_fs6.existsSync)(filePath)) {
|
|
1377
|
+
next();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const mime = MIME[(0, import_path6.extname)(filePath)] ?? "application/octet-stream";
|
|
1381
|
+
res.setHeader("Content-Type", mime);
|
|
1382
|
+
(0, import_fs6.createReadStream)(filePath).pipe(res);
|
|
1383
|
+
};
|
|
1384
|
+
};
|
|
1385
|
+
var buildHtml = ({ baseUrl, brand }) => `<!DOCTYPE html>
|
|
1386
|
+
<html lang="en" class="dark">
|
|
1387
|
+
<head>
|
|
1388
|
+
<meta charset="UTF-8" />
|
|
1389
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1390
|
+
<title>API Docs</title>
|
|
1391
|
+
<script>
|
|
1392
|
+
window.__FLOWDOC_BRAND__ = "${brand}";
|
|
1393
|
+
window.__FLOWDOC_BASE_URL__ = "${baseUrl}";
|
|
1394
|
+
</script>
|
|
1395
|
+
<script type="module" crossorigin src="./assets/ui.js"></script>
|
|
1396
|
+
<link rel="stylesheet" href="./assets/index.css" />
|
|
1397
|
+
</head>
|
|
1398
|
+
<body><div id="root"></div></body>
|
|
1399
|
+
</html>`;
|
|
1400
|
+
|
|
1401
|
+
// src/index.ts
|
|
1402
|
+
var defineConfig = (config) => config;
|
|
1403
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1404
|
+
0 && (module.exports = {
|
|
1405
|
+
defineConfig,
|
|
1406
|
+
flowdoc,
|
|
1407
|
+
generate,
|
|
1408
|
+
init,
|
|
1409
|
+
serve
|
|
1410
|
+
});
|