@superblocksteam/cli 1.9.3 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE.txt +87 -0
  2. package/README.md +6 -6
  3. package/assets/custom-components/setup/package.json +1 -1
  4. package/assets/custom-components/setup/tsconfig.json +0 -1
  5. package/assets/injectedReactShim17.jsx +15 -0
  6. package/assets/injectedReactShim18.jsx +16 -0
  7. package/assets/injectedReactShimShared.jsx +140 -0
  8. package/bin/dev +5 -7
  9. package/bin/run +1 -3
  10. package/dist/appendHotReloadEventPlugin.d.mts +2 -0
  11. package/dist/appendHotReloadEventPlugin.mjs +43 -0
  12. package/dist/commands/commits.d.mts +18 -0
  13. package/dist/commands/{commits.js → commits.mjs} +59 -67
  14. package/dist/commands/components/{create.d.ts → create.d.mts} +2 -2
  15. package/dist/commands/components/{create.js → create.mjs} +84 -93
  16. package/dist/commands/components/{register.d.ts → register.d.mts} +1 -1
  17. package/dist/commands/components/register.mjs +12 -0
  18. package/dist/commands/components/{upload.d.ts → upload.d.mts} +2 -2
  19. package/dist/commands/components/{upload.js → upload.mjs} +39 -43
  20. package/dist/commands/components/{watch.d.ts → watch.d.mts} +1 -1
  21. package/dist/commands/components/{watch.js → watch.mjs} +29 -36
  22. package/dist/commands/config/{set.d.ts → set.d.mts} +2 -2
  23. package/dist/commands/config/{set.js → set.mjs} +28 -32
  24. package/dist/commands/{init.d.ts → init.d.mts} +4 -4
  25. package/dist/commands/{init.js → init.mjs} +63 -65
  26. package/dist/commands/{login.d.ts → login.d.mts} +1 -1
  27. package/dist/commands/login.mjs +55 -0
  28. package/dist/commands/{migrate.d.ts → migrate.d.mts} +1 -1
  29. package/dist/commands/{migrate.js → migrate.mjs} +38 -46
  30. package/dist/commands/pull.d.mts +17 -0
  31. package/dist/commands/{pull.js → pull.mjs} +74 -80
  32. package/dist/commands/push.d.mts +15 -0
  33. package/dist/commands/{push.js → push.mjs} +81 -90
  34. package/dist/commands/{rm.d.ts → rm.d.mts} +2 -2
  35. package/dist/commands/{rm.js → rm.mjs} +34 -40
  36. package/dist/common/{authenticated-command.js → authenticated-command.mjs} +65 -75
  37. package/dist/common/defaults/{create-component-defaults.js → create-component-defaults.mjs} +2 -7
  38. package/dist/common/{version-control.d.ts → version-control.d.mts} +13 -6
  39. package/dist/common/version-control.mjs +1064 -0
  40. package/dist/index.js +1 -5
  41. package/dist/productionCssPlugin.d.mts +2 -0
  42. package/dist/productionCssPlugin.mjs +50 -0
  43. package/dist/reactShimPlugin.d.mts +2 -0
  44. package/dist/reactShimPlugin.mjs +127 -0
  45. package/dist/util/migrationWarningsForApplications.mjs +47 -0
  46. package/dist/util/{migrationsForDotfiles.js → migrationsForDotfiles.mjs} +10 -17
  47. package/oclif.manifest.json +274 -161
  48. package/package.json +45 -45
  49. package/dist/commands/commits.d.ts +0 -18
  50. package/dist/commands/components/register.js +0 -15
  51. package/dist/commands/login.js +0 -61
  52. package/dist/commands/pull.d.ts +0 -17
  53. package/dist/commands/push.d.ts +0 -15
  54. package/dist/common/version-control.js +0 -716
  55. package/dist/util/migrationWarningsForApplications.js +0 -52
  56. /package/dist/common/{authenticated-command.d.ts → authenticated-command.d.mts} +0 -0
  57. /package/dist/common/defaults/{create-component-defaults.d.ts → create-component-defaults.d.mts} +0 -0
  58. /package/dist/util/{migrationWarningsForApplications.d.ts → migrationWarningsForApplications.d.mts} +0 -0
  59. /package/dist/util/{migrationsForDotfiles.d.ts → migrationsForDotfiles.d.mts} +0 -0
