@superblocksteam/vite-plugin-file-sync 2.0.6-next.22 → 2.0.6-next.24

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 (41) hide show
  1. package/dist/ai-service/prompts/generated/library-components/SbButtonPropsDocs.js +1 -1
  2. package/dist/ai-service/prompts/generated/library-components/SbCheckboxPropsDocs.js +1 -1
  3. package/dist/ai-service/prompts/generated/library-components/SbColumnPropsDocs.js +1 -1
  4. package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.js +1 -1
  5. package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.js +1 -1
  6. package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.js +1 -1
  7. package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.js +1 -1
  8. package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.js +1 -1
  9. package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.js +1 -1
  10. package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.js +1 -1
  11. package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.js +1 -1
  12. package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.js +1 -1
  13. package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.js +1 -1
  14. package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.js +1 -1
  15. package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.js +1 -1
  16. package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.js +1 -1
  17. package/dist/ai-service/prompts/generated/library-typedefs/Dim.js +1 -1
  18. package/dist/ai-service/prompts/generated/library-typedefs/SbEventFlow.js +1 -1
  19. package/dist/ai-service/prompts/generated/subprompts/full-examples.js +1 -1
  20. package/dist/ai-service/prompts/generated/subprompts/superblocks-api.js +1 -1
  21. package/dist/ai-service/prompts/generated/subprompts/superblocks-components-rules.js +1 -1
  22. package/dist/ai-service/prompts/generated/subprompts/superblocks-custom-components.js +1 -1
  23. package/dist/ai-service/prompts/generated/subprompts/superblocks-data-filtering.js +1 -1
  24. package/dist/ai-service/prompts/generated/subprompts/superblocks-event-flow.js +1 -1
  25. package/dist/ai-service/prompts/generated/subprompts/superblocks-forms.js +1 -1
  26. package/dist/ai-service/prompts/generated/subprompts/superblocks-layouts.js +1 -1
  27. package/dist/ai-service/prompts/generated/subprompts/superblocks-page.js +1 -1
  28. package/dist/ai-service/prompts/generated/subprompts/superblocks-rbac.js +1 -1
  29. package/dist/ai-service/prompts/generated/subprompts/superblocks-routes.js +1 -1
  30. package/dist/ai-service/prompts/generated/subprompts/superblocks-state.js +1 -1
  31. package/dist/ai-service/prompts/generated/subprompts/superblocks-theming.js +1 -1
  32. package/dist/ai-service/prompts/generated/subprompts/system.js +1 -1
  33. package/dist/file-system-helpers.d.ts +2 -0
  34. package/dist/file-system-helpers.d.ts.map +1 -1
  35. package/dist/file-system-helpers.js +4 -0
  36. package/dist/file-system-helpers.js.map +1 -1
  37. package/dist/file-system-manager.d.ts +30 -34
  38. package/dist/file-system-manager.d.ts.map +1 -1
  39. package/dist/file-system-manager.js +425 -434
  40. package/dist/file-system-manager.js.map +1 -1
  41. package/package.json +5 -5
@@ -12,7 +12,7 @@ import { generateJSXAttribute } from "./codegen.js";
12
12
  import { ComponentsManager } from "./components-manager.js";
13
13
  import { addLegacyCustomComponentVariables, modifyLegacyCustomComponentElements, modifyLegacyCustomComponentImports, } from "./custom-components.js";
14
14
  import { applyErrorHandling, } from "./errors/error-handler.js";
15
- import { getPageFolder, isPageFilePath, PAGES_DIRECTORY, ROUTES_FILE, SCOPE_FILE, } from "./file-system-helpers.js";
15
+ import { getApiFilePath, getPageFolder, isPageFilePath, PAGES_DIRECTORY, ROUTES_FILE, SCOPE_FILE, } from "./file-system-helpers.js";
16
16
  import { generate } from "./generate.js";
17
17
  import { OperationProcessor } from "./operations/operation-processor.js";
18
18
  import { doesElementHaveBinding } from "./parsing/bindings.js";
@@ -61,6 +61,9 @@ export class FileSystemManager extends TracedEventEmitter {
61
61
  registeredComponentPaths = {};
62
62
  renameManager = new RenameManager();
63
63
  _tracer;
64
+ transactionNonce = Date.now();
65
+ pendingTransactions = new Set();
66
+ processedTransactions = [];
64
67
  constructor(fsOperationQueue, generationNumberSequence, tracer) {
65
68
  super(tracer, { captureRejections: true });
66
69
  this.rootDir = "/";
@@ -118,27 +121,7 @@ export class FileSystemManager extends TracedEventEmitter {
118
121
  }
119
122
  return path.join(this.rootDir, "App.tsx");
120
123
  }
121
- updateApi = (content, path) => {
122
- if (!this.rootDir) {
123
- throw new Error("Root directory not set");
124
- }
125
- const { api: apiContents, stepPathMap } = content;
126
- const pageName = getPageName(path);
127
- let scopeId = this.sourceTracker?.getScopeDefinitionForPage(pageName)?.id;
128
- if (!scopeId) {
129
- console.warn("Scope ID not found for API", apiContents.metadata.name);
130
- scopeId = "";
131
- }
132
- const updatedApi = {
133
- apiPb: yaml.parse(JSON.stringify(apiContents)),
134
- pageName,
135
- stepPathMap,
136
- scopeId,
137
- };
138
- const isNewApi = !this.apiFiles[path];
139
- this.apiFiles[path] = updatedApi;
140
- return { updatedApi, pageName, isNewApi };
141
- };
124
+ // MARK: core setup/init
142
125
  async watch(watcher, rootPath) {
143
126
  const logger = getLogger();
144
127
  this.rootDir = rootPath;
@@ -224,11 +207,118 @@ export class FileSystemManager extends TracedEventEmitter {
224
207
  return;
225
208
  const { type, path } = file;
226
209
  if (type === "api") {
227
- this.updateApi(content, path);
210
+ this.updateInternalApiData(content, path);
228
211
  }
229
212
  });
230
213
  watcher.on("all", this.handleFileChange);
231
214
  }
