@superblocksteam/vite-plugin-file-sync 2.0.6-next.59 → 2.0.6-next.6

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 (182) 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 +6 -15
  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 +35 -40
  110. package/dist/file-system-manager.d.ts.map +1 -1
  111. package/dist/file-system-manager.js +490 -553
  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/socket-manager.d.ts +2 -2
  146. package/dist/socket-manager.d.ts.map +1 -1
  147. package/dist/socket-manager.js +2 -2
  148. package/dist/socket-manager.js.map +1 -1
  149. package/dist/source-tracker.d.ts +20 -20
  150. package/dist/source-tracker.d.ts.map +1 -1
  151. package/dist/source-tracker.js +13 -31
  152. package/dist/source-tracker.js.map +1 -1
  153. package/dist/sync-service/index.d.ts.map +1 -1
  154. package/dist/sync-service/index.js +1 -2
  155. package/dist/sync-service/index.js.map +1 -1
  156. package/dist/sync-service/list-dir.js +1 -1
  157. package/dist/sync-service/list-dir.js.map +1 -1
  158. package/dist/util/logger.d.ts +17 -13
  159. package/dist/util/logger.d.ts.map +1 -1
  160. package/dist/util/logger.js +44 -34
  161. package/dist/util/logger.js.map +1 -1
  162. package/package.json +6 -15
  163. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.d.ts +0 -2
  164. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.d.ts.map +0 -1
  165. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.js +0 -6
  166. package/dist/ai-service/prompts/generated/library-typedefs/TextStyleWithVariant.js.map +0 -1
  167. package/dist/binding-extraction/index.d.ts +0 -2
  168. package/dist/binding-extraction/index.d.ts.map +0 -1
  169. package/dist/binding-extraction/index.js +0 -2
  170. package/dist/binding-extraction/index.js.map +0 -1
  171. package/dist/operations/operation-processor.d.ts +0 -24
  172. package/dist/operations/operation-processor.d.ts.map +0 -1
  173. package/dist/operations/operation-processor.js +0 -80
  174. package/dist/operations/operation-processor.js.map +0 -1
  175. package/dist/operations/types.d.ts +0 -8
  176. package/dist/operations/types.d.ts.map +0 -1
  177. package/dist/operations/types.js +0 -2
  178. package/dist/operations/types.js.map +0 -1
  179. package/dist/parsing/index.d.ts +0 -3
  180. package/dist/parsing/index.d.ts.map +0 -1
  181. package/dist/parsing/index.js +0 -3
  182. 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";
@@ -48,34 +47,24 @@ const SUPPORTED_FILETYPES = [
48
47
  },
49
48
  ];
50
49
  const APP_THEME_FILE_NAME = "appTheme.ts";
51
- export class FileSystemManager extends TracedEventEmitter {
50
+ export class FileSyncManager extends TracedEventEmitter {
52
51
  rootDir;
53
52
  tsFiles = {};
54
53
  apiFiles = {};
55
54
  sourceTracker;
56
55
  fsOperationQueue;
57
- operationProcessor;
58
56
  generationNumberSequence;
59
57
  routes = {};
60
58
  watcher;
61
59
  registeredComponentPaths = {};
62
60
  renameManager = new RenameManager();
63
61
  _tracer;
64
- transactionNonce = Date.now();
65
- pendingTransactions = new Set();
66
- processedTransactions = [];
67
62
  constructor(fsOperationQueue, generationNumberSequence, tracer) {
68
63
  super(tracer, { captureRejections: true });
69
64
  this.rootDir = "/";
70
65
  this.fsOperationQueue = fsOperationQueue;
71
66
  this.generationNumberSequence = generationNumberSequence;
72
67
  this._tracer = tracer;
73
- // intentionally a new queue here, we don't want to share the queue with the fsOperationQueue
74
- this.operationProcessor = new OperationProcessor({
75
- batchWindowMs: 50,
76
- maxBatchSize: 100,
77
- });
78
- this.operationProcessor.disable();
79
68
  applyErrorHandling(this, {
80
69
  watch: { operation: "editing from code" },
81
70
  handleCreatePage: { operation: "creating a page" },
@@ -121,7 +110,27 @@ export class FileSystemManager extends TracedEventEmitter {
121
110
  }
122
111
  return path.join(this.rootDir, "App.tsx");
123
112
  }
124
- // 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
+ };
125
134
  async watch(watcher, rootPath) {
126
135
  const logger = getLogger();
127
136
  this.rootDir = rootPath;
@@ -207,10 +216,178 @@ export class FileSystemManager extends TracedEventEmitter {
207
216
  return;
208
217
  const { type, path } = file;
209
218
  if (type === "api") {
210
- 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
+ });
211
349
  }
212
350
  });
213
- 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];
214
391
  }
