@superblocksteam/vite-plugin-file-sync 2.0.6-next.68 → 2.0.6-next.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 (194) hide show
  1. package/dist/ai-service/app-interface/linter.d.ts +2 -7
  2. package/dist/ai-service/app-interface/linter.d.ts.map +1 -1
  3. package/dist/ai-service/app-interface/linter.js +41 -52
  4. package/dist/ai-service/app-interface/linter.js.map +1 -1
  5. package/dist/ai-service/app-interface/shell.d.ts +0 -2
  6. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  7. package/dist/ai-service/app-interface/shell.js +4 -13
  8. package/dist/ai-service/app-interface/shell.js.map +1 -1
  9. package/dist/ai-service/const.d.ts +0 -2
  10. package/dist/ai-service/const.d.ts.map +1 -1
  11. package/dist/ai-service/const.js +0 -2
  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 +12 -4
  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.js +1 -1
  28. package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.js +1 -1
  29. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.d.ts +1 -1
  30. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.d.ts.map +1 -1
  31. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.js +2 -2
  32. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.js.map +1 -1
  33. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.d.ts +1 -1
  34. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.d.ts.map +1 -1
  35. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.js +2 -2
  36. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.js.map +1 -1
  37. package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.js +1 -1
  38. package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.js +1 -1
  39. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.d.ts +1 -1
  40. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.d.ts.map +1 -1
  41. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.js +2 -2
  42. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.js.map +1 -1
  43. package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.js +1 -1
  44. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.d.ts +1 -1
  45. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.d.ts.map +1 -1
  46. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.js +2 -2
  47. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.js.map +1 -1
  48. package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.js +1 -1
  49. package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.js +1 -1
  50. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.d.ts +1 -1
  51. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.d.ts.map +1 -1
  52. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.js +2 -2
  53. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.js.map +1 -1
  54. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.d.ts +1 -1
  55. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.d.ts.map +1 -1
  56. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.js +2 -2
  57. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.js.map +1 -1
  58. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.d.ts +1 -1
  59. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.d.ts.map +1 -1
  60. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.js +2 -2
  61. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.js.map +1 -1
  62. package/dist/ai-service/prompts/generated/library-typedefs/Dim.js +1 -1
  63. package/dist/ai-service/prompts/generated/library-typedefs/SbEventFlow.js +1 -1
  64. package/dist/ai-service/prompts/generated/library-typedefs/index.d.ts +0 -1
  65. package/dist/ai-service/prompts/generated/library-typedefs/index.d.ts.map +1 -1
  66. package/dist/ai-service/prompts/generated/library-typedefs/index.js +0 -1
  67. package/dist/ai-service/prompts/generated/library-typedefs/index.js.map +1 -1
  68. package/dist/ai-service/prompts/generated/subprompts/full-examples.js +1 -1
  69. package/dist/ai-service/prompts/generated/subprompts/superblocks-api.js +1 -1
  70. package/dist/ai-service/prompts/generated/subprompts/superblocks-components-rules.js +1 -1
  71. package/dist/ai-service/prompts/generated/subprompts/superblocks-custom-components.js +1 -1
  72. package/dist/ai-service/prompts/generated/subprompts/superblocks-data-filtering.js +1 -1
  73. package/dist/ai-service/prompts/generated/subprompts/superblocks-event-flow.js +1 -1
  74. package/dist/ai-service/prompts/generated/subprompts/superblocks-forms.js +1 -1
  75. package/dist/ai-service/prompts/generated/subprompts/superblocks-layouts.js +1 -1
  76. package/dist/ai-service/prompts/generated/subprompts/superblocks-page.js +1 -1
  77. package/dist/ai-service/prompts/generated/subprompts/superblocks-rbac.js +1 -1
  78. package/dist/ai-service/prompts/generated/subprompts/superblocks-routes.js +1 -1
  79. package/dist/ai-service/prompts/generated/subprompts/superblocks-state.js +1 -1
  80. package/dist/ai-service/prompts/generated/subprompts/superblocks-theming.js +1 -1
  81. package/dist/ai-service/prompts/generated/subprompts/system.js +1 -1
  82. package/dist/ai-service/prompts/system.d.ts.map +1 -1
  83. package/dist/ai-service/prompts/system.js +0 -4
  84. package/dist/ai-service/prompts/system.js.map +1 -1
  85. package/dist/ai-service/state-machine/clark-fsm.d.ts +0 -2
  86. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  87. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  88. package/dist/ai-service/state-machine/handlers/agent-planning.js +0 -10
  89. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  90. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts +1 -1
  91. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  92. package/dist/ai-service/state-machine/handlers/llm-generating.js +11 -49
  93. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  94. package/dist/ai-service/transform/add-metadata-to-api-yaml/transformer.d.ts.map +1 -1
  95. package/dist/ai-service/transform/add-metadata-to-api-yaml/transformer.js +2 -3
  96. package/dist/ai-service/transform/add-metadata-to-api-yaml/transformer.js.map +1 -1
  97. package/dist/ai-service/transform/shared.d.ts.map +1 -1
  98. package/dist/ai-service/transform/shared.js +8 -9
  99. package/dist/ai-service/transform/shared.js.map +1 -1
  100. package/dist/ai-service/types.d.ts +1 -1
  101. package/dist/ai-service/types.d.ts.map +1 -1
  102. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  103. package/dist/file-sync-vite-plugin.js +12 -25
  104. package/dist/file-sync-vite-plugin.js.map +1 -1
  105. package/dist/file-system-helpers.d.ts +0 -4
  106. package/dist/file-system-helpers.d.ts.map +1 -1
  107. package/dist/file-system-helpers.js +0 -10
  108. package/dist/file-system-helpers.js.map +1 -1
  109. package/dist/file-system-manager.d.ts +39 -52
  110. package/dist/file-system-manager.d.ts.map +1 -1
  111. package/dist/file-system-manager.js +514 -633
  112. package/dist/file-system-manager.js.map +1 -1
  113. package/dist/index.d.ts +0 -1
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +0 -1
  116. package/dist/index.js.map +1 -1
  117. package/dist/inject-index-vite-plugin.d.ts +2 -0
  118. package/dist/inject-index-vite-plugin.d.ts.map +1 -1
  119. package/dist/inject-index-vite-plugin.js +2 -2
  120. package/dist/inject-index-vite-plugin.js.map +1 -1
  121. package/dist/injected-index.d.ts +2 -2
  122. package/dist/injected-index.d.ts.map +1 -1
  123. package/dist/injected-index.js.map +1 -1
  124. package/dist/lock-service/index.d.ts.map +1 -1
  125. package/dist/lock-service/index.js +2 -13
  126. package/dist/lock-service/index.js.map +1 -1
  127. package/dist/parsing/computed/to-code-computed.d.ts.map +1 -1
  128. package/dist/parsing/computed/to-code-computed.js +3 -3
  129. package/dist/parsing/computed/to-code-computed.js.map +1 -1
  130. package/dist/parsing/events/to-code-events.d.ts.map +1 -1
  131. package/dist/parsing/events/to-code-events.js +0 -11
  132. package/dist/parsing/events/to-code-events.js.map +1 -1
  133. package/dist/parsing/events/to-value-events.d.ts.map +1 -1
  134. package/dist/parsing/events/to-value-events.js +0 -47
  135. package/dist/parsing/events/to-value-events.js.map +1 -1
  136. package/dist/parsing/template/index.js +1 -1
  137. package/dist/parsing/template/index.js.map +1 -1
  138. package/dist/parsing/template/to-code-template.d.ts +1 -2
  139. package/dist/parsing/template/to-code-template.d.ts.map +1 -1
  140. package/dist/parsing/template/to-code-template.js +2 -2
  141. package/dist/parsing/template/to-code-template.js.map +1 -1
  142. package/dist/plugin-options.d.ts +2 -0
  143. package/dist/plugin-options.d.ts.map +1 -1
  144. package/dist/plugin-options.js.map +1 -1
  145. package/dist/routing.d.ts +2 -2
  146. package/dist/routing.d.ts.map +1 -1
  147. package/dist/routing.js +1 -21
  148. package/dist/routing.js.map +1 -1
  149. package/dist/socket-manager.d.ts +2 -2
  150. package/dist/socket-manager.d.ts.map +1 -1
  151. package/dist/socket-manager.js +5 -7
  152. package/dist/socket-manager.js.map +1 -1
  153. package/dist/source-tracker.d.ts +20 -20
  154. package/dist/source-tracker.d.ts.map +1 -1
  155. package/dist/source-tracker.js +13 -31
  156. package/dist/source-tracker.js.map +1 -1
  157. package/dist/sync-service/index.d.ts.map +1 -1
  158. package/dist/sync-service/index.js +1 -2
  159. package/dist/sync-service/index.js.map +1 -1
  160. package/dist/sync-service/list-dir.js +1 -1
  161. package/dist/sync-service/list-dir.js.map +1 -1
  162. package/dist/util/logger.d.ts +17 -13
  163. package/dist/util/logger.d.ts.map +1 -1
  164. package/dist/util/logger.js +44 -34
  165. package/dist/util/logger.js.map +1 -1
  166. package/dist/util/tracing.d.ts +4 -0
  167. package/dist/util/tracing.d.ts.map +1 -0
  168. package/dist/util/tracing.js +56 -0
  169. package/dist/util/tracing.js.map +1 -0
  170. package/dist/util.d.ts +0 -1
  171. package/dist/util.d.ts.map +1 -1
  172. package/dist/util.js +0 -8
  173. package/dist/util.js.map +1 -1
  174. package/package.json +6 -15
  175. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.d.ts +0 -2
  176. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.d.ts.map +0 -1
  177. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.js +0 -6
  178. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.js.map +0 -1
  179. package/dist/binding-extraction/index.d.ts +0 -2
  180. package/dist/binding-extraction/index.d.ts.map +0 -1
  181. package/dist/binding-extraction/index.js +0 -2
  182. package/dist/binding-extraction/index.js.map +0 -1
  183. package/dist/operations/operation-processor.d.ts +0 -24
  184. package/dist/operations/operation-processor.d.ts.map +0 -1
  185. package/dist/operations/operation-processor.js +0 -80
  186. package/dist/operations/operation-processor.js.map +0 -1
  187. package/dist/operations/types.d.ts +0 -8
  188. package/dist/operations/types.d.ts.map +0 -1
  189. package/dist/operations/types.js +0 -2
  190. package/dist/operations/types.js.map +0 -1
  191. package/dist/parsing/index.d.ts +0 -3
  192. package/dist/parsing/index.d.ts.map +0 -1
  193. package/dist/parsing/index.js +0 -3
  194. package/dist/parsing/index.js.map +0 -1
