next-openapi-gen 0.8.3 → 0.8.5
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 +28 -0
- package/dist/lib/utils.js +2 -0
- package/dist/lib/zod-converter.js +485 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -738,6 +738,34 @@ type User = z.infer<typeof UserSchema>;
|
|
|
738
738
|
// The library will be able to recognize this schema by reference `UserSchema` or `User` type.
|
|
739
739
|
```
|
|
740
740
|
|
|
741
|
+
### Factory Functions (Schema Generators)
|
|
742
|
+
|
|
743
|
+
The library automatically detects and expands Zod factory functions - any function that returns a Zod schema:
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
// Define reusable schema factory
|
|
747
|
+
export function createPaginatedSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
|
748
|
+
return z.object({
|
|
749
|
+
data: z.array(dataSchema).describe("Array of items"),
|
|
750
|
+
pagination: z.object({
|
|
751
|
+
nextCursor: z.string().nullable(),
|
|
752
|
+
hasMore: z.boolean(),
|
|
753
|
+
limit: z.number().int().positive(),
|
|
754
|
+
}),
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Use in your schemas - automatically expanded in OpenAPI
|
|
759
|
+
export const PaginatedUsersSchema = createPaginatedSchema(UserSchema);
|
|
760
|
+
export const PaginatedProductsSchema = createPaginatedSchema(ProductSchema);
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Factory functions work with any naming convention and support:
|
|
764
|
+
- Generic type parameters
|
|
765
|
+
- Inline schemas as arguments
|
|
766
|
+
- Imported schemas
|
|
767
|
+
- Multiple factory patterns in the same project
|
|
768
|
+
|
|
741
769
|
### Drizzle-Zod Support
|
|
742
770
|
|
|
743
771
|
The library fully supports **drizzle-zod** for generating Zod schemas from Drizzle ORM table definitions. This provides a single source of truth for your database schema, validation, and API documentation.
|
package/dist/lib/utils.js
CHANGED
|
@@ -17,6 +17,14 @@ export class ZodSchemaConverter {
|
|
|
17
17
|
processedModules = new Set();
|
|
18
18
|
typeToSchemaMapping = {};
|
|
19
19
|
drizzleZodImports = new Set();
|
|
20
|
+
factoryCache = new Map(); // Cache for analyzed factory functions
|
|
21
|
+
factoryCheckCache = new Map(); // Cache for non-factory functions
|
|
22
|
+
fileASTCache = new Map(); // Cache for parsed files
|
|
23
|
+
fileImportsCache = new Map(); // Cache for file imports
|
|
24
|
+
// Current processing context (set during file processing)
|
|
25
|
+
currentFilePath;
|
|
26
|
+
currentAST;
|
|
27
|
+
currentImports;
|
|
20
28
|
constructor(schemaDir) {
|
|
21
29
|
this.schemaDir = path.resolve(schemaDir);
|
|
22
30
|
}
|
|
@@ -151,32 +159,47 @@ export class ZodSchemaConverter {
|
|
|
151
159
|
}
|
|
152
160
|
// Parse the file
|
|
153
161
|
const ast = parseTypeScriptFile(content);
|
|
162
|
+
// Cache AST for later use
|
|
163
|
+
this.fileASTCache.set(filePath, ast);
|
|
154
164
|
// Create a map to store imported modules
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
let importedModules = {};
|
|
166
|
+
// Check if we have cached imports
|
|
167
|
+
if (this.fileImportsCache.has(filePath)) {
|
|
168
|
+
importedModules = this.fileImportsCache.get(filePath);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Build imports cache
|
|
172
|
+
traverse(ast, {
|
|
173
|
+
ImportDeclaration: (path) => {
|
|
174
|
+
const source = path.node.source.value;
|
|
175
|
+
// Track drizzle-zod imports
|
|
176
|
+
if (source === "drizzle-zod") {
|
|
177
|
+
path.node.specifiers.forEach((specifier) => {
|
|
178
|
+
if (t.isImportSpecifier(specifier) ||
|
|
179
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
180
|
+
this.drizzleZodImports.add(specifier.local.name);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Process each import specifier
|
|
164
185
|
path.node.specifiers.forEach((specifier) => {
|
|
165
186
|
if (t.isImportSpecifier(specifier) ||
|
|
166
187
|
t.isImportDefaultSpecifier(specifier)) {
|
|
167
|
-
|
|
188
|
+
const importedName = specifier.local.name;
|
|
189
|
+
importedModules[importedName] = source;
|
|
168
190
|
}
|
|
169
191
|
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// Cache imports for this file
|
|
195
|
+
this.fileImportsCache.set(filePath, importedModules);
|
|
196
|
+
}
|
|
197
|
+
// Set current processing context for use by processZodNode during factory expansion
|
|
198
|
+
this.currentFilePath = filePath;
|
|
199
|
+
this.currentAST = ast;
|
|
200
|
+
this.currentImports = importedModules;
|
|
201
|
+
// Look for all exported Zod schemas
|
|
202
|
+
traverse(ast, {
|
|
180
203
|
// For export const SchemaName = z.object({...})
|
|
181
204
|
ExportNamedDeclaration: (path) => {
|
|
182
205
|
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
@@ -213,6 +236,27 @@ export class ZodSchemaConverter {
|
|
|
213
236
|
this.zodSchemas[schemaName] = schema;
|
|
214
237
|
}
|
|
215
238
|
}
|
|
239
|
+
// Check if this is a factory function call
|
|
240
|
+
else if (t.isCallExpression(declaration.init) &&
|
|
241
|
+
t.isIdentifier(declaration.init.callee)) {
|
|
242
|
+
const factoryName = declaration.init.callee.name;
|
|
243
|
+
logger.debug(`[Schema] Detected potential factory function call: ${factoryName} for schema ${schemaName}`);
|
|
244
|
+
const factoryNode = this.findFactoryFunction(factoryName, filePath, ast, importedModules);
|
|
245
|
+
if (factoryNode) {
|
|
246
|
+
logger.debug(`[Schema] Found factory function, attempting to expand...`);
|
|
247
|
+
const schema = this.expandFactoryCall(factoryNode, declaration.init, filePath);
|
|
248
|
+
if (schema) {
|
|
249
|
+
this.zodSchemas[schemaName] = schema;
|
|
250
|
+
logger.debug(`[Schema] Successfully expanded factory function '${factoryName}' for schema '${schemaName}'`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
logger.debug(`[Schema] Failed to expand factory function '${factoryName}'`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
logger.debug(`[Schema] Could not find factory function '${factoryName}'`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
216
260
|
}
|
|
217
261
|
});
|
|
218
262
|
}
|
|
@@ -632,6 +676,30 @@ export class ZodSchemaConverter {
|
|
|
632
676
|
node.arguments.length > 0) {
|
|
633
677
|
return this.processZodLazy(node);
|
|
634
678
|
}
|
|
679
|
+
// Handle potential factory function calls (e.g., createPaginatedSchema(UserSchema))
|
|
680
|
+
// This must be checked before falling back to "Unknown Zod schema node"
|
|
681
|
+
if (t.isCallExpression(node) &&
|
|
682
|
+
t.isIdentifier(node.callee)) {
|
|
683
|
+
logger.debug(`[processZodNode] Attempting to handle potential factory function: ${node.callee.name}`);
|
|
684
|
+
// We need the current file context - try to get it from the processing context
|
|
685
|
+
// Note: This is a limitation - we may not have file context during preprocessing
|
|
686
|
+
// In that case, we'll return a placeholder and let the main processing handle it
|
|
687
|
+
const currentFilePath = this.currentFilePath;
|
|
688
|
+
const currentAST = this.currentAST;
|
|
689
|
+
const importedModules = this.currentImports;
|
|
690
|
+
if (currentFilePath && currentAST && importedModules) {
|
|
691
|
+
const factoryNode = this.findFactoryFunction(node.callee.name, currentFilePath, currentAST, importedModules);
|
|
692
|
+
if (factoryNode) {
|
|
693
|
+
logger.debug(`[processZodNode] Found factory function, expanding...`);
|
|
694
|
+
const schema = this.expandFactoryCall(factoryNode, node, currentFilePath);
|
|
695
|
+
if (schema) {
|
|
696
|
+
logger.debug(`[processZodNode] Successfully expanded factory function '${node.callee.name}'`);
|
|
697
|
+
return schema;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
logger.debug(`[processZodNode] Could not expand factory function '${node.callee.name}' - missing context or not a factory`);
|
|
702
|
+
}
|
|
635
703
|
logger.debug("Unknown Zod schema node:", node);
|
|
636
704
|
return { type: "object" };
|
|
637
705
|
}
|
|
@@ -1497,7 +1565,11 @@ export class ZodSchemaConverter {
|
|
|
1497
1565
|
try {
|
|
1498
1566
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
1499
1567
|
const ast = parseTypeScriptFile(content);
|
|
1500
|
-
//
|
|
1568
|
+
// Cache AST for later use
|
|
1569
|
+
this.fileASTCache.set(filePath, ast);
|
|
1570
|
+
// Collect imports to enable factory function resolution during preprocessing
|
|
1571
|
+
let importedModules = {};
|
|
1572
|
+
// First, collect all drizzle-zod imports and regular imports
|
|
1501
1573
|
traverse(ast, {
|
|
1502
1574
|
ImportDeclaration: (path) => {
|
|
1503
1575
|
const source = path.node.source.value;
|
|
@@ -1509,8 +1581,22 @@ export class ZodSchemaConverter {
|
|
|
1509
1581
|
}
|
|
1510
1582
|
});
|
|
1511
1583
|
}
|
|
1584
|
+
// Track all imports for factory function resolution
|
|
1585
|
+
path.node.specifiers.forEach((specifier) => {
|
|
1586
|
+
if (t.isImportSpecifier(specifier) ||
|
|
1587
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
1588
|
+
const importedName = specifier.local.name;
|
|
1589
|
+
importedModules[importedName] = source;
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1512
1592
|
},
|
|
1513
1593
|
});
|
|
1594
|
+
// Cache imports for this file
|
|
1595
|
+
this.fileImportsCache.set(filePath, importedModules);
|
|
1596
|
+
// Set current processing context for factory function expansion
|
|
1597
|
+
this.currentFilePath = filePath;
|
|
1598
|
+
this.currentAST = ast;
|
|
1599
|
+
this.currentImports = importedModules;
|
|
1514
1600
|
// Collect all exported Zod schemas
|
|
1515
1601
|
traverse(ast, {
|
|
1516
1602
|
ExportNamedDeclaration: (path) => {
|
|
@@ -1580,7 +1666,385 @@ export class ZodSchemaConverter {
|
|
|
1580
1666
|
t.isCallExpression(node.callee.object)) {
|
|
1581
1667
|
return this.isZodSchema(node.callee.object);
|
|
1582
1668
|
}
|
|
1669
|
+
// Do NOT treat unknown function calls as potential Zod schemas here
|
|
1670
|
+
// Factory functions will be detected and handled in processZodNode() instead
|
|
1671
|
+
// This prevents false positives during preprocessing
|
|
1583
1672
|
}
|
|
1584
1673
|
return false;
|
|
1585
1674
|
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Find a factory function by name (lazy detection with caching)
|
|
1677
|
+
* @param functionName - Name of the function to find
|
|
1678
|
+
* @param currentFilePath - Path of the current file being processed
|
|
1679
|
+
* @param currentAST - Already parsed AST of current file
|
|
1680
|
+
* @param importedModules - Map of imported module names to their sources
|
|
1681
|
+
* @returns Factory function node if found and returns Zod schema, null otherwise
|
|
1682
|
+
*/
|
|
1683
|
+
findFactoryFunction(functionName, currentFilePath, currentAST, importedModules) {
|
|
1684
|
+
// Check positive cache first
|
|
1685
|
+
if (this.factoryCache.has(functionName)) {
|
|
1686
|
+
logger.debug(`[Factory] Cache hit for function '${functionName}'`);
|
|
1687
|
+
return this.factoryCache.get(functionName);
|
|
1688
|
+
}
|
|
1689
|
+
// Check negative cache (already checked, not a factory)
|
|
1690
|
+
if (this.factoryCheckCache.has(functionName)) {
|
|
1691
|
+
logger.debug(`[Factory] Negative cache hit for function '${functionName}'`);
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
logger.debug(`[Factory] Searching for function '${functionName}'`);
|
|
1695
|
+
// Look in current file first (AST already parsed)
|
|
1696
|
+
const localFactory = this.findFunctionInAST(currentAST, functionName);
|
|
1697
|
+
if (localFactory && this.returnsZodSchema(localFactory)) {
|
|
1698
|
+
logger.debug(`[Factory] Found Zod factory function '${functionName}' in current file`);
|
|
1699
|
+
this.factoryCache.set(functionName, localFactory);
|
|
1700
|
+
return localFactory;
|
|
1701
|
+
}
|
|
1702
|
+
// Check if function is imported
|
|
1703
|
+
const importSource = importedModules[functionName];
|
|
1704
|
+
if (importSource) {
|
|
1705
|
+
logger.debug(`[Factory] Function '${functionName}' is imported from '${importSource}'`);
|
|
1706
|
+
// Resolve import path
|
|
1707
|
+
const importedFilePath = this.resolveImportPath(currentFilePath, importSource);
|
|
1708
|
+
if (importedFilePath && fs.existsSync(importedFilePath)) {
|
|
1709
|
+
logger.debug(`[Factory] Resolved import to: ${importedFilePath}`);
|
|
1710
|
+
// Parse imported file (with caching) - this will also cache imports
|
|
1711
|
+
const importedAST = this.parseFileWithCache(importedFilePath);
|
|
1712
|
+
if (importedAST) {
|
|
1713
|
+
const importedFactory = this.findFunctionInAST(importedAST, functionName);
|
|
1714
|
+
if (importedFactory && this.returnsZodSchema(importedFactory)) {
|
|
1715
|
+
logger.debug(`[Factory] Found Zod factory function '${functionName}' in imported file`);
|
|
1716
|
+
this.factoryCache.set(functionName, importedFactory);
|
|
1717
|
+
return importedFactory;
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
logger.debug(`[Factory] Function '${functionName}' found in imported file but does not return Zod schema`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
logger.debug(`[Factory] Could not resolve import path for '${importSource}'`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
logger.debug(`[Factory] Function '${functionName}' is not imported`);
|
|
1730
|
+
}
|
|
1731
|
+
// Not found or not a Zod factory - cache negative result
|
|
1732
|
+
logger.debug(`[Factory] Function '${functionName}' is not a Zod factory`);
|
|
1733
|
+
this.factoryCheckCache.set(functionName, false);
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Find a function definition in an AST
|
|
1738
|
+
*/
|
|
1739
|
+
findFunctionInAST(ast, functionName) {
|
|
1740
|
+
let foundFunction = null;
|
|
1741
|
+
traverse(ast, {
|
|
1742
|
+
// Handle: export function createSchema() { ... }
|
|
1743
|
+
FunctionDeclaration: (path) => {
|
|
1744
|
+
if (t.isIdentifier(path.node.id) && path.node.id.name === functionName) {
|
|
1745
|
+
foundFunction = path.node;
|
|
1746
|
+
path.stop();
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
// Handle: export const createSchema = (...) => { ... }
|
|
1750
|
+
VariableDeclarator: (path) => {
|
|
1751
|
+
if (t.isIdentifier(path.node.id) &&
|
|
1752
|
+
path.node.id.name === functionName &&
|
|
1753
|
+
(t.isArrowFunctionExpression(path.node.init) ||
|
|
1754
|
+
t.isFunctionExpression(path.node.init))) {
|
|
1755
|
+
foundFunction = path.node.init;
|
|
1756
|
+
path.stop();
|
|
1757
|
+
}
|
|
1758
|
+
},
|
|
1759
|
+
});
|
|
1760
|
+
return foundFunction;
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Check if a function returns a Zod schema by analyzing return statements
|
|
1764
|
+
*/
|
|
1765
|
+
returnsZodSchema(functionNode) {
|
|
1766
|
+
if (!t.isFunctionDeclaration(functionNode) &&
|
|
1767
|
+
!t.isArrowFunctionExpression(functionNode) &&
|
|
1768
|
+
!t.isFunctionExpression(functionNode)) {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
let returnsZod = false;
|
|
1772
|
+
// For arrow functions with direct return (no block)
|
|
1773
|
+
if (t.isArrowFunctionExpression(functionNode) &&
|
|
1774
|
+
!t.isBlockStatement(functionNode.body)) {
|
|
1775
|
+
returnsZod = this.isZodSchema(functionNode.body);
|
|
1776
|
+
logger.debug(`[Factory] Arrow function direct return, isZodSchema: ${returnsZod}`);
|
|
1777
|
+
return returnsZod;
|
|
1778
|
+
}
|
|
1779
|
+
// For functions with block statements, analyze return statements manually
|
|
1780
|
+
const body = functionNode.body;
|
|
1781
|
+
if (!t.isBlockStatement(body)) {
|
|
1782
|
+
return false;
|
|
1783
|
+
}
|
|
1784
|
+
// Manually walk through statements instead of using traverse
|
|
1785
|
+
const checkStatements = (statements) => {
|
|
1786
|
+
for (const stmt of statements) {
|
|
1787
|
+
if (t.isReturnStatement(stmt) && stmt.argument) {
|
|
1788
|
+
if (this.isZodSchema(stmt.argument)) {
|
|
1789
|
+
logger.debug(`[Factory] Found Zod schema in return statement`);
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
// Check nested blocks (if statements, etc.)
|
|
1794
|
+
else if (t.isIfStatement(stmt)) {
|
|
1795
|
+
if (t.isBlockStatement(stmt.consequent)) {
|
|
1796
|
+
if (checkStatements(stmt.consequent.body))
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
|
|
1800
|
+
if (this.isZodSchema(stmt.consequent.argument))
|
|
1801
|
+
return true;
|
|
1802
|
+
}
|
|
1803
|
+
if (stmt.alternate) {
|
|
1804
|
+
if (t.isBlockStatement(stmt.alternate)) {
|
|
1805
|
+
if (checkStatements(stmt.alternate.body))
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
|
|
1809
|
+
if (this.isZodSchema(stmt.alternate.argument))
|
|
1810
|
+
return true;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return false;
|
|
1816
|
+
};
|
|
1817
|
+
returnsZod = checkStatements(body.body);
|
|
1818
|
+
return returnsZod;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Parse a file with caching (also caches imports)
|
|
1822
|
+
*/
|
|
1823
|
+
parseFileWithCache(filePath) {
|
|
1824
|
+
if (this.fileASTCache.has(filePath)) {
|
|
1825
|
+
return this.fileASTCache.get(filePath);
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1829
|
+
const ast = parseTypeScriptFile(content);
|
|
1830
|
+
this.fileASTCache.set(filePath, ast);
|
|
1831
|
+
// Also build and cache imports for this file
|
|
1832
|
+
if (!this.fileImportsCache.has(filePath)) {
|
|
1833
|
+
const importedModules = {};
|
|
1834
|
+
traverse(ast, {
|
|
1835
|
+
ImportDeclaration: (path) => {
|
|
1836
|
+
const source = path.node.source.value;
|
|
1837
|
+
// Track drizzle-zod imports
|
|
1838
|
+
if (source === "drizzle-zod") {
|
|
1839
|
+
path.node.specifiers.forEach((specifier) => {
|
|
1840
|
+
if (t.isImportSpecifier(specifier) ||
|
|
1841
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
1842
|
+
this.drizzleZodImports.add(specifier.local.name);
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
// Process each import specifier
|
|
1847
|
+
path.node.specifiers.forEach((specifier) => {
|
|
1848
|
+
if (t.isImportSpecifier(specifier) ||
|
|
1849
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
1850
|
+
const importedName = specifier.local.name;
|
|
1851
|
+
importedModules[importedName] = source;
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
},
|
|
1855
|
+
});
|
|
1856
|
+
this.fileImportsCache.set(filePath, importedModules);
|
|
1857
|
+
}
|
|
1858
|
+
return ast;
|
|
1859
|
+
}
|
|
1860
|
+
catch (error) {
|
|
1861
|
+
logger.error(`[Factory] Error parsing file '${filePath}': ${error}`);
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Resolve import path relative to current file
|
|
1867
|
+
*/
|
|
1868
|
+
resolveImportPath(currentFilePath, importSource) {
|
|
1869
|
+
// Handle relative imports
|
|
1870
|
+
if (importSource.startsWith(".")) {
|
|
1871
|
+
const currentDir = path.dirname(currentFilePath);
|
|
1872
|
+
let resolvedPath = path.resolve(currentDir, importSource);
|
|
1873
|
+
// Try adding extensions if not present
|
|
1874
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1875
|
+
if (!path.extname(resolvedPath)) {
|
|
1876
|
+
for (const ext of extensions) {
|
|
1877
|
+
const withExt = resolvedPath + ext;
|
|
1878
|
+
if (fs.existsSync(withExt)) {
|
|
1879
|
+
return withExt;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
// Try index files
|
|
1883
|
+
for (const ext of extensions) {
|
|
1884
|
+
const indexPath = path.join(resolvedPath, `index${ext}`);
|
|
1885
|
+
if (fs.existsSync(indexPath)) {
|
|
1886
|
+
return indexPath;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
else if (fs.existsSync(resolvedPath)) {
|
|
1891
|
+
return resolvedPath;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
// Handle absolute imports from schemaDir
|
|
1895
|
+
// This is a simplified approach - you might need to enhance this based on tsconfig paths
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Expand a factory function call by substituting arguments
|
|
1900
|
+
*/
|
|
1901
|
+
expandFactoryCall(factoryNode, callNode, filePath) {
|
|
1902
|
+
if (!t.isFunctionDeclaration(factoryNode) &&
|
|
1903
|
+
!t.isArrowFunctionExpression(factoryNode) &&
|
|
1904
|
+
!t.isFunctionExpression(factoryNode)) {
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
logger.debug(`[Factory] Expanding factory call with ${callNode.arguments.length} arguments`);
|
|
1908
|
+
// Build parameter -> argument mapping
|
|
1909
|
+
const paramMap = new Map();
|
|
1910
|
+
const params = factoryNode.params;
|
|
1911
|
+
for (let i = 0; i < params.length && i < callNode.arguments.length; i++) {
|
|
1912
|
+
const param = params[i];
|
|
1913
|
+
const arg = callNode.arguments[i];
|
|
1914
|
+
if (t.isIdentifier(param)) {
|
|
1915
|
+
paramMap.set(param.name, arg);
|
|
1916
|
+
logger.debug(`[Factory] Mapped parameter '${param.name}' to argument`);
|
|
1917
|
+
}
|
|
1918
|
+
else if (t.isObjectPattern(param)) {
|
|
1919
|
+
// Handle destructured parameters - simplified for now
|
|
1920
|
+
logger.debug(`[Factory] Skipping destructured parameter (not yet supported)`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
// Extract return statement
|
|
1924
|
+
const returnNode = this.extractReturnNode(factoryNode);
|
|
1925
|
+
if (!returnNode) {
|
|
1926
|
+
logger.debug(`[Factory] No return statement found in factory`);
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
logger.debug(`[Factory] Return node type: ${returnNode.type}`);
|
|
1930
|
+
// Clone and substitute parameters in return node
|
|
1931
|
+
const substitutedNode = this.substituteParameters(returnNode, paramMap, filePath);
|
|
1932
|
+
logger.debug(`[Factory] Substituted node type: ${substitutedNode.type}`);
|
|
1933
|
+
// Process the substituted node as a normal Zod schema
|
|
1934
|
+
const result = this.processZodNode(substitutedNode);
|
|
1935
|
+
if (result) {
|
|
1936
|
+
logger.debug(`[Factory] Successfully processed substituted node, result has ${Object.keys(result).length} keys`);
|
|
1937
|
+
}
|
|
1938
|
+
else {
|
|
1939
|
+
logger.debug(`[Factory] Failed to process substituted node`);
|
|
1940
|
+
}
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Extract the return node from a function
|
|
1945
|
+
*/
|
|
1946
|
+
extractReturnNode(functionNode) {
|
|
1947
|
+
// For arrow functions with direct return (no block)
|
|
1948
|
+
if (t.isArrowFunctionExpression(functionNode) &&
|
|
1949
|
+
!t.isBlockStatement(functionNode.body)) {
|
|
1950
|
+
return functionNode.body;
|
|
1951
|
+
}
|
|
1952
|
+
// For functions with block statements
|
|
1953
|
+
const body = t.isFunctionDeclaration(functionNode) ||
|
|
1954
|
+
t.isArrowFunctionExpression(functionNode) ||
|
|
1955
|
+
t.isFunctionExpression(functionNode)
|
|
1956
|
+
? functionNode.body
|
|
1957
|
+
: null;
|
|
1958
|
+
if (!body || !t.isBlockStatement(body)) {
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
// Find first return statement manually
|
|
1962
|
+
const findReturn = (statements) => {
|
|
1963
|
+
for (const stmt of statements) {
|
|
1964
|
+
if (t.isReturnStatement(stmt) && stmt.argument) {
|
|
1965
|
+
return stmt.argument;
|
|
1966
|
+
}
|
|
1967
|
+
// Check nested blocks
|
|
1968
|
+
if (t.isIfStatement(stmt)) {
|
|
1969
|
+
if (t.isBlockStatement(stmt.consequent)) {
|
|
1970
|
+
const found = findReturn(stmt.consequent.body);
|
|
1971
|
+
if (found)
|
|
1972
|
+
return found;
|
|
1973
|
+
}
|
|
1974
|
+
else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
|
|
1975
|
+
return stmt.consequent.argument;
|
|
1976
|
+
}
|
|
1977
|
+
if (stmt.alternate) {
|
|
1978
|
+
if (t.isBlockStatement(stmt.alternate)) {
|
|
1979
|
+
const found = findReturn(stmt.alternate.body);
|
|
1980
|
+
if (found)
|
|
1981
|
+
return found;
|
|
1982
|
+
}
|
|
1983
|
+
else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
|
|
1984
|
+
return stmt.alternate.argument;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return null;
|
|
1990
|
+
};
|
|
1991
|
+
return findReturn(body.body);
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Substitute parameters with actual arguments in an AST node (deep clone and replace)
|
|
1995
|
+
*/
|
|
1996
|
+
substituteParameters(node, paramMap, filePath) {
|
|
1997
|
+
// Deep clone the node to avoid modifying the original
|
|
1998
|
+
const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ false);
|
|
1999
|
+
// Manual recursive substitution without traverse
|
|
2000
|
+
const substitute = (n) => {
|
|
2001
|
+
if (t.isIdentifier(n)) {
|
|
2002
|
+
// Replace if this is a parameter
|
|
2003
|
+
if (paramMap.has(n.name)) {
|
|
2004
|
+
const replacement = paramMap.get(n.name);
|
|
2005
|
+
return t.cloneNode(replacement, true, false);
|
|
2006
|
+
}
|
|
2007
|
+
return n;
|
|
2008
|
+
}
|
|
2009
|
+
// Handle CallExpression
|
|
2010
|
+
if (t.isCallExpression(n)) {
|
|
2011
|
+
return t.callExpression(substitute(n.callee), n.arguments.map((arg) => {
|
|
2012
|
+
if (t.isSpreadElement(arg)) {
|
|
2013
|
+
return t.spreadElement(substitute(arg.argument));
|
|
2014
|
+
}
|
|
2015
|
+
return substitute(arg);
|
|
2016
|
+
}));
|
|
2017
|
+
}
|
|
2018
|
+
// Handle MemberExpression
|
|
2019
|
+
if (t.isMemberExpression(n)) {
|
|
2020
|
+
return t.memberExpression(substitute(n.object), n.computed ? substitute(n.property) : n.property, n.computed);
|
|
2021
|
+
}
|
|
2022
|
+
// Handle ObjectExpression
|
|
2023
|
+
if (t.isObjectExpression(n)) {
|
|
2024
|
+
return t.objectExpression(n.properties.map((prop) => {
|
|
2025
|
+
if (t.isObjectProperty(prop)) {
|
|
2026
|
+
return t.objectProperty(prop.computed ? substitute(prop.key) : prop.key, substitute(prop.value), prop.computed, prop.shorthand);
|
|
2027
|
+
}
|
|
2028
|
+
if (t.isSpreadElement(prop)) {
|
|
2029
|
+
return t.spreadElement(substitute(prop.argument));
|
|
2030
|
+
}
|
|
2031
|
+
return prop;
|
|
2032
|
+
}));
|
|
2033
|
+
}
|
|
2034
|
+
// Handle ArrayExpression
|
|
2035
|
+
if (t.isArrayExpression(n)) {
|
|
2036
|
+
return t.arrayExpression(n.elements.map((elem) => {
|
|
2037
|
+
if (!elem)
|
|
2038
|
+
return null;
|
|
2039
|
+
if (t.isSpreadElement(elem)) {
|
|
2040
|
+
return t.spreadElement(substitute(elem.argument));
|
|
2041
|
+
}
|
|
2042
|
+
return substitute(elem);
|
|
2043
|
+
}));
|
|
2044
|
+
}
|
|
2045
|
+
// Return as-is for other node types
|
|
2046
|
+
return n;
|
|
2047
|
+
};
|
|
2048
|
+
return substitute(cloned);
|
|
2049
|
+
}
|
|
1586
2050
|
}
|
package/package.json
CHANGED