flowdoc-gen 0.1.0 → 0.1.2
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 +3 -4
- package/dist/{chunk-3GGK6LWE.js → chunk-ZJCRRUX7.js} +0 -2
- package/dist/{generate-E4V2RQYB.js → generate-4NMSVNJF.js} +1 -1
- package/dist/index.d.ts +114 -2
- package/dist/index.js +12 -15
- package/dist/{chunk-VG2YJHSH.js → init-6XHCTCLE.js} +8 -16
- package/dist/{serve-NNDUXHXZ.js → serve-AD6IWZXI.js} +1 -2
- package/package.json +1 -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-XXW6UJOX.js +0 -604
- 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-VKTQ5E5O.js +0 -7
- package/dist/serve-Y4E3DTAJ.js +0 -94
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/bin.ts
|
|
5
4
|
import { program } from "commander";
|
|
@@ -10,15 +9,15 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
10
9
|
var pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
11
10
|
program.name("flowdoc").description("Auto-generate beautiful API documentation from your Express codebase").version(pkg.version);
|
|
12
11
|
program.command("init").description("Scaffold a flowdoc.config.ts in the current directory").action(async () => {
|
|
13
|
-
const { init } = await import("./init-
|
|
12
|
+
const { init } = await import("./init-6XHCTCLE.js");
|
|
14
13
|
init();
|
|
15
14
|
});
|
|
16
15
|
program.command("generate").description("Parse your routes and write docs to the output folder").option("-c, --config <path>", "Path to flowdoc config file").option("-o, --output <path>", "Override output directory").option("-q, --quiet", "Suppress output").action(async (opts) => {
|
|
17
|
-
const { generate } = await import("./generate-
|
|
16
|
+
const { generate } = await import("./generate-4NMSVNJF.js");
|
|
18
17
|
await generate(opts);
|
|
19
18
|
});
|
|
20
19
|
program.command("serve").description("Generate docs and serve them locally").option("-c, --config <path>", "Path to flowdoc config file").option("-o, --output <path>", "Override output directory").option("-p, --port <number>", "Port to serve on (default: 4000)", "4000").option("-w, --watch", "Re-generate docs on source file changes").option("--no-open", "Don't open browser automatically").action(async (opts) => {
|
|
21
|
-
const { serve } = await import("./serve-
|
|
20
|
+
const { serve } = await import("./serve-AD6IWZXI.js");
|
|
22
21
|
const serveOpts = {
|
|
23
22
|
port: opts.port ? parseInt(opts.port, 10) : 4e3,
|
|
24
23
|
noOpen: !opts.open,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,115 @@
|
|
|
1
|
-
import { FlowDocSpec } from '@flowdoc/core';
|
|
2
1
|
import { Request, Response, NextFunction } from 'express';
|
|
3
2
|
|
|
3
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
4
|
+
type ParameterLocation = "path" | "query" | "header" | "cookie";
|
|
5
|
+
type SchemaType = "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
|
|
6
|
+
interface JsonSchema {
|
|
7
|
+
type?: SchemaType | SchemaType[];
|
|
8
|
+
format?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
example?: unknown;
|
|
11
|
+
enum?: unknown[];
|
|
12
|
+
properties?: Record<string, JsonSchema>;
|
|
13
|
+
items?: JsonSchema;
|
|
14
|
+
required?: string[];
|
|
15
|
+
additionalProperties?: boolean | JsonSchema;
|
|
16
|
+
anyOf?: JsonSchema[];
|
|
17
|
+
oneOf?: JsonSchema[];
|
|
18
|
+
allOf?: JsonSchema[];
|
|
19
|
+
nullable?: boolean;
|
|
20
|
+
minimum?: number;
|
|
21
|
+
maximum?: number;
|
|
22
|
+
minLength?: number;
|
|
23
|
+
maxLength?: number;
|
|
24
|
+
minItems?: number;
|
|
25
|
+
maxItems?: number;
|
|
26
|
+
pattern?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
default?: unknown;
|
|
29
|
+
}
|
|
30
|
+
interface RouteParameter {
|
|
31
|
+
name: string;
|
|
32
|
+
in: ParameterLocation;
|
|
33
|
+
required: boolean;
|
|
34
|
+
schema: JsonSchema;
|
|
35
|
+
description?: string;
|
|
36
|
+
example?: unknown;
|
|
37
|
+
}
|
|
38
|
+
interface RequestBody {
|
|
39
|
+
required: boolean;
|
|
40
|
+
description?: string;
|
|
41
|
+
content: {
|
|
42
|
+
"application/json"?: {
|
|
43
|
+
schema: JsonSchema;
|
|
44
|
+
};
|
|
45
|
+
"multipart/form-data"?: {
|
|
46
|
+
schema: JsonSchema;
|
|
47
|
+
};
|
|
48
|
+
"application/x-www-form-urlencoded"?: {
|
|
49
|
+
schema: JsonSchema;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
interface ResponseBody {
|
|
54
|
+
description: string;
|
|
55
|
+
content?: {
|
|
56
|
+
"application/json"?: {
|
|
57
|
+
schema: JsonSchema;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
interface RouteDoc {
|
|
62
|
+
method: HttpMethod;
|
|
63
|
+
path: string;
|
|
64
|
+
summary?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
tags: string[];
|
|
67
|
+
parameters: RouteParameter[];
|
|
68
|
+
requestBody?: RequestBody;
|
|
69
|
+
responses: Record<string, ResponseBody>;
|
|
70
|
+
deprecated?: boolean;
|
|
71
|
+
security?: Array<Record<string, string[]>>;
|
|
72
|
+
middleware?: string[];
|
|
73
|
+
}
|
|
74
|
+
interface ApiGroup {
|
|
75
|
+
name: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
routes: RouteDoc[];
|
|
78
|
+
}
|
|
79
|
+
interface FlowDocSpec {
|
|
80
|
+
info: {
|
|
81
|
+
title: string;
|
|
82
|
+
version: string;
|
|
83
|
+
description?: string;
|
|
84
|
+
baseUrl: string;
|
|
85
|
+
};
|
|
86
|
+
auth?: {
|
|
87
|
+
type: "bearer" | "apiKey" | "basic" | "oauth2";
|
|
88
|
+
headerName?: string;
|
|
89
|
+
queryName?: string;
|
|
90
|
+
};
|
|
91
|
+
groups: ApiGroup[];
|
|
92
|
+
generatedAt: string;
|
|
93
|
+
sourceFramework: "express" | "nestjs";
|
|
94
|
+
}
|
|
95
|
+
interface FlowDocConfig {
|
|
96
|
+
name: string;
|
|
97
|
+
version?: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
framework: "express" | "nestjs";
|
|
100
|
+
entry: string;
|
|
101
|
+
baseUrl?: string;
|
|
102
|
+
auth?: FlowDocSpec["auth"];
|
|
103
|
+
output?: string;
|
|
104
|
+
theme?: {
|
|
105
|
+
brand?: string;
|
|
106
|
+
logo?: string;
|
|
107
|
+
darkMode?: boolean;
|
|
108
|
+
};
|
|
109
|
+
groups?: Record<string, string[]>;
|
|
110
|
+
exclude?: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
4
113
|
interface GenerateOptions {
|
|
5
114
|
config?: string;
|
|
6
115
|
output?: string;
|
|
@@ -33,4 +142,7 @@ interface FlowDocMiddlewareOptions {
|
|
|
33
142
|
*/
|
|
34
143
|
declare const flowdoc: (opts?: FlowDocMiddlewareOptions) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
35
144
|
|
|
36
|
-
|
|
145
|
+
/** Type-safe config helper — use in flowdoc.config.ts */
|
|
146
|
+
declare const defineConfig: (config: FlowDocConfig) => FlowDocConfig;
|
|
147
|
+
|
|
148
|
+
export { type FlowDocConfig, type FlowDocMiddlewareOptions, type FlowDocSpec, type GenerateOptions, type JsonSchema, type RouteDoc, type ServeOptions, defineConfig, flowdoc, generate, init, serve };
|
package/dist/index.js
CHANGED
|
@@ -1244,31 +1244,24 @@ var serve = async (opts = {}) => {
|
|
|
1244
1244
|
import { writeFileSync as writeFileSync2, existsSync as existsSync5 } from "fs";
|
|
1245
1245
|
import { resolve as resolve5 } from "path";
|
|
1246
1246
|
import chalk3 from "chalk";
|
|
1247
|
-
var CONFIG_TEMPLATE = `import
|
|
1247
|
+
var CONFIG_TEMPLATE = `import { defineConfig } from "flowdoc-gen";
|
|
1248
1248
|
|
|
1249
|
-
|
|
1249
|
+
export default defineConfig({
|
|
1250
1250
|
name: "My API",
|
|
1251
1251
|
version: "1.0.0",
|
|
1252
1252
|
description: "API documentation generated by flowdoc",
|
|
1253
1253
|
framework: "express",
|
|
1254
|
-
entry: "./src",
|
|
1255
|
-
baseUrl: "http://localhost:3000",
|
|
1256
|
-
auth: {
|
|
1257
|
-
type: "bearer",
|
|
1258
|
-
},
|
|
1254
|
+
entry: "./src",
|
|
1259
1255
|
output: "./docs-output",
|
|
1260
1256
|
theme: {
|
|
1261
1257
|
brand: "#6366f1",
|
|
1262
1258
|
darkMode: true,
|
|
1263
1259
|
},
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
// "
|
|
1267
|
-
//
|
|
1268
|
-
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
export default config;
|
|
1260
|
+
// groups: [
|
|
1261
|
+
// { name: "Auth", match: "/auth/**" },
|
|
1262
|
+
// { name: "Users", match: "/users/**" },
|
|
1263
|
+
// ],
|
|
1264
|
+
});
|
|
1272
1265
|
`;
|
|
1273
1266
|
var init = (cwd = process.cwd()) => {
|
|
1274
1267
|
const configPath = resolve5(cwd, "flowdoc.config.ts");
|
|
@@ -1366,7 +1359,11 @@ var buildHtml = ({ baseUrl, brand }) => `<!DOCTYPE html>
|
|
|
1366
1359
|
</head>
|
|
1367
1360
|
<body><div id="root"></div></body>
|
|
1368
1361
|
</html>`;
|
|
1362
|
+
|
|
1363
|
+
// src/index.ts
|
|
1364
|
+
var defineConfig = (config) => config;
|
|
1369
1365
|
export {
|
|
1366
|
+
defineConfig,
|
|
1370
1367
|
flowdoc,
|
|
1371
1368
|
generate,
|
|
1372
1369
|
init,
|
|
@@ -2,31 +2,24 @@
|
|
|
2
2
|
import { writeFileSync, existsSync } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
var CONFIG_TEMPLATE = `import
|
|
5
|
+
var CONFIG_TEMPLATE = `import { defineConfig } from "flowdoc-gen";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
export default defineConfig({
|
|
8
8
|
name: "My API",
|
|
9
9
|
version: "1.0.0",
|
|
10
10
|
description: "API documentation generated by flowdoc",
|
|
11
11
|
framework: "express",
|
|
12
|
-
entry: "./src",
|
|
13
|
-
baseUrl: "http://localhost:3000",
|
|
14
|
-
auth: {
|
|
15
|
-
type: "bearer",
|
|
16
|
-
},
|
|
12
|
+
entry: "./src",
|
|
17
13
|
output: "./docs-output",
|
|
18
14
|
theme: {
|
|
19
15
|
brand: "#6366f1",
|
|
20
16
|
darkMode: true,
|
|
21
17
|
},
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// "
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export default config;
|
|
18
|
+
// groups: [
|
|
19
|
+
// { name: "Auth", match: "/auth/**" },
|
|
20
|
+
// { name: "Users", match: "/users/**" },
|
|
21
|
+
// ],
|
|
22
|
+
});
|
|
30
23
|
`;
|
|
31
24
|
var init = (cwd = process.cwd()) => {
|
|
32
25
|
const configPath = resolve(cwd, "flowdoc.config.ts");
|
|
@@ -46,7 +39,6 @@ var init = (cwd = process.cwd()) => {
|
|
|
46
39
|
console.log(` 3. Run ${chalk.cyan("flowdoc serve")} to preview locally`);
|
|
47
40
|
console.log();
|
|
48
41
|
};
|
|
49
|
-
|
|
50
42
|
export {
|
|
51
43
|
init
|
|
52
44
|
};
|
package/package.json
CHANGED
package/dist/bin.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
package/dist/chunk-P6Z6T3W4.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
generate
|
|
3
|
-
} from "./chunk-SAMPAR3A.js";
|
|
4
|
-
|
|
5
|
-
// src/serve.ts
|
|
6
|
-
import { createServer } from "http";
|
|
7
|
-
import { createReadStream, existsSync } from "fs";
|
|
8
|
-
import { join, extname } from "path";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import open from "open";
|
|
11
|
-
var MIME_TYPES = {
|
|
12
|
-
".html": "text/html",
|
|
13
|
-
".js": "application/javascript",
|
|
14
|
-
".css": "text/css",
|
|
15
|
-
".json": "application/json",
|
|
16
|
-
".svg": "image/svg+xml",
|
|
17
|
-
".png": "image/png",
|
|
18
|
-
".ico": "image/x-icon"
|
|
19
|
-
};
|
|
20
|
-
var serve = async (opts = {}) => {
|
|
21
|
-
const spec = await generate({ ...opts, quiet: false });
|
|
22
|
-
const cwd = process.cwd();
|
|
23
|
-
const outputDir = opts.output ?? join(cwd, "docs-output");
|
|
24
|
-
const port = opts.port ?? 4e3;
|
|
25
|
-
const server = createServer((req, res) => {
|
|
26
|
-
const url = req.url === "/" || req.url === "" ? "/index.html" : req.url ?? "/index.html";
|
|
27
|
-
const filePath = join(outputDir, url);
|
|
28
|
-
if (!existsSync(filePath)) {
|
|
29
|
-
res.writeHead(404);
|
|
30
|
-
res.end("Not found");
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const ext = extname(filePath);
|
|
34
|
-
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
35
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
36
|
-
createReadStream(filePath).pipe(res);
|
|
37
|
-
});
|
|
38
|
-
server.listen(port, () => {
|
|
39
|
-
const url = `http://localhost:${port}`;
|
|
40
|
-
console.log();
|
|
41
|
-
console.log(` ${chalk.bold("flowdoc")} is running at ${chalk.cyan(url)}`);
|
|
42
|
-
console.log();
|
|
43
|
-
if (!opts.noOpen) {
|
|
44
|
-
void open(url);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export {
|
|
50
|
-
serve
|
|
51
|
-
};
|
package/dist/chunk-SAMPAR3A.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// src/generate.ts
|
|
2
|
-
import { writeFileSync, mkdirSync, cpSync, existsSync } from "fs";
|
|
3
|
-
import { resolve, join, dirname } from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
import chalk from "chalk";
|
|
6
|
-
import ora from "ora";
|
|
7
|
-
import { findConfigFile, loadConfig, resolveConfig } from "@flowdoc/core";
|
|
8
|
-
import { extractExpressRoutes } from "@flowdoc/parser";
|
|
9
|
-
import { buildSpec } from "@flowdoc/parser";
|
|
10
|
-
var generate = async (opts = {}) => {
|
|
11
|
-
const cwd = process.cwd();
|
|
12
|
-
const spinner = opts.quiet ? null : ora();
|
|
13
|
-
spinner?.start("Loading flowdoc config...");
|
|
14
|
-
const configPath = opts.config ? resolve(cwd, opts.config) : findConfigFile(cwd);
|
|
15
|
-
if (!configPath) {
|
|
16
|
-
spinner?.fail(chalk.red("No flowdoc.config.ts found. Run `flowdoc init` first."));
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
let rawConfig;
|
|
20
|
-
try {
|
|
21
|
-
rawConfig = await loadConfig(configPath);
|
|
22
|
-
} catch (err) {
|
|
23
|
-
spinner?.fail(chalk.red(`Failed to load config: ${String(err)}`));
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
const config = resolveConfig(rawConfig, cwd);
|
|
27
|
-
spinner?.succeed(`Config loaded \u2014 ${chalk.cyan(config.name)}`);
|
|
28
|
-
spinner?.start(`Scanning ${chalk.cyan(config.entry)} for routes...`);
|
|
29
|
-
let routes;
|
|
30
|
-
try {
|
|
31
|
-
routes = await extractExpressRoutes(config);
|
|
32
|
-
} catch (err) {
|
|
33
|
-
spinner?.fail(chalk.red(`Parse failed: ${String(err)}`));
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
spinner?.succeed(
|
|
37
|
-
`Found ${chalk.green(String(routes.length))} routes across ${chalk.cyan(config.framework)} app`
|
|
38
|
-
);
|
|
39
|
-
const spec = buildSpec(routes, config);
|
|
40
|
-
const outputDir = opts.output ? resolve(cwd, opts.output) : config.output ?? resolve(cwd, "docs-output");
|
|
41
|
-
mkdirSync(outputDir, { recursive: true });
|
|
42
|
-
const specPath = join(outputDir, "flowdoc.json");
|
|
43
|
-
writeFileSync(specPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
44
|
-
await writeUiHtml(outputDir, config);
|
|
45
|
-
if (!opts.quiet) {
|
|
46
|
-
console.log();
|
|
47
|
-
console.log(chalk.bold(" flowdoc generated successfully"));
|
|
48
|
-
console.log();
|
|
49
|
-
console.log(` ${chalk.gray("Spec:")} ${chalk.cyan(specPath)}`);
|
|
50
|
-
console.log(` ${chalk.gray("UI:")} ${chalk.cyan(join(outputDir, "index.html"))}`);
|
|
51
|
-
console.log();
|
|
52
|
-
console.log(` ${chalk.gray("Routes:")} ${chalk.green(String(routes.length))}`);
|
|
53
|
-
console.log(` ${chalk.gray("Groups:")} ${chalk.green(String(spec.groups.length))}`);
|
|
54
|
-
console.log();
|
|
55
|
-
}
|
|
56
|
-
return spec;
|
|
57
|
-
};
|
|
58
|
-
var writeUiHtml = async (outputDir, config) => {
|
|
59
|
-
const brand = config.theme?.brand ?? "#6366f1";
|
|
60
|
-
const title = config.name;
|
|
61
|
-
const darkMode = config.theme?.darkMode !== false;
|
|
62
|
-
const cliRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
63
|
-
const uiAssetsSource = join(cliRoot, "ui-assets");
|
|
64
|
-
const uiAssetsDest = join(outputDir, "assets");
|
|
65
|
-
if (existsSync(uiAssetsSource)) {
|
|
66
|
-
mkdirSync(uiAssetsDest, { recursive: true });
|
|
67
|
-
cpSync(uiAssetsSource, uiAssetsDest, { recursive: true });
|
|
68
|
-
}
|
|
69
|
-
const html = generateHtmlShell({ title, brand, darkMode });
|
|
70
|
-
writeFileSync(join(outputDir, "index.html"), html, "utf-8");
|
|
71
|
-
};
|
|
72
|
-
var generateHtmlShell = ({ title, brand, darkMode }) => `<!DOCTYPE html>
|
|
73
|
-
<html lang="en" class="${darkMode ? "dark" : ""}">
|
|
74
|
-
<head>
|
|
75
|
-
<meta charset="UTF-8" />
|
|
76
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
77
|
-
<title>${title} \u2014 API Docs</title>
|
|
78
|
-
<meta name="description" content="API documentation generated by flowdoc" />
|
|
79
|
-
<script>
|
|
80
|
-
window.__FLOWDOC_BRAND__ = "${brand}";
|
|
81
|
-
window.__FLOWDOC_DARK__ = ${String(darkMode)};
|
|
82
|
-
</script>
|
|
83
|
-
<script type="module" crossorigin src="./assets/ui.js"></script>
|
|
84
|
-
<link rel="stylesheet" href="./assets/index.css" />
|
|
85
|
-
</head>
|
|
86
|
-
<body>
|
|
87
|
-
<div id="root"></div>
|
|
88
|
-
</body>
|
|
89
|
-
</html>`;
|
|
90
|
-
|
|
91
|
-
export {
|
|
92
|
-
generate
|
|
93
|
-
};
|
package/dist/chunk-XXW6UJOX.js
DELETED
|
@@ -1,604 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/generate.ts
|
|
4
|
-
import { writeFileSync, mkdirSync, cpSync, existsSync as existsSync3 } from "fs";
|
|
5
|
-
import { resolve as resolve3, join as join3, dirname as dirname2 } from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
import chalk from "chalk";
|
|
8
|
-
import ora from "ora";
|
|
9
|
-
|
|
10
|
-
// ../core/dist/index.js
|
|
11
|
-
import { existsSync } from "fs";
|
|
12
|
-
import { resolve, join } from "path";
|
|
13
|
-
var CONFIG_FILES = [
|
|
14
|
-
"flowdoc.config.ts",
|
|
15
|
-
"flowdoc.config.js",
|
|
16
|
-
"flowdoc.config.mjs"
|
|
17
|
-
];
|
|
18
|
-
var findConfigFile = (cwd) => {
|
|
19
|
-
for (const file of CONFIG_FILES) {
|
|
20
|
-
const full = join(cwd, file);
|
|
21
|
-
if (existsSync(full)) return full;
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
};
|
|
25
|
-
var loadConfig = async (configPath) => {
|
|
26
|
-
const resolved = resolve(configPath);
|
|
27
|
-
const mod = await import(resolved);
|
|
28
|
-
const config = "default" in mod ? mod.default : mod;
|
|
29
|
-
if (!config) throw new Error(`No config exported from ${configPath}`);
|
|
30
|
-
return config;
|
|
31
|
-
};
|
|
32
|
-
var resolveConfig = (config, cwd) => ({
|
|
33
|
-
version: "1.0.0",
|
|
34
|
-
baseUrl: "http://localhost:3000",
|
|
35
|
-
...config,
|
|
36
|
-
entry: resolve(cwd, config.entry),
|
|
37
|
-
output: resolve(cwd, config.output ?? "./docs-output")
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// ../parser/dist/index.js
|
|
41
|
-
import {
|
|
42
|
-
Project,
|
|
43
|
-
Node as Node2,
|
|
44
|
-
SyntaxKind as SyntaxKind2
|
|
45
|
-
} from "ts-morph";
|
|
46
|
-
import { glob } from "glob";
|
|
47
|
-
import { resolve as resolve2, dirname, join as join2 } from "path";
|
|
48
|
-
import { existsSync as existsSync2 } from "fs";
|
|
49
|
-
import { Node } from "ts-morph";
|
|
50
|
-
var zodNodeToJsonSchema = (node) => {
|
|
51
|
-
if (!Node.isCallExpression(node)) return {};
|
|
52
|
-
const expr = node.getExpression();
|
|
53
|
-
const callText = expr.getText();
|
|
54
|
-
if (callText === "z.string") return buildStringSchema(node);
|
|
55
|
-
if (callText === "z.number") return buildNumberSchema(node);
|
|
56
|
-
if (callText === "z.boolean") return { type: "boolean" };
|
|
57
|
-
if (callText === "z.null") return { type: "null" };
|
|
58
|
-
if (callText === "z.literal") return buildLiteralSchema(node);
|
|
59
|
-
if (callText === "z.enum") return buildEnumSchema(node);
|
|
60
|
-
if (callText === "z.nativeEnum") return { type: "string" };
|
|
61
|
-
if (callText === "z.object") return buildObjectSchema(node);
|
|
62
|
-
if (callText === "z.array") return buildArraySchema(node);
|
|
63
|
-
if (callText === "z.union") return buildUnionSchema(node);
|
|
64
|
-
if (callText === "z.optional") return buildOptionalSchema(node);
|
|
65
|
-
if (callText === "z.date") return { type: "string", format: "date-time" };
|
|
66
|
-
if (callText === "z.any") return {};
|
|
67
|
-
if (callText === "z.unknown") return {};
|
|
68
|
-
if (Node.isPropertyAccessExpression(expr)) {
|
|
69
|
-
return buildChainedSchema(node);
|
|
70
|
-
}
|
|
71
|
-
return {};
|
|
72
|
-
};
|
|
73
|
-
var buildStringSchema = (node) => {
|
|
74
|
-
const schema = { type: "string" };
|
|
75
|
-
applyChainedValidations(node, schema);
|
|
76
|
-
return schema;
|
|
77
|
-
};
|
|
78
|
-
var buildNumberSchema = (node) => {
|
|
79
|
-
const schema = { type: "number" };
|
|
80
|
-
applyChainedValidations(node, schema);
|
|
81
|
-
return schema;
|
|
82
|
-
};
|
|
83
|
-
var buildLiteralSchema = (node) => {
|
|
84
|
-
const arg = node.getArguments()[0];
|
|
85
|
-
if (!arg) return {};
|
|
86
|
-
const text = arg.getText().replace(/['"]/g, "");
|
|
87
|
-
return { type: "string", enum: [text] };
|
|
88
|
-
};
|
|
89
|
-
var buildEnumSchema = (node) => {
|
|
90
|
-
const arg = node.getArguments()[0];
|
|
91
|
-
if (!arg || !Node.isArrayLiteralExpression(arg)) return { type: "string" };
|
|
92
|
-
const values = arg.getElements().map((el) => el.getText().replace(/['"]/g, ""));
|
|
93
|
-
return { type: "string", enum: values };
|
|
94
|
-
};
|
|
95
|
-
var buildObjectSchema = (node) => {
|
|
96
|
-
const arg = node.getArguments()[0];
|
|
97
|
-
if (!arg || !Node.isObjectLiteralExpression(arg)) return { type: "object" };
|
|
98
|
-
const properties = {};
|
|
99
|
-
const required = [];
|
|
100
|
-
for (const prop of arg.getProperties()) {
|
|
101
|
-
if (!Node.isPropertyAssignment(prop)) continue;
|
|
102
|
-
const name = prop.getName();
|
|
103
|
-
const init = prop.getInitializer();
|
|
104
|
-
if (!init) continue;
|
|
105
|
-
const childSchema = zodNodeToJsonSchema(init);
|
|
106
|
-
properties[name] = childSchema;
|
|
107
|
-
if (!isOptionalZodExpr(init)) {
|
|
108
|
-
required.push(name);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
type: "object",
|
|
113
|
-
properties,
|
|
114
|
-
...required.length > 0 ? { required } : {}
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
var buildArraySchema = (node) => {
|
|
118
|
-
const arg = node.getArguments()[0];
|
|
119
|
-
if (!arg) return { type: "array" };
|
|
120
|
-
return { type: "array", items: zodNodeToJsonSchema(arg) };
|
|
121
|
-
};
|
|
122
|
-
var buildUnionSchema = (node) => {
|
|
123
|
-
const arg = node.getArguments()[0];
|
|
124
|
-
if (!arg || !Node.isArrayLiteralExpression(arg)) return {};
|
|
125
|
-
const schemas = arg.getElements().map((el) => zodNodeToJsonSchema(el));
|
|
126
|
-
return { anyOf: schemas };
|
|
127
|
-
};
|
|
128
|
-
var buildOptionalSchema = (node) => {
|
|
129
|
-
const arg = node.getArguments()[0];
|
|
130
|
-
if (!arg) return {};
|
|
131
|
-
return zodNodeToJsonSchema(arg);
|
|
132
|
-
};
|
|
133
|
-
var buildChainedSchema = (node) => {
|
|
134
|
-
const chain = unwrapChain(node);
|
|
135
|
-
if (!chain.root) return {};
|
|
136
|
-
const base = zodNodeToJsonSchema(chain.root);
|
|
137
|
-
applyChainedCallsToSchema(chain.methods, base);
|
|
138
|
-
return base;
|
|
139
|
-
};
|
|
140
|
-
var unwrapChain = (node) => {
|
|
141
|
-
const methods = [];
|
|
142
|
-
let current = node;
|
|
143
|
-
while (Node.isCallExpression(current)) {
|
|
144
|
-
const expr = current.getExpression();
|
|
145
|
-
if (Node.isPropertyAccessExpression(expr)) {
|
|
146
|
-
const obj = expr.getExpression();
|
|
147
|
-
if (Node.isIdentifier(obj)) {
|
|
148
|
-
return { root: current, methods };
|
|
149
|
-
}
|
|
150
|
-
const methodName = expr.getName();
|
|
151
|
-
const args = current.getArguments();
|
|
152
|
-
methods.unshift({ name: methodName, args });
|
|
153
|
-
current = obj;
|
|
154
|
-
} else {
|
|
155
|
-
return { root: current, methods };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return { root: null, methods };
|
|
159
|
-
};
|
|
160
|
-
var applyChainedValidations = (node, schema) => {
|
|
161
|
-
const chain = unwrapChain(node);
|
|
162
|
-
applyChainedCallsToSchema(chain.methods, schema);
|
|
163
|
-
};
|
|
164
|
-
var applyChainedCallsToSchema = (methods, schema) => {
|
|
165
|
-
for (const { name, args } of methods) {
|
|
166
|
-
switch (name) {
|
|
167
|
-
case "email":
|
|
168
|
-
schema.format = "email";
|
|
169
|
-
break;
|
|
170
|
-
case "url":
|
|
171
|
-
schema.format = "uri";
|
|
172
|
-
break;
|
|
173
|
-
case "uuid":
|
|
174
|
-
schema.format = "uuid";
|
|
175
|
-
break;
|
|
176
|
-
case "datetime":
|
|
177
|
-
schema.format = "date-time";
|
|
178
|
-
break;
|
|
179
|
-
case "min": {
|
|
180
|
-
const val = getNumericArg(args[0]);
|
|
181
|
-
if (val !== null) {
|
|
182
|
-
if (schema.type === "string") schema.minLength = val;
|
|
183
|
-
else schema.minimum = val;
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
case "max": {
|
|
188
|
-
const val = getNumericArg(args[0]);
|
|
189
|
-
if (val !== null) {
|
|
190
|
-
if (schema.type === "string") schema.maxLength = val;
|
|
191
|
-
else schema.maximum = val;
|
|
192
|
-
}
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
case "describe": {
|
|
196
|
-
const desc = getStringArg(args[0]);
|
|
197
|
-
if (desc) schema.description = desc;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
case "default": {
|
|
201
|
-
const arg = args[0];
|
|
202
|
-
if (arg) schema.default = tryParseValue(arg.getText());
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
case "optional":
|
|
206
|
-
case "nullish":
|
|
207
|
-
schema.nullable = true;
|
|
208
|
-
break;
|
|
209
|
-
case "positive":
|
|
210
|
-
schema.minimum = 0;
|
|
211
|
-
break;
|
|
212
|
-
case "negative":
|
|
213
|
-
schema.maximum = 0;
|
|
214
|
-
break;
|
|
215
|
-
case "int":
|
|
216
|
-
schema.type = "integer";
|
|
217
|
-
break;
|
|
218
|
-
case "regex": {
|
|
219
|
-
const pattern = args[0]?.getText();
|
|
220
|
-
if (pattern) schema.pattern = pattern.replace(/^\/|\/[gimsuy]*$/g, "");
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
var isOptionalZodExpr = (node) => {
|
|
227
|
-
const text = node.getText();
|
|
228
|
-
return text.includes(".optional()") || text.includes(".nullish()") || text.startsWith("z.optional(");
|
|
229
|
-
};
|
|
230
|
-
var getNumericArg = (node) => {
|
|
231
|
-
if (!node) return null;
|
|
232
|
-
const val = Number(node.getText());
|
|
233
|
-
return isNaN(val) ? null : val;
|
|
234
|
-
};
|
|
235
|
-
var getStringArg = (node) => {
|
|
236
|
-
if (!node) return null;
|
|
237
|
-
const text = node.getText();
|
|
238
|
-
const match = text.match(/^['"`](.*?)['"`]$/s);
|
|
239
|
-
return match?.[1] ?? null;
|
|
240
|
-
};
|
|
241
|
-
var tryParseValue = (text) => {
|
|
242
|
-
try {
|
|
243
|
-
return JSON.parse(text);
|
|
244
|
-
} catch {
|
|
245
|
-
return text.replace(/['"]/g, "");
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
var extractZodSchemas = (sourceFile) => {
|
|
249
|
-
const schemas = {};
|
|
250
|
-
const varDeclarations = sourceFile.getVariableDeclarations();
|
|
251
|
-
for (const decl of varDeclarations) {
|
|
252
|
-
const init = decl.getInitializer();
|
|
253
|
-
if (!init) continue;
|
|
254
|
-
const text = init.getText();
|
|
255
|
-
if (!text.startsWith("z.")) continue;
|
|
256
|
-
const name = decl.getName();
|
|
257
|
-
try {
|
|
258
|
-
schemas[name] = zodNodeToJsonSchema(init);
|
|
259
|
-
} catch {
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return schemas;
|
|
263
|
-
};
|
|
264
|
-
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
265
|
-
var findTsConfig = (startDir) => {
|
|
266
|
-
let dir = startDir;
|
|
267
|
-
for (let i = 0; i < 5; i++) {
|
|
268
|
-
const candidate = join2(dir, "tsconfig.json");
|
|
269
|
-
if (existsSync2(candidate)) return candidate;
|
|
270
|
-
const parent = dirname(dir);
|
|
271
|
-
if (parent === dir) break;
|
|
272
|
-
dir = parent;
|
|
273
|
-
}
|
|
274
|
-
return void 0;
|
|
275
|
-
};
|
|
276
|
-
var extractExpressRoutes = async (config) => {
|
|
277
|
-
const cwd = existsSync2(config.entry) && !config.entry.endsWith(".ts") && !config.entry.endsWith(".js") ? config.entry : dirname(config.entry);
|
|
278
|
-
const tsConfigPath = findTsConfig(cwd);
|
|
279
|
-
const project = tsConfigPath ? new Project({ tsConfigFilePath: tsConfigPath, skipAddingFilesFromTsConfig: true }) : new Project({ compilerOptions: { allowJs: true, strict: false } });
|
|
280
|
-
const patterns = ["**/*.ts", "**/*.js"].map((p) => resolve2(cwd, "**", p));
|
|
281
|
-
const files = await glob(`${cwd}/**/*.{ts,js}`, {
|
|
282
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/*.d.ts", "**/*.spec.*", "**/*.test.*"]
|
|
283
|
-
});
|
|
284
|
-
for (const file of files) {
|
|
285
|
-
project.addSourceFileAtPath(file);
|
|
286
|
-
}
|
|
287
|
-
const globalZodSchemas = {};
|
|
288
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
289
|
-
Object.assign(globalZodSchemas, extractZodSchemas(sourceFile));
|
|
290
|
-
}
|
|
291
|
-
const allRoutes = [];
|
|
292
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
293
|
-
const ctx = { zodSchemas: globalZodSchemas, routerPrefix: "" };
|
|
294
|
-
const routes = extractRoutesFromFile(sourceFile, ctx);
|
|
295
|
-
allRoutes.push(...routes);
|
|
296
|
-
}
|
|
297
|
-
return deduplicateRoutes(allRoutes);
|
|
298
|
-
};
|
|
299
|
-
var extractRoutesFromFile = (sourceFile, ctx) => {
|
|
300
|
-
const routes = [];
|
|
301
|
-
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
302
|
-
for (const call of callExpressions) {
|
|
303
|
-
const route = tryExtractRoute(call, ctx);
|
|
304
|
-
if (route) routes.push(route);
|
|
305
|
-
}
|
|
306
|
-
return routes;
|
|
307
|
-
};
|
|
308
|
-
var tryExtractRoute = (call, ctx) => {
|
|
309
|
-
const expr = call.getExpression();
|
|
310
|
-
if (!Node2.isPropertyAccessExpression(expr)) return null;
|
|
311
|
-
const methodName = expr.getName().toUpperCase();
|
|
312
|
-
if (!HTTP_METHODS.includes(methodName)) return null;
|
|
313
|
-
const args = call.getArguments();
|
|
314
|
-
if (args.length < 2) return null;
|
|
315
|
-
const firstArg = args[0];
|
|
316
|
-
if (!firstArg) return null;
|
|
317
|
-
if (!Node2.isStringLiteral(firstArg) && !Node2.isTemplateExpression(firstArg) && !Node2.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
const rawPath = firstArg.getText().replace(/['"'`]/g, "");
|
|
321
|
-
const path = normalizePath(ctx.routerPrefix, rawPath);
|
|
322
|
-
const method = methodName;
|
|
323
|
-
const middlewareArgs = args.slice(1, -1);
|
|
324
|
-
const handlerArg = args[args.length - 1];
|
|
325
|
-
const { requestBody, parameters } = extractFromMiddleware(middlewareArgs, ctx);
|
|
326
|
-
const pathParams = extractPathParameters(path);
|
|
327
|
-
const handlerInfo = extractHandlerInfo(handlerArg);
|
|
328
|
-
const allParameters = mergeParameters(parameters, pathParams);
|
|
329
|
-
const tags = inferTags(path);
|
|
330
|
-
const route = {
|
|
331
|
-
method,
|
|
332
|
-
path,
|
|
333
|
-
tags,
|
|
334
|
-
parameters: allParameters,
|
|
335
|
-
responses: buildDefaultResponses(method),
|
|
336
|
-
middleware: middlewareArgs.map((m) => m.getText())
|
|
337
|
-
};
|
|
338
|
-
if (handlerInfo.summary !== void 0) route.summary = handlerInfo.summary;
|
|
339
|
-
if (handlerInfo.description !== void 0) route.description = handlerInfo.description;
|
|
340
|
-
if (requestBody !== null) route.requestBody = requestBody;
|
|
341
|
-
return route;
|
|
342
|
-
};
|
|
343
|
-
var extractFromMiddleware = (middleware, ctx) => {
|
|
344
|
-
let requestBody = null;
|
|
345
|
-
const parameters = [];
|
|
346
|
-
for (const mw of middleware) {
|
|
347
|
-
const text = mw.getText();
|
|
348
|
-
const bodyMatch = text.match(/(?:validateBody|validate|zodValidate|bodyParser)\((\w+)\)/);
|
|
349
|
-
if (bodyMatch) {
|
|
350
|
-
const schemaName = bodyMatch[1];
|
|
351
|
-
const schema = schemaName ? ctx.zodSchemas[schemaName] : null;
|
|
352
|
-
if (schema) {
|
|
353
|
-
requestBody = {
|
|
354
|
-
required: true,
|
|
355
|
-
content: { "application/json": { schema } }
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
const queryMatch = text.match(/(?:validateQuery|queryValidator)\((\w+)\)/);
|
|
360
|
-
if (queryMatch) {
|
|
361
|
-
const schemaName = queryMatch[1];
|
|
362
|
-
const schema = schemaName ? ctx.zodSchemas[schemaName] : null;
|
|
363
|
-
if (schema?.type === "object" && schema.properties) {
|
|
364
|
-
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
365
|
-
parameters.push({
|
|
366
|
-
name,
|
|
367
|
-
in: "query",
|
|
368
|
-
required: schema.required?.includes(name) ?? false,
|
|
369
|
-
schema: propSchema
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return { requestBody, parameters };
|
|
376
|
-
};
|
|
377
|
-
var extractPathParameters = (path) => {
|
|
378
|
-
const paramRegex = /:(\w+)/g;
|
|
379
|
-
const params = [];
|
|
380
|
-
let match;
|
|
381
|
-
while ((match = paramRegex.exec(path)) !== null) {
|
|
382
|
-
const name = match[1];
|
|
383
|
-
if (name) {
|
|
384
|
-
params.push({
|
|
385
|
-
name,
|
|
386
|
-
in: "path",
|
|
387
|
-
required: true,
|
|
388
|
-
schema: { type: "string" }
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return params;
|
|
393
|
-
};
|
|
394
|
-
var extractHandlerInfo = (handler) => {
|
|
395
|
-
const leadingComments = handler.getLeadingCommentRanges();
|
|
396
|
-
if (leadingComments.length === 0) return {};
|
|
397
|
-
const comment = leadingComments[leadingComments.length - 1];
|
|
398
|
-
if (!comment) return {};
|
|
399
|
-
const text = comment.getText();
|
|
400
|
-
const summary = extractJsDocTag(text, null);
|
|
401
|
-
const description = extractJsDocTag(text, "description");
|
|
402
|
-
const result = {};
|
|
403
|
-
if (summary !== null) result.summary = summary;
|
|
404
|
-
if (description !== null) result.description = description;
|
|
405
|
-
return result;
|
|
406
|
-
};
|
|
407
|
-
var extractJsDocTag = (comment, tag) => {
|
|
408
|
-
if (tag === null) {
|
|
409
|
-
const match2 = comment.match(/\/\*\*\s*\n?\s*\*\s*(.+)/);
|
|
410
|
-
return match2?.[1]?.trim() ?? null;
|
|
411
|
-
}
|
|
412
|
-
const match = comment.match(new RegExp(`@${tag}\\s+(.+)`));
|
|
413
|
-
return match?.[1]?.trim() ?? null;
|
|
414
|
-
};
|
|
415
|
-
var mergeParameters = (fromMiddleware, fromPath) => {
|
|
416
|
-
const existing = new Set(fromMiddleware.map((p) => `${p.in}:${p.name}`));
|
|
417
|
-
const merged = [...fromMiddleware];
|
|
418
|
-
for (const param of fromPath) {
|
|
419
|
-
if (!existing.has(`${param.in}:${param.name}`)) {
|
|
420
|
-
merged.push(param);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return merged;
|
|
424
|
-
};
|
|
425
|
-
var normalizePath = (prefix, path) => {
|
|
426
|
-
const combined = `${prefix}${path}`.replace(/\/+/g, "/");
|
|
427
|
-
return combined.startsWith("/") ? combined : `/${combined}`;
|
|
428
|
-
};
|
|
429
|
-
var inferTags = (path) => {
|
|
430
|
-
const segments = path.split("/").filter(Boolean);
|
|
431
|
-
const tag = segments.find((s) => !s.startsWith(":"));
|
|
432
|
-
return tag ? [tag] : ["default"];
|
|
433
|
-
};
|
|
434
|
-
var buildDefaultResponses = (method) => {
|
|
435
|
-
const responses = {
|
|
436
|
-
"200": { description: method === "DELETE" ? "Success" : "Successful response" },
|
|
437
|
-
"400": { description: "Bad request" },
|
|
438
|
-
"401": { description: "Unauthorized" },
|
|
439
|
-
"500": { description: "Internal server error" }
|
|
440
|
-
};
|
|
441
|
-
if (method === "POST") {
|
|
442
|
-
responses["201"] = { description: "Created" };
|
|
443
|
-
}
|
|
444
|
-
if (method === "DELETE") {
|
|
445
|
-
responses["204"] = { description: "No content" };
|
|
446
|
-
}
|
|
447
|
-
return responses;
|
|
448
|
-
};
|
|
449
|
-
var deduplicateRoutes = (routes) => {
|
|
450
|
-
const seen = /* @__PURE__ */ new Set();
|
|
451
|
-
return routes.filter((r) => {
|
|
452
|
-
const key = `${r.method}:${r.path}`;
|
|
453
|
-
if (seen.has(key)) return false;
|
|
454
|
-
seen.add(key);
|
|
455
|
-
return true;
|
|
456
|
-
});
|
|
457
|
-
};
|
|
458
|
-
var buildSpec = (routes, config) => {
|
|
459
|
-
const groups = groupRoutes(routes, config.groups);
|
|
460
|
-
return {
|
|
461
|
-
info: {
|
|
462
|
-
title: config.name,
|
|
463
|
-
version: config.version ?? "1.0.0",
|
|
464
|
-
baseUrl: config.baseUrl ?? "http://localhost:3000",
|
|
465
|
-
...config.description !== void 0 ? { description: config.description } : {}
|
|
466
|
-
},
|
|
467
|
-
...config.auth !== void 0 ? { auth: config.auth } : {},
|
|
468
|
-
groups,
|
|
469
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
470
|
-
sourceFramework: config.framework
|
|
471
|
-
};
|
|
472
|
-
};
|
|
473
|
-
var groupRoutes = (routes, groupConfig) => {
|
|
474
|
-
if (!groupConfig) {
|
|
475
|
-
return groupByTag(routes);
|
|
476
|
-
}
|
|
477
|
-
const groups = [];
|
|
478
|
-
const assignedRoutes = /* @__PURE__ */ new Set();
|
|
479
|
-
for (const [groupName, patterns] of Object.entries(groupConfig)) {
|
|
480
|
-
const matched = routes.filter((r) => {
|
|
481
|
-
const key = `${r.method}:${r.path}`;
|
|
482
|
-
if (assignedRoutes.has(key)) return false;
|
|
483
|
-
const matches = patterns.some((pattern) => matchesGlob(r.path, pattern));
|
|
484
|
-
if (matches) assignedRoutes.add(key);
|
|
485
|
-
return matches;
|
|
486
|
-
});
|
|
487
|
-
if (matched.length > 0) {
|
|
488
|
-
groups.push({ name: groupName, routes: matched });
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const unassigned = routes.filter(
|
|
492
|
-
(r) => !assignedRoutes.has(`${r.method}:${r.path}`)
|
|
493
|
-
);
|
|
494
|
-
if (unassigned.length > 0) {
|
|
495
|
-
groups.push({ name: "Other", routes: unassigned });
|
|
496
|
-
}
|
|
497
|
-
return groups;
|
|
498
|
-
};
|
|
499
|
-
var groupByTag = (routes) => {
|
|
500
|
-
const tagMap = /* @__PURE__ */ new Map();
|
|
501
|
-
for (const route of routes) {
|
|
502
|
-
const tag = route.tags[0] ?? "default";
|
|
503
|
-
if (!tagMap.has(tag)) tagMap.set(tag, []);
|
|
504
|
-
tagMap.get(tag).push(route);
|
|
505
|
-
}
|
|
506
|
-
return Array.from(tagMap.entries()).map(([name, groupRoutes2]) => ({
|
|
507
|
-
name: capitalize(name),
|
|
508
|
-
routes: groupRoutes2
|
|
509
|
-
}));
|
|
510
|
-
};
|
|
511
|
-
var matchesGlob = (path, pattern) => {
|
|
512
|
-
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*$/, "(/.*)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
513
|
-
return new RegExp(`^${regexStr}$`).test(path);
|
|
514
|
-
};
|
|
515
|
-
var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
516
|
-
|
|
517
|
-
// src/generate.ts
|
|
518
|
-
var generate = async (opts = {}) => {
|
|
519
|
-
const cwd = process.cwd();
|
|
520
|
-
const spinner = opts.quiet ? null : ora();
|
|
521
|
-
spinner?.start("Loading flowdoc config...");
|
|
522
|
-
const configPath = opts.config ? resolve3(cwd, opts.config) : findConfigFile(cwd);
|
|
523
|
-
if (!configPath) {
|
|
524
|
-
spinner?.fail(chalk.red("No flowdoc.config.ts found. Run `flowdoc init` first."));
|
|
525
|
-
process.exit(1);
|
|
526
|
-
}
|
|
527
|
-
let rawConfig;
|
|
528
|
-
try {
|
|
529
|
-
rawConfig = await loadConfig(configPath);
|
|
530
|
-
} catch (err) {
|
|
531
|
-
spinner?.fail(chalk.red(`Failed to load config: ${String(err)}`));
|
|
532
|
-
process.exit(1);
|
|
533
|
-
}
|
|
534
|
-
const config = resolveConfig(rawConfig, cwd);
|
|
535
|
-
spinner?.succeed(`Config loaded \u2014 ${chalk.cyan(config.name)}`);
|
|
536
|
-
spinner?.start(`Scanning ${chalk.cyan(config.entry)} for routes...`);
|
|
537
|
-
let routes;
|
|
538
|
-
try {
|
|
539
|
-
routes = await extractExpressRoutes(config);
|
|
540
|
-
} catch (err) {
|
|
541
|
-
spinner?.fail(chalk.red(`Parse failed: ${String(err)}`));
|
|
542
|
-
process.exit(1);
|
|
543
|
-
}
|
|
544
|
-
spinner?.succeed(
|
|
545
|
-
`Found ${chalk.green(String(routes.length))} routes across ${chalk.cyan(config.framework)} app`
|
|
546
|
-
);
|
|
547
|
-
const spec = buildSpec(routes, config);
|
|
548
|
-
const outputDir = opts.output ? resolve3(cwd, opts.output) : config.output ?? resolve3(cwd, "docs-output");
|
|
549
|
-
mkdirSync(outputDir, { recursive: true });
|
|
550
|
-
const specPath = join3(outputDir, "flowdoc.json");
|
|
551
|
-
writeFileSync(specPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
552
|
-
await writeUiHtml(outputDir, config);
|
|
553
|
-
if (!opts.quiet) {
|
|
554
|
-
console.log();
|
|
555
|
-
console.log(chalk.bold(" flowdoc generated successfully"));
|
|
556
|
-
console.log();
|
|
557
|
-
console.log(` ${chalk.gray("Spec:")} ${chalk.cyan(specPath)}`);
|
|
558
|
-
console.log(` ${chalk.gray("UI:")} ${chalk.cyan(join3(outputDir, "index.html"))}`);
|
|
559
|
-
console.log();
|
|
560
|
-
console.log(` ${chalk.gray("Routes:")} ${chalk.green(String(routes.length))}`);
|
|
561
|
-
console.log(` ${chalk.gray("Groups:")} ${chalk.green(String(spec.groups.length))}`);
|
|
562
|
-
console.log();
|
|
563
|
-
}
|
|
564
|
-
return spec;
|
|
565
|
-
};
|
|
566
|
-
var writeUiHtml = async (outputDir, config) => {
|
|
567
|
-
const brand = config.theme?.brand ?? "#6366f1";
|
|
568
|
-
const title = config.name;
|
|
569
|
-
const darkMode = config.theme?.darkMode !== false;
|
|
570
|
-
const cliRoot = dirname2(dirname2(fileURLToPath(import.meta.url)));
|
|
571
|
-
const uiAssetsSource = join3(cliRoot, "ui-assets");
|
|
572
|
-
const uiAssetsDest = join3(outputDir, "assets");
|
|
573
|
-
if (existsSync3(uiAssetsSource)) {
|
|
574
|
-
mkdirSync(uiAssetsDest, { recursive: true });
|
|
575
|
-
cpSync(uiAssetsSource, uiAssetsDest, { recursive: true });
|
|
576
|
-
}
|
|
577
|
-
const html = generateHtmlShell({ title, brand, darkMode });
|
|
578
|
-
writeFileSync(join3(outputDir, "index.html"), html, "utf-8");
|
|
579
|
-
};
|
|
580
|
-
var generateHtmlShell = ({ title, brand, darkMode }) => `<!DOCTYPE html>
|
|
581
|
-
<html lang="en" class="${darkMode ? "dark" : ""}">
|
|
582
|
-
<head>
|
|
583
|
-
<meta charset="UTF-8" />
|
|
584
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
585
|
-
<title>${title} \u2014 API Docs</title>
|
|
586
|
-
<meta name="description" content="API documentation generated by flowdoc" />
|
|
587
|
-
<script>
|
|
588
|
-
window.__FLOWDOC_BRAND__ = "${brand}";
|
|
589
|
-
window.__FLOWDOC_DARK__ = ${String(darkMode)};
|
|
590
|
-
</script>
|
|
591
|
-
<script type="module" crossorigin src="./assets/ui.js"></script>
|
|
592
|
-
<link rel="stylesheet" href="./assets/index.css" />
|
|
593
|
-
</head>
|
|
594
|
-
<body>
|
|
595
|
-
<div id="root"></div>
|
|
596
|
-
</body>
|
|
597
|
-
</html>`;
|
|
598
|
-
|
|
599
|
-
export {
|
|
600
|
-
findConfigFile,
|
|
601
|
-
loadConfig,
|
|
602
|
-
resolveConfig,
|
|
603
|
-
generate
|
|
604
|
-
};
|
package/dist/init-27XS6ADW.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/init.ts
|
|
4
|
-
import { writeFileSync, existsSync } from "fs";
|
|
5
|
-
import { resolve } from "path";
|
|
6
|
-
import chalk from "chalk";
|
|
7
|
-
var CONFIG_TEMPLATE = `import type { FlowDocConfig } from "@flowdoc/core";
|
|
8
|
-
|
|
9
|
-
const config: FlowDocConfig = {
|
|
10
|
-
name: "My API",
|
|
11
|
-
version: "1.0.0",
|
|
12
|
-
description: "API documentation generated by flowdoc",
|
|
13
|
-
framework: "express",
|
|
14
|
-
entry: "./src", // folder or file containing your Express routes
|
|
15
|
-
baseUrl: "http://localhost:3000",
|
|
16
|
-
auth: {
|
|
17
|
-
type: "bearer",
|
|
18
|
-
},
|
|
19
|
-
output: "./docs-output",
|
|
20
|
-
theme: {
|
|
21
|
-
brand: "#6366f1",
|
|
22
|
-
darkMode: true,
|
|
23
|
-
},
|
|
24
|
-
// Optional: manually group routes under named sections
|
|
25
|
-
// groups: {
|
|
26
|
-
// "User Management": ["/users/**"],
|
|
27
|
-
// "Auth": ["/auth/**"],
|
|
28
|
-
// },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export default config;
|
|
32
|
-
`;
|
|
33
|
-
var init = (cwd = process.cwd()) => {
|
|
34
|
-
const configPath = resolve(cwd, "flowdoc.config.ts");
|
|
35
|
-
if (existsSync(configPath)) {
|
|
36
|
-
console.log(chalk.yellow(" flowdoc.config.ts already exists \u2014 skipping."));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
writeFileSync(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
40
|
-
console.log();
|
|
41
|
-
console.log(` ${chalk.bold("flowdoc")} initialized`);
|
|
42
|
-
console.log();
|
|
43
|
-
console.log(` Created ${chalk.cyan("flowdoc.config.ts")}`);
|
|
44
|
-
console.log();
|
|
45
|
-
console.log(" Next steps:");
|
|
46
|
-
console.log(` 1. Edit ${chalk.cyan("flowdoc.config.ts")} \u2014 set your entry path`);
|
|
47
|
-
console.log(` 2. Run ${chalk.cyan("flowdoc generate")} to build your docs`);
|
|
48
|
-
console.log(` 3. Run ${chalk.cyan("flowdoc serve")} to preview locally`);
|
|
49
|
-
console.log();
|
|
50
|
-
};
|
|
51
|
-
export {
|
|
52
|
-
init
|
|
53
|
-
};
|
package/dist/init-HVJTHT4U.js
DELETED
package/dist/serve-VKTQ5E5O.js
DELETED
package/dist/serve-Y4E3DTAJ.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
findConfigFile,
|
|
4
|
-
generate,
|
|
5
|
-
loadConfig,
|
|
6
|
-
resolveConfig
|
|
7
|
-
} from "./chunk-3GGK6LWE.js";
|
|
8
|
-
|
|
9
|
-
// src/serve.ts
|
|
10
|
-
import { createServer } from "http";
|
|
11
|
-
import { createReadStream, existsSync } from "fs";
|
|
12
|
-
import { join, extname, resolve } from "path";
|
|
13
|
-
import chalk from "chalk";
|
|
14
|
-
import open from "open";
|
|
15
|
-
import chokidar from "chokidar";
|
|
16
|
-
var MIME_TYPES = {
|
|
17
|
-
".html": "text/html",
|
|
18
|
-
".js": "application/javascript",
|
|
19
|
-
".css": "text/css",
|
|
20
|
-
".json": "application/json",
|
|
21
|
-
".svg": "image/svg+xml",
|
|
22
|
-
".png": "image/png",
|
|
23
|
-
".ico": "image/x-icon"
|
|
24
|
-
};
|
|
25
|
-
var serve = async (opts = {}) => {
|
|
26
|
-
const cwd = process.cwd();
|
|
27
|
-
const outputDir = opts.output ?? join(cwd, "docs-output");
|
|
28
|
-
const port = opts.port ?? 4e3;
|
|
29
|
-
await generate({ ...opts, quiet: false });
|
|
30
|
-
const server = createServer((req, res) => {
|
|
31
|
-
const url = req.url === "/" || req.url === "" ? "/index.html" : req.url ?? "/index.html";
|
|
32
|
-
const filePath = join(outputDir, url);
|
|
33
|
-
if (!existsSync(filePath)) {
|
|
34
|
-
res.writeHead(404);
|
|
35
|
-
res.end("Not found");
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const ext = extname(filePath);
|
|
39
|
-
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
40
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
41
|
-
createReadStream(filePath).pipe(res);
|
|
42
|
-
});
|
|
43
|
-
server.listen(port, () => {
|
|
44
|
-
const url = `http://localhost:${port}`;
|
|
45
|
-
console.log();
|
|
46
|
-
console.log(` ${chalk.bold("flowdoc")} is running at ${chalk.cyan(url)}`);
|
|
47
|
-
if (opts.watch) {
|
|
48
|
-
console.log(` ${chalk.dim("watching for changes\u2026")}`);
|
|
49
|
-
}
|
|
50
|
-
console.log();
|
|
51
|
-
if (!opts.noOpen) {
|
|
52
|
-
void open(url);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
if (!opts.watch) return;
|
|
56
|
-
const configPath = opts.config ?? findConfigFile(cwd) ?? "";
|
|
57
|
-
let watchGlob = "src/**/*.ts";
|
|
58
|
-
if (configPath) {
|
|
59
|
-
try {
|
|
60
|
-
const raw = await loadConfig(configPath);
|
|
61
|
-
const config = resolveConfig(raw, cwd);
|
|
62
|
-
const entryDir = resolve(cwd, config.entry).replace(/\/[^/]+$/, "");
|
|
63
|
-
watchGlob = `${entryDir}/**/*.ts`;
|
|
64
|
-
} catch {
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
let rebuilding = false;
|
|
68
|
-
const watcher = chokidar.watch(watchGlob, {
|
|
69
|
-
ignoreInitial: true,
|
|
70
|
-
ignored: ["**/node_modules/**", "**/dist/**", "**/docs-output/**"]
|
|
71
|
-
});
|
|
72
|
-
const rebuild = async (filePath) => {
|
|
73
|
-
if (rebuilding) return;
|
|
74
|
-
rebuilding = true;
|
|
75
|
-
console.log(` ${chalk.dim("\u2192")} ${chalk.yellow(filePath.replace(cwd + "/", ""))} changed \u2014 regenerating\u2026`);
|
|
76
|
-
try {
|
|
77
|
-
await generate({ ...opts, quiet: true });
|
|
78
|
-
console.log(` ${chalk.green("\u2713")} docs updated`);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.error(` ${chalk.red("\u2717")} regeneration failed:`, err instanceof Error ? err.message : err);
|
|
81
|
-
} finally {
|
|
82
|
-
rebuilding = false;
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
watcher.on("add", rebuild).on("change", rebuild).on("unlink", rebuild);
|
|
86
|
-
process.on("SIGINT", () => {
|
|
87
|
-
void watcher.close();
|
|
88
|
-
server.close();
|
|
89
|
-
process.exit(0);
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
export {
|
|
93
|
-
serve
|
|
94
|
-
};
|