215
+ initializeSourceTracker() {
216
+ this.sourceTracker = new SourceTracker(this._tracer);
217
+ this.sourceTracker.initializeSourceTracker({
218
+ files: this.tsFiles,
219
+ });
220
+ }
221
+ handleNonVisualChangeByDeletingIds(path, content) {
222
+ if (!this.sourceTracker) {
223
+ throw new Error("Source tracker not initialized");
224
+ }
225
+ return this.sourceTracker.handleNonVisualChangeByDeletingIds(path, content);
226
+ }
227
+ async getFileWithIds(filePath) {
228
+ const logger = getLogger();
229
+ if (!this.sourceTracker) {
230
+ throw new Error("Source tracker not initialized");
231
+ }
232
+ const files = this.sourceTracker.getCurrentFiles();
233
+ if (!files[filePath]) {
234
+ throw new Error("File not found in source tracker " + filePath);
235
+ }
236
+ const ast = files[filePath].ast;
237
+ if (!ast) {
238
+ throw new Error("No AST found for file in source tracker " + filePath);
239
+ }
240
+ const clonedAst = await transformFromAstAsync(ast, undefined, {
241
+ ast: true,
242
+ });
243
+ if (!clonedAst?.ast) {
244
+ throw new Error("Failed to clone AST for file " + filePath);
245
+ }
246
+ const componentsManager = ComponentsManager.getInstance();
247
+ const processedTransactions = this.getProcessedTransactionsWithNonce();
248
+ const customImports = new Set();
249
+ traverse(clonedAst.ast, {
250
+ Program: {
251
+ exit(nodePath) {
252
+ if (isCustomBuildEnabled()) {
253
+ addLegacyCustomComponentVariables(nodePath, customImports);
254
+ }
255
+ // we want to inject the component name into the file so we can use it for hot reloading purposes
256
+ const name = componentsManager.getComponentName(filePath);
257
+ if (name) {
258
+ nodePath.pushContainer("body", t.variableDeclaration("const", [
259
+ t.variableDeclarator(t.identifier("componentName"), t.stringLiteral(name)),
260
+ ]));
261
+ }
262
+ },
263
+ },
264
+ ImportDeclaration(path) {
265
+ if (isCustomBuildEnabled()) {
266
+ // when we see a custom component, we want to track the import and modify it to point
267
+ // to the built source
268
+ modifyLegacyCustomComponentImports(path, customImports);
269
+ }
270
+ },
271
+ JSXElement(path) {
272
+ if (isCustomBuildEnabled()) {
273
+ // based on the imports we see, we want to modify the JSXElement that uses the custom component
274
+ modifyLegacyCustomComponentElements(path, customImports);
275
+ }
276
+ },
277
+ ReturnStatement(path) {
278
+ const argument = path.get("argument");
279
+ if (!argument.isJSXElement()) {
280
+ return;
281
+ }
282
+ const openingElement = argument.get("openingElement");
283
+ if (!openingElement.isJSXOpeningElement()) {
284
+ return;
285
+ }
286
+ openingElement.pushContainer("attributes", makeJSXAttribute(ROOT_WIDGET_ATTRIBUTE, "true"));
287
+ openingElement.pushContainer("attributes", generateJSXAttribute(PROCESSED_TRANSACTIONS_ATTRIBUTE, {
288
+ value: processedTransactions,
289
+ type: "EXPRESSION",
290
+ __info: true,
291
+ }));
292
+ },
293
+ JSXOpeningElement(path) {
294
+ const elementId = getSbElementId(path.node);
295
+ if (elementId) {
296
+ path.pushContainer("attributes", makeJSXAttribute(WIDGET_SOURCE_ID_ATTRIBUTE, elementId));
297
+ }
298
+ },
299
+ ObjectProperty(path) {
300
+ const elementId = getSbElementId(path.node);
301
+ if (!elementId) {
302
+ path.skip();
303
+ return;
304
+ }
305
+ path.traverse({
306
+ ObjectExpression(path) {
307
+ path.pushContainer("properties", t.objectProperty(t.identifier("id"), t.stringLiteral(elementId)));
308
+ path.skip();
309
+ },
310
+ });
311
+ },
312
+ });
313
+ try {
314
+ const result = generate(clonedAst.ast);
315
+ return result.code;
316
+ }
317
+ catch (e) {
318
+ logger.error("Error generating file with ids", getErrorMeta(e));
319
+ return null;
320
+ }
321
+ }
232
322
  enableOperationsQueue() {
233
323
  this.operationProcessor.enable();
234
324
  }
