@superblocksteam/vite-plugin-file-sync 2.0.6-next.9 → 2.0.6-next.91

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 (249) hide show
  1. package/dist/ai-service/app-interface/linter.d.ts +7 -2
  2. package/dist/ai-service/app-interface/linter.d.ts.map +1 -1
  3. package/dist/ai-service/app-interface/linter.js +52 -41
  4. package/dist/ai-service/app-interface/linter.js.map +1 -1
  5. package/dist/ai-service/app-interface/shell.d.ts +2 -0
  6. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  7. package/dist/ai-service/app-interface/shell.js +13 -4
  8. package/dist/ai-service/app-interface/shell.js.map +1 -1
  9. package/dist/ai-service/const.d.ts +2 -0
  10. package/dist/ai-service/const.d.ts.map +1 -1
  11. package/dist/ai-service/const.js +2 -0
  12. package/dist/ai-service/const.js.map +1 -1
  13. package/dist/ai-service/eval/template-renderer.d.ts.map +1 -1
  14. package/dist/ai-service/eval/template-renderer.js +1 -1
  15. package/dist/ai-service/eval/template-renderer.js.map +1 -1
  16. package/dist/ai-service/index.d.ts.map +1 -1
  17. package/dist/ai-service/index.js +4 -12
  18. package/dist/ai-service/index.js.map +1 -1
  19. package/dist/ai-service/prompts/generated/library-components/SbButtonPropsDocs.d.ts +1 -1
  20. package/dist/ai-service/prompts/generated/library-components/SbButtonPropsDocs.d.ts.map +1 -1
  21. package/dist/ai-service/prompts/generated/library-components/SbButtonPropsDocs.js +2 -2
  22. package/dist/ai-service/prompts/generated/library-components/SbButtonPropsDocs.js.map +1 -1
  23. package/dist/ai-service/prompts/generated/library-components/SbCheckboxPropsDocs.d.ts +1 -1
  24. package/dist/ai-service/prompts/generated/library-components/SbCheckboxPropsDocs.d.ts.map +1 -1
  25. package/dist/ai-service/prompts/generated/library-components/SbCheckboxPropsDocs.js +2 -2
  26. package/dist/ai-service/prompts/generated/library-components/SbCheckboxPropsDocs.js.map +1 -1
  27. package/dist/ai-service/prompts/generated/library-components/SbColumnPropsDocs.d.ts +1 -1
  28. package/dist/ai-service/prompts/generated/library-components/SbColumnPropsDocs.d.ts.map +1 -1
  29. package/dist/ai-service/prompts/generated/library-components/SbColumnPropsDocs.js +2 -2
  30. package/dist/ai-service/prompts/generated/library-components/SbColumnPropsDocs.js.map +1 -1
  31. package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.d.ts +1 -1
  32. package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.d.ts.map +1 -1
  33. package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.js +2 -2
  34. package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.js.map +1 -1
  35. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.d.ts +1 -1
  36. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.d.ts.map +1 -1
  37. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.js +2 -2
  38. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.js.map +1 -1
  39. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.d.ts +1 -1
  40. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.d.ts.map +1 -1
  41. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.js +2 -2
  42. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.js.map +1 -1
  43. package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.d.ts +1 -1
  44. package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.d.ts.map +1 -1
  45. package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.js +2 -2
  46. package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.js.map +1 -1
  47. package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.d.ts +1 -1
  48. package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.d.ts.map +1 -1
  49. package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.js +2 -2
  50. package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.js.map +1 -1
  51. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.d.ts +1 -1
  52. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.d.ts.map +1 -1
  53. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.js +2 -2
  54. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.js.map +1 -1
  55. package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.d.ts +1 -1
  56. package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.d.ts.map +1 -1
  57. package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.js +2 -2
  58. package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.js.map +1 -1
  59. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.d.ts +1 -1
  60. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.d.ts.map +1 -1
  61. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.js +2 -2
  62. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.js.map +1 -1
  63. package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.d.ts +1 -1
  64. package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.d.ts.map +1 -1
  65. package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.js +2 -2
  66. package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.js.map +1 -1
  67. package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.d.ts +1 -1
  68. package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.d.ts.map +1 -1
  69. package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.js +2 -2
  70. package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.js.map +1 -1
  71. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.d.ts +1 -1
  72. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.d.ts.map +1 -1
  73. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.js +2 -2
  74. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.js.map +1 -1
  75. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.d.ts +1 -1
  76. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.d.ts.map +1 -1
  77. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.js +2 -2
  78. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.js.map +1 -1
  79. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.d.ts +1 -1
  80. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.d.ts.map +1 -1
  81. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.js +2 -2
  82. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.js.map +1 -1
  83. package/dist/ai-service/prompts/generated/library-typedefs/Dim.js +1 -1
  84. package/dist/ai-service/prompts/generated/library-typedefs/SbEventFlow.js +1 -1
  85. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.d.ts +2 -0
  86. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.d.ts.map +1 -0
  87. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.js +6 -0
  88. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.js.map +1 -0
  89. package/dist/ai-service/prompts/generated/library-typedefs/index.d.ts +1 -0
  90. package/dist/ai-service/prompts/generated/library-typedefs/index.d.ts.map +1 -1
  91. package/dist/ai-service/prompts/generated/library-typedefs/index.js +1 -0
  92. package/dist/ai-service/prompts/generated/library-typedefs/index.js.map +1 -1
  93. package/dist/ai-service/prompts/generated/subprompts/full-examples.js +1 -1
  94. package/dist/ai-service/prompts/generated/subprompts/superblocks-api.d.ts +1 -1
  95. package/dist/ai-service/prompts/generated/subprompts/superblocks-api.d.ts.map +1 -1
  96. package/dist/ai-service/prompts/generated/subprompts/superblocks-api.js +2 -2
  97. package/dist/ai-service/prompts/generated/subprompts/superblocks-api.js.map +1 -1
  98. package/dist/ai-service/prompts/generated/subprompts/superblocks-components-rules.js +1 -1
  99. package/dist/ai-service/prompts/generated/subprompts/superblocks-custom-components.js +1 -1
  100. package/dist/ai-service/prompts/generated/subprompts/superblocks-data-filtering.js +1 -1
  101. package/dist/ai-service/prompts/generated/subprompts/superblocks-event-flow.js +1 -1
  102. package/dist/ai-service/prompts/generated/subprompts/superblocks-forms.js +1 -1
  103. package/dist/ai-service/prompts/generated/subprompts/superblocks-layouts.js +1 -1
  104. package/dist/ai-service/prompts/generated/subprompts/superblocks-page.js +1 -1
  105. package/dist/ai-service/prompts/generated/subprompts/superblocks-rbac.js +1 -1
  106. package/dist/ai-service/prompts/generated/subprompts/superblocks-routes.js +1 -1
  107. package/dist/ai-service/prompts/generated/subprompts/superblocks-state.js +1 -1
  108. package/dist/ai-service/prompts/generated/subprompts/superblocks-theming.js +1 -1
  109. package/dist/ai-service/prompts/generated/subprompts/system.js +1 -1
  110. package/dist/ai-service/prompts/system.d.ts.map +1 -1
  111. package/dist/ai-service/prompts/system.js +4 -0
  112. package/dist/ai-service/prompts/system.js.map +1 -1
  113. package/dist/ai-service/state-machine/clark-fsm.d.ts +2 -0
  114. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  115. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  116. package/dist/ai-service/state-machine/handlers/agent-planning.js +10 -0
  117. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  118. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts +1 -1
  119. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  120. package/dist/ai-service/state-machine/handlers/llm-generating.js +70 -24
  121. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  122. package/dist/ai-service/test-utils/app-generation-mocks/orders-app.d.ts +3 -0
  123. package/dist/ai-service/test-utils/app-generation-mocks/orders-app.d.ts.map +1 -0
  124. package/dist/ai-service/test-utils/app-generation-mocks/orders-app.js +851 -0
  125. package/dist/ai-service/test-utils/app-generation-mocks/orders-app.js.map +1 -0
  126. package/dist/ai-service/test-utils/app-generation-mocks/smoketest.d.ts +3 -0
  127. package/dist/ai-service/test-utils/app-generation-mocks/smoketest.d.ts.map +1 -0
  128. package/dist/ai-service/test-utils/app-generation-mocks/smoketest.js +111 -0
  129. package/dist/ai-service/test-utils/app-generation-mocks/smoketest.js.map +1 -0
  130. package/dist/ai-service/test-utils/mock-utils.d.ts +22 -0
  131. package/dist/ai-service/test-utils/mock-utils.d.ts.map +1 -0
  132. package/dist/ai-service/test-utils/mock-utils.js +46 -0
  133. package/dist/ai-service/test-utils/mock-utils.js.map +1 -0
  134. package/dist/ai-service/transform/add-metadata-to-api-yaml/transformer.d.ts.map +1 -1
  135. package/dist/ai-service/transform/add-metadata-to-api-yaml/transformer.js +3 -2
  136. package/dist/ai-service/transform/add-metadata-to-api-yaml/transformer.js.map +1 -1
  137. package/dist/ai-service/transform/shared.d.ts.map +1 -1
  138. package/dist/ai-service/transform/shared.js +9 -8
  139. package/dist/ai-service/transform/shared.js.map +1 -1
  140. package/dist/ai-service/types.d.ts +23 -1
  141. package/dist/ai-service/types.d.ts.map +1 -1
  142. package/dist/ai-service/types.js.map +1 -1
  143. package/dist/binding-extraction/index.d.ts +2 -0
  144. package/dist/binding-extraction/index.d.ts.map +1 -0
  145. package/dist/binding-extraction/index.js +2 -0
  146. package/dist/binding-extraction/index.js.map +1 -0
  147. package/dist/codegen.js +1 -1
  148. package/dist/codegen.js.map +1 -1
  149. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  150. package/dist/file-sync-vite-plugin.js +34 -13
  151. package/dist/file-sync-vite-plugin.js.map +1 -1
  152. package/dist/file-system-helpers.d.ts +4 -0
  153. package/dist/file-system-helpers.d.ts.map +1 -1
  154. package/dist/file-system-helpers.js +10 -0
  155. package/dist/file-system-helpers.js.map +1 -1
  156. package/dist/file-system-manager.d.ts +52 -39
  157. package/dist/file-system-manager.d.ts.map +1 -1
  158. package/dist/file-system-manager.js +633 -514
  159. package/dist/file-system-manager.js.map +1 -1
  160. package/dist/index.d.ts +1 -0
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +1 -0
  163. package/dist/index.js.map +1 -1
  164. package/dist/inject-index-vite-plugin.d.ts +0 -2
  165. package/dist/inject-index-vite-plugin.d.ts.map +1 -1
  166. package/dist/inject-index-vite-plugin.js +2 -2
  167. package/dist/inject-index-vite-plugin.js.map +1 -1
  168. package/dist/injected-index.d.ts +2 -2
  169. package/dist/injected-index.d.ts.map +1 -1
  170. package/dist/injected-index.js.map +1 -1
  171. package/dist/lock-service/index.d.ts.map +1 -1
  172. package/dist/lock-service/index.js +13 -2
  173. package/dist/lock-service/index.js.map +1 -1
  174. package/dist/operations/operation-processor.d.ts +24 -0
  175. package/dist/operations/operation-processor.d.ts.map +1 -0
  176. package/dist/operations/operation-processor.js +80 -0
  177. package/dist/operations/operation-processor.js.map +1 -0
  178. package/dist/operations/types.d.ts +8 -0
  179. package/dist/operations/types.d.ts.map +1 -0
  180. package/dist/operations/types.js +2 -0
  181. package/dist/operations/types.js.map +1 -0
  182. package/dist/parsing/computed/to-code-computed.d.ts.map +1 -1
  183. package/dist/parsing/computed/to-code-computed.js +7 -3
  184. package/dist/parsing/computed/to-code-computed.js.map +1 -1
  185. package/dist/parsing/events/to-code-events.d.ts.map +1 -1
  186. package/dist/parsing/events/to-code-events.js +12 -1
  187. package/dist/parsing/events/to-code-events.js.map +1 -1
  188. package/dist/parsing/events/to-value-events.d.ts.map +1 -1
  189. package/dist/parsing/events/to-value-events.js +47 -0
  190. package/dist/parsing/events/to-value-events.js.map +1 -1
  191. package/dist/parsing/index.d.ts +3 -0
  192. package/dist/parsing/index.d.ts.map +1 -0
  193. package/dist/parsing/index.js +3 -0
  194. package/dist/parsing/index.js.map +1 -0
  195. package/dist/parsing/template/index.js +1 -1
  196. package/dist/parsing/template/index.js.map +1 -1
  197. package/dist/parsing/template/to-code-template.d.ts +2 -1
  198. package/dist/parsing/template/to-code-template.d.ts.map +1 -1
  199. package/dist/parsing/template/to-code-template.js +2 -2
  200. package/dist/parsing/template/to-code-template.js.map +1 -1
  201. package/dist/plugin-options.d.ts +0 -2
  202. package/dist/plugin-options.d.ts.map +1 -1
  203. package/dist/plugin-options.js.map +1 -1
  204. package/dist/refactor/entities.d.ts +6 -0
  205. package/dist/refactor/entities.d.ts.map +1 -0
  206. package/dist/refactor/entities.js +62 -0
  207. package/dist/refactor/entities.js.map +1 -0
  208. package/dist/refactor/javascript.d.ts +0 -4
  209. package/dist/refactor/javascript.d.ts.map +1 -1
  210. package/dist/refactor/javascript.js +0 -8
  211. package/dist/refactor/javascript.js.map +1 -1
  212. package/dist/rename-manager.d.ts +0 -5
  213. package/dist/rename-manager.d.ts.map +1 -1
  214. package/dist/rename-manager.js +1 -27
  215. package/dist/rename-manager.js.map +1 -1
  216. package/dist/routing.d.ts +2 -2
  217. package/dist/routing.d.ts.map +1 -1
  218. package/dist/routing.js +21 -1
  219. package/dist/routing.js.map +1 -1
  220. package/dist/sb-scope-manager.d.ts +1 -1
  221. package/dist/sb-scope-manager.d.ts.map +1 -1
  222. package/dist/sb-scope-manager.js +10 -0
  223. package/dist/sb-scope-manager.js.map +1 -1
  224. package/dist/socket-manager.d.ts +2 -2
  225. package/dist/socket-manager.d.ts.map +1 -1
  226. package/dist/socket-manager.js +7 -5
  227. package/dist/socket-manager.js.map +1 -1
  228. package/dist/source-tracker.d.ts +20 -20
  229. package/dist/source-tracker.d.ts.map +1 -1
  230. package/dist/source-tracker.js +33 -17
  231. package/dist/source-tracker.js.map +1 -1
  232. package/dist/sync-service/index.d.ts.map +1 -1
  233. package/dist/sync-service/index.js +2 -1
  234. package/dist/sync-service/index.js.map +1 -1
  235. package/dist/sync-service/list-dir.js +1 -1
  236. package/dist/sync-service/list-dir.js.map +1 -1
  237. package/dist/util/logger.d.ts +13 -17
  238. package/dist/util/logger.d.ts.map +1 -1
  239. package/dist/util/logger.js +34 -44
  240. package/dist/util/logger.js.map +1 -1
  241. package/dist/util.d.ts +1 -0
  242. package/dist/util.d.ts.map +1 -1
  243. package/dist/util.js +8 -0
  244. package/dist/util.js.map +1 -1
  245. package/package.json +15 -6
  246. package/dist/util/tracing.d.ts +0 -4
  247. package/dist/util/tracing.d.ts.map +0 -1
  248. package/dist/util/tracing.js +0 -56
  249. package/dist/util/tracing.js.map +0 -1
