@flink-app/flink 0.14.3 → 2.0.0-alpha.100
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/CHANGELOG.md +1051 -0
- package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
- package/SIMPLE_AST_FEASIBILITY.md +570 -0
- package/bin/flink.ts +13 -2
- package/cli/build.ts +24 -44
- package/cli/clean.ts +13 -25
- package/cli/cli-utils.ts +190 -17
- package/cli/dev.ts +252 -0
- package/cli/loadEnvFiles.ts +116 -0
- package/cli/run.ts +45 -62
- package/dist/bin/flink.js +61 -2
- package/dist/cli/build.js +20 -25
- package/dist/cli/clean.js +12 -10
- package/dist/cli/cli-utils.d.ts +34 -3
- package/dist/cli/cli-utils.js +193 -12
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +279 -0
- package/dist/cli/loadEnvFiles.d.ts +30 -0
- package/dist/cli/loadEnvFiles.js +113 -0
- package/dist/cli/run.js +47 -46
- package/dist/src/DependencyTracker.d.ts +44 -0
- package/dist/src/DependencyTracker.js +239 -0
- package/dist/src/FlinkApp.d.ts +163 -10
- package/dist/src/FlinkApp.js +847 -184
- package/dist/src/FlinkContext.d.ts +41 -0
- package/dist/src/FlinkErrors.d.ts +19 -6
- package/dist/src/FlinkErrors.js +36 -42
- package/dist/src/FlinkHttpHandler.d.ts +219 -26
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/dist/src/FlinkJob.d.ts +10 -0
- package/dist/src/FlinkLog.d.ts +82 -18
- package/dist/src/FlinkLog.js +165 -13
- package/dist/src/FlinkLogFactory.d.ts +288 -0
- package/dist/src/FlinkLogFactory.js +619 -0
- package/dist/src/FlinkRepo.d.ts +10 -2
- package/dist/src/FlinkRepo.js +11 -1
- package/dist/src/FlinkRequestContext.d.ts +63 -0
- package/dist/src/FlinkRequestContext.js +74 -0
- package/dist/src/FlinkResponse.d.ts +6 -0
- package/dist/src/FlinkService.d.ts +38 -0
- package/dist/src/FlinkService.js +46 -0
- package/dist/src/LeaderElection.d.ts +45 -0
- package/dist/src/LeaderElection.js +269 -0
- package/dist/src/SchemaCache.d.ts +84 -0
- package/dist/src/SchemaCache.js +289 -0
- package/dist/src/TypeScriptCompiler.d.ts +161 -51
- package/dist/src/TypeScriptCompiler.js +1253 -617
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +760 -0
- package/dist/src/ai/ConversationAgent.d.ts +279 -0
- package/dist/src/ai/ConversationAgent.js +404 -0
- package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
- package/dist/src/ai/ConversationFlinkAgent.js +404 -0
- package/dist/src/ai/FlinkAgent.d.ts +690 -0
- package/dist/src/ai/FlinkAgent.js +729 -0
- package/dist/src/ai/FlinkTool.d.ts +135 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
- package/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/dist/src/ai/LLMAdapter.d.ts +148 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
- package/dist/src/ai/PersistentFlinkAgent.js +403 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
- package/dist/src/ai/SubAgentExecutor.js +223 -0
- package/dist/src/ai/ToolExecutor.d.ts +64 -0
- package/dist/src/ai/ToolExecutor.js +497 -0
- package/dist/src/ai/agentInstructions.d.ts +68 -0
- package/dist/src/ai/agentInstructions.js +286 -0
- package/dist/src/ai/index.d.ts +8 -0
- package/dist/src/ai/index.js +26 -0
- package/dist/src/ai/instructionFileLoader.d.ts +44 -0
- package/dist/src/ai/instructionFileLoader.js +179 -0
- package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +17 -0
- package/dist/src/loadPluginSchemas.d.ts +45 -0
- package/dist/src/loadPluginSchemas.js +143 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
- package/dist/src/schema-extraction/index.d.ts +2 -0
- package/dist/src/schema-extraction/index.js +20 -0
- package/dist/src/schema-extraction/types.d.ts +31 -0
- package/dist/src/schema-extraction/types.js +2 -0
- package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
- package/dist/src/utils/loadFlinkConfig.js +77 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
- package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
- package/dist/src/workers/WorkerPool.d.ts +60 -0
- package/dist/src/workers/WorkerPool.js +306 -0
- package/examples/logging-hierarchical-example.ts +125 -0
- package/package.json +29 -4
- package/readme.md +499 -0
- package/spec/AgentDescendantDetection.spec.ts +335 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentObserver.spec.ts +266 -0
- package/spec/AgentRunner.spec.ts +1062 -0
- package/spec/AsyncLocalStorageContext.spec.ts +223 -0
- package/spec/ConversationHooks.spec.ts +257 -0
- package/spec/FlinkAgent.spec.ts +681 -0
- package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
- package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/spec/FlinkJob.spec.ts +171 -0
- package/spec/FlinkLogFactory.spec.ts +337 -0
- package/spec/FlinkRepo.spec.ts +1 -1
- package/spec/LeaderElection.spec.ts +174 -0
- package/spec/StreamingIntegration.spec.ts +139 -0
- package/spec/ToolExecutor.spec.ts +465 -0
- package/spec/TypeScriptCompiler.spec.ts +1 -1
- package/spec/TypeScriptSourceParser.spec.ts +1215 -0
- package/spec/TypeScriptTokenizer.spec.ts +366 -0
- package/spec/ai/ContextCompaction.spec.ts +405 -0
- package/spec/ai/ConversationAgent.spec.ts +520 -0
- package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
- package/spec/ai/agentInstructions.spec.ts +358 -0
- package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
- package/spec/fixtures/agent-instructions/simple.md +3 -0
- package/spec/fixtures/agent-instructions/template.md +18 -0
- package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
- package/spec/mock-project/dist/.tsbuildinfo +1 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +119 -0
- package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
- package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
- package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
- package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
- package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
- package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
- package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +52 -76
- package/spec/mock-project/dist/src/index.js.map +1 -0
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
- package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/Car.js +3 -1
- package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/schema-generation-nested-objects.spec.ts +97 -0
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +78 -0
- package/spec/utils.spec.ts +13 -13
- package/src/DependencyTracker.ts +166 -0
- package/src/FlinkApp.ts +919 -155
- package/src/FlinkContext.ts +43 -0
- package/src/FlinkErrors.ts +32 -12
- package/src/FlinkHttpHandler.ts +246 -28
- package/src/FlinkJob.ts +11 -0
- package/src/FlinkLog.ts +119 -12
- package/src/FlinkLogFactory.ts +699 -0
- package/src/FlinkRepo.ts +10 -3
- package/src/FlinkRequestContext.ts +95 -0
- package/src/FlinkResponse.ts +6 -0
- package/src/FlinkService.ts +49 -0
- package/src/LeaderElection.ts +203 -0
- package/src/SchemaCache.ts +232 -0
- package/src/TypeScriptCompiler.ts +1347 -610
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +646 -0
- package/src/ai/ConversationAgent.ts +413 -0
- package/src/ai/FlinkAgent.ts +1069 -0
- package/src/ai/FlinkTool.ts +165 -0
- package/src/ai/InMemoryConversationAgent.ts +149 -0
- package/src/ai/LLMAdapter.ts +126 -0
- package/src/ai/ToolExecutor.ts +485 -0
- package/src/ai/agentInstructions.ts +245 -0
- package/src/ai/index.ts +8 -0
- package/src/ai/instructionFileLoader.ts +156 -0
- package/src/auth/FlinkAuthPlugin.ts +2 -1
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +14 -0
- package/src/loadPluginSchemas.ts +141 -0
- package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
- package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
- package/src/schema-extraction/index.ts +2 -0
- package/src/schema-extraction/types.ts +34 -0
- package/src/utils/loadFlinkConfig.ts +89 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +6 -1
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { FlinkToolTypeArgs, PropertyMetadata, SchemaTypeDetection, TypeDefinition } from "./types";
|
|
2
|
+
import { TypeScriptTokenizer } from "./TypeScriptTokenizer";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure utility class for parsing TypeScript source code as text.
|
|
6
|
+
* All methods are static and side-effect free for easy testing.
|
|
7
|
+
*
|
|
8
|
+
* This class provides fast, regex-based parsing without using ts-morph
|
|
9
|
+
* or the TypeScript compiler, making it suitable for performance-critical
|
|
10
|
+
* schema extraction.
|
|
11
|
+
*/
|
|
12
|
+
export class TypeScriptSourceParser {
|
|
13
|
+
/**
|
|
14
|
+
* Remove comments from source code to avoid false positives in schema detection.
|
|
15
|
+
* Handles both single-line (//) and multi-line (/* *\/) comments.
|
|
16
|
+
*/
|
|
17
|
+
private static removeComments(sourceText: string): string {
|
|
18
|
+
// Remove multi-line comments: /* ... */
|
|
19
|
+
let text = sourceText.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
20
|
+
// Remove single-line comments: // ...
|
|
21
|
+
text = text.replace(/\/\/.*$/gm, '');
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detect if a tool source file uses Zod or JSON schemas.
|
|
27
|
+
* If true, TypeScript schema extraction should be skipped.
|
|
28
|
+
*
|
|
29
|
+
* Note: Strips comments first to avoid false positives from commented-out schemas.
|
|
30
|
+
*/
|
|
31
|
+
static detectSchemaType(sourceText: string): SchemaTypeDetection {
|
|
32
|
+
// Strip comments to avoid matching commented-out schema definitions
|
|
33
|
+
const textWithoutComments = this.removeComments(sourceText);
|
|
34
|
+
|
|
35
|
+
const hasZodSchemas = textWithoutComments.includes('inputSchema:') || textWithoutComments.includes('outputSchema:');
|
|
36
|
+
const hasJsonSchemas = textWithoutComments.includes('inputJsonSchema:') || textWithoutComments.includes('outputJsonSchema:');
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
hasZodSchemas,
|
|
40
|
+
hasJsonSchemas,
|
|
41
|
+
shouldSkipTypeScriptExtraction: hasZodSchemas || hasJsonSchemas,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract type arguments from Handler<Ctx, Req, Res, Params, Query> declaration.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* parseHandlerTypeArgs('const handler: Handler<AppCtx, LoginReq, LoginRes>')
|
|
51
|
+
* // Returns: { contextType: 'AppCtx', reqType: 'LoginReq', resType: 'LoginRes' }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
static parseHandlerTypeArgs(sourceText: string): { contextType: string; reqType?: string; resType?: string; paramsType?: string; queryType?: string } | null {
|
|
55
|
+
// Try to find Handler< or GetHandler< or PostHandler< etc.
|
|
56
|
+
const handlerMatch = sourceText.match(/\b(Get|Post|Put|Patch|Delete)?Handler\s*</);
|
|
57
|
+
if (!handlerMatch) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handlerType = handlerMatch[0].replace(/\s*<$/, '');
|
|
62
|
+
const startIdx = sourceText.indexOf(handlerMatch[0]);
|
|
63
|
+
if (startIdx === -1) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Find the matching closing > by counting bracket depth
|
|
68
|
+
let depth = 0;
|
|
69
|
+
let endIdx = -1;
|
|
70
|
+
for (let i = startIdx + handlerMatch[0].length; i < sourceText.length; i++) {
|
|
71
|
+
if (sourceText[i] === '<') {
|
|
72
|
+
depth++;
|
|
73
|
+
} else if (sourceText[i] === '>') {
|
|
74
|
+
if (depth === 0) {
|
|
75
|
+
endIdx = i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
depth--;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (endIdx === -1) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const typeArgsText = sourceText.slice(startIdx + handlerMatch[0].length, endIdx);
|
|
87
|
+
const typeArgs = this.splitTopLevelCommas(typeArgsText);
|
|
88
|
+
|
|
89
|
+
if (typeArgs.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handler<Ctx, Req, Res, Params, Query>
|
|
94
|
+
// GetHandler<Ctx, Res, Params, Query> (no Req)
|
|
95
|
+
// PostHandler<Ctx, Req, Res, Params, Query>
|
|
96
|
+
// etc.
|
|
97
|
+
|
|
98
|
+
const isGetHandler = handlerType === 'GetHandler';
|
|
99
|
+
|
|
100
|
+
if (isGetHandler) {
|
|
101
|
+
// GetHandler<Ctx, Res, Params, Query>
|
|
102
|
+
return {
|
|
103
|
+
contextType: typeArgs[0]?.trim(),
|
|
104
|
+
resType: typeArgs[1]?.trim(),
|
|
105
|
+
paramsType: typeArgs[2]?.trim(),
|
|
106
|
+
queryType: typeArgs[3]?.trim(),
|
|
107
|
+
};
|
|
108
|
+
} else {
|
|
109
|
+
// Handler<Ctx, Req, Res, Params, Query>
|
|
110
|
+
return {
|
|
111
|
+
contextType: typeArgs[0]?.trim(),
|
|
112
|
+
reqType: typeArgs[1]?.trim(),
|
|
113
|
+
resType: typeArgs[2]?.trim(),
|
|
114
|
+
paramsType: typeArgs[3]?.trim(),
|
|
115
|
+
queryType: typeArgs[4]?.trim(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract type arguments from FlinkTool<Ctx, Input, Output> declaration.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* parseFlinkToolTypeArgs('const handler: FlinkTool<Ctx, CreateDocInput, ToolResult<DocOutput>>')
|
|
126
|
+
* // Returns: { contextType: 'Ctx', inputType: 'CreateDocInput', outputType: 'ToolResult<DocOutput>' }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
static parseFlinkToolTypeArgs(sourceText: string): FlinkToolTypeArgs | null {
|
|
130
|
+
// Find FlinkTool< and then manually parse to find the matching closing >
|
|
131
|
+
const startIdx = sourceText.indexOf('FlinkTool<');
|
|
132
|
+
if (startIdx === -1) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Find the matching closing > by counting bracket depth
|
|
137
|
+
let depth = 0;
|
|
138
|
+
let endIdx = -1;
|
|
139
|
+
for (let i = startIdx + 'FlinkTool<'.length; i < sourceText.length; i++) {
|
|
140
|
+
if (sourceText[i] === '<') {
|
|
141
|
+
depth++;
|
|
142
|
+
} else if (sourceText[i] === '>') {
|
|
143
|
+
if (depth === 0) {
|
|
144
|
+
endIdx = i;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
depth--;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (endIdx === -1) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Extract the content between the brackets
|
|
156
|
+
const typeArgsStr = sourceText.substring(startIdx + 'FlinkTool<'.length, endIdx);
|
|
157
|
+
|
|
158
|
+
// Parse the type arguments by splitting on commas at the top level (not inside angle brackets)
|
|
159
|
+
const typeArgs = this.splitTopLevelCommas(typeArgsStr);
|
|
160
|
+
|
|
161
|
+
if (typeArgs.length < 3) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
contextType: typeArgs[0].trim(),
|
|
167
|
+
inputType: typeArgs[1].trim(),
|
|
168
|
+
outputType: typeArgs[2].trim(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Split a string by commas, but only at the top level (not inside angle brackets).
|
|
174
|
+
* Helper for parsing generic type arguments like "Ctx, Input, ToolResult<Output>"
|
|
175
|
+
*/
|
|
176
|
+
private static splitTopLevelCommas(str: string): string[] {
|
|
177
|
+
const result: string[] = [];
|
|
178
|
+
let current = '';
|
|
179
|
+
let depth = 0;
|
|
180
|
+
|
|
181
|
+
for (const char of str) {
|
|
182
|
+
if (char === '<') {
|
|
183
|
+
depth++;
|
|
184
|
+
current += char;
|
|
185
|
+
} else if (char === '>') {
|
|
186
|
+
depth--;
|
|
187
|
+
current += char;
|
|
188
|
+
} else if (char === ',' && depth === 0) {
|
|
189
|
+
result.push(current);
|
|
190
|
+
current = '';
|
|
191
|
+
} else {
|
|
192
|
+
current += char;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (current) {
|
|
197
|
+
result.push(current);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if a type should generate a schema.
|
|
205
|
+
* Returns false for primitives and special types, true for named types and inline objects.
|
|
206
|
+
*/
|
|
207
|
+
static shouldGenerateSchema(typeName: string): boolean {
|
|
208
|
+
// Skip 'any' and 'unknown'
|
|
209
|
+
if (typeName === 'any' || typeName === 'unknown') {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Generate schemas for inline object types like {} or { foo: string }
|
|
214
|
+
if (typeName.startsWith('{')) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Skip primitive types
|
|
219
|
+
const primitives = ['string', 'number', 'boolean', 'void', 'undefined', 'null'];
|
|
220
|
+
if (primitives.includes(typeName.toLowerCase())) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Skip binary/non-serializable built-in types that can never have JSON schemas
|
|
225
|
+
const nonSerializableTypes = ['Buffer', 'ArrayBuffer', 'SharedArrayBuffer', 'Blob', 'ReadableStream', 'WritableStream', 'DataView', 'Uint8Array'];
|
|
226
|
+
if (nonSerializableTypes.includes(typeName)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Must be a named type (starts with uppercase letter)
|
|
231
|
+
return /^[A-Z]/.test(typeName);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if a type is a built-in TypeScript type.
|
|
236
|
+
*/
|
|
237
|
+
static isBuiltInType(typeName: string): boolean {
|
|
238
|
+
const builtIns = [
|
|
239
|
+
'String', 'Number', 'Boolean', 'Date', 'Array', 'Object', 'Record',
|
|
240
|
+
'Promise', 'Set', 'Map', 'Partial', 'Required', 'Pick', 'Omit',
|
|
241
|
+
'Readonly', 'ReadonlyArray'
|
|
242
|
+
];
|
|
243
|
+
return builtIns.includes(typeName);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Extract T from ToolResult<T> type name.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* unwrapToolResultType('ToolResult<DocOutput>') // Returns: 'DocOutput'
|
|
252
|
+
* unwrapToolResultType('DocOutput') // Returns: 'DocOutput'
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
static unwrapToolResultType(typeName: string): string {
|
|
256
|
+
const match = typeName.match(/ToolResult<(.+)>/);
|
|
257
|
+
if (match) {
|
|
258
|
+
return match[1].trim();
|
|
259
|
+
}
|
|
260
|
+
return typeName;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Convert inline object type to interface definition.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* convertInlineObjectToInterface('{}', 'MyInput')
|
|
269
|
+
* // Returns: 'export interface MyInput {}'
|
|
270
|
+
*
|
|
271
|
+
* convertInlineObjectToInterface('{ foo: string }', 'MyInput')
|
|
272
|
+
* // Returns: 'export interface MyInput {\n foo: string;\n}'
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
static convertInlineObjectToInterface(inlineType: string, interfaceName: string): string {
|
|
276
|
+
// For empty object, create empty interface
|
|
277
|
+
if (inlineType.trim() === '{}') {
|
|
278
|
+
return `export interface ${interfaceName} {}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// For non-empty inline object, convert to interface
|
|
282
|
+
// Remove outer braces and clean up
|
|
283
|
+
let body = inlineType.trim().slice(1, -1).trim();
|
|
284
|
+
|
|
285
|
+
// Ensure properties end with semicolons (convert commas to semicolons if needed)
|
|
286
|
+
body = body.replace(/,\s*$/g, ';').replace(/([^;])\s*$/g, '$1;');
|
|
287
|
+
|
|
288
|
+
return `export interface ${interfaceName} {\n ${body}\n}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Find an interface or type definition in source text.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* const source = `
|
|
297
|
+
* interface User {
|
|
298
|
+
* name: string;
|
|
299
|
+
* }
|
|
300
|
+
* `;
|
|
301
|
+
* findTypeDefinition(source, 'User')
|
|
302
|
+
* // Returns: { name: 'User', definition: 'interface User { ... }', kind: 'interface' }
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
static findTypeDefinition(sourceText: string, typeName: string): TypeDefinition | null {
|
|
306
|
+
// Skip if it's an inline type
|
|
307
|
+
if (typeName.startsWith('{')) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Try to find interface definition first
|
|
312
|
+
// Pattern: (optional JSDoc) interface Name (optional generics) (optional extends) { ... }
|
|
313
|
+
const interfacePattern = new RegExp(
|
|
314
|
+
`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)interface\\s+${typeName}\\s*`,
|
|
315
|
+
'gm'
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const interfaceMatch = interfacePattern.exec(sourceText);
|
|
319
|
+
if (interfaceMatch) {
|
|
320
|
+
const startIndex = interfaceMatch.index;
|
|
321
|
+
const jsDocAndInterface = interfaceMatch[0];
|
|
322
|
+
|
|
323
|
+
// Find the opening brace
|
|
324
|
+
const afterInterface = sourceText.slice(interfaceMatch.index + interfaceMatch[0].length);
|
|
325
|
+
const braceMatch = afterInterface.match(/^[^{]*\{/);
|
|
326
|
+
|
|
327
|
+
if (braceMatch) {
|
|
328
|
+
const braceStart = interfaceMatch.index + interfaceMatch[0].length + braceMatch[0].length - 1;
|
|
329
|
+
|
|
330
|
+
// Use tokenizer for robust bracket matching (handles strings and comments)
|
|
331
|
+
const tokenizer = new TypeScriptTokenizer(sourceText);
|
|
332
|
+
const braceEnd = tokenizer.findMatchingBracketAt(braceStart, '{');
|
|
333
|
+
|
|
334
|
+
if (braceEnd !== -1) {
|
|
335
|
+
const definition = sourceText.slice(startIndex, braceEnd + 1).trim();
|
|
336
|
+
return {
|
|
337
|
+
name: typeName,
|
|
338
|
+
definition,
|
|
339
|
+
kind: 'interface',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Match type alias: type Name = ...;
|
|
346
|
+
const typePattern = new RegExp(
|
|
347
|
+
`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)type\\s+${typeName}\\s*=\\s*`,
|
|
348
|
+
'gm'
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const typeMatch = typePattern.exec(sourceText);
|
|
352
|
+
if (typeMatch) {
|
|
353
|
+
const startIndex = typeMatch.index;
|
|
354
|
+
const afterEquals = typeMatch.index + typeMatch[0].length;
|
|
355
|
+
|
|
356
|
+
// Use tokenizer to find semicolon, properly handling strings and comments
|
|
357
|
+
const tokenizer = new TypeScriptTokenizer(sourceText);
|
|
358
|
+
let depth = 0;
|
|
359
|
+
let i = afterEquals;
|
|
360
|
+
|
|
361
|
+
while (i < sourceText.length) {
|
|
362
|
+
const token = tokenizer.getNextToken(i);
|
|
363
|
+
|
|
364
|
+
// Only process non-string, non-comment tokens
|
|
365
|
+
if (token.type !== 'string' && token.type !== 'comment') {
|
|
366
|
+
const char = token.value;
|
|
367
|
+
|
|
368
|
+
// Track depth
|
|
369
|
+
if (char === '{' || char === '[' || char === '(' || char === '<') {
|
|
370
|
+
depth++;
|
|
371
|
+
} else if (char === '}' || char === ']' || char === ')' || char === '>') {
|
|
372
|
+
depth--;
|
|
373
|
+
|
|
374
|
+
// Closing a bracket back to top level may terminate a
|
|
375
|
+
// semicolon-free type alias (e.g. `type Params = { ... }`
|
|
376
|
+
// with no trailing `;`). Only stop if the type does not
|
|
377
|
+
// continue with an operator such as `|` or `&`.
|
|
378
|
+
if (depth === 0 && !this.typeContinuesAfter(sourceText, token.endPos)) {
|
|
379
|
+
const definition = sourceText.slice(startIndex, token.endPos).trim();
|
|
380
|
+
return {
|
|
381
|
+
name: typeName,
|
|
382
|
+
definition,
|
|
383
|
+
kind: 'type',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// End of type at semicolon (at depth 0)
|
|
389
|
+
if (char === ';' && depth === 0) {
|
|
390
|
+
const definition = sourceText.slice(startIndex, i + 1).trim();
|
|
391
|
+
return {
|
|
392
|
+
name: typeName,
|
|
393
|
+
definition,
|
|
394
|
+
kind: 'type',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
i = token.endPos;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Peek at the next non-whitespace, non-comment character starting at `fromPos`.
|
|
408
|
+
* Returns the character, or undefined if only whitespace/comments remain.
|
|
409
|
+
*/
|
|
410
|
+
private static peekNextSignificantChar(sourceText: string, fromPos: number): string | undefined {
|
|
411
|
+
const tokenizer = new TypeScriptTokenizer(sourceText);
|
|
412
|
+
let i = fromPos;
|
|
413
|
+
|
|
414
|
+
while (i < sourceText.length) {
|
|
415
|
+
const token = tokenizer.getNextToken(i);
|
|
416
|
+
|
|
417
|
+
if (token.type === 'comment') {
|
|
418
|
+
i = token.endPos;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (token.type === 'string') {
|
|
423
|
+
return token.value[0];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!/\s/.test(token.value)) {
|
|
427
|
+
return token.value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
i = token.endPos;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Determine whether a type expression continues after `fromPos`, i.e. whether
|
|
438
|
+
* the character that follows is a type-combinator/continuation operator
|
|
439
|
+
* (`|`, `&`, `[`, `.`, etc.) rather than the start of an unrelated statement.
|
|
440
|
+
*
|
|
441
|
+
* Used to decide if a top-level closing bracket terminates a semicolon-free
|
|
442
|
+
* type alias or whether the alias keeps going (e.g. a union of object types).
|
|
443
|
+
*/
|
|
444
|
+
private static typeContinuesAfter(sourceText: string, fromPos: number): boolean {
|
|
445
|
+
const next = this.peekNextSignificantChar(sourceText, fromPos);
|
|
446
|
+
if (next === undefined) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
// Operators that mean the type expression continues across the boundary.
|
|
450
|
+
return '|&[.<'.includes(next);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Rename an interface or type definition.
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* renameTypeDefinition('interface User { name: string }', 'User', 'Person')
|
|
459
|
+
* // Returns: 'export interface Person { name: string }'
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
static renameTypeDefinition(definition: string, oldName: string, newName: string, exportIt = true): string {
|
|
463
|
+
const exportPrefix = exportIt ? 'export ' : '';
|
|
464
|
+
|
|
465
|
+
// Try interface rename
|
|
466
|
+
if (definition.includes('interface')) {
|
|
467
|
+
return definition.replace(
|
|
468
|
+
new RegExp(`interface\\s+${oldName}\\b`),
|
|
469
|
+
`${exportPrefix}interface ${newName}`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Try type alias rename
|
|
474
|
+
if (definition.includes('type')) {
|
|
475
|
+
return definition.replace(
|
|
476
|
+
new RegExp(`type\\s+${oldName}\\b`),
|
|
477
|
+
`${exportPrefix}type ${newName}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return definition;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Extract all type names referenced in a type definition.
|
|
486
|
+
* Excludes JSDoc comments and string literals.
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* ```typescript
|
|
490
|
+
* extractReferencedTypes('interface Doc { author: User; tags: Tag[] }')
|
|
491
|
+
* // Returns: ['User', 'Tag']
|
|
492
|
+
* extractReferencedTypes('PaginatedResponse<Car>')
|
|
493
|
+
* // Returns: ['PaginatedResponse', 'Car']
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
static extractReferencedTypes(typeDefinition: string): string[] {
|
|
497
|
+
const types: string[] = [];
|
|
498
|
+
|
|
499
|
+
// Remove JSDoc comments to avoid extracting type names from documentation
|
|
500
|
+
const withoutJsDoc = typeDefinition.replace(/\/\*\*[\s\S]*?\*\//g, '');
|
|
501
|
+
|
|
502
|
+
// Match type references in actual code
|
|
503
|
+
// Types appear after: : < Array< extends implements | &
|
|
504
|
+
// Also match standalone types (e.g., PaginatedResponse in "PaginatedResponse<Car>")
|
|
505
|
+
// But exclude types after "interface" or "type" keywords (those are definitions, not references)
|
|
506
|
+
const typeContextRegex = /(?<!interface\s)(?<!type\s)(?::\s*|<|Array<|extends\s+|implements\s+|\|\s*|&\s*)?([A-Z][a-zA-Z0-9_]*)/g;
|
|
507
|
+
let match;
|
|
508
|
+
|
|
509
|
+
while ((match = typeContextRegex.exec(withoutJsDoc)) !== null) {
|
|
510
|
+
const typeName = match[1];
|
|
511
|
+
|
|
512
|
+
// Skip if this looks like it's part of "interface Name" or "type Name"
|
|
513
|
+
const beforeMatch = withoutJsDoc.substring(Math.max(0, match.index - 15), match.index);
|
|
514
|
+
if (/\b(interface|type)\s*$/.test(beforeMatch)) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Filter out built-in types and duplicates
|
|
519
|
+
if (!this.isBuiltInType(typeName) && !types.includes(typeName)) {
|
|
520
|
+
types.push(typeName);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return types;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Check if a type name is an inline object type.
|
|
529
|
+
*/
|
|
530
|
+
static isInlineObjectType(typeName: string): boolean {
|
|
531
|
+
return typeName.trim().startsWith('{');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Parse JSDoc comment text to extract description.
|
|
536
|
+
* Strips JSDoc comment markers and leading asterisks.
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* ```typescript
|
|
540
|
+
* parseJsDocDescription('/** Max cars to retrieve *\/')
|
|
541
|
+
* // Returns: "Max cars to retrieve"
|
|
542
|
+
*
|
|
543
|
+
* parseJsDocDescription('/**\n * Max cars to retrieve\n * from the database\n *\/')
|
|
544
|
+
* // Returns: "Max cars to retrieve from the database"
|
|
545
|
+
*
|
|
546
|
+
* parseJsDocDescription('')
|
|
547
|
+
* // Returns: ""
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
static parseJsDocDescription(jsDocText: string): string {
|
|
551
|
+
if (!jsDocText) {
|
|
552
|
+
return '';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Remove /** and */ markers
|
|
556
|
+
let cleaned = jsDocText.replace(/^\/\*\*/, '').replace(/\*\/$/, '');
|
|
557
|
+
|
|
558
|
+
// Remove leading * from each line
|
|
559
|
+
cleaned = cleaned
|
|
560
|
+
.split('\n')
|
|
561
|
+
.map(line => line.trim().replace(/^\*\s?/, ''))
|
|
562
|
+
.filter(line => line.length > 0)
|
|
563
|
+
.join(' ');
|
|
564
|
+
|
|
565
|
+
// Collapse multiple spaces into single space
|
|
566
|
+
cleaned = cleaned.replace(/\s+/g, ' ');
|
|
567
|
+
|
|
568
|
+
return cleaned.trim();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Extract property metadata from an object type body.
|
|
573
|
+
* Parses properties with optional JSDoc comments.
|
|
574
|
+
* Only extracts top-level properties (not nested object properties).
|
|
575
|
+
*
|
|
576
|
+
* @param objectBody The content between { and } in an object type
|
|
577
|
+
* @returns Array of property metadata (name + description)
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```typescript
|
|
581
|
+
* const body = `
|
|
582
|
+
* /** Max cars to retrieve *\/
|
|
583
|
+
* limit: string;
|
|
584
|
+
* page?: number;
|
|
585
|
+
* `;
|
|
586
|
+
* extractPropertiesFromObjectType(body)
|
|
587
|
+
* // Returns: [
|
|
588
|
+
* // { name: "limit", description: "Max cars to retrieve" },
|
|
589
|
+
* // { name: "page", description: "" }
|
|
590
|
+
* // ]
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
static extractPropertiesFromObjectType(objectBody: string): PropertyMetadata[] {
|
|
594
|
+
const properties: PropertyMetadata[] = [];
|
|
595
|
+
let i = 0;
|
|
596
|
+
|
|
597
|
+
while (i < objectBody.length) {
|
|
598
|
+
// Skip whitespace
|
|
599
|
+
while (i < objectBody.length && /\s/.test(objectBody[i])) {
|
|
600
|
+
i++;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (i >= objectBody.length) break;
|
|
604
|
+
|
|
605
|
+
// Check for JSDoc comment
|
|
606
|
+
let jsDoc = '';
|
|
607
|
+
if (objectBody[i] === '/' && i + 1 < objectBody.length && objectBody[i + 1] === '*' && i + 2 < objectBody.length && objectBody[i + 2] === '*') {
|
|
608
|
+
const endIndex = objectBody.indexOf('*/', i + 3);
|
|
609
|
+
if (endIndex !== -1) {
|
|
610
|
+
jsDoc = objectBody.slice(i, endIndex + 2);
|
|
611
|
+
i = endIndex + 2;
|
|
612
|
+
} else {
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Skip whitespace after JSDoc
|
|
618
|
+
while (i < objectBody.length && /\s/.test(objectBody[i])) {
|
|
619
|
+
i++;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (i >= objectBody.length) break;
|
|
623
|
+
|
|
624
|
+
// Extract property name
|
|
625
|
+
const propertyNameMatch = objectBody.slice(i).match(/^(\w+)\??:/);
|
|
626
|
+
if (!propertyNameMatch) {
|
|
627
|
+
i++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const propertyName = propertyNameMatch[1];
|
|
632
|
+
i += propertyNameMatch[0].length;
|
|
633
|
+
|
|
634
|
+
// Skip the type until we hit a delimiter at top level (;, ,, or closing })
|
|
635
|
+
// Use tokenizer to properly handle strings and comments
|
|
636
|
+
const tokenizer = new TypeScriptTokenizer(objectBody);
|
|
637
|
+
let depth = 0;
|
|
638
|
+
|
|
639
|
+
while (i < objectBody.length) {
|
|
640
|
+
const token = tokenizer.getNextToken(i);
|
|
641
|
+
|
|
642
|
+
// Only process non-string, non-comment tokens
|
|
643
|
+
if (token.type !== 'string' && token.type !== 'comment') {
|
|
644
|
+
const char = token.value;
|
|
645
|
+
|
|
646
|
+
// Track depth for nested objects/arrays
|
|
647
|
+
if (char === '{' || char === '[' || char === '(') {
|
|
648
|
+
depth++;
|
|
649
|
+
} else if (char === '}' || char === ']' || char === ')') {
|
|
650
|
+
depth--;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// End of property at top level
|
|
654
|
+
if (depth === 0 && (char === ';' || char === ',')) {
|
|
655
|
+
i = token.endPos; // Move past delimiter
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// End of property at a top-level newline when no `;`/`,`
|
|
660
|
+
// delimiter is used (semicolon-free style, issue #77). Only
|
|
661
|
+
// terminate when the next line starts a new member (an
|
|
662
|
+
// identifier, quoted key, or JSDoc) - not a continuation of
|
|
663
|
+
// the current type such as a leading `|` union arm.
|
|
664
|
+
if (depth === 0 && (char === '\n' || char === '\r')) {
|
|
665
|
+
const next = this.peekNextSignificantChar(objectBody, token.endPos);
|
|
666
|
+
if (next === undefined || /[A-Za-z_$"'/]/.test(next)) {
|
|
667
|
+
break; // Leave i at the newline; outer loop skips whitespace
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
i = token.endPos;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Add property
|
|
676
|
+
const description = this.parseJsDocDescription(jsDoc);
|
|
677
|
+
properties.push({
|
|
678
|
+
name: propertyName,
|
|
679
|
+
description,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return properties;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Copy a type definition and recursively find all its dependencies.
|
|
688
|
+
* Returns structured data without side effects.
|
|
689
|
+
*
|
|
690
|
+
* @param sourceText Full source file text to search for types
|
|
691
|
+
* @param typeName Name of the type to copy
|
|
692
|
+
* @param newName New name for the type (use same name for dependencies)
|
|
693
|
+
* @param copiedTypes Set of already-copied type names to avoid duplication
|
|
694
|
+
* @returns Object containing the main definition, dependency definitions, and import statements
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```typescript
|
|
698
|
+
* const source = `
|
|
699
|
+
* import { Address } from './Address';
|
|
700
|
+
* interface User {
|
|
701
|
+
* profile: Profile;
|
|
702
|
+
* address: Address;
|
|
703
|
+
* }
|
|
704
|
+
* interface Profile {
|
|
705
|
+
* name: string;
|
|
706
|
+
* }
|
|
707
|
+
* `;
|
|
708
|
+
* const result = copyTypeWithDependencies(source, 'User', 'UserSchema', new Set());
|
|
709
|
+
* // Returns: {
|
|
710
|
+
* // mainDefinition: 'export interface UserSchema { profile: Profile; address: Address; }',
|
|
711
|
+
* // dependencies: ['export interface Profile { name: string; }'],
|
|
712
|
+
* // imports: ['import { Address } from "./Address"'],
|
|
713
|
+
* // copiedTypes: Set(['User', 'Profile'])
|
|
714
|
+
* // }
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
717
|
+
static copyTypeWithDependencies(
|
|
718
|
+
sourceText: string,
|
|
719
|
+
typeName: string,
|
|
720
|
+
newName: string,
|
|
721
|
+
copiedTypes: Set<string>
|
|
722
|
+
): {
|
|
723
|
+
mainDefinition: string | null;
|
|
724
|
+
dependencies: string[];
|
|
725
|
+
imports: string[];
|
|
726
|
+
copiedTypes: Set<string>;
|
|
727
|
+
} {
|
|
728
|
+
const dependencies: string[] = [];
|
|
729
|
+
const imports: string[] = [];
|
|
730
|
+
|
|
731
|
+
// Handle inline object types specially
|
|
732
|
+
let mainDefinition: string | null = null;
|
|
733
|
+
|
|
734
|
+
if (this.isInlineObjectType(typeName)) {
|
|
735
|
+
mainDefinition = this.convertInlineObjectToInterface(typeName, newName);
|
|
736
|
+
} else {
|
|
737
|
+
// Find and copy the main type from file
|
|
738
|
+
const found = this.findTypeDefinition(sourceText, typeName);
|
|
739
|
+
if (found) {
|
|
740
|
+
mainDefinition = this.renameTypeDefinition(found.definition, typeName, newName);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!mainDefinition) {
|
|
745
|
+
return { mainDefinition: null, dependencies: [], imports: [], copiedTypes };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Mark this type as copied
|
|
749
|
+
copiedTypes.add(typeName);
|
|
750
|
+
|
|
751
|
+
// Find all referenced types in the definition
|
|
752
|
+
const referencedTypes = this.extractReferencedTypes(mainDefinition);
|
|
753
|
+
|
|
754
|
+
// Recursively copy each referenced type (if not already copied)
|
|
755
|
+
for (const refType of referencedTypes) {
|
|
756
|
+
if (!copiedTypes.has(refType) && !this.isBuiltInType(refType)) {
|
|
757
|
+
// Check if this type is defined in the current file
|
|
758
|
+
const typeDef = this.findTypeDefinition(sourceText, refType);
|
|
759
|
+
|
|
760
|
+
if (typeDef) {
|
|
761
|
+
// Type is defined in same file - recursively copy it
|
|
762
|
+
const result = this.copyTypeWithDependencies(sourceText, refType, refType, copiedTypes);
|
|
763
|
+
|
|
764
|
+
if (result.mainDefinition) {
|
|
765
|
+
dependencies.push(result.mainDefinition);
|
|
766
|
+
}
|
|
767
|
+
dependencies.push(...result.dependencies);
|
|
768
|
+
imports.push(...result.imports);
|
|
769
|
+
} else {
|
|
770
|
+
// Type is not in current file - must be imported
|
|
771
|
+
// Extract the import statement for it
|
|
772
|
+
const importStmt = this.extractImportForType(sourceText, refType);
|
|
773
|
+
if (importStmt && !imports.includes(importStmt)) {
|
|
774
|
+
imports.push(importStmt);
|
|
775
|
+
}
|
|
776
|
+
// Mark as copied to avoid trying to find it again
|
|
777
|
+
copiedTypes.add(refType);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return { mainDefinition, dependencies, imports, copiedTypes };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Extract import statement for a specific type from source text.
|
|
787
|
+
*
|
|
788
|
+
* @param sourceText Full source file text
|
|
789
|
+
* @param typeName Name of the type to find import for
|
|
790
|
+
* @returns Import statement or null if not found
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```typescript
|
|
794
|
+
* const source = `
|
|
795
|
+
* import { User } from '../schemas/User';
|
|
796
|
+
* import type { Profile } from '../schemas/Profile';
|
|
797
|
+
* import Foo from '../schemas/Foo';
|
|
798
|
+
* `;
|
|
799
|
+
* extractImportForType(source, 'User')
|
|
800
|
+
* // Returns: "import { User } from '../schemas/User'"
|
|
801
|
+
*
|
|
802
|
+
* extractImportForType(source, 'Profile')
|
|
803
|
+
* // Returns: "import type { Profile } from '../schemas/Profile'"
|
|
804
|
+
*
|
|
805
|
+
* extractImportForType(source, 'Foo')
|
|
806
|
+
* // Returns: "import Foo from '../schemas/Foo'"
|
|
807
|
+
* ```
|
|
808
|
+
*/
|
|
809
|
+
static extractImportForType(sourceText: string, typeName: string): string | null {
|
|
810
|
+
// Supports: import Foo from '...' OR import { Foo } from '...'
|
|
811
|
+
// Also supports: import type Foo from '...' OR import type { Foo } from '...'
|
|
812
|
+
const importRegex = new RegExp(
|
|
813
|
+
`import\\s+(?:type\\s+)?(?:{[^}]*\\b${typeName}\\b[^}]*}|${typeName})\\s+from\\s+(['"][^'"]+['"])`,
|
|
814
|
+
'gm'
|
|
815
|
+
);
|
|
816
|
+
const match = importRegex.exec(sourceText);
|
|
817
|
+
|
|
818
|
+
if (match) {
|
|
819
|
+
return match[0];
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Extract all imports from a source file.
|
|
827
|
+
* Used to get transitive dependencies when importing a type.
|
|
828
|
+
*
|
|
829
|
+
* @param sourceText Source code to extract imports from
|
|
830
|
+
* @returns Array of import statements
|
|
831
|
+
*/
|
|
832
|
+
static extractAllImports(sourceText: string): string[] {
|
|
833
|
+
const imports: string[] = [];
|
|
834
|
+
// Match all import statements (including type imports, named imports, default imports, namespace imports)
|
|
835
|
+
const importRegex = /import\s+(?:type\s+)?(?:{[^}]+}|\w+|(?:\*\s+as\s+\w+))\s+from\s+['"][^'"]+['"]/gm;
|
|
836
|
+
let match;
|
|
837
|
+
|
|
838
|
+
while ((match = importRegex.exec(sourceText)) !== null) {
|
|
839
|
+
imports.push(match[0]);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return imports;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Adjust import path to be relative to .flink/schemas/ directory.
|
|
847
|
+
*
|
|
848
|
+
* @param importStatement Original import statement
|
|
849
|
+
* @returns Adjusted import statement with corrected path
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* ```typescript
|
|
853
|
+
* adjustImportPathForSchemas('import { User } from "../schemas/User"')
|
|
854
|
+
* // Returns: 'import { User } from "../../src/schemas/User"'
|
|
855
|
+
*
|
|
856
|
+
* adjustImportPathForSchemas('import Profile from "../../schemas/Profile"')
|
|
857
|
+
* // Returns: 'import Profile from "../../src/schemas/Profile"'
|
|
858
|
+
*
|
|
859
|
+
* adjustImportPathForSchemas('import { Req } from "../clients/SupermetricsClient"')
|
|
860
|
+
* // Returns: 'import { Req } from "../../src/clients/SupermetricsClient"'
|
|
861
|
+
*
|
|
862
|
+
* adjustImportPathForSchemas('import { Foo } from "@company/shared"')
|
|
863
|
+
* // Returns: 'import { Foo } from "@company/shared"' (unchanged - not relative)
|
|
864
|
+
* ```
|
|
865
|
+
*/
|
|
866
|
+
static adjustImportPathForSchemas(importStatement: string): string {
|
|
867
|
+
// schemas.ts is at: .flink/schemas/schemas.ts
|
|
868
|
+
// Handler imports can be from anywhere in src/ (handlers, schemas, clients, etc.)
|
|
869
|
+
// We need to adjust ALL relative imports to work from .flink/schemas/
|
|
870
|
+
|
|
871
|
+
// Extract the path from import statement
|
|
872
|
+
const pathMatch = importStatement.match(/from\s+(['"])([^'"]+)['"]/);
|
|
873
|
+
if (!pathMatch) {
|
|
874
|
+
return importStatement;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const quote = pathMatch[1];
|
|
878
|
+
const importPath = pathMatch[2];
|
|
879
|
+
|
|
880
|
+
// Skip non-relative imports (package imports like "@company/shared", "lodash")
|
|
881
|
+
if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
|
|
882
|
+
return importStatement;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// For relative imports, we need to adjust the path to work from .flink/schemas/
|
|
886
|
+
// Original: from handler at src/handlers/foo/Bar.ts importing "../clients/Client"
|
|
887
|
+
// Resolves to: src/clients/Client
|
|
888
|
+
// From schemas.ts at .flink/schemas/schemas.ts, we need: "../../src/clients/Client"
|
|
889
|
+
|
|
890
|
+
// Extract the path after src/ directory
|
|
891
|
+
// Patterns we handle:
|
|
892
|
+
// - "../schemas/User" → "../../src/schemas/User"
|
|
893
|
+
// - "../../schemas/User" → "../../src/schemas/User"
|
|
894
|
+
// - "../clients/Client" → "../../src/clients/Client"
|
|
895
|
+
// - "../../types/Foo" → "../../src/types/Foo"
|
|
896
|
+
|
|
897
|
+
// Find the src directory marker in the path
|
|
898
|
+
let adjustedPath: string;
|
|
899
|
+
|
|
900
|
+
if (importPath.includes('/schemas/')) {
|
|
901
|
+
// Special case for schemas - extract everything after '/schemas/'
|
|
902
|
+
const schemasIndex = importPath.indexOf('/schemas/');
|
|
903
|
+
const pathAfterSchemas = importPath.substring(schemasIndex + '/schemas/'.length);
|
|
904
|
+
adjustedPath = `../../src/schemas/${pathAfterSchemas}`;
|
|
905
|
+
} else {
|
|
906
|
+
// General case: assume the import resolves to src/<something>
|
|
907
|
+
// Count the number of "../" to understand directory depth, then reconstruct
|
|
908
|
+
// For simplicity, we'll assume all imports from handlers resolve to src/*
|
|
909
|
+
// Pattern: any number of "../" optionally followed by "./" followed by a directory path
|
|
910
|
+
// Strip both "../" and "./" prefixes
|
|
911
|
+
const pathWithoutLeadingDots = importPath.replace(/^(\.\.\/)*/, '').replace(/^\.\//, '');
|
|
912
|
+
adjustedPath = `../../src/${pathWithoutLeadingDots}`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return importStatement.replace(
|
|
916
|
+
`from ${quote}${importPath}${quote}`,
|
|
917
|
+
`from ${quote}${adjustedPath}${quote}`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Extract property metadata from a named type definition.
|
|
923
|
+
* Searches for the type definition and parses its properties.
|
|
924
|
+
*
|
|
925
|
+
* @param sourceText Full source file text
|
|
926
|
+
* @param typeName Name of the type to extract properties from
|
|
927
|
+
* @returns Array of property metadata, or null if type not found
|
|
928
|
+
*
|
|
929
|
+
* @example
|
|
930
|
+
* ```typescript
|
|
931
|
+
* const source = `
|
|
932
|
+
* interface Query {
|
|
933
|
+
* /** Max items *\/
|
|
934
|
+
* limit: string;
|
|
935
|
+
* page?: number;
|
|
936
|
+
* }
|
|
937
|
+
* `;
|
|
938
|
+
* extractPropertyMetadata(source, 'Query')
|
|
939
|
+
* // Returns: [
|
|
940
|
+
* // { name: "limit", description: "Max items" },
|
|
941
|
+
* // { name: "page", description: "" }
|
|
942
|
+
* // ]
|
|
943
|
+
* ```
|
|
944
|
+
*/
|
|
945
|
+
static extractPropertyMetadata(sourceText: string, typeName: string): PropertyMetadata[] | null {
|
|
946
|
+
// Find the type definition
|
|
947
|
+
const typeDef = this.findTypeDefinition(sourceText, typeName);
|
|
948
|
+
|
|
949
|
+
if (!typeDef) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Extract the object body (content between { and })
|
|
954
|
+
const definition = typeDef.definition;
|
|
955
|
+
|
|
956
|
+
// Find the opening brace
|
|
957
|
+
const openBraceIndex = definition.indexOf('{');
|
|
958
|
+
if (openBraceIndex === -1) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Use tokenizer to find the matching closing brace (handles strings and comments)
|
|
963
|
+
const tokenizer = new TypeScriptTokenizer(definition);
|
|
964
|
+
const closeBraceIndex = tokenizer.findMatchingBracketAt(openBraceIndex, '{');
|
|
965
|
+
|
|
966
|
+
if (closeBraceIndex === -1) {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Extract the object body (excluding the outer braces)
|
|
971
|
+
const objectBody = definition.slice(openBraceIndex + 1, closeBraceIndex);
|
|
972
|
+
|
|
973
|
+
return this.extractPropertiesFromObjectType(objectBody);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Consolidates duplicate imports from the same module.
|
|
978
|
+
* Merges named imports and preserves default imports.
|
|
979
|
+
*
|
|
980
|
+
* @param imports Array of import statements
|
|
981
|
+
* @returns Consolidated import statements with duplicates merged
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* ```typescript
|
|
985
|
+
* const imports = [
|
|
986
|
+
* 'import { Ad } from "../../src/schemas/Ad"',
|
|
987
|
+
* 'import { AdStatus } from "../../src/schemas/Ad"',
|
|
988
|
+
* 'import { User } from "../../src/schemas/User"'
|
|
989
|
+
* ];
|
|
990
|
+
* consolidateImports(imports)
|
|
991
|
+
* // Returns: [
|
|
992
|
+
* // 'import { Ad, AdStatus } from "../../src/schemas/Ad"',
|
|
993
|
+
* // 'import { User } from "../../src/schemas/User"'
|
|
994
|
+
* // ]
|
|
995
|
+
* ```
|
|
996
|
+
*/
|
|
997
|
+
static consolidateImports(imports: string[]): string[] {
|
|
998
|
+
// Map: module path -> { namedImports: Set<string>, defaultImport?: string }
|
|
999
|
+
const importMap = new Map<string, { namedImports: Set<string>; defaultImport?: string }>();
|
|
1000
|
+
|
|
1001
|
+
for (const importStmt of imports) {
|
|
1002
|
+
// Parse import statement to extract module path and imports
|
|
1003
|
+
// Patterns:
|
|
1004
|
+
// - import { Foo, Bar } from "path"
|
|
1005
|
+
// - import type { Foo, Bar } from "path"
|
|
1006
|
+
// - import Foo from "path"
|
|
1007
|
+
// - import type Foo from "path"
|
|
1008
|
+
const namedMatch = importStmt.match(/import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+(['"])([^'"]+)['"]/);
|
|
1009
|
+
const defaultMatch = importStmt.match(/import\s+(?:type\s+)?(\w+)\s+from\s+(['"])([^'"]+)['"]/);
|
|
1010
|
+
|
|
1011
|
+
if (namedMatch) {
|
|
1012
|
+
const namedImports = namedMatch[1]
|
|
1013
|
+
.split(',')
|
|
1014
|
+
.map((s) => s.trim())
|
|
1015
|
+
.filter((s) => s.length > 0);
|
|
1016
|
+
const modulePath = namedMatch[3];
|
|
1017
|
+
|
|
1018
|
+
if (!importMap.has(modulePath)) {
|
|
1019
|
+
importMap.set(modulePath, { namedImports: new Set() });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const entry = importMap.get(modulePath)!;
|
|
1023
|
+
namedImports.forEach((name) => entry.namedImports.add(name));
|
|
1024
|
+
} else if (defaultMatch) {
|
|
1025
|
+
const defaultImport = defaultMatch[1];
|
|
1026
|
+
const modulePath = defaultMatch[3];
|
|
1027
|
+
|
|
1028
|
+
if (!importMap.has(modulePath)) {
|
|
1029
|
+
importMap.set(modulePath, { namedImports: new Set() });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const entry = importMap.get(modulePath)!;
|
|
1033
|
+
entry.defaultImport = defaultImport;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Generate consolidated import statements
|
|
1038
|
+
const consolidated: string[] = [];
|
|
1039
|
+
const entries = Array.from(importMap.entries());
|
|
1040
|
+
|
|
1041
|
+
for (const [modulePath, { namedImports, defaultImport }] of entries) {
|
|
1042
|
+
if (defaultImport && namedImports.size > 0) {
|
|
1043
|
+
// Both default and named imports
|
|
1044
|
+
const named = Array.from(namedImports).sort().join(', ');
|
|
1045
|
+
consolidated.push(`import ${defaultImport}, { ${named} } from "${modulePath}"`);
|
|
1046
|
+
} else if (defaultImport) {
|
|
1047
|
+
// Default import only
|
|
1048
|
+
consolidated.push(`import ${defaultImport} from "${modulePath}"`);
|
|
1049
|
+
} else if (namedImports.size > 0) {
|
|
1050
|
+
// Named imports only
|
|
1051
|
+
const named = Array.from(namedImports).sort().join(', ');
|
|
1052
|
+
consolidated.push(`import { ${named} } from "${modulePath}"`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return consolidated;
|
|
1057
|
+
}
|
|
1058
|
+
}
|