@superblocksteam/vite-plugin-file-sync 2.0.6-next.4 → 2.0.6-next.40
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/app-interface/shell.d.ts +1 -0
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +8 -1
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/const.d.ts +2 -0
- package/dist/ai-service/const.d.ts.map +1 -1
- package/dist/ai-service/const.js +2 -0
- package/dist/ai-service/const.js.map +1 -1
- 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/ai-service/state-machine/clark-fsm.d.ts +1 -0
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +2 -0
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +40 -8
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/types.d.ts +1 -0
- package/dist/ai-service/types.d.ts.map +1 -1
- package/dist/ai-service/types.js.map +1 -1
- package/dist/binding-extraction/index.d.ts +2 -0
- package/dist/binding-extraction/index.d.ts.map +1 -0
- package/dist/binding-extraction/index.js +2 -0
- package/dist/binding-extraction/index.js.map +1 -0
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +12 -3
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/file-system-helpers.d.ts +4 -0
- package/dist/file-system-helpers.d.ts.map +1 -1
- package/dist/file-system-helpers.js +10 -0
- package/dist/file-system-helpers.js.map +1 -1
- package/dist/file-system-manager.d.ts +40 -35
- package/dist/file-system-manager.d.ts.map +1 -1
- package/dist/file-system-manager.js +534 -471
- package/dist/file-system-manager.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inject-index-vite-plugin.js.map +1 -1
- package/dist/injected-index.d.ts +2 -2
- package/dist/injected-index.d.ts.map +1 -1
- package/dist/injected-index.js.map +1 -1
- package/dist/lock-service/index.d.ts.map +1 -1
- package/dist/lock-service/index.js +10 -2
- package/dist/lock-service/index.js.map +1 -1
- package/dist/operations/operation-processor.d.ts +24 -0
- package/dist/operations/operation-processor.d.ts.map +1 -0
- package/dist/operations/operation-processor.js +80 -0
- package/dist/operations/operation-processor.js.map +1 -0
- package/dist/operations/types.d.ts +8 -0
- package/dist/operations/types.d.ts.map +1 -0
- package/dist/operations/types.js +2 -0
- package/dist/operations/types.js.map +1 -0
- package/dist/parsing/computed/to-code-computed.d.ts.map +1 -1
- package/dist/parsing/computed/to-code-computed.js +3 -3
- package/dist/parsing/computed/to-code-computed.js.map +1 -1
- package/dist/parsing/events/to-code-events.d.ts.map +1 -1
- package/dist/parsing/events/to-code-events.js +7 -0
- package/dist/parsing/events/to-code-events.js.map +1 -1
- package/dist/parsing/events/to-value-events.d.ts.map +1 -1
- package/dist/parsing/events/to-value-events.js +28 -0
- package/dist/parsing/events/to-value-events.js.map +1 -1
- package/dist/parsing/index.d.ts +3 -0
- package/dist/parsing/index.d.ts.map +1 -0
- package/dist/parsing/index.js +3 -0
- package/dist/parsing/index.js.map +1 -0
- package/dist/parsing/template/index.js +1 -1
- package/dist/parsing/template/index.js.map +1 -1
- package/dist/parsing/template/to-code-template.d.ts +2 -1
- package/dist/parsing/template/to-code-template.d.ts.map +1 -1
- package/dist/parsing/template/to-code-template.js +2 -2
- package/dist/parsing/template/to-code-template.js.map +1 -1
- package/dist/socket-manager.d.ts +2 -2
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/source-tracker.d.ts +20 -20
- package/dist/source-tracker.d.ts.map +1 -1
- package/dist/source-tracker.js +31 -13
- package/dist/source-tracker.js.map +1 -1
- package/package.json +14 -6
|
@@ -12,8 +12,9 @@ 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, 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
|
+
import { OperationProcessor } from "./operations/operation-processor.js";
|
|
17
18
|
import { doesElementHaveBinding } from "./parsing/bindings.js";
|
|
18
19
|
import { getSbElementId } from "./parsing/ids.js";
|
|
19
20
|
import { makeJSXAttribute } from "./parsing/jsx.js";
|
|
@@ -47,24 +48,34 @@ const SUPPORTED_FILETYPES = [
|
|
|
47
48
|
},
|
|
48
49
|
];
|
|
49
50
|
const APP_THEME_FILE_NAME = "appTheme.ts";
|
|
50
|
-
export class
|
|
51
|
+
export class FileSystemManager extends TracedEventEmitter {
|
|
51
52
|
rootDir;
|
|
52
53
|
tsFiles = {};
|
|
53
54
|
apiFiles = {};
|
|
54
55
|
sourceTracker;
|
|
55
56
|
fsOperationQueue;
|
|
57
|
+
operationProcessor;
|
|
56
58
|
generationNumberSequence;
|
|
57
59
|
routes = {};
|
|
58
60
|
watcher;
|
|
59
61
|
registeredComponentPaths = {};
|
|
60
62
|
renameManager = new RenameManager();
|
|
61
63
|
_tracer;
|
|
64
|
+
transactionNonce = Date.now();
|
|
65
|
+
pendingTransactions = new Set();
|
|
66
|
+
processedTransactions = [];
|
|
62
67
|
constructor(fsOperationQueue, generationNumberSequence, tracer) {
|
|
63
68
|
super(tracer, { captureRejections: true });
|
|
64
69
|
this.rootDir = "/";
|
|
65
70
|
this.fsOperationQueue = fsOperationQueue;
|
|
66
71
|
this.generationNumberSequence = generationNumberSequence;
|
|
67
72
|
this._tracer = tracer;
|
|
73
|
+
// intentionally a new queue here, we don't want to share the queue with the fsOperationQueue
|
|
74
|
+
this.operationProcessor = new OperationProcessor({
|
|
75
|
+
batchWindowMs: 50,
|
|
76
|
+
maxBatchSize: 100,
|
|
77
|
+
});
|
|
78
|
+
this.operationProcessor.disable();
|
|
68
79
|
applyErrorHandling(this, {
|
|
69
80
|
watch: { operation: "editing from code" },
|
|
70
81
|
handleCreatePage: { operation: "creating a page" },
|
|
@@ -110,27 +121,7 @@ export class FileSyncManager extends TracedEventEmitter {
|
|
|
110
121
|
}
|
|
111
122
|
return path.join(this.rootDir, "App.tsx");
|
|
112
123
|
}
|
|
113
|
-
|
|
114
|
-
if (!this.rootDir) {
|
|
115
|
-
throw new Error("Root directory not set");
|
|
116
|
-
}
|
|
117
|
-
const { api: apiContents, stepPathMap } = content;
|
|
118
|
-
const pageName = getPageName(path);
|
|
119
|
-
let scopeId = this.sourceTracker?.getScopeDefinitionForPage(pageName)?.id;
|
|
120
|
-
if (!scopeId) {
|
|
121
|
-
console.warn("Scope ID not found for API", apiContents.metadata.name);
|
|
122
|
-
scopeId = "";
|
|
123
|
-
}
|
|
124
|
-
const updatedApi = {
|
|
125
|
-
apiPb: yaml.parse(JSON.stringify(apiContents)),
|
|
126
|
-
pageName,
|
|
127
|
-
stepPathMap,
|
|
128
|
-
scopeId,
|
|
129
|
-
};
|
|
130
|
-
const isNewApi = !this.apiFiles[path];
|
|
131
|
-
this.apiFiles[path] = updatedApi;
|
|
132
|
-
return { updatedApi, pageName, isNewApi };
|
|
133
|
-
};
|
|
124
|
+
// MARK: core setup/init
|
|
134
125
|
async watch(watcher, rootPath) {
|
|
135
126
|
const logger = getLogger();
|
|
136
127
|
this.rootDir = rootPath;
|
|
@@ -216,178 +207,10 @@ export class FileSyncManager extends TracedEventEmitter {
|
|
|
216
207
|
return;
|
|
217
208
|
const { type, path } = file;
|
|
218
209
|
if (type === "api") {
|
|
219
|
-
this.
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
const handleFileChange = async (event, filePath) => {
|
|
223
|
-
logger.info(`File changed: ${filePath}, event: ${event}`);
|
|
224
|
-
switch (event) {
|
|
225
|
-
case "add": {
|
|
226
|
-
const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
|
|
227
|
-
if (!fileType || !filePath.startsWith(rootPath)) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const data = await readFile(filePath);
|
|
231
|
-
if (typeof data !== "string")
|
|
232
|
-
return;
|
|
233
|
-
// if we match pages/*/index.tsx, we want to emit an addPage event
|
|
234
|
-
if (/pages\/.*\/index\.tsx/.test(filePath)) {
|
|
235
|
-
const file = await readFile(filePath);
|
|
236
|
-
if (!file) {
|
|
237
|
-
logger.error(`Failed to read file: ${filePath}`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (!(filePath in this.tsFiles)) {
|
|
241
|
-
this.tsFiles[filePath] = file;
|
|
242
|
-
this.handleNonVisualChangeByDeletingIds(filePath, file);
|
|
243
|
-
this.emit("addPage", filePath);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
switch (fileType.type) {
|
|
247
|
-
case "api":
|
|
248
|
-
case "python-api-step":
|
|
249
|
-
case "js-api-step": {
|
|
250
|
-
await this.processApiUpdates(filePath, fileType);
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
case "change": {
|
|
257
|
-
// Special routes file handling
|
|
258
|
-
if (filePath === routePath) {
|
|
259
|
-
const data = await readFile(filePath);
|
|
260
|
-
try {
|
|
261
|
-
this.routes = JSON.parse(data);
|
|
262
|
-
this.emit("routesChanged", this.routes);
|
|
263
|
-
}
|
|
264
|
-
catch (e) {
|
|
265
|
-
logger.error("Error parsing routes file", getErrorMeta(e));
|
|
266
|
-
}
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
|
|
270
|
-
if (!fileType || !filePath.startsWith(rootPath)) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const data = await readFile(filePath);
|
|
274
|
-
if (typeof data !== "string")
|
|
275
|
-
return;
|
|
276
|
-
switch (fileType.type) {
|
|
277
|
-
case "tsx":
|
|
278
|
-
case "scope":
|
|
279
|
-
{
|
|
280
|
-
if (!(filePath in this.tsFiles && this.tsFiles[filePath] === data)) {
|
|
281
|
-
logger.info(`File changed: ${filePath} updating AST tracker`);
|
|
282
|
-
this.tsFiles[filePath] = data;
|
|
283
|
-
// only update the source tracker if the file is different
|
|
284
|
-
this.handleNonVisualChangeByDeletingIds(filePath, data);
|
|
285
|
-
this.emit("fileChanged", filePath, data, true);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
logger.info(`File unchanged from last tracked state: ${filePath}`);
|
|
289
|
-
this.emit("fileChanged", filePath, data, false);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
break;
|
|
293
|
-
case "api":
|
|
294
|
-
case "python-api-step":
|
|
295
|
-
case "js-api-step":
|
|
296
|
-
{
|
|
297
|
-
await this.processApiUpdates(filePath, fileType);
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
case "unlink": {
|
|
304
|
-
if (filePath in this.apiFiles) {
|
|
305
|
-
const api = this.apiFiles[filePath];
|
|
306
|
-
delete this.apiFiles[filePath];
|
|
307
|
-
if (!api || !this.sourceTracker)
|
|
308
|
-
break;
|
|
309
|
-
const scopeId = await this.sourceTracker.deleteApi({
|
|
310
|
-
pageName: api.pageName,
|
|
311
|
-
apiName: api.apiPb.metadata.name,
|
|
312
|
-
});
|
|
313
|
-
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
314
|
-
await this.writeChanges(changes);
|
|
315
|
-
this.emit("apiManualDelete", {
|
|
316
|
-
api: {
|
|
317
|
-
id: getClientApiId(api.apiPb.metadata.name, api.pageName),
|
|
318
|
-
apiName: api.apiPb.metadata.name,
|
|
319
|
-
// TODO(saksham): get pagename more defensively
|
|
320
|
-
pageName: getPageName(filePath),
|
|
321
|
-
scopeId,
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
if (filePath in this.tsFiles) {
|
|
326
|
-
delete this.tsFiles[filePath];
|
|
327
|
-
this.sourceTracker?.removeFile(filePath);
|
|
328
|
-
this.emit("deletePage", filePath);
|
|
329
|
-
}
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
watcher.on("all", async (event, filePath) => {
|
|
335
|
-
const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
|
|
336
|
-
switch (fileType?.type) {
|
|
337
|
-
case "tsx":
|
|
338
|
-
case "scope":
|
|
339
|
-
// these can be awaited to ensure sequential execution
|
|
340
|
-
this.fsOperationQueue.enqueue(async () => {
|
|
341
|
-
return await handleFileChange(event, filePath);
|
|
342
|
-
});
|
|
343
|
-
break;
|
|
344
|
-
default:
|
|
345
|
-
// some files including APIs cannot be awaited currently
|
|
346
|
-
this.fsOperationQueue.enqueue(async () => {
|
|
347
|
-
void handleFileChange(event, filePath);
|
|
348
|
-
});
|
|
210
|
+
this.updateInternalApiData(content, path);
|
|
349
211
|
}
|
|
350
212
|
});
|
|
351
|
-
|
|
352
|
-
getApiFiles() {
|
|
353
|
-
return this.formatApisToClientApis(this.apiFiles);
|
|
354
|
-
}
|
|
355
|
-
getTsFilePaths() {
|
|
356
|
-
return Object.keys(this.tsFiles);
|
|
357
|
-
}
|
|
358
|
-
getSourceTracker() {
|
|
359
|
-
return this.sourceTracker;
|
|
360
|
-
}
|
|
361
|
-
async writeFile(path, content, kind) {
|
|
362
|
-
if (kind === "ts") {
|
|
363
|
-
// happens eagerly regardless of error - possible desync
|
|
364
|
-
this.tsFiles[path] = content;
|
|
365
|
-
// TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
|
|
366
|
-
await this.fsOperationQueue.enqueue(async () => {
|
|
367
|
-
await fs.writeFile(path, content);
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
else if (kind === "api") {
|
|
371
|
-
const currentApiFile = this.apiFiles[path];
|
|
372
|
-
const apiPb = yaml.parse(content);
|
|
373
|
-
// Client APIs have id, but server APIs don't
|
|
374
|
-
if (apiPb.metadata.id) {
|
|
375
|
-
delete apiPb.metadata.id;
|
|
376
|
-
}
|
|
377
|
-
const stepPathMap = currentApiFile?.stepPathMap ?? {};
|
|
378
|
-
this.updateApi({ api: apiPb, stepPathMap }, path);
|
|
379
|
-
// TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
|
|
380
|
-
await this.fsOperationQueue.enqueue(async () => {
|
|
381
|
-
await writeApiFiles({ apiPb }, "api", path.split("/").slice(0, -1).join("/"), false, [], [], { extractLargeSourceFiles: true, minLinesForExtraction: 1 }, new Set(Object.keys(this.apiFiles)), stepPathMap);
|
|
382
|
-
// stepPathMap will be generated after the write when the file doesn't exist
|
|
383
|
-
if (this.apiFiles[path]) {
|
|
384
|
-
this.apiFiles[path].stepPathMap = stepPathMap;
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
readFile(path) {
|
|
390
|
-
return this.tsFiles[path];
|
|
213
|
+
watcher.on("all", this.handleFileChange);
|
|
391
214
|
}
|
|
392
215
|
initializeSourceTracker() {
|
|
393
216
|
this.sourceTracker = new SourceTracker(this._tracer);
|
|
@@ -496,6 +319,235 @@ export class FileSyncManager extends TracedEventEmitter {
|
|
|
496
319
|
return null;
|
|
497
320
|
}
|
|
498
321
|
}
|
|
322
|
+
enableOperationsQueue() {
|
|
323
|
+
this.operationProcessor.enable();
|
|
324
|
+
}
|
|
325
|
+
disableOperationsQueue() {
|
|
326
|
+
this.operationProcessor.disable();
|
|
327
|
+
}
|
|
328
|
+
async flushOperations() {
|
|
329
|
+
await this.operationProcessor.flush();
|
|
330
|
+
}
|
|
331
|
+
// MARK: file change handling
|
|
332
|
+
handleFileChange = async (event, filePath) => {
|
|
333
|
+
const logger = getLogger();
|
|
334
|
+
logger.info(`File changed: ${filePath}, event: ${event}`);
|
|
335
|
+
const rootPath = this.rootDir;
|
|
336
|
+
if (!rootPath) {
|
|
337
|
+
throw new Error("Root directory not set");
|
|
338
|
+
}
|
|
339
|
+
// Skip directory events
|
|
340
|
+
if (event === "addDir" || event === "unlinkDir") {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const routePath = path.join(rootPath, ROUTES_FILE);
|
|
344
|
+
const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
|
|
345
|
+
// Only handle files we care about and that are in our root path
|
|
346
|
+
if (!fileType || !filePath.startsWith(rootPath)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Queue the operation based on the event type
|
|
350
|
+
switch (event) {
|
|
351
|
+
case "add": {
|
|
352
|
+
const data = await readFile(filePath);
|
|
353
|
+
if (typeof data !== "string")
|
|
354
|
+
return;
|
|
355
|
+
const isPage = isPageFilePath(filePath);
|
|
356
|
+
if (isPage) {
|
|
357
|
+
void this.operationProcessor.addOperation({
|
|
358
|
+
metadata: {
|
|
359
|
+
filePath,
|
|
360
|
+
},
|
|
361
|
+
execute: async () => {
|
|
362
|
+
const file = await readFile(filePath);
|
|
363
|
+
if (!file) {
|
|
364
|
+
logger.error(`Failed to read file: ${filePath}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!(filePath in this.tsFiles)) {
|
|
368
|
+
this.tsFiles[filePath] = file;
|
|
369
|
+
this.handleNonVisualChangeByDeletingIds(filePath, file);
|
|
370
|
+
this.emit("addPage", filePath);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
void this.operationProcessor.addOperation({
|
|
377
|
+
metadata: {
|
|
378
|
+
filePath,
|
|
379
|
+
},
|
|
380
|
+
execute: async () => {
|
|
381
|
+
switch (fileType.type) {
|
|
382
|
+
case "api":
|
|
383
|
+
case "python-api-step":
|
|
384
|
+
case "js-api-step": {
|
|
385
|
+
await this.processApiFileUpdates(filePath, fileType);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case "change": {
|
|
395
|
+
if (filePath === routePath) {
|
|
396
|
+
void this.operationProcessor.addOperation({
|
|
397
|
+
metadata: {
|
|
398
|
+
filePath,
|
|
399
|
+
},
|
|
400
|
+
priority: true,
|
|
401
|
+
execute: async () => {
|
|
402
|
+
try {
|
|
403
|
+
const data = JSON.parse((await readFile(filePath)) ?? "{}");
|
|
404
|
+
if (!isEqual(this.routes, data)) {
|
|
405
|
+
this.routes = data;
|
|
406
|
+
this.emit("routesChanged", this.routes);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
logger.error("Error parsing routes file", getErrorMeta(e));
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
void this.operationProcessor.addOperation({
|
|
418
|
+
metadata: {
|
|
419
|
+
filePath,
|
|
420
|
+
},
|
|
421
|
+
execute: async () => {
|
|
422
|
+
const fileType = SUPPORTED_FILETYPES.find((f) => filePath.endsWith(f.extension));
|
|
423
|
+
if (!fileType || !filePath.startsWith(rootPath)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const data = await readFile(filePath);
|
|
427
|
+
if (typeof data !== "string")
|
|
428
|
+
return;
|
|
429
|
+
switch (fileType.type) {
|
|
430
|
+
case "tsx":
|
|
431
|
+
case "scope":
|
|
432
|
+
{
|
|
433
|
+
if (!(filePath in this.tsFiles &&
|
|
434
|
+
this.tsFiles[filePath] === data)) {
|
|
435
|
+
logger.info(`File changed: ${filePath} updating AST tracker`);
|
|
436
|
+
this.tsFiles[filePath] = data;
|
|
437
|
+
// only update the source tracker if the file is different
|
|
438
|
+
this.handleNonVisualChangeByDeletingIds(filePath, data);
|
|
439
|
+
this.emit("fileChanged", filePath, data, true);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
logger.info(`File unchanged from last tracked state: ${filePath}`);
|
|
443
|
+
this.emit("fileChanged", filePath, data, false);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
case "api":
|
|
448
|
+
case "python-api-step":
|
|
449
|
+
case "js-api-step":
|
|
450
|
+
{
|
|
451
|
+
await this.processApiFileUpdates(filePath, fileType);
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "unlink": {
|
|
461
|
+
if (filePath in this.tsFiles) {
|
|
462
|
+
void this.operationProcessor.addOperation({
|
|
463
|
+
metadata: {
|
|
464
|
+
filePath,
|
|
465
|
+
},
|
|
466
|
+
execute: async () => {
|
|
467
|
+
await this.deleteTsFile(filePath);
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
else if (filePath in this.apiFiles) {
|
|
472
|
+
void this.operationProcessor.addOperation({
|
|
473
|
+
metadata: {
|
|
474
|
+
filePath,
|
|
475
|
+
},
|
|
476
|
+
execute: async () => {
|
|
477
|
+
await this.removeApiData(filePath);
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
async deleteTsFile(filePath) {
|
|
486
|
+
delete this.tsFiles[filePath];
|
|
487
|
+
this.sourceTracker?.removeFile(filePath);
|
|
488
|
+
if (isPageFilePath(filePath)) {
|
|
489
|
+
this.emit("deletePage", filePath);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
getTsFilePaths() {
|
|
493
|
+
return Object.keys(this.tsFiles);
|
|
494
|
+
}
|
|
495
|
+
getSourceTracker() {
|
|
496
|
+
return this.sourceTracker;
|
|
497
|
+
}
|
|
498
|
+
// MARK: fs read/write
|
|
499
|
+
async writeFile(path, content, kind) {
|
|
500
|
+
if (kind === "ts") {
|
|
501
|
+
// happens eagerly regardless of error - possible desync
|
|
502
|
+
this.tsFiles[path] = content;
|
|
503
|
+
// TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
|
|
504
|
+
await this.fsOperationQueue.enqueue(async () => {
|
|
505
|
+
await fs.writeFile(path, content);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
else if (kind === "api") {
|
|
509
|
+
const currentApiFile = this.apiFiles[path];
|
|
510
|
+
const apiPb = yaml.parse(content);
|
|
511
|
+
// Client APIs have id, but server APIs don't
|
|
512
|
+
if (apiPb.metadata.id) {
|
|
513
|
+
delete apiPb.metadata.id;
|
|
514
|
+
}
|
|
515
|
+
const stepPathMap = currentApiFile?.stepPathMap ?? {};
|
|
516
|
+
this.updateInternalApiData({ api: apiPb, stepPathMap }, path);
|
|
517
|
+
// TODO(george): as an optimization, we could update the memory snapshot of the file that SyncService is holding
|
|
518
|
+
await this.fsOperationQueue.enqueue(async () => {
|
|
519
|
+
await writeApiFiles({ apiPb }, "api", path.split("/").slice(0, -1).join("/"), false, [], [], { extractLargeSourceFiles: true, minLinesForExtraction: 1 }, new Set(Object.keys(this.apiFiles)), stepPathMap);
|
|
520
|
+
// stepPathMap will be generated after the write when the file doesn't exist
|
|
521
|
+
if (this.apiFiles[path]) {
|
|
522
|
+
this.apiFiles[path].stepPathMap = stepPathMap;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
readFile(path) {
|
|
528
|
+
return this.tsFiles[path];
|
|
529
|
+
}
|
|
530
|
+
async addRoute(route, filePath) {
|
|
531
|
+
if (!this.rootDir) {
|
|
532
|
+
throw new Error("Root directory not set");
|
|
533
|
+
}
|
|
534
|
+
this.routes[route] = { file: this.getRelativeRoutePath(filePath) };
|
|
535
|
+
await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
|
|
536
|
+
}
|
|
537
|
+
async removeRoute(filePath) {
|
|
538
|
+
if (!this.rootDir) {
|
|
539
|
+
throw new Error("Root directory not set");
|
|
540
|
+
}
|
|
541
|
+
const relativeFilePath = this.getRelativeRoutePath(filePath);
|
|
542
|
+
this.routes = Object.fromEntries(Object.entries(this.routes).filter(([_, value]) => value.file !== relativeFilePath));
|
|
543
|
+
await this.writeFile(path.join(this.rootDir, ROUTES_FILE), JSON.stringify(this.routes, null, 2));
|
|
544
|
+
}
|
|
545
|
+
async writeChanges(changes, callback) {
|
|
546
|
+
return Promise.all(changes.map(async ({ fileName, source, kind }) => {
|
|
547
|
+
await this.writeFile(fileName, source, kind);
|
|
548
|
+
return callback?.(fileName, source);
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
499
551
|
getLocalBindingEntities(path) {
|
|
500
552
|
const logger = getLogger();
|
|
501
553
|
const files = this.sourceTracker?.getCurrentFiles();
|
|
@@ -523,8 +575,36 @@ export class FileSyncManager extends TracedEventEmitter {
|
|
|
523
575
|
});
|
|
524
576
|
return Array.from(localBindingEntities);
|
|
525
577
|
}
|
|
526
|
-
|
|
527
|
-
|
|
578
|
+
getPageRoots(filePath) {
|
|
579
|
+
const scopeFilePath = path.join(path.dirname(filePath), "index.tsx");
|
|
580
|
+
const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
|
|
581
|
+
if (!currentFile) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
return getPageRoots(filePath, currentFile);
|
|
585
|
+
}
|
|
586
|
+
getScope(filePath) {
|
|
587
|
+
// get the scope.ts file in the same directory as path
|
|
588
|
+
const scopeFilePath = path.join(path.dirname(filePath), SCOPE_FILE);
|
|
589
|
+
const currentFile = this.sourceTracker?.getCurrentFiles()[scopeFilePath];
|
|
590
|
+
if (!currentFile) {
|
|
591
|
+
console.log("File not found", scopeFilePath);
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const scope = getScope(scopeFilePath, currentFile);
|
|
595
|
+
return scope;
|
|
596
|
+
}
|
|
597
|
+
getRoutes() {
|
|
598
|
+
const routes = [];
|
|
599
|
+
for (const [path, { file }] of Object.entries(this.routes)) {
|
|
600
|
+
routes.push({
|
|
601
|
+
path,
|
|
602
|
+
component: file,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return routes;
|
|
606
|
+
}
|
|
607
|
+
// MARK: transaction handling
|
|
528
608
|
flushTransactions = () => {
|
|
529
609
|
// TODO do something more sophisticated than this.
|
|
530
610
|
this.processedTransactions = this.processedTransactions.slice(-20);
|
|
@@ -537,13 +617,13 @@ export class FileSyncManager extends TracedEventEmitter {
|
|
|
537
617
|
}
|
|
538
618
|
this.pendingTransactions.add(transactionId);
|
|
539
619
|
};
|
|
540
|
-
transactionNonce = Date.now();
|
|
541
620
|
// Until we have a sophisticated way to track ALL operations, including non-UI initiated ops, we will use a nonce
|
|
542
621
|
// TODO https://github.com/superblocksteam/superblocks/pull/11788
|
|
543
622
|
getProcessedTransactionsWithNonce() {
|
|
544
623
|
const nonce = `t-${this.transactionNonce++}`;
|
|
545
624
|
return this.processedTransactions.concat(nonce);
|
|
546
625
|
}
|
|
626
|
+
// MARK: editor operations
|
|
547
627
|
handleCreatePage = async (payload) => {
|
|
548
628
|
const { name } = payload;
|
|
549
629
|
if (!this.rootDir) {
|
|
@@ -560,7 +640,9 @@ function Page() {
|
|
|
560
640
|
|
|
561
641
|
export default registerPage(Page, { name: "${name}" });
|
|
562
642
|
`;
|
|
563
|
-
await
|
|
643
|
+
await this.fsOperationQueue.enqueue(async () => {
|
|
644
|
+
await fs.mkdir(pagePath, { recursive: true });
|
|
645
|
+
});
|
|
564
646
|
await this.writeFile(pageIndexPath, pageContent, "ts");
|
|
565
647
|
await this.handleNonVisualChangeByDeletingIds(pageIndexPath, pageContent);
|
|
566
648
|
this.emit("addPage", pageIndexPath);
|
|
@@ -568,11 +650,11 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
568
650
|
handleReparent = async (payload, writeFile = true) => {
|
|
569
651
|
const { from, to, changedProps, transaction } = payload;
|
|
570
652
|
this.trackTransaction(transaction?.id);
|
|
571
|
-
|
|
653
|
+
this.sourceTracker?.setProperties({
|
|
572
654
|
source: from.source,
|
|
573
655
|
changes: changedProps ?? {},
|
|
574
656
|
});
|
|
575
|
-
|
|
657
|
+
this.sourceTracker?.moveElement({
|
|
576
658
|
from,
|
|
577
659
|
to,
|
|
578
660
|
});
|
|
@@ -586,7 +668,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
586
668
|
};
|
|
587
669
|
handleCreateComponent = async (payload, writeFile = true) => {
|
|
588
670
|
this.trackTransaction(payload.transaction?.id);
|
|
589
|
-
const sourceId =
|
|
671
|
+
const sourceId = this.sourceTracker?.addElement(payload);
|
|
590
672
|
if (writeFile) {
|
|
591
673
|
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
592
674
|
await this.writeChanges(changes ?? [], (fileName) => {
|
|
@@ -600,7 +682,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
600
682
|
const { elements, transaction } = payload;
|
|
601
683
|
this.trackTransaction(transaction?.id);
|
|
602
684
|
for (const element of elements) {
|
|
603
|
-
|
|
685
|
+
this.sourceTracker?.deleteElement({
|
|
604
686
|
source: element.source,
|
|
605
687
|
scopeName: element.scopeName,
|
|
606
688
|
});
|
|
@@ -616,7 +698,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
616
698
|
handleSetProperty = async (payload, writeFile = true) => {
|
|
617
699
|
const { element: { source }, property, value, transaction, } = payload;
|
|
618
700
|
this.trackTransaction(transaction?.id);
|
|
619
|
-
|
|
701
|
+
this.sourceTracker?.setProperty({
|
|
620
702
|
source,
|
|
621
703
|
property,
|
|
622
704
|
info: value,
|
|
@@ -631,7 +713,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
631
713
|
handleSetProperties = async (payload, writeFile = true) => {
|
|
632
714
|
const { element: { source }, properties, transaction, } = payload;
|
|
633
715
|
this.trackTransaction(transaction?.id);
|
|
634
|
-
|
|
716
|
+
this.sourceTracker?.setProperties({
|
|
635
717
|
source,
|
|
636
718
|
changes: properties,
|
|
637
719
|
});
|
|
@@ -687,184 +769,9 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
687
769
|
this.flushTransactions();
|
|
688
770
|
return returnValues;
|
|
689
771
|
};
|
|
690
|
-
|
|
691
|
-
const { api } = payload;
|
|
692
|
-
if (!this.sourceTracker) {
|
|
693
|
-
throw new Error("Source tracker not initialized");
|
|
694
|
-
}
|
|
695
|
-
if (!this.rootDir) {
|
|
696
|
-
throw new Error("Root directory not set");
|
|
697
|
-
}
|
|
698
|
-
if (!api.pageName) {
|
|
699
|
-
throw new Error("API page name is not set");
|
|
700
|
-
}
|
|
701
|
-
const apiName = api.apiPb.metadata.name;
|
|
702
|
-
if (!apiName) {
|
|
703
|
-
throw new Error("API name is not set");
|
|
704
|
-
}
|
|
705
|
-
const apiDir = path.join(this.rootDir, "pages", api.pageName, "apis", apiName);
|
|
706
|
-
const apiPath = path.join(apiDir, "api.yaml");
|
|
707
|
-
const isNewApi = !this.getApiFiles()[apiPath];
|
|
708
|
-
try {
|
|
709
|
-
const stats = await fs.stat(apiDir);
|
|
710
|
-
if (!stats.isDirectory()) {
|
|
711
|
-
await fs.mkdir(apiDir, { recursive: true });
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
await fs.mkdir(apiDir, { recursive: true });
|
|
716
|
-
}
|
|
717
|
-
await this.writeFile(apiPath, yaml.stringify(api.apiPb), "api");
|
|
718
|
-
const generationNumber = this.generationNumberSequence.next();
|
|
719
|
-
const apiDef = this.createClientApi(api);
|
|
720
|
-
let scopeId = "";
|
|
721
|
-
if (isNewApi) {
|
|
722
|
-
scopeId = await this.createScopedApi(api);
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
const scopeDef = this.sourceTracker.getScopeDefinitionForPage(api.pageName);
|
|
726
|
-
scopeId = scopeDef?.id ?? "";
|
|
727
|
-
}
|
|
728
|
-
this.emit("apiUpdate", { api: apiDef, scopeId });
|
|
729
|
-
return { api: apiDef, scopeId, generationNumber };
|
|
730
|
-
};
|
|
731
|
-
createScopedApi = async (api) => {
|
|
732
|
-
if (!this.sourceTracker) {
|
|
733
|
-
throw new Error("Source tracker not initialized");
|
|
734
|
-
}
|
|
735
|
-
// We want to add the API entity to our scope, but we do not want to emit entity events, because
|
|
736
|
-
// the API update event handles this particular side effect.
|
|
737
|
-
const scopeId = await this.sourceTracker.addApi({
|
|
738
|
-
pageName: api.pageName,
|
|
739
|
-
apiName: api.apiPb.metadata.name,
|
|
740
|
-
});
|
|
741
|
-
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
742
|
-
await this.writeChanges(changes);
|
|
743
|
-
return scopeId;
|
|
744
|
-
};
|
|
745
|
-
handleDeleteApi = async (payload) => {
|
|
746
|
-
const logger = getLogger();
|
|
747
|
-
const { apis } = payload;
|
|
748
|
-
if (!this.rootDir) {
|
|
749
|
-
throw new Error("Root directory not set");
|
|
750
|
-
}
|
|
751
|
-
const rootDir = this.rootDir;
|
|
752
|
-
const executeDeleteApis = Promise.all(apis.map(({ apiName, pageName }) => {
|
|
753
|
-
return new Promise(
|
|
754
|
-
// eslint-disable-next-line no-async-promise-executor
|
|
755
|
-
async (resolve) => {
|
|
756
|
-
const apiFilePath = path.join(rootDir, "pages", pageName, "apis", apiName, "api.yaml");
|
|
757
|
-
const api = this.apiFiles[apiFilePath];
|
|
758
|
-
if (!api || !this.sourceTracker) {
|
|
759
|
-
return resolve(undefined);
|
|
760
|
-
}
|
|
761
|
-
const apiDir = path.join(rootDir, "pages", pageName, "apis", apiName);
|
|
762
|
-
try {
|
|
763
|
-
const stats = await fs.stat(apiDir);
|
|
764
|
-
if (stats.isDirectory()) {
|
|
765
|
-
await fs.rmdir(apiDir, { recursive: true });
|
|
766
|
-
}
|
|
767
|
-
delete this.apiFiles[apiFilePath];
|
|
768
|
-
const scopeId = await this.sourceTracker.deleteApi({
|
|
769
|
-
pageName,
|
|
770
|
-
apiName,
|
|
771
|
-
});
|
|
772
|
-
resolve({
|
|
773
|
-
apiName,
|
|
774
|
-
pageName,
|
|
775
|
-
scopeId,
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
catch (e) {
|
|
779
|
-
logger.warn(`Could not delete api ${apiFilePath} ${JSON.stringify(e)}`);
|
|
780
|
-
resolve(undefined);
|
|
781
|
-
}
|
|
782
|
-
resolve(undefined);
|
|
783
|
-
});
|
|
784
|
-
}));
|
|
785
|
-
const deletedApis = (await executeDeleteApis).filter((api) => api !== undefined);
|
|
786
|
-
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
787
|
-
await this.writeChanges(changes);
|
|
788
|
-
this.emit("apiDelete", {
|
|
789
|
-
apis: deletedApis.filter((api) => api !== undefined),
|
|
790
|
-
});
|
|
791
|
-
return { deletedApis };
|
|
792
|
-
};
|
|
793
|
-
handleRenameApi = async (payload) => {
|
|
794
|
-
const { oldName, newName, pageName } = payload;
|
|
795
|
-
if (!this.rootDir) {
|
|
796
|
-
throw new Error("Root directory not set");
|
|
797
|
-
}
|
|
798
|
-
const pagePath = getPageFolder(this.rootDir, pageName);
|
|
799
|
-
const existingApiFolder = path.join(this.rootDir, "pages", pageName, "apis", oldName);
|
|
800
|
-
const newApiFolder = path.join(this.rootDir, "pages", pageName, "apis", newName);
|
|
801
|
-
const files = this.sourceTracker?.getCurrentFiles();
|
|
802
|
-
const file = files?.[path.join(pagePath, "index.tsx")];
|
|
803
|
-
if (!file || !file.ast) {
|
|
804
|
-
throw new Error(`Page ${pageName} not found`);
|
|
805
|
-
}
|
|
806
|
-
const apiFilePath = path.join(existingApiFolder, "api.yaml");
|
|
807
|
-
const apiDef = this.apiFiles[apiFilePath];
|
|
808
|
-
if (!apiDef) {
|
|
809
|
-
throw new Error(`API ${oldName} not found`);
|
|
810
|
-
}
|
|
811
|
-
const otherPageAPIs = structuredClone(Object.keys(this.apiFiles)
|
|
812
|
-
.filter((path) => path.startsWith(pagePath) && path !== apiFilePath)
|
|
813
|
-
.map((path) => ({
|
|
814
|
-
api: { apiPb: this.apiFiles[path]?.apiPb },
|
|
815
|
-
filePath: path,
|
|
816
|
-
}))
|
|
817
|
-
.filter((api) => !!api.api));
|
|
818
|
-
const newApiFolderExists = await fs.stat(newApiFolder).catch(() => false);
|
|
819
|
-
if (newApiFolderExists) {
|
|
820
|
-
throw new Error(`API ${newName} already exists`);
|
|
821
|
-
}
|
|
822
|
-
this.watcher?.unwatch(existingApiFolder);
|
|
823
|
-
this.watcher?.unwatch(newApiFolder);
|
|
824
|
-
apiDef.apiPb.metadata.name = newName;
|
|
825
|
-
await fs.rename(existingApiFolder, newApiFolder);
|
|
826
|
-
const scopeDef = this.sourceTracker?.getScopeDefinitionForPage(pageName);
|
|
827
|
-
if (!scopeDef) {
|
|
828
|
-
throw new Error(`Scope definition not found for API`);
|
|
829
|
-
}
|
|
830
|
-
this.sourceTracker?.renameEntity({
|
|
831
|
-
oldName,
|
|
832
|
-
newName,
|
|
833
|
-
entityId: scopeDef.scopeNameToEntityId[oldName],
|
|
834
|
-
});
|
|
835
|
-
delete this.apiFiles[apiFilePath];
|
|
836
|
-
this.writeFile(path.join(newApiFolder, "api.yaml"), yaml.stringify(apiDef.apiPb), "api");
|
|
837
|
-
this.watcher?.add(existingApiFolder);
|
|
838
|
-
this.watcher?.add(newApiFolder);
|
|
839
|
-
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
840
|
-
await this.writeChanges(changes);
|
|
841
|
-
this.emit("apiManualUpdate", {
|
|
842
|
-
api: this.createClientApi(apiDef),
|
|
843
|
-
oldName,
|
|
844
|
-
pageName,
|
|
845
|
-
scopeId: scopeDef?.id ?? "",
|
|
846
|
-
}, true);
|
|
847
|
-
await this.renameManager.renameEntityInApis({
|
|
848
|
-
oldName,
|
|
849
|
-
newName,
|
|
850
|
-
apis: otherPageAPIs,
|
|
851
|
-
});
|
|
852
|
-
// only save the APIs that have changed
|
|
853
|
-
await Promise.all(otherPageAPIs.map(async ({ api, filePath }) => {
|
|
854
|
-
if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
|
|
855
|
-
return Promise.resolve();
|
|
856
|
-
}
|
|
857
|
-
await this.writeFile(filePath, yaml.stringify(api?.apiPb), "api");
|
|
858
|
-
this.emit("apiManualUpdate", {
|
|
859
|
-
api: this.createClientApi(api),
|
|
860
|
-
pageName,
|
|
861
|
-
scopeId: scopeDef?.id ?? "",
|
|
862
|
-
}, true);
|
|
863
|
-
// TODO: Should I delete here?
|
|
864
|
-
}));
|
|
865
|
-
};
|
|
772
|
+
// MARK: entity operations
|
|
866
773
|
handleAddEntity = async (payload) => {
|
|
867
|
-
|
|
774
|
+
this.sourceTracker?.addEntity({
|
|
868
775
|
scopeId: payload.scopeId,
|
|
869
776
|
entity: {
|
|
870
777
|
type: payload.type,
|
|
@@ -878,7 +785,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
878
785
|
});
|
|
879
786
|
};
|
|
880
787
|
handleUpdateEntity = async (payload) => {
|
|
881
|
-
|
|
788
|
+
this.sourceTracker?.updateEntity({
|
|
882
789
|
entityId: payload.entityId,
|
|
883
790
|
updates: payload.updates,
|
|
884
791
|
});
|
|
@@ -888,7 +795,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
888
795
|
});
|
|
889
796
|
};
|
|
890
797
|
handleDeleteEntity = async (payload) => {
|
|
891
|
-
const deletedEntityName =
|
|
798
|
+
const deletedEntityName = this.sourceTracker?.deleteEntity({
|
|
892
799
|
entityId: payload.entityId,
|
|
893
800
|
});
|
|
894
801
|
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
@@ -904,13 +811,14 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
904
811
|
throw new Error("Root directory not set");
|
|
905
812
|
}
|
|
906
813
|
const filePath = path.join(this.rootDir, APP_THEME_FILE_NAME);
|
|
907
|
-
|
|
814
|
+
this.sourceTracker?.updateTheme({
|
|
908
815
|
themeFilePath: filePath,
|
|
909
816
|
theme,
|
|
910
817
|
});
|
|
911
818
|
const changes = (await this.sourceTracker?.getAndFlushChanges()) ?? [];
|
|
912
819
|
await this.writeChanges(changes);
|
|
913
820
|
};
|
|
821
|
+
// MARK: rename operations
|
|
914
822
|
handleRenameElement = async (payload) => {
|
|
915
823
|
if (payload.kind === "component") {
|
|
916
824
|
return this.handleRenameComponent(payload);
|
|
@@ -942,7 +850,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
942
850
|
};
|
|
943
851
|
handleRenameEntity = async (payload) => {
|
|
944
852
|
const { elementId, newName, oldName, scopeName } = payload;
|
|
945
|
-
|
|
853
|
+
this.sourceTracker?.renameEntity({
|
|
946
854
|
entityId: elementId,
|
|
947
855
|
oldName,
|
|
948
856
|
newName,
|
|
@@ -994,98 +902,204 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
994
902
|
this.watcher?.add(newPageFolder);
|
|
995
903
|
this.watcher?.add(oldPageFolder);
|
|
996
904
|
};
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (!currentFile) {
|
|
1001
|
-
return null;
|
|
905
|
+
getRelativeRoutePath(filePath) {
|
|
906
|
+
if (!this.rootDir) {
|
|
907
|
+
throw new Error("Root directory not set");
|
|
1002
908
|
}
|
|
1003
|
-
|
|
909
|
+
// no leading slash
|
|
910
|
+
return path.relative(path.join(this.rootDir, PAGES_DIRECTORY), filePath);
|
|
1004
911
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
console.log("File not found", scopeFilePath);
|
|
1011
|
-
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");
|
|
1012
917
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
getRoutes() {
|
|
1017
|
-
const routes = [];
|
|
1018
|
-
for (const [path, { file }] of Object.entries(this.routes)) {
|
|
1019
|
-
routes.push({
|
|
1020
|
-
path,
|
|
1021
|
-
component: file,
|
|
1022
|
-
});
|
|
918
|
+
if (!this.rootDir) {
|
|
919
|
+
throw new Error("Root directory not set");
|
|
1023
920
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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;
|
|
1027
957
|
if (!this.rootDir) {
|
|
1028
958
|
throw new Error("Root directory not set");
|
|
1029
959
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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;
|
|
1034
998
|
if (!this.rootDir) {
|
|
1035
999
|
throw new Error("Root directory not set");
|
|
1036
1000
|
}
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
return callback?.(fileName, source);
|
|
1045
|
-
}));
|
|
1046
|
-
}
|
|
1047
|
-
getNodeForWidgetSourceId(id) {
|
|
1048
|
-
return this.sourceTracker?.getElementToLocation(id);
|
|
1049
|
-
}
|
|
1050
|
-
getAstForWidgetSourceId(id) {
|
|
1051
|
-
const filePath = this.sourceTracker?.getElementToFilePath(id);
|
|
1052
|
-
if (!filePath) {
|
|
1053
|
-
return null;
|
|
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`);
|
|
1054
1008
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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);
|
|
1059
1050
|
await this.renameManager.renameEntityInApis({
|
|
1060
1051
|
oldName,
|
|
1061
1052
|
newName,
|
|
1062
|
-
|
|
1063
|
-
apis: apisInScope,
|
|
1053
|
+
apis: otherPageAPIs,
|
|
1064
1054
|
});
|
|
1065
1055
|
// only save the APIs that have changed
|
|
1066
|
-
await Promise.all(
|
|
1056
|
+
await Promise.all(otherPageAPIs.map(async ({ api, filePath }) => {
|
|
1067
1057
|
if (isEqual(api?.apiPb, this.apiFiles[filePath]?.apiPb)) {
|
|
1068
1058
|
return Promise.resolve();
|
|
1069
1059
|
}
|
|
1070
|
-
|
|
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?
|
|
1071
1067
|
}));
|
|
1072
1068
|
};
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
if (!
|
|
1076
|
-
return
|
|
1077
|
-
}
|
|
1078
|
-
const pagePath = path.dirname(filePath);
|
|
1079
|
-
return Object.entries(this.apiFiles)
|
|
1080
|
-
.filter(([filePath]) => filePath.includes(pagePath))
|
|
1081
|
-
.map(([filePath, api]) => ({ api, filePath }));
|
|
1082
|
-
};
|
|
1083
|
-
getRelativeRoutePath(filePath) {
|
|
1084
|
-
if (!this.rootDir) {
|
|
1085
|
-
throw new Error("Root directory not set");
|
|
1069
|
+
async removeApiData(filePath) {
|
|
1070
|
+
const api = this.apiFiles[filePath];
|
|
1071
|
+
if (!api) {
|
|
1072
|
+
return;
|
|
1086
1073
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
+
}, {});
|
|
1089
1103
|
}
|
|
1090
1104
|
// Utilities for converting server API format to Client API format
|
|
1091
1105
|
// We internally save the API as the server does, but we return should always return it
|
|
@@ -1102,19 +1116,21 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
1102
1116
|
},
|
|
1103
1117
|
};
|
|
1104
1118
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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) {
|
|
1118
1134
|
let yamlPath = filePath;
|
|
1119
1135
|
if (fileType.type === "python-api-step" ||
|
|
1120
1136
|
fileType.type === "js-api-step") {
|
|
@@ -1132,7 +1148,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
1132
1148
|
const parsedData = { apiPb: apiContent?.api };
|
|
1133
1149
|
if (!(yamlPath in this.apiFiles &&
|
|
1134
1150
|
isEqual(this.apiFiles[yamlPath]?.apiPb, parsedData.apiPb))) {
|
|
1135
|
-
const { updatedApi, pageName, isNewApi } = this.
|
|
1151
|
+
const { updatedApi, pageName, isNewApi } = this.updateInternalApiData({
|
|
1136
1152
|
api: parsedData.apiPb,
|
|
1137
1153
|
stepPathMap: apiContent.stepPathMap,
|
|
1138
1154
|
}, yamlPath);
|
|
@@ -1143,7 +1159,7 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
1143
1159
|
return;
|
|
1144
1160
|
}
|
|
1145
1161
|
if (isNewApi) {
|
|
1146
|
-
await this.
|
|
1162
|
+
await this.addApiToScope(updatedApi);
|
|
1147
1163
|
}
|
|
1148
1164
|
this.emit("apiManualUpdate", {
|
|
1149
1165
|
api: this.createClientApi(updatedApi),
|
|
@@ -1159,9 +1175,56 @@ export default registerPage(Page, { name: "${name}" });
|
|
|
1159
1175
|
logger.error(`Error updating API: ${yamlPath}, error: ${error}`);
|
|
1160
1176
|
}
|
|
1161
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
|
+
};
|
|
1162
1225
|
}
|
|
1163
1226
|
// Add new mock implementation
|
|
1164
|
-
export class MockFileSyncManager extends
|
|
1227
|
+
export class MockFileSyncManager extends FileSystemManager {
|
|
1165
1228
|
tsFiles = {};
|
|
1166
1229
|
apiFiles = {};
|
|
1167
1230
|
async watch(_watcher, _path) {
|
|
@@ -1237,7 +1300,7 @@ async function readFiles(dir) {
|
|
|
1237
1300
|
const getMergedApiContent = async (path) => {
|
|
1238
1301
|
return readAppApiYamlFile(path.split("/").slice(0, -1).join("/"));
|
|
1239
1302
|
};
|
|
1240
|
-
const getPageName = (path) => {
|
|
1303
|
+
export const getPageName = (path) => {
|
|
1241
1304
|
const parts = path.split("/");
|
|
1242
1305
|
const pagesIndex = parts.findIndex((part) => part === "pages");
|
|
1243
1306
|
if (pagesIndex !== -1 && parts[pagesIndex + 1]) {
|