@@ -12,8 +12,9 @@ import { generateJSXAttribute } from "./codegen.js";
12
12
  import { ComponentsManager } from "./components-manager.js";
13
13
  import { addLegacyCustomComponentVariables, modifyLegacyCustomComponentElements, modifyLegacyCustomComponentImports, } from "./custom-components.js";
14
14
  import { applyErrorHandling, } from "./errors/error-handler.js";
15
- import { getPageFolder, PAGES_DIRECTORY, ROUTES_FILE, SCOPE_FILE, } from "./file-system-helpers.js";
15
+ import { getApiFilePath, getPageFolder, isPageFilePath, PAGES_DIRECTORY, ROUTES_FILE, SCOPE_FILE, } from "./file-system-helpers.js";
16
16
  import { generate } from "./generate.js";
17
+ import { OperationProcessor } from "./operations/operation-processor.js";
17
18
  import { doesElementHaveBinding } from "./parsing/bindings.js";
18
19
  import { getSbElementId } from "./parsing/ids.js";
19
20
  import { makeJSXAttribute } from "./parsing/jsx.js";
@@ -24,6 +25,7 @@ import { RenameManager } from "./rename-manager.js";
24
25
  import { SourceTracker } from "./source-tracker.js";
25
26
  import { traverse } from "./traverse.js";
26
27
  import { getErrorMeta, getLogger } from "./util/logger.js";
