@superblocksteam/sdk 1.14.2 → 2.0.3-next.46

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 (152) hide show
  1. package/.mocharc.json +7 -0
  2. package/.prettierrc +18 -0
  3. package/dist/cli-replacement/dev.d.mts +19 -0
  4. package/dist/cli-replacement/dev.d.mts.map +1 -0
  5. package/dist/cli-replacement/dev.mjs +122 -0
  6. package/dist/cli-replacement/dev.mjs.map +1 -0
  7. package/dist/cli-replacement/init.d.ts +14 -0
  8. package/dist/cli-replacement/init.d.ts.map +1 -0
  9. package/dist/cli-replacement/init.js +26 -0
  10. package/dist/cli-replacement/init.js.map +1 -0
  11. package/dist/client.d.ts +31 -17
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +137 -155
  14. package/dist/client.js.map +1 -0
  15. package/dist/dbfs/client.d.ts +6 -0
  16. package/dist/dbfs/client.d.ts.map +1 -0
  17. package/dist/dbfs/client.js +117 -0
  18. package/dist/dbfs/client.js.map +1 -0
  19. package/dist/dbfs/local.d.ts +15 -0
  20. package/dist/dbfs/local.d.ts.map +1 -0
  21. package/dist/dbfs/local.js +126 -0
  22. package/dist/dbfs/local.js.map +1 -0
  23. package/dist/dev-utils/custom-build.d.mts +4 -0
  24. package/dist/dev-utils/custom-build.d.mts.map +1 -0
  25. package/dist/dev-utils/custom-build.mjs +99 -0
  26. package/dist/dev-utils/custom-build.mjs.map +1 -0
  27. package/dist/dev-utils/custom-config.d.mts +2 -0
  28. package/dist/dev-utils/custom-config.d.mts.map +1 -0
  29. package/dist/dev-utils/custom-config.mjs +57 -0
  30. package/dist/dev-utils/custom-config.mjs.map +1 -0
  31. package/dist/dev-utils/dev-logger.d.mts +8 -0
  32. package/dist/dev-utils/dev-logger.d.mts.map +1 -0
  33. package/dist/dev-utils/dev-logger.mjs +25 -0
  34. package/dist/dev-utils/dev-logger.mjs.map +1 -0
  35. package/dist/dev-utils/dev-server.d.mts +18 -0
  36. package/dist/dev-utils/dev-server.d.mts.map +1 -0
  37. package/dist/dev-utils/dev-server.mjs +265 -0
  38. package/dist/dev-utils/dev-server.mjs.map +1 -0
  39. package/dist/dev-utils/dev-tracer.d.ts +3 -0
  40. package/dist/dev-utils/dev-tracer.d.ts.map +1 -0
  41. package/dist/dev-utils/dev-tracer.js +28 -0
  42. package/dist/dev-utils/dev-tracer.js.map +1 -0
  43. package/dist/dev-utils/vite-plugin-dd-rum.d.mts +10 -0
  44. package/dist/dev-utils/vite-plugin-dd-rum.d.mts.map +1 -0
  45. package/dist/dev-utils/vite-plugin-dd-rum.mjs +34 -0
  46. package/dist/dev-utils/vite-plugin-dd-rum.mjs.map +1 -0
  47. package/dist/dev-utils/vite-plugin-react-transform.d.mts +7 -0
  48. package/dist/dev-utils/vite-plugin-react-transform.d.mts.map +1 -0
  49. package/dist/dev-utils/vite-plugin-react-transform.mjs +110 -0
  50. package/dist/dev-utils/vite-plugin-react-transform.mjs.map +1 -0
  51. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts +34 -0
  52. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts.map +1 -0
  53. package/dist/dev-utils/vite-plugin-sb-cdn.mjs +720 -0
  54. package/dist/dev-utils/vite-plugin-sb-cdn.mjs.map +1 -0
  55. package/dist/errors.d.ts +1 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +4 -9
  58. package/dist/errors.js.map +1 -0
  59. package/dist/flag.d.ts +2 -2
  60. package/dist/flag.d.ts.map +1 -0
  61. package/dist/flag.js +5 -9
  62. package/dist/flag.js.map +1 -0
  63. package/dist/index.d.ts +10 -4
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +10 -20
  66. package/dist/index.js.map +1 -0
  67. package/dist/sdk.d.ts +42 -18
  68. package/dist/sdk.d.ts.map +1 -0
  69. package/dist/sdk.js +47 -43
  70. package/dist/sdk.js.map +1 -0
  71. package/dist/socket/handlers.d.ts +3 -128
  72. package/dist/socket/handlers.d.ts.map +1 -0
  73. package/dist/socket/handlers.js +7 -9
  74. package/dist/socket/handlers.js.map +1 -0
  75. package/dist/socket/index.d.ts +4 -3
  76. package/dist/socket/index.d.ts.map +1 -0
  77. package/dist/socket/index.js +12 -21
  78. package/dist/socket/index.js.map +1 -0
  79. package/dist/socket/signing.d.ts +3 -1
  80. package/dist/socket/signing.d.ts.map +1 -0
  81. package/dist/socket/signing.js +8 -17
  82. package/dist/socket/signing.js.map +1 -0
  83. package/dist/socket/socket.d.ts +3 -2
  84. package/dist/socket/socket.d.ts.map +1 -0
  85. package/dist/socket/socket.js +9 -19
  86. package/dist/socket/socket.js.map +1 -0
  87. package/dist/types/common.d.ts +2 -103
  88. package/dist/types/common.d.ts.map +1 -0
  89. package/dist/types/common.js +8 -24
  90. package/dist/types/common.js.map +1 -0
  91. package/dist/types/index.d.ts +5 -4
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/index.js +5 -20
  94. package/dist/types/index.js.map +1 -0
  95. package/dist/types/plugin.d.ts +1 -0
  96. package/dist/types/plugin.d.ts.map +1 -0
  97. package/dist/types/plugin.js +3 -5
  98. package/dist/types/plugin.js.map +1 -0
  99. package/dist/types/signing.d.ts +2 -1
  100. package/dist/types/signing.d.ts.map +1 -0
  101. package/dist/types/signing.js +2 -2
  102. package/dist/types/signing.js.map +1 -0
  103. package/dist/types/socket.d.ts +1 -0
  104. package/dist/types/socket.d.ts.map +1 -0
  105. package/dist/types/socket.js +2 -5
  106. package/dist/types/socket.js.map +1 -0
  107. package/dist/utils.d.ts +3 -1
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/utils.js +14 -23
  110. package/dist/utils.js.map +1 -0
  111. package/dist/version-control.d.mts +59 -0
  112. package/dist/version-control.d.mts.map +1 -0
  113. package/dist/version-control.mjs +899 -0
  114. package/dist/version-control.mjs.map +1 -0
  115. package/eslint.config.js +85 -0
  116. package/package.json +72 -32
  117. package/src/cli-replacement/dev.mts +182 -0
  118. package/src/cli-replacement/init.ts +47 -0
  119. package/src/client.ts +114 -38
  120. package/src/dbfs/client.ts +162 -0
  121. package/src/dbfs/local.ts +163 -0
  122. package/src/dev-utils/custom-build.mts +113 -0
  123. package/src/dev-utils/custom-config.mts +66 -0
  124. package/src/dev-utils/dev-logger.mts +39 -0
  125. package/src/dev-utils/dev-server.mts +342 -0
  126. package/src/dev-utils/dev-tracer.ts +31 -0
  127. package/src/dev-utils/vite-plugin-dd-rum.mts +47 -0
  128. package/src/dev-utils/vite-plugin-react-transform.mts +130 -0
  129. package/src/dev-utils/vite-plugin-sb-cdn.mts +988 -0
  130. package/src/flag.ts +2 -3
  131. package/src/index.ts +119 -4
  132. package/src/sdk.ts +91 -17
  133. package/src/socket/handlers.ts +9 -147
  134. package/src/socket/index.ts +6 -9
  135. package/src/socket/signing.ts +7 -8
  136. package/src/socket/socket.ts +8 -9
  137. package/src/types/common.ts +2 -119
  138. package/src/types/index.ts +4 -4
  139. package/src/types/signing.ts +1 -1
  140. package/src/types/socket.ts +1 -1
  141. package/src/utils.ts +5 -6
  142. package/src/version-control.mts +1351 -0
  143. package/test/dev-utils/fixture/index.html +12 -0
  144. package/test/dev-utils/fixture/main.jsx +22 -0
  145. package/test/dev-utils/fixture/package-lock.json +25 -0
  146. package/test/dev-utils/fixture/package.json +9 -0
  147. package/test/dev-utils/vite-plugin-sb-cdn.test.mts +74 -0
  148. package/test/tsconfig.json +9 -0
  149. package/test/version-control.test.mts +1412 -0
  150. package/tsconfig.json +15 -4
  151. package/tsconfig.tsbuildinfo +1 -1
  152. package/.eslintrc.json +0 -55