@@ -12,9 +12,8 @@ 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 { getApiFilePath, getPageFolder, isPageFilePath, PAGES_DIRECTORY, ROUTES_FILE, SCOPE_FILE, } from "./file-system-helpers.js";
15
+ import { getPageFolder, 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";
18
17
  import { doesElementHaveBinding } from "./parsing/bindings.js";
19
18
  import { getSbElementId } from "./parsing/ids.js";
20
19
  import { makeJSXAttribute } from "./parsing/jsx.js";
@@ -25,7 +24,6 @@ import { RenameManager } from "./rename-manager.js";
25
24
  import { SourceTracker } from "./source-tracker.js";
26
25
  import { traverse } from "./traverse.js";
27
26
  import { getErrorMeta, getLogger } from "./util/logger.js";
28
- import { getPageName } from "./util.js";
29
27
  const SUPPORTED_FILETYPES = [
30
28
  {
31
29
  type: "tsx",
@@ -47,41 +45,26 @@ const SUPPORTED_FILETYPES = [
47
45
  type: "js-api-step",
48
46
  extension: ".js",
49
47
  },
50
- {
51
- type: "json",
52
- extension: ".json",
53
- },
54
48
  ];
55
49
  const APP_THEME_FILE_NAME = "appTheme.ts";
56
- export class FileSystemManager extends TracedEventEmitter {
50
+ export class FileSyncManager extends TracedEventEmitter {
57
51
  rootDir;
58
52
  tsFiles = {};
59
53
  apiFiles = {};
60
54
  sourceTracker;
61
55
  fsOperationQueue;
62
- operationProcessor;
63
56
  generationNumberSequence;
64
57
  routes = {};
65
- routeChangesQueue = [];
66
58
  watcher;
67
59
  registeredComponentPaths = {};
68
60
  renameManager = new RenameManager();
69
61
  _tracer;
70
- transactionNonce = Date.now();
71
- pendingTransactions = new Set();
72
- processedTransactions = [];
73
62
  constructor(fsOperationQueue, generationNumberSequence, tracer) {
74
63
  super(tracer, { captureRejections: true });
75
64
  this.rootDir = "/";
76
65
  this.fsOperationQueue = fsOperationQueue;
77
66
  this.generationNumberSequence = generationNumberSequence;
78
67
  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();
85
68
  applyErrorHandling(this, {
86
69
  watch: { operation: "editing from code" },
87
70
  handleCreatePage: { operation: "creating a page" },
@@ -127,7 +110,27 @@ export class FileSystemManager extends TracedEventEmitter {
127
110
  }
128
111
  return path.join(this.rootDir, "App.tsx");
129
112
  }
130
- // MARK: core setup/init
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
+ };
131
134
  async watch(watcher, rootPath) {
132
135
  const logger = getLogger();
133
136
  this.rootDir = rootPath;
@@ -213,10 +216,178 @@ export class FileSystemManager extends TracedEventEmitter {
213
216
  return;
214
217
  const { type, path } = file;
215
218
  if (type === "api") {
216
- this.updateInternalApiData(content, path);
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
+ });
217
349
  }
218
350
  });
219
- watcher.on("all", this.handleFileChange);
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];
220
391
  }
221
392
  initializeSourceTracker() {
222
393
  this.sourceTracker = new SourceTracker(this._tracer);
@@ -325,246 +496,6 @@ export class FileSystemManager extends TracedEventEmitter {
325
496
  return null;
326
497
  }
327
498
  }
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
- }
568
499
  getLocalBindingEntities(path) {
569
500
  const logger = getLogger();
570
501
  const files = this.sourceTracker?.getCurrentFiles();
@@ -592,36 +523,8 @@ export class FileSystemManager extends TracedEventEmitter {
592
523
  });