@@ -238,6 +328,7 @@ export class FileSystemManager extends TracedEventEmitter {
238
328
  async flushOperations() {
239
329
  await this.operationProcessor.flush();
240
330
  }
331
+ // MARK: file change handling
241
332
  handleFileChange = async (event, filePath) => {
242
333
  const logger = getLogger();
243
334
  logger.info(`File changed: ${filePath}, event: ${event}`);
@@ -291,7 +382,7 @@ export class FileSystemManager extends TracedEventEmitter {
291
382
  case "api":
292
383
  case "python-api-step":
293
384
  case "js-api-step": {
294
- await this.processApiUpdates(filePath, fileType);
385
+ await this.processApiFileUpdates(filePath, fileType);
295
386
  break;
296
387
  }
297
388
  }
@@ -357,7 +448,7 @@ export class FileSystemManager extends TracedEventEmitter {
357
448
  case "python-api-step":
358
449
  case "js-api-step":
359
450
  {
360
- await this.processApiUpdates(filePath, fileType);
451
+ await this.processApiFileUpdates(filePath, fileType);
361
452
  }
362
453
  break;
363
454
  }
@@ -383,7 +474,7 @@ export class FileSystemManager extends TracedEventEmitter {
383
474
  filePath,
384
475
  },
385
476
  execute: async () => {
386
- await this.deleteApiFile(filePath);
477
+ await this.removeApiData(filePath);
387
478
  },
388
479
  });
389
480
  }
@@ -391,9 +482,6 @@ export class FileSystemManager extends TracedEventEmitter {
391
482
  }
392
483
  }
393
484
  };
394
- getApiFiles() {
395
- return this.formatApisToClientApis(this.apiFiles);
396
- }
397
485
  async deleteTsFile(filePath) {
398
486
  delete this.tsFiles[filePath];
399
487
  this.sourceTracker?.removeFile(filePath);
@@ -401,35 +489,13 @@ export class FileSystemManager extends TracedEventEmitter {
401
489
  this.emit("deletePage", filePath);
402
490
  }
403
491
  }
404
- async deleteApiFile(filePath) {
405
- const api = this.apiFiles[filePath];
406
- if (!api) {
407
- return;
408
- }
409
- delete this.apiFiles[filePath];
410
- this.sourceTracker?.deleteApi({
411
- pageName: api.pageName,
412
- apiName: api.apiPb.metadata.name,
413
- });
414
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
415
- await this.writeChanges(changes);
416
- const scopeId = this.sourceTracker?.getScopeDefinitionForPage(api.pageName)?.id;
417
- this.emit("apiManualDelete", {
418
- api: {
419
- id: getClientApiId(api.apiPb.metadata.name, api.pageName),
420
- apiName: api.apiPb.metadata.name,
421
- // TODO(saksham): get pagename more defensively
422
- pageName: getPageName(filePath),
423
- scopeId,
424
- },
425
- });
426
- }
427
492
  getTsFilePaths() {
428
493
  return Object.keys(this.tsFiles);
429
494
  }
430
495
  getSourceTracker() {
431
496
  return this.sourceTracker;
432
497
  }
