@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
|
@@ -1,53 +1,186 @@
|
|
|
1
1
|
import fs, { promises as fsPromises } from "fs";
|
|
2
|
-
import { JSONSchema7 } from "json-schema";
|
|
3
2
|
import { join } from "path";
|
|
4
3
|
import glob from "tiny-glob";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
ArrayLiteralExpression,
|
|
8
|
-
DiagnosticCategory,
|
|
9
|
-
ImportDeclarationStructure,
|
|
10
|
-
OptionalKind,
|
|
11
|
-
Project,
|
|
12
|
-
SourceFile,
|
|
13
|
-
Symbol,
|
|
14
|
-
SyntaxKind,
|
|
15
|
-
ts,
|
|
16
|
-
Type,
|
|
17
|
-
TypeReferenceNode,
|
|
18
|
-
VariableDeclarationKind,
|
|
19
|
-
} from "ts-morph";
|
|
4
|
+
import { ArrayLiteralExpression, DiagnosticCategory, ImportDeclarationStructure, OptionalKind, Project, SourceFile, SyntaxKind, ts } from "ts-morph";
|
|
5
|
+
import { FlinkLogFactory } from "./FlinkLogFactory";
|
|
20
6
|
import { writeJsonFile } from "./FsUtils";
|
|
21
|
-
import {
|
|
7
|
+
import { TypeScriptSourceParser } from "./schema-extraction";
|
|
22
8
|
import { getCollectionNameForRepo, getHttpMethodFromHandlerName, getRepoInstanceName } from "./utils";
|
|
9
|
+
import { FlinkCompilerPlugin, loadFlinkConfig } from "./utils/loadFlinkConfig";
|
|
10
|
+
|
|
11
|
+
const perfLog = FlinkLogFactory.createLogger("flink.perf");
|
|
12
|
+
const initLog = FlinkLogFactory.createLogger("flink.init");
|
|
23
13
|
|
|
24
14
|
class TypeScriptCompiler {
|
|
15
|
+
private cwd: string;
|
|
25
16
|
private project: Project;
|
|
26
|
-
private schemaGenerator?: SchemaGenerator;
|
|
27
17
|
private isEsm: boolean;
|
|
18
|
+
private schemaGenerator?: ((source: string, options?: any) => any) | null;
|
|
28
19
|
|
|
29
20
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
* Handler schemas collected during parseHandlers, to be generated later
|
|
22
|
+
*/
|
|
23
|
+
private handlerSchemasToGenerate: {
|
|
24
|
+
reqSchemaType?: string;
|
|
25
|
+
resSchemaType?: string;
|
|
26
|
+
sourceFile: SourceFile;
|
|
27
|
+
}[] = [];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tool schemas collected during parseTools, to be generated later
|
|
31
|
+
*/
|
|
32
|
+
private toolSchemasToGenerate: {
|
|
33
|
+
inputSchemaType?: string;
|
|
34
|
+
outputSchemaType?: string;
|
|
35
|
+
inputTypeHint?: "void" | "any" | "named";
|
|
36
|
+
outputTypeHint?: "void" | "any" | "named";
|
|
37
|
+
sourceFile: SourceFile;
|
|
38
|
+
}[] = [];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pre-segmented source files by type (cached for performance)
|
|
42
|
+
*/
|
|
43
|
+
private handlerFiles: SourceFile[] = [];
|
|
44
|
+
private repoFiles: SourceFile[] = [];
|
|
45
|
+
private toolFiles: SourceFile[] = [];
|
|
46
|
+
private agentFiles: SourceFile[] = [];
|
|
47
|
+
private jobFiles: SourceFile[] = [];
|
|
48
|
+
private serviceFiles: SourceFile[] = [];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tool ID registry for agent validation (built during segmentation)
|
|
52
|
+
*/
|
|
53
|
+
private toolIdRegistry: Set<string> = new Set();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compiler plugins loaded from flink.config.js
|
|
57
|
+
*/
|
|
58
|
+
private compilerPlugins: FlinkCompilerPlugin[] = [];
|
|
59
|
+
private disableServices = false;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extension files collected during segmentation, keyed by generatedFile name
|
|
34
63
|
*/
|
|
35
|
-
private
|
|
64
|
+
private extensionFiles: Map<string, SourceFile[]> = new Map();
|
|
36
65
|
|
|
37
66
|
/**
|
|
38
|
-
*
|
|
67
|
+
* Generates a schema $id from a file path and type name using the same algorithm
|
|
68
|
+
* as the schema generator's defineId callback.
|
|
39
69
|
*
|
|
40
|
-
*
|
|
70
|
+
* Examples:
|
|
71
|
+
* - src/schemas/Car.ts, "Car" → "Car"
|
|
72
|
+
* - src/schemas/wrappers/PartialLoginReq.ts, "PartialLoginReq" → "wrappers.PartialLoginReq"
|
|
73
|
+
* - src/schemas/PatchCarSchemas.ts, "PatchCarReq" → "PatchCarSchemas.PatchCarReq"
|
|
74
|
+
*/
|
|
75
|
+
private filePathToSchemaId(absolutePath: string, typeName: string): string {
|
|
76
|
+
const path = require("path");
|
|
77
|
+
const schemaDir = path.join(this.cwd, "src/schemas");
|
|
78
|
+
|
|
79
|
+
// Get path relative to src/schemas/
|
|
80
|
+
const relativePath = path.relative(schemaDir, absolutePath);
|
|
81
|
+
|
|
82
|
+
// Remove .ts extension
|
|
83
|
+
const pathWithoutExt = relativePath.replace(/\.ts$/, "");
|
|
84
|
+
|
|
85
|
+
// Split by path separator
|
|
86
|
+
const parts = pathWithoutExt.split(path.sep);
|
|
87
|
+
const fileName = parts[parts.length - 1];
|
|
88
|
+
|
|
89
|
+
// Root file with matching name: just type name
|
|
90
|
+
if (parts.length === 1 && fileName === typeName) {
|
|
91
|
+
return typeName;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Nested file or multiple exports: include path
|
|
95
|
+
const pathPrefix = parts.join(".");
|
|
96
|
+
if (fileName === typeName) {
|
|
97
|
+
return pathPrefix; // Avoid duplication
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `${pathPrefix}.${typeName}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves a type name to its schema $id by looking up its import path.
|
|
105
|
+
* Returns undefined if the type is not imported from src/schemas/.
|
|
41
106
|
*/
|
|
42
|
-
private
|
|
107
|
+
private resolveTypeNameToSchemaId(fileText: string, typeName: string, handlerFilePath: string): string | undefined {
|
|
108
|
+
const path = require("path");
|
|
109
|
+
|
|
110
|
+
// Extract import statement for this type
|
|
111
|
+
const importStmt = TypeScriptSourceParser.extractImportForType(fileText, typeName);
|
|
112
|
+
if (!importStmt) {
|
|
113
|
+
perfLog.trace(`Type ${typeName} not found in imports`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract module specifier from import
|
|
118
|
+
// e.g., import { Foo } from "../schemas/Bar" → "../schemas/Bar"
|
|
119
|
+
const moduleMatch = importStmt.match(/from\s+["']([^"']+)["']/);
|
|
120
|
+
if (!moduleMatch) {
|
|
121
|
+
perfLog.trace(`Could not extract module specifier from: ${importStmt}`);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const moduleSpecifier = moduleMatch[1];
|
|
126
|
+
|
|
127
|
+
// Resolve relative import to absolute path
|
|
128
|
+
const handlerDir = path.dirname(handlerFilePath);
|
|
129
|
+
const importPath = path.resolve(handlerDir, moduleSpecifier);
|
|
130
|
+
|
|
131
|
+
// Add .ts extension if not present
|
|
132
|
+
const importPathWithExt = importPath.endsWith(".ts") ? importPath : importPath + ".ts";
|
|
133
|
+
|
|
134
|
+
// Check if this is from src/schemas/
|
|
135
|
+
const schemaDir = path.join(this.cwd, "src/schemas");
|
|
136
|
+
if (!importPathWithExt.startsWith(schemaDir)) {
|
|
137
|
+
perfLog.trace(`Type ${typeName} is not from src/schemas/ (from ${importPathWithExt})`);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Generate schema $id using the same algorithm
|
|
142
|
+
return this.filePathToSchemaId(importPathWithExt, typeName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
constructor(cwd: string) {
|
|
146
|
+
const path = require("path");
|
|
147
|
+
// Convert to absolute path to ensure consistent path comparisons
|
|
148
|
+
this.cwd = path.resolve(cwd);
|
|
43
149
|
|
|
44
|
-
constructor(private cwd: string) {
|
|
45
150
|
// Detect if project is using ESM based solely on package.json "type": "module"
|
|
46
|
-
this.isEsm = this.isEsmProject(cwd);
|
|
151
|
+
this.isEsm = this.isEsmProject(this.cwd);
|
|
152
|
+
|
|
153
|
+
const tsConfigPath = join(this.cwd, "tsconfig.json");
|
|
154
|
+
|
|
155
|
+
// Read and inherit important settings from tsconfig.json
|
|
156
|
+
let userTsConfig: any = {};
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(tsConfigPath)) {
|
|
159
|
+
const tsConfigContent = fs.readFileSync(tsConfigPath, "utf8");
|
|
160
|
+
userTsConfig = JSON.parse(tsConfigContent);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn("Warning: Could not read tsconfig.json:", error);
|
|
164
|
+
}
|
|
47
165
|
|
|
48
166
|
const compilerOptions: ts.CompilerOptions = {
|
|
49
167
|
noEmit: false, // We need to emit files
|
|
50
168
|
outDir: join(cwd, "dist"),
|
|
169
|
+
// Inherit important performance options from user's tsconfig
|
|
170
|
+
skipLibCheck: userTsConfig.compilerOptions?.skipLibCheck ?? true, // Default to true for performance
|
|
171
|
+
|
|
172
|
+
// Performance optimizations for emit phase
|
|
173
|
+
// Since we already run getPreEmitDiagnostics() for type checking,
|
|
174
|
+
// we can use faster transpilation-only options
|
|
175
|
+
isolatedModules: true, // Transpile each file independently (much faster, no cross-file analysis)
|
|
176
|
+
// removeComments: true, // Smaller output, faster emit
|
|
177
|
+
importHelpers: false, // Don't import tslib helpers
|
|
178
|
+
|
|
179
|
+
// EXPERIMENTAL: Enable incremental compilation
|
|
180
|
+
// This creates a .tsbuildinfo file to cache type information
|
|
181
|
+
// Should speed up subsequent builds by avoiding full type checking
|
|
182
|
+
// incremental: true,
|
|
183
|
+
// tsBuildInfoFile: join(cwd, "dist", ".tsbuildinfo"),
|
|
51
184
|
};
|
|
52
185
|
|
|
53
186
|
// Set appropriate module settings based on detected module system
|
|
@@ -62,20 +195,326 @@ class TypeScriptCompiler {
|
|
|
62
195
|
compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs;
|
|
63
196
|
}
|
|
64
197
|
|
|
65
|
-
|
|
66
|
-
console.log("
|
|
67
|
-
console.log("TypeScript version:", ts.version);
|
|
198
|
+
initLog.info("TypeScript version:", ts.version, "config path", require("path").resolve(tsConfigPath));
|
|
199
|
+
// console.log("Compiler options:", JSON.stringify(compilerOptions, null, 2));
|
|
68
200
|
|
|
201
|
+
const projectStartTime = Date.now();
|
|
69
202
|
this.project = new Project({
|
|
70
203
|
tsConfigFilePath: tsConfigPath,
|
|
71
204
|
compilerOptions,
|
|
205
|
+
skipAddingFilesFromTsConfig: true, // Don't auto-load all files - we'll add specific directories we need
|
|
206
|
+
skipFileDependencyResolution: true, // Don't resolve /// <reference> and type dependencies from node_modules
|
|
72
207
|
});
|
|
208
|
+
const projectTime = Date.now() - projectStartTime;
|
|
209
|
+
perfLog.debug(`✓ Project initialization completed in ${projectTime}ms`);
|
|
210
|
+
|
|
211
|
+
// Load Flink-specific source paths from tsconfig.json if available
|
|
212
|
+
const additionalSourcePaths = this.getFlinkSourcePaths(tsConfigPath);
|
|
213
|
+
|
|
214
|
+
// Load all TypeScript files from src/ directory
|
|
215
|
+
// This is simpler and faster than recursive import resolution
|
|
216
|
+
// We exclude test files and declarations to keep it lean
|
|
217
|
+
const defaultSourcePaths = [
|
|
218
|
+
join(cwd, "src/**/*.ts"),
|
|
219
|
+
"!" + join(cwd, "src/**/*.spec.ts"),
|
|
220
|
+
"!" + join(cwd, "src/**/*.test.ts"),
|
|
221
|
+
"!" + join(cwd, "src/**/*.d.ts"),
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const allSourcePaths = [...defaultSourcePaths, ...additionalSourcePaths];
|
|
225
|
+
|
|
226
|
+
const loadStartTime = Date.now();
|
|
227
|
+
this.project.addSourceFilesAtPaths(allSourcePaths);
|
|
228
|
+
const loadTime = Date.now() - loadStartTime;
|
|
229
|
+
const fileCount = this.project.getSourceFiles().length;
|
|
230
|
+
perfLog.debug(`✓ All source files loaded in ${loadTime}ms (${fileCount} files)`);
|
|
231
|
+
|
|
232
|
+
// Load config from flink.config.js
|
|
233
|
+
const flinkCfg = loadFlinkConfig(this.cwd);
|
|
234
|
+
this.compilerPlugins = flinkCfg?.compilerPlugins ?? [];
|
|
235
|
+
this.disableServices = flinkCfg?.disableServices ?? false;
|
|
236
|
+
|
|
237
|
+
if (this.compilerPlugins.length > 0) {
|
|
238
|
+
initLog.info(
|
|
239
|
+
`Compiler plugins loaded (${this.compilerPlugins.length}):`,
|
|
240
|
+
this.compilerPlugins.map((p) => `${p.package} → ${p.scanDir}`).join(", ")
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
initLog.info("No compiler plugins configured");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Segment files by type for efficient processing
|
|
247
|
+
this.segmentSourceFiles();
|
|
73
248
|
|
|
74
249
|
console.log("Loaded", this.project.getSourceFiles().length, "source file(s) from", cwd);
|
|
75
250
|
console.log("Module system:", this.isEsm ? "ESM" : "CommonJS");
|
|
76
251
|
console.log("Using module:", compilerOptions.module === ts.ModuleKind.ESNext ? "ESNext" : "CommonJS");
|
|
77
252
|
}
|
|
78
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Loads additional source paths from tsconfig.json's flink configuration.
|
|
256
|
+
* Allows projects to specify extra directories to include in compilation.
|
|
257
|
+
*
|
|
258
|
+
* Example tsconfig.json:
|
|
259
|
+
* {
|
|
260
|
+
* "flink": {
|
|
261
|
+
* "sourcePaths": ["src/custom-types/**\/*.ts", "src/utils/**\/*.ts"]
|
|
262
|
+
* }
|
|
263
|
+
* }
|
|
264
|
+
*/
|
|
265
|
+
private getFlinkSourcePaths(tsConfigPath: string): string[] {
|
|
266
|
+
try {
|
|
267
|
+
if (fs.existsSync(tsConfigPath)) {
|
|
268
|
+
const tsConfigContent = fs.readFileSync(tsConfigPath, "utf8");
|
|
269
|
+
const tsConfig = JSON.parse(tsConfigContent);
|
|
270
|
+
|
|
271
|
+
if (tsConfig.flink && Array.isArray(tsConfig.flink.sourcePaths)) {
|
|
272
|
+
console.log("Found Flink-specific source paths:", tsConfig.flink.sourcePaths);
|
|
273
|
+
return tsConfig.flink.sourcePaths.map((path: string) => join(this.cwd, path));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn("Error reading Flink source paths from tsconfig.json:", error);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Initializes the schema generator.
|
|
285
|
+
*
|
|
286
|
+
* Note: ts-source-to-json-schema is ESM-only while @flink-app/flink is CommonJS.
|
|
287
|
+
* We use new Function() to preserve the actual import() in compiled output.
|
|
288
|
+
* Without this, TypeScript converts import() to require() which can't load ESM.
|
|
289
|
+
*/
|
|
290
|
+
private async initSchemaGenerator() {
|
|
291
|
+
if (this.schemaGenerator !== undefined) {
|
|
292
|
+
return; // Already initialized
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
// Preserve dynamic import() in CommonJS output (TypeScript would convert to require())
|
|
297
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
298
|
+
const module = await dynamicImport("@flink-app/ts-source-to-json-schema");
|
|
299
|
+
|
|
300
|
+
if (typeof module.toJsonSchemasFromFile !== "function") {
|
|
301
|
+
throw new Error("toJsonSchemasFromFile function not found. Please update @flink-app/ts-source-to-json-schema to >= 0.2.0");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.schemaGenerator = module.toJsonSchemasFromFile;
|
|
305
|
+
} catch (error: any) {
|
|
306
|
+
console.error(
|
|
307
|
+
"\n❌ Schema generator could not be loaded:\n" +
|
|
308
|
+
` Error: ${error.message}\n` +
|
|
309
|
+
"\n💡 Make sure @flink-app/ts-source-to-json-schema is installed:\n" +
|
|
310
|
+
" npm install @flink-app/ts-source-to-json-schema\n"
|
|
311
|
+
);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Recursively resolves and adds imported files to the project.
|
|
318
|
+
* This ensures that files imported by handlers, schemas, etc. are available for type resolution.
|
|
319
|
+
*
|
|
320
|
+
* Handles three types of imports:
|
|
321
|
+
* 1. Relative imports (./foo, ../bar) - always resolved
|
|
322
|
+
* 2. Workspace package imports (@mycompany/shared) - resolved if symlinked outside node_modules
|
|
323
|
+
* 3. External packages (lodash, express) - skipped to avoid loading entire dependency trees
|
|
324
|
+
*/
|
|
325
|
+
private resolveImportedFiles() {
|
|
326
|
+
// TODO: Check if this really is needed!
|
|
327
|
+
|
|
328
|
+
const processedFiles = new Set<string>();
|
|
329
|
+
const filesToProcess = [...this.project.getSourceFiles()];
|
|
330
|
+
|
|
331
|
+
let totalImports = 0;
|
|
332
|
+
let skippedImports = 0;
|
|
333
|
+
let resolvedImports = 0;
|
|
334
|
+
|
|
335
|
+
while (filesToProcess.length > 0) {
|
|
336
|
+
const sourceFile = filesToProcess.pop()!;
|
|
337
|
+
const filePath = sourceFile.getFilePath();
|
|
338
|
+
|
|
339
|
+
if (processedFiles.has(filePath)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
processedFiles.add(filePath);
|
|
344
|
+
|
|
345
|
+
// Get all import declarations
|
|
346
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
347
|
+
|
|
348
|
+
for (const importDecl of importDeclarations) {
|
|
349
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
350
|
+
totalImports++;
|
|
351
|
+
|
|
352
|
+
// For relative imports (./foo, ../bar), always resolve
|
|
353
|
+
const isRelativeImport = moduleSpecifier.startsWith(".") || moduleSpecifier.startsWith("/");
|
|
354
|
+
|
|
355
|
+
// For package imports, check if it might be a workspace package
|
|
356
|
+
// Workspace packages are typically scoped (@company/pkg) or match the workspace pattern
|
|
357
|
+
const isLikelyWorkspacePackage =
|
|
358
|
+
!isRelativeImport && (moduleSpecifier.startsWith("@" + this.getWorkspaceScope() + "/") || this.isLikelyWorkspaceImport(moduleSpecifier));
|
|
359
|
+
|
|
360
|
+
// Skip resolution for external packages (performance optimization)
|
|
361
|
+
// Only resolve relative imports and likely workspace packages
|
|
362
|
+
if (!isRelativeImport && !isLikelyWorkspacePackage) {
|
|
363
|
+
skippedImports++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Try to resolve the imported file (expensive operation)
|
|
368
|
+
resolvedImports++;
|
|
369
|
+
const moduleSourceFile = importDecl.getModuleSpecifierSourceFile();
|
|
370
|
+
|
|
371
|
+
if (moduleSourceFile) {
|
|
372
|
+
const importedPath = moduleSourceFile.getFilePath();
|
|
373
|
+
|
|
374
|
+
// Double-check it's not in node_modules (for workspace packages)
|
|
375
|
+
const isWorkspacePackage = !isRelativeImport && !importedPath.includes("node_modules");
|
|
376
|
+
|
|
377
|
+
// Add to processing queue if it's a relative import or workspace package
|
|
378
|
+
if ((isRelativeImport || isWorkspacePackage) && !processedFiles.has(importedPath)) {
|
|
379
|
+
filesToProcess.push(moduleSourceFile);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log("Resolved imports, total files loaded:", processedFiles.size);
|
|
386
|
+
console.log(` Import stats: ${totalImports} total, ${skippedImports} skipped, ${resolvedImports} resolved`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Gets the workspace scope from package.json (e.g., "@mycompany" from "@mycompany/my-app")
|
|
391
|
+
* Returns empty string if not a scoped package.
|
|
392
|
+
*/
|
|
393
|
+
private getWorkspaceScope(): string {
|
|
394
|
+
try {
|
|
395
|
+
const packageJson = JSON.parse(fs.readFileSync(join(this.cwd, "package.json"), "utf8"));
|
|
396
|
+
const name = packageJson.name || "";
|
|
397
|
+
if (name.startsWith("@")) {
|
|
398
|
+
return name.split("/")[0].substring(1); // Remove @ and get scope
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
// Ignore errors
|
|
402
|
+
}
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Checks if an import is likely to be a workspace package.
|
|
408
|
+
* This is a heuristic - we check for common workspace patterns but can't be 100% sure
|
|
409
|
+
* without actually resolving. This is a performance optimization to avoid resolving
|
|
410
|
+
* obvious external packages like "express", "lodash", etc.
|
|
411
|
+
*/
|
|
412
|
+
private isLikelyWorkspaceImport(moduleSpecifier: string): boolean {
|
|
413
|
+
// If it contains a workspace marker in the name, it might be workspace
|
|
414
|
+
// Common patterns: starts with project name, contains "internal", etc.
|
|
415
|
+
// For now, be conservative and return false - only workspace packages with
|
|
416
|
+
// matching scope will be resolved
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Segments source files by type for efficient processing.
|
|
422
|
+
* Performs all path checks and AST inspections in a single pass.
|
|
423
|
+
*
|
|
424
|
+
* This runs once during construction and caches results to avoid
|
|
425
|
+
* multiple full-file iterations during parse methods.
|
|
426
|
+
*/
|
|
427
|
+
private segmentSourceFiles() {
|
|
428
|
+
const startTime = Date.now();
|
|
429
|
+
const allFiles = this.project.getSourceFiles();
|
|
430
|
+
|
|
431
|
+
let handlerTime = 0,
|
|
432
|
+
repoTime = 0,
|
|
433
|
+
toolTime = 0,
|
|
434
|
+
agentTime = 0,
|
|
435
|
+
jobTime = 0,
|
|
436
|
+
serviceTime = 0;
|
|
437
|
+
|
|
438
|
+
for (const sf of allFiles) {
|
|
439
|
+
const filePath = sf.getFilePath();
|
|
440
|
+
const fileStartTime = Date.now();
|
|
441
|
+
|
|
442
|
+
// Handlers: simple path check
|
|
443
|
+
if (filePath.includes("src/handlers/")) {
|
|
444
|
+
this.handlerFiles.push(sf);
|
|
445
|
+
handlerTime += Date.now() - fileStartTime;
|
|
446
|
+
continue; // Skip further checks (mutually exclusive)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Repos: simple path check
|
|
450
|
+
if (filePath.includes("src/repos/")) {
|
|
451
|
+
this.repoFiles.push(sf);
|
|
452
|
+
repoTime += Date.now() - fileStartTime;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Tools: path check + fast text-based detection
|
|
457
|
+
// Check if file contains "FlinkToolProps" without parsing AST (much faster)
|
|
458
|
+
if (filePath.includes("src/tools/")) {
|
|
459
|
+
const fileText = sf.getFullText();
|
|
460
|
+
|
|
461
|
+
// Fast text check: must have both "export" and ": FlinkToolProps"
|
|
462
|
+
if (fileText.includes("FlinkToolProps") && fileText.includes("export")) {
|
|
463
|
+
this.toolFiles.push(sf);
|
|
464
|
+
|
|
465
|
+
// Extract tool ID using regex (faster than AST inspection)
|
|
466
|
+
// Matches: id: "tool-name" or name: "tool-name"
|
|
467
|
+
const idMatch = fileText.match(/(?:id|name):\s*["']([^"']+)["']/);
|
|
468
|
+
if (idMatch) {
|
|
469
|
+
this.toolIdRegistry.add(idMatch[1]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
toolTime += Date.now() - fileStartTime;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Agents: convention-based detection (all .ts files in src/agents/)
|
|
477
|
+
// Trust that files in src/agents/ are valid agents - validate at runtime
|
|
478
|
+
if (filePath.includes("src/agents/")) {
|
|
479
|
+
this.agentFiles.push(sf);
|
|
480
|
+
agentTime += Date.now() - fileStartTime;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Jobs: simple path check
|
|
485
|
+
if (filePath.includes("src/jobs/")) {
|
|
486
|
+
this.jobFiles.push(sf);
|
|
487
|
+
jobTime += Date.now() - fileStartTime;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Services: simple path check (opt-out via flink.config.js disableServices)
|
|
492
|
+
if (!this.disableServices && filePath.includes("src/services/")) {
|
|
493
|
+
this.serviceFiles.push(sf);
|
|
494
|
+
serviceTime += Date.now() - fileStartTime;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Extension dirs from compiler plugins
|
|
499
|
+
for (const ext of this.compilerPlugins) {
|
|
500
|
+
if (filePath.includes(ext.scanDir)) {
|
|
501
|
+
if (!ext.detectBy || ext.detectBy(sf.getFullText(), filePath)) {
|
|
502
|
+
const list = this.extensionFiles.get(ext.generatedFile) ?? [];
|
|
503
|
+
list.push(sf);
|
|
504
|
+
this.extensionFiles.set(ext.generatedFile, list);
|
|
505
|
+
}
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const segmentTime = Date.now() - startTime;
|
|
512
|
+
perfLog.debug(
|
|
513
|
+
`✓ File segmentation completed in ${segmentTime}ms ` +
|
|
514
|
+
`(${this.handlerFiles.length} handlers, ${this.repoFiles.length} repos, ${this.toolFiles.length} tools, ${this.agentFiles.length} agents, ${this.jobFiles.length} jobs, ${this.serviceFiles.length} services)`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
79
518
|
/**
|
|
80
519
|
* Detects if the project is using ESM (ECMAScript Modules)
|
|
81
520
|
* by checking type in package.json.
|
|
@@ -98,16 +537,36 @@ class TypeScriptCompiler {
|
|
|
98
537
|
|
|
99
538
|
/**
|
|
100
539
|
* Gets the module specifier for imports, adding .js extension for ESM
|
|
540
|
+
* Uses fast path calculation instead of ts-morph's getRelativePathAsModuleSpecifierTo
|
|
541
|
+
* which triggers expensive language service initialization
|
|
101
542
|
*/
|
|
102
543
|
private getModuleSpecifier(fromFile: SourceFile, toFile: SourceFile): string {
|
|
103
|
-
|
|
544
|
+
const path = require("path");
|
|
545
|
+
|
|
546
|
+
// Get directory paths
|
|
547
|
+
const fromDir = path.dirname(fromFile.getFilePath());
|
|
548
|
+
const toPath = toFile.getFilePath();
|
|
549
|
+
|
|
550
|
+
// Calculate relative path
|
|
551
|
+
let relativePath = path.relative(fromDir, toPath);
|
|
552
|
+
|
|
553
|
+
// Convert to forward slashes (module specifiers use forward slashes)
|
|
554
|
+
relativePath = relativePath.replace(/\\/g, "/");
|
|
104
555
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
556
|
+
// Remove .ts extension
|
|
557
|
+
relativePath = relativePath.replace(/\.ts$/, "");
|
|
558
|
+
|
|
559
|
+
// Ensure it starts with ./ or ../
|
|
560
|
+
if (!relativePath.startsWith(".")) {
|
|
561
|
+
relativePath = "./" + relativePath;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Add .js extension for ESM imports
|
|
565
|
+
if (this.isEsm) {
|
|
566
|
+
relativePath += ".js";
|
|
108
567
|
}
|
|
109
568
|
|
|
110
|
-
return
|
|
569
|
+
return relativePath;
|
|
111
570
|
}
|
|
112
571
|
|
|
113
572
|
/**
|
|
@@ -132,10 +591,108 @@ class TypeScriptCompiler {
|
|
|
132
591
|
}
|
|
133
592
|
|
|
134
593
|
/**
|
|
135
|
-
*
|
|
594
|
+
* Saves all modified source files in a single batch operation.
|
|
595
|
+
* Call this before emit() to persist all changes to disk.
|
|
596
|
+
*/
|
|
597
|
+
async saveAllModifiedFiles() {
|
|
598
|
+
const startTime = Date.now();
|
|
599
|
+
|
|
600
|
+
// Only save files in .flink directory (don't persist metadata in source files)
|
|
601
|
+
const flinkFiles = this.project.getSourceFiles("**/.flink/**/*.ts");
|
|
602
|
+
const unsavedFlinkFiles = flinkFiles.filter((sf) => !sf.isSaved());
|
|
603
|
+
|
|
604
|
+
await Promise.all(unsavedFlinkFiles.map((sf) => sf.save()));
|
|
605
|
+
|
|
606
|
+
const saveTime = Date.now() - startTime;
|
|
607
|
+
perfLog.debug(`✓ Batch save completed in ${saveTime}ms (${unsavedFlinkFiles.length} files)`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Emits compiled javascript source to dist folder using swc (20-50x faster than tsc)
|
|
612
|
+
*/
|
|
613
|
+
async emitWithSwc() {
|
|
614
|
+
const emitStartTime = Date.now();
|
|
615
|
+
const swc = require("@swc/core");
|
|
616
|
+
const fs = require("fs-extra");
|
|
617
|
+
const path = require("path");
|
|
618
|
+
|
|
619
|
+
// Get all source files from the ts-morph project (includes src/, .flink/, spec/ when entry is a test runner)
|
|
620
|
+
// Uses in-memory text so generated files (.flink/) work without being saved to disk first
|
|
621
|
+
const allSourceFiles = this.project.getSourceFiles().filter((sf) => {
|
|
622
|
+
const filePath = sf.getFilePath();
|
|
623
|
+
return !filePath.endsWith(".d.ts") && !filePath.includes("/node_modules/");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
initLog.debug(`Starting swc compilation for ${allSourceFiles.length} source files...`);
|
|
627
|
+
|
|
628
|
+
// Transpile all files in parallel using swc
|
|
629
|
+
await Promise.all(
|
|
630
|
+
allSourceFiles.map(async (sf) => {
|
|
631
|
+
const filePath = sf.getFilePath();
|
|
632
|
+
const code = sf.getFullText();
|
|
633
|
+
const result = await swc.transform(code, {
|
|
634
|
+
filename: filePath,
|
|
635
|
+
jsc: {
|
|
636
|
+
parser: {
|
|
637
|
+
syntax: "typescript",
|
|
638
|
+
decorators: true,
|
|
639
|
+
tsx: false, // Flink doesn't use JSX
|
|
640
|
+
},
|
|
641
|
+
target: "es2017", // Async/await support, modern enough for Node 14+
|
|
642
|
+
transform: {
|
|
643
|
+
legacyDecorator: true,
|
|
644
|
+
decoratorMetadata: true, // Required for reflect-metadata
|
|
645
|
+
},
|
|
646
|
+
keepClassNames: true, // Preserve class names for debugging
|
|
647
|
+
},
|
|
648
|
+
module: {
|
|
649
|
+
type: this.isEsm ? "es6" : "commonjs",
|
|
650
|
+
},
|
|
651
|
+
sourceMaps: true,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Preserve directory structure: src/foo.ts → dist/src/foo.js
|
|
655
|
+
const relativePath = path.relative(this.cwd, filePath);
|
|
656
|
+
const outPath = path.join(this.cwd, "dist", relativePath).replace(/\.ts$/, ".js");
|
|
657
|
+
|
|
658
|
+
await fs.ensureDir(path.dirname(outPath));
|
|
659
|
+
await fs.writeFile(outPath, result.code);
|
|
660
|
+
|
|
661
|
+
if (result.map) {
|
|
662
|
+
await fs.writeFile(outPath + ".map", result.map);
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const emitTime = Date.now() - emitStartTime;
|
|
668
|
+
initLog.debug(`✓ Emitted ${allSourceFiles.length} files with swc in ${emitTime}ms (${(emitTime / allSourceFiles.length).toFixed(1)}ms per file)`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Emits compiled javascript source to dist folder (defaults to swc for speed, falls back to tsc)
|
|
673
|
+
*/
|
|
674
|
+
async emit() {
|
|
675
|
+
try {
|
|
676
|
+
require.resolve("@swc/core");
|
|
677
|
+
return await this.emitWithSwc();
|
|
678
|
+
} catch {
|
|
679
|
+
initLog.debug("@swc/core not found, falling back to TypeScript compiler (slower)");
|
|
680
|
+
return this.emitWithTsc();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Emits compiled javascript source to dist folder using TypeScript compiler (slower, kept for fallback)
|
|
136
686
|
*/
|
|
137
|
-
|
|
687
|
+
emitWithTsc() {
|
|
688
|
+
const emitStartTime = Date.now();
|
|
689
|
+
const sourceFileCount = this.project.getSourceFiles().length;
|
|
690
|
+
perfLog.debug(`Starting TypeScript compilation for ${sourceFileCount} source files...`);
|
|
691
|
+
|
|
138
692
|
this.project.emitSync();
|
|
693
|
+
|
|
694
|
+
const emitTime = Date.now() - emitStartTime;
|
|
695
|
+
perfLog.debug(`✓ Emitted ${sourceFileCount} files with tsc in ${emitTime}ms (${(emitTime / sourceFileCount).toFixed(1)}ms per file)`);
|
|
139
696
|
}
|
|
140
697
|
|
|
141
698
|
/**
|
|
@@ -179,7 +736,9 @@ class TypeScriptCompiler {
|
|
|
179
736
|
* Also extract handlers request and response schemas from Handler
|
|
180
737
|
* type arguments.
|
|
181
738
|
*/
|
|
182
|
-
async parseHandlers(
|
|
739
|
+
async parseHandlers() {
|
|
740
|
+
const startTime = Date.now();
|
|
741
|
+
|
|
183
742
|
const generatedFile = this.createSourceFile(
|
|
184
743
|
["generatedHandlers.ts"],
|
|
185
744
|
`// Generated ${new Date()}
|
|
@@ -188,19 +747,25 @@ export const handlers = [];
|
|
|
188
747
|
autoRegisteredHandlers.push(...handlers);
|
|
189
748
|
`
|
|
190
749
|
);
|
|
750
|
+
|
|
191
751
|
const handlersArr = generatedFile.getVariableDeclarationOrThrow("handlers").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
192
752
|
|
|
193
753
|
const handlers = await this.parseHandlerDir(generatedFile, handlersArr);
|
|
194
754
|
|
|
195
755
|
generatedFile.addImportDeclarations(handlers.imports);
|
|
196
756
|
|
|
197
|
-
|
|
757
|
+
// Defer save until batch save at end (performance optimization)
|
|
198
758
|
|
|
199
|
-
|
|
759
|
+
// Store handler schemas for later batch processing
|
|
760
|
+
this.handlerSchemasToGenerate = handlers.schemasToGenerate;
|
|
200
761
|
|
|
201
|
-
|
|
762
|
+
// Cleanup: forget handler file nodes to reduce memory overhead (ts-morph performance optimization)
|
|
763
|
+
this.handlerFiles.forEach((sf) => {
|
|
764
|
+
sf.getClasses().forEach((cls) => cls.forget());
|
|
765
|
+
});
|
|
202
766
|
|
|
203
|
-
|
|
767
|
+
const handlerParseTime = Date.now() - startTime;
|
|
768
|
+
perfLog.info(`✓ Handler parsing completed in ${handlerParseTime}ms`);
|
|
204
769
|
|
|
205
770
|
return generatedFile;
|
|
206
771
|
}
|
|
@@ -210,75 +775,55 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
210
775
|
*/
|
|
211
776
|
private async parseHandlerDir(generatedFile: SourceFile, handlersArr: ArrayLiteralExpression) {
|
|
212
777
|
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
213
|
-
let i = 0;
|
|
214
778
|
const schemasToGenerate: {
|
|
215
779
|
reqSchemaType?: string;
|
|
216
780
|
resSchemaType?: string;
|
|
217
781
|
sourceFile: SourceFile;
|
|
218
782
|
}[] = [];
|
|
219
783
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
784
|
+
// Use pre-segmented handler files (no filtering needed)
|
|
785
|
+
const handlerFiles = this.handlerFiles;
|
|
786
|
+
|
|
787
|
+
// Process handlers in parallel using text-based schema extraction
|
|
788
|
+
const handlerResults = await Promise.all(
|
|
789
|
+
handlerFiles.map(async (sf, index) => {
|
|
790
|
+
const startTime = Date.now();
|
|
791
|
+
const isAutoRegister = this.isAutoRegisterableHandler(sf);
|
|
792
|
+
const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + index;
|
|
793
|
+
const assumedHttpMethod = getHttpMethodFromHandlerName(sf.getBaseName());
|
|
794
|
+
|
|
795
|
+
// Extract schema information using text parsing
|
|
796
|
+
const schemaTypes = isAutoRegister ? await this.extractSchemasFromHandlerFast(sf.getFilePath()) : undefined;
|
|
797
|
+
|
|
798
|
+
perfLog.trace(`${sf.getBaseName()} took ${Date.now() - startTime}ms`);
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
sf,
|
|
802
|
+
isAutoRegister,
|
|
803
|
+
namespaceImport,
|
|
804
|
+
assumedHttpMethod,
|
|
805
|
+
schemaTypes,
|
|
806
|
+
index,
|
|
807
|
+
};
|
|
808
|
+
})
|
|
809
|
+
);
|
|
226
810
|
|
|
227
|
-
|
|
811
|
+
// Now process results sequentially to maintain order and update source files
|
|
812
|
+
// Collect handler elements first, then add in one batch
|
|
813
|
+
const handlerElements: string[] = [];
|
|
228
814
|
|
|
229
|
-
|
|
815
|
+
for (const result of handlerResults) {
|
|
816
|
+
const { sf, isAutoRegister, namespaceImport, assumedHttpMethod, schemaTypes } = result;
|
|
230
817
|
|
|
231
818
|
imports.push({
|
|
232
819
|
defaultImport: "* as " + namespaceImport,
|
|
233
820
|
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
234
821
|
});
|
|
235
822
|
|
|
236
|
-
const assumedHttpMethod = getHttpMethodFromHandlerName(sf.getBaseName());
|
|
237
|
-
|
|
238
|
-
// Only extract schemas for auto-registered handlers
|
|
239
|
-
const schemaTypes = isAutoRegister ? await this.extractSchemasFromHandlerSourceFile(sf) : undefined;
|
|
240
|
-
|
|
241
|
-
// Remove existing metadata variables if they exist (to avoid redeclaration errors)
|
|
242
|
-
const existingVars = sf.getVariableStatements().filter((vs) => {
|
|
243
|
-
const varNames = vs.getDeclarations().map((d) => d.getName());
|
|
244
|
-
return varNames.some((name) => ["__assumedHttpMethod", "__file", "__query", "__params"].includes(name));
|
|
245
|
-
});
|
|
246
|
-
existingVars.forEach((v) => v.remove());
|
|
247
|
-
|
|
248
|
-
// Append schemas and metadata to source file that will be part of emitted dist bundle (javascript)
|
|
249
|
-
sf.addVariableStatement({
|
|
250
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
251
|
-
isExported: true,
|
|
252
|
-
declarations: [
|
|
253
|
-
{
|
|
254
|
-
name: "__assumedHttpMethod",
|
|
255
|
-
initializer: `"${assumedHttpMethod || ""}"`,
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: "__file",
|
|
259
|
-
initializer: `"${sf.getBaseName()}"`,
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
name: "__query",
|
|
263
|
-
initializer: `[${(schemaTypes?.queryMetadata || [])
|
|
264
|
-
.map(({ description, name }) => `{description: "${description}", name: "${name}"}`)
|
|
265
|
-
.join(",")}]`,
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
name: "__params",
|
|
269
|
-
initializer: `[${(schemaTypes?.paramsMetadata || [])
|
|
270
|
-
.map(({ description, name }) => `{description: "${description}", name: "${name}"}`)
|
|
271
|
-
.join(",")}]`,
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
});
|
|
275
|
-
|
|
276
823
|
if (isAutoRegister) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
`{handler: ${namespaceImport}, assumedHttpMethod: ${assumedHttpMethod ? "HttpMethod." + assumedHttpMethod : undefined}}`
|
|
824
|
+
handlerElements.push(
|
|
825
|
+
`{handler: ${namespaceImport}, assumedHttpMethod: ${assumedHttpMethod ? "HttpMethod." + assumedHttpMethod : undefined}, __file: "${this.getRelativePath(sf)}"}`
|
|
280
826
|
);
|
|
281
|
-
i++;
|
|
282
827
|
|
|
283
828
|
// Add schemas to generate list
|
|
284
829
|
if (schemaTypes) {
|
|
@@ -287,6 +832,9 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
287
832
|
}
|
|
288
833
|
}
|
|
289
834
|
|
|
835
|
+
// Add all handler elements in one batch operation
|
|
836
|
+
handlersArr.addElements(handlerElements);
|
|
837
|
+
|
|
290
838
|
return {
|
|
291
839
|
imports,
|
|
292
840
|
schemasToGenerate,
|
|
@@ -294,6 +842,8 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
294
842
|
}
|
|
295
843
|
|
|
296
844
|
async parseRepos() {
|
|
845
|
+
const startTime = Date.now();
|
|
846
|
+
|
|
297
847
|
const generatedFile = this.createSourceFile(
|
|
298
848
|
["generatedRepos.ts"],
|
|
299
849
|
`// Generated ${new Date()}
|
|
@@ -307,13 +857,13 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
307
857
|
|
|
308
858
|
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
309
859
|
|
|
310
|
-
|
|
860
|
+
// Use pre-segmented repo files (no filtering needed)
|
|
861
|
+
const repoFiles = this.repoFiles;
|
|
311
862
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
863
|
+
// Collect all repo elements first, then add in one batch
|
|
864
|
+
const repoElements: string[] = [];
|
|
316
865
|
|
|
866
|
+
for (const sf of repoFiles) {
|
|
317
867
|
console.log(`Detected repo ${sf.getBaseName()}`);
|
|
318
868
|
|
|
319
869
|
imports.push({
|
|
@@ -321,531 +871,517 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
321
871
|
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
322
872
|
});
|
|
323
873
|
|
|
324
|
-
|
|
325
|
-
i,
|
|
874
|
+
repoElements.push(
|
|
326
875
|
`{collectionName: "${getCollectionNameForRepo(sf.getBaseName())}", repoInstanceName: "${getRepoInstanceName(
|
|
327
876
|
sf.getBaseName()
|
|
328
877
|
)}", Repo: ${sf.getBaseNameWithoutExtension()}}`
|
|
329
878
|
);
|
|
330
|
-
|
|
331
|
-
i++;
|
|
332
879
|
}
|
|
333
880
|
|
|
881
|
+
// Add all repo elements in one batch operation
|
|
882
|
+
reposArr.addElements(repoElements);
|
|
883
|
+
|
|
334
884
|
generatedFile.addImportDeclarations(imports);
|
|
335
885
|
|
|
336
|
-
|
|
886
|
+
// Defer save until batch save at end (performance optimization)
|
|
887
|
+
|
|
888
|
+
// Cleanup: forget repo file nodes to reduce memory overhead (ts-morph performance optimization)
|
|
889
|
+
this.repoFiles.forEach((sf) => {
|
|
890
|
+
sf.getClasses().forEach((cls) => cls.forget());
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
const repoParseTime = Date.now() - startTime;
|
|
894
|
+
initLog.info(`✓ Repo parsing completed in ${repoParseTime}ms (${repoElements.length} repos)`);
|
|
337
895
|
|
|
338
896
|
return generatedFile;
|
|
339
897
|
}
|
|
340
898
|
|
|
341
899
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
900
|
+
* Scans project for tools and adds those to Flink
|
|
901
|
+
* "singleton" property `autoRegisteredTools` so they can
|
|
902
|
+
* be registered during start.
|
|
344
903
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
904
|
+
* Also extracts input and output schemas from FlinkTool type arguments
|
|
905
|
+
* when manual schemas are not provided.
|
|
347
906
|
*/
|
|
348
|
-
async
|
|
349
|
-
|
|
350
|
-
console.error(`Cannot find entry script '${appEntryScript}'`);
|
|
351
|
-
return process.exit(1);
|
|
352
|
-
}
|
|
907
|
+
async parseTools() {
|
|
908
|
+
const startTime = Date.now();
|
|
353
909
|
|
|
354
|
-
|
|
355
|
-
|
|
910
|
+
// Initialize schema generator BEFORE schema extraction
|
|
911
|
+
await this.initSchemaGenerator();
|
|
912
|
+
|
|
913
|
+
const generatedFile = this.createSourceFile(
|
|
914
|
+
["generatedTools.ts"],
|
|
356
915
|
`// Generated ${new Date()}
|
|
357
|
-
import
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
export default {}; // Export an empty object to make it a module
|
|
362
|
-
`
|
|
916
|
+
import { autoRegisteredTools } from "@flink-app/flink";
|
|
917
|
+
export const tools = [];
|
|
918
|
+
autoRegisteredTools.push(...tools);
|
|
919
|
+
`
|
|
363
920
|
);
|
|
364
921
|
|
|
365
|
-
|
|
922
|
+
const toolsArr = generatedFile.getVariableDeclarationOrThrow("tools").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
366
923
|
|
|
367
|
-
|
|
368
|
-
|
|
924
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
925
|
+
const schemasToGenerate: {
|
|
926
|
+
inputSchemaType?: string;
|
|
927
|
+
outputSchemaType?: string;
|
|
928
|
+
inputTypeHint?: "void" | "any" | "named";
|
|
929
|
+
outputTypeHint?: "void" | "any" | "named";
|
|
930
|
+
sourceFile: SourceFile;
|
|
931
|
+
}[] = [];
|
|
369
932
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
933
|
+
// Use pre-segmented tool files (filtering and AST inspection already done)
|
|
934
|
+
const toolFiles = this.toolFiles;
|
|
935
|
+
|
|
936
|
+
// Process tools synchronously to avoid ts-morph lazy parsing overhead
|
|
937
|
+
const toolResults: any[] = [];
|
|
938
|
+
for (let index = 0; index < toolFiles.length; index++) {
|
|
939
|
+
const sf = toolFiles[index];
|
|
940
|
+
(function () {
|
|
941
|
+
// Get file path ONCE and cache it to avoid triggering ts-morph lazy parsing
|
|
942
|
+
const filePath = sf.getFilePath();
|
|
943
|
+
const path = require("path");
|
|
944
|
+
const baseNameWithoutExt = path.basename(filePath, path.extname(filePath));
|
|
945
|
+
|
|
946
|
+
const namespaceImport = baseNameWithoutExt.replace(/\./g, "_") + "_" + index;
|
|
947
|
+
|
|
948
|
+
// Extract the export name using fast text matching instead of AST parsing
|
|
949
|
+
// Read file directly from disk to avoid ts-morph's lazy parsing
|
|
950
|
+
// Matches: export const Tool: FlinkToolProps or export const MyName: FlinkToolProps
|
|
951
|
+
const fileText = fs.readFileSync(filePath, "utf8");
|
|
952
|
+
|
|
953
|
+
const exportMatch = fileText.match(/export\s+const\s+(\w+)\s*:\s*FlinkToolProps/);
|
|
954
|
+
const toolPropsExportName = exportMatch ? exportMatch[1] : "Tool"; // Default to "Tool" if not found
|
|
955
|
+
|
|
956
|
+
toolResults.push({
|
|
957
|
+
sf,
|
|
958
|
+
filePath, // Cache file path to avoid triggering sf.getFilePath() later
|
|
959
|
+
namespaceImport,
|
|
960
|
+
toolPropsExportName,
|
|
961
|
+
index,
|
|
962
|
+
});
|
|
963
|
+
})();
|
|
964
|
+
}
|
|
375
965
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
* Handler<Ctx, {car: Car}>
|
|
390
|
-
* // Inline type definition with literal values
|
|
391
|
-
* Handler<Ctx, {car: {model: string}}>
|
|
392
|
-
* // Array
|
|
393
|
-
* Handler<Ctx, Car[]>
|
|
394
|
-
* // Array with inline type definition
|
|
395
|
-
* Handler<Ctx, {car: Car}[]>
|
|
396
|
-
* ```
|
|
397
|
-
*
|
|
398
|
-
* Return names of req and/or res schema types.
|
|
399
|
-
*/
|
|
400
|
-
private async extractSchemasFromHandlerSourceFile(handlerSourceFile: SourceFile) {
|
|
401
|
-
const defaultExport = getDefaultExport(handlerSourceFile);
|
|
966
|
+
// Schema extraction phase - separate from metadata collection
|
|
967
|
+
for (const result of toolResults) {
|
|
968
|
+
// Extract schema information using text-based parsing
|
|
969
|
+
const schemaTypes = await this.extractSchemasFromToolFast(result.filePath);
|
|
970
|
+
|
|
971
|
+
schemasToGenerate.push({
|
|
972
|
+
inputSchemaType: schemaTypes?.inputSchemaType,
|
|
973
|
+
outputSchemaType: schemaTypes?.outputSchemaType,
|
|
974
|
+
inputTypeHint: schemaTypes?.inputTypeHint,
|
|
975
|
+
outputTypeHint: schemaTypes?.outputTypeHint,
|
|
976
|
+
sourceFile: result.sf,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
402
979
|
|
|
403
|
-
|
|
404
|
-
|
|
980
|
+
const toolParseTime = Date.now() - startTime;
|
|
981
|
+
initLog.info(`✓ Tool parsing completed in ${toolParseTime}ms (${toolFiles.length} tools)`);
|
|
405
982
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
}
|
|
983
|
+
// Now process results sequentially to maintain order and update source files
|
|
984
|
+
// Collect tool elements first, then add in one batch
|
|
985
|
+
const toolElements: string[] = [];
|
|
411
986
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
*/
|
|
415
|
-
private copyInterfaceWithDependencies(interfaceDecl: any, handlerFile: SourceFile): void {
|
|
416
|
-
const interfaceName = interfaceDecl.getName?.() || interfaceDecl.getFirstChildByKind(SyntaxKind.Identifier)?.getText();
|
|
417
|
-
if (!interfaceName) return;
|
|
987
|
+
for (const result of toolResults) {
|
|
988
|
+
const { sf, namespaceImport, toolPropsExportName, schemaTypes } = result;
|
|
418
989
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
.map((typeRefNode: any) => typeRefNode.getFirstChildIfKindOrThrow(SyntaxKind.Identifier));
|
|
434
|
-
|
|
435
|
-
for (const typeRefIdentifier of typeRefIdentifiers) {
|
|
436
|
-
const typeSymbol = typeRefIdentifier.getSymbol();
|
|
437
|
-
if (typeSymbol) {
|
|
438
|
-
const declaredType = typeSymbol.getDeclaredType();
|
|
439
|
-
const declaration = declaredType.getSymbol()?.getDeclarations()[0];
|
|
440
|
-
if (declaration && declaration.getSourceFile() === handlerFile) {
|
|
441
|
-
// Same file - recursively copy this dependency
|
|
442
|
-
this.copyInterfaceWithDependencies(declaration, handlerFile);
|
|
443
|
-
} else if (declaration && declaration.getSourceFile() !== handlerFile) {
|
|
444
|
-
// Different file - add to imports
|
|
445
|
-
const declaredTypeSymbol = declaredType.getSymbol();
|
|
446
|
-
if (declaredTypeSymbol) {
|
|
447
|
-
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
990
|
+
imports.push({
|
|
991
|
+
defaultImport: "* as " + namespaceImport,
|
|
992
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Create an object that wraps the namespace and provides normalized access to the tool props
|
|
996
|
+
// This creates a consistent "Tool" property regardless of the source export name
|
|
997
|
+
// Example: {Tool: SearchCarsTool_0.MyCustomToolConfig} works even if export was "MyCustomToolConfig"
|
|
998
|
+
// __file is set here on the registration object (not injected into source files)
|
|
999
|
+
toolElements.push(`{...${namespaceImport}, Tool: ${namespaceImport}.${toolPropsExportName}, __file: "${this.getRelativePath(sf)}"}`);
|
|
1000
|
+
|
|
1001
|
+
// Add schemas to generate list
|
|
1002
|
+
if (schemaTypes) {
|
|
1003
|
+
schemasToGenerate.push({ ...schemaTypes, sourceFile: sf });
|
|
450
1004
|
}
|
|
451
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
// Add all tool elements in one batch operation
|
|
1008
|
+
toolsArr.addElements(toolElements);
|
|
1009
|
+
|
|
1010
|
+
generatedFile.addImportDeclarations(imports);
|
|
1011
|
+
|
|
1012
|
+
// Defer save until batch save at end (performance optimization)
|
|
1013
|
+
|
|
1014
|
+
// Store tool schemas for later batch processing
|
|
1015
|
+
this.toolSchemasToGenerate = schemasToGenerate;
|
|
1016
|
+
|
|
1017
|
+
// Cleanup: forget tool file nodes to reduce memory overhead (ts-morph performance optimization)
|
|
1018
|
+
// Note: We avoid calling methods on sf during iteration since we cached all needed data earlier
|
|
1019
|
+
this.toolFiles.forEach((sf) => {
|
|
1020
|
+
// Only forget if we haven't already processed this file
|
|
1021
|
+
const classes = sf.getClasses();
|
|
1022
|
+
classes.forEach((cls) => cls.forget());
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
return generatedFile;
|
|
452
1026
|
}
|
|
453
1027
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1028
|
+
/**
|
|
1029
|
+
* Scans project for agents and validates tool references.
|
|
1030
|
+
* Agents are classes extending FlinkAgent exported as default.
|
|
1031
|
+
*/
|
|
1032
|
+
async parseAgents() {
|
|
1033
|
+
const startTime = Date.now();
|
|
458
1034
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1035
|
+
const generatedFile = this.createSourceFile(
|
|
1036
|
+
["generatedAgents.ts"],
|
|
1037
|
+
`// Generated ${new Date()}
|
|
1038
|
+
import { autoRegisteredAgents } from "@flink-app/flink";
|
|
1039
|
+
export const agents = [];
|
|
1040
|
+
autoRegisteredAgents.push(...agents);
|
|
1041
|
+
`
|
|
1042
|
+
);
|
|
464
1043
|
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
let generatedSchemaInterfaceStr = "";
|
|
468
|
-
|
|
469
|
-
const schemaInterfaceName = `${handlerFileName}_${suffix}`;
|
|
470
|
-
|
|
471
|
-
if (schema.isInterface()) {
|
|
472
|
-
/*
|
|
473
|
-
* Type argument is an interface. This should be normal case when
|
|
474
|
-
* schema is defined directly for example `Handler<Ctx, Car>`
|
|
475
|
-
*/
|
|
476
|
-
const schemaSymbol = schema.getSymbolOrThrow();
|
|
477
|
-
const interfaceName = getInterfaceName(schemaSymbol);
|
|
478
|
-
const declaration = schemaSymbol.getDeclarations()[0];
|
|
479
|
-
|
|
480
|
-
if (declaration.getSourceFile() === handlerFile) {
|
|
481
|
-
// Interface is declared within handler file
|
|
482
|
-
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} {
|
|
483
|
-
${schema
|
|
484
|
-
.getProperties()
|
|
485
|
-
.map((p) => p.getValueDeclarationOrThrow().getText())
|
|
486
|
-
.join("\n")}
|
|
487
|
-
}`;
|
|
488
|
-
|
|
489
|
-
for (const typeToImport of getTypesToImport(declaration)) {
|
|
490
|
-
const typeSymbol = typeToImport.getSymbol();
|
|
491
|
-
if (typeSymbol) {
|
|
492
|
-
const declaredTypeSymbol = typeSymbol.getDeclaredType().getSymbol();
|
|
493
|
-
if (declaredTypeSymbol) {
|
|
494
|
-
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
1044
|
+
const agentsArr = generatedFile.getVariableDeclarationOrThrow("agents").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
498
1045
|
|
|
499
|
-
|
|
500
|
-
for (const prop of schema.getProperties()) {
|
|
501
|
-
const propDecl = prop.getValueDeclaration();
|
|
502
|
-
if (propDecl) {
|
|
503
|
-
const propText = propDecl.getText();
|
|
504
|
-
// Match interface names in patterns like: Partial<InterfaceName["prop"]>
|
|
505
|
-
const interfaceNameMatches = propText.match(/\b([A-Z][a-zA-Z0-9]*)\s*\[/g);
|
|
506
|
-
if (interfaceNameMatches) {
|
|
507
|
-
for (const match of interfaceNameMatches) {
|
|
508
|
-
const referencedInterfaceName = match.replace(/\s*\[$/, '').trim();
|
|
509
|
-
// Try to find this interface in the handler file
|
|
510
|
-
const referencedInterfaceDecl = handlerFile.getInterface(referencedInterfaceName) || handlerFile.getTypeAlias(referencedInterfaceName);
|
|
511
|
-
if (referencedInterfaceDecl) {
|
|
512
|
-
// Interface is in same file - copy it and all its dependencies recursively
|
|
513
|
-
this.copyInterfaceWithDependencies(referencedInterfaceDecl, handlerFile);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
// Interface is imported from other file
|
|
521
|
-
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends ${interfaceName} {}`;
|
|
522
|
-
this.tsSchemasSymbolsToImports.push(schemaSymbol);
|
|
523
|
-
}
|
|
524
|
-
} else if (schema.isArray()) {
|
|
525
|
-
const arrayTypeArg = schema.getTypeArguments()[0];
|
|
526
|
-
const schemaSymbol = arrayTypeArg.getSymbolOrThrow();
|
|
527
|
-
const interfaceName = schemaSymbol.getEscapedName();
|
|
528
|
-
const declaration = schemaSymbol.getDeclarations()[0];
|
|
529
|
-
|
|
530
|
-
if (declaration.getSourceFile() !== handlerFile) {
|
|
531
|
-
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends Array<${interfaceName}> {}`;
|
|
532
|
-
this.tsSchemasSymbolsToImports.push(schemaSymbol);
|
|
533
|
-
} else {
|
|
534
|
-
if (arrayTypeArg.isInterface()) {
|
|
535
|
-
const props = arrayTypeArg
|
|
536
|
-
.getProperties()
|
|
537
|
-
.map((p) => p.getValueDeclarationOrThrow().getText())
|
|
538
|
-
.join(" ");
|
|
539
|
-
|
|
540
|
-
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends Array<{${props}}> {}`;
|
|
541
|
-
} else {
|
|
542
|
-
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends Array<${declaration.getText()}> {}`;
|
|
543
|
-
}
|
|
1046
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
544
1047
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (typeSymbol) {
|
|
548
|
-
const declaredTypeSymbol = typeSymbol.getDeclaredType().getSymbol();
|
|
549
|
-
if (declaredTypeSymbol) {
|
|
550
|
-
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
} else if (schema.isObject()) {
|
|
556
|
-
/*
|
|
557
|
-
* Schema is defined inline, for example `Handler<Ctx, {car: Car}>`
|
|
558
|
-
* We need extract `{car: Car}` into its own interface and make sure
|
|
559
|
-
* to import types if needed to
|
|
560
|
-
*/
|
|
561
|
-
|
|
562
|
-
// Try to get symbol - it may not exist for utility types (Partial, Omit, Pick, etc.)
|
|
563
|
-
const schemaSymbol = schema.getSymbol();
|
|
564
|
-
const declarations = schemaSymbol?.getDeclarations();
|
|
565
|
-
const declaration = declarations?.[0];
|
|
566
|
-
|
|
567
|
-
// Build property signatures using resolved types instead of source text
|
|
568
|
-
// This ensures generic type parameters are properly expanded
|
|
569
|
-
const propertySignatures = schema.getProperties().map((prop) => {
|
|
570
|
-
const propName = prop.getName();
|
|
571
|
-
const propType = prop.getTypeAtLocation(handlerFile);
|
|
572
|
-
const propTypeText = propType.getText(undefined, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
573
|
-
|
|
574
|
-
// Check if property is optional
|
|
575
|
-
// For utility types (Omit, Pick, etc.), properties may not have value declarations
|
|
576
|
-
const valueDeclaration = prop.getValueDeclaration();
|
|
577
|
-
let isOptional = false;
|
|
578
|
-
|
|
579
|
-
if (valueDeclaration) {
|
|
580
|
-
// Property has a source declaration (normal case)
|
|
581
|
-
isOptional = valueDeclaration.getType().isNullable() || (valueDeclaration.compilerNode as any).questionToken !== undefined;
|
|
582
|
-
} else {
|
|
583
|
-
// Property is synthetic (from utility types like Omit, Pick, etc.)
|
|
584
|
-
// Check if the property itself is optional by examining the symbol flags
|
|
585
|
-
isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional);
|
|
586
|
-
}
|
|
1048
|
+
// Use pre-built tool ID registry (no scan needed)
|
|
1049
|
+
const registeredToolIds = this.toolIdRegistry;
|
|
587
1050
|
|
|
588
|
-
|
|
589
|
-
|
|
1051
|
+
// Collect agent elements first, then add in one batch
|
|
1052
|
+
const agentElements: string[] = [];
|
|
590
1053
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1054
|
+
// Use pre-segmented agent files (convention-based: all files in src/agents/ are agents)
|
|
1055
|
+
this.agentFiles.forEach((sf, index) => {
|
|
1056
|
+
// Convention-based approach: Trust that all files in src/agents/ are valid agents
|
|
1057
|
+
// Find the first exported class (assume it's the agent)
|
|
1058
|
+
const agentClass = sf.getClasses().find((cls) => cls.isExported() || cls.isDefaultExport());
|
|
594
1059
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
this.tsSchemasSymbolsToImports.push(typeSymbol);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
1060
|
+
// Skip if no exported class found (shouldn't happen, but defensive)
|
|
1061
|
+
if (!agentClass) {
|
|
1062
|
+
perfLog.debug(`⚠ Skipping ${sf.getBaseName()} - no exported class found`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
603
1065
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1066
|
+
// Create unique namespace import (same pattern as handlers and tools)
|
|
1067
|
+
const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + index;
|
|
1068
|
+
|
|
1069
|
+
// Validate tool references exist (read from class properties)
|
|
1070
|
+
const toolsProperty = agentClass.getProperty("tools");
|
|
1071
|
+
if (toolsProperty) {
|
|
1072
|
+
const initializer = toolsProperty.getInitializer();
|
|
1073
|
+
if (initializer && initializer.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
1074
|
+
const toolsArray = initializer as any;
|
|
1075
|
+
const toolElements = toolsArray.getElements();
|
|
1076
|
+
for (const toolElement of toolElements) {
|
|
1077
|
+
let toolName: string;
|
|
1078
|
+
|
|
1079
|
+
// Handle string literals, method calls, and identifier references (tool imports)
|
|
1080
|
+
if (toolElement.getKind() === SyntaxKind.StringLiteral) {
|
|
1081
|
+
// Direct string: "tool-name"
|
|
1082
|
+
toolName = toolElement.getText().replace(/['"]/g, "");
|
|
1083
|
+
} else if (toolElement.getKind() === SyntaxKind.CallExpression) {
|
|
1084
|
+
// Method call: this.useTool("tool-name")
|
|
1085
|
+
const args = toolElement.getArguments();
|
|
1086
|
+
if (args.length > 0 && args[0].getKind() === SyntaxKind.StringLiteral) {
|
|
1087
|
+
toolName = args[0].getText().replace(/['"]/g, "");
|
|
1088
|
+
} else {
|
|
1089
|
+
console.warn(`Agent ${sf.getBaseName()} has non-string tool reference, skipping validation`);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
} else if (toolElement.getKind() === SyntaxKind.Identifier) {
|
|
1093
|
+
// Tool file reference (imported): SearchCarsByBrandTool
|
|
1094
|
+
// Look up the import to find the actual tool file
|
|
1095
|
+
const importName = toolElement.getText();
|
|
1096
|
+
const importDecl = sf.getImportDeclarations().find((imp) => {
|
|
1097
|
+
const namedImports = imp.getNamedImports();
|
|
1098
|
+
return namedImports.some((ni) => ni.getName() === importName);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
if (!importDecl) {
|
|
1102
|
+
// Try namespace import (* as Foo)
|
|
1103
|
+
const namespaceImport = sf.getImportDeclarations().find((imp) => {
|
|
1104
|
+
return imp.getNamespaceImport()?.getText() === importName;
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
if (namespaceImport) {
|
|
1108
|
+
const moduleSpecifier = namespaceImport.getModuleSpecifierValue();
|
|
1109
|
+
// Extract tool ID from the tool file path
|
|
1110
|
+
// e.g., "../tools/SearchCarsByBrandTool" -> find in registeredToolIds
|
|
1111
|
+
const toolFileName = moduleSpecifier.split("/").pop()?.replace(/\.ts$/, "");
|
|
1112
|
+
const matchingTool = Array.from(registeredToolIds).find((id) => {
|
|
1113
|
+
// Try to match by searching for the tool ID
|
|
1114
|
+
// This is a heuristic - we'll validate it exists
|
|
1115
|
+
return true; // Skip validation for imported tools for now
|
|
1116
|
+
});
|
|
1117
|
+
// Skip validation - tool imports are validated at runtime
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
console.warn(`Agent ${sf.getBaseName()} references tool "${importName}" but it's not imported, skipping validation`);
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
615
1124
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const interfaceNameMatches = currentPropTypeText.match(/\b([A-Z][a-zA-Z0-9]*)\s*\[/g);
|
|
622
|
-
if (interfaceNameMatches) {
|
|
623
|
-
for (const match of interfaceNameMatches) {
|
|
624
|
-
const interfaceName = match.replace(/\s*\[$/, '').trim();
|
|
625
|
-
// Try to find this interface in the handler file
|
|
626
|
-
const interfaceDecl = handlerFile.getInterface(interfaceName) || handlerFile.getTypeAlias(interfaceName);
|
|
627
|
-
if (interfaceDecl) {
|
|
628
|
-
// Interface is in same file - copy it and all its dependencies recursively
|
|
629
|
-
this.copyInterfaceWithDependencies(interfaceDecl, handlerFile);
|
|
1125
|
+
// Skip validation for tool file references - they're validated at runtime
|
|
1126
|
+
continue;
|
|
1127
|
+
} else {
|
|
1128
|
+
console.warn(`Agent ${sf.getBaseName()} has unexpected tool reference format: ${toolElement.getText()}`);
|
|
1129
|
+
continue;
|
|
630
1130
|
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
1131
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
for (const typeArg of typeArgs) {
|
|
638
|
-
const argSymbol = typeArg.getSymbol();
|
|
639
|
-
if (argSymbol) {
|
|
640
|
-
const argDeclaration = argSymbol.getDeclarations()[0];
|
|
641
|
-
if (argDeclaration && argDeclaration.getSourceFile() !== handlerFile) {
|
|
642
|
-
this.tsSchemasSymbolsToImports.push(argSymbol);
|
|
643
|
-
}
|
|
1132
|
+
if (!registeredToolIds.has(toolName)) {
|
|
1133
|
+
console.error(`Agent ${sf.getBaseName()} references tool "${toolName}" which does not exist`);
|
|
1134
|
+
throw new Error(`Invalid tool reference in agent ${sf.getBaseName()}`);
|
|
644
1135
|
}
|
|
645
1136
|
}
|
|
646
1137
|
}
|
|
647
1138
|
}
|
|
648
1139
|
|
|
649
|
-
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (typeSymbol) {
|
|
654
|
-
const declaredTypeSymbol = typeSymbol.getDeclaredType().getSymbol();
|
|
655
|
-
if (declaredTypeSymbol) {
|
|
656
|
-
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
1140
|
+
const className = agentClass.getName();
|
|
1141
|
+
if (!className) {
|
|
1142
|
+
console.error(`Agent class in ${sf.getBaseName()} has no name`);
|
|
1143
|
+
return;
|
|
660
1144
|
}
|
|
661
1145
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
private initJsonSchemaGenerator(schemaFilePath: string) {
|
|
676
|
-
const tsconfigPath = join(this.cwd, "tsconfig.json");
|
|
677
|
-
const conf: CompletedConfig = {
|
|
678
|
-
path: schemaFilePath, // Point to the intermediate schema file
|
|
679
|
-
expose: "none", // Do not create shared $ref definitions.
|
|
680
|
-
topRef: false, // Removes the wrapper object around the schema.
|
|
681
|
-
additionalProperties: false,
|
|
682
|
-
jsDoc: "basic",
|
|
683
|
-
sortProps: false,
|
|
684
|
-
strictTuples: false,
|
|
685
|
-
minify: false,
|
|
686
|
-
markdownDescription: false,
|
|
687
|
-
skipTypeCheck: false,
|
|
688
|
-
encodeRefs: false,
|
|
689
|
-
extraTags: [],
|
|
690
|
-
functions: "fail",
|
|
691
|
-
discriminatorType: "json-schema",
|
|
692
|
-
tsconfig: tsconfigPath,
|
|
693
|
-
};
|
|
1146
|
+
imports.push({
|
|
1147
|
+
defaultImport: "* as " + namespaceImport,
|
|
1148
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// Register the agent class (access default export via namespace)
|
|
1152
|
+
// __file is set here on the registration object (not injected into source files)
|
|
1153
|
+
agentElements.push(`{ default: ${namespaceImport}.default, __file: "${this.getRelativePath(sf)}" }`);
|
|
1154
|
+
});
|
|
694
1155
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
console.log(" tsconfig:", tsconfigPath);
|
|
1156
|
+
// Add all agent elements in one batch operation
|
|
1157
|
+
agentsArr.addElements(agentElements);
|
|
698
1158
|
|
|
699
|
-
|
|
700
|
-
// This ensures ts-json-schema-generator can find the types we just generated
|
|
701
|
-
let program;
|
|
702
|
-
try {
|
|
703
|
-
program = createProgram(conf);
|
|
704
|
-
} catch (error: any) {
|
|
705
|
-
// Format the error in a more developer-friendly way
|
|
706
|
-
console.error("\n❌ Schema generation failed due to TypeScript compilation errors:\n");
|
|
707
|
-
|
|
708
|
-
if (error.diagnostic && error.diagnostic.relatedInformation) {
|
|
709
|
-
// Extract and display only the relevant error messages
|
|
710
|
-
for (const info of error.diagnostic.relatedInformation) {
|
|
711
|
-
if (info.file) {
|
|
712
|
-
const { line, character } = info.file.getLineAndCharacterOfPosition(info.start);
|
|
713
|
-
const fileName = info.file.fileName.replace(this.cwd, ".");
|
|
714
|
-
const message = typeof info.messageText === "string" ? info.messageText : info.messageText.messageText;
|
|
715
|
-
|
|
716
|
-
console.error(` ${fileName}:${line + 1}:${character + 1}`);
|
|
717
|
-
console.error(` ${message}\n`);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
} else if (error.message) {
|
|
721
|
-
console.error(` ${error.message}\n`);
|
|
722
|
-
}
|
|
1159
|
+
generatedFile.addImportDeclarations(imports);
|
|
723
1160
|
|
|
724
|
-
|
|
725
|
-
process.exit(1);
|
|
726
|
-
}
|
|
1161
|
+
// Defer save until batch save at end (performance optimization)
|
|
727
1162
|
|
|
728
|
-
|
|
729
|
-
|
|
1163
|
+
// Cleanup: forget agent file nodes to reduce memory overhead (ts-morph performance optimization)
|
|
1164
|
+
this.agentFiles.forEach((sf) => {
|
|
1165
|
+
sf.getClasses().forEach((cls) => cls.forget());
|
|
1166
|
+
});
|
|
730
1167
|
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
const generator = new SchemaGenerator(program, parser, formatter, conf);
|
|
1168
|
+
const agentParseTime = Date.now() - startTime;
|
|
1169
|
+
initLog.info(`✓ Agent parsing completed in ${agentParseTime}ms (${agentElements.length} agents)`);
|
|
734
1170
|
|
|
735
|
-
return
|
|
1171
|
+
return generatedFile;
|
|
736
1172
|
}
|
|
737
1173
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* Generates a start script that will import references to handlers, repos and the
|
|
1176
|
+
* actual Flink app to start.
|
|
1177
|
+
*
|
|
1178
|
+
* Note that order is of importance so generated metadata are imported and initialized before start of flink app.
|
|
1179
|
+
* Otherwise singletons `autoRegisteredRepos` and `autoRegisteredHandlers` will not have been set.
|
|
1180
|
+
*/
|
|
1181
|
+
async generateStartScript(appEntryScript = "/src/index.ts") {
|
|
1182
|
+
// Add the entry script to the project if not already loaded
|
|
1183
|
+
const path = require("path");
|
|
1184
|
+
const entryScriptPath = path.resolve(this.cwd, appEntryScript.replace(/^\//, ""));
|
|
741
1185
|
|
|
742
|
-
|
|
1186
|
+
if (!fs.existsSync(entryScriptPath)) {
|
|
1187
|
+
console.error(`Cannot find entry script '${appEntryScript}' at ${entryScriptPath}`);
|
|
1188
|
+
return process.exit(1);
|
|
1189
|
+
}
|
|
743
1190
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1191
|
+
// Add to project if not already present
|
|
1192
|
+
if (!this.project.getSourceFile((sf) => sf.getFilePath().endsWith(appEntryScript))) {
|
|
1193
|
+
this.project.addSourceFileAtPath(entryScriptPath);
|
|
1194
|
+
|
|
1195
|
+
// If the entry script is in the spec directory (test runner), also add all spec files
|
|
1196
|
+
// This handles Jasmine test runners that load specs dynamically via glob patterns
|
|
1197
|
+
if (appEntryScript.includes("/spec/")) {
|
|
1198
|
+
const specFiles = await glob(join(this.cwd, "spec/**/*.ts"));
|
|
1199
|
+
this.project.addSourceFilesAtPaths(specFiles);
|
|
1200
|
+
console.log(`Added ${specFiles.length} spec files to compilation`);
|
|
750
1201
|
}
|
|
1202
|
+
|
|
1203
|
+
// Resolve any imports from the entry script
|
|
1204
|
+
this.resolveImportedFiles();
|
|
751
1205
|
}
|
|
752
1206
|
|
|
753
|
-
const
|
|
754
|
-
(
|
|
755
|
-
|
|
756
|
-
out.definitions = { ...out.definitions, ...schema.definitions };
|
|
757
|
-
}
|
|
758
|
-
return out;
|
|
759
|
-
},
|
|
760
|
-
{
|
|
761
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
762
|
-
$ref: "#/definitions/Schemas",
|
|
763
|
-
definitions: {},
|
|
764
|
-
}
|
|
765
|
-
);
|
|
1207
|
+
const extensionImports = this.compilerPlugins
|
|
1208
|
+
.map((ext) => `import "./${ext.generatedFile}${this.isEsm ? ".js" : ""}";`)
|
|
1209
|
+
.join("\n");
|
|
766
1210
|
|
|
767
|
-
const
|
|
1211
|
+
const sf = this.createSourceFile(
|
|
1212
|
+
["start.ts"],
|
|
1213
|
+
`// Generated ${new Date()}
|
|
1214
|
+
import "./generatedHandlers${this.isEsm ? ".js" : ""}";
|
|
1215
|
+
import "./generatedRepos${this.isEsm ? ".js" : ""}";
|
|
1216
|
+
import "./generatedTools${this.isEsm ? ".js" : ""}";
|
|
1217
|
+
import "./generatedAgents${this.isEsm ? ".js" : ""}";
|
|
1218
|
+
import "./generatedJobs${this.isEsm ? ".js" : ""}";
|
|
1219
|
+
import "./generatedServices${this.isEsm ? ".js" : ""}";
|
|
1220
|
+
${extensionImports ? extensionImports + "\n" : ""}import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
|
|
1221
|
+
export default {}; // Export an empty object to make it a module
|
|
1222
|
+
`
|
|
1223
|
+
);
|
|
768
1224
|
|
|
769
|
-
|
|
1225
|
+
// Defer save until batch save at end (performance optimization)
|
|
770
1226
|
|
|
771
|
-
|
|
1227
|
+
return sf;
|
|
1228
|
+
}
|
|
772
1229
|
|
|
773
|
-
|
|
1230
|
+
private createSourceFile(filename: string[], contents: string) {
|
|
1231
|
+
return this.project.createSourceFile(join(this.cwd, ".flink", ...filename), contents, {
|
|
1232
|
+
overwrite: true,
|
|
1233
|
+
});
|
|
774
1234
|
}
|
|
775
1235
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
//
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
let params: Type<ts.Type> | undefined;
|
|
793
|
-
let query: Type<ts.Type> | undefined;
|
|
794
|
-
|
|
795
|
-
if (handlerType === "Handler") {
|
|
796
|
-
// `Handler<Ctx, ReqSchema, ResSchema, Params, Query>`
|
|
797
|
-
// 0 = Ctx, 1 = Req schema, 2 = Res schema, 3 = Params, 4 = Query
|
|
798
|
-
reqSchema = handlerTypeArgs[1];
|
|
799
|
-
resSchema = handlerTypeArgs[2];
|
|
800
|
-
params = handlerTypeArgs[3];
|
|
801
|
-
query = handlerTypeArgs[4];
|
|
802
|
-
} else if (handlerType === "GetHandler") {
|
|
803
|
-
// `GetHandler<Ctx, ResSchema, Params, Query>`
|
|
804
|
-
// 0 = Ctx, 1 = Res schema, 2 = Params, 3 = Query
|
|
805
|
-
resSchema = handlerTypeArgs[1];
|
|
806
|
-
params = handlerTypeArgs[2];
|
|
807
|
-
query = handlerTypeArgs[3];
|
|
808
|
-
} else {
|
|
809
|
-
throw new Error(`Unknown handler type ${handlerType} in ${handlerTypeReference.getSourceFile().getBaseName()} - should be Handler or GetHandler`);
|
|
1236
|
+
/**
|
|
1237
|
+
* Extracts schema information from a tool file using text-based parsing.
|
|
1238
|
+
* Resolves type names to schema $ids from the schema universe.
|
|
1239
|
+
*/
|
|
1240
|
+
private async extractSchemasFromToolFast(filePath: string) {
|
|
1241
|
+
const fileText = fs.readFileSync(filePath, "utf8");
|
|
1242
|
+
|
|
1243
|
+
// Check if this tool already has schemas defined (Zod or JSON Schema)
|
|
1244
|
+
// These tools don't need TypeScript schema extraction
|
|
1245
|
+
const schemaDetection = TypeScriptSourceParser.detectSchemaType(fileText);
|
|
1246
|
+
|
|
1247
|
+
if (schemaDetection.shouldSkipTypeScriptExtraction) {
|
|
1248
|
+
return {
|
|
1249
|
+
inputSchemaType: undefined,
|
|
1250
|
+
outputSchemaType: undefined,
|
|
1251
|
+
};
|
|
810
1252
|
}
|
|
811
1253
|
|
|
812
|
-
|
|
1254
|
+
// Extract FlinkTool type arguments using utility parser
|
|
1255
|
+
const typeArgs = TypeScriptSourceParser.parseFlinkToolTypeArgs(fileText);
|
|
813
1256
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
:
|
|
1257
|
+
if (!typeArgs) {
|
|
1258
|
+
const baseName = require("path").basename(filePath, require("path").extname(filePath));
|
|
1259
|
+
perfLog.trace(` Tool ${baseName}: Could not parse FlinkTool type arguments`);
|
|
1260
|
+
return {
|
|
1261
|
+
inputSchemaType: undefined,
|
|
1262
|
+
outputSchemaType: undefined,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
817
1265
|
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
1266
|
+
const inputTypeName = typeArgs.inputType;
|
|
1267
|
+
const outputTypeName = typeArgs.outputType;
|
|
1268
|
+
|
|
1269
|
+
// Resolve type names to schema $ids
|
|
1270
|
+
let inputSchemaType: string | undefined = undefined;
|
|
1271
|
+
let outputSchemaType: string | undefined = undefined;
|
|
1272
|
+
let inputTypeHint: "void" | "any" | "named" | undefined = undefined;
|
|
1273
|
+
let outputTypeHint: "void" | "any" | "named" | undefined = undefined;
|
|
1274
|
+
|
|
1275
|
+
// Determine input type hint
|
|
1276
|
+
if (inputTypeName.toLowerCase() === "void") {
|
|
1277
|
+
inputTypeHint = "void";
|
|
1278
|
+
} else if (inputTypeName.toLowerCase() === "any") {
|
|
1279
|
+
inputTypeHint = "any";
|
|
1280
|
+
} else if (TypeScriptSourceParser.shouldGenerateSchema(inputTypeName)) {
|
|
1281
|
+
inputTypeHint = "named";
|
|
1282
|
+
inputSchemaType = this.resolveTypeNameToSchemaId(fileText, inputTypeName, filePath);
|
|
1283
|
+
if (!inputSchemaType) {
|
|
1284
|
+
const baseName = require("path").basename(filePath);
|
|
1285
|
+
perfLog.warn(`Tool ${baseName}: Could not resolve input type "${inputTypeName}" to schema $id. Make sure it's exported from src/schemas/`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
821
1288
|
|
|
822
|
-
|
|
1289
|
+
// Determine output type hint
|
|
1290
|
+
const unwrappedTypeName = TypeScriptSourceParser.unwrapToolResultType(outputTypeName);
|
|
1291
|
+
if (unwrappedTypeName.toLowerCase() === "void") {
|
|
1292
|
+
outputTypeHint = "void";
|
|
1293
|
+
} else if (unwrappedTypeName.toLowerCase() === "any") {
|
|
1294
|
+
outputTypeHint = "any";
|
|
1295
|
+
} else if (TypeScriptSourceParser.shouldGenerateSchema(unwrappedTypeName)) {
|
|
1296
|
+
outputTypeHint = "named";
|
|
1297
|
+
outputSchemaType = this.resolveTypeNameToSchemaId(fileText, unwrappedTypeName, filePath);
|
|
1298
|
+
if (!outputSchemaType) {
|
|
1299
|
+
const baseName = require("path").basename(filePath);
|
|
1300
|
+
perfLog.warn(`Tool ${baseName}: Could not resolve output type "${unwrappedTypeName}" to schema $id. Make sure it's exported from src/schemas/`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
823
1303
|
|
|
824
1304
|
return {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1305
|
+
inputSchemaType,
|
|
1306
|
+
outputSchemaType,
|
|
1307
|
+
inputTypeHint,
|
|
1308
|
+
outputTypeHint,
|
|
829
1309
|
};
|
|
830
1310
|
}
|
|
831
1311
|
|
|
832
1312
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
1313
|
+
* Extracts schema information from a handler file using text-based parsing.
|
|
1314
|
+
* Resolves type names to schema $ids from the schema universe.
|
|
835
1315
|
*/
|
|
836
|
-
private async
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1316
|
+
private async extractSchemasFromHandlerFast(filePath: string) {
|
|
1317
|
+
const fileText = fs.readFileSync(filePath, "utf8");
|
|
1318
|
+
|
|
1319
|
+
// Extract Handler type arguments using utility parser
|
|
1320
|
+
const typeArgs = TypeScriptSourceParser.parseHandlerTypeArgs(fileText);
|
|
1321
|
+
|
|
1322
|
+
if (!typeArgs) {
|
|
1323
|
+
const baseName = require("path").basename(filePath, require("path").extname(filePath));
|
|
1324
|
+
perfLog.trace(` Handler ${baseName}: Could not parse Handler type arguments`);
|
|
1325
|
+
return {
|
|
1326
|
+
reqSchemaType: undefined,
|
|
1327
|
+
resSchemaType: undefined,
|
|
1328
|
+
queryMetadata: [],
|
|
1329
|
+
paramsMetadata: [],
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const reqTypeName = typeArgs.reqType;
|
|
1334
|
+
const resTypeName = typeArgs.resType;
|
|
1335
|
+
const paramsTypeName = typeArgs.paramsType;
|
|
1336
|
+
const queryTypeName = typeArgs.queryType;
|
|
842
1337
|
|
|
843
|
-
|
|
1338
|
+
// Resolve type names to schema $ids
|
|
1339
|
+
let reqSchemaType: string | undefined = undefined;
|
|
1340
|
+
let resSchemaType: string | undefined = undefined;
|
|
844
1341
|
|
|
845
|
-
|
|
1342
|
+
// Check if handler skips validation — no need to warn about unresolved schema types
|
|
1343
|
+
const isSkipValidation = /ValidationMode\.SkipValidation/.test(fileText);
|
|
846
1344
|
|
|
847
|
-
|
|
848
|
-
|
|
1345
|
+
if (reqTypeName && TypeScriptSourceParser.shouldGenerateSchema(reqTypeName)) {
|
|
1346
|
+
reqSchemaType = this.resolveTypeNameToSchemaId(fileText, reqTypeName, filePath);
|
|
1347
|
+
if (!reqSchemaType && !isSkipValidation) {
|
|
1348
|
+
const baseName = require("path").basename(filePath);
|
|
1349
|
+
perfLog.warn(`Handler ${baseName}: Could not resolve request type "${reqTypeName}" to schema $id. Make sure it's exported from src/schemas/`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (resTypeName && TypeScriptSourceParser.shouldGenerateSchema(resTypeName)) {
|
|
1354
|
+
resSchemaType = this.resolveTypeNameToSchemaId(fileText, resTypeName, filePath);
|
|
1355
|
+
if (!resSchemaType && !isSkipValidation) {
|
|
1356
|
+
const baseName = require("path").basename(filePath);
|
|
1357
|
+
perfLog.warn(`Handler ${baseName}: Could not resolve response type "${resTypeName}" to schema $id. Make sure it's exported from src/schemas/`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Extract property metadata for params and query
|
|
1362
|
+
let paramsMetadata: any[] = [];
|
|
1363
|
+
let queryMetadata: any[] = [];
|
|
1364
|
+
|
|
1365
|
+
if (paramsTypeName && paramsTypeName !== "any" && paramsTypeName !== "void") {
|
|
1366
|
+
const metadata = TypeScriptSourceParser.extractPropertyMetadata(fileText, paramsTypeName);
|
|
1367
|
+
if (metadata) {
|
|
1368
|
+
paramsMetadata = metadata;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (queryTypeName && queryTypeName !== "any" && queryTypeName !== "void") {
|
|
1373
|
+
const metadata = TypeScriptSourceParser.extractPropertyMetadata(fileText, queryTypeName);
|
|
1374
|
+
if (metadata) {
|
|
1375
|
+
queryMetadata = metadata;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return {
|
|
1380
|
+
reqSchemaType,
|
|
1381
|
+
resSchemaType,
|
|
1382
|
+
queryMetadata,
|
|
1383
|
+
paramsMetadata,
|
|
1384
|
+
};
|
|
849
1385
|
}
|
|
850
1386
|
|
|
851
1387
|
/**
|
|
@@ -870,60 +1406,167 @@ ${this.parsedTsSchemas.join("\n\n")}`
|
|
|
870
1406
|
}
|
|
871
1407
|
|
|
872
1408
|
/**
|
|
873
|
-
*
|
|
1409
|
+
* Generates JSON schemas for all handlers and tools.
|
|
1410
|
+
* Should be called after parseHandlers() and parseTools() have completed.
|
|
874
1411
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
1412
|
+
* NEW SIMPLIFIED APPROACH:
|
|
1413
|
+
* 1. Generate schema universe from src/schemas/**\/*.ts (includes wrappers/)
|
|
1414
|
+
* 2. Handlers/tools reference schemas by $id
|
|
1415
|
+
* 3. Create manifest with schema universe and references
|
|
877
1416
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1417
|
+
async generateAllSchemas(): Promise<void> {
|
|
1418
|
+
const schemaGenStartTime = Date.now();
|
|
1419
|
+
|
|
1420
|
+
// Initialize schema generator
|
|
1421
|
+
await this.initSchemaGenerator();
|
|
1422
|
+
|
|
1423
|
+
if (!this.schemaGenerator) {
|
|
1424
|
+
throw new Error("Schema generator not initialized");
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const path = require("path");
|
|
1428
|
+
const schemaDir = path.join(this.cwd, "src/schemas");
|
|
1429
|
+
|
|
1430
|
+
// Check if schemas directory exists
|
|
1431
|
+
if (!fs.existsSync(schemaDir)) {
|
|
1432
|
+
perfLog.warn("No src/schemas/ directory found. Skipping schema generation.");
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Find all schema files in src/schemas/ (including subdirectories)
|
|
1437
|
+
const schemaFiles = await glob(path.join(schemaDir, "**/*.ts"));
|
|
1438
|
+
|
|
1439
|
+
perfLog.debug(`Found ${schemaFiles.length} schema files in src/schemas/`);
|
|
1440
|
+
|
|
1441
|
+
// Generate schemas from each file and merge into schema universe
|
|
1442
|
+
let schemaUniverse: Record<string, any> = {};
|
|
1443
|
+
|
|
1444
|
+
for (const schemaFile of schemaFiles) {
|
|
1445
|
+
const absolutePath = path.join(this.cwd, schemaFile);
|
|
1446
|
+
|
|
1447
|
+
try {
|
|
1448
|
+
const fileSchemas = this.schemaGenerator(absolutePath, {
|
|
1449
|
+
followImports: "local",
|
|
1450
|
+
schemaVersion: "http://json-schema.org/draft-07/schema#",
|
|
1451
|
+
includeJSDoc: true,
|
|
1452
|
+
strictObjects: false,
|
|
1453
|
+
additionalProperties: undefined,
|
|
1454
|
+
defineId: (typeName: string, declaration: any, context: any) => {
|
|
1455
|
+
if (!context) return typeName;
|
|
1456
|
+
|
|
1457
|
+
// Generate stable $id using same algorithm as resolveTypeNameToSchemaId
|
|
1458
|
+
return this.filePathToSchemaId(context.absolutePath, typeName);
|
|
1459
|
+
},
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
// Merge schemas from this file into universe
|
|
1463
|
+
schemaUniverse = { ...schemaUniverse, ...fileSchemas };
|
|
1464
|
+
} catch (error: any) {
|
|
1465
|
+
perfLog.warn(`Failed to generate schemas from ${schemaFile}: ${error.message}`);
|
|
892
1466
|
}
|
|
1467
|
+
}
|
|
893
1468
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1469
|
+
perfLog.debug(`Generated ${Object.keys(schemaUniverse).length} schemas from ${schemaFiles.length} files`);
|
|
1470
|
+
perfLog.debug(`Sample schema $ids: ${Object.keys(schemaUniverse).slice(0, 10).join(", ")}...`);
|
|
1471
|
+
|
|
1472
|
+
// Create manifest with schema universe and handler/tool references
|
|
1473
|
+
await this.generateSchemaManifest(schemaUniverse);
|
|
1474
|
+
|
|
1475
|
+
const schemaGenTime = Date.now() - schemaGenStartTime;
|
|
1476
|
+
initLog.info(
|
|
1477
|
+
`✓ Schema generation completed in ${schemaGenTime}ms ` +
|
|
1478
|
+
`(${Object.keys(schemaUniverse).length} schemas, ${this.handlerSchemasToGenerate.length} handlers, ${this.toolSchemasToGenerate.length} tools)`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Computes relative path from project root for a source file.
|
|
1484
|
+
* This is used consistently for manifest keys and __file exports.
|
|
1485
|
+
*/
|
|
1486
|
+
private getRelativePath(sf: SourceFile): string {
|
|
1487
|
+
const filePath = sf.getFilePath();
|
|
1488
|
+
return filePath.startsWith(this.cwd) ? filePath.substring(this.cwd.length + 1) : filePath;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Generates schema manifest with schema universe and handler/tool references.
|
|
1493
|
+
*/
|
|
1494
|
+
private async generateSchemaManifest(schemaUniverse: Record<string, any>): Promise<void> {
|
|
1495
|
+
const manifest: any = {
|
|
1496
|
+
version: "2.0",
|
|
1497
|
+
generated: new Date().toISOString(),
|
|
1498
|
+
schemas: schemaUniverse,
|
|
1499
|
+
handlers: {} as Record<string, { reqSchemaName?: string; resSchemaName?: string; queryMetadata?: any[]; paramsMetadata?: any[] }>,
|
|
1500
|
+
tools: {} as Record<
|
|
1501
|
+
string,
|
|
1502
|
+
{
|
|
1503
|
+
inputSchemaName?: string;
|
|
1504
|
+
outputSchemaName?: string;
|
|
1505
|
+
inputTypeHint?: "void" | "any" | "named";
|
|
1506
|
+
outputTypeHint?: "void" | "any" | "named";
|
|
1507
|
+
}
|
|
1508
|
+
>,
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
// Map handlers with their schema references
|
|
1512
|
+
for (const handler of this.handlerSchemasToGenerate) {
|
|
1513
|
+
const relativePath = this.getRelativePath(handler.sourceFile);
|
|
1514
|
+
|
|
1515
|
+
// Validate schema references exist
|
|
1516
|
+
if (handler.reqSchemaType && !schemaUniverse[handler.reqSchemaType]) {
|
|
1517
|
+
perfLog.warn(
|
|
1518
|
+
`Handler ${handler.sourceFile.getBaseName()} references request schema "${handler.reqSchemaType}" which was not found in schema universe`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
if (handler.resSchemaType && !schemaUniverse[handler.resSchemaType]) {
|
|
1522
|
+
perfLog.warn(
|
|
1523
|
+
`Handler ${handler.sourceFile.getBaseName()} references response schema "${handler.resSchemaType}" which was not found in schema universe`
|
|
1524
|
+
);
|
|
897
1525
|
}
|
|
898
1526
|
|
|
899
|
-
|
|
900
|
-
|
|
1527
|
+
manifest.handlers[relativePath] = {
|
|
1528
|
+
reqSchemaName: handler.reqSchemaType,
|
|
1529
|
+
resSchemaName: handler.resSchemaType,
|
|
1530
|
+
queryMetadata: (handler as any).queryMetadata || [],
|
|
1531
|
+
paramsMetadata: (handler as any).paramsMetadata || [],
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
901
1534
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1535
|
+
// Map tools with their schema references
|
|
1536
|
+
for (const tool of this.toolSchemasToGenerate) {
|
|
1537
|
+
const relativePath = this.getRelativePath(tool.sourceFile);
|
|
1538
|
+
|
|
1539
|
+
// Validate schema references exist
|
|
1540
|
+
if (tool.inputSchemaType && !schemaUniverse[tool.inputSchemaType]) {
|
|
1541
|
+
perfLog.warn(`Tool ${tool.sourceFile.getBaseName()} references input schema "${tool.inputSchemaType}" which was not found in schema universe`);
|
|
1542
|
+
}
|
|
1543
|
+
if (tool.outputSchemaType && !schemaUniverse[tool.outputSchemaType]) {
|
|
1544
|
+
perfLog.warn(
|
|
1545
|
+
`Tool ${tool.sourceFile.getBaseName()} references output schema "${tool.outputSchemaType}" which was not found in schema universe`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
manifest.tools[relativePath] = {
|
|
1550
|
+
inputSchemaName: tool.inputSchemaType,
|
|
1551
|
+
outputSchemaName: tool.outputSchemaType,
|
|
1552
|
+
inputTypeHint: tool.inputTypeHint,
|
|
1553
|
+
outputTypeHint: tool.outputTypeHint,
|
|
1554
|
+
};
|
|
920
1555
|
}
|
|
1556
|
+
|
|
1557
|
+
// Write manifest to .flink directory
|
|
1558
|
+
const manifestPath = join(this.cwd, "dist/.flink/schema-manifest.json");
|
|
1559
|
+
await writeJsonFile(manifestPath, manifest, { ensureDir: true });
|
|
1560
|
+
|
|
1561
|
+
perfLog.debug(`Schema manifest written to: ${manifestPath}`);
|
|
921
1562
|
}
|
|
922
1563
|
|
|
923
1564
|
/**
|
|
924
1565
|
* Scans project for jobs so they can be registered during start.
|
|
925
1566
|
*/
|
|
926
1567
|
async parseJobs() {
|
|
1568
|
+
const startTime = Date.now();
|
|
1569
|
+
|
|
927
1570
|
const generatedFile = this.createSourceFile(
|
|
928
1571
|
["generatedJobs.ts"],
|
|
929
1572
|
`// Generated ${new Date()}
|
|
@@ -936,13 +1579,12 @@ autoRegisteredJobs.push(...jobs);
|
|
|
936
1579
|
const jobsArr = generatedFile.getVariableDeclarationOrThrow("jobs").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
937
1580
|
|
|
938
1581
|
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
939
|
-
let i = 0;
|
|
940
1582
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
continue;
|
|
944
|
-
}
|
|
1583
|
+
// Collect job elements first, then add in one batch
|
|
1584
|
+
const jobElements: string[] = [];
|
|
945
1585
|
|
|
1586
|
+
// Use pre-segmented job files (no filtering needed)
|
|
1587
|
+
this.jobFiles.forEach((sf, i) => {
|
|
946
1588
|
console.log(`Detected job ${sf.getBaseName()}`);
|
|
947
1589
|
|
|
948
1590
|
const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
@@ -952,36 +1594,131 @@ autoRegisteredJobs.push(...jobs);
|
|
|
952
1594
|
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
953
1595
|
});
|
|
954
1596
|
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1597
|
+
// __file is set on the registration object (not injected into source files)
|
|
1598
|
+
jobElements.push(`{...${namespaceImport}, __file: "${this.getRelativePath(sf)}"}`);
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
// Add all job elements in one batch operation
|
|
1602
|
+
jobsArr.addElements(jobElements);
|
|
1603
|
+
|
|
1604
|
+
generatedFile.addImportDeclarations(imports);
|
|
1605
|
+
|
|
1606
|
+
// Defer save until batch save at end (performance optimization)
|
|
1607
|
+
|
|
1608
|
+
// Cleanup: forget job file nodes to reduce memory overhead (ts-morph performance optimization)
|
|
1609
|
+
this.jobFiles.forEach((sf) => {
|
|
1610
|
+
sf.getClasses().forEach((cls) => cls.forget());
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
const jobParseTime = Date.now() - startTime;
|
|
1614
|
+
perfLog.info(`✓ Job parsing completed in ${jobParseTime}ms (${jobElements.length} jobs)`);
|
|
1615
|
+
|
|
1616
|
+
return generatedFile;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Scans project for services so they can be registered during start.
|
|
1621
|
+
*/
|
|
1622
|
+
async parseServices() {
|
|
1623
|
+
if (this.disableServices) {
|
|
1624
|
+
initLog.info("Services disabled via flink.config.js (disableServices: true)");
|
|
1625
|
+
}
|
|
973
1626
|
|
|
974
|
-
|
|
1627
|
+
const startTime = Date.now();
|
|
975
1628
|
|
|
976
|
-
|
|
1629
|
+
const generatedFile = this.createSourceFile(
|
|
1630
|
+
["generatedServices.ts"],
|
|
1631
|
+
`// Generated ${new Date()}
|
|
1632
|
+
import { autoRegisteredServices } from "@flink-app/flink";
|
|
1633
|
+
export const services: any[] = [];
|
|
1634
|
+
autoRegisteredServices.push(...services);
|
|
1635
|
+
`
|
|
1636
|
+
);
|
|
1637
|
+
|
|
1638
|
+
const servicesArr = generatedFile.getVariableDeclarationOrThrow("services").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
1639
|
+
|
|
1640
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
1641
|
+
|
|
1642
|
+
const serviceElements: string[] = [];
|
|
1643
|
+
|
|
1644
|
+
for (const sf of this.serviceFiles) {
|
|
1645
|
+
console.log(`Detected service ${sf.getBaseName()}`);
|
|
1646
|
+
|
|
1647
|
+
imports.push({
|
|
1648
|
+
defaultImport: sf.getBaseNameWithoutExtension(),
|
|
1649
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
serviceElements.push(
|
|
1653
|
+
`{serviceInstanceName: "${getRepoInstanceName(sf.getBaseName())}", Service: ${sf.getBaseNameWithoutExtension()}}`
|
|
1654
|
+
);
|
|
977
1655
|
}
|
|
978
1656
|
|
|
1657
|
+
servicesArr.addElements(serviceElements);
|
|
1658
|
+
|
|
1659
|
+
generatedFile.addImportDeclarations(imports);
|
|
1660
|
+
|
|
1661
|
+
// Cleanup: forget service file nodes to reduce memory overhead
|
|
1662
|
+
this.serviceFiles.forEach((sf) => {
|
|
1663
|
+
sf.getClasses().forEach((cls) => cls.forget());
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
const serviceParseTime = Date.now() - startTime;
|
|
1667
|
+
perfLog.info(`✓ Service parsing completed in ${serviceParseTime}ms (${serviceElements.length} services)`);
|
|
1668
|
+
|
|
1669
|
+
return generatedFile;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
|
|
1674
|
+
* Mirrors the same namespace-import + spread pattern used by parseJobs.
|
|
1675
|
+
*/
|
|
1676
|
+
async parseExtensionDir(ext: FlinkCompilerPlugin): Promise<SourceFile> {
|
|
1677
|
+
const startTime = Date.now();
|
|
1678
|
+
const files = this.extensionFiles.get(ext.generatedFile) ?? [];
|
|
1679
|
+
|
|
1680
|
+
const generatedFile = this.createSourceFile(
|
|
1681
|
+
[`${ext.generatedFile}.ts`],
|
|
1682
|
+
`// Generated ${new Date()}
|
|
1683
|
+
import { ${ext.registrationVar} } from "${ext.package}";
|
|
1684
|
+
export const items: any[] = [];
|
|
1685
|
+
${ext.registrationVar}.push(...items);
|
|
1686
|
+
`
|
|
1687
|
+
);
|
|
1688
|
+
|
|
1689
|
+
const itemsArr = generatedFile.getVariableDeclarationOrThrow("items").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
1690
|
+
|
|
1691
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
1692
|
+
const itemElements: string[] = [];
|
|
1693
|
+
|
|
1694
|
+
files.forEach((sf, i) => {
|
|
1695
|
+
const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
1696
|
+
imports.push({
|
|
1697
|
+
defaultImport: "* as " + namespaceImport,
|
|
1698
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
1699
|
+
});
|
|
1700
|
+
itemElements.push(`{...${namespaceImport}, __file: "${this.getRelativePath(sf)}"}`);
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
itemsArr.addElements(itemElements);
|
|
979
1704
|
generatedFile.addImportDeclarations(imports);
|
|
980
1705
|
|
|
981
|
-
|
|
1706
|
+
const elapsed = Date.now() - startTime;
|
|
1707
|
+
initLog.info(`✓ Extension dir "${ext.scanDir}" parsed in ${elapsed}ms (${files.length} files)`);
|
|
982
1708
|
|
|
983
1709
|
return generatedFile;
|
|
984
1710
|
}
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* Iterates all compilerPlugins from flink.config.js and generates
|
|
1714
|
+
* a .flink/generatedXxx.ts file for each one.
|
|
1715
|
+
* Call this after parseJobs() and before generateStartScript().
|
|
1716
|
+
*/
|
|
1717
|
+
public async parseAllExtensionDirs(): Promise<void> {
|
|
1718
|
+
for (const ext of this.compilerPlugins) {
|
|
1719
|
+
await this.parseExtensionDir(ext);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
985
1722
|
}
|
|
986
1723
|
|
|
987
1724
|
export default TypeScriptCompiler;
|