593
524
  return Array.from(localBindingEntities);
594
525
  }
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
526
+ pendingTransactions = new Set();
527
+ processedTransactions = [];
625
528
  flushTransactions = () => {
626
529
  // TODO do something more sophisticated than this.
627
530
  this.processedTransactions = this.processedTransactions.slice(-20);
@@ -634,92 +537,42 @@ export class FileSystemManager extends TracedEventEmitter {
634
537
  }
635
538
  this.pendingTransactions.add(transactionId);
636
539
  };
540
+ transactionNonce = Date.now();
637
541
  // Until we have a sophisticated way to track ALL operations, including non-UI initiated ops, we will use a nonce
638
542
  // TODO https://github.com/superblocksteam/superblocks/pull/11788
639
543
  getProcessedTransactionsWithNonce() {
640
544
  const nonce = `t-${this.transactionNonce++}`;
641
545
  return this.processedTransactions.concat(nonce);
642
546
  }
643
- // MARK: editor operations
644
547
  handleCreatePage = async (payload) => {
645
- this.trackTransaction(payload.transaction?.id);
646
- const { name, route, navigateToRoute, routeTestParams } = payload;
548
+ const { name } = payload;
647
549
  if (!this.rootDir) {
648
550
  throw new Error("Root directory not set");
649
551
  }
650
- if (!route) {
651
- throw new Error("Route is required when creating a page");
652
- }
653
552
  const pagePath = getPageFolder(this.rootDir, name);
654
553
  const pageIndexPath = path.join(pagePath, "index.tsx");
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
-
554
+ const pageContent = /*js*/ `import { SbPage, SbContainer, registerPage } from "@superblocksteam/library";
665
555
  function Page() {
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
- );
556
+ return <SbPage name="${name}">
557
+ <SbContainer width={Dim.fill()} />
558
+ </SbPage>;
674
559
  }
675
560
 
676
- export default registerPage(Page, ${name}Scope);
561
+ export default registerPage(Page, { name: "${name}" });
677
562
  `;
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);
693
563
  await fs.mkdir(pagePath, { recursive: true });