@@ -0,0 +1,1351 @@
1
+ import * as https from "https";
2
+ import path from "node:path";
3
+ import { dirname } from "path";
4
+ import { ExportViewMode } from "@superblocksteam/shared";
5
+ import {
6
+ RESOURCE_CONFIG_PATH,
7
+ SUPERBLOCKS_HOME_FOLDER_NAME,
8
+ getSuperblocksApplicationConfigIfExists,
9
+ getSuperblocksApplicationConfigJson,
10
+ getSuperblocksBackendConfigIfExists,
11
+ getSuperblocksApplicationV2ConfigJson,
12
+ readAppApiYamlFile,
13
+ writeApiFiles,
14
+ DEFAULT_LINES_FOR_LARGE_STEPS,
15
+ } from "@superblocksteam/util";
16
+ import { bold } from "colorette";
17
+ import fs from "fs-extra";
18
+ import { isEmpty, isArray, isObject, get } from "lodash-es";
19
+ import * as semver from "semver";
20
+ import { simpleGit } from "simple-git";
21
+ import slugify from "slugify";
22
+ import { parse, stringify as ymlstringify } from "yaml";
23
+ import type {
24
+ ApplicationWrapper,
25
+ MultiPageApplicationWrapper,
26
+ MultiPageApplicationWrapperWithComponents,
27
+ CodeModeApplicationWrapper,
28
+ } from "./client.js";
29
+ import type { FeatureFlags } from "./flag.js";
30
+ import type { SuperblocksSdk } from "./sdk.js";
31
+ import type { ApiWithPb, Page } from "./types/index.js";
32
+ import type {
33
+ LocalGitRepoState,
34
+ SuperblocksBackendConfig,
35
+ SuperblocksMonorepoConfig,
36
+ SuperblocksApplicationConfig,
37
+ SuperblocksResourceConfig,
38
+ SuperblocksResourceConfigMetadata,
39
+ VersionedResourceConfig,
40
+ ApiInfo,
41
+ PageInfo,
42
+ SuperblocksApplicationV2Config,
43
+ ApiWrapper,
44
+ AdditionalStepFiles,
45
+ } from "@superblocksteam/util";
46
+ import type { SimpleGit, StatusResult } from "simple-git";
47
+
48
+ export const LATEST_EDITS_MODE = "latest-edits";
49
+ export const MOST_RECENT_COMMIT_MODE = "most-recent-commit";
50
+ export const DEPLOYED_MODE = "deployed";
51
+
52
+ export const DEFAULT_BRANCH = "main";
53
+
54
+ export const modeFlagValuesMap: Record<string, string> = {
55
+ [LATEST_EDITS_MODE]: "Latest edits",
56
+ [MOST_RECENT_COMMIT_MODE]: "Most recent commit",
57
+ [DEPLOYED_MODE]: "Deployed",
58
+ };
59
+
60
+ export enum FileStructureType {
61
+ SINGLE_PAGE = "single-page",
62
+ MULTI_PAGE = "multi-page",
63
+ }
64
+
65
+ export function modeFlagToViewMode(modeFlag: ModeFlag): ExportViewMode {
66
+ switch (modeFlag) {
67
+ case LATEST_EDITS_MODE:
68
+ return ExportViewMode.EXPORT_LIVE;
69
+ case MOST_RECENT_COMMIT_MODE:
70
+ return ExportViewMode.EXPORT_LATEST;
71
+ case DEPLOYED_MODE:
72
+ return ExportViewMode.EXPORT_DEPLOYED;
73
+ default:
74
+ throw new Error(`Unsupported mode flag: ${modeFlag}`);
75
+ }
76
+ }
77
+
78
+ export type ModeFlag = (keyof typeof modeFlagValuesMap)[number];
79
+
80
+ export const SELECT_PROMPT_HELP = "Use ↑/↓ arrow keys, Enter to confirm";
81
+ export const MULTI_SELECT_PROMPT_HELP =
82
+ "Type to filter, Use ↑/↓ arrow keys, Space to select, Enter to confirm";
83
+
84
+ export const atLeastOneSelection = (value: string[]) => {
85
+ if (isEmpty(value)) {
86
+ return `Please select at least one item ${bold("by pressing space")}`;
87
+ }
88
+
89
+ return true;
90
+ };
91
+
92
+ export type ApiRepresentation = {
93
+ extractLargeSourceFiles: boolean;
94
+ minLinesForExtraction: number;
95
+ };
96
+
97
+ const DEFAULT_FILE_VERSION = "0.1.0";
98
+ const SPLIT_LARGE_API_STEPS_VERSION = "0.2.0";
99
+ const LATEST_FILE_VERSION = SPLIT_LARGE_API_STEPS_VERSION;
100
+
101
+ export function getApiRepresentation(
102
+ featureFlags: FeatureFlags,
103
+ resourceConfig: SuperblocksResourceConfig,
104
+ ): ApiRepresentation {
105
+ const linesForLargeSteps =
106
+ featureFlags.linesForLargeSteps() ?? DEFAULT_LINES_FOR_LARGE_STEPS;
107
+ if (
108
+ featureFlags.splitLargeApiStepsEnabled() &&
109
+ isPostSplitLargeApiSteps(resourceConfig)
110
+ ) {
111
+ return {
112
+ extractLargeSourceFiles: true,
113
+ minLinesForExtraction: linesForLargeSteps,
114
+ };
115
+ }
116
+ // Return the default
117
+ return {
118
+ extractLargeSourceFiles: false,
119
+ minLinesForExtraction: linesForLargeSteps,
120
+ };
121
+ }
122
+
123
+ function isPostSplitLargeApiSteps(
124
+ resourceConfig: SuperblocksResourceConfig | undefined,
125
+ ) {
126
+ if (!resourceConfig) {
127
+ return false;
128
+ }
129
+ const version = resourceConfig.metadata?.fileVersion ?? DEFAULT_FILE_VERSION;
130
+ return semver.compare(version, SPLIT_LARGE_API_STEPS_VERSION) >= 0;
131
+ }
132
+
133
+ function slugifyName(originalName: any): string {
134
+ return (slugify as any)(originalName, {
135
+ replacement: "_",
136
+ lower: true,
137
+ });
138
+ }
139
+
140
+ // resolves a true promise when download is complete. If there's an error, resolve false
141
+ async function downloadFile(
142
+ rootDirectory: string,
143
+ filepath: string,
144
+ url: string,
145
+ ): Promise<string> {
146
+ const fullPath = `${rootDirectory}/${filepath}`;
147
+ // eslint-disable-next-line no-async-promise-executor
148
+ const result = await new Promise<boolean>(async (resolve) => {
149
+ try {
150
+ // create directory path if it doesn't exist yet
151
+ if (!(await fs.pathExists(fullPath))) {
152
+ await fs.mkdir(dirname(fullPath), { recursive: true });
153
+ }
154
+ const file = fs.createWriteStream(fullPath);
155
+ https.get(url, (resp) => {
156
+ resp.pipe(file);
157
+ file
158
+ .on("finish", () => {
159
+ file.end(() => resolve(true));
160
+ })
161
+ .on("error", () => {
162
+ fs.unlink(filepath);
163
+ resolve(false);
164
+ });
165
+ });
166
+ } catch {
167
+ return resolve(false);
168
+ }
169
+ });
170
+
171
+ // failed to download correctly, attempt to clean up file
172
+ if (!result) {
173
+ try {
174
+ await fs.unlink(fullPath);
175
+ } catch {
176
+ console.log("Failed to delete file", fullPath);
177
+ }
178
+
179
+ return Promise.resolve(fullPath);
180
+ }
181
+ return Promise.resolve("");
182
+ }
183
+
184
+ async function readYamlFile(path: string): Promise<any> {
185
+ return parse(await fs.readFile(path, "utf8"));
186
+ }
187
+
188
+ function getFileStructureTypeFromResourceConfig(
189
+ superblocksConfig: SuperblocksApplicationConfig,
190
+ ) {
191
+ if (superblocksConfig.pages) {
192
+ return FileStructureType.MULTI_PAGE;
193
+ }
194
+ return FileStructureType.SINGLE_PAGE;
195
+ }
196
+
197
+ export async function getFileStructureType(
198
+ rootPath: string,
199
+ existingRelativeLocation: string,
200
+ ): Promise<FileStructureType> {
201
+ try {
202
+ const superblocksConfig = await getSuperblocksApplicationConfigJson(
203
+ `${rootPath}/${existingRelativeLocation}`,
204
+ );
205
+ return getFileStructureTypeFromResourceConfig(superblocksConfig);
206
+ } catch {
207
+ await getSuperblocksApplicationV2ConfigJson(
208
+ `${rootPath}/${existingRelativeLocation}`,
209
+ );
210
+ return FileStructureType.MULTI_PAGE;
211
+ }
212
+ }
213
+
214
+ // NOTE: If a change is made to how applications are read from disk, please update
215
+ // logic to write applications to disk in the "writeResourceToDisk" function accordingly.
216
+ // @deprecated this can be removed once all customers move to multi page applications
217
+ export async function readApplicationFromDisk(
218
+ rootPath: string,
219
+ existingRelativeLocation: string,
220
+ ): Promise<ApplicationWrapper> {
221
+ const application = await readYamlFile(
222
+ `${rootPath}/${existingRelativeLocation}/application.yaml`,
223
+ );
224
+
225
+ const page = await readYamlFile(
226
+ `${rootPath}/${existingRelativeLocation}/page.yaml`,
227
+ );
228
+
229
+ const apisDirName = `${rootPath}/${existingRelativeLocation}/apis`;
230
+ const apis: any[] = [];
231
+
232
+ if (await fs.pathExists(apisDirName)) {
233
+ const apiFiles = await fs.readdir(apisDirName);
234
+ for (const apiFile of apiFiles) {
235
+ const { api: apiContent } = await readAppApiYamlFile(
236
+ apisDirName,
237
+ apiFile,
238
+ );
239
+ // This mimics the shape of the ApiV3Dto object
240
+ apis.push({
241
+ id: apiContent.metadata.id,
242
+ apiPb: apiContent,
243
+ });
244
+ }
245
+ }
246
+
247
+ return {
248
+ application,
249
+ apis,
250
+ page,
251
+ };
252
+ }
253
+
254
+ // NOTE: If a change is made to how applications are read from disk, please update
255
+ // logic to write applications to disk in the "writeResourceToDisk" function accordingly.
256
+ export async function readMultiPageApplicationFromDisk(
257
+ rootPath: string,
258
+ existingRelativeLocation: string,
259
+ ): Promise<MultiPageApplicationWrapper> {
260
+ const superblocksApplicationConfig =
261
+ await getSuperblocksApplicationConfigJson(
262
+ `${rootPath}/${existingRelativeLocation}`,
263
+ );
264
+ const application = await readYamlFile(
265
+ `${rootPath}/${existingRelativeLocation}/application.yaml`,
266
+ );
267
+
268
+ const pagesDirName = `${rootPath}/${existingRelativeLocation}/pages`;
269
+ const pages: Page[] = [];
270
+
271
+ if (await fs.pathExists(pagesDirName)) {
272
+ for (const page of Object.values(
273
+ superblocksApplicationConfig.pages ?? {},
274
+ )) {
275
+ // Read in the page definition
276
+ const pageContent = await readYamlFile(
277
+ `${pagesDirName}/${slugifyName(page.name)}/page.yaml`,
278
+ );
279
+
280
+ // Read in the API definitions for this page
281
+ const pageApisDirName = `${pagesDirName}/${slugifyName(page.name)}/apis`;
282
+ const apis: ApiWithPb[] = [];
283
+
284
+ if (await fs.pathExists(pageApisDirName)) {
285
+ // Get the API names from pageApis if it exists, or from apis if it doesn't
286
+ const pageApiNames = getApiNamesFromPageConfig(page);
287
+ for (const apiName of pageApiNames) {
288
+ const { api: apiContent } = await readAppApiYamlFile(
289
+ pageApisDirName,
290
+ apiName,
291
+ );
292
+ // This mimics the shape of the ApiV3Dto object
293
+ apis.push({
294
+ id: apiContent.metadata.id,
295
+ pageId: page.id, // Alternatively, pageId is also stored in apiPb.trigger.application.pageId
296
+ apiPb: apiContent,
297
+ });
298
+ }
299
+ }
300
+ pages.push({
301
+ id: pageContent.id,
302
+ name: pageContent.name,
303
+ applicationId: pageContent.applicationId,
304
+ isHidden: pageContent.isHidden,
305
+ layouts: pageContent.layouts,
306
+ apis,
307
+ });
308
+ }
309
+ }
310
+
311
+ const appApisDirName = `${rootPath}/${existingRelativeLocation}/apis`;
312
+ const apis: any[] = [];
313
+
314
+ if (await fs.pathExists(appApisDirName)) {
315
+ // Get the API names from appApis if it exists, or from apis if it doesn't
316
+ const appConfig = superblocksApplicationConfig;
317
+ const apiNames = getApiNamesFromApplicationConfig(appConfig);
318
+ for (const apiName of apiNames) {
319
+ const { api: apiContent } = await readAppApiYamlFile(
320
+ appApisDirName,
321
+ apiName,
322
+ );
323
+ // This mimics the shape of the ApiV3Dto object
324
+ apis.push({
325
+ id: apiContent.metadata.id,
326
+ apiPb: apiContent,
327
+ });
328
+ }
329
+ }
330
+
331
+ return {
332
+ application,
333
+ apis,
334
+ pages,
335
+ };
336
+ }
337
+
338
+ function getApiNamesFromApplicationConfig(
339
+ applicationConfig: SuperblocksApplicationConfig,
340
+ ): string[] {
341
+ return applicationConfig.appApis
342
+ ? Object.values(applicationConfig.appApis).map((api) => api.name)
343
+ : Object.values(applicationConfig.apis ?? {});
344
+ }
345
+
346
+ function getApiNamesFromPageConfig(pageConfig: PageInfo): string[] {
347
+ return pageConfig.pageApis
348
+ ? Object.values(pageConfig.pageApis).map((api) => api.name)
349
+ : Object.values(pageConfig.apis ?? {});
350
+ }
351
+
352
+ export async function readApiFromDisk(
353
+ rootPath: string,
354
+ existingRelativeLocation: string,
355
+ ): Promise<ApiWrapper> {
356
+ const path = `${rootPath}/${existingRelativeLocation}`;
357
+ const { api: apiContent } = await readAppApiYamlFile(path);
358
+ return {
359
+ apiPb: apiContent,
360
+ };
361
+ }
362
+
363
+ // NOTE: If a change is made to how applications are written to disk, please update
364
+ // logic to read applications from disk in the "readApplicationFromDisk" function accordingly.
365
+ export async function writeResourceToDisk(
366
+ resourceType: string,
367
+ resourceId: string,
368
+ resource: any,
369
+ rootPath: string,
370
+ featureFlags: FeatureFlags,
371
+ existingRelativeLocation?: string,
372
+ preferredApiRepresentation?: ApiRepresentation,
373
+ ): Promise<VersionedResourceConfig> {
374
+ switch (resourceType) {
375
+ case "APPLICATION": {
376
+ const parentDirName = "apps";
377
+ const newRelativeLocation = `${parentDirName}/${slugifyName(
378
+ resource.application.name,
379
+ )}`;
380
+ const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
381
+ const appDirName = path.resolve(rootPath, relativeLocation);
382
+ if (!(await fs.pathExists(appDirName))) {
383
+ await fs.mkdir(appDirName, { recursive: true });
384
+ }
385
+
386
+ const applicationContent = ymlstringify(resource.application, {
387
+ sortMapEntries: true,
388
+ });
389
+ await fs.outputFile(`${appDirName}/application.yaml`, applicationContent);
390
+ if (resource.page) {
391
+ const pageContent = ymlstringify(resource.page, {
392
+ sortMapEntries: true,
393
+ blockQuote: "literal",
394
+ });
395
+ await fs.outputFile(`${appDirName}/page.yaml`, pageContent);
396
+ }
397
+
398
+ const apiPromises: Array<Promise<void>> = [];
399
+ const apisDirName = `${appDirName}/apis`;
400
+ await fs.ensureDir(apisDirName);
401
+
402
+ const existingApplicationConfig =
403
+ await getSuperblocksApplicationConfigIfExists(appDirName);
404
+ const existingFilePaths = getExistingFilePathsForApplicationApi(
405
+ existingApplicationConfig,
406
+ appDirName,
407
+ );
408
+
409
+ const newApplicationConfig: SuperblocksApplicationConfig = {
410
+ configType: "APPLICATION",
411
+ defaultPageId: resource.page?.id,
412
+ id: resource.application.id,
413
+ metadata: getResourceConfigMetadata(
414
+ featureFlags,
415
+ existingApplicationConfig,
416
+ ),
417
+ };
418
+
419
+ const apiRepresentation =
420
+ preferredApiRepresentation ??
421
+ getApiRepresentation(featureFlags, newApplicationConfig);
422
+
423
+ if (resource.apis) {
424
+ for (const api of resource.apis as ApiWrapper[]) {
425
+ const apiInfo = await writeAppApi(
426
+ api,
427
+ appDirName,
428
+ existingFilePaths,
429
+ apiPromises,
430
+ apiRepresentation,
431
+ );
432
+ if (apiRepresentation.extractLargeSourceFiles) {
433
+ if (!newApplicationConfig.appApis) {
434
+ newApplicationConfig.appApis = {};
435
+ }
436
+ newApplicationConfig.appApis[api.apiPb.metadata.id] = apiInfo;
437
+ } else {
438
+ if (!newApplicationConfig.apis) {
439
+ newApplicationConfig.apis = {};
440
+ }
441
+ newApplicationConfig.apis[api.apiPb.metadata.id] = apiInfo.name;
442
+ }
443
+ }
444
+
445
+ await Promise.all(apiPromises);
446
+ }
447
+
448
+ // Delete any existing files that were not overwritten
449
+ for (const filePath of existingFilePaths) {
450
+ await fs.remove(filePath);
451
+ }
452
+
453
+ await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
454
+
455
+ await fs.writeFile(
456
+ `${appDirName}/${RESOURCE_CONFIG_PATH}`,
457
+ JSON.stringify(sortByKey(newApplicationConfig), null, 2),
458
+ );
459
+
460
+ const createdFiles = await Promise.resolve<string[]>(
461
+ // Defensive check for when application settings are missing componentFiles
462
+ resource.componentFiles?.map(
463
+ (file: { filename: string; url: string }) =>
464
+ downloadFile(appDirName, file.filename, file.url),
465
+ ) ?? [],
466
+ );
467
+
468
+ // print out failed downloads synchronously here
469
+ createdFiles
470
+ .filter((createdFiles) => createdFiles.length)
471
+ .forEach((createdFile) => {
472
+ console.log(`Unable to download ${createdFile}`);
473
+ });
474
+
475
+ return {
476
+ location: relativeLocation,
477
+ resourceType: "APPLICATION",
478
+ };
479
+ }
480
+
481
+ case "BACKEND": {
482
+ const parentDirName = "backends";
483
+ const apiName = slugifyName(extractApiName(resource));
484
+ const newRelativeLocation = `${parentDirName}/${apiName}`;
485
+ const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
486
+ const backendDirName = path.resolve(rootPath, relativeLocation);
487
+ if (!(await fs.pathExists(backendDirName))) {
488
+ await fs.mkdir(backendDirName, { recursive: true });
489
+ }
490
+
491
+ const existingBackendConfig =
492
+ await getSuperblocksBackendConfigIfExists(backendDirName);
493
+ const existingFilePaths = getExistingFilePathsForBackendApi(
494
+ existingBackendConfig,
495
+ backendDirName,
496
+ );
497
+ const backendConfig: SuperblocksBackendConfig = {
498
+ id: resourceId,
499
+ configType: "BACKEND",
500
+ metadata: getResourceConfigMetadata(
501
+ featureFlags,
502
+ existingBackendConfig,
503
+ ),
504
+ };
505
+
506
+ const apiRepresentation =
507
+ preferredApiRepresentation ??
508
+ getApiRepresentation(featureFlags, backendConfig);
509
+
510
+ // Write the API file(s)
511
+ const apiInfo = await writeBackendApi(
512
+ resource,
513
+ backendDirName,
514
+ [],
515
+ apiRepresentation,
516
+ existingFilePaths,
517
+ );
518
+ if (apiRepresentation.extractLargeSourceFiles) {
519
+ backendConfig.sourceFiles = apiInfo.sourceFiles;
520
+ }
521
+
522
+ // Write the backend config file
523
+ await fs.ensureDir(`${backendDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
524
+ await fs.writeFile(
525
+ `${backendDirName}/${RESOURCE_CONFIG_PATH}`,
526
+ JSON.stringify(sortByKey(backendConfig), null, 2),
527
+ );
528
+
529
+ // Delete any existing files that were not overwritten
530
+ for (const filePath of existingFilePaths) {
531
+ await fs.remove(filePath);
532
+ }
533
+
534
+ return {
535
+ location: relativeLocation,
536
+ resourceType: "BACKEND",
537
+ };
538
+ }
539
+
540
+ default: {
541
+ throw new Error(`Unsupported resource type: ${resourceType}`);
542
+ }
543
+ }
544
+ }
545
+
546
+ // NOTE: If a change is made to how applications are written to disk, please update
547
+ // logic to read applications from disk in the "readMultiPageApplicationFromDisk" function accordingly.
548
+ export async function writeApplicationToDisk({
549
+ sdk,
550
+ resource,
551
+ projectRootFolder,
552
+ appRelativePath,
553
+ featureFlags,
554
+ migrateFromSinglePage,
555
+ preferredApiRepresentation,
556
+ }: {
557
+ sdk: SuperblocksSdk;
558
+ resource:
559
+ | MultiPageApplicationWrapperWithComponents
560
+ | CodeModeApplicationWrapper;
561
+ projectRootFolder: string;
562
+ appRelativePath?: string;
563
+ featureFlags: FeatureFlags;
564
+ migrateFromSinglePage?: boolean;
565
+ preferredApiRepresentation?: ApiRepresentation;
566
+ }): Promise<VersionedResourceConfig> {
567
+ const parentDirName = "apps";
568
+ const newRelativeLocation = `${parentDirName}/${slugifyName(
569
+ resource.application.name,
570
+ )}`;
571
+ const relativeLocation = appRelativePath ?? newRelativeLocation;
572
+ const appDirName = path.resolve(projectRootFolder, relativeLocation);
573
+ if (!(await fs.pathExists(appDirName))) {
574
+ await fs.mkdir(appDirName, { recursive: true });
575
+ }
576
+
577
+ if (resource.type === "code-mode") {
578
+ await writeCodeModeApplicationToDisk(
579
+ sdk,
580
+ resource,
581
+ appDirName,
582
+ featureFlags,
583
+ );
584
+ return {
585
+ location: relativeLocation,
586
+ resourceType: "APPLICATION" as const,
587
+ };
588
+ } else {
589
+ return await writeV1ApplicationToDisk(
590
+ resource,
591
+ appDirName,
592
+ featureFlags,
593
+ relativeLocation,
594
+ migrateFromSinglePage,
595
+ preferredApiRepresentation,
596
+ );
597
+ }
598
+ }
599
+
600
+ async function writeV1ApplicationToDisk(
601
+ resource: MultiPageApplicationWrapperWithComponents,
602
+ appDirName: string,
603
+ featureFlags: FeatureFlags,
604
+ relativeLocation: string,
605
+ migrateFromSinglePage?: boolean,
606
+ preferredApiRepresentation?: ApiRepresentation,
607
+ ) {
608
+ const applicationContent = ymlstringify(resource.application, {
609
+ sortMapEntries: true,
610
+ });
611
+ await fs.outputFile(`${appDirName}/application.yaml`, applicationContent);
612
+
613
+ // Find the existing application config and existing file paths
614
+ const existingApplicationConfig =
615
+ await getSuperblocksApplicationConfigIfExists(appDirName);
616
+ const existingFilePaths = getExistingFilePathsForApplicationApi(
617
+ existingApplicationConfig,
618
+ appDirName,
619
+ );
620
+
621
+ const newApplicationConfig: SuperblocksApplicationConfig = {
622
+ configType: "APPLICATION",
623
+ id: resource.application.id,
624
+ metadata: getResourceConfigMetadata(
625
+ featureFlags,
626
+ existingApplicationConfig,
627
+ ),
628
+ };
629
+
630
+ const apiRepresentation =
631
+ preferredApiRepresentation ??
632
+ getApiRepresentation(featureFlags, newApplicationConfig);
633
+
634
+ newApplicationConfig.pages = {};
635
+
636
+ const apiPromises: Array<Promise<void>> = [];
637
+
638
+ for (const page of Object.values(resource.pages)) {
639
+ const pageId = page.id;
640
+ const pageDirName = `${appDirName}/pages/${slugifyName(page.name)}`;
641
+ if (!(await fs.pathExists(pageDirName))) {
642
+ await fs.mkdir(pageDirName, { recursive: true });
643
+ }
644
+ newApplicationConfig.pages[pageId] = {
645
+ id: page.id,
646
+ name: page.name,
647
+ };
648
+ const pageConfig = newApplicationConfig.pages[pageId];
649
+
650
+ const pageContent = ymlstringify(
651
+ {
652
+ id: page.id,
653
+ name: page.name,
654
+ applicationId: page.applicationId,
655
+ isHidden: page.isHidden,
656
+ layouts: page.layouts,
657
+ },
658
+ {
659
+ sortMapEntries: true,
660
+ blockQuote: "literal",
661
+ },
662
+ );
663
+ const pageFilePath = `${pageDirName}/page.yaml`;
664
+ await fs.outputFile(pageFilePath, pageContent);
665
+ existingFilePaths.delete(pageFilePath);
666
+ for (const api of page.apis as ApiWrapper[]) {
667
+ const apiInfo = await writeAppApi(
668
+ api,
669
+ pageDirName,
670
+ existingFilePaths,
671
+ apiPromises,
672
+ apiRepresentation,
673
+ );
674
+ // Update the page config with the new api info
675
+ const apiId = api.apiPb.metadata.id;
676
+ if (apiRepresentation.extractLargeSourceFiles) {
677
+ // Add the new pageApis field whenever large source files might be extracted
678
+ if (!pageConfig.pageApis) {
679
+ pageConfig.pageApis = {};
680
+ }
681
+ pageConfig.pageApis[apiId] = {
682
+ name: apiInfo.name,
683
+ sourceFiles: apiInfo.sourceFiles?.sort(),
684
+ };
685
+ } else {
686
+ if (!pageConfig.apis) {
687
+ pageConfig.apis = {};
688
+ }
689
+ pageConfig.apis[apiId] = apiInfo.name;
690
+ }
691
+ }
692
+ }
693
+
694
+ const appApis: Record<string, ApiInfo> = {};
695
+ if (resource.apis && resource.apis.length) {
696
+ for (const api of resource.apis as ApiWrapper[]) {
697
+ const apiInfo = await writeAppApi(
698
+ api,
699
+ appDirName,
700
+ existingFilePaths,
701
+ apiPromises,
702
+ apiRepresentation,
703
+ );
704
+ const apiId = api.apiPb.metadata.id;
705
+ if (apiRepresentation.extractLargeSourceFiles) {
706
+ if (!newApplicationConfig.appApis) {
707
+ newApplicationConfig.appApis = {};
708
+ }
709
+ newApplicationConfig.appApis[apiId] = apiInfo;
710
+ } else {
711
+ if (!newApplicationConfig.apis) {
712
+ newApplicationConfig.apis = {};
713
+ }
714
+ newApplicationConfig.apis[apiId] = apiInfo.name;
715
+ }
716
+ }
717
+ await Promise.all(apiPromises);
718
+ }
719
+ if (apiRepresentation.extractLargeSourceFiles) {
720
+ newApplicationConfig.appApis = appApis;
721
+ } else if (!newApplicationConfig.apis) {
722
+ // Make sure there is an empty object even if there are no APIs
723
+ newApplicationConfig.apis = {};
724
+ }
725
+
726
+ // Delete any existing files that were not overwritten
727
+ for (const filePath of existingFilePaths) {
728
+ await fs.remove(filePath);
729
+ }
730
+
731
+ await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
732
+
733
+ await fs.writeFile(
734
+ `${appDirName}/${RESOURCE_CONFIG_PATH}`,
735
+ JSON.stringify(sortByKey(newApplicationConfig), null, 2),
736
+ );
737
+
738
+ const createdFiles = await Promise.resolve<string[]>(
739
+ // Defensive check for when application settings are missing componentFiles
740
+ resource.componentFiles?.map((file: { filename: string; url: string }) =>
741
+ downloadFile(appDirName, file.filename, file.url),
742
+ ) ?? [],
743
+ );
744
+
745
+ // print out failed downloads synchronously here
746
+ createdFiles
747
+ .filter((createdFiles) => createdFiles.length)
748
+ .forEach((createdFile) => {
749
+ console.log(`Unable to download ${createdFile}`);
750
+ });
751
+
752
+ // do a post-migration cleanup if necessary
753
+ if (migrateFromSinglePage) {
754
+ await fs.remove(`${appDirName}/page.yaml`);
755
+ // delete app-level apis if they are not present in the new application config
756
+ if (!resource.apis || !resource.apis.length) {
757
+ await fs.remove(`${appDirName}/apis`);
758
+ }
759
+ }
760
+
761
+ return {
762
+ location: relativeLocation,
763
+ resourceType: "APPLICATION" as const,
764
+ };
765
+ }
766
+
767
+ async function writeCodeModeApplicationToDisk(
768
+ sdk: SuperblocksSdk,
769
+ resource: CodeModeApplicationWrapper,
770
+ appDirName: string,
771
+ featureFlags: FeatureFlags,
772
+ ): Promise<void> {
773
+ // Find the existing application config and existing file paths
774
+ const existingApplicationConfig =
775
+ await getSuperblocksApplicationConfigIfExists(appDirName);
776
+
777
+ const newApplicationConfig: SuperblocksApplicationV2Config = {
778
+ configType: "APPLICATION_V2",
779
+ id: resource.application.id,
780
+ metadata: getResourceConfigMetadata(
781
+ featureFlags,
782
+ existingApplicationConfig,
783
+ ),
784
+ };
785
+
786
+ await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
787
+
788
+ await fs.writeFile(
789
+ `${appDirName}/${RESOURCE_CONFIG_PATH}`,
790
+ JSON.stringify(sortByKey(newApplicationConfig), null, 2),
791
+ );
792
+
793
+ // This writes to DBFS
794
+ try {
795
+ await sdk.dbfsGetApplication({
796
+ applicationId: resource.application.id,
797
+ branch: DEFAULT_BRANCH,
798
+ localDirectoryPath: appDirName,
799
+ });
800
+ } catch (e) {
801
+ // This is most likely to happen if the application is not initialized
802
+ console.log(e);
803
+ throw e;
804
+ }
805
+ }
806
+
807
+ function getExistingFilePathsForApplicationApi(
808
+ superblocksApplicationConfig: SuperblocksApplicationConfig | undefined,
809
+ location: string,
810
+ ): Set<string> {
811
+ const paths = new Set<string>();
812
+ if (!superblocksApplicationConfig) {
813
+ return paths;
814
+ }
815
+ // Pages
816
+ if (superblocksApplicationConfig.pages) {
817
+ for (const page of Object.values(superblocksApplicationConfig.pages)) {
818
+ // Page YAML
819
+ const pageNameSlug = slugifyName(page.name);
820
+ const pagePath = `${location}/pages/${pageNameSlug}`;
821
+ paths.add(`${pagePath}/page.yaml`);
822
+
823
+ // Page APIs
824
+ if (page.pageApis) {
825
+ for (const api of Object.values(page.pageApis)) {
826
+ addExistingFilePathsForApi(api, `${pagePath}/apis`, paths, true);
827
+ }
828
+ } else if (page.apis) {
829
+ for (const apiName of Object.values(page.apis)) {
830
+ addExistingFilePathsForApi(
831
+ { name: apiName },
832
+ `${pagePath}/apis`,
833
+ paths,
834
+ true,
835
+ );
836
+ }
837
+ }
838
+ }
839
+ }
840
+ // APIs
841
+ if (superblocksApplicationConfig.appApis) {
842
+ // For file version >= 0.2.0, there are API files either the 'apis' folder or in an API-specific folder
843
+ for (const api of Object.values(superblocksApplicationConfig.appApis)) {
844
+ addExistingFilePathsForApi(api, `${location}/apis`, paths, true);
845
+ }
846
+ } else if (superblocksApplicationConfig.apis) {
847
+ // For file version < 0.2.0, there are only API file
848
+ for (const apiName of Object.values(superblocksApplicationConfig.apis)) {
849
+ addExistingFilePathsForApi(
850
+ { name: apiName },
851
+ `${location}/apis`,
852
+ paths,
853
+ true,
854
+ );
855
+ }
856
+ }
857
+ return paths;
858
+ }
859
+
860
+ function getExistingFilePathsForBackendApi(
861
+ superblocksBackendConfig: SuperblocksBackendConfig | undefined,
862
+ location: string,
863
+ ): Set<string> {
864
+ const paths = new Set<string>();
865
+ if (!superblocksBackendConfig) {
866
+ return paths;
867
+ }
868
+ // Pages
869
+ if (superblocksBackendConfig.sourceFiles) {
870
+ addExistingFilePathsForApi(
871
+ { name: "api", sourceFiles: superblocksBackendConfig.sourceFiles },
872
+ location,
873
+ paths,
874
+ false,
875
+ );
876
+ } else {
877
+ addExistingFilePathsForApi({ name: "api" }, location, paths, false);
878
+ }
879
+ return paths;
880
+ }
881
+
882
+ export function addExistingFilePathsForApi(
883
+ api: ApiInfo,
884
+ location: string,
885
+ paths: Set<string>,
886
+ useNestedFolder: boolean,
887
+ ) {
888
+ const apiNameSlug = slugifyName(api.name);
889
+ if (api.sourceFiles) {
890
+ // File version 0.2.0 and later
891
+ // API files are in an API-specific folder
892
+ const apiDirPath = useNestedFolder
893
+ ? `${location}/${apiNameSlug}`
894
+ : location;
895
+ paths.add(`${apiDirPath}/api.yaml`);
896
+ // And there are source files
897
+ for (const sourceFile of api.sourceFiles ?? []) {
898
+ paths.add(`${apiDirPath}/${sourceFile}`);
899
+ }
900
+ } else {
901
+ // Pre-file version 0.2.0
902
+ // API file is in the 'apis' folder with no separate source files
903
+ paths.add(`${location}/${apiNameSlug}.yaml`);
904
+ }
905
+ return paths;
906
+ }
907
+
908
+ function getResourceConfigMetadata(
909
+ featureFlags: FeatureFlags,
910
+ existingResourceConfig: SuperblocksResourceConfig | undefined,
911
+ ): SuperblocksResourceConfigMetadata | undefined {
912
+ if (!existingResourceConfig) {
913
+ // This is a new application, and we may need to add a metadata field to the application config
914
+ if (featureFlags.splitLargeApiStepsInNewEnabled()) {
915
+ return {
916
+ fileVersion: LATEST_FILE_VERSION,
917
+ };
918
+ }
919
+ } else if (existingResourceConfig.metadata) {
920
+ return existingResourceConfig.metadata;
921
+ }
922
+ return undefined;
923
+ }
924
+
925
+ export async function removeResourceFromDisk(
926
+ rootPath: string,
927
+ resourceRelativeLocation: string,
928
+ ) {
929
+ const resourceLocation = path.resolve(rootPath, resourceRelativeLocation);
930
+ await fs.remove(resourceLocation);
931
+ }
932
+
933
+ export async function getMode(
934
+ task: any,
935
+ mode: ModeFlag,
936
+ ): Promise<ExportViewMode> {
937
+ if (mode) {
938
+ return modeFlagToViewMode(mode);
939
+ }
940
+
941
+ const selectedMode = await task.prompt([
942
+ {
943
+ type: "Select",
944
+ name: "mode",
945
+ message: `Select which version of a resource you would like to fetch (${SELECT_PROMPT_HELP})`,
946
+ choices: Object.keys(modeFlagValuesMap).map((mode: string) => ({
947
+ name: mode,
948
+ message: modeFlagValuesMap[mode],
949
+ })),
950
+ initial: 0,
951
+ multiple: false,
952
+ },
953
+ ]);
954
+
955
+ return modeFlagToViewMode(selectedMode);
956
+ }
957
+
958
+ export function sortByKey(obj: unknown): unknown {
959
+ if (isArray(obj)) {
960
+ return obj.map((item) => sortByKey(item));
961
+ }
962
+
963
+ if (isObject(obj)) {
964
+ const sortedKeys = Object.keys(obj).sort();
965
+ const sortedObj: Record<string, any> = {};
966
+ for (const key of sortedKeys) {
967
+ sortedObj[key] = sortByKey(get(obj, key));
968
+ }
969
+
970
+ return sortedObj;
971
+ }
972
+
973
+ return obj;
974
+ }
975
+
976
+ export async function getLocalGitRepoState(
977
+ overrideLocalBranch?: string,
978
+ ): Promise<LocalGitRepoState> {
979
+ const git = simpleGit();
980
+
981
+ let status: StatusResult;
982
+ try {
983
+ // do not return untracked files which can be slow, we only care about the branch info
984
+ status = await git.status(["--untracked-files=no"]);
985
+ } catch {
986
+ return { status: "NO_GIT" };
987
+ }
988
+
989
+ let localBranchName: string;
990
+ if (overrideLocalBranch) {
991
+ const branches = await git.branch();
992
+ if (!branches.all.includes(overrideLocalBranch)) {
993
+ throw new Error(`There is no branch named ${overrideLocalBranch}`);
994
+ }
995
+ localBranchName = overrideLocalBranch;
996
+ } else {
997
+ if (status.detached || !status.current) {
998
+ return { status: "DETACHED" };
999
+ }
1000
+ localBranchName = status.current;
1001
+ }
1002
+
1003
+ const remoteName = (await git.getConfig(`branch.${localBranchName}.remote`))
1004
+ ?.value;
1005
+ if (!remoteName) {
1006
+ return { status: "IN_A_BRANCH", localBranchName };
1007
+ }
1008
+ const remoteFetchUrl = (await git.getConfig(`remote.${remoteName}.url`))
1009
+ ?.value;
1010
+ if (!remoteFetchUrl) {
1011
+ return { status: "IN_A_BRANCH", localBranchName };
1012
+ }
1013
+ const remotePushUrl = (await git.getConfig(`remote.${remoteName}.pushurl`))
1014
+ ?.value;
1015
+ let upstreamBranchName = (
1016
+ await git.revparse(["--abbrev-ref", "--symbolic-full-name", "@{u}"])
1017
+ ).split("\n", 1)[0];
1018
+ if (upstreamBranchName.startsWith(`${remoteName}/`)) {
1019
+ // `upstreamBranchName` is in the form `$remoteName/$branchName`, so we need to remove the `$remoteName/` part
1020
+ upstreamBranchName = upstreamBranchName.substring(remoteName.length + 1);
1021
+ }
1022
+ return {
1023
+ status: "IN_A_BRANCH",
1024
+ localBranchName,
1025
+ upstream: {
1026
+ branchName: upstreamBranchName,
1027
+ remoteName,
1028
+ url: remoteFetchUrl,
1029
+ pushUrl: remotePushUrl,
1030
+ },
1031
+ };
1032
+ }
1033
+
1034
+ /**
1035
+ * Returns the current git branch, or undefined if not in a git repo
1036
+ */
1037
+ export async function getCurrentGitBranchIfGit(): Promise<string | null> {
1038
+ const git: SimpleGit = simpleGit();
1039
+ try {
1040
+ // do not return untracked files which can be slow, we only care about the branch info
1041
+ const status = await git.status(["--untracked-files=no"]);
1042
+ const currentBranch = status.current;
1043
+ return currentBranch;
1044
+ } catch {
1045
+ // ignore
1046
+ }
1047
+ return null;
1048
+ }
1049
+
1050
+ /**
1051
+ * Returns the current git branch, or throws if not in a git repo
1052
+ */
1053
+ export async function getCurrentGitBranch(): Promise<string> {
1054
+ const currentBranch = await getCurrentGitBranchIfGit();
1055
+ if (!currentBranch) {
1056
+ throw new Error("No git repository found");
1057
+ }
1058
+ return currentBranch;
1059
+ }
1060
+
1061
+ export async function getHeadCommit(branch: string): Promise<[string, string]> {
1062
+ const git: SimpleGit = simpleGit();
1063
+ let headCommitId;
1064
+ let headCommitMessage;
1065
+ const logResponse = await git.show(["--no-patch", "--format=%H%n%s", branch]);
1066
+ const logLines = logResponse.split("\n");
1067
+ if (logLines.length > 1) {
1068
+ headCommitId = logLines[0];
1069
+ headCommitMessage = logLines[1];
1070
+ }
1071
+ if (!headCommitId || !headCommitMessage) {
1072
+ throw new Error(`Failed to get head commit for branch '${branch}'`);
1073
+ }
1074
+ return [headCommitId, headCommitMessage];
1075
+ }
1076
+
1077
+ export function isCI(): boolean {
1078
+ return process.env.CI === "true";
1079
+ }
1080
+
1081
+ export async function isGitRepoDirty(): Promise<boolean> {
1082
+ if (isCI()) {
1083
+ // Skip dirtiness check in CI environments
1084
+ return false;
1085
+ }
1086
+ const git: SimpleGit = simpleGit();
1087
+ const status = await git.status();
1088
+ return !status.isClean();
1089
+ }
1090
+
1091
+ export function extractApiName(api: ApiWrapper): string {
1092
+ return api.apiPb?.metadata?.name ?? api?.name ?? "Unknown";
1093
+ }
1094
+
1095
+ export async function writeAppApi(
1096
+ api: ApiWrapper,
1097
+ directoryPath: string,
1098
+ existingFilePaths: Set<string>,
1099
+ apiPromises: Array<Promise<void>>,
1100
+ apiRepresentation?: ApiRepresentation,
1101
+ ): Promise<ApiInfo> {
1102
+ const originalApiName = extractApiName(api);
1103
+ const additionalStepFiles: AdditionalStepFiles[] = [];
1104
+ await writeApiFiles(
1105
+ api,
1106
+ slugifyName(originalApiName),
1107
+ `${directoryPath}/apis`,
1108
+ true,
1109
+ apiPromises,
1110
+ additionalStepFiles,
1111
+ apiRepresentation,
1112
+ existingFilePaths,
1113
+ );
1114
+ return {
1115
+ name: originalApiName,
1116
+ sourceFiles: additionalStepFiles.map((file) => file.relativePath).sort(),
1117
+ };
1118
+ }
1119
+
1120
+ async function writeBackendApi(
1121
+ api: ApiWrapper,
1122
+ directoryPath: string,
1123
+ apiPromises: Array<Promise<void>>,
1124
+ apiRepresentation?: ApiRepresentation,
1125
+ existingFilePaths?: Set<string>,
1126
+ ): Promise<ApiInfo> {
1127
+ const originalApiName = extractApiName(api);
1128
+ const additionalStepFiles: AdditionalStepFiles[] = [];
1129
+ await writeApiFiles(
1130
+ api,
1131
+ "api",
1132
+ directoryPath,
1133
+ false,
1134
+ apiPromises,
1135
+ additionalStepFiles,
1136
+ apiRepresentation,
1137
+ existingFilePaths,
1138
+ );
1139
+ return {
1140
+ name: originalApiName,
1141
+ sourceFiles: additionalStepFiles.map((file) => file.relativePath).sort(),
1142
+ };
1143
+ }
1144
+
1145
+ async function validateMultiPageApplication(
1146
+ applicationConfig: SuperblocksApplicationConfig,
1147
+ superblocksRootPath: string,
1148
+ location: string,
1149
+ ): Promise<string | undefined> {
1150
+ // validate app level APIs
1151
+ const apiNames = getApiNamesFromApplicationConfig(applicationConfig);
1152
+ for (const apiName of apiNames) {
1153
+ const apiPath = path.resolve(
1154
+ superblocksRootPath,
1155
+ location,
1156
+ "apis",
1157
+ `${slugifyName(apiName)}.yaml`,
1158
+ );
1159
+ const validateApiError = await validateYamlFile(apiPath);
1160
+ if (validateApiError) {
1161
+ return validateApiError;
1162
+ }
1163
+ }
1164
+ // validate pages
1165
+ for (const page of Object.values(applicationConfig.pages ?? {})) {
1166
+ const pagePath = path.resolve(
1167
+ superblocksRootPath,
1168
+ location,
1169
+ "pages",
1170
+ slugifyName(page.name),
1171
+ "page.yaml",
1172
+ );
1173
+ const validatePageError = await validateYamlFile(pagePath);
1174
+ if (validatePageError) {
1175
+ return validatePageError;
1176
+ }
1177
+ // validate page level APIs
1178
+ const pageApiNames = getApiNamesFromPageConfig(page);
1179
+ for (const apiName of pageApiNames) {
1180
+ const apisPath = path.resolve(
1181
+ superblocksRootPath,
1182
+ location,
1183
+ "pages",
1184
+ slugifyName(page.name),
1185
+ "apis",
1186
+ );
1187
+ // Try the API path not in a nested directory
1188
+ const apiPath = path.resolve(apisPath, `${slugifyName(apiName)}.yaml`);
1189
+ if (fs.pathExistsSync(apiPath)) {
1190
+ const validateApiError = await validateYamlFile(apiPath);
1191
+ if (validateApiError) {
1192
+ return validateApiError;
1193
+ }
1194
+ } else {
1195
+ // Try the API path in a nested directory
1196
+ const nestedApiPath = path.resolve(
1197
+ apisPath,
1198
+ slugifyName(apiName),
1199
+ "api.yaml",
1200
+ );
1201
+ if (fs.pathExistsSync(nestedApiPath)) {
1202
+ const validateApiError = await validateYamlFile(nestedApiPath);
1203
+ if (validateApiError) {
1204
+ return validateApiError;
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ return undefined;
1211
+ }
1212
+
1213
+ export async function validateLocalResource(
1214
+ superblocksRootPath: string,
1215
+ resource: VersionedResourceConfig,
1216
+ ): Promise<string | undefined> {
1217
+ switch (resource.resourceType) {
1218
+ case "APPLICATION": {
1219
+ // make sure application config exists
1220
+ const applicationConfigPath = path.resolve(
1221
+ superblocksRootPath,
1222
+ resource.location,
1223
+ RESOURCE_CONFIG_PATH,
1224
+ );
1225
+ if (!(await fs.pathExists(applicationConfigPath))) {
1226
+ return `File ${relativeToCurrentDir(
1227
+ applicationConfigPath,
1228
+ )} not found. Superblocks CLI commands cannot function without it.`;
1229
+ }
1230
+ let applicationConfig:
1231
+ | SuperblocksApplicationConfig
1232
+ | SuperblocksApplicationV2Config
1233
+ | undefined = undefined;
1234
+ try {
1235
+ applicationConfig = await getSuperblocksApplicationConfigJson(
1236
+ path.join(superblocksRootPath, resource.location),
1237
+ );
1238
+ } catch {
1239
+ // Noop
1240
+ }
1241
+ if (!applicationConfig) {
1242
+ try {
1243
+ applicationConfig = await getSuperblocksApplicationV2ConfigJson(
1244
+ path.join(superblocksRootPath, resource.location),
1245
+ );
1246
+ if (!applicationConfig) {
1247
+ throw new Error();
1248
+ }
1249
+ } catch {
1250
+ return `File ${relativeToCurrentDir(
1251
+ applicationConfigPath,
1252
+ )} is not a valid JSON file. Please be sure it's valid JSON and rerun the command.`;
1253
+ }
1254
+ }
1255
+ if (applicationConfig.configType === "APPLICATION_V2") {
1256
+ // TODO(wylie): Implement validation for pushing V2 applications
1257
+ break;
1258
+ }
1259
+ const applicationYamlPath = path.resolve(
1260
+ superblocksRootPath,
1261
+ resource.location,
1262
+ "application.yaml",
1263
+ );
1264
+ // make sure application.yaml is a well-formed yaml file
1265
+ try {
1266
+ await readYamlFile(applicationYamlPath);
1267
+ } catch {
1268
+ return `File ${relativeToCurrentDir(
1269
+ applicationYamlPath,
1270
+ )} is not a valid YAML file. Please be sure it's valid YAML and rerun the command.`;
1271
+ }
1272
+ const validationError = validateMultiPageApplication(
1273
+ applicationConfig,
1274
+ superblocksRootPath,
1275
+ resource.location,
1276
+ );
1277
+ if (validationError) {
1278
+ return validationError;
1279
+ }
1280
+ break;
1281
+ }
1282
+ case "BACKEND": {
1283
+ // make sure the backend config exists
1284
+ const backendConfigPath = path.resolve(
1285
+ superblocksRootPath,
1286
+ resource.location,
1287
+ RESOURCE_CONFIG_PATH,
1288
+ );
1289
+ if (!(await fs.pathExists(backendConfigPath))) {
1290
+ return `File ${relativeToCurrentDir(
1291
+ backendConfigPath,
1292
+ )} not found. Superblocks CLI commands cannot function without it.`;
1293
+ }
1294
+ // make sure it's a well-formed backend config
1295
+ try {
1296
+ await fs.readJSON(backendConfigPath);
1297
+ } catch {
1298
+ return `File ${relativeToCurrentDir(
1299
+ backendConfigPath,
1300
+ )} is not a valid JSON file. Please be sure it's valid JSON and rerun the command.`;
1301
+ }
1302
+ // make sure that api.yaml exists
1303
+ const apiYamlPath = path.resolve(
1304
+ superblocksRootPath,
1305
+ resource.location,
1306
+ "api.yaml",
1307
+ );
1308
+ const validateYamlFileError = await validateYamlFile(apiYamlPath);
1309
+ if (validateYamlFileError) {
1310
+ return validateYamlFileError;
1311
+ }
1312
+ break;
1313
+ }
1314
+ }
1315
+ return undefined;
1316
+ }
1317
+
1318
+ export async function deleteResourcesAndUpdateRootConfig(
1319
+ removedResourceIds: string[],
1320
+ existingSuperblocksRootConfig: SuperblocksMonorepoConfig,
1321
+ superblocksRootPath: string,
1322
+ superblocksRootConfigPath: string,
1323
+ ): Promise<void> {
1324
+ for (const resourceId of removedResourceIds) {
1325
+ const resource = existingSuperblocksRootConfig?.resources[resourceId];
1326
+ await removeResourceFromDisk(superblocksRootPath, resource.location);
1327
+ }
1328
+ for (const removedResourceId of removedResourceIds) {
1329
+ delete existingSuperblocksRootConfig.resources[removedResourceId];
1330
+ }
1331
+ // update superblocks.json file with removed resources
1332
+ await fs.writeFile(
1333
+ superblocksRootConfigPath,
1334
+ JSON.stringify(sortByKey(existingSuperblocksRootConfig), null, 2),
1335
+ );
1336
+ }
1337
+
1338
+ async function validateYamlFile(yamlPath: string): Promise<string | undefined> {
1339
+ // make sure a yaml file is well-formed
1340
+ try {
1341
+ await readYamlFile(yamlPath);
1342
+ } catch {
1343
+ return `File ${relativeToCurrentDir(
1344
+ yamlPath,
1345
+ )} is not a valid YAML file. Please be sure it's valid YAML and rerun the command.`;
1346
+ }
1347
+ }
1348
+
1349
+ function relativeToCurrentDir(applicationConfigPath: string) {
1350
+ return `./${path.relative(process.cwd(), applicationConfigPath)}`;
1351
+ }