@superblocksteam/vite-plugin-file-sync 2.0.6-next.23 → 2.0.6-next.25
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.
- package/dist/ai-service/prompts/generated/library-components/SbButtonPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbCheckboxPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbColumnPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbContainerPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbDatePickerPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbDropdownPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbIconPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbImagePropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbInputPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbModalPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbPagePropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbSectionPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbSlideoutPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbSwitchPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbTablePropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-components/SbTextPropsDocs.js +1 -1
- package/dist/ai-service/prompts/generated/library-typedefs/Dim.js +1 -1
- package/dist/ai-service/prompts/generated/library-typedefs/SbEventFlow.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/full-examples.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-api.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-components-rules.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-custom-components.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-data-filtering.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-event-flow.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-forms.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-layouts.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-page.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-rbac.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-routes.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-state.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/superblocks-theming.js +1 -1
- package/dist/ai-service/prompts/generated/subprompts/system.js +1 -1
- package/dist/file-system-helpers.d.ts +2 -0
- package/dist/file-system-helpers.d.ts.map +1 -1
- package/dist/file-system-helpers.js +4 -0
- package/dist/file-system-helpers.js.map +1 -1
- package/dist/file-system-manager.d.ts +30 -34
- package/dist/file-system-manager.d.ts.map +1 -1
- package/dist/file-system-manager.js +425 -434
- package/dist/file-system-manager.js.map +1 -1
- package/dist/parsing/computed/to-code-computed.d.ts.map +1 -1
- package/dist/parsing/computed/to-code-computed.js +6 -1
- package/dist/parsing/computed/to-code-computed.js.map +1 -1
- 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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
471
|
-
if (!this.
|
|
472
|
-
throw new Error("
|
|
537
|
+
async removeRoute(filePath) {
|
|
538
|
+
if (!this.rootDir) {
|
|
539
|
+
throw new Error("Root directory not set");
|
|
473
540
|
}
|
|
474
|
-
|
|
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
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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[
|
|
558
|
+
const ast = files[path].ast;
|
|
486
559
|
if (!ast) {
|
|
487
|
-
|
|
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
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
532
|
-
if (
|
|
533
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (!currentFile) {
|
|
1073
|
-
return null;
|
|
905
|
+
getRelativeRoutePath(filePath) {
|
|
906
|
+
if (!this.rootDir) {
|
|
907
|
+
throw new Error("Root directory not set");
|
|
1074
908
|
}
|
|
1075
|
-
|
|
909
|
+
// no leading slash
|
|
910
|
+
return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
|
|
1076
911
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1135
|
-
apis: apisInScope,
|
|
1053
|
+
apis: otherPageAPIs,
|
|
1136
1054
|
});
|
|
1137
1055
|
// only save the APIs that have changed
|
|
1138
|
-
await Promise.all(
|
|
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
|
-
|
|
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
|
-
|
|
1146
|
-
const
|
|
1147
|
-
if (!
|
|
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
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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.
|
|
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.
|
|
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 {
|