@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.
Files changed (280) hide show
  1. package/CHANGELOG.md +1051 -0
  2. package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
  3. package/SIMPLE_AST_FEASIBILITY.md +570 -0
  4. package/bin/flink.ts +13 -2
  5. package/cli/build.ts +24 -44
  6. package/cli/clean.ts +13 -25
  7. package/cli/cli-utils.ts +190 -17
  8. package/cli/dev.ts +252 -0
  9. package/cli/loadEnvFiles.ts +116 -0
  10. package/cli/run.ts +45 -62
  11. package/dist/bin/flink.js +61 -2
  12. package/dist/cli/build.js +20 -25
  13. package/dist/cli/clean.js +12 -10
  14. package/dist/cli/cli-utils.d.ts +34 -3
  15. package/dist/cli/cli-utils.js +193 -12
  16. package/dist/cli/dev.d.ts +2 -0
  17. package/dist/cli/dev.js +279 -0
  18. package/dist/cli/loadEnvFiles.d.ts +30 -0
  19. package/dist/cli/loadEnvFiles.js +113 -0
  20. package/dist/cli/run.js +47 -46
  21. package/dist/src/DependencyTracker.d.ts +44 -0
  22. package/dist/src/DependencyTracker.js +239 -0
  23. package/dist/src/FlinkApp.d.ts +163 -10
  24. package/dist/src/FlinkApp.js +847 -184
  25. package/dist/src/FlinkContext.d.ts +41 -0
  26. package/dist/src/FlinkErrors.d.ts +19 -6
  27. package/dist/src/FlinkErrors.js +36 -42
  28. package/dist/src/FlinkHttpHandler.d.ts +219 -26
  29. package/dist/src/FlinkHttpHandler.js +37 -1
  30. package/dist/src/FlinkJob.d.ts +10 -0
  31. package/dist/src/FlinkLog.d.ts +82 -18
  32. package/dist/src/FlinkLog.js +165 -13
  33. package/dist/src/FlinkLogFactory.d.ts +288 -0
  34. package/dist/src/FlinkLogFactory.js +619 -0
  35. package/dist/src/FlinkRepo.d.ts +10 -2
  36. package/dist/src/FlinkRepo.js +11 -1
  37. package/dist/src/FlinkRequestContext.d.ts +63 -0
  38. package/dist/src/FlinkRequestContext.js +74 -0
  39. package/dist/src/FlinkResponse.d.ts +6 -0
  40. package/dist/src/FlinkService.d.ts +38 -0
  41. package/dist/src/FlinkService.js +46 -0
  42. package/dist/src/LeaderElection.d.ts +45 -0
  43. package/dist/src/LeaderElection.js +269 -0
  44. package/dist/src/SchemaCache.d.ts +84 -0
  45. package/dist/src/SchemaCache.js +289 -0
  46. package/dist/src/TypeScriptCompiler.d.ts +161 -51
  47. package/dist/src/TypeScriptCompiler.js +1253 -617
  48. package/dist/src/TypeScriptUtils.js +4 -0
  49. package/dist/src/ai/AgentRunner.d.ts +39 -0
  50. package/dist/src/ai/AgentRunner.js +760 -0
  51. package/dist/src/ai/ConversationAgent.d.ts +279 -0
  52. package/dist/src/ai/ConversationAgent.js +404 -0
  53. package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
  54. package/dist/src/ai/ConversationFlinkAgent.js +404 -0
  55. package/dist/src/ai/FlinkAgent.d.ts +690 -0
  56. package/dist/src/ai/FlinkAgent.js +729 -0
  57. package/dist/src/ai/FlinkTool.d.ts +135 -0
  58. package/dist/src/ai/FlinkTool.js +2 -0
  59. package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
  60. package/dist/src/ai/InMemoryConversationAgent.js +209 -0
  61. package/dist/src/ai/LLMAdapter.d.ts +148 -0
  62. package/dist/src/ai/LLMAdapter.js +2 -0
  63. package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
  64. package/dist/src/ai/PersistentFlinkAgent.js +403 -0
  65. package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
  66. package/dist/src/ai/SubAgentExecutor.js +223 -0
  67. package/dist/src/ai/ToolExecutor.d.ts +64 -0
  68. package/dist/src/ai/ToolExecutor.js +497 -0
  69. package/dist/src/ai/agentInstructions.d.ts +68 -0
  70. package/dist/src/ai/agentInstructions.js +286 -0
  71. package/dist/src/ai/index.d.ts +8 -0
  72. package/dist/src/ai/index.js +26 -0
  73. package/dist/src/ai/instructionFileLoader.d.ts +44 -0
  74. package/dist/src/ai/instructionFileLoader.js +179 -0
  75. package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
  76. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  77. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  78. package/dist/src/index.d.ts +14 -0
  79. package/dist/src/index.js +17 -0
  80. package/dist/src/loadPluginSchemas.d.ts +45 -0
  81. package/dist/src/loadPluginSchemas.js +143 -0
  82. package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
  83. package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
  84. package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
  85. package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
  86. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
  87. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
  88. package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
  89. package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
  90. package/dist/src/schema-extraction/index.d.ts +2 -0
  91. package/dist/src/schema-extraction/index.js +20 -0
  92. package/dist/src/schema-extraction/types.d.ts +31 -0
  93. package/dist/src/schema-extraction/types.js +2 -0
  94. package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
  95. package/dist/src/utils/loadFlinkConfig.js +77 -0
  96. package/dist/src/utils.d.ts +30 -0
  97. package/dist/src/utils.js +52 -0
  98. package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
  99. package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
  100. package/dist/src/workers/WorkerPool.d.ts +60 -0
  101. package/dist/src/workers/WorkerPool.js +306 -0
  102. package/examples/logging-hierarchical-example.ts +125 -0
  103. package/package.json +29 -4
  104. package/readme.md +499 -0
  105. package/spec/AgentDescendantDetection.spec.ts +335 -0
  106. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  107. package/spec/AgentObserver.spec.ts +266 -0
  108. package/spec/AgentRunner.spec.ts +1062 -0
  109. package/spec/AsyncLocalStorageContext.spec.ts +223 -0
  110. package/spec/ConversationHooks.spec.ts +257 -0
  111. package/spec/FlinkAgent.spec.ts +681 -0
  112. package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
  113. package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
  114. package/spec/FlinkApp.onError.spec.ts +1 -2
  115. package/spec/FlinkApp.query.spec.ts +107 -0
  116. package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
  117. package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
  118. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  119. package/spec/FlinkJob.spec.ts +171 -0
  120. package/spec/FlinkLogFactory.spec.ts +337 -0
  121. package/spec/FlinkRepo.spec.ts +1 -1
  122. package/spec/LeaderElection.spec.ts +174 -0
  123. package/spec/StreamingIntegration.spec.ts +139 -0
  124. package/spec/ToolExecutor.spec.ts +465 -0
  125. package/spec/TypeScriptCompiler.spec.ts +1 -1
  126. package/spec/TypeScriptSourceParser.spec.ts +1215 -0
  127. package/spec/TypeScriptTokenizer.spec.ts +366 -0
  128. package/spec/ai/ContextCompaction.spec.ts +405 -0
  129. package/spec/ai/ConversationAgent.spec.ts +520 -0
  130. package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
  131. package/spec/ai/agentInstructions.spec.ts +358 -0
  132. package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
  133. package/spec/fixtures/agent-instructions/simple.md +3 -0
  134. package/spec/fixtures/agent-instructions/template.md +18 -0
  135. package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
  136. package/spec/mock-project/dist/.tsbuildinfo +1 -0
  137. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
  138. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
  139. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
  140. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
  141. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
  142. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
  143. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
  144. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
  145. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
  146. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  147. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  148. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
  149. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
  150. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
  151. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
  152. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
  153. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
  154. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
  155. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
  156. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
  157. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  158. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  159. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  160. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  161. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  162. package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
  163. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  164. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  165. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  166. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  167. package/spec/mock-project/dist/src/FlinkLog.js +119 -0
  168. package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
  169. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  170. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  171. package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
  172. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  173. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  174. package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
  175. package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
  176. package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
  177. package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
  178. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  179. package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
  180. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  181. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
  182. package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
  183. package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
  184. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  185. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  186. package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
  187. package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
  188. package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
  189. package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
  190. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
  191. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
  192. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
  193. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
  194. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
  195. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
  196. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
  197. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
  198. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
  199. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
  200. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
  201. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
  202. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
  203. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
  204. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
  205. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
  206. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
  207. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
  208. package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
  209. package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
  210. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
  211. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
  212. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
  213. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
  214. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
  215. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
  216. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
  217. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
  218. package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
  219. package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
  220. package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
  221. package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
  222. package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
  223. package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
  224. package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
  225. package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
  226. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  227. package/spec/mock-project/dist/src/index.js +52 -76
  228. package/spec/mock-project/dist/src/index.js.map +1 -0
  229. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  230. package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
  231. package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
  232. package/spec/mock-project/dist/src/schemas/Car.js +3 -1
  233. package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
  234. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
  235. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
  236. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
  237. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
  238. package/spec/mock-project/dist/src/utils.js +290 -0
  239. package/spec/mock-project/tsconfig.json +6 -1
  240. package/spec/schema-generation-nested-objects.spec.ts +97 -0
  241. package/spec/testHelpers.ts +49 -0
  242. package/spec/utils.caseConversion.spec.ts +78 -0
  243. package/spec/utils.spec.ts +13 -13
  244. package/src/DependencyTracker.ts +166 -0
  245. package/src/FlinkApp.ts +919 -155
  246. package/src/FlinkContext.ts +43 -0
  247. package/src/FlinkErrors.ts +32 -12
  248. package/src/FlinkHttpHandler.ts +246 -28
  249. package/src/FlinkJob.ts +11 -0
  250. package/src/FlinkLog.ts +119 -12
  251. package/src/FlinkLogFactory.ts +699 -0
  252. package/src/FlinkRepo.ts +10 -3
  253. package/src/FlinkRequestContext.ts +95 -0
  254. package/src/FlinkResponse.ts +6 -0
  255. package/src/FlinkService.ts +49 -0
  256. package/src/LeaderElection.ts +203 -0
  257. package/src/SchemaCache.ts +232 -0
  258. package/src/TypeScriptCompiler.ts +1347 -610
  259. package/src/TypeScriptUtils.ts +5 -0
  260. package/src/ai/AgentRunner.ts +646 -0
  261. package/src/ai/ConversationAgent.ts +413 -0
  262. package/src/ai/FlinkAgent.ts +1069 -0
  263. package/src/ai/FlinkTool.ts +165 -0
  264. package/src/ai/InMemoryConversationAgent.ts +149 -0
  265. package/src/ai/LLMAdapter.ts +126 -0
  266. package/src/ai/ToolExecutor.ts +485 -0
  267. package/src/ai/agentInstructions.ts +245 -0
  268. package/src/ai/index.ts +8 -0
  269. package/src/ai/instructionFileLoader.ts +156 -0
  270. package/src/auth/FlinkAuthPlugin.ts +2 -1
  271. package/src/handlers/StreamWriterFactory.ts +84 -0
  272. package/src/index.ts +14 -0
  273. package/src/loadPluginSchemas.ts +141 -0
  274. package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
  275. package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
  276. package/src/schema-extraction/index.ts +2 -0
  277. package/src/schema-extraction/types.ts +34 -0
  278. package/src/utils/loadFlinkConfig.ts +89 -0
  279. package/src/utils.ts +52 -0
  280. 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 { CompletedConfig, createFormatter, createParser, createProgram, Schema, SchemaGenerator } from "ts-json-schema-generator";
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 { addImports, getDefaultExport, getInterfaceName, getTypeMetadata, getTypesToImport } from "./TypeScriptUtils";
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
- * Parsed typescript schemas that will be added to intermediate
31
- * schemas.ts file.
32
- *
33
- * This will be written to file in a batch for performance reasons.
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 parsedTsSchemas: string[] = [];
64
+ private extensionFiles: Map<string, SourceFile[]> = new Map();
36
65
 