498
+ // MARK: fs read/write
433
499
  async writeFile(path, content, kind) {
434
500
  if (kind === "ts") {
435
501
  // happens eagerly regardless of error - possible desync
@@ -447,7 +513,7 @@ export class FileSystemManager extends TracedEventEmitter {
447
513
  delete apiPb.metadata.id;
448
514
  }
449
515
  const stepPathMap = currentApiFile?.stepPathMap ?? {};
450
- this.updateApi({ api: apiPb, stepPathMap }, path);
516
+ this.updateInternalApiData({ api: apiPb, stepPathMap }, path);
451
517
  // TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
452
518
  await this.fsOperationQueue.enqueue(async () => {
453
519
  await writeApiFiles({ apiPb }, "api", path.split("/").slice(0, -1).join("/"), false, [], [], { extractLargeSourceFiles: true, minLinesForExtraction: 1 }, new Set(Object.keys(this.apiFiles)), stepPathMap);
@@ -461,142 +527,84 @@ export class FileSystemManager extends TracedEventEmitter {
461
527
  readFile(path) {
462
528
  return this.tsFiles[path];
463
529
  }
464
- initializeSourceTracker() {
465
- this.sourceTracker = new SourceTracker(this._tracer);
466
- this.sourceTracker.initializeSourceTracker({
467
- files: this.tsFiles,
468
- });
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));
469
536
  }
470
- handleNonVisualChangeByDeletingIds(path, content) {
471
- if (!this.sourceTracker) {
472
- throw new Error("Source tracker not initialized");
537
+ async removeRoute(filePath) {
538
+ if (!this.rootDir) {
539
+ throw new Error("Root directory not set");
473
540
  }
474
- return this.sourceTracker.handleNonVisualChangeByDeletingIds(path, content);
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));
475
544
  }
476
- async getFileWithIds(filePath) {
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
+ getLocalBindingEntities(path) {
477
552
  const logger = getLogger();
478
- if (!this.sourceTracker) {
479
- throw new Error("Source tracker not initialized");
480
- }
481
- const files = this.sourceTracker.getCurrentFiles();
482
- if (!files[filePath]) {
483
- throw new Error("File not found in source tracker " + filePath);
553
+ const files = this.sourceTracker?.getCurrentFiles();
554
+ if (!files || !files[path]) {
555
+ logger.warn(`File not found in source tracker for local binding entities: ${path}`);
556
+ return [];
484
557
  }
485
- const ast = files[filePath].ast;
558
+ const ast = files[path].ast;
486
559
  if (!ast) {
487
- throw new Error("No AST found for file in source tracker " + filePath);
488
- }
489
- const clonedAst = await transformFromAstAsync(ast, undefined, {
490
- ast: true,
491
- });
492
- if (!clonedAst?.ast) {
493
- throw new Error("Failed to clone AST for file " + filePath);
560
+ logger.warn(`No AST found for file in source tracker for local binding entities: ${path}`);
561
+ return [];
494
562
  }
495
- const componentsManager = ComponentsManager.getInstance();
496
- const processedTransactions = this.getProcessedTransactionsWithNonce();
497
- const customImports = new Set();
498
- traverse(clonedAst.ast, {
499
- Program: {
500
- exit(nodePath) {
501
- if (isCustomBuildEnabled()) {
502
- addLegacyCustomComponentVariables(nodePath, customImports);
503
- }
504
- // we want to inject the component name into the file so we can use it for hot reloading purposes
505
- const name = componentsManager.getComponentName(filePath);
506
- if (name) {
507
- nodePath.pushContainer("body", t.variableDeclaration("const", [
508
- t.variableDeclarator(t.identifier("componentName"), t.stringLiteral(name)),
509
- ]));
510
- }
511
- },
512
- },
513
- ImportDeclaration(path) {
514
- if (isCustomBuildEnabled()) {
515
- // when we see a custom component, we want to track the import and modify it to point
516
- // to the built source
517
- modifyLegacyCustomComponentImports(path, customImports);
518
- }
519
- },
520
- JSXElement(path) {
521
- if (isCustomBuildEnabled()) {
522
- // based on the imports we see, we want to modify the JSXElement that uses the custom component
523
- modifyLegacyCustomComponentElements(path, customImports);
524
- }
525
- },
526
- ReturnStatement(path) {
527
- const argument = path.get("argument");
528
- if (!argument.isJSXElement()) {
563
+ const localBindingEntities = new Set();
564
+ traverse(ast, {
565
+ JSXOpeningElement(path) {
566
+ const elementId = getSbElementId(path.node);
567
+ if (!elementId) {
529
568
  return;
530
569
  }
531
- const openingElement = argument.get("openingElement");
532
- if (!openingElement.isJSXOpeningElement()) {
533
- return;
570
+ const hasLocalBinding = doesElementHaveBinding(path);
571
+ if (hasLocalBinding) {
572
+ localBindingEntities.add(elementId);
534
573
  }
535
- openingElement.pushContainer("attributes", makeJSXAttribute(ROOT_WIDGET_ATTRIBUTE, "true"));
536
- openingElement.pushContainer("attributes", generateJSXAttribute(PROCESSED_TRANSACTIONS_ATTRIBUTE, {
537
- value: processedTransactions,
538
- type: "EXPRESSION",
539
- __info: true,
540
- }));
541
- },
542
- JSXOpeningElement(path) {
543
- const elementId = getSbElementId(path.node);
544
- if (elementId) {
545
- path.pushContainer("attributes", makeJSXAttribute(WIDGET_SOURCE_ID_ATTRIBUTE, elementId));
546
- }
547
- },
548
- ObjectProperty(path) {
549
- const elementId = getSbElementId(path.node);
550
- if (!elementId) {
551
- path.skip();
552
- return;
553
- }
554
- path.traverse({
555
- ObjectExpression(path) {
556
- path.pushContainer("properties", t.objectProperty(t.identifier("id"), t.stringLiteral(elementId)));
557
- path.skip();
558
- },
559
- });
560
574
  },
561
575
  });
562
- try {
563
- const result = generate(clonedAst.ast);
564
- return result.code;
565
- }
566
- catch (e) {
567
- logger.error("Error generating file with ids", getErrorMeta(e));
576
+ return Array.from(localBindingEntities);
577
+ }
578
+ getPageRoots(filePath) {
579
+ const scopeFilePath = path.join(path.dirname(filePath), "index.tsx");
580
+ const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
581
+ if (!currentFile) {
568
582
  return null;
569
583
  }
584
+ return getPageRoots(filePath, currentFile);
570
585
  }
571
- getLocalBindingEntities(path) {
572
- const logger = getLogger();
573
- const files = this.sourceTracker?.getCurrentFiles();
574
- if (!files || !files[path]) {
575
- logger.warn(`File not found in source tracker for local binding entities: ${path}`);
576
- return [];
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;
577
593
  }
578
- const ast = files[path].ast;
579
- if (!ast) {
580
- logger.warn(`No AST found for file in source tracker for local binding entities: ${path}`);
581
- return [];
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
+ });
582
604
  }
583
- const localBindingEntities = new Set();
584
- traverse(ast, {
585
- JSXOpeningElement(path) {
586
- const elementId = getSbElementId(path.node);
587
- if (!elementId) {
588
- return;
589
- }
590
- const hasLocalBinding = doesElementHaveBinding(path);
591
- if (hasLocalBinding) {
592
- localBindingEntities.add(elementId);
593
- }
594
- },
595
- });
596
- return Array.from(localBindingEntities);
605
+ return routes;
597
606
  }
598
- pendingTransactions = new Set();
599
- processedTransactions = [];
607
+ // MARK: transaction handling
600
608
  flushTransactions = () => {
601
609
  // TODO do something more sophisticated than this.
602
610
  this.processedTransactions = this.processedTransactions.slice(-20);
@@ -609,13 +617,13 @@ export class FileSystemManager extends TracedEventEmitter {
609
617
  }
610
618
  this.pendingTransactions.add(transactionId);
611
619
  };
612
- transactionNonce = Date.now();
613
620
  // Until we have a sophisticated way to track ALL operations, including non-UI initiated ops, we will use a nonce
614
621
  // TODO https://github.com/superblocksteam/superblocks/pull/11788
615
622
  getProcessedTransactionsWithNonce() {
616
623
  const nonce = `t-${this.transactionNonce++}`;
617
624
  return this.processedTransactions.concat(nonce);
618
625
  }
626
+ // MARK: editor operations
619
627
  handleCreatePage = async (payload) => {
620
628
  const { name } = payload;
621
629
  if (!this.rootDir) {
@@ -632,7 +640,9 @@ function Page() {
632
640
 
633
641
  export default registerPage(Page, { name: "${name}" });
634
642
  `;
635
- await fs.mkdir(pagePath, { recursive: true });
643
+ await this.fsOperationQueue.enqueue(async () => {
644
+ await fs.mkdir(pagePath, { recursive: true });
645
+ });
636
646
  await this.writeFile(pageIndexPath, pageContent, "ts");
637
647
  await this.handleNonVisualChangeByDeletingIds(pageIndexPath, pageContent);
638
648
  this.emit("addPage", pageIndexPath);
@@ -759,182 +769,7 @@ export default registerPage(Page, { name: "${name}" });
759
769
  this.flushTransactions();
760
770
  return returnValues;
761
771
  };
762
- handleUpdateApi = async (payload) => {
763
- const { api } = payload;
764
- if (!this.sourceTracker) {
765
- throw new Error("Source tracker not initialized");
766
- }
767
- if (!this.rootDir) {
768
- throw new Error("Root directory not set");
769
- }
770
- if (!api.pageName) {
771
- throw new Error("API page name is not set");
772
- }
773
- const apiName = api.apiPb.metadata.name;
774
- if (!apiName) {
775
- throw new Error("API name is not set");
776
- }
777
- const apiDir = path.join(this.rootDir, "pages", api.pageName, "apis", apiName);
778
- const apiPath = path.join(apiDir, "api.yaml");
779
- const isNewApi = !this.getApiFiles()[apiPath];
780
- try {
781
- const stats = await fs.stat(apiDir);
782
- if (!stats.isDirectory()) {
783
- await fs.mkdir(apiDir, { recursive: true });
784
- }
785
- }
786
- catch {
787
- await fs.mkdir(apiDir, { recursive: true });
788
- }
789
- await this.writeFile(apiPath, yaml.stringify(api.apiPb), "api");
790
- const generationNumber = this.generationNumberSequence.next();
791
- const apiDef = this.createClientApi(api);
792
- let scopeId = "";
793
- if (isNewApi) {
794
- scopeId = await this.createScopedApi(api);
795
- }
796
- else {
797
- const scopeDef = this.sourceTracker.getScopeDefinitionForPage(api.pageName);
798
- scopeId = scopeDef?.id ?? "";
799
- }
800
- this.emit("apiUpdate", { api: apiDef, scopeId });
801
- return { api: apiDef, scopeId, generationNumber };
802
- };
803
- createScopedApi = async (api) => {
804
- if (!this.sourceTracker) {
805
- throw new Error("Source tracker not initialized");
806
- }
807
- // We want to add the API entity to our scope, but we do not want to emit entity events, because
808
- // the API update event handles this particular side effect.
809
- const scopeId = await this.sourceTracker.addApi({
810
- pageName: api.pageName,
811
- apiName: api.apiPb.metadata.name,
812
- });
813
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
814
- await this.writeChanges(changes);
815
- return scopeId;
816
- };
817
- handleDeleteApi = async (payload) => {
818
- const logger = getLogger();
819
- const { apis } = payload;
820
- if (!this.rootDir) {
821
- throw new Error("Root directory not set");
822
- }
823
- const rootDir = this.rootDir;
824
- const executeDeleteApis = Promise.all(apis.map(({ apiName, pageName }) => {
825
- return new Promise(
826
- // eslint-disable-next-line no-async-promise-executor
827
- async (resolve) => {
828
- const apiFilePath = path.join(rootDir, "pages", pageName, "apis", apiName, "api.yaml");
829
- const api = this.apiFiles[apiFilePath];
830
- if (!api || !this.sourceTracker) {
831
- return resolve(undefined);
832
- }
833
- const apiDir = path.join(rootDir, "pages", pageName, "apis", apiName);
834
- try {
835
- const stats = await fs.stat(apiDir);
836
- if (stats.isDirectory()) {
837
- await fs.rmdir(apiDir, { recursive: true });
838
- }
839
- delete this.apiFiles[apiFilePath];
840
- const scopeId = this.sourceTracker.deleteApi({
841
- pageName,
842
- apiName,
843
- });
844
- resolve({
845
- apiName,
846
- pageName,
847
- scopeId,
848
- });
849
- }
850
- catch (e) {
851
- logger.warn(`Could not delete api ${apiFilePath} ${JSON.stringify(e)}`);
852
- resolve(undefined);
853
- }
854
- resolve(undefined);
855
- });
856
- }));
857
- const deletedApis = (await executeDeleteApis).filter((api) => api !== undefined);
858
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
859
- await this.writeChanges(changes);
860
- this.emit("apiDelete", {
861
- apis: deletedApis.filter((api) => api !== undefined),
862
- });
863
- return { deletedApis };
864
- };
865
- handleRenameApi = async (payload) => {
866
- const { oldName, newName, pageName } = payload;
867
- if (!this.rootDir) {
868
- throw new Error("Root directory not set");
869
- }
870
- const pagePath = getPageFolder(this.rootDir, pageName);
871
- const existingApiFolder = path.join(this.rootDir, "pages", pageName, "apis", oldName);
872
- const newApiFolder = path.join(this.rootDir, "pages", pageName, "apis", newName);
873
- const files = this.sourceTracker?.getCurrentFiles();
874
- const file = files?.[path.join(pagePath, "index.tsx")];
875
- if (!file || !file.ast) {
876
- throw new Error(`Page ${pageName} not found`);
877
- }
878
- const apiFilePath = path.join(existingApiFolder, "api.yaml");
879
- const apiDef = this.apiFiles[apiFilePath];
880
- if (!apiDef) {
881
- throw new Error(`API ${oldName} not found`);
882
- }
883
- const otherPageAPIs = structuredClone(Object.keys(this.apiFiles)
884
- .filter((path) => path.startsWith(pagePath) && path !== apiFilePath)
885
- .map((path) => ({
886
- api: { apiPb: this.apiFiles[path]?.apiPb },
887
- filePath: path,
888
- }))
889
- .filter((api) => !!api.api));
890
- const newApiFolderExists = await fs.stat(newApiFolder).catch(() => false);
891
- if (newApiFolderExists) {
892
- throw new Error(`API ${newName} already exists`);
893
- }
894
- this.watcher?.unwatch(existingApiFolder);
895
- this.watcher?.unwatch(newApiFolder);
896
- apiDef.apiPb.metadata.name = newName;
897
- await fs.rename(existingApiFolder, newApiFolder);
898
- const scopeDef = this.sourceTracker?.getScopeDefinitionForPage(pageName);
899
- if (!scopeDef) {
900
- throw new Error(`Scope definition not found for API`);
901
- }
902
- this.sourceTracker?.renameEntity({
903
- oldName,
904
- newName,
905
- entityId: scopeDef.scopeNameToEntityId[oldName],
906
- });
907
- delete this.apiFiles[apiFilePath];
908
- this.writeFile(path.join(newApiFolder, "api.yaml"), yaml.stringify(apiDef.apiPb), "api");
909
- this.watcher?.add(existingApiFolder);
910
- this.watcher?.add(newApiFolder);
911
- const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
912
- await this.writeChanges(changes);
913
- this.emit("apiManualUpdate", {
914
- api: this.createClientApi(apiDef),
915
- oldName,
916
- pageName,
917
- scopeId: scopeDef?.id ?? "",
918
- }, true);
919
- await this.renameManager.renameEntityInApis({
920
- oldName,
921
- newName,
922
- apis: otherPageAPIs,
923
- });
924
- // only save the APIs that have changed
925
- await Promise.all(otherPageAPIs.map(async ({ api, filePath }) => {
926
- if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
927
- return Promise.resolve();
928
- }
929
- await this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
930
- this.emit("apiManualUpdate", {
931
- api: this.createClientApi(api),
932
- pageName,
933
- scopeId: scopeDef?.id ?? "",
934
- }, true);
935
- // TODO: Should I delete here?
936
- }));
937
- };
772
+ // MARK: entity operations
938
773
  handleAddEntity = async (payload) => {
939
774
  this.sourceTracker?.addEntity({
940
775
  scopeId: payload.scopeId,
@@ -983,6 +818,7 @@ export default registerPage(Page, { name: "${name}" });
983
818
  const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
984
819
  await this.writeChanges(changes);
985
820
  };
821
+ // MARK: rename operations
986
822
  handleRenameElement = async (payload) => {
987
823
  if (payload.kind === "component") {
988
824
  return this.handleRenameComponent(payload);
@@ -1066,98 +902,204 @@ export default registerPage(Page, { name: "${name}" });
1066
902
  this.watcher?.add(newPageFolder);
1067
903
  this.watcher?.add(oldPageFolder);
1068
904
  };
1069
- getPageRoots(filePath) {
1070
- const scopeFilePath = path.join(path.dirname(filePath), "index.tsx");
1071
- const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
1072
- if (!currentFile) {
1073
- return null;
905
+ getRelativeRoutePath(filePath) {
906
+ if (!this.rootDir) {
907
+ throw new Error("Root directory not set");
1074
908
  }
1075
- return getPageRoots(filePath, currentFile);
909
+ // no leading slash
910
+ return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
1076
911
  }
1077
- getScope(filePath) {
1078
- // get the scope.ts file in the same directory as path
1079
- const scopeFilePath = path.join(path.dirname(filePath), SCOPE_FILE);
1080
- const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
1081
- if (!currentFile) {
1082
- console.log("File not found", scopeFilePath);
1083
- return null;
912
+ // MARK: API operations
913
+ handleUpdateApi = async (payload) => {
914
+ const { api } = payload;
915
+ if (!this.sourceTracker) {
916
+ throw new Error("Source tracker not initialized");
1084
917
  }
1085
- const scope = getScope(scopeFilePath, currentFile);
1086
- return scope;
1087
- }
1088
- getRoutes() {
1089
- const routes = [];
1090
- for (const [path, { file }] of Object.entries(this.routes)) {
1091
- routes.push({
1092
- path,
1093
- component: file,
1094
- });
918
+ if (!this.rootDir) {
919
+ throw new Error("Root directory not set");
1095
920
  }
1096
- return routes;
1097
- }
1098
- async addRoute(route, filePath) {
921
+ if (!api.pageName) {
922
+ throw new Error("API page name is not set");
923
+ }
924
+ const apiName = api.apiPb.metadata.name;
925
+ if (!apiName) {
926
+ throw new Error("API name is not set");
927
+ }
928
+ const apiFilePath = getApiFilePath(this.rootDir, api.pageName, apiName);
929
+ const apiDir = path.dirname(apiFilePath);
930
+ const isNewApi = !this.getApiFiles()[apiFilePath];
931
+ try {
932
+ const stats = await fs.stat(apiDir);
933
+ if (!stats.isDirectory()) {
934
+ await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
935
+ }
936
+ }
937
+ catch {
938
+ await this.fsOperationQueue.enqueue(async () => await fs.mkdir(apiDir, { recursive: true }));
939
+ }
940
+ await this.writeFile(apiFilePath, yaml.stringify(api.apiPb), "api");
941
+ const generationNumber = this.generationNumberSequence.next();
942
+ const apiDef = this.createClientApi(api);
943
+ let scopeId = "";
944
+ if (isNewApi) {
945
+ scopeId = await this.addApiToScope(api);
946
+ }
947
+ else {
948
+ const scopeDef = this.sourceTracker.getScopeDefinitionForPage(api.pageName);
949
+ scopeId = scopeDef?.id ?? "";
950
+ }
951
+ this.emit("apiUpdate", { api: apiDef, scopeId });
952
+ return { api: apiDef, scopeId, generationNumber };
953
+ };
954
+ handleDeleteApi = async (payload) => {
955
+ const logger = getLogger();
956
+ const { apis } = payload;
1099
957
  if (!this.rootDir) {
1100
958
  throw new Error("Root directory not set");
1101
959
  }
1102
- this.routes[route] = { file: this.getRelativeRoutePath(filePath) };
1103
- await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
1104
- }
1105
- async removeRoute(filePath) {
960
+ 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 });
973
+ }
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
+ }
989
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
990
+ await this.writeChanges(changes);
991
+ this.emit("apiDelete", {
992
+ apis: deletedApis.filter((api) => api !== undefined),
993
+ });
994
+ return { deletedApis };
995
+ };
996
+ handleRenameApi = async (payload) => {
997
+ const { oldName, newName, pageName } = payload;
1106
998
  if (!this.rootDir) {
1107
999
  throw new Error("Root directory not set");
1108
1000
  }
1109
- const relativeFilePath = this.getRelativeRoutePath(filePath);
1110
- this.routes = Object.fromEntries(Object.entries(this.routes).filter(([_, value]) => value.file !== relativeFilePath));
1111
- await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
1112
- }
1113
- async writeChanges(changes, callback) {
1114
- return Promise.all(changes.map(async ({ fileName, source, kind }) => {
1115
- await this.writeFile(fileName, source, kind);
1116
- return callback?.(fileName, source);
1117
- }));
1118
- }
1119
- getNodeForWidgetSourceId(id) {
1120
- return this.sourceTracker?.getElementToLocation(id);
1121
- }
1122
- getAstForWidgetSourceId(id) {
1123
- const filePath = this.sourceTracker?.getElementToFilePath(id);
1124
- if (!filePath) {
1125
- return null;
1001
+ const pagePath = getPageFolder(this.rootDir, pageName);
1002
+ const existingApiFolder = path.join(this.rootDir, "pages", pageName, "apis", oldName);
1003
+ const newApiFolder = path.join(this.rootDir, "pages", pageName, "apis", newName);
1004
+ const files = this.sourceTracker?.getCurrentFiles();
1005
+ const file = files?.[path.join(pagePath, "index.tsx")];
1006
+ if (!file || !file.ast) {
1007
+ throw new Error(`Page ${pageName} not found`);
1126
1008
  }
1127
- return this.sourceTracker?.getCurrentFiles()[filePath]?.ast;
1128
- }
1129
- renameIdentifierInApis = async ({ elementId, oldName, newName, parentBinding, }) => {
1130
- const apisInScope = structuredClone(this.getApisInScope(elementId));
1009
+ const apiFilePath = path.join(existingApiFolder, "api.yaml");
1010
+ const apiDef = this.apiFiles[apiFilePath];
1011
+ if (!apiDef) {
1012
+ throw new Error(`API ${oldName} not found`);
1013
+ }
1014
+ const otherPageAPIs = structuredClone(Object.keys(this.apiFiles)
1015
+ .filter((path) => path.startsWith(pagePath) && path !== apiFilePath)
1016
+ .map((path) => ({
1017
+ api: { apiPb: this.apiFiles[path]?.apiPb },
1018
+ filePath: path,
1019
+ }))
1020
+ .filter((api) => !!api.api));
1021
+ const newApiFolderExists = await fs.stat(newApiFolder).catch(() => false);
1022
+ if (newApiFolderExists) {
1023
+ throw new Error(`API ${newName} already exists`);
1024
+ }
1025
+ this.watcher?.unwatch(existingApiFolder);
1026
+ this.watcher?.unwatch(newApiFolder);
1027
+ apiDef.apiPb.metadata.name = newName;
1028
+ await fs.rename(existingApiFolder, newApiFolder);
1029
+ const scopeDef = this.sourceTracker?.getScopeDefinitionForPage(pageName);
1030
+ if (!scopeDef) {
1031
+ throw new Error(`Scope definition not found for API`);
1032
+ }
1033
+ this.sourceTracker?.renameEntity({
1034
+ oldName,
1035
+ newName,
1036
+ entityId: scopeDef.scopeNameToEntityId[oldName],
1037
+ });
1038
+ delete this.apiFiles[apiFilePath];
1039
+ this.writeFile(path.join(newApiFolder, "api.yaml"), yaml.stringify(apiDef.apiPb), "api");
1040
+ this.watcher?.add(existingApiFolder);
1041
+ this.watcher?.add(newApiFolder);
1042
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1043
+ await this.writeChanges(changes);
1044
+ this.emit("apiManualUpdate", {
1045
+ api: this.createClientApi(apiDef),
1046
+ oldName,
1047
+ pageName,
1048
+ scopeId: scopeDef?.id ?? "",
1049
+ }, true);
1131
1050
  await this.renameManager.renameEntityInApis({
1132
1051
  oldName,
1133
1052
  newName,
1134
- parentBinding,
1135
- apis: apisInScope,
1053
+ apis: otherPageAPIs,
1136
1054
  });
1137
1055
  // only save the APIs that have changed
1138
- await Promise.all(apisInScope.map(({ api, filePath }) => {
1056
+ await Promise.all(otherPageAPIs.map(async ({ api, filePath }) => {
1139
1057
  if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
1140
1058
  return Promise.resolve();
1141
1059
  }
1142
- return this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
1060
+ await this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
1061
+ this.emit("apiManualUpdate", {
1062
+ api: this.createClientApi(api),
1063
+ pageName,
1064
+ scopeId: scopeDef?.id ?? "",
1065
+ }, true);
1066
+ // TODO: Should I delete here?
1143
1067
  }));
1144
1068
  };
1145
- getApisInScope = (elementId) => {
1146
- const filePath = this.sourceTracker?.getElementToFilePath(elementId);
1147
- if (!filePath) {
1148
- return [];
1149
- }
1150
- const pagePath = path.dirname(filePath);
1151
- return Object.entries(this.apiFiles)
1152
- .filter(([filePath]) => filePath.includes(pagePath))
1153
- .map(([filePath, api]) => ({ api, filePath }));
1154
- };
1155
- getRelativeRoutePath(filePath) {
1156
- if (!this.rootDir) {
1157
- throw new Error("Root directory not set");
1069
+ async removeApiData(filePath) {
1070
+ const api = this.apiFiles[filePath];
1071
+ if (!api) {
1072
+ return;
1158
1073
  }
1159
- // no leading slash
1160
- return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
1074
+ delete this.apiFiles[filePath];
1075
+ this.sourceTracker?.deleteApi({
1076
+ pageName: api.pageName,
1077
+ apiName: api.apiPb.metadata.name,
1078
+ });
1079
+ const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
1080
+ 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
+ },
1090
+ });
1091
+ }
1092
+ getApiFiles() {
1093
+ return Object.keys(this.apiFiles).reduce((acc, key) => {
1094
+ if (!this.apiFiles[key]) {
1095
+ return acc;
1096
+ }
1097
+ acc[key] = {
1098
+ api: this.createClientApi(this.apiFiles[key]),
1099
+ scopeId: this.apiFiles[key].scopeId,
1100
+ };
1101
+ return acc;
1102
+ }, {});
1161
1103
  }
1162
1104
  // Utilities for converting server API format to Client API format
1163
1105
  // We internally save the API as the server does, but we return should always return it
@@ -1174,19 +1116,21 @@ export default registerPage(Page, { name: "${name}" });
1174
1116
  },
1175
1117
  };
1176
1118
  }
1177
- formatApisToClientApis(serverApis) {
1178
- return Object.keys(serverApis).reduce((acc, key) => {
1179
- if (!serverApis[key]) {
1180
- return acc;
1181
- }
1182
- acc[key] = {
1183
- api: this.createClientApi(serverApis[key]),
1184
- scopeId: serverApis[key].scopeId,
1185
- };
1186
- return acc;
1187
- }, {});
1188
- }
1189
- async processApiUpdates(filePath, fileType) {
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) {
1190
1134
  let yamlPath = filePath;
1191
1135
  if (fileType.type === "python-api-step" ||
1192
1136
  fileType.type === "js-api-step") {
@@ -1204,7 +1148,7 @@ export default registerPage(Page, { name: "${name}" });
1204
1148
  const parsedData = { apiPb: apiContent?.api };
1205
1149
  if (!(yamlPath in this.apiFiles &&
1206
1150
  isEqual(this.apiFiles[yamlPath]?.apiPb, parsedData.apiPb))) {
1207
- const { updatedApi, pageName, isNewApi } = this.updateApi({
1151
+ const { updatedApi, pageName, isNewApi } = this.updateInternalApiData({
1208
1152
  api: parsedData.apiPb,
1209
1153
  stepPathMap: apiContent.stepPathMap,
1210
1154
  }, yamlPath);
@@ -1215,7 +1159,7 @@ export default registerPage(Page, { name: "${name}" });
1215
1159
  return;
1216
1160
  }
1217
1161
  if (isNewApi) {
1218
- await this.createScopedApi(updatedApi);
1162
+ await this.addApiToScope(updatedApi);
1219
1163
  }
1220
1164
  this.emit("apiManualUpdate", {
1221
1165
  api: this.createClientApi(updatedApi),
@@ -1231,6 +1175,53 @@ export default registerPage(Page, { name: "${name}" });
1231
1175
  logger.error(`Error updating API: ${yamlPath}, error: ${error}`);
1232
1176
  }
1233
1177
  }
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
+ };
1234
1225
  }
1235
1226
  // Add new mock implementation
1236
1227
  export class MockFileSyncManager extends FileSystemManager {