215
392
  initializeSourceTracker() {
216
393
  this.sourceTracker = new SourceTracker(this._tracer);
@@ -319,235 +496,6 @@ export class FileSystemManager extends TracedEventEmitter {
319
496
  return null;
320
497
  }
321
498
  }
322
- enableOperationsQueue() {
323
- this.operationProcessor.enable();
324
- }
325
- disableOperationsQueue() {
326
- this.operationProcessor.disable();
327
- }
328
- async flushOperations() {
329
- await this.operationProcessor.flush();
330
- }
331
- // MARK: file change handling
332
- handleFileChange = async (event, filePath) => {
333
- const logger = getLogger();
334
- logger.info(`File changed: ${filePath}, event: ${event}`);
335
- const rootPath = this.rootDir;
336
- if (!rootPath) {
337
- throw new Error("Root directory not set");
338
- }
339
- // Skip directory events
340
- if (event === "addDir" || event === "unlinkDir") {
341
- return;
342
- }
343
- const routePath = path.join(rootPath, ROUTES_FILE);
344
- const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
345
- // Only handle files we care about and that are in our root path
346
- if (!fileType || !filePath.startsWith(rootPath)) {
347
- return;
348
- }
349
- // Queue the operation based on the event type
350
- switch (event) {
351
- case "add": {
352
- const data = await readFile(filePath);
353
- if (typeof data !== "string")
354
- return;
355
- const isPage = isPageFilePath(filePath);
356
- if (isPage) {
357
- void this.operationProcessor.addOperation({
358
- metadata: {
359
- filePath,
360
- },
361
- execute: async () => {
362
- const file = await readFile(filePath);
363
- if (!file) {
364
- logger.error(`Failed to read file: ${filePath}`);
365
- return;
366
- }
367
- if (!(filePath in this.tsFiles)) {
368
- this.tsFiles[filePath] = file;
369
- this.handleNonVisualChangeByDeletingIds(filePath, file);
370
- this.emit("addPage", filePath);
371
- }
372
- },
373
- });
374
- }
375
- else {
376
- void this.operationProcessor.addOperation({
377
- metadata: {
378
- filePath,
379
- },
380
- execute: async () => {
381
- switch (fileType.type) {
382
- case "api":
383
- case "python-api-step":
384
- case "js-api-step": {
385
- await this.processApiFileUpdates(filePath, fileType);
386
- break;
387
- }
388
- }
389
- },
390
- });
391
- }
392
- break;
393
- }
394
- case "change": {
395
- if (filePath === routePath) {
396
- void this.operationProcessor.addOperation({
397
- metadata: {
398
- filePath,
399
- },
400
- priority: true,
401
- execute: async () => {
402
- try {
403
- const data = JSON.parse((await readFile(filePath)) ?? "{}");
404
- if (!isEqual(this.routes, data)) {
405
- this.routes = data;
406
- this.emit("routesChanged", this.routes);
407
- }
408
- }
409
- catch (e) {
410
- logger.error("Error parsing routes file", getErrorMeta(e));
411
- }
412
- },
413
- });
414
- return;
415
- }
416
- else {
417
- void this.operationProcessor.addOperation({
418
- metadata: {
419
- filePath,
420
- },
421
- execute: async () => {
422
- const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
423
- if (!fileType || !filePath.startsWith(rootPath)) {
424
- return;
425
- }
426
- const data = await readFile(filePath);
427
- if (typeof data !== "string")
428
- return;
429
- switch (fileType.type) {
430
- case "tsx":
431
- case "scope":
432
- {
433
- if (!(filePath in this.tsFiles &&
434
- this.tsFiles[filePath] === data)) {
435
- logger.info(`File changed: ${filePath} updating AST tracker`);
436
- this.tsFiles[filePath] = data;
437
- // only update the source tracker if the file is different
438
- this.handleNonVisualChangeByDeletingIds(filePath, data);
439
- this.emit("fileChanged", filePath, data, true);
440
- }
441
- else {
442
- logger.info(`File unchanged from last tracked state: ${filePath}`);
443
- this.emit("fileChanged", filePath, data, false);
444
- }
445
- }
446
- break;
447
- case "api":
448
- case "python-api-step":
449
- case "js-api-step":
450
- {
451
- await this.processApiFileUpdates(filePath, fileType);
452
- }
453
- break;
454
- }
455
- },
456
- });
457
- }
458
- break;
459
- }
460
- case "unlink": {
461
- if (filePath in this.tsFiles) {
462
- void this.operationProcessor.addOperation({
463
- metadata: {
464
- filePath,
465
- },
466
- execute: async () => {
467
- await this.deleteTsFile(filePath);
468
- },
469
- });
470
- }
471
- else if (filePath in this.apiFiles) {
472
- void this.operationProcessor.addOperation({
473
- metadata: {
474
- filePath,
475
- },
476
- execute: async () => {
477
- await this.removeApiData(filePath);
478
- },
479
- });
480
- }
481
- break;
482
- }
483
- }
484
- };
485
- async deleteTsFile(filePath) {
486
- delete this.tsFiles[filePath];
487
- this.sourceTracker?.removeFile(filePath);
488
- if (isPageFilePath(filePath)) {
489
- this.emit("deletePage", filePath);
490
- }
491
- }
492
- getTsFilePaths() {
493
- return Object.keys(this.tsFiles);
494
- }
495
- getSourceTracker() {
496
- return this.sourceTracker;
497
- }
498
- // MARK: fs read/write
499
- async writeFile(path, content, kind) {
500
- if (kind === "ts") {
501
- // happens eagerly regardless of error - possible desync
502
- this.tsFiles[path] = content;
503
- // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
504
- await this.fsOperationQueue.enqueue(async () => {
505
- await fs.writeFile(path, content);
506
- });
507
- }
508
- else if (kind === "api") {
509
- const currentApiFile = this.apiFiles[path];
510
- const apiPb = yaml.parse(content);
511
- // Client APIs have id, but server APIs don't
512
- if (apiPb.metadata.id) {
513
- delete apiPb.metadata.id;
514
- }
515
- const stepPathMap = currentApiFile?.stepPathMap ?? {};
516
- this.updateInternalApiData({ api: apiPb, stepPathMap }, path);
517
- // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
518
- await this.fsOperationQueue.enqueue(async () => {
519
- await writeApiFiles({ apiPb }, "api", path.split("/").slice(0, -1).join("/"), false, [], [], { extractLargeSourceFiles: true, minLinesForExtraction: 1 }, new Set(Object.keys(this.apiFiles)), stepPathMap);
520
- // stepPathMap will be generated after the write when the file doesn't exist
521
- if (this.apiFiles[path]) {
522
- this.apiFiles[path].stepPathMap = stepPathMap;
523
- }
524
- });
525
- }
526
- }
527
- readFile(path) {
528
- return this.tsFiles[path];
529
- }
530
- async addRoute(route, filePath) {
531
- if (!this.rootDir) {
532
- throw new Error("Root directory not set");
533
- }
534
- this.routes[route] = { file: this.getRelativeRoutePath(filePath) };
535
- await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
536
- }
537
- async removeRoute(filePath) {
538
- if (!this.rootDir) {
539
- throw new Error("Root directory not set");
540
- }
541
- const relativeFilePath = this.getRelativeRoutePath(filePath);
542
- this.routes = Object.fromEntries(Object.entries(this.routes).filter(([_, value]) => value.file !== relativeFilePath));
543
- await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
544
- }
545
- async writeChanges(changes, callback) {
546
- return Promise.all(changes.map(async ({ fileName, source, kind }) => {
547
- await this.writeFile(fileName, source, kind);
548
- return callback?.(fileName, source);
549
- }));
550
- }
551
499
  getLocalBindingEntities(path) {
552
500
  const logger = getLogger();
553
501
  const files = this.sourceTracker?.getCurrentFiles();
@@ -575,36 +523,8 @@ export class FileSystemManager extends TracedEventEmitter {
575
523
  });