37
66
  /**
38
- * Imports needed for schemas.ts.
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
- * This will be added to file in a batch for performance reasons.
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 tsSchemasSymbolsToImports: Symbol[] = [];
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
- const tsConfigPath = join(cwd, "tsconfig.json");
66
- console.log("TypeScript config path:", require("path").resolve(tsConfigPath));
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
- let moduleSpecifier = fromFile.getRelativePathAsModuleSpecifierTo(toFile);
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
- // Add .js extension for ESM imports (only for relative paths)
106
- if (this.isEsm && !moduleSpecifier.startsWith("@") && !moduleSpecifier.endsWith(".js")) {
107
- moduleSpecifier += ".js";
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 moduleSpecifier;
569
+ return relativePath;
111
570
  }
112
571
 
113
572
  /**
@@ -132,10 +591,108 @@ class TypeScriptCompiler {
132
591
  }
133
592
 
134
593
  /**
135
- * Emits compiled javascript source to dist folder
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
- emit() {
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(excludeDirs: string[] = []) {
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
- await generatedFile.save();
757
+ // Defer save until batch save at end (performance optimization)
198
758
 
199
- const schemaFilePath = await this.createIntermediateSchemaFile();
759
+ // Store handler schemas for later batch processing
760
+ this.handlerSchemasToGenerate = handlers.schemasToGenerate;
200
761
 
201
- const jsonSchemas = await this.generateAndSaveJsonSchemas(handlers.schemasToGenerate, schemaFilePath);
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
- this.appendSchemasToHandlerSourceFiles(handlers.schemasToGenerate, jsonSchemas);
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
- for (const sf of this.project.getSourceFiles()) {
221
- if (!sf.getFilePath().includes("src/handlers/")) {
222
- continue;
223
- }
224
-
225
- const isAutoRegister = this.isAutoRegisterableHandler(sf);
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
- console.log(`Detected handler ${sf.getBaseName()} ${!isAutoRegister ? "(requires manual registration)" : ""}`);
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
- const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
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
- handlersArr.insertElement(
278
- i,
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
- let i = 0;
860
+ // Use pre-segmented repo files (no filtering needed)
861
+ const repoFiles = this.repoFiles;
311
862
 
312
- for (const sf of this.project.getSourceFiles()) {
313
- if (!sf.getFilePath().includes("src/repos/")) {
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
- reposArr.insertElement(
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
- await generatedFile.save();
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
- * Generates a start script that will import references to handlers, repos and the
343
- * actual Flink app to start.
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
- * Note that order is of importance so generated metadata are imported and initialized before start of flink app.
346
- * Otherwise singletons `autoRegisteredRepos` and `autoRegisteredHandlers` will not have been set.
904
+ * Also extracts input and output schemas from FlinkTool type arguments
905
+ * when manual schemas are not provided.
347
906
  */