694
564
  await this.writeFile(pageIndexPath, pageContent, "ts");
695
- await this.writeFile(scopePath, scopeContent, "ts");
696
565
  await this.handleNonVisualChangeByDeletingIds(pageIndexPath, pageContent);
697
- await this.handleNonVisualChangeByDeletingIds(scopePath, scopeContent);
698
- this.watcher?.add(pagePath);
699
- await this.addRoute(route, pageIndexPath);
700
566
  this.emit("addPage", pageIndexPath);
701
567
  };
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
- }
715
568
  handleReparent = async (payload, writeFile = true) => {
716
569
  const { from, to, changedProps, transaction } = payload;
717
570
  this.trackTransaction(transaction?.id);
718
- this.sourceTracker?.setProperties({
571
+ await this.sourceTracker?.setProperties({
719
572
  source: from.source,
720
573
  changes: changedProps ?? {},
721
574
  });
722
- this.sourceTracker?.moveElement({
575
+ await this.sourceTracker?.moveElement({
723
576
  from,
724
577
  to,
725
578
  });
@@ -733,7 +586,7 @@ export const ${name} = ${name}Scope.entities;
733
586
  };
734
587
  handleCreateComponent = async (payload, writeFile = true) => {
735
588
  this.trackTransaction(payload.transaction?.id);
736
- const sourceId = this.sourceTracker?.addElement(payload);
589
+ const sourceId = await this.sourceTracker?.addElement(payload);
737
590
  if (writeFile) {
738
591
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
739
592
  await this.writeChanges(changes ?? [], (fileName) => {
@@ -747,7 +600,7 @@ export const ${name} = ${name}Scope.entities;
747
600
  const { elements, transaction } = payload;
748
601
  this.trackTransaction(transaction?.id);
749
602
  for (const element of elements) {
750
- this.sourceTracker?.deleteElement({
603
+ await this.sourceTracker?.deleteElement({
751
604
  source: element.source,
752
605
  scopeName: element.scopeName,
753
606
  });
@@ -763,7 +616,7 @@ export const ${name} = ${name}Scope.entities;
763
616
  handleSetProperty = async (payload, writeFile = true) => {
764
617
  const { element: { source }, property, value, transaction, } = payload;
765
618
  this.trackTransaction(transaction?.id);
766
- this.sourceTracker?.setProperty({
619
+ await this.sourceTracker?.setProperty({
767
620
  source,
768
621
  property,
769
622
  info: value,
@@ -778,7 +631,7 @@ export const ${name} = ${name}Scope.entities;
778
631
  handleSetProperties = async (payload, writeFile = true) => {
779
632
  const { element: { source }, properties, transaction, } = payload;
780
633
  this.trackTransaction(transaction?.id);
781
- this.sourceTracker?.setProperties({
634
+ await this.sourceTracker?.setProperties({
782
635
  source,
783
636
  changes: properties,
784
637
  });
@@ -834,180 +687,39 @@ export const ${name} = ${name}Scope.entities;
834
687
  this.flushTransactions();
835
688
  return returnValues;
836
689
  };
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;
690
+ handleUpdateApi = async (payload) => {
691
+ const { api } = payload;
692
+ if (!this.sourceTracker) {
693
+ throw new Error("Source tracker not initialized");
694
+ }
875
695
  if (!this.rootDir) {
876
696
  throw new Error("Root directory not set");
877
697
  }
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);
698
+ if (!api.pageName) {
699
+ throw new Error("API page name is not set");
890
700
  }
891
- else if (payload.kind === "entity") {
892
- return this.handleRenameEntity(payload);
701
+ const apiName = api.apiPb.metadata.name;
702
+ if (!apiName) {
703
+ throw new Error("API name is not set");
893
704
  }
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];
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];
996
708
  try {
997
709
  const stats = await fs.stat(apiDir);
998
710
  if (!stats.isDirectory()) {
999
- await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
711
+ await fs.mkdir(apiDir, { recursive: true });
1000
712
  }
1001
713
  }
1002
714
  catch {
1003
- await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
715
+ await fs.mkdir(apiDir, { recursive: true });
1004
716
  }
1005
- await this.writeFile(apiFilePath, yaml.stringify(api.apiPb), "api");
717
+ await this.writeFile(apiPath, yaml.stringify(api.apiPb), "api");
1006
718
  const generationNumber = this.generationNumberSequence.next();
1007
719
  const apiDef = this.createClientApi(api);
1008
720
  let scopeId = "";
1009
721
  if (isNewApi) {
1010
- scopeId = await this.addApiToScope(api);
722
+ scopeId = await this.createScopedApi(api);
1011
723
  }
1012
724
  else {
1013
725
  const scopeDef = this.sourceTracker.getScopeDefinitionForPage(api.pageName);
@@ -1016,6 +728,20 @@ export const ${name} = ${name}Scope.entities;
1016
728
  this.emit("apiUpdate", { api: apiDef, scopeId });
1017
729
  return { api: apiDef, scopeId, generationNumber };
1018
730
  };
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
+ };
1019
745
  handleDeleteApi = async (payload) => {
1020
746
  const logger = getLogger();
1021
747
  const { apis } = payload;
@@ -1023,34 +749,40 @@ export const ${name} = ${name}Scope.entities;
1023
749
  throw new Error("Root directory not set");
1024
750
  }
1025
751
  const rootDir = this.rootDir;
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 });
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);
1038
760
  }
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
- }
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);
781
+ }
782
+ resolve(undefined);
783
+ });
784
+ }));
785
+ const deletedApis = (await executeDeleteApis).filter((api) => api !== undefined);
1054
786
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1055
787
  await this.writeChanges(changes);
1056
788
  this.emit("apiDelete", {
@@ -1131,40 +863,229 @@ export const ${name} = ${name}Scope.entities;
1131
863
  // TODO: Should I delete here?
1132
864
  }));
1133
865
  };
1134
- async removeApiData(filePath) {
1135
- const api = this.apiFiles[filePath];
1136
- if (!api) {
1137
- return;
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");
1138
905
  }
1139
- delete this.apiFiles[filePath];
1140
- this.sourceTracker?.deleteApi({
1141
- pageName: api.pageName,
1142
- apiName: api.apiPb.metadata.name,
906
+ const filePath = path.join(this.rootDir, APP_THEME_FILE_NAME);
907
+ await this.sourceTracker?.updateTheme({
908
+ themeFilePath: filePath,
909
+ theme,
1143
910
  });
1144
911
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1145
912
  await this.writeChanges(changes);
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
- },
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,
1155
977
  });
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);
1156
1004
  }