576
524
  return Array.from(localBindingEntities);
577
525
  }
578
- getPageRoots(filePath) {
579
- const scopeFilePath = path.join(path.dirname(filePath), "index.tsx");
580
- const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
581
- if (!currentFile) {
582
- return null;
583
- }
584
- return getPageRoots(filePath, currentFile);
585
- }
586
- getScope(filePath) {
587
- // get the scope.ts file in the same directory as path
588
- const scopeFilePath = path.join(path.dirname(filePath), SCOPE_FILE);
589
- const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
590
- if (!currentFile) {
591
- console.log("File not found", scopeFilePath);
592
- return null;
593
- }
594
- const scope = getScope(scopeFilePath, currentFile);
595
- return scope;
596
- }
597
- getRoutes() {
598
- const routes = [];
599
- for (const [path, { file }] of Object.entries(this.routes)) {
600
- routes.push({
601
- path,
602
- component: file,
603
- });
604
- }
605
- return routes;
606
- }
607
- // MARK: transaction handling
526
+ pendingTransactions = new Set();
527
+ processedTransactions = [];
608
528
  flushTransactions = () => {
609
529
  // TODO do something more sophisticated than this.
610
530
  this.processedTransactions = this.processedTransactions.slice(-20);
@@ -617,13 +537,13 @@ export class FileSystemManager extends TracedEventEmitter {
617
537
  }
618
538
  this.pendingTransactions.add(transactionId);
619
539
  };