@@ -0,0 +1,1064 @@
1
+ import * as https from "https";
2
+ import path from "node:path";
3
+ import { dirname } from "path";
4
+ import { DEFAULT_LINES_FOR_LARGE_STEPS, } from "@superblocksteam/sdk";
5
+ import { RESOURCE_CONFIG_PATH, SUPERBLOCKS_HOME_FOLDER_NAME, getSuperblocksApplicationConfigIfExists, getSuperblocksApplicationConfigJson, getSuperblocksBackendConfigIfExists, } from "@superblocksteam/util";
6
+ import { bold } from "colorette";
7
+ import fs from "fs-extra";
8
+ import { isEmpty, isArray, isObject, get, cloneDeep } from "lodash-es";
9
+ import * as semver from "semver";
10
+ import { simpleGit } from "simple-git";
11
+ import slugify from "slugify";
12
+ import { parse, stringify as ymlstringify } from "yaml";
13
+ export const LATEST_EDITS_MODE = "latest-edits";
14
+ export const MOST_RECENT_COMMIT_MODE = "most-recent-commit";
15
+ export const DEPLOYED_MODE = "deployed";
16
+ export const DEFAULT_BRANCH = "main";
17
+ const LANGUAGE_STEP_EXTENSIONS = {
18
+ javascript: "js",
19
+ python: "py",
20
+ };
21
+ export const modeFlagValuesMap = {
22
+ [LATEST_EDITS_MODE]: "Latest edits",
23
+ [MOST_RECENT_COMMIT_MODE]: "Most recent commit",
24
+ [DEPLOYED_MODE]: "Deployed",
25
+ };
26
+ export var FileStructureType;
27
+ (function (FileStructureType) {
28
+ FileStructureType["SINGLE_PAGE"] = "single-page";
29
+ FileStructureType["MULTI_PAGE"] = "multi-page";
30
+ })(FileStructureType || (FileStructureType = {}));
31
+ export function modeFlagToViewMode(modeFlag) {
32
+ switch (modeFlag) {
33
+ case LATEST_EDITS_MODE:
34
+ return "export-live";
35
+ case MOST_RECENT_COMMIT_MODE:
36
+ return "export-latest";
37
+ case DEPLOYED_MODE:
38
+ return "export-deployed";
39
+ default:
40
+ throw new Error(`Unsupported mode flag: ${modeFlag}`);
41
+ }
42
+ }
43
+ export const SELECT_PROMPT_HELP = "Use ↑/↓ arrow keys, Enter to confirm";
44
+ export const MULTI_SELECT_PROMPT_HELP = "Type to filter, Use ↑/↓ arrow keys, Space to select, Enter to confirm";
45
+ export const atLeastOneSelection = (value) => {
46
+ if (isEmpty(value)) {
47
+ return `Please select at least one item ${bold("by pressing space")}`;
48
+ }
49
+ return true;
50
+ };
51
+ const DEFAULT_FILE_VERSION = "0.1.0";
52
+ const SPLIT_LARGE_API_STEPS_VERSION = "0.2.0";
53
+ const LATEST_FILE_VERSION = SPLIT_LARGE_API_STEPS_VERSION;
54
+ export function getApiRepresentation(featureFlags, resourceConfig) {
55
+ const linesForLargeSteps = featureFlags.linesForLargeSteps() ?? DEFAULT_LINES_FOR_LARGE_STEPS;
56
+ if (featureFlags.splitLargeApiStepsEnabled() &&
57
+ isPostSplitLargeApiSteps(resourceConfig)) {
58
+ return {
59
+ extractLargeSourceFiles: true,
60
+ minLinesForExtraction: linesForLargeSteps,
61
+ };
62
+ }
63
+ // Return the default
64
+ return {
65
+ extractLargeSourceFiles: false,
66
+ minLinesForExtraction: linesForLargeSteps,
67
+ };
68
+ }
69
+ function isPostSplitLargeApiSteps(resourceConfig) {
70
+ if (!resourceConfig) {
71
+ return false;
72
+ }
73
+ const version = resourceConfig.metadata?.fileVersion ?? DEFAULT_FILE_VERSION;
74
+ return semver.compare(version, SPLIT_LARGE_API_STEPS_VERSION) >= 0;
75
+ }
76
+ function slugifyName(originalName) {
77
+ return slugify(originalName, {
78
+ replacement: "_",
79
+ lower: true,
80
+ });
81
+ }
82
+ // resolves a true promise when download is complete. If there's an error, resolve false
83
+ async function downloadFile(rootDirectory, filepath, url) {
84
+ const fullPath = `${rootDirectory}/${filepath}`;
85
+ // eslint-disable-next-line no-async-promise-executor
86
+ const result = await new Promise(async (resolve) => {
87
+ try {
88
+ // create directory path if it doesn't exist yet
89
+ if (!(await fs.pathExists(fullPath))) {
90
+ await fs.mkdir(dirname(fullPath), { recursive: true });
91
+ }
92
+ const file = fs.createWriteStream(fullPath);
93
+ https.get(url, (resp) => {
94
+ resp.pipe(file);
95
+ file
96
+ .on("finish", () => {
97
+ file.end(() => resolve(true));
98
+ })
99
+ .on("error", () => {
100
+ fs.unlink(filepath);
101
+ resolve(false);
102
+ });
103
+ });
104
+ }
105
+ catch {
106
+ return resolve(false);
107
+ }
108
+ });
109
+ // failed to download correctly, attempt to clean up file
110
+ if (!result) {
111
+ try {
112
+ await fs.unlink(fullPath);
113
+ }
114
+ catch {
115
+ console.log("Failed to delete file", fullPath);
116
+ }
117
+ return Promise.resolve(fullPath);
118
+ }
119
+ return Promise.resolve("");
120
+ }
121
+ export async function readAppApiYamlFile(parentDirectory, apiName) {
122
+ // The API is either stored in its entirety in a single YAML file, or
123
+ // or in a subdirectory containing the YAML file and zero or more language-specific files.
124
+ const apiNameSlug = slugifyName(apiName ?? "api");
125
+ const singularApiYamlPath = `${parentDirectory}/${apiNameSlug}.yaml`;
126
+ const apiDirPath = `${parentDirectory}/${apiNameSlug}`;
127
+ const nestedApiYamlPath = `${apiDirPath}/api.yaml`;
128
+ // Read the YAML file
129
+ let yamlPath;
130
+ let yamlParentPath;
131
+ if (await fs.pathExists(singularApiYamlPath)) {
132
+ // The API YAML file is in the parent directory
133
+ yamlPath = singularApiYamlPath;
134
+ yamlParentPath = parentDirectory;
135
+ }
136
+ else if (await fs.pathExists(nestedApiYamlPath)) {
137
+ // The API YAML file is nested in an API-specific directory
138
+ yamlPath = nestedApiYamlPath;
139
+ yamlParentPath = apiDirPath;
140
+ }
141
+ else {
142
+ throw new Error(`API ${apiName ?? ""} not found at ${parentDirectory}`);
143
+ }
144
+ // That is nested in an API-specific directory
145
+ const apiDefn = (await readYamlFile(yamlPath));
146
+ // The API YAML file may or may not have language-specific content in separate files.
147
+ // Replace any file references with the actual content
148
+ await resolveLanguageSpecificStepContentFromBlocks(yamlParentPath, apiDefn.blocks ?? []);
149
+ return apiDefn;
150
+ }
151
+ async function resolveLanguageSpecificStepContentFromBlocks(parentPath, blocks) {
152
+ for (const block of blocks) {
153
+ // Handle language-specific step content
154
+ if (block.step) {
155
+ const step = block.step;
156
+ await resolveLanguageSpecificStepContentFromStep(parentPath, step);
157
+ }
158
+ // Handle conditional blocks
159
+ if (block.conditional) {
160
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.conditional.if?.blocks ?? []);
161
+ for (const elseIfBlock of block.conditional.elseIf) {
162
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, elseIfBlock.blocks);
163
+ }
164
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.conditional.else?.blocks ?? []);
165
+ }
166
+ // Handle loop blocks
167
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.loop?.blocks ?? []);
168
+ // Handle try-catch blocks
169
+ if (block.tryCatch) {
170
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.tryCatch.try?.blocks ?? []);
171
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.tryCatch.catch?.blocks ?? []);
172
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.tryCatch.finally?.blocks ?? []);
173
+ }
174
+ // Handle parallel blocks
175
+ if (block.parallel) {
176
+ for (const path of Object.values(block.parallel?.static?.paths ?? {})) {
177
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, path.blocks);
178
+ }
179
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.parallel?.dynamic?.blocks ?? []);
180
+ }
181
+ // Handle stream blocks
182
+ if (block.stream) {
183
+ await resolveLanguageSpecificStepContentFromBlocks(parentPath, block.stream.process?.blocks ?? []);
184
+ await resolveLanguageSpecificStepContentFromStep(parentPath, block.stream.trigger?.step ?? {});
185
+ }
186
+ }
187
+ }
188
+ async function resolveLanguageSpecificStepContentFromStep(parentPath, step) {
189
+ // Handle language-specific step content
190
+ if (!step?.integration || !LANGUAGE_STEP_EXTENSIONS[step.integration]) {
191
+ return;
192
+ }
193
+ const languageContent = step[step.integration];
194
+ if (languageContent.body &&
195
+ typeof languageContent.body === "object" &&
196
+ "path" in languageContent.body) {
197
+ const languageFilePath = `${parentPath}/${languageContent.body.path}`;
198
+ if (await fs.pathExists(languageFilePath)) {
199
+ // Read the file content
200
+ const stepBodyContent = await fs.readFile(languageFilePath, "utf8");
201
+ if (stepBodyContent && languageContent) {
202
+ languageContent.body = stepBodyContent;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ async function readYamlFile(path) {
208
+ return parse(await fs.readFile(path, "utf8"));
209
+ }
210
+ function getFileStructureTypeFromResourceConfig(superblocksConfig) {
211
+ if (superblocksConfig.pages) {
212
+ return FileStructureType.MULTI_PAGE;
213
+ }
214
+ return FileStructureType.SINGLE_PAGE;
215
+ }
216
+ export async function getFileStructureType(rootPath, existingRelativeLocation) {
217
+ const superblocksConfig = await getSuperblocksApplicationConfigJson(`${rootPath}/${existingRelativeLocation}`);
218
+ return getFileStructureTypeFromResourceConfig(superblocksConfig);
219
+ }
220
+ // NOTE: If a change is made to how applications are read from disk, please update
221
+ // logic to write applications to disk in the "writeResourceToDisk" function accordingly.
222
+ // @deprecated this can be removed once all customers move to multi page applications
223
+ export async function readApplicationFromDisk(rootPath, existingRelativeLocation) {
224
+ const application = await readYamlFile(`${rootPath}/${existingRelativeLocation}/application.yaml`);
225
+ const page = await readYamlFile(`${rootPath}/${existingRelativeLocation}/page.yaml`);
226
+ const apisDirName = `${rootPath}/${existingRelativeLocation}/apis`;
227
+ const apis = [];
228
+ if (await fs.pathExists(apisDirName)) {
229
+ const apiFiles = await fs.readdir(apisDirName);
230
+ for (const apiFile of apiFiles) {
231
+ const apiContent = await readAppApiYamlFile(apisDirName, apiFile);
232
+ // This mimics the shape of the ApiV3Dto object
233
+ apis.push({
234
+ id: apiContent.metadata.id,
235
+ apiPb: apiContent,
236
+ });
237
+ }
238
+ }
239
+ return {
240
+ application,
241
+ apis,
242
+ page,
243
+ };
244
+ }
245
+ // NOTE: If a change is made to how applications are read from disk, please update
246
+ // logic to write applications to disk in the "writeResourceToDisk" function accordingly.
247
+ export async function readMultiPageApplicationFromDisk(rootPath, existingRelativeLocation) {
248
+ const superblocksApplicationConfig = await getSuperblocksApplicationConfigJson(`${rootPath}/${existingRelativeLocation}`);
249
+ const application = await readYamlFile(`${rootPath}/${existingRelativeLocation}/application.yaml`);
250
+ const pagesDirName = `${rootPath}/${existingRelativeLocation}/pages`;
251
+ const pages = [];
252
+ if (await fs.pathExists(pagesDirName)) {
253
+ for (const page of Object.values(superblocksApplicationConfig.pages ?? {})) {
254
+ // Read in the page definition
255
+ const pageContent = await readYamlFile(`${pagesDirName}/${slugifyName(page.name)}/page.yaml`);
256
+ // Read in the API definitions for this page
257
+ const pageApisDirName = `${pagesDirName}/${slugifyName(page.name)}/apis`;
258
+ const apis = [];
259
+ if (await fs.pathExists(pageApisDirName)) {
260
+ // Get the API names from pageApis if it exists, or from apis if it doesn't
261
+ const pageApiNames = getApiNamesFromPageConfig(page);
262
+ for (const apiName of pageApiNames) {
263
+ const apiContent = await readAppApiYamlFile(pageApisDirName, apiName);
264
+ // This mimics the shape of the ApiV3Dto object
265
+ apis.push({
266
+ id: apiContent.metadata.id,
267
+ pageId: page.id, // Alternatively, pageId is also stored in apiPb.trigger.application.pageId
268
+ apiPb: apiContent,
269
+ });
270
+ }
271
+ }
272
+ pages.push({
273
+ id: pageContent.id,
274
+ name: pageContent.name,
275
+ applicationId: pageContent.applicationId,
276
+ isHidden: pageContent.isHidden,
277
+ layouts: pageContent.layouts,
278
+ apis,
279
+ });
280
+ }
281
+ }
282
+ const appApisDirName = `${rootPath}/${existingRelativeLocation}/apis`;
283
+ const apis = [];
284
+ if (await fs.pathExists(appApisDirName)) {
285
+ // Get the API names from appApis if it exists, or from apis if it doesn't
286
+ const appConfig = superblocksApplicationConfig;
287
+ const apiNames = getApiNamesFromApplicationConfig(appConfig);
288
+ for (const apiName of apiNames) {
289
+ const apiContent = await readAppApiYamlFile(appApisDirName, apiName);
290
+ // This mimics the shape of the ApiV3Dto object
291
+ apis.push({
292
+ id: apiContent.metadata.id,
293
+ apiPb: apiContent,
294
+ });
295
+ }
296
+ }
297
+ return {
298
+ application,
299
+ apis,
300
+ pages,
301
+ };
302
+ }
303
+ function getApiNamesFromApplicationConfig(applicationConfig) {
304
+ return applicationConfig.appApis
305
+ ? Object.values(applicationConfig.appApis).map((api) => api.name)
306
+ : Object.values(applicationConfig.apis ?? {});
307
+ }
308
+ function getApiNamesFromPageConfig(pageConfig) {
309
+ return pageConfig.pageApis
310
+ ? Object.values(pageConfig.pageApis).map((api) => api.name)
311
+ : Object.values(pageConfig.apis ?? {});
312
+ }
313
+ export async function readApiFromDisk(rootPath, existingRelativeLocation) {
314
+ const path = `${rootPath}/${existingRelativeLocation}`;
315
+ const apiContent = await readAppApiYamlFile(path);
316
+ return {
317
+ apiPb: apiContent,
318
+ };
319
+ }
320
+ // NOTE: If a change is made to how applications are written to disk, please update
321
+ // logic to read applications from disk in the "readApplicationFromDisk" function accordingly.
322
+ export async function writeResourceToDisk(resourceType, resourceId, resource, rootPath, featureFlags, existingRelativeLocation, preferredApiRepresentation) {
323
+ switch (resourceType) {
324
+ case "APPLICATION": {
325
+ const parentDirName = "apps";
326
+ const newRelativeLocation = `${parentDirName}/${slugifyName(resource.application.name)}`;
327
+ const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
328
+ const appDirName = path.resolve(rootPath, relativeLocation);
329
+ if (!(await fs.pathExists(appDirName))) {
330
+ await fs.mkdir(appDirName, { recursive: true });
331
+ }
332
+ const applicationContent = ymlstringify(resource.application, {
333
+ sortMapEntries: true,
334
+ });
335
+ await fs.outputFile(`${appDirName}/application.yaml`, applicationContent);
336
+ if (resource.page) {
337
+ const pageContent = ymlstringify(resource.page, {
338
+ sortMapEntries: true,
339
+ blockQuote: "literal",
340
+ });
341
+ await fs.outputFile(`${appDirName}/page.yaml`, pageContent);
342
+ }
343
+ const apiPromises = [];
344
+ const apisDirName = `${appDirName}/apis`;
345
+ await fs.ensureDir(apisDirName);
346
+ const existingApplicationConfig = await getSuperblocksApplicationConfigIfExists(appDirName);
347
+ const existingFilePaths = getExistingFilePathsForApplicationApi(existingApplicationConfig, appDirName);
348
+ const newApplicationConfig = {
349
+ configType: "APPLICATION",
350
+ defaultPageId: resource.page?.id,
351
+ id: resource.application.id,
352
+ metadata: getResourceConfigMetadata(featureFlags, existingApplicationConfig),
353
+ };
354
+ const apiRepresentation = preferredApiRepresentation ??
355
+ getApiRepresentation(featureFlags, newApplicationConfig);
356
+ if (resource.apis) {
357
+ for (const api of resource.apis) {
358
+ const apiInfo = await writeAppApi(api, appDirName, existingFilePaths, apiPromises, apiRepresentation);
359
+ if (apiRepresentation.extractLargeSourceFiles) {
360
+ if (!newApplicationConfig.appApis) {
361
+ newApplicationConfig.appApis = {};
362
+ }
363
+ newApplicationConfig.appApis[api.apiPb.metadata.id] = apiInfo;
364
+ }
365
+ else {
366
+ if (!newApplicationConfig.apis) {
367
+ newApplicationConfig.apis = {};
368
+ }
369
+ newApplicationConfig.apis[api.apiPb.metadata.id] = apiInfo.name;
370
+ }
371
+ }
372
+ await Promise.all(apiPromises);
373
+ }
374
+ // Delete any existing files that were not overwritten
375
+ for (const filePath of existingFilePaths) {
376
+ await fs.remove(filePath);
377
+ }
378
+ await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
379
+ await fs.writeFile(`${appDirName}/${RESOURCE_CONFIG_PATH}`, JSON.stringify(sortByKey(newApplicationConfig), null, 2));
380
+ const createdFiles = await Promise.resolve(
381
+ // Defensive check for when application settings are missing componentFiles
382
+ resource.componentFiles?.map((file) => downloadFile(appDirName, file.filename, file.url)) ?? []);
383
+ // print out failed downloads synchronously here
384
+ createdFiles
385
+ .filter((createdFiles) => createdFiles.length)
386
+ .forEach((createdFile) => {
387
+ console.log(`Unable to download ${createdFile}`);
388
+ });
389
+ return {
390
+ location: relativeLocation,
391
+ resourceType: "APPLICATION",
392
+ };
393
+ }
394
+ case "BACKEND": {
395
+ const parentDirName = "backends";
396
+ const apiName = slugifyName(extractApiName(resource));
397
+ const newRelativeLocation = `${parentDirName}/${apiName}`;
398
+ const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
399
+ const backendDirName = path.resolve(rootPath, relativeLocation);
400
+ if (!(await fs.pathExists(backendDirName))) {
401
+ await fs.mkdir(backendDirName, { recursive: true });
402
+ }
403
+ const existingBackendConfig = await getSuperblocksBackendConfigIfExists(backendDirName);
404
+ const existingFilePaths = getExistingFilePathsForBackendApi(existingBackendConfig, backendDirName);
405
+ const backendConfig = {
406
+ id: resourceId,
407
+ configType: "BACKEND",
408
+ metadata: getResourceConfigMetadata(featureFlags, existingBackendConfig),
409
+ };
410
+ const apiRepresentation = preferredApiRepresentation ??
411
+ getApiRepresentation(featureFlags, backendConfig);
412
+ // Write the API file(s)
413
+ const apiInfo = await writeBackendApi(resource, backendDirName, [], apiRepresentation, existingFilePaths);
414
+ if (apiRepresentation.extractLargeSourceFiles) {
415
+ backendConfig.sourceFiles = apiInfo.sourceFiles;
416
+ }
417
+ // Write the backend config file
418
+ await fs.ensureDir(`${backendDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
419
+ await fs.writeFile(`${backendDirName}/${RESOURCE_CONFIG_PATH}`, JSON.stringify(sortByKey(backendConfig), null, 2));
420
+ // Delete any existing files that were not overwritten
421
+ for (const filePath of existingFilePaths) {
422
+ await fs.remove(filePath);
423
+ }
424
+ return {
425
+ location: relativeLocation,
426
+ resourceType: "BACKEND",
427
+ };
428
+ }
429
+ default: {
430
+ throw new Error(`Unsupported resource type: ${resourceType}`);
431
+ }
432
+ }
433
+ }
434
+ // NOTE: If a change is made to how applications are written to disk, please update
435
+ // logic to read applications from disk in the "readMultiPageApplicationFromDisk" function accordingly.
436
+ export async function writeMultiPageApplicationToDisk(resource, rootPath, featureFlags, existingRelativeLocation, migrateFromSinglePage, preferredApiRepresentation) {
437
+ const parentDirName = "apps";
438
+ const newRelativeLocation = `${parentDirName}/${slugifyName(resource.application.name)}`;
439
+ const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
440
+ const appDirName = path.resolve(rootPath, relativeLocation);
441
+ if (!(await fs.pathExists(appDirName))) {
442
+ await fs.mkdir(appDirName, { recursive: true });
443
+ }
444
+ const applicationContent = ymlstringify(resource.application, {
445
+ sortMapEntries: true,
446
+ });
447
+ await fs.outputFile(`${appDirName}/application.yaml`, applicationContent);
448
+ // Find the existing application config and existing file paths
449
+ const existingApplicationConfig = await getSuperblocksApplicationConfigIfExists(appDirName);
450
+ const existingFilePaths = getExistingFilePathsForApplicationApi(existingApplicationConfig, appDirName);
451
+ const newApplicationConfig = {
452
+ configType: "APPLICATION",
453
+ id: resource.application.id,
454
+ metadata: getResourceConfigMetadata(featureFlags, existingApplicationConfig),
455
+ };
456
+ const apiRepresentation = preferredApiRepresentation ??
457
+ getApiRepresentation(featureFlags, newApplicationConfig);
458
+ newApplicationConfig.pages = {};
459
+ const apiPromises = [];
460
+ for (const page of Object.values(resource.pages)) {
461
+ const pageId = page.id;
462
+ const pageDirName = `${appDirName}/pages/${slugifyName(page.name)}`;
463
+ if (!(await fs.pathExists(pageDirName))) {
464
+ await fs.mkdir(pageDirName, { recursive: true });
465
+ }
466
+ newApplicationConfig.pages[pageId] = {
467
+ id: page.id,
468
+ name: page.name,
469
+ };
470
+ const pageConfig = newApplicationConfig.pages[pageId];
471
+ const pageContent = ymlstringify({
472
+ id: page.id,
473
+ name: page.name,
474
+ applicationId: page.applicationId,
475
+ isHidden: page.isHidden,
476
+ layouts: page.layouts,
477
+ }, {
478
+ sortMapEntries: true,
479
+ blockQuote: "literal",
480
+ });
481
+ const pageFilePath = `${pageDirName}/page.yaml`;
482
+ await fs.outputFile(pageFilePath, pageContent);
483
+ existingFilePaths.delete(pageFilePath);
484
+ for (const api of page.apis) {
485
+ const apiInfo = await writeAppApi(api, pageDirName, existingFilePaths, apiPromises, apiRepresentation);
486
+ // Update the page config with the new api info
487
+ const apiId = api.apiPb.metadata.id;
488
+ if (apiRepresentation.extractLargeSourceFiles) {
489
+ // Add the new pageApis field whenever large source files might be extracted
490
+ if (!pageConfig.pageApis) {
491
+ pageConfig.pageApis = {};
492
+ }
493
+ pageConfig.pageApis[apiId] = {
494
+ name: apiInfo.name,
495
+ sourceFiles: apiInfo.sourceFiles?.sort(),
496
+ };
497
+ }
498
+ else {
499
+ if (!pageConfig.apis) {
500
+ pageConfig.apis = {};
501
+ }
502
+ pageConfig.apis[apiId] = apiInfo.name;
503
+ }
504
+ }
505
+ }
506
+ const appApis = {};
507
+ if (resource.apis && resource.apis.length) {
508
+ for (const api of resource.apis) {
509
+ const apiInfo = await writeAppApi(api, appDirName, existingFilePaths, apiPromises, apiRepresentation);
510
+ const apiId = api.apiPb.metadata.id;
511
+ if (apiRepresentation.extractLargeSourceFiles) {
512
+ if (!newApplicationConfig.appApis) {
513
+ newApplicationConfig.appApis = {};
514
+ }
515
+ newApplicationConfig.appApis[apiId] = apiInfo;
516
+ }
517
+ else {
518
+ if (!newApplicationConfig.apis) {
519
+ newApplicationConfig.apis = {};
520
+ }
521
+ newApplicationConfig.apis[apiId] = apiInfo.name;
522
+ }
523
+ }
524
+ await Promise.all(apiPromises);
525
+ }
526
+ if (apiRepresentation.extractLargeSourceFiles) {
527
+ newApplicationConfig.appApis = appApis;
528
+ }
529
+ else if (!newApplicationConfig.apis) {
530
+ // Make sure there is an empty object even if there are no APIs
531
+ newApplicationConfig.apis = {};
532
+ }
533
+ // Delete any existing files that were not overwritten
534
+ for (const filePath of existingFilePaths) {
535
+ await fs.remove(filePath);
536
+ }
537
+ await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
538
+ await fs.writeFile(`${appDirName}/${RESOURCE_CONFIG_PATH}`, JSON.stringify(sortByKey(newApplicationConfig), null, 2));
539
+ const createdFiles = await Promise.resolve(
540
+ // Defensive check for when application settings are missing componentFiles
541
+ resource.componentFiles?.map((file) => downloadFile(appDirName, file.filename, file.url)) ?? []);
542
+ // print out failed downloads synchronously here
543
+ createdFiles
544
+ .filter((createdFiles) => createdFiles.length)
545
+ .forEach((createdFile) => {
546
+ console.log(`Unable to download ${createdFile}`);
547
+ });
548
+ // do a post-migration cleanup if necessary
549
+ if (migrateFromSinglePage) {
550
+ await fs.remove(`${appDirName}/page.yaml`);
551
+ // delete app-level apis if they are not present in the new application config
552
+ if (!resource.apis || !resource.apis.length) {
553
+ await fs.remove(`${appDirName}/apis`);
554
+ }
555
+ }
556
+ return {
557
+ location: relativeLocation,
558
+ resourceType: "APPLICATION",
559
+ };
560
+ }
561
+ function getExistingFilePathsForApplicationApi(superblocksApplicationConfig, location) {
562
+ const paths = new Set();
563
+ if (!superblocksApplicationConfig) {
564
+ return paths;
565
+ }
566
+ // Pages
567
+ if (superblocksApplicationConfig.pages) {
568
+ for (const page of Object.values(superblocksApplicationConfig.pages)) {
569
+ // Page YAML
570
+ const pageNameSlug = slugifyName(page.name);
571
+ const pagePath = `${location}/pages/${pageNameSlug}`;
572
+ paths.add(`${pagePath}/page.yaml`);
573
+ // Page APIs
574
+ if (page.pageApis) {
575
+ for (const api of Object.values(page.pageApis)) {
576
+ addExistingFilePathsForApi(api, `${pagePath}/apis`, paths, true);
577
+ }
578
+ }
579
+ else if (page.apis) {
580
+ for (const apiName of Object.values(page.apis)) {
581
+ addExistingFilePathsForApi({ name: apiName }, `${pagePath}/apis`, paths, true);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ // APIs
587
+ if (superblocksApplicationConfig.appApis) {
588
+ // For file version >= 0.2.0, there are API files either the 'apis' folder or in an API-specific folder
589
+ for (const api of Object.values(superblocksApplicationConfig.appApis)) {
590
+ addExistingFilePathsForApi(api, `${location}/apis`, paths, true);
591
+ }
592
+ }
593
+ else if (superblocksApplicationConfig.apis) {
594
+ // For file version < 0.2.0, there are only API file
595
+ for (const apiName of Object.values(superblocksApplicationConfig.apis)) {
596
+ addExistingFilePathsForApi({ name: apiName }, `${location}/apis`, paths, true);
597
+ }
598
+ }
599
+ return paths;
600
+ }
601
+ function getExistingFilePathsForBackendApi(superblocksBackendConfig, location) {
602
+ const paths = new Set();
603
+ if (!superblocksBackendConfig) {
604
+ return paths;
605
+ }
606
+ // Pages
607
+ if (superblocksBackendConfig.sourceFiles) {
608
+ addExistingFilePathsForApi({ name: "api", sourceFiles: superblocksBackendConfig.sourceFiles }, location, paths, false);
609
+ }
610
+ else {
611
+ addExistingFilePathsForApi({ name: "api" }, location, paths, false);
612
+ }
613
+ return paths;
614
+ }
615
+ export function addExistingFilePathsForApi(api, location, paths, useNestedFolder) {
616
+ const apiNameSlug = slugifyName(api.name);
617
+ if (api.sourceFiles?.length) {
618
+ // API files are in an API-specific folder
619
+ const apiDirPath = useNestedFolder
620
+ ? `${location}/${apiNameSlug}`
621
+ : location;
622
+ paths.add(`${apiDirPath}/${apiNameSlug}.yaml`);
623
+ // And there are source files
624
+ for (const sourceFile of api.sourceFiles ?? []) {
625
+ paths.add(`${apiDirPath}/${sourceFile}`);
626
+ }
627
+ }
628
+ else {
629
+ // API file is in the 'apis' folder with no separate source files
630
+ paths.add(`${location}/${apiNameSlug}.yaml`);
631
+ }
632
+ return paths;
633
+ }
634
+ function getResourceConfigMetadata(featureFlags, existingResourceConfig) {
635
+ if (!existingResourceConfig) {
636
+ // This is a new application, and we may need to add a metadata field to the application config
637
+ if (featureFlags.splitLargeApiStepsInNewEnabled()) {
638
+ return {
639
+ fileVersion: LATEST_FILE_VERSION,
640
+ };
641
+ }
642
+ }
643
+ else if (existingResourceConfig.metadata) {
644
+ return existingResourceConfig.metadata;
645
+ }
646
+ return undefined;
647
+ }
648
+ export async function removeResourceFromDisk(rootPath, resourceRelativeLocation) {
649
+ const resourceLocation = path.resolve(rootPath, resourceRelativeLocation);
650
+ await fs.remove(resourceLocation);
651
+ }
652
+ export async function getMode(task, mode) {
653
+ if (mode) {
654
+ return modeFlagToViewMode(mode);
655
+ }
656
+ const selectedMode = await task.prompt([
657
+ {
658
+ type: "Select",
659
+ name: "mode",
660
+ message: `Select which version of a resource you would like to fetch (${SELECT_PROMPT_HELP})`,
661
+ choices: Object.keys(modeFlagValuesMap).map((mode) => ({
662
+ name: mode,
663
+ message: modeFlagValuesMap[mode],
664
+ })),
665
+ initial: 0,
666
+ multiple: false,
667
+ },
668
+ ]);
669
+ return modeFlagToViewMode(selectedMode);
670
+ }
671
+ export function sortByKey(obj) {
672
+ if (isArray(obj)) {
673
+ return obj.map((item) => sortByKey(item));
674
+ }
675
+ if (isObject(obj)) {
676
+ const sortedKeys = Object.keys(obj).sort();
677
+ const sortedObj = {};
678
+ for (const key of sortedKeys) {
679
+ sortedObj[key] = sortByKey(get(obj, key));
680
+ }
681
+ return sortedObj;
682
+ }
683
+ return obj;
684
+ }
685
+ export async function getLocalGitRepoState(overrideLocalBranch) {
686
+ const git = simpleGit();
687
+ let status;
688
+ try {
689
+ // do not return untracked files which can be slow, we only care about the branch info
690
+ status = await git.status(["--untracked-files=no"]);
691
+ }
692
+ catch {
693
+ return { status: "NO_GIT" };
694
+ }
695
+ let localBranchName;
696
+ if (overrideLocalBranch) {
697
+ const branches = await git.branch();
698
+ if (!branches.all.includes(overrideLocalBranch)) {
699
+ throw new Error(`There is no branch named ${overrideLocalBranch}`);
700
+ }
701
+ localBranchName = overrideLocalBranch;
702
+ }
703
+ else {
704
+ if (status.detached || !status.current) {
705
+ return { status: "DETACHED" };
706
+ }
707
+ localBranchName = status.current;
708
+ }
709
+ const remoteName = (await git.getConfig(`branch.${localBranchName}.remote`))
710
+ ?.value;
711
+ if (!remoteName) {
712
+ return { status: "IN_A_BRANCH", localBranchName };
713
+ }
714
+ const remoteFetchUrl = (await git.getConfig(`remote.${remoteName}.url`))
715
+ ?.value;
716
+ if (!remoteFetchUrl) {
717
+ return { status: "IN_A_BRANCH", localBranchName };
718
+ }
719
+ const remotePushUrl = (await git.getConfig(`remote.${remoteName}.pushurl`))
720
+ ?.value;
721
+ let upstreamBranchName = (await git.revparse(["--abbrev-ref", "--symbolic-full-name", "@{u}"])).split("\n", 1)[0];
722
+ if (upstreamBranchName.startsWith(`${remoteName}/`)) {
723
+ // `upstreamBranchName` is in the form `$remoteName/$branchName`, so we need to remove the `$remoteName/` part
724
+ upstreamBranchName = upstreamBranchName.substring(remoteName.length + 1);
725
+ }
726
+ return {
727
+ status: "IN_A_BRANCH",
728
+ localBranchName,
729
+ upstream: {
730
+ branchName: upstreamBranchName,
731
+ remoteName,
732
+ url: remoteFetchUrl,
733
+ pushUrl: remotePushUrl,
734
+ },
735
+ };
736
+ }
737
+ /**
738
+ * Returns the current git branch, or undefined if not in a git repo
739
+ */
740
+ export async function getCurrentGitBranchIfGit() {
741
+ const git = simpleGit();
742
+ try {
743
+ // do not return untracked files which can be slow, we only care about the branch info
744
+ const status = await git.status(["--untracked-files=no"]);
745
+ const currentBranch = status.current;
746
+ return currentBranch;
747
+ }
748
+ catch {
749
+ // ignore
750
+ }
751
+ return null;
752
+ }
753
+ /**
754
+ * Returns the current git branch, or throws if not in a git repo
755
+ */
756
+ export async function getCurrentGitBranch() {
757
+ const currentBranch = await getCurrentGitBranchIfGit();
758
+ if (!currentBranch) {
759
+ throw new Error("No git repository found");
760
+ }
761
+ return currentBranch;
762
+ }
763
+ export async function getHeadCommit(branch) {
764
+ const git = simpleGit();
765
+ let headCommitId;
766
+ let headCommitMessage;
767
+ const logResponse = await git.show(["--no-patch", "--format=%H%n%s", branch]);
768
+ const logLines = logResponse.split("\n");
769
+ if (logLines.length > 1) {
770
+ headCommitId = logLines[0];
771
+ headCommitMessage = logLines[1];
772
+ }
773
+ if (!headCommitId || !headCommitMessage) {
774
+ throw new Error(`Failed to get head commit for branch '${branch}'`);
775
+ }
776
+ return [headCommitId, headCommitMessage];
777
+ }
778
+ export function isCI() {
779
+ return process.env.CI === "true";
780
+ }
781
+ export async function isGitRepoDirty() {
782
+ if (isCI()) {
783
+ // Skip dirtiness check in CI environments
784
+ return false;
785
+ }
786
+ const git = simpleGit();
787
+ const status = await git.status();
788
+ return !status.isClean();
789
+ }
790
+ export function extractApiName(api) {
791
+ return api.apiPb?.metadata?.name ?? api?.name ?? "Unknown";
792
+ }
793
+ export async function writeAppApi(api, directoryPath, existingFilePaths, apiPromises, apiRepresentation) {
794
+ const originalApiName = extractApiName(api);
795
+ const additionalStepFiles = [];
796
+ await writeApiFiles(api, slugifyName(originalApiName), `${directoryPath}/apis`, true, apiPromises, additionalStepFiles, apiRepresentation, existingFilePaths);
797
+ return {
798
+ name: originalApiName,
799
+ sourceFiles: additionalStepFiles.map((file) => file.relativePath).sort(),
800
+ };
801
+ }
802
+ async function writeBackendApi(api, directoryPath, apiPromises, apiRepresentation, existingFilePaths) {
803
+ const originalApiName = extractApiName(api);
804
+ const additionalStepFiles = [];
805
+ await writeApiFiles(api, "api", directoryPath, false, apiPromises, additionalStepFiles, apiRepresentation, existingFilePaths);
806
+ return {
807
+ name: originalApiName,
808
+ sourceFiles: additionalStepFiles.map((file) => file.relativePath).sort(),
809
+ };
810
+ }
811
+ async function writeApiFiles(api, apiNameSlug, directoryPath, nestedApiFiles, apiPromises, additionalStepFiles, apiRepresentation, existingFilePaths) {
812
+ let pathForApiFile = directoryPath;
813
+ let yamlFileName = `${apiNameSlug}.yaml`;
814
+ // Determine whether to extract into separate files the source code from the steps
815
+ const updatedApi = extractAdditionalStepFiles(api.apiPb, additionalStepFiles, apiRepresentation);
816
+ if (apiRepresentation?.extractLargeSourceFiles) {
817
+ if (nestedApiFiles) {
818
+ // This API has at least one large step code file, so we need a nested directory
819
+ pathForApiFile = `${pathForApiFile}/${apiNameSlug}`;
820
+ yamlFileName = "api.yaml";
821
+ }
822
+ // Make sure the directory exists
823
+ await fs.ensureDir(pathForApiFile);
824
+ // Write any additional step source code files to the disk
825
+ for (const stepFile of additionalStepFiles) {
826
+ const sourceFilePath = `${pathForApiFile}/${stepFile.relativePath}`;
827
+ const handleApi = async () => {
828
+ await fs.outputFile(sourceFilePath, stepFile.contents);
829
+ };
830
+ apiPromises.push(handleApi());
831
+ // Mark the existing source file as being reused
832
+ existingFilePaths?.delete(sourceFilePath);
833
+ }
834
+ }
835
+ else {
836
+ // No additional step files to write, so mark the API directory as existing so that it will be removed later.
837
+ // Do this even if the current representation does not extract large source files, in case they were previously
838
+ existingFilePaths?.add(`${directoryPath}/${apiNameSlug}`);
839
+ }
840
+ // Convert the updated API to YAML and write it to disk
841
+ const apiContent = ymlstringify(updatedApi, {
842
+ sortMapEntries: true,
843
+ blockQuote: "literal",
844
+ });
845
+ const yamlFilePath = `${pathForApiFile}/${yamlFileName}`;
846
+ const handleApi = async () => {
847
+ await fs.outputFile(yamlFilePath, apiContent);
848
+ };
849
+ apiPromises.push(handleApi());
850
+ // Mark the existing source file as being reused
851
+ existingFilePaths?.delete(yamlFilePath);
852
+ }
853
+ function extractAdditionalStepFiles(api, additionalStepFiles, apiRepresentation) {
854
+ if (!api.blocks || !apiRepresentation?.extractLargeSourceFiles) {
855
+ return api;
856
+ }
857
+ const apiClone = cloneDeep(api);
858
+ const minLinesForExtraction = apiRepresentation?.minLinesForExtraction ?? DEFAULT_LINES_FOR_LARGE_STEPS;
859
+ const existingFilePaths = new Set();
860
+ extractAdditionalStepFilesFromBlocks(apiClone.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
861
+ return apiClone;
862
+ }
863
+ function extractAdditionalStepFilesFromBlocks(blocks, additionalStepFiles, existingFilePaths, minLinesForExtraction) {
864
+ for (const block of blocks) {
865
+ // Handle language step files
866
+ if (block.step) {
867
+ extractAdditionalStepFilesFromStep(block.step, block.name, additionalStepFiles, existingFilePaths, minLinesForExtraction);
868
+ }
869
+ // Handle loops
870
+ extractAdditionalStepFilesFromBlocks(block.loop?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
871
+ // Handle try/catch
872
+ if (block.tryCatch) {
873
+ extractAdditionalStepFilesFromBlocks(block.tryCatch.try?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
874
+ extractAdditionalStepFilesFromBlocks(block.tryCatch.catch?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
875
+ extractAdditionalStepFilesFromBlocks(block.tryCatch.finally?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
876
+ }
877
+ // Handle conditional
878
+ if (block.conditional) {
879
+ extractAdditionalStepFilesFromBlocks(block.conditional.if?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
880
+ for (const elseIfBlock of block.conditional.elseIf ?? []) {
881
+ extractAdditionalStepFilesFromBlocks(elseIfBlock.blocks, additionalStepFiles, existingFilePaths, minLinesForExtraction);
882
+ }
883
+ extractAdditionalStepFilesFromBlocks(block.conditional.else?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
884
+ }
885
+ // Handle Parallel API
886
+ if (block.parallel) {
887
+ for (const path of Object.values(block.parallel?.static?.paths ?? {})) {
888
+ extractAdditionalStepFilesFromBlocks(path.blocks, additionalStepFiles, existingFilePaths, minLinesForExtraction);
889
+ }
890
+ extractAdditionalStepFilesFromBlocks(block.parallel?.dynamic?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
891
+ }
892
+ // Handle stream
893
+ if (block.stream) {
894
+ const trigger = block.stream.trigger;
895
+ if (trigger && trigger.step) {
896
+ extractAdditionalStepFilesFromStep(trigger.step, trigger.name, additionalStepFiles, existingFilePaths, minLinesForExtraction);
897
+ }
898
+ extractAdditionalStepFilesFromBlocks(block.stream.process?.blocks ?? [], additionalStepFiles, existingFilePaths, minLinesForExtraction);
899
+ }
900
+ }
901
+ }
902
+ function extractAdditionalStepFilesFromStep(step, name, additionalStepFiles, existingFilePaths, minLinesForExtraction) {
903
+ // Handle language step files
904
+ if (step && "integration" in step) {
905
+ const integration = step.integration;
906
+ const extension = LANGUAGE_STEP_EXTENSIONS[integration];
907
+ if (extension) {
908
+ const languageContent = step[integration];
909
+ if (languageContent) {
910
+ const blockBody = languageContent.body;
911
+ if (blockBody && typeof blockBody === "string") {
912
+ const lines = blockBody?.split("\n");
913
+ const linesNum = lines?.length;
914
+ if (linesNum && linesNum > minLinesForExtraction) {
915
+ // Record the separate file is needed, with the language contents
916
+ const relativePath = findUnusedFilePath(slugifyName(name), extension, existingFilePaths);
917
+ additionalStepFiles.push({
918
+ relativePath: relativePath,
919
+ contents: blockBody,
920
+ language: integration,
921
+ });
922
+ // Replace the body with a reference to the separate file
923
+ languageContent.body = {
924
+ path: `./${relativePath}`,
925
+ };
926
+ }
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+ function findUnusedFilePath(filename, extension, existingFilePaths) {
933
+ let proposedFilename = `${filename}.${extension}`;
934
+ let i = 1;
935
+ while (existingFilePaths.has(proposedFilename)) {
936
+ proposedFilename = `${filename}_${i}.${extension}`;
937
+ i++;
938
+ }
939
+ existingFilePaths.add(proposedFilename);
940
+ return proposedFilename;
941
+ }
942
+ async function validateMultiPageApplication(applicationConfig, superblocksRootPath, location) {
943
+ // validate app level APIs
944
+ const apiNames = getApiNamesFromApplicationConfig(applicationConfig);
945
+ for (const apiName of apiNames) {
946
+ const apiPath = path.resolve(superblocksRootPath, location, "apis", `${slugifyName(apiName)}.yaml`);
947
+ const validateApiError = await validateYamlFile(apiPath);
948
+ if (validateApiError) {
949
+ return validateApiError;
950
+ }
951
+ }
952
+ // validate pages
953
+ for (const page of Object.values(applicationConfig.pages ?? {})) {
954
+ const pagePath = path.resolve(superblocksRootPath, location, "pages", slugifyName(page.name), "page.yaml");
955
+ const validatePageError = await validateYamlFile(pagePath);
956
+ if (validatePageError) {
957
+ return validatePageError;
958
+ }
959
+ // validate page level APIs
960
+ const pageApiNames = getApiNamesFromPageConfig(page);
961
+ for (const apiName of pageApiNames) {
962
+ const apisPath = path.resolve(superblocksRootPath, location, "pages", slugifyName(page.name), "apis");
963
+ // Try the API path not in a nested directory
964
+ const apiPath = path.resolve(apisPath, `${slugifyName(apiName)}.yaml`);
965
+ if (fs.pathExistsSync(apiPath)) {
966
+ const validateApiError = await validateYamlFile(apiPath);
967
+ if (validateApiError) {
968
+ return validateApiError;
969
+ }
970
+ }
971
+ else {
972
+ // Try the API path in a nested directory
973
+ const nestedApiPath = path.resolve(apisPath, slugifyName(apiName), "api.yaml");
974
+ if (fs.pathExistsSync(nestedApiPath)) {
975
+ const validateApiError = await validateYamlFile(nestedApiPath);
976
+ if (validateApiError) {
977
+ return validateApiError;
978
+ }
979
+ }
980
+ }
981
+ }
982
+ }
983
+ return undefined;
984
+ }
985
+ export async function validateLocalResource(superblocksRootPath, resource) {
986
+ switch (resource.resourceType) {
987
+ case "APPLICATION": {
988
+ // make sure application config exists
989
+ const applicationConfigPath = path.resolve(superblocksRootPath, resource.location, RESOURCE_CONFIG_PATH);
990
+ if (!(await fs.pathExists(applicationConfigPath))) {
991
+ return `File ${relativeToCurrentDir(applicationConfigPath)} not found. Superblocks CLI commands cannot function without it.`;
992
+ }
993
+ let applicationConfig = undefined;
994
+ try {
995
+ // make sure it's a well-formed application config
996
+ applicationConfig = await fs.readJSON(applicationConfigPath);
997
+ if (!applicationConfig) {
998
+ throw new Error();
999
+ }
1000
+ }
1001
+ catch {
1002
+ return `File ${relativeToCurrentDir(applicationConfigPath)} is not a valid JSON file. Please be sure it's valid JSON and rerun the command.`;
1003
+ }
1004
+ const applicationYamlPath = path.resolve(superblocksRootPath, resource.location, "application.yaml");
1005
+ // make sure application.yaml is a well-formed yaml file
1006
+ try {
1007
+ await readYamlFile(applicationYamlPath);
1008
+ }
1009
+ catch {
1010
+ return `File ${relativeToCurrentDir(applicationYamlPath)} is not a valid YAML file. Please be sure it's valid YAML and rerun the command.`;
1011
+ }
1012
+ const validationError = validateMultiPageApplication(applicationConfig, superblocksRootPath, resource.location);
1013
+ if (validationError) {
1014
+ return validationError;
1015
+ }
1016
+ break;
1017
+ }
1018
+ case "BACKEND": {
1019
+ // make sure the backend config exists
1020
+ const backendConfigPath = path.resolve(superblocksRootPath, resource.location, RESOURCE_CONFIG_PATH);
1021
+ if (!(await fs.pathExists(backendConfigPath))) {
1022
+ return `File ${relativeToCurrentDir(backendConfigPath)} not found. Superblocks CLI commands cannot function without it.`;
1023
+ }
1024
+ // make sure it's a well-formed backend config
1025
+ try {
1026
+ await fs.readJSON(backendConfigPath);
1027
+ }
1028
+ catch {
1029
+ return `File ${relativeToCurrentDir(backendConfigPath)} is not a valid JSON file. Please be sure it's valid JSON and rerun the command.`;
1030
+ }
1031
+ // make sure that api.yaml exists
1032
+ const apiYamlPath = path.resolve(superblocksRootPath, resource.location, "api.yaml");
1033
+ const validateYamlFileError = await validateYamlFile(apiYamlPath);
1034
+ if (validateYamlFileError) {
1035
+ return validateYamlFileError;
1036
+ }
1037
+ break;
1038
+ }
1039
+ }
1040
+ return undefined;
1041
+ }
1042
+ export async function deleteResourcesAndUpdateRootConfig(removedResourceIds, existingSuperblocksRootConfig, superblocksRootPath, superblocksRootConfigPath) {
1043
+ for (const resourceId of removedResourceIds) {
1044
+ const resource = existingSuperblocksRootConfig?.resources[resourceId];
1045
+ await removeResourceFromDisk(superblocksRootPath, resource.location);
1046
+ }
1047
+ for (const removedResourceId of removedResourceIds) {
1048
+ delete existingSuperblocksRootConfig.resources[removedResourceId];
1049
+ }
1050
+ // update superblocks.json file with removed resources
1051
+ await fs.writeFile(superblocksRootConfigPath, JSON.stringify(sortByKey(existingSuperblocksRootConfig), null, 2));
1052
+ }
1053
+ async function validateYamlFile(yamlPath) {
1054
+ // make sure a yaml file is well-formed
1055
+ try {
1056
+ await readYamlFile(yamlPath);
1057
+ }
1058
+ catch {
1059
+ return `File ${relativeToCurrentDir(yamlPath)} is not a valid YAML file. Please be sure it's valid YAML and rerun the command.`;
1060
+ }
1061
+ }
1062
+ function relativeToCurrentDir(applicationConfigPath) {
1063
+ return `./${path.relative(process.cwd(), applicationConfigPath)}`;
1064
+ }