autodocsync 1.0.0 → 1.1.0

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/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # AutoDocSync
2
+
3
+ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) ![Node.js](https://img.shields.io/badge/Node.js-20+-green.svg) ![Ollama](https://img.shields.io/badge/AI-Ollama-purple.svg) ![AST](https://img.shields.io/badge/AST-TypeScript-blue.svg)
4
+
5
+ **Automated OpenAPI Documentation with AST & Local LLMs**
6
+
7
+ AutoDocSync is a high-performance CLI tool for Express.js developers. It bridges the gap between your source code and documentation by using **TypeScript AST (Abstract Syntax Tree)** to statically analyze your project and **Local AI (Ollama)** to generate human-readable endpoint descriptions — entirely on your machine.
8
+
9
+ > [!NOTE]
10
+ > AutoDocSync was built to solve the "Stale Docs" problem. By making documentation generation part of your workflow, your OpenAPI spec stays accurate as your code evolves.
11
+
12
+ ---
13
+
14
+ ## How It Works
15
+
16
+ AutoDocSync does not guess your routes or rely on manual annotations. It performs a deep static analysis of your Express project to understand exactly how your API behaves.
17
+
18
+ ```mermaid
19
+ graph TD
20
+ A[Source Code] --> B{AST Scanner}
21
+ B -- "Express Routes" --> C[Logic Extraction]
22
+ B -- "Mongoose Models" --> D[Schema Parsing]
23
+ C --> E[Context Builder]
24
+ D --> E
25
+ E --> F((Local Ollama))
26
+ F -- "Mistral" --> G[AI Enhancement]
27
+ G --> H[Final OpenAPI 3.0 Spec]
28
+ ```
29
+
30
+ **Deep Integration**
31
+
32
+ - **AST Parsing** — Powered by `ts-morph`, the scanner traces `.use()` calls to resolve full URL prefixes and identifies imported handlers and middlewares.
33
+ - **Payload Extraction** — Automatically detects request bodies, query parameters, and URL params directly from your handler logic.
34
+ - **Smart Schemas** — Parses Mongoose models to construct accurate OpenAPI components, including required fields, enums, and contextual examples (e.g., `email` → `user@example.com`).
35
+ - **Privacy-First AI** — Uses a local Ollama instance. No API keys, no data sent to the cloud, and zero inference cost.
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ Follow these steps in order to get AutoDocSync running in your Express project.
42
+
43
+ ### Step 1 — Install AutoDocSync
44
+
45
+ Inside your Express project root, install AutoDocSync as a development dependency:
46
+
47
+ ```bash
48
+ npm install --save-dev autodocsync
49
+ ```
50
+
51
+ ### Step 2 — Install Ollama
52
+
53
+ AutoDocSync uses a locally running Ollama instance to generate AI-powered descriptions.
54
+
55
+ Download and install Ollama from the official site: [https://ollama.com](https://ollama.com)
56
+
57
+ Once installed, verify it is running:
58
+
59
+ ```bash
60
+ ollama --version
61
+ ```
62
+
63
+ ### Step 3 — Pull the Mistral Model
64
+
65
+ AutoDocSync uses the Mistral model for description generation. Pull it with:
66
+
67
+ ```bash
68
+ ollama pull mistral
69
+ ```
70
+
71
+ > [!TIP]
72
+ > This is a one-time download. Once pulled, the model is cached locally and reused across all future runs.
73
+
74
+ ### Step 4 — Generate Your Documentation
75
+
76
+ With everything set up, run the following command from your Express project root:
77
+
78
+ ```bash
79
+ npx autodocsync generate . -o ./docs
80
+ ```
81
+
82
+ This scans your project and writes a production-ready `openapi.json` file to the `./docs` directory.
83
+
84
+ ---
85
+
86
+ ## Example Output
87
+
88
+ Running `npx autodocsync generate . -o ./docs` on a standard Express + Mongoose project produces a file like `docs/openapi.json`. Below is a realistic excerpt:
89
+
90
+ ```json
91
+ {
92
+ "openapi": "3.0.3",
93
+ "info": {
94
+ "title": "My Backend Service",
95
+ "description": "Industry standard API documentation for professional backend integration.",
96
+ "version": "1.0.0",
97
+ "contact": {
98
+ "name": "Engineering Team Support"
99
+ }
100
+ },
101
+ "servers": [
102
+ {
103
+ "url": "/api/v1",
104
+ "description": "Primary Production/Active version API server"
105
+ }
106
+ ],
107
+ "tags": [
108
+ { "name": "Auth", "description": "Authentication algorithms, secure login, and session management" },
109
+ { "name": "User", "description": "User profile management, account settings, and event registrations" }
110
+ ],
111
+ "paths": {
112
+ "/auth/login": {
113
+ "post": {
114
+ "tags": ["Auth"],
115
+ "summary": "Login user",
116
+ "description": "Authenticates a registered user using their email and password. Returns a signed JWT token and sets an httpOnly access token cookie on success.",
117
+ "security": [],
118
+ "requestBody": {
119
+ "required": true,
120
+ "content": {
121
+ "application/json": {
122
+ "schema": {
123
+ "type": "object",
124
+ "properties": {
125
+ "email": { "type": "string", "example": "user@example.com" },
126
+ "password": { "type": "string", "example": "********" }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ },
132
+ "responses": {
133
+ "200": {
134
+ "description": "Successful operation",
135
+ "content": {
136
+ "application/json": {
137
+ "schema": { "$ref": "#/components/schemas/SuccessResponse" }
138
+ }
139
+ }
140
+ },
141
+ "401": {
142
+ "description": "Action failed due to client or server error",
143
+ "content": {
144
+ "application/json": {
145
+ "schema": { "$ref": "#/components/schemas/ErrorResponse" }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ },
152
+ "/users/{id}": {
153
+ "get": {
154
+ "tags": ["User"],
155
+ "summary": "Get user by id",
156
+ "description": "Retrieves the full profile of a user by their MongoDB ObjectId. Requires a valid bearer token or session cookie.",
157
+ "parameters": [
158
+ {
159
+ "name": "id",
160
+ "in": "path",
161
+ "required": true,
162
+ "schema": { "type": "string", "example": "60d0fe4f5311236168a109ca" }
163
+ }
164
+ ],
165
+ "responses": {
166
+ "200": {
167
+ "description": "Successful operation",
168
+ "content": {
169
+ "application/json": {
170
+ "schema": { "$ref": "#/components/schemas/SuccessResponse" }
171
+ }
172
+ }
173
+ },
174
+ "404": {
175
+ "description": "Action failed due to client or server error",
176
+ "content": {
177
+ "application/json": {
178
+ "schema": { "$ref": "#/components/schemas/ErrorResponse" }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ },
186
+ "components": {
187
+ "securitySchemes": {
188
+ "cookieAuth": { "type": "apiKey", "in": "cookie", "name": "accessToken" },
189
+ "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
190
+ },
191
+ "schemas": {
192
+ "SuccessResponse": {
193
+ "type": "object",
194
+ "properties": {
195
+ "success": { "type": "boolean", "example": true },
196
+ "message": { "type": "string", "example": "Operation completed successfully." },
197
+ "data": { "type": "object", "nullable": true }
198
+ }
199
+ },
200
+ "ErrorResponse": {
201
+ "type": "object",
202
+ "properties": {
203
+ "success": { "type": "boolean", "example": false },
204
+ "message": { "type": "string", "example": "Detailed error message explaining the failure." },
205
+ "error": { "type": "object", "nullable": true }
206
+ }
207
+ }
208
+ }
209
+ },
210
+ "security": [
211
+ { "cookieAuth": [] },
212
+ { "bearerAuth": [] }
213
+ ]
214
+ }
215
+ ```
216
+
217
+ > [!TIP]
218
+ > Drop the generated `openapi.json` into [Swagger UI](https://swagger.io/tools/swagger-ui/) or [Redoc](https://redocly.com/redoc/) for instant, interactive API documentation.
219
+
220
+ ---
221
+
222
+ ## CLI Reference
223
+
224
+ | Command | Argument | Option | Description |
225
+ | :--- | :--- | :--- | :--- |
226
+ | `generate` | `[projectDir]` | `-o, --output <dir>` | Scans the project and writes `openapi.json` to the specified output directory. |
227
+
228
+ ---
229
+
230
+ ## Why AutoDocSync?
231
+
232
+ | Feature | Benefit |
233
+ | :--- | :--- |
234
+ | Real-time accuracy | Docs are always in sync with your actual code — no drift. |
235
+ | $0 inference cost | Runs on your own hardware. No API subscriptions required. |
236
+ | OpenAPI 3.0 compliant | Output is compatible with Swagger UI, Redoc, and any standard tooling. |
237
+ | Zero manual effort | No `@swagger` JSDoc comments or route decorators needed. |
238
+
239
+ ---
240
+
241
+ ## Project Structure
242
+
243
+ | Path | Purpose |
244
+ | :--- | :--- |
245
+ | `bin/index.js` | CLI entry point and argument parsing. |
246
+ | `src/scanners/` | AST logic for Express route and Mongoose model parsing. |
247
+ | `src/llm.js` | Integration with the local Ollama API. |
248
+ | `src/openapi-gen.js` | Core logic for compiling and writing the OpenAPI spec. |
249
+
250
+ ---
251
+
252
+ ## Troubleshooting
253
+
254
+ | Issue | Resolution |
255
+ | :--- | :--- |
256
+ | Ollama is not running | Open the Ollama desktop app or run `ollama serve` in a separate terminal. |
257
+ | Model not found | Run `ollama pull mistral` to download the required model. |
258
+ | Slow generation or timeout | Large codebases may take a moment. Ensure at least **8 GB of RAM** is available and Ollama is idle before running. |
259
+ | Empty or partial output | Confirm your project uses standard Express route patterns (`.get`, `.post`, `.use`, etc.). |
260
+
261
+ ---
262
+
263
+ ## Contributing
264
+
265
+ Contributions are welcome. Whether it's adding support for additional frameworks (Fastify, NestJS, Hono) or improving the LLM prompts for better descriptions, feel free to open an issue or submit a pull request.
266
+
267
+ ---
268
+
269
+ ## License
270
+
271
+ Distributed under the **MIT License**.
272
+
273
+ ---
274
+
275
+ *Built for developers who want documentation that actually keeps up with their code.*
package/bin/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  describeEndpointWithMistral,
14
14
  summarizeProjectWithMistral,
15
+ generateREADMEWithMistral,
15
16
  } from "../src/llm.js";
16
17
  import { generateOpenAPI } from "../src/openapi-gen.js";
17
18
 
@@ -100,6 +101,13 @@ program
100
101
  console.log(
101
102
  `[docSync] SUCCESS: openapi.json generated at ${path.join(outDir, "openapi.json")}`,
102
103
  );
104
+
105
+ console.log("[docSync] Projecting professional README.md with Mistral...");
106
+ const readmeContent = await generateREADMEWithMistral(openApiSpec);
107
+ fs.writeFileSync(path.join(outDir, "README.md"), readmeContent, "utf-8");
108
+ console.log(
109
+ `[docSync] SUCCESS: README.md generated at ${path.join(outDir, "README.md")}`,
110
+ );
103
111
  } else {
104
112
  console.log(
105
113
  "[docSync] Generating openapi.json (without AI enhancement)...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autodocsync",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Combines TS code parsing and local LLMs to generate OpenAPI specs automatically",
5
5
  "main": "src/openapi-gen.js",
6
6
  "type": "module",
@@ -27,4 +27,4 @@
27
27
  "glob": "^10.3.10",
28
28
  "ts-morph": "^21.0.1"
29
29
  }
30
- }
30
+ }
@@ -25,6 +25,7 @@ export function printOllamaGuide() {
25
25
  ║ autodocs-sync — setup required ║
26
26
  ╠══════════════════════════════════════════════════╣
27
27
  ║ Ollama is not running on this machine. ║
28
+ ║ ❌ Run: ollama serve ║
28
29
  ║ ║
29
30
  ║ To enable AI-powered docs: ║
30
31
  ║ ║
@@ -35,7 +36,7 @@ export function printOllamaGuide() {
35
36
  ║ $ ollama pull mistral ║
36
37
  ║ ║
37
38
  ║ 3. Regenerate docs manually after setup ║
38
- ║ $ npx autodocs
39
+ ║ $ npx autodocsync generate . -o ./docs
39
40
  ╚══════════════════════════════════════════════════╝
40
41
  `);
41
42
  }
@@ -44,7 +45,7 @@ export function printModelGuide() {
44
45
  console.log(`
45
46
  [autodocs] Ollama is running but mistral is not pulled yet.
46
47
  Run: ollama pull mistral
47
- Then run: npx autodocs
48
+ Then run: npx autodocsync generate . -o ./docs
48
49
  `);
49
50
  }
50
51
 
package/src/llm.js CHANGED
@@ -3,19 +3,19 @@ import axios from "axios";
3
3
  export async function describeEndpointWithMistral(endpoint) {
4
4
  const method = endpoint.method.toUpperCase();
5
5
  const url = endpoint.url;
6
-
6
+
7
7
  // Extract the resource from the URL (e.g., /users, /posts, /comments)
8
8
  const resourceMatch = url.match(/\/([a-zA-Z]+)(\?|$|\/{1})/);
9
- const resource = resourceMatch ? resourceMatch[1] : 'resource';
10
-
9
+ const resource = resourceMatch ? resourceMatch[1] : "resource";
10
+
11
11
  try {
12
12
  // Try to use Ollama HTTP API
13
13
  const prompt = `As a Senior API Architect, generate a highly professional, detailed description for this API endpoint. Explain its purpose and its role in the system. Use industry-standard terminology.\n\nMethod: ${method}\nURL: ${url}\n\nReturn only the description text.`;
14
-
15
- const response = await axios.post('http://localhost:11434/api/generate', {
16
- model: 'mistral',
14
+
15
+ const response = await axios.post("http://localhost:11434/api/generate", {
16
+ model: "mistral",
17
17
  prompt: prompt,
18
- stream: false
18
+ stream: false,
19
19
  });
20
20
 
21
21
  const data = response.data;
@@ -30,10 +30,10 @@ export async function describeEndpointWithMistral(endpoint) {
30
30
  export async function summarizeProjectWithMistral(readme) {
31
31
  try {
32
32
  const prompt = `Based on this README, generate a brief (1-2 sentences), professional description of this project for API documentation:\n\n${readme.slice(0, 3000)}`;
33
- const response = await axios.post('http://localhost:11434/api/generate', {
34
- model: 'mistral',
33
+ const response = await axios.post("http://localhost:11434/api/generate", {
34
+ model: "mistral",
35
35
  prompt: prompt,
36
- stream: false
36
+ stream: false,
37
37
  });
38
38
 
39
39
  const data = response.data;
@@ -44,13 +44,63 @@ export async function summarizeProjectWithMistral(readme) {
44
44
  }
45
45
  }
46
46
 
47
+ export async function generateREADMEWithMistral(openApiSpec) {
48
+ try {
49
+ const prompt = `As a Visionary Technical Architect and documentation specialist, generate an "Ultra-Premium" (Vercel/Linear-tier) README.md file for the following API project.
50
+
51
+ Project Name: ${openApiSpec.info.title}
52
+ Project Goal: ${openApiSpec.info.description}
53
+
54
+ ---
55
+
56
+ ## 🏗️ High-Level Structuring Instructions:
57
+
58
+ 1. **Header**: Premium title with dynamic-looking badges (License, Node.js version, Tech used).
59
+ 2. **The Vision**: A high-impact executive summary and a "The Problem vs. The Solution" section illustrating why this project matters.
60
+ 3. **Real-Time Architecture**: Provide a high-quality Mermaid.js diagram illustrating the core logic and flow of the API.
61
+ 4. **Premium Feature Grid**: Use a clear, sophisticated layout to list key features with professional, benefit-driven copy.
62
+ 5. **Tech Stack**: List all core technologies (Node.js, Express, etc.).
63
+ 6. **Interactive API Documentation**: Generate a clean, comprehensive "API Reference" table with Path, Method, and Description.
64
+ 7. **The Golden Path (Quick Start)**: A copy-paste ready installation and usage section designed for zero friction.
65
+ 8. **Development & Roadmap**: Sections for contributors, including a vision for future improvements.
66
+ 9. **Premium Formatting**: Use GitHub-style "Alerts" (> [!TIP], > [!NOTE]) and professional hierarchy throughout.
67
+
68
+ ---
69
+
70
+ ## Technical Context (OpenAPI 3.0):
71
+ ${JSON.stringify(openApiSpec, null, 2).slice(0, 6000)}
72
+
73
+ ---
74
+
75
+ **CRITICAL**: Return ONLY the Markdown content. Do not add conversational text or wrap in code blocks. Make it look better than any standard technical README. Focus on clarity, aesthetics, and developer experience.`;
76
+
77
+ const response = await axios.post("http://localhost:11434/api/generate", {
78
+ model: "mistral",
79
+ prompt: prompt,
80
+ stream: false,
81
+ });
82
+
83
+ const data = response.data;
84
+ return (
85
+ data.response?.trim() ||
86
+ "# " + openApiSpec.info.title + "\n\nGenerated API documentation."
87
+ );
88
+ } catch (error) {
89
+ console.error("[llm] Failed to generate README:", error.message);
90
+ return "# " + openApiSpec.info.title + "\n\nGenerated API documentation.";
91
+ }
92
+ }
93
+
47
94
  function generateDefaultDescription(method, resource, url) {
48
95
  const descriptions = {
49
- 'GET': `Retrieves ${resource} data from the server.`,
50
- 'POST': `Creates a new ${resource.slice(0, -1) || 'resource'} and returns the created object.`,
51
- 'PUT': `Updates an existing ${resource.slice(0, -1) || 'resource'} with the provided data.`,
52
- 'DELETE': `Permanently deletes the specified ${resource.slice(0, -1) || 'resource'} from the server.`,
53
- 'PATCH': `Partially updates a ${resource.slice(0, -1) || 'resource'} with the provided changes.`
96
+ GET: `Retrieves ${resource} data from the server.`,
97
+ POST: `Creates a new ${resource.slice(0, -1) || "resource"} and returns the created object.`,
98
+ PUT: `Updates an existing ${resource.slice(0, -1) || "resource"} with the provided data.`,
99
+ DELETE: `Permanently deletes the specified ${resource.slice(0, -1) || "resource"} from the server.`,
100
+ PATCH: `Partially updates a ${resource.slice(0, -1) || "resource"} with the provided changes.`,
54
101
  };
55
- return descriptions[method] || `Performs a ${method} request to the ${resource} endpoint.`;
102
+ return (
103
+ descriptions[method] ||
104
+ `Performs a ${method} request to the ${resource} endpoint.`
105
+ );
56
106
  }
@@ -127,7 +127,14 @@ export function generateOpenAPI(scan, enhancedDescriptions = {}) {
127
127
  let description =
128
128
  enhancedDescriptions[route.path]?.[route.method] ||
129
129
  `Performs a ${route.method} operation on the ${relativePath} resource.`;
130
- if (!enhancedDescriptions[route.path]?.[route.method]) {
130
+
131
+ const isCleanHandler =
132
+ route.handlerName &&
133
+ route.handlerName !== "[Anonymous]" &&
134
+ !route.handlerName.includes("{") &&
135
+ !route.handlerName.includes("=>");
136
+
137
+ if (!enhancedDescriptions[route.path]?.[route.method] && isCleanHandler) {
131
138
  description += `\n\n**Implementation details:** This endpoint is handled by \`${route.handlerName}\`.`;
132
139
  }
133
140
 
@@ -139,9 +146,9 @@ export function generateOpenAPI(scan, enhancedDescriptions = {}) {
139
146
 
140
147
  const operation = {
141
148
  tags: [tagName],
142
- summary: camelToTitle(
143
- route.handlerName || `${route.method} ${relativePath}`,
144
- ),
149
+ summary: isCleanHandler
150
+ ? camelToTitle(route.handlerName)
151
+ : `${route.method} ${relativePath}`,
145
152
  description: description,
146
153
  responses: {},
147
154
  };
@@ -41,11 +41,16 @@ export function scanExpressProject(project, root) {
41
41
  for (const sourceFile of project.getSourceFiles()) {
42
42
  const routePrefix = filePrefixMap.get(sourceFile.getFilePath()) || "";
43
43
  const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
44
+ const excludeCallers = ["axios", "fetch", "superagent", "request"];
44
45
 
45
46
  for (const callExpr of callExpressions) {
46
47
  const expression = callExpr.getExpression();
47
48
  if (expression.getKind() !== SyntaxKind.PropertyAccessExpression) continue;
48
49
 
50
+ // Skip common HTTP clients to avoid false positives (e.g., axios.post)
51
+ const callerName = expression.getExpression().getText();
52
+ if (excludeCallers.includes(callerName.toLowerCase())) continue;
53
+
49
54
  const methodName = expression.getName();
50
55
  if (!methods.includes(methodName)) continue;
51
56
 
@@ -57,6 +62,10 @@ export function scanExpressProject(project, root) {
57
62
  // Pattern 1: router.get('/path', handler)
58
63
  if (args.length >= 2 && args[0].getKind() === SyntaxKind.StringLiteral) {
59
64
  pathStr = args[0].getLiteralText();
65
+
66
+ // Skip external URLs (http:// or https://)
67
+ if (pathStr.startsWith("http")) continue;
68
+
60
69
  for (let i = 1; i < args.length - 1; i++) {
61
70
  middlewares.push(args[i].getText());
62
71
  }
@@ -70,6 +79,10 @@ export function scanExpressProject(project, root) {
70
79
  const routeArgs = routeCall.getArguments();
71
80
  if (routeArgs.length > 0 && routeArgs[0].getKind() === SyntaxKind.StringLiteral) {
72
81
  pathStr = routeArgs[0].getLiteralText();
82
+
83
+ // Skip external URLs (http:// or https://)
84
+ if (pathStr.startsWith("http")) continue;
85
+
73
86
  for (let i = 0; i < args.length - 1; i++) {
74
87
  middlewares.push(args[i].getText());
75
88
  }
@@ -80,16 +93,26 @@ export function scanExpressProject(project, root) {
80
93
 
81
94
  if (!pathStr || !handlerNode) continue;
82
95
 
96
+ // Ensure the handler is actually a function or a reference (exclude object literals)
97
+ const isFunctionHandler = [SyntaxKind.Identifier, SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction, SyntaxKind.CallExpression].includes(handlerNode.getKind());
98
+ if (!isFunctionHandler) continue;
99
+
83
100
  const fullPath = (routePrefix + pathStr).replace(/\/\//g, "/").replace(/\/$/, "") || "/";
84
101
  const resolvedHandler = resolveHandler(handlerNode);
85
102
  const analysis = analyzeHandler(resolvedHandler);
86
103
 
104
+ // Clean handler name: if it's an inline function, mark it for the generator to handle
105
+ let handlerName = handlerNode.getText();
106
+ if (handlerName.length > 50 || handlerName.includes("{") || handlerName.includes("=>")) {
107
+ handlerName = `[Anonymous]`;
108
+ }
109
+
87
110
  routes.push({
88
111
  method: methodName.toUpperCase(),
89
112
  path: fullPath,
90
113
  file: path.relative(root, sourceFile.getFilePath()),
91
114
  middlewares,
92
- handlerName: handlerNode.getText(),
115
+ handlerName: handlerName,
93
116
  ...analysis,
94
117
  handlerCode: resolvedHandler ? resolvedHandler.getText().slice(0, 500) : ""
95
118
  });
@@ -7,12 +7,19 @@ export function scanModels(project) {
7
7
  const models = {};
8
8
 
9
9
  for (const sourceFile of project.getSourceFiles()) {
10
- // Look for new Schema(...) or mongoose.Schema(...)
10
+ const schemaAliases = getMongooseSchemaAliases(sourceFile);
11
11
  const newExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression);
12
12
 
13
13
  for (const newExpr of newExpressions) {
14
14
  const typeText = newExpr.getExpression().getText();
15
- if (typeText === "Schema" || typeText === "mongoose.Schema" || typeText.endsWith(".Schema")) {
15
+
16
+ // Detect schema instantiations based on dynamic imports or standard signatures
17
+ let isSchema = false;
18
+ if (schemaAliases.has(typeText) || typeText.endsWith("Schema")) {
19
+ isSchema = true;
20
+ }
21
+
22
+ if (isSchema) {
16
23
  const args = newExpr.getArguments();
17
24
  if (args.length > 0 && args[0].getKind() === SyntaxKind.ObjectLiteralExpression) {
18
25
  const schemaObj = args[0];
@@ -29,26 +36,122 @@ export function scanModels(project) {
29
36
  return models;
30
37
  }
31
38
 
39
+ function getMongooseSchemaAliases(sourceFile) {
40
+ const aliases = new Set(["Schema", "mongoose.Schema"]);
41
+
42
+ // Check ES6 imports
43
+ for (const imp of sourceFile.getImportDeclarations()) {
44
+ if (imp.getModuleSpecifierValue() === 'mongoose') {
45
+ const defaultImport = imp.getDefaultImport();
46
+ if (defaultImport) {
47
+ aliases.add(`${defaultImport.getText()}.Schema`);
48
+ }
49
+ for (const namedImport of imp.getNamedImports()) {
50
+ if (namedImport.getName() === "Schema") {
51
+ aliases.add(namedImport.getAliasNode() ? namedImport.getAliasNode().getText() : "Schema");
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // Check CommonJS requires
58
+ for (const stmt of sourceFile.getVariableStatements()) {
59
+ for (const dec of stmt.getDeclarations()) {
60
+ const init = dec.getInitializer();
61
+ if (init && init.getKind() === SyntaxKind.CallExpression && init.getExpression().getText() === "require") {
62
+ const args = init.getArguments();
63
+ if (args.length > 0 && (args[0].getText() === "'mongoose'" || args[0].getText() === '"mongoose"')) {
64
+ const nameNode = dec.getNameNode();
65
+ if (nameNode.getKind() === SyntaxKind.Identifier) {
66
+ aliases.add(`${nameNode.getText()}.Schema`);
67
+ } else if (nameNode.getKind() === SyntaxKind.ObjectBindingPattern) {
68
+ for (const el of nameNode.getElements()) {
69
+ if (el.getPropertyNameNode() && el.getPropertyNameNode().getText() === "Schema") {
70
+ aliases.add(el.getNameNode().getText());
71
+ } else if (!el.getPropertyNameNode() && el.getNameNode().getText() === "Schema") {
72
+ aliases.add("Schema");
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ return aliases;
82
+ }
83
+
32
84
  function inferModelName(sourceFile, node) {
33
- // Try to find the variable name it's assigned to (e.g., const userSchema = new Schema(...))
85
+ // 1. Try to find if it's passed directly to mongoose.model('Name', new Schema(...)) or similar
86
+ const parentCall = node.getFirstAncestorByKind(SyntaxKind.CallExpression);
87
+ if (parentCall) {
88
+ const exprText = parentCall.getExpression().getText();
89
+ if (exprText.includes("mongoose.model") || exprText.includes(".model")) {
90
+ const args = parentCall.getArguments();
91
+ if (args.length > 0) {
92
+ const name = resolveStringValue(args[0]);
93
+ if (name) return name;
94
+ }
95
+ }
96
+ }
97
+
98
+ // 2. Try to find the variable name it's assigned to
34
99
  const varDec = node.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
35
100
  if (varDec) {
36
- let name = varDec.getName();
37
- // Remove 'Schema' suffix if present
38
- return name.replace(/Schema$/i, "");
101
+ const varName = varDec.getName();
102
+
103
+ // Look for model registered somewhere else in the same file: mongoose.model('ModelName', varName)
104
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
105
+ for (const call of calls) {
106
+ const exprText = call.getExpression().getText();
107
+ if (exprText.includes("model")) {
108
+ const args = call.getArguments();
109
+ if (args.length >= 2 && args[1].getText() === varName) {
110
+ const name = resolveStringValue(args[0]);
111
+ if (name) return name;
112
+ }
113
+ }
114
+ }
115
+
116
+ // Remove 'Schema' suffix if present as a fallback
117
+ return varName.replace(/Schema$/i, "");
39
118
  }
40
119
 
41
- // Check if it's exported as a model: mongoose.model('User', userSchema)
42
- const sourceFileText = sourceFile.getText();
43
- const modelMatch = sourceFileText.match(/mongoose\.model\(['"]([^'"]+)['"]/);
44
- if (modelMatch) {
45
- return modelMatch[1];
46
- }
47
-
48
120
  // Fallback to filename
49
121
  return sourceFile.getBaseNameWithoutExtension();
50
122
  }
51
123
 
124
+ function resolveStringValue(node) {
125
+ if (node.getKind() === SyntaxKind.StringLiteral || node.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
126
+ return node.getLiteralText ? node.getLiteralText() : node.getText().replace(/['"]/g, "");
127
+ }
128
+
129
+ // Advanced: Try standard TS resolution for constants (e.g., MODEL_NAMES.USER)
130
+ try {
131
+ const symbol = node.getSymbol();
132
+ if (symbol) {
133
+ const declarations = symbol.getDeclarations();
134
+ if (declarations && declarations.length > 0) {
135
+ const decl = declarations[0];
136
+ if (decl.getKind() === SyntaxKind.PropertyAssignment || decl.getKind() === SyntaxKind.VariableDeclaration) {
137
+ const init = decl.getInitializer();
138
+ if (init && (init.getKind() === SyntaxKind.StringLiteral || init.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral)) {
139
+ return init.getLiteralText ? init.getLiteralText() : init.getText().replace(/['"]/g, "");
140
+ }
141
+ }
142
+ if (decl.getKind() === SyntaxKind.EnumMember) {
143
+ const val = decl.getValue();
144
+ if (typeof val === 'string') return val;
145
+ }
146
+ }
147
+ }
148
+ } catch(e) {
149
+ // Ignore resolution errors if ts-morph typechecker is unavailable
150
+ }
151
+
152
+ return null;
153
+ }
154
+
52
155
  function parseSchemaObject(obj) {
53
156
  const properties = {
54
157
  _id: { type: "string", example: "60d0fe4f5311236168a109ca" }
@@ -86,6 +189,15 @@ function parseFieldInfo(node, fieldName = "") {
86
189
  type = mapMongooseType(text);
87
190
  } else if (node.getKind() === SyntaxKind.ObjectLiteralExpression) {
88
191
  const typeProp = node.getProperty("type");
192
+
193
+ // Recursive feature: If there is no 'type' property, treat as a nested subdocument schema
194
+ if (!typeProp) {
195
+ return {
196
+ schema: parseSchemaObject(node),
197
+ required: false
198
+ };
199
+ }
200
+
89
201
  if (typeProp && typeProp.getKind() === SyntaxKind.PropertyAssignment) {
90
202
  const typeInit = typeProp.getInitializer();
91
203
  if (typeInit) type = mapMongooseType(typeInit.getText());