540
+ transactionNonce = Date.now();
620
541
  // Until we have a sophisticated way to track ALL operations, including non-UI initiated ops, we will use a nonce
621
542
  // TODO https://github.com/superblocksteam/superblocks/pull/11788
622
543
  getProcessedTransactionsWithNonce() {
623
544
  const nonce = `t-${this.transactionNonce++}`;
624
545
  return this.processedTransactions.concat(nonce);
625
546
  }
626
- // MARK: editor operations
627
547
  handleCreatePage = async (payload) => {
628
548
  const { name } = payload;
629
549
  if (!this.rootDir) {
@@ -640,9 +560,7 @@ function Page() {
640
560
 
641
561
  export default registerPage(Page, { name: "${name}" });
642
562
  `;
643
- await this.fsOperationQueue.enqueue(async () => {
644
- await fs.mkdir(pagePath, { recursive: true });
645
- });
563
+ await fs.mkdir(pagePath, { recursive: true });
646
564
  await this.writeFile(pageIndexPath, pageContent, "ts");
647
565
  await this.handleNonVisualChangeByDeletingIds(pageIndexPath, pageContent);
648
566
  this.emit("addPage", pageIndexPath);
@@ -650,11 +568,11 @@ export default registerPage(Page, { name: "${name}" });
650
568
  handleReparent = async (payload, writeFile = true) => {
651
569
  const { from, to, changedProps, transaction } = payload;
652
570
  this.trackTransaction(transaction?.id);
653
- this.sourceTracker?.setProperties({
571
+ await this.sourceTracker?.setProperties({
654
572
  source: from.source,
655
573
  changes: changedProps ?? {},
656
574
  });
657
- this.sourceTracker?.moveElement({
575
+ await this.sourceTracker?.moveElement({
658
576
  from,
659
577
  to,
660
578
  });
@@ -668,7 +586,7 @@ export default registerPage(Page, { name: "${name}" });
668
586
  };
669
587
  handleCreateComponent = async (payload, writeFile = true) => {
670
588
  this.trackTransaction(payload.transaction?.id);
671
- const sourceId = this.sourceTracker?.addElement(payload);
589
+ const sourceId = await this.sourceTracker?.addElement(payload);
672
590
  if (writeFile) {
673
591
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
674
592
  await this.writeChanges(changes ?? [], (fileName) => {
@@ -682,7 +600,7 @@ export default registerPage(Page, { name: "${name}" });
682
600
  const { elements, transaction } = payload;
683
601
  this.trackTransaction(transaction?.id);
684
602
  for (const element of elements) {
685
- this.sourceTracker?.deleteElement({
603
+ await this.sourceTracker?.deleteElement({
686
604
  source: element.source,
687
605
  scopeName: element.scopeName,
688
606
  });
@@ -698,7 +616,7 @@ export default registerPage(Page, { name: "${name}" });
698
616
  handleSetProperty = async (payload, writeFile = true) => {
699
617
  const { element: { source }, property, value, transaction, } = payload;
700
618
  this.trackTransaction(transaction?.id);
701
- this.sourceTracker?.setProperty({
619
+ await this.sourceTracker?.setProperty({
702
620
  source,
703
621
  property,
704
622
  info: value,
@@ -713,7 +631,7 @@ export default registerPage(Page, { name: "${name}" });
713
631
  handleSetProperties = async (payload, writeFile = true) => {
714
632
  const { element: { source }, properties, transaction, } = payload;
715
633
  this.trackTransaction(transaction?.id);
716
- this.sourceTracker?.setProperties({
634
+ await this.sourceTracker?.setProperties({
717
635
  source,
718
636
  changes: properties,
719
637
  });
@@ -769,147 +687,6 @@ export default registerPage(Page, { name: "${name}" });
769
687
  this.flushTransactions();
770
688
  return returnValues;
771
689
  };
772
- // MARK: entity operations
773
- handleAddEntity = async (payload) => {
774
- this.sourceTracker?.addEntity({
775
- scopeId: payload.scopeId,
776
- entity: {
777
- type: payload.type,
778
- name: payload.name,
779
- attributes: payload.attributes,
780
- },
781
- });
782
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
783
- await this.writeChanges(changes, (fileName) => {
784
- this.emit("addEntity", fileName, payload);
785
- });
786
- };
787
- handleUpdateEntity = async (payload) => {
788
- this.sourceTracker?.updateEntity({
789
- entityId: payload.entityId,
790
- updates: payload.updates,
791
- });
792
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
793
- await this.writeChanges(changes, (fileName) => {
794
- this.emit("updateEntity", fileName, payload);
795
- });
796
- };
797
- handleDeleteEntity = async (payload) => {
798
- const deletedEntityName = this.sourceTracker?.deleteEntity({
799
- entityId: payload.entityId,
800
- });
801
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
802
- await this.writeChanges(changes, (fileName) => {
803
- if (deletedEntityName) {
804
- this.emit("deleteEntity", fileName, deletedEntityName);
805
- }
806
- });
807
- };
808
- handleUpdateTheme = async (payload) => {
809
- const { theme } = payload;
810
- if (!this.rootDir) {
811
- throw new Error("Root directory not set");
812
- }
813
- const filePath = path.join(this.rootDir, APP_THEME_FILE_NAME);
814
- this.sourceTracker?.updateTheme({
815
- themeFilePath: filePath,
816
- theme,
817
- });
818
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
819
- await this.writeChanges(changes);
820
- };
821
- // MARK: rename operations
822
- handleRenameElement = async (payload) => {
823
- if (payload.kind === "component") {
824
- return this.handleRenameComponent(payload);
825
- }
826
- else if (payload.kind === "entity") {
827
- return this.handleRenameEntity(payload);
828
- }
829
- else if (payload.kind === "page") {
830
- return this.handleRenamePage(payload);
831
- }
832
- };
833
- handleRenameComponent = async (payload) => {
834
- const { elementId, newName, oldName, scopeName } = payload;
835
- await this.sourceTracker?.renameComponent({
836
- widgetSourceId: elementId,
837
- oldName,
838
- newName,
839
- scopeName,
840
- });
841
- await this.renameIdentifierInApis({
842
- elementId,
843
- oldName,
844
- newName,
845
- });
846
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
847
- await this.writeChanges(changes, (fileName) => {
848
- this.emit("renameComponent", fileName);
849
- });
850
- };
851
- handleRenameEntity = async (payload) => {
852
- const { elementId, newName, oldName, scopeName } = payload;
853
- this.sourceTracker?.renameEntity({
854
- entityId: elementId,
855
- oldName,
856
- newName,
857
- scopeName,
858
- });
859
- await this.renameIdentifierInApis(payload);
860
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
861
- await this.writeChanges(changes, (fileName) => {
862
- this.emit("renameEntity", fileName);
863
- });
864
- };
865
- handleRenamePage = async (payload) => {
866
- const { newName, oldName } = payload;
867
- if (!this.rootDir) {
868
- throw new Error("Root directory not set");
869
- }
870
- const newPageFolder = getPageFolder(this.rootDir, newName);
871
- const newIndexFilePath = path.join(newPageFolder, "index.tsx");
872
- const oldIndexFilePath = path.join(this.rootDir, "pages", oldName, "index.tsx");
873
- const oldPageFolder = getPageFolder(this.rootDir, oldName);
874
- this.watcher?.unwatch(newPageFolder);
875
- this.watcher?.unwatch(oldPageFolder);
876
- const existingRoute = Object.keys(this.routes).find((route) => this.routes[route]?.file ===
877
- this.getRelativeRoutePath(oldIndexFilePath));
878
- if (!existingRoute) {
879
- throw new Error(`Route for ${oldName} not found`);
880
- }
881
- // Write the name attribute to the page file
882
- await this.sourceTracker?.renamePage({
883
- newName,
884
- filePath: oldIndexFilePath,
885
- });
886
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
887
- await this.writeChanges(changes);
888
- // A rename of a folder is an unlink followed by an add, so the file watcher will take over initializing this
889
- // "new" page
890
- await fs.rename(oldPageFolder, newPageFolder);
891
- // Now we just clean up the routes
892
- await this.removeRoute(oldIndexFilePath);
893
- await this.addRoute(existingRoute, newIndexFilePath);
894
- const newIndexFile = await readFile(newIndexFilePath);
895
- if (!newIndexFile) {
896
- throw new Error(`New index file ${newIndexFilePath} not found`);
897
- }
898
- this.tsFiles[newIndexFilePath] = newIndexFile;
899
- await this.handleNonVisualChangeByDeletingIds(newIndexFilePath, newIndexFile);
900
- this.emit("renamePage", newIndexFilePath);
901
- // Re-add the watcher
902
- this.watcher?.add(newPageFolder);
903
- this.watcher?.add(oldPageFolder);
904
- };
905
- getRelativeRoutePath(filePath) {
906
- if (!this.rootDir) {
907
- throw new Error("Root directory not set");
908
- }
909
- // no leading slash
910
- return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
911
- }
912
- // MARK: API operations
913
690
  handleUpdateApi = async (payload) => {
914
691
  const { api } = payload;
915
692
  if (!this.sourceTracker) {
@@ -925,24 +702,24 @@ export default registerPage(Page, { name: "${name}" });
925
702
  if (!apiName) {
926
703
  throw new Error("API name is not set");
927
704
  }
928
- const apiFilePath = getApiFilePath(this.rootDir, api.pageName, apiName);
929
- const apiDir = path.dirname(apiFilePath);
930
- 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];
931
708
  try {
932
709
  const stats = await fs.stat(apiDir);
933
710
  if (!stats.isDirectory()) {
934
- await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
711
+ await fs.mkdir(apiDir, { recursive: true });
935
712
  }
936
713
  }
937
714
  catch {
938
- await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
715
+ await fs.mkdir(apiDir, { recursive: true });
939
716
  }
940
- await this.writeFile(apiFilePath, yaml.stringify(api.apiPb), "api");
717
+ await this.writeFile(apiPath, yaml.stringify(api.apiPb), "api");
941
718
  const generationNumber = this.generationNumberSequence.next();
942
719
  const apiDef = this.createClientApi(api);
943
720
  let scopeId = "";
944
721
  if (isNewApi) {
945
- scopeId = await this.addApiToScope(api);
722
+ scopeId = await this.createScopedApi(api);
946
723
  }
947
724
  else {
948
725
  const scopeDef = this.sourceTracker.getScopeDefinitionForPage(api.pageName);
@@ -951,6 +728,20 @@ export default registerPage(Page, { name: "${name}" });
951
728
  this.emit("apiUpdate", { api: apiDef, scopeId });
952
729
  return { api: apiDef, scopeId, generationNumber };
953
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
+ };
954
745
  handleDeleteApi = async (payload) => {
955
746
  const logger = getLogger();
956
747
  const { apis } = payload;
@@ -958,34 +749,40 @@ export default registerPage(Page, { name: "${name}" });
958
749
  throw new Error("Root directory not set");
959
750
  }
960
751
  const rootDir = this.rootDir;
961
- const deletedApis = [];
962
- for (const { apiName, pageName } of apis) {
963
- const apiFilePath = getApiFilePath(rootDir, pageName, apiName);
964
- const api = this.apiFiles[apiFilePath];
965
- if (!api || !this.sourceTracker) {
966
- continue;
967
- }
968
- const apiDir = path.dirname(apiFilePath);
969
- try {
970
- const stats = await fs.stat(apiDir);
971
- if (stats.isDirectory()) {
972
- 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);
973
760
  }
974
- delete this.apiFiles[apiFilePath];
975
- const scopeId = this.sourceTracker.deleteApi({
976
- pageName,
977
- apiName,
978
- });
979
- deletedApis.push({
980
- apiName,
981
- pageName,
982
- scopeId,
983
- });
984
- }
985
- catch (e) {
986
- logger.warn(`Could not delete api ${apiFilePath} ${JSON.stringify(e)}`);
987
- }
988
- }
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);
989
786
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
990
787
  await this.writeChanges(changes);
991
788
  this.emit("apiDelete", {
@@ -1066,40 +863,229 @@ export default registerPage(Page, { name: "${name}" });
1066
863
  // TODO: Should I delete here?
1067
864
  }));
1068
865
  };
1069
- async removeApiData(filePath) {
1070
- const api = this.apiFiles[filePath];
1071
- if (!api) {
1072
- 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");
1073
905
  }
1074
- delete this.apiFiles[filePath];
1075
- this.sourceTracker?.deleteApi({
1076
- pageName: api.pageName,
1077
- 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,
1078
910
  });
1079
911
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1080
912
  await this.writeChanges(changes);
1081
- const scopeId = this.sourceTracker?.getScopeDefinitionForPage(api.pageName)?.id;
1082
- this.emit("apiManualDelete", {
1083
- api: {
1084
- id: getClientApiId(api.apiPb.metadata.name, api.pageName),
1085
- apiName: api.apiPb.metadata.name,
1086
- // TODO(saksham): get pagename more defensively
1087
- pageName: getPageName(filePath),
1088
- scopeId,
1089
- },
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,
1090
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,
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);
1091
1004
  }
1092
- getApiFiles() {
1093
- return Object.keys(this.apiFiles).reduce((acc, key) => {
1094
- if (!this.apiFiles[key]) {
1095
- 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();
1096
1069
  }
1097
- acc[key] = {
1098
- api: this.createClientApi(this.apiFiles[key]),
1099
- scopeId: this.apiFiles[key].scopeId,
1100
- };
1101
- return acc;
1102
- }, {});
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);
1103
1089
  }
1104
1090
  // Utilities for converting server API format to Client API format
1105
1091
  // We internally save the API as the server does, but we return should always return it
@@ -1116,21 +1102,19 @@ export default registerPage(Page, { name: "${name}" });
1116
1102
  },
1117
1103
  };
1118
1104
  }
1119
- addApiToScope = async (api) => {
1120
- if (!this.sourceTracker) {
1121
- throw new Error("Source tracker not initialized");
1122
- }
1123
- // We want to add the API entity to our scope, but we do not want to emit entity events, because
1124
- // the API update event handles this particular side effect.
1125
- const scopeId = await this.sourceTracker.addApi({
1126
- pageName: api.pageName,
1127
- apiName: api.apiPb.metadata.name,
1128
- });
1129
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1130
- await this.writeChanges(changes);
1131
- return scopeId;
1132
- };
1133
- 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) {
1134
1118
  let yamlPath = filePath;
1135
1119
  if (fileType.type === "python-api-step" ||
1136
1120
  fileType.type === "js-api-step") {
@@ -1148,7 +1132,7 @@ export default registerPage(Page, { name: "${name}" });
1148
1132
  const parsedData = { apiPb: apiContent?.api };
1149
1133
  if (!(yamlPath in this.apiFiles &&
1150
1134
  isEqual(this.apiFiles[yamlPath]?.apiPb, parsedData.apiPb))) {
1151
- const { updatedApi, pageName, isNewApi } = this.updateInternalApiData({
1135
+ const { updatedApi, pageName, isNewApi } = this.updateApi({
1152
1136
  api: parsedData.apiPb,
1153
1137
  stepPathMap: apiContent.stepPathMap,
1154
1138
  }, yamlPath);
@@ -1159,7 +1143,7 @@ export default registerPage(Page, { name: "${name}" });
1159
1143
  return;
1160
1144
  }
1161
1145
  if (isNewApi) {
1162
- await this.addApiToScope(updatedApi);
1146
+ await this.createScopedApi(updatedApi);
1163
1147
  }
1164
1148
  this.emit("apiManualUpdate", {
1165
1149
  api: this.createClientApi(updatedApi),
@@ -1175,56 +1159,9 @@ export default registerPage(Page, { name: "${name}" });
1175
1159
  logger.error(`Error updating API: ${yamlPath}, error: ${error}`);
1176
1160
  }
1177
1161
  }
1178
- updateInternalApiData = (content, path) => {
1179
- if (!this.rootDir) {
1180
- throw new Error("Root directory not set");
1181
- }
1182
- const { api: apiContents, stepPathMap } = content;
1183
- const pageName = getPageName(path);
1184
- let scopeId = this.sourceTracker?.getScopeDefinitionForPage(pageName)?.id;
1185
- if (!scopeId) {
1186
- console.warn("Scope ID not found for API", apiContents.metadata.name);
1187
- scopeId = "";
1188
- }
1189
- const updatedApi = {
1190
- apiPb: yaml.parse(JSON.stringify(apiContents)),
1191
- pageName,
1192
- stepPathMap,
1193
- scopeId,
1194
- };
1195
- const isNewApi = !this.apiFiles[path];
1196
- this.apiFiles[path] = updatedApi;
1197
- return { updatedApi, pageName, isNewApi };
1198
- };
1199
- renameIdentifierInApis = async ({ elementId, oldName, newName, parentBinding, }) => {
1200
- const apisInScope = structuredClone(this.getApisInScope(elementId));
1201
- await this.renameManager.renameEntityInApis({
1202
- oldName,
1203
- newName,
1204
- parentBinding,
1205
- apis: apisInScope,
1206
- });
1207
- // only save the APIs that have changed
1208
- await Promise.all(apisInScope.map(({ api, filePath }) => {
1209
- if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
1210
- return Promise.resolve();
1211
- }
1212
- return this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
1213
- }));
1214
- };
1215
- getApisInScope = (elementId) => {
1216
- const filePath = this.sourceTracker?.getElementToFilePath(elementId);
1217
- if (!filePath) {
1218
- return [];
1219
- }
1220
- const pagePath = path.dirname(filePath);
1221
- return Object.entries(this.apiFiles)
1222
- .filter(([filePath]) => filePath.includes(pagePath))
1223
- .map(([filePath, api]) => ({ api, filePath }));
1224
- };
1225
1162
  }
1226
1163
  // Add new mock implementation
1227
- export class MockFileSyncManager extends FileSystemManager {
1164
+ export class MockFileSyncManager extends FileSyncManager {
1228
1165
  tsFiles = {};
1229
1166
  apiFiles = {};
1230
1167
  async watch(_watcher, _path) {
@@ -1300,7 +1237,7 @@ async function readFiles(dir) {
1300
1237
  const getMergedApiContent = async (path) => {
1301
1238
  return readAppApiYamlFile(path.split("/").slice(0, -1).join("/"));
1302
1239
  };
1303
- export const getPageName = (path) => {
1240
+ const getPageName = (path) => {
1304
1241
  const parts = path.split("/");
1305
1242
  const pagesIndex = parts.findIndex((part) => part === "pages");
1306
1243
  if (pagesIndex !== -1 && parts[pagesIndex + 1]) {