1157
- getApiFiles() {
1158
- return Object.keys(this.apiFiles).reduce((acc, key) => {
1159
- if (!this.apiFiles[key]) {
1160
- return acc;
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
+ }
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();
1161
1069
  }
1162
- acc[key] = {
1163
- api: this.createClientApi(this.apiFiles[key]),
1164
- scopeId: this.apiFiles[key].scopeId,
1165
- };
1166
- return acc;
1167
- }, {});
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);
1168
1089
  }
1169
1090
  // Utilities for converting server API format to Client API format
1170
1091
  // We internally save the API as the server does, but we return should always return it
@@ -1181,21 +1102,19 @@ export const ${name} = ${name}Scope.entities;
1181
1102
  },
1182
1103
  };
1183
1104
  }
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) {
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) {
1199
1118
  let yamlPath = filePath;
1200
1119
  if (fileType.type === "python-api-step" ||
1201
1120
  fileType.type === "js-api-step") {
@@ -1213,7 +1132,7 @@ export const ${name} = ${name}Scope.entities;
1213
1132
  const parsedData = { apiPb: apiContent?.api };
1214
1133
  if (!(yamlPath in this.apiFiles &&
1215
1134
  isEqual(this.apiFiles[yamlPath]?.apiPb, parsedData.apiPb))) {
1216
- const { updatedApi, pageName, isNewApi } = this.updateInternalApiData({
1135
+ const { updatedApi, pageName, isNewApi } = this.updateApi({
1217
1136
  api: parsedData.apiPb,
1218
1137
  stepPathMap: apiContent.stepPathMap,
1219
1138
  }, yamlPath);
@@ -1224,7 +1143,7 @@ export const ${name} = ${name}Scope.entities;
1224
1143
  return;
1225
1144
  }
1226
1145
  if (isNewApi) {
1227
- await this.addApiToScope(updatedApi);
1146
+ await this.createScopedApi(updatedApi);
1228
1147
  }
1229
1148
  this.emit("apiManualUpdate", {
1230
1149
  api: this.createClientApi(updatedApi),
@@ -1240,56 +1159,9 @@ export const ${name} = ${name}Scope.entities;
1240
1159
  logger.error(`Error updating API: ${yamlPath}, error: ${error}`);
1241
1160
  }
1242
1161
  }
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
- };
1290
1162
  }
1291
1163
  // Add new mock implementation
1292
- export class MockFileSyncManager extends FileSystemManager {
1164
+ export class MockFileSyncManager extends FileSyncManager {
1293
1165
  tsFiles = {};
1294
1166
  apiFiles = {};
1295
1167
  async watch(_watcher, _path) {
@@ -1343,7 +1215,8 @@ async function readFile(path) {
1343
1215
  // in order for try-catch to work, we need to intentionally await the readFile call,
1344
1216
  // otherwise the error won't be be caught
1345
1217
  // see: https://github.com/nodejs/node/issues/51894#issuecomment-1974017737
1346
- return await fs.readFile(path, "utf-8");
1218
+ const content = await fs.readFile(path, "utf-8");
1219
+ return content;
1347
1220
  }
1348
1221
  catch (e) {
1349
1222
  getLogger().error(`error reading file: ${path}`, getErrorMeta(e));
@@ -1364,4 +1237,12 @@ async function readFiles(dir) {
1364
1237
  const getMergedApiContent = async (path) => {
1365
1238
  return readAppApiYamlFile(path.split("/").slice(0, -1).join("/"));
1366
1239
  };
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
+ };
1367
1248
  //# sourceMappingURL=file-system-manager.js.map