@superblocksteam/vite-plugin-file-sync 2.0.6 → 2.0.7

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