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 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-27XS6ADW.js");
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-J6FGBLQ4.js");
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-Y4E3DTAJ.js");
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,
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  // src/generate.ts
4
2
  import { writeFileSync, mkdirSync, cpSync, existsSync as existsSync3 } from "fs";
5
3
  import { resolve as resolve3, join as join3, dirname as dirname2 } from "path";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-SAMPAR3A.js";
3
+ } from "./chunk-ZJCRRUX7.js";
4
4
  export {
5
5
  generate
6
6
  };
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
- export { type FlowDocMiddlewareOptions, type GenerateOptions, type ServeOptions, flowdoc, generate, init, serve };
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 type { FlowDocConfig } from "@flowdoc/core";
1247
+ var CONFIG_TEMPLATE = `import { defineConfig } from "flowdoc-gen";
1248
1248
 
1249
- const config: FlowDocConfig = {
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", // folder or file containing your Express routes
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
- // Optional: manually group routes under named sections
1265
- // groups: {
1266
- // "User Management": ["/users/**"],
1267
- // "Auth": ["/auth/**"],
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 type { FlowDocConfig } from "@flowdoc/core";
5
+ var CONFIG_TEMPLATE = `import { defineConfig } from "flowdoc-gen";
6
6
 
7
- const config: FlowDocConfig = {
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", // folder or file containing your Express routes
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
- // Optional: manually group routes under named sections
23
- // groups: {
24
- // "User Management": ["/users/**"],
25
- // "Auth": ["/auth/**"],
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
  };
@@ -1,10 +1,9 @@
1
- #!/usr/bin/env node
2
1
  import {
3
2
  findConfigFile,
4
3
  generate,
5
4
  loadConfig,
6
5
  resolveConfig
7
- } from "./chunk-XXW6UJOX.js";
6
+ } from "./chunk-ZJCRRUX7.js";
8
7
 
9
8
  // src/serve.ts
10
9
  import { createServer } from "http";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowdoc-gen",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Auto-generate beautiful API documentation from your Express codebase — no annotations required",
5
5
  "type": "module",
6
6
  "bin": {
package/dist/bin.d.ts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node
@@ -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
- };
@@ -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
- };
@@ -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
- };
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- generate
4
- } from "./chunk-3GGK6LWE.js";
5
- export {
6
- generate
7
- };
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- generate
4
- } from "./chunk-XXW6UJOX.js";
5
- export {
6
- generate
7
- };
@@ -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
- };
@@ -1,6 +0,0 @@
1
- import {
2
- init
3
- } from "./chunk-VG2YJHSH.js";
4
- export {
5
- init
6
- };
@@ -1,7 +0,0 @@
1
- import {
2
- serve
3
- } from "./chunk-P6Z6T3W4.js";
4
- import "./chunk-SAMPAR3A.js";
5
- export {
6
- serve
7
- };
@@ -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
- };