348
- async generateStartScript(appEntryScript = "/src/index.ts") {
349
- if (!this.project.getSourceFile((sf) => sf.getFilePath().endsWith(appEntryScript))) {
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
- const sf = this.createSourceFile(
355
- ["start.ts"],
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 "./generatedHandlers${this.isEsm ? ".js" : ""}";
358
- import "./generatedRepos${this.isEsm ? ".js" : ""}";
359
- import "./generatedJobs${this.isEsm ? ".js" : ""}";
360
- import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
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
- await sf.save();
922
+ const toolsArr = generatedFile.getVariableDeclarationOrThrow("tools").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
366
923
 
367
- return sf;
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
- private createSourceFile(filename: string[], contents: string) {
371
- return this.project.createSourceFile(join(this.cwd, ".flink", ...filename), contents, {
372
- overwrite: true,
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
- * Parses handlers `Handler<...>` function and its type arguments to extract
378
- * which schemas to use.
379
- *
380
- * There are multiple ways of defining schema types as valid ts and this
381
- * implementation aims to support all of these.
382
- *
383
- * Some examples of different cases (check spec/mock-project/src/handlers for more):
384
- *
385
- * ```
386
- * // Interface reference
387
- * Handler<Ctx, Car>
388
- * // Inline type definition with reference to interface
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
- if (defaultExport) {
404
- const handlerTypeRef = defaultExport.getFirstDescendantByKindOrThrow(SyntaxKind.TypeReference);
980
+ const toolParseTime = Date.now() - startTime;
981
+ initLog.info(`✓ Tool parsing completed in ${toolParseTime}ms (${toolFiles.length} tools)`);
405
982
 
406
- return this.extractSchemaTypeFromHandler(handlerTypeRef);
407
- } else {
408
- console.warn(`Handler ${handlerSourceFile.getBaseName()} is missing default exported handler function`);
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
- * Recursively copies an interface and all its dependencies from the same file
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
- // Check if already copied
420
- const existingInterface = this.parsedTsSchemas.find(
421
- s => s.includes(`interface ${interfaceName} `) || s.includes(`type ${interfaceName} =`)
422
- );
423
- if (existingInterface) return;
424
-
425
- // Copy the interface
426
- this.parsedTsSchemas.push(interfaceDecl.getText());
427
-
428
- // Find and recursively copy dependencies from the same file
429
- // First, find direct type references in this interface
430
- const typeRefIdentifiers = interfaceDecl
431
- .getDescendantsOfKind(SyntaxKind.TypeReference)
432
- .filter((typeRefNode: any) => !!typeRefNode.getFirstChildIfKind(SyntaxKind.Identifier))
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
- private async saveIntermediateTsSchema(schema: Type<ts.Type>, handlerFile: SourceFile, suffix: string) {
455
- if (schema.isAny()) {
456
- return; // 'any' indicates that no schema is used
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
- // Check for void and undefined types - these indicate no schema validation needed
460
- const schemaText = schema.getText();
461
- if (schemaText === 'void' || schemaText === 'undefined') {
462
- return;
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 handlerFileName = handlerFile.getBaseNameWithoutExtension().replace(/\./g, "_");
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
- // Also check for utility types with indexed access patterns like Partial<Foo["bar"]>
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
- for (const typeToImport of getTypesToImport(declaration)) {
546
- const typeSymbol = typeToImport.getSymbol();
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
- return `${propName}${isOptional ? "?" : ""}: ${propTypeText}`;
589
- });
1051
+ // Collect agent elements first, then add in one batch
1052
+ const agentElements: string[] = [];
590
1053
 
591
- // Extract type references for imports from resolved types
592
- for (const prop of schema.getProperties()) {
593
- const propType = prop.getTypeAtLocation(handlerFile);
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
- // Get symbol for the property type
596
- const typeSymbol = propType.getSymbol();
597
- if (typeSymbol) {
598
- const typeDeclaration = typeSymbol.getDeclarations()[0];
599
- if (typeDeclaration && typeDeclaration.getSourceFile() !== handlerFile) {
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
- // Also check for array element types
605
- if (propType.isArray()) {
606
- const elementType = propType.getArrayElementType();
607
- const elementSymbol = elementType?.getSymbol();
608
- if (elementSymbol) {
609
- const elementDeclaration = elementSymbol.getDeclarations()[0];
610
- if (elementDeclaration && elementDeclaration.getSourceFile() !== handlerFile) {
611
- this.tsSchemasSymbolsToImports.push(elementSymbol);
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
- // Check for utility types (Partial, Omit, Pick, etc.) and extract their type arguments
617
- // For example: Partial<Foo["bar"]> should extract Foo
618
- // Use a pragmatic text-based approach to find interface references in type expressions
619
- // Match interface names in patterns like: Partial<InterfaceName["prop"]>, Omit<InterfaceName, "key">, etc.
620
- const currentPropTypeText = propType.getText(undefined, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
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
- // Also check regular type arguments (for types like Array<Foo>, Promise<Bar>)
635
- const typeArgs = propType.getTypeArguments();
636
- if (typeArgs && typeArgs.length > 0) {
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
- // If we have a declaration, check if we need to import any types
650
- if (declaration) {
651
- for (const typeToImport of getTypesToImport(declaration)) {
652
- const typeSymbol = typeToImport.getSymbol();
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
- generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} { ${propertySignatures.join(";\n")} }`;
663
- } else {
664
- console.log("[WARN] Unknown schema type", schema.getText());
665
- }
666
-
667
- if (generatedSchemaInterfaceStr) {
668
- this.parsedTsSchemas.push(generatedSchemaInterfaceStr);
669
-
670
- return schemaInterfaceName;
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
- console.log("Creating TypeScript program for schema generation:");
696
- console.log(" Schema file:", schemaFilePath);
697
- console.log(" tsconfig:", tsconfigPath);
1156
+ // Add all agent elements in one batch operation
1157
+ agentsArr.addElements(agentElements);
698
1158
 
699
- // Create a fresh TypeScript Program that includes the schema file
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
- console.error("💡 Tip: Fix the TypeScript errors above and try again.\n");
725
- process.exit(1);
726
- }
1161
+ // Defer save until batch save at end (performance optimization)
727
1162
 
728
- console.log(" TypeScript version:", ts.version);
729
- console.log(" Program root files:", program.getRootFileNames().length);
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 formatter = createFormatter(conf);
732
- const parser = createParser(program, conf);
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 generator;
1171
+ return generatedFile;
736
1172
  }
737
1173
 
738
- private async generateAndSaveJsonSchemas(schemas: { reqSchemaType?: string; resSchemaType?: string }[], schemaFilePath: string) {
739
- // Reset schema generator to use the newly created intermediate schema file
740
- this.schemaGenerator = undefined;
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
- const jsonSchemas: Schema[] = [];
1186
+ if (!fs.existsSync(entryScriptPath)) {
1187
+ console.error(`Cannot find entry script '${appEntryScript}' at ${entryScriptPath}`);
1188
+ return process.exit(1);
1189
+ }
743
1190
 
744
- for (const { reqSchemaType, resSchemaType } of schemas) {
745
- if (reqSchemaType) {
746
- jsonSchemas.push({ definitions: { [reqSchemaType]: this.generateJsonSchema(reqSchemaType, schemaFilePath) } });
747
- }
748
- if (resSchemaType) {
749
- jsonSchemas.push({ definitions: { [resSchemaType]: this.generateJsonSchema(resSchemaType, schemaFilePath) } });
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 mergedSchemas = jsonSchemas.reduce(
754
- (out, schema) => {
755
- if (schema.definitions) {
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 filePath = join(this.cwd, ".flink", "schemas", "schemas.json");
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
- await writeJsonFile(filePath, mergedSchemas);
1225
+ // Defer save until batch save at end (performance optimization)
770
1226
 
771
- this.project.addSourceFileAtPath(filePath);
1227
+ return sf;
1228
+ }
772
1229
 
773
- return mergedSchemas;
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
- private generateJsonSchema(typeName: string, schemaFilePath: string) {
777
- if (!this.schemaGenerator) {
778
- this.schemaGenerator = this.initJsonSchemaGenerator(schemaFilePath);
779
- }
780
- return this.schemaGenerator.createSchema(typeName);
781
- }
782
-
783
- private async extractSchemaTypeFromHandler(handlerTypeReference: TypeReferenceNode) {
784
- // Name of Handler function - should be either `Handler` or `GetHandler`
785
- const handlerType = handlerTypeReference.getTypeName().getText();
786
-
787
- // Get type arguments a.k.a. generics which holds schemas such as this `Handler<Ctx, ReqSchema, ResSchema>`
788
- const handlerTypeArgs = handlerTypeReference.getType().getAliasTypeArguments();
789
-
790
- let reqSchema: Type<ts.Type> | undefined;
791
- let resSchema: Type<ts.Type> | undefined;
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
- const sf = handlerTypeReference.getSourceFile();
1254
+ // Extract FlinkTool type arguments using utility parser
1255
+ const typeArgs = TypeScriptSourceParser.parseFlinkToolTypeArgs(fileText);
813
1256
 
814
- const createReqSchemaPromise = reqSchema
815
- ? this.saveIntermediateTsSchema(reqSchema, sf, `${handlerTypeReference.getStartLineNumber()}_ReqSchema`)
816
- : Promise.resolve("");
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 createResSchemaPromise = resSchema
819
- ? this.saveIntermediateTsSchema(resSchema, sf, `${handlerTypeReference.getStartLineNumber()}_ResSchema`)
820
- : Promise.resolve("");
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
- const [reqSchemaType, resSchemaType] = await Promise.all([createReqSchemaPromise, createResSchemaPromise]);
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
- reqSchemaType,
826
- resSchemaType,
827
- queryMetadata: getTypeMetadata(query),
828
- paramsMetadata: getTypeMetadata(params),
1305
+ inputSchemaType,
1306
+ outputSchemaType,
1307
+ inputTypeHint,
1308
+ outputTypeHint,
829
1309
  };
830
1310
  }
831
1311
 
832
1312
  /**
833
- * Creates generated source file that contains all
834
- * TypeScript schemas that has been derived from handlers.
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 createIntermediateSchemaFile() {
837
- const schemaSourceFile = this.createSourceFile(
838
- ["schemas", `schemas.ts`],
839
- `// Generated ${new Date()}
840
- ${this.parsedTsSchemas.join("\n\n")}`
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
- addImports(schemaSourceFile, this.tsSchemasSymbolsToImports);
1338
+ // Resolve type names to schema $ids
1339
+ let reqSchemaType: string | undefined = undefined;
1340
+ let resSchemaType: string | undefined = undefined;
844
1341
 
845
- await schemaSourceFile.save();
1342
+ // Check if handler skips validation — no need to warn about unresolved schema types
1343
+ const isSkipValidation = /ValidationMode\.SkipValidation/.test(fileText);
846
1344
 
847
- // Return the file path so it can be used by ts-json-schema-generator
848
- return schemaSourceFile.getFilePath();
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
- * Appends generated json schemas to handler source files.
1409
+ * Generates JSON schemas for all handlers and tools.
1410
+ * Should be called after parseHandlers() and parseTools() have completed.
874
1411
  *
875
- * @param handlers
876
- * @param jsonSchemas
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
- private appendSchemasToHandlerSourceFiles(
879
- handlers: {
880
- sourceFile: SourceFile;
881
- reqSchemaType?: string;
882
- resSchemaType?: string;
883
- }[],
884
- jsonSchemas: JSONSchema7
885
- ) {
886
- const jsonSchemaDefs = jsonSchemas.definitions || {};
887
-
888
- for (const { sourceFile, reqSchemaType, resSchemaType } of handlers) {
889
- if (reqSchemaType && !jsonSchemaDefs[reqSchemaType]) {
890
- console.error(`Handler ${sourceFile.getBaseName()} has request schema (${reqSchemaType}) defined, but no JSON schema has been generated`);
891
- continue;
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
- if (resSchemaType && !jsonSchemaDefs[resSchemaType]) {
895
- console.error(`Handler ${sourceFile.getBaseName()} has response schema (${resSchemaType}) defined, but no JSON schema has been generated`);
896
- continue;
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
- const reqJsonSchema = JSON.stringify(reqSchemaType ? jsonSchemaDefs[reqSchemaType] : undefined);
900
- const resJsonSchema = JSON.stringify(resSchemaType ? jsonSchemaDefs[resSchemaType] : undefined);
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
- // Remove existing __schemas variable if it exists (to avoid redeclaration errors)
903
- const existingSchemas = sourceFile.getVariableStatements().filter((vs) => {
904
- const varNames = vs.getDeclarations().map((d) => d.getName());
905
- return varNames.includes("__schemas");
906
- });
907
- existingSchemas.forEach((v) => v.remove());
908
-
909
- sourceFile.addVariableStatement({
910
- declarationKind: VariableDeclarationKind.Const,
911
- isExported: true,
912
- declarations: [
913
- {
914
- name: "__schemas",
915
- type: "any",
916
- initializer: `{ reqSchema: ${reqJsonSchema}, resSchema: ${resJsonSchema} }`,
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
- for (const sf of this.project.getSourceFiles()) {
942
- if (!sf.getFilePath().includes("src/jobs/")) {
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
- // Remove existing __file variable if it exists (to avoid redeclaration errors)
956
- const existingFile = sf.getVariableStatements().filter((vs) => {
957
- const varNames = vs.getDeclarations().map((d) => d.getName());
958
- return varNames.includes("__file");
959
- });
960
- existingFile.forEach((v) => v.remove());
961
-
962
- // Append metadata to source file that will be part of emitted dist bundle (javascript)
963
- sf.addVariableStatement({
964
- declarationKind: VariableDeclarationKind.Const,
965
- isExported: true,
966
- declarations: [
967
- {
968
- name: "__file",
969
- initializer: `"${sf.getBaseName()}"`,
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
- jobsArr.insertElement(i, namespaceImport);
1627
+ const startTime = Date.now();
975
1628
 
976
- i++;
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
- await generatedFile.save();
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;