28
+ import { getPageName } from "./util.js";
27
29
  const SUPPORTED_FILETYPES = [
28
30
  {
29
31
  type: "tsx",
@@ -45,26 +47,41 @@ const SUPPORTED_FILETYPES = [
45
47
  type: "js-api-step",
46
48
  extension: ".js",
47
49
  },
50
+ {
51
+ type: "json",
52
+ extension: ".json",
53
+ },
48
54
  ];
49
55
  const APP_THEME_FILE_NAME = "appTheme.ts";
50
- export class FileSyncManager extends TracedEventEmitter {
56
+ export class FileSystemManager extends TracedEventEmitter {
51
57
  rootDir;
52
58
  tsFiles = {};
53
59
  apiFiles = {};
54
60
  sourceTracker;
55
61
  fsOperationQueue;
62
+ operationProcessor;
56
63
  generationNumberSequence;
57
64
  routes = {};
65
+ routeChangesQueue = [];
58
66
  watcher;
59
67
  registeredComponentPaths = {};
60
68
  renameManager = new RenameManager();
61
69
  _tracer;
70
+ transactionNonce = Date.now();
71
+ pendingTransactions = new Set();
72
+ processedTransactions = [];
62
73
  constructor(fsOperationQueue, generationNumberSequence, tracer) {
63
74
  super(tracer, { captureRejections: true });
64
75
  this.rootDir = "/";
65
76
  this.fsOperationQueue = fsOperationQueue;
66
77
  this.generationNumberSequence = generationNumberSequence;
67
78
  this._tracer = tracer;
79
+ // intentionally a new queue here, we don't want to share the queue with the fsOperationQueue
80
+ this.operationProcessor = new OperationProcessor({
81
+ batchWindowMs: 50,
82
+ maxBatchSize: 100,
83
+ });
84
+ this.operationProcessor.disable();
68
85
  applyErrorHandling(this, {
69
86
  watch: { operation: "editing from code" },
70
87
  handleCreatePage: { operation: "creating a page" },
@@ -110,27 +127,7 @@ export class FileSyncManager extends TracedEventEmitter {
110
127
  }
111
128
  return path.join(this.rootDir, "App.tsx");
112
129
  }
113
- updateApi = (content, path) => {
114
- if (!this.rootDir) {
115
- throw new Error("Root directory not set");
116
- }
117
- const { api: apiContents, stepPathMap } = content;
118
- const pageName = getPageName(path);
119
- let scopeId = this.sourceTracker?.getScopeDefinitionForPage(pageName)?.id;
120
- if (!scopeId) {
121
- console.warn("Scope ID not found for API", apiContents.metadata.name);
122
- scopeId = "";
123
- }
124
- const updatedApi = {
125
- apiPb: yaml.parse(JSON.stringify(apiContents)),
126
- pageName,
127
- stepPathMap,
128
- scopeId,
129
- };
130
- const isNewApi = !this.apiFiles[path];
131
- this.apiFiles[path] = updatedApi;
132
- return { updatedApi, pageName, isNewApi };
133
- };
130
+ // MARK: core setup/init
134
131
  async watch(watcher, rootPath) {
135
132
  const logger = getLogger();
136
133
  this.rootDir = rootPath;
@@ -216,178 +213,10 @@ export class FileSyncManager extends TracedEventEmitter {
216
213
  return;
217
214
  const { type, path } = file;
218
215
  if (type === "api") {
219
- this.updateApi(content, path);
220
- }
221
- });
222
- const handleFileChange = async (event, filePath) => {
223
- logger.info(`File changed: ${filePath}, event: ${event}`);
224
- switch (event) {
225
- case "add": {
226
- const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
227
- if (!fileType || !filePath.startsWith(rootPath)) {
228
- return;
229
- }
230
- const data = await readFile(filePath);
231
- if (typeof data !== "string")
232
- return;
233
- // if we match pages/*/index.tsx, we want to emit an addPage event
234
- if (/pages\/.*\/index\.tsx/.test(filePath)) {
235
- const file = await readFile(filePath);
236
- if (!file) {
237
- logger.error(`Failed to read file: ${filePath}`);
238
- return;
239
- }
240
- if (!(filePath in this.tsFiles)) {
241
- this.tsFiles[filePath] = file;
242
- this.handleNonVisualChangeByDeletingIds(filePath, file);
243
- this.emit("addPage", filePath);
244
- }
245
- }
246
- switch (fileType.type) {
247
- case "api":
248
- case "python-api-step":
249
- case "js-api-step": {
250
- await this.processApiUpdates(filePath, fileType);
251
- break;
252
- }
253
- }
254
- break;
255
- }
256
- case "change": {
257
- // Special routes file handling
258
- if (filePath === routePath) {
259
- const data = await readFile(filePath);
260
- try {
261
- this.routes = JSON.parse(data);
262
- this.emit("routesChanged", this.routes);
263
- }
264
- catch (e) {
265
- logger.error("Error parsing routes file", getErrorMeta(e));
266
- }
267
- return;
268
- }
269
- const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
270
- if (!fileType || !filePath.startsWith(rootPath)) {
271
- return;
272
- }
273
- const data = await readFile(filePath);
274
- if (typeof data !== "string")
275
- return;
276
- switch (fileType.type) {
277
- case "tsx":
278
- case "scope":
279
- {
280
- if (!(filePath in this.tsFiles && this.tsFiles[filePath] === data)) {
281
- logger.info(`File changed: ${filePath} updating AST tracker`);
282
- this.tsFiles[filePath] = data;
283
- // only update the source tracker if the file is different
284
- this.handleNonVisualChangeByDeletingIds(filePath, data);
285
- this.emit("fileChanged", filePath, data, true);
286
- }
287
- else {
288
- logger.info(`File unchanged from last tracked state: ${filePath}`);
289
- this.emit("fileChanged", filePath, data, false);
290
- }
291
- }
292
- break;
293
- case "api":
294
- case "python-api-step":
295
- case "js-api-step":
296
- {
297
- await this.processApiUpdates(filePath, fileType);
298
- }
299
- break;
300
- }
301
- break;
302
- }
303
- case "unlink": {
304
- if (filePath in this.apiFiles) {
305
- const api = this.apiFiles[filePath];
306
- delete this.apiFiles[filePath];
307
- if (!api || !this.sourceTracker)
308
- break;
309
- const scopeId = await this.sourceTracker.deleteApi({
310
- pageName: api.pageName,
311
- apiName: api.apiPb.metadata.name,
312
- });
313
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
314
- await this.writeChanges(changes);
315
- this.emit("apiManualDelete", {
316
- api: {
317
- id: getClientApiId(api.apiPb.metadata.name, api.pageName),
318
- apiName: api.apiPb.metadata.name,
319
- // TODO(saksham): get pagename more defensively
320
- pageName: getPageName(filePath),
321
- scopeId,
322
- },
323
- });
324
- }
325
- if (filePath in this.tsFiles) {
326
- delete this.tsFiles[filePath];
327
- this.sourceTracker?.removeFile(filePath);
328
- this.emit("deletePage", filePath);
329
- }
330
- break;
331
- }
332
- }
333
- };
334
- watcher.on("all", async (event, filePath) => {
335
- const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
336
- switch (fileType?.type) {
337
- case "tsx":
338
- case "scope":
339
- // these can be awaited to ensure sequential execution
340
- this.fsOperationQueue.enqueue(async () => {
341
- return await handleFileChange(event, filePath);
342
- });
343
- break;
344
- default:
345
- // some files including APIs cannot be awaited currently
346
- this.fsOperationQueue.enqueue(async () => {
347
- void handleFileChange(event, filePath);
348
- });
216
+ this.updateInternalApiData(content, path);
349
217
  }
350
218
  });
351
- }
352
- getApiFiles() {
353
- return this.formatApisToClientApis(this.apiFiles);
354
- }
355
- getTsFilePaths() {
356
- return Object.keys(this.tsFiles);
357
- }
358
- getSourceTracker() {
359
- return this.sourceTracker;
360
- }
361
- async writeFile(path, content, kind) {
362
- if (kind === "ts") {
363
- // happens eagerly regardless of error - possible desync
364
- this.tsFiles[path] = content;
365
- // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
366
- await this.fsOperationQueue.enqueue(async () => {
367
- await fs.writeFile(path, content);
368
- });
369
- }
370
- else if (kind === "api") {
371
- const currentApiFile = this.apiFiles[path];
372
- const apiPb = yaml.parse(content);
373
- // Client APIs have id, but server APIs don't
374
- if (apiPb.metadata.id) {
375
- delete apiPb.metadata.id;
376
- }
377
- const stepPathMap = currentApiFile?.stepPathMap ?? {};
378
- this.updateApi({ api: apiPb, stepPathMap }, path);
379
- // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
380
- await this.fsOperationQueue.enqueue(async () => {
381
- await writeApiFiles({ apiPb }, "api", path.split("/").slice(0, -1).join("/"), false, [], [], { extractLargeSourceFiles: true, minLinesForExtraction: 1 }, new Set(Object.keys(this.apiFiles)), stepPathMap);
382
- // stepPathMap will be generated after the write when the file doesn't exist
383
- if (this.apiFiles[path]) {
384
- this.apiFiles[path].stepPathMap = stepPathMap;
385
- }
386
- });
387
- }
388
- }
389
- readFile(path) {
390
- return this.tsFiles[path];
219
+ watcher.on("all", this.handleFileChange);
391
220
  }
392
221
  initializeSourceTracker() {
393
222
  this.sourceTracker = new SourceTracker(this._tracer);
@@ -496,6 +325,246 @@ export class FileSyncManager extends TracedEventEmitter {
496
325
  return null;
497
326
  }
498
327
  }
328
+ enableOperationsQueue() {
329
+ this.operationProcessor.enable();
330
+ }
331
+ disableOperationsQueue() {
332
+ this.operationProcessor.disable();
333
+ }
334
+ async flushOperations() {
335
+ await this.operationProcessor.flush();
336
+ }
337
+ // MARK: file change handling
338
+ handleFileChange = async (event, filePath) => {
339
+ const logger = getLogger();
340
+ logger.info(`File changed: ${filePath}, event: ${event}`);
341
+ const rootPath = this.rootDir;
342
+ if (!rootPath) {
343
+ throw new Error("Root directory not set");
344
+ }
345
+ // Skip directory events
346
+ if (event === "addDir" || event === "unlinkDir") {
347
+ return;
348
+ }
349
+ const routePath = path.join(rootPath, ROUTES_FILE);
350
+ const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
351
+ // Only handle files we care about and that are in our root path
352
+ if (!fileType || !filePath.startsWith(rootPath)) {
353
+ return;
354
+ }
355
+ // Queue the operation based on the event type
356
+ switch (event) {
357
+ case "add": {
358
+ const data = await readFile(filePath);
359
+ if (typeof data !== "string")
360
+ return;
361
+ const isPage = isPageFilePath(filePath);
362
+ if (isPage) {
363
+ void this.operationProcessor.addOperation({
364
+ metadata: {
365
+ filePath,
366
+ },
367
+ execute: async () => {
368
+ const file = await readFile(filePath);
369
+ if (!file) {
370
+ logger.error(`Failed to read file: ${filePath}`);
371
+ return;
372
+ }
373
+ if (!(filePath in this.tsFiles)) {
374
+ this.tsFiles[filePath] = file;
375
+ this.handleNonVisualChangeByDeletingIds(filePath, file);
376
+ this.emit("addPage", filePath);
377
+ }
378
+ },
379
+ });
380
+ }
381
+ else {
382
+ void this.operationProcessor.addOperation({
383
+ metadata: {
384
+ filePath,
385
+ },
386
+ execute: async () => {
387
+ switch (fileType.type) {
388
+ case "api":
389
+ case "python-api-step":
390
+ case "js-api-step": {
391
+ await this.processApiFileUpdates(filePath, fileType);
392
+ break;
393
+ }
394
+ }
395
+ },
396
+ });
397
+ }
398
+ break;
399
+ }
400
+ case "change": {
401
+ if (filePath === routePath) {
402
+ void this.operationProcessor.addOperation({
403
+ metadata: {
404
+ filePath,
405
+ },
406
+ priority: true,
407
+ execute: async () => {
408
+ try {
409
+ const data = JSON.parse((await readFile(filePath)) ?? "{}");
410
+ if (!isEqual(this.routes, data))
411
+ this.routes = data;
412
+ // this.addRoute assigns this.routes itself, causing this.routes === data
413
+ // but we still want to emit the event to HMR root.tsx
414
+ this.emit("routesChanged", this.routes);
415
+ }
416
+ catch (e) {
417
+ logger.error("Error parsing routes file", getErrorMeta(e));
418
+ }
419
+ },
420
+ });
421
+ return;
422
+ }
423
+ else {
424
+ void this.operationProcessor.addOperation({
425
+ metadata: {
426
+ filePath,
427
+ },
428
+ execute: async () => {
429
+ const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
430
+ if (!fileType || !filePath.startsWith(rootPath)) {
431
+ return;
432
+ }
433
+ const data = await readFile(filePath);
434
+ if (typeof data !== "string")
435
+ return;
436
+ switch (fileType.type) {
437
+ case "tsx":
438
+ case "scope":
439
+ {
440
+ if (!(filePath in this.tsFiles &&
441
+ this.tsFiles[filePath] === data)) {
442
+ logger.info(`File changed: ${filePath} updating AST tracker`);
443
+ this.tsFiles[filePath] = data;
444
+ // only update the source tracker if the file is different
445
+ this.handleNonVisualChangeByDeletingIds(filePath, data);
446
+ this.emit("fileChanged", filePath, data, true);
447
+ }
448
+ else {
449
+ logger.info(`File unchanged from last tracked state: ${filePath}`);
450
+ this.emit("fileChanged", filePath, data, false);
451
+ }
452
+ }
453
+ break;
454
+ case "api":
455
+ case "python-api-step":
456
+ case "js-api-step":
457
+ {
458
+ await this.processApiFileUpdates(filePath, fileType);
459
+ }
460
+ break;
461
+ }
462
+ },
463
+ });
464
+ }
465
+ break;
466
+ }
467
+ case "unlink": {
468
+ if (filePath in this.tsFiles) {
469
+ void this.operationProcessor.addOperation({
470
+ metadata: {
471
+ filePath,
472
+ },
473
+ execute: async () => {
474
+ await this.deleteTsFile(filePath);
475
+ },
476
+ });
477
+ }
478
+ else if (filePath in this.apiFiles) {
479
+ void this.operationProcessor.addOperation({
480
+ metadata: {
481
+ filePath,
482
+ },
483
+ execute: async () => {
484
+ await this.removeApiData(filePath);
485
+ },
486
+ });
487
+ }
488
+ break;
489
+ }
490
+ }
491
+ };
492
+ async deleteTsFile(filePath) {
493
+ delete this.tsFiles[filePath];
494
+ this.sourceTracker?.removeFile(filePath);
495
+ if (isPageFilePath(filePath)) {
496
+ this.emit("deletePage", filePath);
497
+ }
498
+ }
499
+ getTsFilePaths() {
500
+ return Object.keys(this.tsFiles);
501
+ }
502
+ getSourceTracker() {
503
+ return this.sourceTracker;
504
+ }
505
+ // MARK: fs read/write
506
+ async writeFile(path, content, kind) {
507
+ switch (kind) {
508
+ case "ts": {
509
+ // happens eagerly regardless of error - possible desync
510
+ this.tsFiles[path] = content;
511
+ // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
512
+ await this.fsOperationQueue.enqueue(async () => {
513
+ await fs.writeFile(path, content);
514
+ });
515
+ break;
516
+ }
517
+ case "api": {
518
+ const currentApiFile = this.apiFiles[path];
519
+ const apiPb = yaml.parse(content);
520
+ // Client APIs have id, but server APIs don't
521
+ if (apiPb.metadata.id) {
522
+ delete apiPb.metadata.id;
523
+ }
524
+ const stepPathMap = currentApiFile?.stepPathMap ?? {};
525
+ this.updateInternalApiData({ api: apiPb, stepPathMap }, path);
526
+ // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
527
+ await this.fsOperationQueue.enqueue(async () => {
528
+ await writeApiFiles({ apiPb }, "api", path.split("/").slice(0, -1).join("/"), false, [], [], { extractLargeSourceFiles: true, minLinesForExtraction: 1 }, new Set(Object.keys(this.apiFiles)), stepPathMap);
529
+ // stepPathMap will be generated after the write when the file doesn't exist
530
+ if (this.apiFiles[path]) {
531
+ this.apiFiles[path].stepPathMap = stepPathMap;
532
+ }
533
+ });
534
+ break;
535
+ }
536
+ default: {
537
+ // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
538
+ await this.fsOperationQueue.enqueue(async () => {
539
+ await fs.writeFile(path, content);
540
+ });
541
+ }
542
+ }
543
+ }
544
+ readFile(path) {
545
+ return this.tsFiles[path];
546
+ }
547
+ async addRoute(route, filePath) {
548
+ if (!this.rootDir) {
549
+ throw new Error("Root directory not set");
550
+ }
551
+ this.routes[route] = { file: this.getRelativeRoutePath(filePath) };
552
+ await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
553
+ }
554
+ async removeRoute(filePath) {
555
+ if (!this.rootDir) {
556
+ throw new Error("Root directory not set");
557
+ }
558
+ const relativeFilePath = this.getRelativeRoutePath(filePath);
559
+ this.routes = Object.fromEntries(Object.entries(this.routes).filter(([_, value]) => value.file !== relativeFilePath));
560
+ await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
561
+ }
562
+ async writeChanges(changes, callback) {
563
+ return Promise.all(changes.map(async ({ fileName, source, kind }) => {
564
+ await this.writeFile(fileName, source, kind);
565
+ return callback?.(fileName, source);
566
+ }));
567
+ }
499
568
  getLocalBindingEntities(path) {
500
569
  const logger = getLogger();
501
570
  const files = this.sourceTracker?.getCurrentFiles();
@@ -523,8 +592,36 @@ export class FileSyncManager extends TracedEventEmitter {
523
592
  });
524
593
  return Array.from(localBindingEntities);
525
594
  }
526
- pendingTransactions = new Set();
527
- processedTransactions = [];
595
+ getPageRoots(filePath) {
596
+ const scopeFilePath = path.join(path.dirname(filePath), "index.tsx");
597
+ const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
598
+ if (!currentFile) {
599
+ return null;
600
+ }
601
+ return getPageRoots(filePath, currentFile);
602
+ }
603
+ getScope(filePath) {
604
+ // get the scope.ts file in the same directory as path
605
+ const scopeFilePath = path.join(path.dirname(filePath), SCOPE_FILE);
606
+ const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
607
+ if (!currentFile) {
608
+ console.log("File not found", scopeFilePath);
609
+ return null;
610
+ }
611
+ const scope = getScope(scopeFilePath, currentFile);
612
+ return scope;
613
+ }
614
+ getRoutes() {
615
+ const routes = [];
616
+ for (const [path, { file }] of Object.entries(this.routes)) {
617
+ routes.push({
618
+ path,
619
+ component: file,
620
+ });
621
+ }
622
+ return routes;
623
+ }
624
+ // MARK: transaction handling
528
625
  flushTransactions = () => {
529
626
  // TODO do something more sophisticated than this.
530
627
  this.processedTransactions = this.processedTransactions.slice(-20);
@@ -537,42 +634,92 @@ export class FileSyncManager extends TracedEventEmitter {
537
634
  }
538
635
  this.pendingTransactions.add(transactionId);
539
636
  };
540
- transactionNonce = Date.now();
541
637
  // Until we have a sophisticated way to track ALL operations, including non-UI initiated ops, we will use a nonce
542
638
  // TODO https://github.com/superblocksteam/superblocks/pull/11788
543
639
  getProcessedTransactionsWithNonce() {
544
640
  const nonce = `t-${this.transactionNonce++}`;
545
641
  return this.processedTransactions.concat(nonce);
546
642
  }
643
+ // MARK: editor operations
547
644
  handleCreatePage = async (payload) => {
548
- const { name } = payload;
645
+ this.trackTransaction(payload.transaction?.id);
646
+ const { name, route, navigateToRoute, routeTestParams } = payload;
549
647
  if (!this.rootDir) {
550
648
  throw new Error("Root directory not set");
551
649
  }
650
+ if (!route) {
651
+ throw new Error("Route is required when creating a page");
652
+ }
552
653
  const pagePath = getPageFolder(this.rootDir, name);
553
654
  const pageIndexPath = path.join(pagePath, "index.tsx");
554
- const pageContent = /*js*/ `import { SbPage, SbContainer, registerPage } from "@superblocksteam/library";
655
+ const scopePath = path.join(pagePath, "scope.ts");
656
+ const pageContent = /*js*/ `import {
657
+ SbPage,
658
+ Dim,
659
+ SbSection,
660
+ SbColumn,
661
+ registerPage,
662
+ } from "@superblocksteam/library";
663
+ import { ${name}, ${name}Scope } from "./scope";
664
+
555
665
  function Page() {
556
- return <SbPage name="${name}">
557
- <SbContainer width={Dim.fill()} />
558
- </SbPage>;
666
+ const {} = ${name};
667
+ return (
668
+ <SbPage name="${name}" height={Dim.fill()} width={Dim.fill()}>
669
+ <SbSection height={Dim.fill()}>
670
+ <SbColumn width={Dim.fill()}></SbColumn>
671
+ </SbSection>
672
+ </SbPage>
673
+ );
559
674
  }
560
675
 
561
- export default registerPage(Page, { name: "${name}" });
676
+ export default registerPage(Page, ${name}Scope);
562
677
  `;
678
+ const scopeContent = /*js*/ `import { createSbScope } from "@superblocksteam/library";
679
+
680
+ export const ${name}Scope = createSbScope<{}>(({ entities }) => ({}), {
681
+ name: "${name}",
682
+ });
683
+
684
+ export const ${name} = ${name}Scope.entities;
685
+ `;
686
+ if (navigateToRoute) {
687
+ this.routeChangesQueue.push({
688
+ route: route,
689
+ routeTestParams: routeTestParams,
690
+ });
691
+ }
692
+ this.watcher?.unwatch(pagePath);
563
693
  await fs.mkdir(pagePath, { recursive: true });
564
694
  await this.writeFile(pageIndexPath, pageContent, "ts");
695
+ await this.writeFile(scopePath, scopeContent, "ts");
565
696
  await this.handleNonVisualChangeByDeletingIds(pageIndexPath, pageContent);
697
+ await this.handleNonVisualChangeByDeletingIds(scopePath, scopeContent);
698
+ this.watcher?.add(pagePath);
699
+ await this.addRoute(route, pageIndexPath);
566
700
  this.emit("addPage", pageIndexPath);
567
701
  };
702
+ handleDeletePage = async (payload) => {
703
+ const { name } = payload;
704
+ if (!this.rootDir) {
705
+ throw new Error("Root directory not set");
706
+ }
707
+ const pagePath = getPageFolder(this.rootDir, name);
708
+ await fs.rm(pagePath, { recursive: true, force: true });
709
+ await this.removeRoute(name);
710
+ this.emit("deletePage", pagePath);
711
+ };
712
+ get routeChange() {
713
+ return this.routeChangesQueue.shift();
714
+ }
568
715
  handleReparent = async (payload, writeFile = true) => {
569
716
  const { from, to, changedProps, transaction } = payload;
570
717
  this.trackTransaction(transaction?.id);
571
- await this.sourceTracker?.setProperties({
718
+ this.sourceTracker?.setProperties({
572
719
  source: from.source,
573
720
  changes: changedProps ?? {},
574
721
  });
575
- await this.sourceTracker?.moveElement({
722
+ this.sourceTracker?.moveElement({
576
723
  from,
577
724
  to,
578
725
  });
@@ -586,7 +733,7 @@ export default registerPage(Page, { name: "${name}" });
586
733
  };
587
734
  handleCreateComponent = async (payload, writeFile = true) => {
588
735
  this.trackTransaction(payload.transaction?.id);
589
- const sourceId = await this.sourceTracker?.addElement(payload);
736
+ const sourceId = this.sourceTracker?.addElement(payload);
590
737
  if (writeFile) {
591
738
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
592
739
  await this.writeChanges(changes ?? [], (fileName) => {
@@ -600,7 +747,7 @@ export default registerPage(Page, { name: "${name}" });
600
747
  const { elements, transaction } = payload;
601
748
  this.trackTransaction(transaction?.id);
602
749
  for (const element of elements) {
603
- await this.sourceTracker?.deleteElement({
750
+ this.sourceTracker?.deleteElement({
604
751
  source: element.source,
605
752
  scopeName: element.scopeName,
606
753
  });
@@ -616,7 +763,7 @@ export default registerPage(Page, { name: "${name}" });
616
763
  handleSetProperty = async (payload, writeFile = true) => {
617
764
  const { element: { source }, property, value, transaction, } = payload;
618
765
  this.trackTransaction(transaction?.id);
619
- await this.sourceTracker?.setProperty({
766
+ this.sourceTracker?.setProperty({
620
767
  source,
621
768
  property,
622
769
  info: value,
@@ -631,7 +778,7 @@ export default registerPage(Page, { name: "${name}" });
631
778
  handleSetProperties = async (payload, writeFile = true) => {
632
779
  const { element: { source }, properties, transaction, } = payload;
633
780
  this.trackTransaction(transaction?.id);
634
- await this.sourceTracker?.setProperties({
781
+ this.sourceTracker?.setProperties({
635
782
  source,
636
783
  changes: properties,
637
784
  });
@@ -687,39 +834,180 @@ export default registerPage(Page, { name: "${name}" });
687
834
  this.flushTransactions();
688
835
  return returnValues;
689
836
  };
690
- handleUpdateApi = async (payload) => {
691
- const { api } = payload;
692
- if (!this.sourceTracker) {
693
- throw new Error("Source tracker not initialized");
694
- }
837
+ // MARK: entity operations
838
+ handleAddEntity = async (payload) => {
839
+ this.sourceTracker?.addEntity({
840
+ scopeId: payload.scopeId,
841
+ entity: {
842
+ type: payload.type,
843
+ name: payload.name,
844
+ attributes: payload.attributes,
845
+ },
846
+ });
847
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
848
+ await this.writeChanges(changes, (fileName) => {
849
+ this.emit("addEntity", fileName, payload);
850
+ });
851
+ };
852
+ handleUpdateEntity = async (payload) => {
853
+ this.sourceTracker?.updateEntity({
854
+ entityId: payload.entityId,
855
+ updates: payload.updates,
856
+ });
857
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
858
+ await this.writeChanges(changes, (fileName) => {
859
+ this.emit("updateEntity", fileName, payload);
860
+ });
861
+ };
862
+ handleDeleteEntity = async (payload) => {
863
+ const deletedEntityName = this.sourceTracker?.deleteEntity({
864
+ entityId: payload.entityId,
865
+ });
866
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
867
+ await this.writeChanges(changes, (fileName) => {
868
+ if (deletedEntityName) {
869
+ this.emit("deleteEntity", fileName, deletedEntityName);
870
+ }
871
+ });
872
+ };
873
+ handleUpdateTheme = async (payload) => {
874
+ const { theme } = payload;
695
875
  if (!this.rootDir) {
696
876
  throw new Error("Root directory not set");
697
877
  }
698
- if (!api.pageName) {
699
- throw new Error("API page name is not set");
878
+ const filePath = path.join(this.rootDir, APP_THEME_FILE_NAME);
879
+ this.sourceTracker?.updateTheme({
880
+ themeFilePath: filePath,
881
+ theme,
882
+ });
883
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
884
+ await this.writeChanges(changes);
885
+ };
886
+ // MARK: rename operations
887
+ handleRenameElement = async (payload) => {
888
+ if (payload.kind === "component") {
889
+ return this.handleRenameComponent(payload);
700
890
  }
701
- const apiName = api.apiPb.metadata.name;
702
- if (!apiName) {
703
- throw new Error("API name is not set");
891
+ else if (payload.kind === "entity") {
892
+ return this.handleRenameEntity(payload);
704
893
  }
705
- const apiDir = path.join(this.rootDir, "pages", api.pageName, "apis", apiName);
706
- const apiPath = path.join(apiDir, "api.yaml");
707
- const isNewApi = !this.getApiFiles()[apiPath];
894
+ else if (payload.kind === "page") {
895
+ return this.handleRenamePage(payload);
896
+ }
897
+ };
898
+ handleRenameComponent = async (payload) => {
899
+ const { elementId, newName, oldName, scopeName } = payload;
900
+ await this.sourceTracker?.renameComponent({
901
+ widgetSourceId: elementId,
902
+ oldName,
903
+ newName,
904
+ scopeName,
905
+ });
906
+ await this.renameIdentifierInApis({
907
+ elementId,
908
+ oldName,
909
+ newName,
910
+ });
911
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
912
+ await this.writeChanges(changes, (fileName) => {
913
+ this.emit("renameComponent", fileName);
914
+ });
915
+ };
916
+ handleRenameEntity = async (payload) => {
917
+ const { elementId, newName, oldName, scopeName } = payload;
918
+ this.sourceTracker?.renameEntity({
919
+ entityId: elementId,
920
+ oldName,
921
+ newName,
922
+ scopeName,
923
+ });
924
+ await this.renameIdentifierInApis(payload);
925
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
926
+ await this.writeChanges(changes, (fileName) => {
927
+ this.emit("renameEntity", fileName);
928
+ });
929
+ };
930
+ handleRenamePage = async (payload) => {
931
+ const { newName, oldName } = payload;
932
+ if (!this.rootDir) {
933
+ throw new Error("Root directory not set");
934
+ }
935
+ const newPageFolder = getPageFolder(this.rootDir, newName);
936
+ const newIndexFilePath = path.join(newPageFolder, "index.tsx");
937
+ const oldIndexFilePath = path.join(this.rootDir, "pages", oldName, "index.tsx");
938
+ const oldPageFolder = getPageFolder(this.rootDir, oldName);
939
+ this.watcher?.unwatch(newPageFolder);
940
+ this.watcher?.unwatch(oldPageFolder);
941
+ const existingRoute = Object.keys(this.routes).find((route) => this.routes[route]?.file ===
942
+ this.getRelativeRoutePath(oldIndexFilePath));
943
+ if (!existingRoute) {
944
+ throw new Error(`Route for ${oldName} not found`);
945
+ }
946
+ // Write the name attribute to the page file
947
+ await this.sourceTracker?.renamePage({
948
+ newName,
949
+ filePath: oldIndexFilePath,
950
+ });
951
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
952
+ await this.writeChanges(changes);
953
+ // A rename of a folder is an unlink followed by an add, so the file watcher will take over initializing this
954
+ // "new" page
955
+ await fs.rename(oldPageFolder, newPageFolder);
956
+ // Now we just clean up the routes
957
+ await this.removeRoute(oldIndexFilePath);
958
+ await this.addRoute(existingRoute, newIndexFilePath);
959
+ const newIndexFile = await readFile(newIndexFilePath);
960
+ if (!newIndexFile) {
961
+ throw new Error(`New index file ${newIndexFilePath} not found`);
962
+ }
963
+ this.tsFiles[newIndexFilePath] = newIndexFile;
964
+ await this.handleNonVisualChangeByDeletingIds(newIndexFilePath, newIndexFile);
965
+ this.emit("renamePage", newIndexFilePath);
966
+ // Re-add the watcher
967
+ this.watcher?.add(newPageFolder);
968
+ this.watcher?.add(oldPageFolder);
969
+ };
970
+ getRelativeRoutePath(filePath) {
971
+ if (!this.rootDir) {
972
+ throw new Error("Root directory not set");
973
+ }
974
+ // no leading slash
975
+ return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
976
+ }
977
+ // MARK: API operations
978
+ handleUpdateApi = async (payload) => {
979
+ const { api } = payload;
980
+ if (!this.sourceTracker) {
981
+ throw new Error("Source tracker not initialized");
982
+ }
983
+ if (!this.rootDir) {
984
+ throw new Error("Root directory not set");
985
+ }
986
+ if (!api.pageName) {
987
+ throw new Error("API page name is not set");
988
+ }
989
+ const apiName = api.apiPb.metadata.name;
990
+ if (!apiName) {
991
+ throw new Error("API name is not set");
992
+ }
993
+ const apiFilePath = getApiFilePath(this.rootDir, api.pageName, apiName);
994
+ const apiDir = path.dirname(apiFilePath);
995
+ const isNewApi = !this.getApiFiles()[apiFilePath];
708
996
  try {
709
997
  const stats = await fs.stat(apiDir);
710
998
  if (!stats.isDirectory()) {
711
- await fs.mkdir(apiDir, { recursive: true });
999
+ await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
712
1000
  }
713
1001
  }
714
1002
  catch {
715
- await fs.mkdir(apiDir, { recursive: true });
1003
+ await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
716
1004
  }
717
- await this.writeFile(apiPath, yaml.stringify(api.apiPb), "api");
1005
+ await this.writeFile(apiFilePath, yaml.stringify(api.apiPb), "api");
718
1006
  const generationNumber = this.generationNumberSequence.next();
719
1007
  const apiDef = this.createClientApi(api);
720
1008
  let scopeId = "";
721
1009
  if (isNewApi) {
722
- scopeId = await this.createScopedApi(api);
1010
+ scopeId = await this.addApiToScope(api);
723
1011
  }
724
1012
  else {
725
1013
  const scopeDef = this.sourceTracker.getScopeDefinitionForPage(api.pageName);
@@ -728,20 +1016,6 @@ export default registerPage(Page, { name: "${name}" });
728
1016
  this.emit("apiUpdate", { api: apiDef, scopeId });
729
1017
  return { api: apiDef, scopeId, generationNumber };
730
1018
  };
731
- createScopedApi = async (api) => {
732
- if (!this.sourceTracker) {
733
- throw new Error("Source tracker not initialized");
734
- }
735
- // We want to add the API entity to our scope, but we do not want to emit entity events, because
736
- // the API update event handles this particular side effect.
737
- const scopeId = await this.sourceTracker.addApi({
738
- pageName: api.pageName,
739
- apiName: api.apiPb.metadata.name,
740
- });
741
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
742
- await this.writeChanges(changes);
743
- return scopeId;
744
- };
745
1019
  handleDeleteApi = async (payload) => {
746
1020
  const logger = getLogger();
747
1021
  const { apis } = payload;
@@ -749,40 +1023,34 @@ export default registerPage(Page, { name: "${name}" });
749
1023
  throw new Error("Root directory not set");
750
1024
  }
751
1025
  const rootDir = this.rootDir;
752
- const executeDeleteApis = Promise.all(apis.map(({ apiName, pageName }) => {
753
- return new Promise(
754
- // eslint-disable-next-line no-async-promise-executor
755
- async (resolve) => {
756
- const apiFilePath = path.join(rootDir, "pages", pageName, "apis", apiName, "api.yaml");
757
- const api = this.apiFiles[apiFilePath];
758
- if (!api || !this.sourceTracker) {
759
- return resolve(undefined);
760
- }
761
- const apiDir = path.join(rootDir, "pages", pageName, "apis", apiName);
762
- try {
763
- const stats = await fs.stat(apiDir);
764
- if (stats.isDirectory()) {
765
- await fs.rmdir(apiDir, { recursive: true });
766
- }
767
- delete this.apiFiles[apiFilePath];
768
- const scopeId = await this.sourceTracker.deleteApi({
769
- pageName,
770
- apiName,
771
- });
772
- resolve({
773
- apiName,
774
- pageName,
775
- scopeId,
776
- });
777
- }
778
- catch (e) {
779
- logger.warn(`Could not delete api ${apiFilePath} ${JSON.stringify(e)}`);
780
- resolve(undefined);
1026
+ const deletedApis = [];
1027
+ for (const { apiName, pageName } of apis) {
1028
+ const apiFilePath = getApiFilePath(rootDir, pageName, apiName);
1029
+ const api = this.apiFiles[apiFilePath];
1030
+ if (!api || !this.sourceTracker) {
1031
+ continue;
1032
+ }
1033
+ const apiDir = path.dirname(apiFilePath);
1034
+ try {
1035
+ const stats = await fs.stat(apiDir);
1036
+ if (stats.isDirectory()) {
1037
+ await fs.rmdir(apiDir, { recursive: true });
781
1038
  }
782
- resolve(undefined);
783
- });
784
- }));
785
- const deletedApis = (await executeDeleteApis).filter((api) => api !== undefined);
1039
+ delete this.apiFiles[apiFilePath];
1040
+ const scopeId = this.sourceTracker.deleteApi({
1041
+ pageName,
1042
+ apiName,
1043
+ });
1044
+ deletedApis.push({
1045
+ apiName,
1046
+ pageName,
1047
+ scopeId,
1048
+ });
1049
+ }
1050
+ catch (e) {
1051
+ logger.warn(`Could not delete api ${apiFilePath} ${JSON.stringify(e)}`);
1052
+ }
1053
+ }
786
1054
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
787
1055
  await this.writeChanges(changes);
788
1056
  this.emit("apiDelete", {
@@ -863,229 +1131,40 @@ export default registerPage(Page, { name: "${name}" });
863
1131
  // TODO: Should I delete here?
864
1132
  }));
865
1133
  };
866
- handleAddEntity = async (payload) => {
867
- await this.sourceTracker?.addEntity({
868
- scopeId: payload.scopeId,
869
- entity: {
870
- type: payload.type,
871
- name: payload.name,
872
- attributes: payload.attributes,
873
- },
874
- });
875
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
876
- await this.writeChanges(changes, (fileName) => {
877
- this.emit("addEntity", fileName, payload);
878
- });
879
- };
880
- handleUpdateEntity = async (payload) => {
881
- await this.sourceTracker?.updateEntity({
882
- entityId: payload.entityId,
883
- updates: payload.updates,
884
- });
885
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
886
- await this.writeChanges(changes, (fileName) => {
887
- this.emit("updateEntity", fileName, payload);
888
- });
889
- };
890
- handleDeleteEntity = async (payload) => {
891
- const deletedEntityName = await this.sourceTracker?.deleteEntity({
892
- entityId: payload.entityId,
893
- });
894
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
895
- await this.writeChanges(changes, (fileName) => {
896
- if (deletedEntityName) {
897
- this.emit("deleteEntity", fileName, deletedEntityName);
898
- }
899
- });
900
- };
901
- handleUpdateTheme = async (payload) => {
902
- const { theme } = payload;
903
- if (!this.rootDir) {
904
- throw new Error("Root directory not set");
1134
+ async removeApiData(filePath) {
1135
+ const api = this.apiFiles[filePath];
1136
+ if (!api) {
1137
+ return;
905
1138
  }
906
- const filePath = path.join(this.rootDir, APP_THEME_FILE_NAME);
907
- await this.sourceTracker?.updateTheme({
908
- themeFilePath: filePath,
909
- theme,
1139
+ delete this.apiFiles[filePath];
1140
+ this.sourceTracker?.deleteApi({
1141
+ pageName: api.pageName,
1142
+ apiName: api.apiPb.metadata.name,
910
1143
  });
911
1144
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
912
1145
  await this.writeChanges(changes);
913
- };
914
- handleRenameElement = async (payload) => {
915
- if (payload.kind === "component") {
916
- return this.handleRenameComponent(payload);
917
- }
918
- else if (payload.kind === "entity") {
919
- return this.handleRenameEntity(payload);
920
- }
921
- else if (payload.kind === "page") {
922
- return this.handleRenamePage(payload);
923
- }
924
- };
925
- handleRenameComponent = async (payload) => {
926
- const { elementId, newName, oldName, scopeName } = payload;
927
- await this.sourceTracker?.renameComponent({
928
- widgetSourceId: elementId,
929
- oldName,
930
- newName,
931
- scopeName,
932
- });
933
- await this.renameIdentifierInApis({
934
- elementId,
935
- oldName,
936
- newName,
937
- });
938
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
939
- await this.writeChanges(changes, (fileName) => {
940
- this.emit("renameComponent", fileName);
941
- });
942
- };
943
- handleRenameEntity = async (payload) => {
944
- const { elementId, newName, oldName, scopeName } = payload;
945
- await this.sourceTracker?.renameEntity({
946
- entityId: elementId,
947
- oldName,
948
- newName,
949
- scopeName,
950
- });
951
- await this.renameIdentifierInApis(payload);
952
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
953
- await this.writeChanges(changes, (fileName) => {
954
- this.emit("renameEntity", fileName);
955
- });
956
- };
957
- handleRenamePage = async (payload) => {
958
- const { newName, oldName } = payload;
959
- if (!this.rootDir) {
960
- throw new Error("Root directory not set");
961
- }
962
- const newPageFolder = getPageFolder(this.rootDir, newName);
963
- const newIndexFilePath = path.join(newPageFolder, "index.tsx");
964
- const oldIndexFilePath = path.join(this.rootDir, "pages", oldName, "index.tsx");
965
- const oldPageFolder = getPageFolder(this.rootDir, oldName);
966
- this.watcher?.unwatch(newPageFolder);
967
- this.watcher?.unwatch(oldPageFolder);
968
- const existingRoute = Object.keys(this.routes).find((route) => this.routes[route]?.file ===
969
- this.getRelativeRoutePath(oldIndexFilePath));
970
- if (!existingRoute) {
971
- throw new Error(`Route for ${oldName} not found`);
972
- }
973
- // Write the name attribute to the page file
974
- await this.sourceTracker?.renamePage({
975
- newName,
976
- filePath: oldIndexFilePath,
1146
+ const scopeId = this.sourceTracker?.getScopeDefinitionForPage(api.pageName)?.id;
1147
+ this.emit("apiManualDelete", {
1148
+ api: {
1149
+ id: getClientApiId(api.apiPb.metadata.name, api.pageName),
1150
+ apiName: api.apiPb.metadata.name,
1151
+ // TODO(saksham): get pagename more defensively
1152
+ pageName: getPageName(filePath),
1153
+ scopeId,
1154
+ },
977
1155
  });
978
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
979
- await this.writeChanges(changes);
980
- // A rename of a folder is an unlink followed by an add, so the file watcher will take over initializing this
981
- // "new" page
982
- await fs.rename(oldPageFolder, newPageFolder);
983
- // Now we just clean up the routes
984
- await this.removeRoute(oldIndexFilePath);
985
- await this.addRoute(existingRoute, newIndexFilePath);
986
- const newIndexFile = await readFile(newIndexFilePath);
987
- if (!newIndexFile) {
988
- throw new Error(`New index file ${newIndexFilePath} not found`);
989
- }
990
- this.tsFiles[newIndexFilePath] = newIndexFile;
991
- await this.handleNonVisualChangeByDeletingIds(newIndexFilePath, newIndexFile);
992
- this.emit("renamePage", newIndexFilePath);
993
- // Re-add the watcher
994
- this.watcher?.add(newPageFolder);
995
- this.watcher?.add(oldPageFolder);
996
- };
997
- getPageRoots(filePath) {
998
- const scopeFilePath = path.join(path.dirname(filePath), "index.tsx");
999
- const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
1000
- if (!currentFile) {
1001
- return null;
1002
- }
1003
- return getPageRoots(filePath, currentFile);
1004
- }
1005
- getScope(filePath) {
1006
- // get the scope.ts file in the same directory as path
1007
- const scopeFilePath = path.join(path.dirname(filePath), SCOPE_FILE);
1008
- const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
1009
- if (!currentFile) {
1010
- console.log("File not found", scopeFilePath);
1011
- return null;
1012
- }
1013
- const scope = getScope(scopeFilePath, currentFile);
1014
- return scope;
1015
1156
  }
1016
- getRoutes() {
1017
- const routes = [];
1018
- for (const [path, { file }] of Object.entries(this.routes)) {
1019
- routes.push({
1020
- path,
1021
- component: file,
1022
- });
1023
- }
1024
- return routes;
1025
- }
1026
- async addRoute(route, filePath) {
1027
- if (!this.rootDir) {
1028
- throw new Error("Root directory not set");
1029
- }
1030
- this.routes[route] = { file: this.getRelativeRoutePath(filePath) };
1031
- await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
1032
- }
1033
- async removeRoute(filePath) {
1034
- if (!this.rootDir) {
1035
- throw new Error("Root directory not set");
1036
- }
1037
- const relativeFilePath = this.getRelativeRoutePath(filePath);
1038
- this.routes = Object.fromEntries(Object.entries(this.routes).filter(([_, value]) => value.file !== relativeFilePath));
1039
- await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
1040
- }
1041
- async writeChanges(changes, callback) {
1042
- return Promise.all(changes.map(async ({ fileName, source, kind }) => {
1043
- await this.writeFile(fileName, source, kind);
1044
- return callback?.(fileName, source);
1045
- }));
1046
- }
1047
- getNodeForWidgetSourceId(id) {
1048
- return this.sourceTracker?.getElementToLocation(id);
1049
- }
1050
- getAstForWidgetSourceId(id) {
1051
- const filePath = this.sourceTracker?.getElementToFilePath(id);
1052
- if (!filePath) {
1053
- return null;
1054
- }
1055
- return this.sourceTracker?.getCurrentFiles()[filePath]?.ast;
1056
- }
1057
- renameIdentifierInApis = async ({ elementId, oldName, newName, parentBinding, }) => {
1058
- const apisInScope = structuredClone(this.getApisInScope(elementId));
1059
- await this.renameManager.renameEntityInApis({
1060
- oldName,
1061
- newName,
1062
- parentBinding,
1063
- apis: apisInScope,
1064
- });
1065
- // only save the APIs that have changed
1066
- await Promise.all(apisInScope.map(({ api, filePath }) => {
1067
- if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
1068
- return Promise.resolve();
1157
+ getApiFiles() {
1158
+ return Object.keys(this.apiFiles).reduce((acc, key) => {
1159
+ if (!this.apiFiles[key]) {
1160
+ return acc;
1069
1161
  }
1070
- return this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
1071
- }));
1072
- };
1073
- getApisInScope = (elementId) => {
1074
- const filePath = this.sourceTracker?.getElementToFilePath(elementId);
1075
- if (!filePath) {
1076
- return [];
1077
- }
1078
- const pagePath = path.dirname(filePath);
1079
- return Object.entries(this.apiFiles)
1080
- .filter(([filePath]) => filePath.includes(pagePath))
1081
- .map(([filePath, api]) => ({ api, filePath }));
1082
- };
1083
- getRelativeRoutePath(filePath) {
1084
- if (!this.rootDir) {
1085
- throw new Error("Root directory not set");
1086
- }
1087
- // no leading slash
1088
- return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
1162
+ acc[key] = {
1163
+ api: this.createClientApi(this.apiFiles[key]),
1164
+ scopeId: this.apiFiles[key].scopeId,
1165
+ };
1166
+ return acc;
1167
+ }, {});
1089
1168
  }
1090
1169
  // Utilities for converting server API format to Client API format
1091
1170
  // We internally save the API as the server does, but we return should always return it
@@ -1102,19 +1181,21 @@ export default registerPage(Page, { name: "${name}" });
1102
1181
  },
1103
1182
  };
1104
1183
  }
1105
- formatApisToClientApis(serverApis) {
1106
- return Object.keys(serverApis).reduce((acc, key) => {
1107
- if (!serverApis[key]) {
1108
- return acc;
1109
- }
1110
- acc[key] = {
1111
- api: this.createClientApi(serverApis[key]),
1112
- scopeId: serverApis[key].scopeId,
1113
- };
1114
- return acc;
1115
- }, {});
1116
- }
1117
- async processApiUpdates(filePath, fileType) {
1184
+ addApiToScope = async (api) => {
1185
+ if (!this.sourceTracker) {
1186
+ throw new Error("Source tracker not initialized");
1187
+ }
1188
+ // We want to add the API entity to our scope, but we do not want to emit entity events, because
1189
+ // the API update event handles this particular side effect.
1190
+ const scopeId = await this.sourceTracker.addApi({
1191
+ pageName: api.pageName,
1192
+ apiName: api.apiPb.metadata.name,
1193
+ });
1194
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1195
+ await this.writeChanges(changes);
1196
+ return scopeId;
1197
+ };
1198
+ async processApiFileUpdates(filePath, fileType) {
1118
1199
  let yamlPath = filePath;
1119
1200
  if (fileType.type === "python-api-step" ||
1120
1201
  fileType.type === "js-api-step") {
@@ -1132,7 +1213,7 @@ export default registerPage(Page, { name: "${name}" });
1132
1213
  const parsedData = { apiPb: apiContent?.api };
1133
1214
  if (!(yamlPath in this.apiFiles &&
1134
1215
  isEqual(this.apiFiles[yamlPath]?.apiPb, parsedData.apiPb))) {
1135
- const { updatedApi, pageName, isNewApi } = this.updateApi({
1216
+ const { updatedApi, pageName, isNewApi } = this.updateInternalApiData({
1136
1217
  api: parsedData.apiPb,
1137
1218
  stepPathMap: apiContent.stepPathMap,
1138
1219
  }, yamlPath);
@@ -1143,7 +1224,7 @@ export default registerPage(Page, { name: "${name}" });
1143
1224
  return;
1144
1225
  }
1145
1226
  if (isNewApi) {
1146
- await this.createScopedApi(updatedApi);
1227
+ await this.addApiToScope(updatedApi);
1147
1228
  }
1148
1229
  this.emit("apiManualUpdate", {
1149
1230
  api: this.createClientApi(updatedApi),
@@ -1159,9 +1240,56 @@ export default registerPage(Page, { name: "${name}" });
1159
1240
  logger.error(`Error updating API: ${yamlPath}, error: ${error}`);
1160
1241
  }
1161
1242
  }
1243
+ updateInternalApiData = (content, path) => {
1244
+ if (!this.rootDir) {
1245
+ throw new Error("Root directory not set");
1246
+ }
1247
+ const { api: apiContents, stepPathMap } = content;
1248
+ const pageName = getPageName(path);
1249
+ let scopeId = this.sourceTracker?.getScopeDefinitionForPage(pageName)?.id;
1250
+ if (!scopeId) {
1251
+ console.warn("Scope ID not found for API", apiContents.metadata.name);
1252
+ scopeId = "";
1253
+ }
1254
+ const updatedApi = {
1255
+ apiPb: yaml.parse(JSON.stringify(apiContents)),
1256
+ pageName,
1257
+ stepPathMap,
1258
+ scopeId,
1259
+ };
1260
+ const isNewApi = !this.apiFiles[path];
1261
+ this.apiFiles[path] = updatedApi;
1262
+ return { updatedApi, pageName, isNewApi };
1263
+ };
1264
+ renameIdentifierInApis = async ({ elementId, oldName, newName, parentBinding, }) => {
1265
+ const apisInScope = structuredClone(this.getApisInScope(elementId));
1266
+ await this.renameManager.renameEntityInApis({
1267
+ oldName,
1268
+ newName,
1269
+ parentBinding,
1270
+ apis: apisInScope,
1271
+ });
1272
+ // only save the APIs that have changed
1273
+ await Promise.all(apisInScope.map(({ api, filePath }) => {
1274
+ if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
1275
+ return Promise.resolve();
1276
+ }
1277
+ return this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
1278
+ }));
1279
+ };
1280
+ getApisInScope = (elementId) => {
1281
+ const filePath = this.sourceTracker?.getElementToFilePath(elementId);
1282
+ if (!filePath) {
1283
+ return [];
1284
+ }
1285
+ const pagePath = path.dirname(filePath);
1286
+ return Object.entries(this.apiFiles)
1287
+ .filter(([filePath]) => filePath.includes(pagePath))
1288
+ .map(([filePath, api]) => ({ api, filePath }));
1289
+ };
1162
1290
  }
1163
1291
  // Add new mock implementation
1164
- export class MockFileSyncManager extends FileSyncManager {
1292
+ export class MockFileSyncManager extends FileSystemManager {
1165
1293
  tsFiles = {};
1166
1294
  apiFiles = {};
1167
1295
  async watch(_watcher, _path) {
@@ -1215,8 +1343,7 @@ async function readFile(path) {
1215
1343
  // in order for try-catch to work, we need to intentionally await the readFile call,
1216
1344
  // otherwise the error won't be be caught
1217
1345
  // see: https://github.com/nodejs/node/issues/51894#issuecomment-1974017737
1218
- const content = await fs.readFile(path, "utf-8");
1219
- return content;
1346
+ return await fs.readFile(path, "utf-8");
1220
1347
  }
1221
1348
  catch (e) {
1222
1349
  getLogger().error(`error reading file: ${path}`, getErrorMeta(e));
@@ -1237,12 +1364,4 @@ async function readFiles(dir) {
1237
1364
  const getMergedApiContent = async (path) => {
1238
1365
  return readAppApiYamlFile(path.split("/").slice(0, -1).join("/"));
1239
1366
  };
1240
- const getPageName = (path) => {
1241
- const parts = path.split("/");
1242
- const pagesIndex = parts.findIndex((part) => part === "pages");
1243
- if (pagesIndex !== -1 && parts[pagesIndex + 1]) {
1244
- return parts[pagesIndex + 1] ?? "";
1245
- }
1246
- return "";
1247
- };
1248
1367
  //# sourceMappingURL=file-system-manager.js.map