@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.
- package/LICENSE.txt +87 -0
- package/README.md +6 -6
- package/assets/custom-components/setup/package.json +1 -1
- package/assets/custom-components/setup/tsconfig.json +0 -1
- package/assets/injectedReactShim17.jsx +15 -0
- package/assets/injectedReactShim18.jsx +16 -0
- package/assets/injectedReactShimShared.jsx +140 -0
- package/bin/dev +5 -7
- package/bin/run +1 -3
- package/dist/appendHotReloadEventPlugin.d.mts +2 -0
- package/dist/appendHotReloadEventPlugin.mjs +43 -0
- package/dist/commands/commits.d.mts +18 -0
- package/dist/commands/{commits.js → commits.mjs} +59 -67
- package/dist/commands/components/{create.d.ts → create.d.mts} +2 -2
- package/dist/commands/components/{create.js → create.mjs} +84 -93
- package/dist/commands/components/{register.d.ts → register.d.mts} +1 -1
- package/dist/commands/components/register.mjs +12 -0
- package/dist/commands/components/{upload.d.ts → upload.d.mts} +2 -2
- package/dist/commands/components/{upload.js → upload.mjs} +39 -43
- package/dist/commands/components/{watch.d.ts → watch.d.mts} +1 -1
- package/dist/commands/components/{watch.js → watch.mjs} +29 -36
- package/dist/commands/config/{set.d.ts → set.d.mts} +2 -2
- package/dist/commands/config/{set.js → set.mjs} +28 -32
- package/dist/commands/{init.d.ts → init.d.mts} +4 -4
- package/dist/commands/{init.js → init.mjs} +63 -65
- package/dist/commands/{login.d.ts → login.d.mts} +1 -1
- package/dist/commands/login.mjs +55 -0
- package/dist/commands/{migrate.d.ts → migrate.d.mts} +1 -1
- package/dist/commands/{migrate.js → migrate.mjs} +38 -46
- package/dist/commands/pull.d.mts +17 -0
- package/dist/commands/{pull.js → pull.mjs} +74 -80
- package/dist/commands/push.d.mts +15 -0
- package/dist/commands/{push.js → push.mjs} +81 -90
- package/dist/commands/{rm.d.ts → rm.d.mts} +2 -2
- package/dist/commands/{rm.js → rm.mjs} +34 -40
- package/dist/common/{authenticated-command.js → authenticated-command.mjs} +65 -75
- package/dist/common/defaults/{create-component-defaults.js → create-component-defaults.mjs} +2 -7
- package/dist/common/{version-control.d.ts → version-control.d.mts} +13 -6
- package/dist/common/version-control.mjs +1064 -0
- package/dist/index.js +1 -5
- package/dist/productionCssPlugin.d.mts +2 -0
- package/dist/productionCssPlugin.mjs +50 -0
- package/dist/reactShimPlugin.d.mts +2 -0
- package/dist/reactShimPlugin.mjs +127 -0
- package/dist/util/migrationWarningsForApplications.mjs +47 -0
- package/dist/util/{migrationsForDotfiles.js → migrationsForDotfiles.mjs} +10 -17
- package/oclif.manifest.json +274 -161
- package/package.json +45 -45
- package/dist/commands/commits.d.ts +0 -18
- package/dist/commands/components/register.js +0 -15
- package/dist/commands/login.js +0 -61
- package/dist/commands/pull.d.ts +0 -17
- package/dist/commands/push.d.ts +0 -15
- package/dist/common/version-control.js +0 -716
- package/dist/util/migrationWarningsForApplications.js +0 -52
- /package/dist/common/{authenticated-command.d.ts → authenticated-command.d.mts} +0 -0
- /package/dist/common/defaults/{create-component-defaults.d.ts → create-component-defaults.d.mts} +0 -0
- /package/dist/util/{migrationWarningsForApplications.d.ts → migrationWarningsForApplications.d.mts} +0 -0
- /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
|
+
}
|